File size: 43,355 Bytes
16756bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3da5b2
16756bd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>記憶的編織 - 水墨投影系統</title>
    <style>
        /* CSS Reset and Base Styles */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Inter', sans-serif; /* 使用 Inter 字體 */
            /* 純黑白介面: 從深灰到淺灰的漸變背景 */
            background: linear-gradient(135deg, #333333 0%, #1a1a1a 100%);
            color: #e0e0e0; /* 淺灰色文字 */
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden; /* 防止滾動條 */
        }

        /* Main Container for the Application */
        .container {
            display: flex;
            /* 調整容器尺寸,讓整個介面框框更小 */
            width: 85%; 
            max-width: 1200px; /* 限制最大寬度 */
            height: 85vh; /* 限制最大高度 */
            border-radius: 15px;
            overflow: hidden;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); /* 更深的陰影 */
            background: rgba(0, 0, 0, 0.4); /* 半透明黑色 */
        }

        /* Left and Right Panels Styling */
        .left-panel, .right-panel {
            flex: 1; /* 初始兩側面板等寬 */
            padding: 20px;
            display: flex;
            flex-direction: column;
            gap: 15px;
            background: rgba(0, 0, 0, 0.3); /* 更深的半透明背景 */
            backdrop-filter: blur(5px);
            margin: 8px; /* 容器內部內容的邊距 */
            border-radius: 12px;
        }

        /* Title Styling */
        .title {
            text-align: center;
            font-size: 1.8rem;
            font-weight: 800;
            color: #ffffff; /* 純白色標題 */
            margin-bottom: 15px;
        }

        /* Status Panel Styling */
        .status-panel, .info-panel {
            background: rgba(255, 255, 255, 0.05); /* 淺色透明灰,更暗 */
            border-radius: 8px;
            padding: 12px;
            margin-bottom: 10px;
            color: #cccccc; /* 淺灰色文字 */
        }

        .status-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 0.9rem;
        }

        .status-item:last-child {
            margin-bottom: 0; /* 最後一個項目沒有邊距 */
        }

        /* Controls Button Styling */
        .controls {
            display: flex;
            gap: 8px;
            margin-bottom: 15px;
        }

        .btn {
            flex: 1;
            padding: 10px 15px; /* 稍大的內邊距以改善觸控目標 */
            border: none;
            border-radius: 8px; /* 更圓的角 */
            color: #ffffff; /* 按鈕文字為白色 */
            font-weight: bold;
            font-size: 0.9rem; /* 稍大的字體 */
            cursor: pointer;
            transition: transform 0.2s ease, box-shadow 0.2s ease; /* 添加陰影過渡 */
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* 更深的陰影 */
        }

        .btn:hover {
            transform: translateY(-2px); /* 更明顯的抬升效果 */
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4); /* 懸停時更深的陰影 */
        }

        /* 純黑白按鈕顏色 */
        .btn-primary { background: #444444; } /* 深灰 */
        .btn-secondary { background: #666666; } /* 中灰 */
        .btn-tertiary { background: #888888; } /* 淺灰 */
        .btn-special { background: #aaaaaa; } /* 更淺的灰 */

        /* Canvas Container */
        .canvas-container {
            flex: 1; /* 讓容器佔滿可用空間 */
            background: #000; /* 純黑色背景 */
            border-radius: 8px;
            border: 2px solid #555555; /* 深灰色邊框 */
            overflow: hidden;
            position: relative;
            display: flex; /* 使用 flex 來居中 canvas 內容 */
            align-items: center;
            justify-content: center;
            min-height: 300px; /* 確保 canvas 區域有最小高度 */
        }

        canvas {
            width: 100%;
            height: 100%;
            display: block;
            cursor: crosshair;
            background-color: #f0f0f0; /* 淺灰色畫布背景,確保黑色筆觸可見 */
        }

        .overlay {
            position: absolute;
            top: 10px; /* 調整位置 */
            left: 10px;
            background: rgba(0, 0, 0, 0.8); /* 更深的半透明黑色 */
            padding: 6px 10px; /* 稍大的內邊距 */
            border-radius: 5px; /* 更圓的角 */
            font-size: 0.8rem;
            color: #fff;
        }

        /* AI Area Styling */
        .ai-area {
            flex: 4; /* 在右側面板佔據更多空間 */
            background: rgba(255, 255, 255, 0.05); /* 淺色透明灰,更暗 */
            border-radius: 8px;
            padding: 15px; /* 增加內邊距 */
            border: 2px solid #888888; /* 中灰色邊框 */
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 350px; /* 確保有足夠的高度 */
            position: relative; /* 用於載入覆蓋 */
        }

        .ai-area h3 {
            color: #ffffff; /* 純白色標題 */
            margin-bottom: 10px;
            font-size: 1.2rem; /* 較大的字體 */
        }

        /* Hidden by default, will be replaced by canvas */
        .ai-area img { 
            display: none; 
        }

        #aiStatus {
            font-size: 1.1rem; /* 較大的字體 */
            color: #ffffff; /* 純白色文字 */
            text-align: center;
            padding: 20px;
            position: absolute; /* 在 ai-area 內居中 */
            background: rgba(0, 0, 0, 0.8); /* 更深的半透明黑色 */
            border-radius: 8px;
            width: calc(100% - 30px); /* 調整寬度以考慮內邊距 */
            height: calc(100% - 30px); /* 調整高度 */
            display: flex;
            align-items: center;
            justify-content: center;
            top: 15px;
            left: 15px;
        }

        #aiCanvas {
            display: none; /* 預設隱藏,生成後顯示 */
            border-radius: 8px;
            box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
            width: 100%;
            height: 100%;
        }

        /* Info Panel within Right Panel */
        .info-panel h4 {
            color: #ffffff; /* 純白色文字 */
            margin-bottom: 8px;
            font-size: 1.05rem;
        }

        .prompt-display {
            background: rgba(0, 0, 0, 0.6); /* 更深的半透明黑色 */
            padding: 10px;
            border-radius: 6px;
            font-size: 0.85rem;
            word-break: break-all;
            min-height: 60px; /* 確保較長提示詞的可見性 */
            display: flex;
            align-items: center;
            color: #eeeeee; /* 淺灰色文字 */
        }

        /* Message Box */
        .message {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 10px 20px;
            border-radius: 6px;
            color: white;
            font-weight: bold;
            z-index: 1000;
            transform: translateX(120%);
            transition: transform 0.3s ease, opacity 0.3s ease;
            opacity: 0;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
        }

        .message.show {
            transform: translateX(0);
            opacity: 1;
        }

        /* 純黑白訊息框顏色 */
        .message.success { background: #444444; } /* 深灰 */
        .message.error { background: #888888; } /* 中灰 */
        .message.info { background: #666666; } /* 淺灰 */

        /* Loading Spinner */
        .spinner {
            border: 4px solid rgba(255, 255, 255, 0.3); /* 淺色半透明邊框 */
            border-top: 4px solid #ffffff; /* 純白色旋轉部分 */
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin-bottom: 10px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        /* Responsive adjustments */
        @media (max-width: 900px) {
            .container {
                flex-direction: column;
                height: auto;
                min-height: 95vh;
            }

            .left-panel, .right-panel {
                width: calc(100% - 16px); /* 寬度佔滿減去邊距 */
                margin-bottom: 16px;
            }

            .controls {
                flex-wrap: wrap; /* 讓按鈕換行 */
            }

            .btn {
                flex: none;
                width: 48%; /* 每行兩個按鈕 */
            }

            .ai-area {
                min-height: 250px; /* 調整小螢幕的最小高度 */
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="left-panel">
            <h1 class="title">記憶的編織</h1>
            
            <div class="status-panel">
                <div class="status-item">
                    <span>系統狀態:</span>
                    <span id="systemStatus">待機</span>
                </div>
                <div class="status-item">
                    <span>FPS:</span>
                    <span id="fpsDisplay">0</span>
                </div>
                <div class="status-item">
                    <span>漣漪數:</span>
                    <span id="rippleCount">0</span>
                </div>
            </div>

            <div class="controls">
                <button class="btn btn-primary" onclick="startSystem()">啟動</button>
                <button class="btn btn-secondary" onclick="resetSystem()">重置</button>
                <button class="btn btn-tertiary" onclick="togglePause()">暫停</button>
                <button class="btn btn-special" onclick="generateCanvasArt()">生成</button>
            </div>

            <div class="canvas-container">
                <canvas id="mainCanvas"></canvas>
                <div class="overlay">水墨投影 - 移動滑鼠體驗</div>
            </div>
        </div>

        <div class="right-panel">
            <h1 class="title">程式化生成</h1>
            
            <div class="info-panel">
                <h4>狀態信息</h4>
                <div class="status-item">
                    <span>滑鼠速度:</span>
                    <span id="mouseSpeed">0</span>
                </div>
                <div class="status-item">
                    <span>生成狀態:</span>
                    <span id="aiGenStatus">待機</span>
                </div>
            </div>

            <div class="info-panel">
                <h4>生成描述</h4>
                <div class="prompt-display" id="promptText">
                    點擊「生成」按鈕以產生程式化圖案...
                </div>
            </div>

            <div class="ai-area">
                <h3>生成結果</h3>
                <div id="aiStatus">
                    <div class="spinner" style="display:none;"></div>
                    點擊生成按鈕開始創作
                </div>
                <canvas id="aiCanvas" style="display: none;"></canvas>
                <div class="action-buttons" style="display: flex; gap: 10px; margin-top: 15px;">
                    <button class="btn btn-secondary" onclick="copyAICanvasImage()">複製圖片</button>
                    <button class="btn btn-primary" onclick="downloadAICanvasImage()">下載圖片</button>
                </div>
            </div>
        </div>
    </div>

    <div class="message" id="messageBox"></div>

    <script>
        console.log('🎨 啟動水墨系統...');

        // 全域變數
        let canvas, ctx; // 主水墨畫布
        let aiCanvas, aiCtx; // AI 生成畫布
        let isRunning = false;
        let isPaused = false;
        let mouseX = 0, mouseY = 0, mouseSpeed = 0;
        let time = 0;
        let nodes = [];
        let ripples = [];
        let frameCount = 0;
        let lastFpsTime = Date.now();

        // 追蹤當前 AI 生成圖案模式
        let currentGenerationMode = 0; // 0 for connected dots (weaving), 1 for flow field

        /**
         * 初始化主畫布和 AI 畫布,並設置事件監聽器。
         * 此函數在 DOM 完全載入後調用。
         */
        function init() {
            console.log('初始化系統...');
            
            canvas = document.getElementById('mainCanvas');
            aiCanvas = document.getElementById('aiCanvas');

            if (!canvas || !aiCanvas) {
                console.error('找不到畫布元素');
                return;
            }
            
            ctx = canvas.getContext('2d');
            aiCtx = aiCanvas.getContext('2d');
            
            setupCanvas(); // 設置主畫布尺寸
            setupAICanvas(); // 設置 AI 畫布尺寸
            setupEvents(); // 設置事件監聽
            createNodes(); // 初始化水墨節點
            
            showMessage('系統初始化完成!', 'success');
            console.log('✅ 初始化完成');
        }

        /**
         * 設置主畫布尺寸以匹配其父容器,並初始化滑鼠座標。
         */
        function setupCanvas() {
            const container = canvas.parentElement;
            const rect = container.getBoundingClientRect();
            
            // 設置畫布尺寸以匹配容器,確保最小尺寸
            canvas.width = rect.width; // 讓畫布完全填滿容器
            canvas.height = rect.height; // 讓畫布完全填滿容器
            
            // 初始將滑鼠置於中心
            mouseX = canvas.width / 2;
            mouseY = canvas.height / 2;
            
            console.log(`主畫布: ${canvas.width}x${canvas.height}`);
            
            // 在畫布上繪製初始訊息
            ctx.fillStyle = '#f0f0f0'; /* 淺色背景以呈現水墨效果,與 CSS 中的畫布背景同步 */
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.fillStyle = '#333333'; /* 深灰色文字 */
            ctx.font = '16px Inter';
            ctx.textAlign = 'center';
            ctx.fillText('系統準備就緒', canvas.width/2, canvas.height/2);
        }

        /**
         * 設置 AI 畫布尺寸以匹配其父容器。
         */
        function setupAICanvas() {
            const aiArea = document.querySelector('.ai-area');
            aiCanvas.width = aiArea.clientWidth;
            aiCanvas.height = aiArea.clientHeight;
            console.log(`AI 畫布: ${aiCanvas.width}x${aiCanvas.height}`);
            // 如果 AI 畫布在 resize 後需要立即重繪其內容,可以在這裡調用 drawAIAbstract()
        }

        /**
         * 設置滑鼠移動和點擊事件監聽器。
         */
        function setupEvents() {
            // 滑鼠移動處理器
            canvas.addEventListener('mousemove', (e) => {
                const rect = canvas.getBoundingClientRect();
                // 計算比例因子以獲取高 DPI 螢幕上的精確滑鼠座標
                const scaleX = canvas.width / rect.width;
                const scaleY = canvas.height / rect.height;
                
                const newX = (e.clientX - rect.left) * scaleX;
                const newY = (e.clientY - rect.top) * scaleY;
                
                // 計算滑鼠速度以實現動態漣漪生成
                mouseSpeed = Math.sqrt((newX - mouseX)**2 + (newY - mouseY)**2);
                mouseX = newX;
                mouseY = newY;
                
                // 更新滑鼠速度的 UI 顯示
                document.getElementById('mouseSpeed').textContent = Math.round(mouseSpeed);
                
                // 根據滑鼠速度創建漣漪
                // 僅當速度超過閾值且漣漪數量未過多時才創建新漣漪
                if (mouseSpeed > 3 && ripples.length < 20) { 
                    createRipple(mouseX, mouseY, mouseSpeed);
                }
            });

            // 點擊處理器以創建強烈漣漪並觸發圖案生成
            canvas.addEventListener('click', () => {
                createRipple(mouseX, mouseY, 20); // 點擊時產生更強的漣漪
                generateCanvasArt(); // 觸發畫布圖案生成
            });

            // 處理視窗大小調整以保持畫布響應性
            window.addEventListener('resize', () => {
                setupCanvas(); // 調整主畫布大小
                setupAICanvas(); // 調整 AI 畫布大小
            });

            console.log('事件設置完成');
        }

        /**
         * 填充 `nodes` 陣列,它構成水墨效果的基礎。
         * 節點以一種更稀疏且均勻的方式分佈。
         */
        function createNodes() {
            nodes = [];
            
            const nodeCount = 80; // 增加節點數量以獲得更明顯的編織密度

            // 將節點初始分佈在一個較大的中心區域,模擬參考圖案的分散感
            const centerX = canvas.width / 2;
            const centerY = canvas.height / 2;
            const initialSpreadRadius = Math.min(canvas.width, canvas.height) * 0.4; // 調整初始散佈半徑

            for (let i = 0; i < nodeCount; i++) {
                const angle = (i / nodeCount) * Math.PI * 2; // 均勻分佈初始角度
                const radius = initialSpreadRadius + (Math.random() - 0.5) * 100; // 在基礎半徑上隨機偏移,增加散佈
                
                // 使用角度和半徑計算初始位置,增加點的規律性
                let x = centerX + Math.cos(angle) * radius; 
                let y = centerY + Math.sin(angle) * radius;
                
                // 確保節點仍然在畫布範圍內
                x = Math.max(5, Math.min(canvas.width - 5, x));
                y = Math.max(5, Math.min(canvas.height - 5, y));

                nodes.push({
                    x: x,
                    y: y,
                    originalX: x, // 存儲原始位置以實現彈回效果
                    originalY: y,
                    vx: 0, // X 速度
                    vy: 0, // Y 速度
                    phase: Math.random() * Math.PI * 2 // 隨機相位以實現獨立運動
                });
            }
            
            console.log(`創建了 ${nodes.length} 個節點`);
        }

        /**
         * 向 `ripples` 陣列添加新的漣漪效果。
         * @param {number} x - 漣漪中心的 X 座標。
         * @param {number} y - 漣漪中心的 Y 座標。
         * @param {number} intensity - 漣漪的初始強度。
         */
        function createRipple(x, y, intensity) {
            ripples.push({
                x: x,
                y: y,
                radius: 0, // 漣漪當前半徑
                maxRadius: 60 + intensity * 2, // 顯著增加最大半徑,使漣漪更顯著
                life: 1, // 生命週期因子,從 1(完整)到 0(消失)
                intensity: Math.min(intensity * 0.1, 1.0) // 視覺效果的限定強度
            });
            
            document.getElementById('rippleCount').textContent = ripples.length;
        }

        /**
         * 啟動水墨模擬系統。
         */
        function startSystem() {
            console.log('啟動系統');
            isRunning = true;
            isPaused = false;
            
            document.getElementById('systemStatus').textContent = '運行中';
            showMessage('水墨系統已啟動!', 'success');
            
            animate(); // 啟動動畫循環
        }

        /**
         * 將水墨模擬重置為其初始狀態。
         */
        function resetSystem() {
            console.log('重置系統');
            time = 0;
            ripples = [];
            createNodes(); // 重新創建節點
            
            document.getElementById('rippleCount').textContent = '0';
            showMessage('系統已重置', 'info');
            // 重置 AI 畫布區域
            document.getElementById('aiCanvas').style.display = 'none';
            document.getElementById('aiStatus').style.display = 'flex'; // 顯示預設狀態
            document.getElementById('aiStatus').innerHTML = '點擊生成按鈕開始創作';
            document.getElementById('aiGenStatus').textContent = '待機';
            document.getElementById('promptText').textContent = '點擊「生成」按鈕以產生程式化圖案...';
        }

        /**
         * 切換模擬的暫停狀態。
         */
        function togglePause() {
            isPaused = !isPaused;
            const status = isPaused ? '暫停' : '繼續';
            document.getElementById('systemStatus').textContent = status;
            showMessage(`系統已${status}`, 'info');
        }

        /**
         * 更新節點和漣漪的物理屬性。
         * 包括基本波浪運動、滑鼠互動和漣漪力。
         */
        function updatePhysics() {
            // 更新節點位置和速度
            nodes.forEach(node => {
                // 節點的基本流體運動
                const waveX = Math.sin(time * 1.5 + node.phase) * 2; 
                const waveY = Math.cos(time * 1.2 + node.phase) * 2;
                
                const targetX = node.originalX + waveX;
                const targetY = node.originalY + waveY;
                
                // 類似彈簧的力將節點拉回目標位置,增加彈性
                node.vx += (targetX - node.x) * 0.03; 
                node.vy += (targetY - node.y) * 0.03;
                
                // 滑鼠對節點的影響 - 更強的效果
                const distToMouse = Math.sqrt((node.x - mouseX)**2 + (node.y - mouseY)**2);
                if (distToMouse < 200) { // 更大的影響半徑
                    const influence = (1 - distToMouse / 200) * 1.5; // 更強的影響力
                    const mouseAngle = Math.atan2(node.y - mouseY, node.x - mouseX);
                    
                    node.vx += Math.cos(mouseAngle) * influence * mouseSpeed * 0.03; // 影響力與滑鼠速度相關
                    node.vy += Math.sin(mouseAngle) * influence * mouseSpeed * 0.03;
                }
                
                // 漣漪對節點的影響 (水紋影響織布的核心)
                ripples.forEach(ripple => {
                    const distToRipple = Math.sqrt((node.x - ripple.x)**2 + (node.y - ripple.y)**2);
                    // 檢查節點是否在漣漪擴展邊緣附近
                    if (Math.abs(distToRipple - ripple.radius) < 40 && distToRipple > 0) { // 更寬的活躍帶,影響更廣
                        const rippleForce = ripple.intensity * ripple.life * 2.0; // 更強的漣漪力
                        const rippleAngle = Math.atan2(node.y - ripple.y, node.x - ripple.x);
                        node.vx += Math.cos(rippleAngle) * rippleForce;
                        node.vy += Math.sin(rippleAngle) * rippleForce;
                    }
                });
                
                // 施加摩擦並更新位置
                node.vx *= 0.7; // 更多阻尼
                node.vy *= 0.7;
                node.x += node.vx;
                node.y += node.vy;

                // 邊界碰撞(簡單的反彈)
                if (node.x < 0 || node.x > canvas.width) node.vx *= -0.8;
                if (node.y < 0 || node.y > canvas.height) node.vy *= -0.8;
            });
            
            // 更新漣漪:擴展和衰減
            ripples = ripples.filter(ripple => {
                ripple.radius += 3.5; // 更快的擴展
                ripple.life -= 0.01; // 較慢的衰減,讓漣漪存在時間長一點
                return ripple.life > 0 && ripple.radius < ripple.maxRadius;
            });
            
            document.getElementById('rippleCount').textContent = ripples.length;
        }

        /**
         * 在畫布上渲染水墨效果。
         */
        function render() {
            if (!ctx) return;
            
            // 用淺色背景清除畫布以呈現水墨效果
            ctx.fillStyle = '#f0f0f0'; /* 畫布背景色與 CSS 中的 #f0f0f0 同步 */
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 應用混合模式以實現墨水般的透明度
            ctx.globalCompositeOperation = 'multiply'; 
            
            // 渲染漣漪
            ripples.forEach(ripple => {
                const opacity = ripple.life * ripple.intensity;
                
                ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 1.0})`; /* 漣漪完全不透明,更明顯 */
                ctx.lineWidth = 3.5; /* 顯著增加漣漪線條粗細 */
                ctx.beginPath();
                ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
                ctx.stroke();
                
                // 內部漣漪以實現分層效果
                if (ripple.radius > 10) {
                    ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.6})`; /* 內部漣漪維持較淡但仍可見 */
                    ctx.lineWidth = 2.0;
                    ctx.beginPath();
                    ctx.arc(ripple.x, ripple.y, ripple.radius * 0.7, 0, Math.PI * 2); 
                    ctx.stroke();
                }
            });
            
            // 渲染節點連接(線條)- 織布效果的核心
            for (let i = 0; i < nodes.length; i++) {
                for (let j = i + 1; j < nodes.length; j++) {
                    const nodeA = nodes[i];
                    const nodeB = nodes[j];
                    const distance = Math.sqrt((nodeA.x - nodeB.x)**2 + (nodeA.y - nodeB.y)**2);
                    
                    const maxConnectDistance = 250; // 增加最大連接距離,讓更廣泛的連接可見
                    if (distance < maxConnectDistance) { 
                        // 線性衰減不透明度,並提高基礎值,讓織布感更明顯且不過黑
                        // 將不透明度基礎值顯著提高,以確保線條可見
                        const opacity = (1 - distance / maxConnectDistance) * 0.9; /* 調整為線性衰減,最高不透明度 0.9 */
                        if (opacity > 0.1) { // 設定最小可見度閾值,確保微弱線條也顯示
                            ctx.strokeStyle = `rgba(0, 0, 0, ${opacity})`;
                            ctx.lineWidth = 3.0; /* 顯著增加線條粗細,強化織布感 */
                            ctx.beginPath();
                            ctx.moveTo(nodeA.x, nodeA.y);
                            ctx.lineTo(nodeB.x, nodeB.y);
                            ctx.stroke();
                        }
                    }
                }
            }
            
            /* // 移除節點圓點的繪製
            nodes.forEach(node => {
                const opacity = 0.9 + Math.sin(time * 2 + node.phase) * 0.1; 
                ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
                ctx.beginPath();
                ctx.arc(node.x, node.y, 4.5, 0, Math.PI * 2); 
                ctx.fill();
            });
            */
            
            /* // 移除滑鼠指示器圓點
            ctx.fillStyle = 'rgba(0, 0, 0, 1.0)'; 
            ctx.beginPath();
            ctx.arc(mouseX, mouseY, 7, 0, Math.PI * 2); 
            ctx.fill();
            */
            
            /* // 移除滑鼠脈衝效果
            const pulseSize = 9 + Math.sin(time * 6) * 3; 
            ctx.strokeStyle = 'rgba(0, 0, 0, 0.9)'; 
            ctx.lineWidth = 3.0;
            ctx.beginPath();
            ctx.arc(mouseX, mouseY, pulseSize, 0, Math.PI * 2);
            ctx.stroke();
            */
            
            // 重置混合模式和濾鏡
            ctx.filter = 'none';
            ctx.globalCompositeOperation = 'source-over';
        }

        /**
         * 主要動畫循環。
         */
        function animate() {
            if (!isRunning) return;
            
            if (!isPaused) {
                time += 0.02; // 時間前進
                updatePhysics(); // 更新節點和漣漪物理
                render(); // 渲染畫布
                
                // 計算並顯示 FPS
                frameCount++;
                const now = Date.now();
                if (now - lastFpsTime >= 1000) {
                    document.getElementById('fpsDisplay').textContent = frameCount;
                    frameCount = 0;
                    lastFpsTime = now;
                }
            }
            
            requestAnimationFrame(animate); // 請求下一幀
        }

        /**
         * 將 AI 生成的畫布內容複製到剪貼簿。
         */
        async function copyAICanvasImage() {
            try {
                // 將 canvas 內容轉換為 Blob
                aiCanvas.toBlob(async (blob) => {
                    // 檢查瀏覽器是否支援 ClipboardItem 和 navigator.clipboard.write
                    if (navigator.clipboard && navigator.clipboard.write) {
                        try {
                            const item = new ClipboardItem({ "image/png": blob });
                            await navigator.clipboard.write([item]);
                            showMessage('圖片已複製到剪貼簿!', 'success');
                        } catch (err) {
                            console.error('複製到剪貼簿失敗 (Clipboard API):', err);
                            // Fallback for older browsers or restricted environments
                            fallbackCopyImage();
                        }
                    } else {
                        // Fallback for browsers that don't support ClipboardItem
                        fallbackCopyImage();
                    }
                }, 'image/png');
            } catch (error) {
                console.error('複製圖片失敗:', error);
                showMessage('複製圖片失敗!', 'error');
            }
        }

        /**
         * 舊版瀏覽器或受限環境的複製圖片備用方案。
         * 注意:此方法可能因瀏覽器安全限制而失敗。
         */
        function fallbackCopyImage() {
            try {
                // 創建一個臨時的 img 元素
                const img = document.createElement('img');
                img.src = aiCanvas.toDataURL('image/png');
                document.body.appendChild(img); // 必須添加到 DOM 才能選中

                const range = document.createRange();
                range.selectNode(img);
                window.getSelection().removeAllRanges();
                window.getSelection().addRange(range);
                document.execCommand('copy');
                window.getSelection().removeAllRanges();
                document.body.removeChild(img); // 移除臨時元素

                showMessage('圖片已複製到剪貼簿 (可能需要手動貼上)!', 'info');
            } catch (err) {
                console.error('備用複製方法失敗:', err);
                showMessage('複製圖片失敗,請嘗試下載!', 'error');
            }
        }

        /**
         * 下載 AI 生成的畫布圖片。
         */
        function downloadAICanvasImage() {
            try {
                const imageUrl = aiCanvas.toDataURL('image/png');
                const a = document.createElement('a');
                a.href = imageUrl;
                a.download = 'generated_art.png';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                showMessage('圖片已下載!', 'success');
            } catch (error) {
                console.error('下載圖片失敗:', error);
                showMessage('下載圖片失敗!', 'error');
            }
        }

        /**
         * 繪製抽象的程式化圖案到 AI 畫布上。
         * 這個圖案是基於畫布的現有狀態(節點和漣漪)生成的。
         */
        function drawAIAbstract() {
            if (!aiCtx) return;

            aiCtx.clearRect(0, 0, aiCanvas.width, aiCanvas.height);
            aiCtx.fillStyle = '#f0f0f0'; /* 淺色背景,與主畫布同步 */
            aiCtx.fillRect(0, 0, aiCanvas.width, aiCanvas.height);

            // 將主畫布的節點和漣漪資訊映射到 AI 畫布上
            const scaleX = aiCanvas.width / canvas.width;
            const scaleY = aiCanvas.height / canvas.height;

            if (currentGenerationMode === 0) {
                // 模式 0: 連接點的「織布」風格 - 複製主畫布的編織感
                document.getElementById('promptText').textContent = '基於水墨狀態的程式化網狀結構。';

                // 繪製基於節點的連接線
                aiCtx.strokeStyle = 'rgba(0, 0, 0, 0.9)'; /* 更深的不透明度 */
                aiCtx.lineWidth = 3.0; // 匹配主畫布的線寬
                nodes.forEach(nodeA => {
                    nodes.forEach(nodeB => {
                        if (nodeA !== nodeB) {
                            const dist = Math.sqrt(
                                (nodeA.x - nodeB.x) ** 2 + (nodeA.y - nodeB.y) ** 2
                            );
                            const aiMaxConnectDistance = 250; // 匹配主畫布的連接距離
                            if (dist < aiMaxConnectDistance) { 
                                const opacity = (1 - dist / aiMaxConnectDistance) * 0.9; // 匹配主畫布的透明度衰減
                                if (opacity > 0.1) {
                                    aiCtx.strokeStyle = `rgba(0, 0, 0, ${opacity})`;
                                    aiCtx.beginPath();
                                    aiCtx.moveTo(nodeA.x * scaleX, nodeA.y * scaleY);
                                    aiCtx.lineTo(nodeB.x * scaleX, nodeB.y * scaleY);
                                    aiCtx.stroke();
                                }
                            }
                        }
                    });
                });

                // 漣漪 (在模式0中以圓圈呈現)
                ripples.forEach(ripple => {
                    const opacity = ripple.life * 1.0; /* 漣漪完全不透明 */
                    aiCtx.strokeStyle = `rgba(50, 50, 50, ${opacity})`; 
                    aiCtx.lineWidth = 3.5; // 匹配主畫布的線寬
                    aiCtx.beginPath();
                    aiCtx.arc(ripple.x * scaleX, ripple.y * scaleY, ripple.radius * scaleX * 0.6, 0, Math.PI * 2);
                    aiCtx.stroke();
                });

                /* // 移除數據點 (節點) 的繪製
                aiCtx.fillStyle = 'rgba(0, 0, 0, 0.8)'; 
                nodes.forEach(node => {
                    aiCtx.beginPath();
                    aiCtx.arc(node.x * scaleX, node.y * scaleY, 4.0, 0, Math.PI * 2); 
                    aiCtx.fill();
                });
                */

            } else {
                // 模式 1: 粒子流動場 (全新畫面) - 提升複雜度和可見度
                document.getElementById('promptText').textContent = '隨機粒子流動的抽象演算法圖案。';

                const particles = [];
                const numParticles = 1500; // 大幅增加粒子數量以獲得更豐富的流動
                const particleSpeed = 5 + mouseSpeed * 0.12; // 粒子速度受滑鼠速度影響更明顯
                const particleLife = 400; // 粒子生命週期增加,軌跡更長

                // 根據漣漪位置初始化一些粒子,或隨機初始化
                if (ripples.length > 0) {
                    ripples.forEach(ripple => {
                        for(let i=0; i<numParticles/ripples.length; i++) {
                            particles.push({
                                x: ripple.x * scaleX + (Math.random() - 0.5) * 200, // 從漣漪周圍開始
                                y: ripple.y * scaleY + (Math.random() - 0.5) * 200,
                                vx: (Math.random() - 0.5) * particleSpeed,
                                vy: (Math.random() - 0.5) * particleSpeed,
                                life: particleLife,
                                originalColor: `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})` // 基礎透明度大幅提高
                            });
                        }
                    });
                } else {
                    for (let i = 0; i < numParticles; i++) {
                        particles.push({
                            x: Math.random() * aiCanvas.width,
                            y: Math.random() * aiCanvas.height,
                            vx: (Math.random() - 0.5) * particleSpeed,
                            vy: (Math.random() - 0.5) * particleSpeed,
                            life: particleLife,
                            originalColor: `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})`
                        });
                    }
                }

                // 繪製粒子軌跡
                for (let step = 0; step < 150; step++) { // 增加繪製步進,生成更密集的軌跡
                    aiCtx.globalAlpha = 0.3; // 提高全局透明度,讓軌跡更明顯
                    aiCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; // 軌跡顏色更深
                    aiCtx.lineWidth = 4.0; // 軌跡線條更粗

                    for (let i = 0; i < particles.length; i++) {
                        const p = particles[i];

                        const currentOpacity = (p.life / particleLife) * (parseFloat(p.originalColor.split(',')[3]) || 0.8); // 調整透明度計算
                        aiCtx.strokeStyle = `rgba(0, 0, 0, ${currentOpacity})`;
                        aiCtx.beginPath();
                        aiCtx.moveTo(p.x, p.y);

                        // 簡單的流場:基於位置和時間影響速度
                        p.vx += Math.cos(p.y * 0.01 + time * 0.5) * 0.4; // 增加流場影響
                        p.vy += Math.sin(p.x * 0.01 + time * 0.5) * 0.4;

                        // 摩擦力
                        p.vx *= 0.90; // 略微減少摩擦
                        p.vy *= 0.90;

                        p.x += p.vx;
                        p.y += p.vy;
                        p.life--;

                        aiCtx.lineTo(p.x, p.y);
                        aiCtx.stroke();

                        // 粒子邊界反彈
                        if (p.x < 0 || p.x > aiCanvas.width) p.vx *= -1;
                        if (p.y < 0 || p.y > aiCanvas.height) p.vy *= -1;

                        // 如果粒子生命週期結束,重新初始化
                        if (p.life <= 0) {
                            p.x = Math.random() * aiCanvas.width;
                            p.y = Math.random() * aiCanvas.height;
                            p.vx = (Math.random() - 0.5) * particleSpeed;
                            p.vy = (Math.random() - 0.5) * particleSpeed;
                            p.life = particleLife;
                            p.originalColor = `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})`;
                        }
                    }
                }
                aiCtx.globalAlpha = 1; // 重置全局透明度
            }
        }

        /**
         * 觸發本地畫布圖案生成。
         * 不再調用外部 AI API。
         */
        async function generateCanvasArt() {
            console.log('正在生成本地畫布圖案...');
            
            const aiStatusElement = document.getElementById('aiStatus');
            const aiGenStatusDisplay = document.getElementById('aiGenStatus');
            const spinnerElement = aiStatusElement.querySelector('.spinner');

            // 顯示載入狀態
            aiGenStatusDisplay.textContent = '生成中...';
            aiCanvas.style.display = 'none'; // 隱藏 AI 畫布,顯示狀態覆蓋
            aiStatusElement.style.display = 'flex'; 
            aiStatusElement.innerHTML = `<div class="spinner"></div><p>正在生成圖案,請稍候...</p>`;
            // Show action buttons container
            document.querySelector('.action-buttons').style.display = 'none';


            if (spinnerElement) { 
                spinnerElement.style.display = 'block'; 
            }

            // 模擬生成延遲
            await new Promise(resolve => setTimeout(resolve, 500)); 

            try {
                // 在兩種模式間切換
                currentGenerationMode = (currentGenerationMode + 1) % 2; 
                drawAIAbstract(); // 繪製抽象圖案到 AI 畫布上

                aiCanvas.style.display = 'block'; // 顯示 AI 畫布
                aiStatusElement.style.display = 'none'; // 隱藏載入狀態
                document.querySelector('.action-buttons').style.display = 'flex'; // Show action buttons

                aiGenStatusDisplay.textContent = '完成';
                showMessage('本地圖案生成完成!', 'success');
            } catch (error) {
                console.error('生成本地圖案失敗:', error);
                aiStatusElement.style.display = 'flex'; 
                aiStatusElement.innerHTML = `生成失敗: ${error.message}`;
                aiCanvas.style.display = 'none'; 
                document.querySelector('.action-buttons').style.display = 'none'; // Hide action buttons on error
                
                aiGenStatusDisplay.textContent = '失敗';
                showMessage(`生成失敗: ${error.message}`, 'error');
            } finally {
                if (spinnerElement) { 
                    spinnerElement.style.display = 'none'; 
                }
            }
        }

        /**
         * 向使用者顯示臨時訊息框。
         * @param {string} text - 訊息文本。
         * @param {string} type - 訊息類型 ('success', 'error', 'info')。
         */
        function showMessage(text, type) {
            const msg = document.getElementById('messageBox');
            msg.textContent = text;
            msg.className = `message show ${type}`;
            
            // 訊息在 4 秒後淡出
            setTimeout(() => {
                msg.classList.remove('show');
            }, 4000); 
        }

        // 當 DOM 內容完全載入時初始化系統
        document.addEventListener('DOMContentLoaded', () => {
            console.log('DOM 載入完成');
            init();
        });
    </script>
</body>
</html>