-
Notifications
You must be signed in to change notification settings - Fork 0
/
p5wgex.js
11000 lines (10533 loc) · 463 KB
/
p5wgex.js
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
// --------------------------- //
// まず、...
// うまくいくんかいな。まあ別に死ぬわけじゃないし。死にかけたし。気楽にやろ。死ぬことが無いなら何でもできる。
// まるごと移してしまえ。えいっ
// でもってalphaをtrueで上書き。えいっ(どうなっても知らないよ...)
// 1.7.0はwebgl2なのでもう不要です
// 卍解済み
// 2023-07-18
// 暫定的にこれでいこう
// copyPainterのalphaBlendingをいじりました
// Separateのone,oneでalphaを掛ける
// とりあえずこれで。
// って思ったのに
// Separateやめたら綺麗になった・・
// もうわからん~
// offsetがoffseyになってたので直しました
// OpenProcessingが原因で発生する余計なインデントを殺しました
// ジオメトリインスタンシング導入しました
// 使い方は簡単。divisorを1以上に指定してshaderサイドでそれを使っていろいろ計算するだけ
// Instancedの付いたドローコールをしないとひとつしか描画されませんので注意しましょう
// transformFeedbackを導入しました
// ラスタライズしない処理を導入しました
// 2023-08-05
// transform feedback
// enableAttributesにおいて
// 「attrがundefined」という条件を追加しました
// これがないと描画と同時に更新することができないんですよね
// attributeの入れ替えの際に入れ替えたattributeがoutIndexをもって
// いない、また入れ替えでinされるattributeがoutIndexを持っている
// ことにより不具合が生じるわけ
// 同じIndexを指定しておくことで、これを回避できる。
// そういうことです
// TFでoutIndex>=0であってもattrがundefinedでないならば
// つまりinで宣言されているならば
// 通常の処理をしないといけないし
// 逆に通常の処理をしていたattributeにTF処理をさせたかったら
// あらかじめoutIndexを設定しておかないといけないのだよ
// 以上だよ。
// CUBE_MAPですが、いけそうですね...
// ほとんどのパートはTEXTURE_2DをTEXTURE_CUBE_MAPに変えるだけ。
// 登録時に6枚要求するところ以外はほぼ一緒
// getMat3を追加
// ex.getInverseTranspose3x3を追加
// 20230906
// foxIA移植
// OrbitControlのパッチを削除(もう要らない)
// 20230911
// foxIAの移植、カメラの改造/差し替え、Transformなどの改名、改変、CCとCMの導入、などなど。
// 20230914
// 問題発生
// 問題解消しました
// 20230917
// CCとLSを新しくして色々改良中
// 20230926
// copyPainter死んだ
// draw時のblendを可能にした
// 今んとこそんくらい
// 20231002
// pfcをDOMに変更
// foxIAについてInteractionコンストラクタにcanvasを導入
// factoryで何かしたいならoptionsに含めてください
// 今後はconstructorで初期化するのでそこら辺変更点が多いです
// Locater大幅更新、pointersを使ってタッチの場合は末尾ベースで更新することにしました
// 20231113
// StandardLightingSystemに大幅な変更
// setLightingUniformsのoptionsとしてrenderType,deferredの場合はこれをdeferredにしてね
// renderingTypeは廃止しました(意味が無いので)
// setMatrixUniformsを射影テクスチャに対応させるために法線関連の行列をセットしないようにするとか色々
// renderTypeをotherに
// またlinesを追加しました、initializeの際にtype:"lines"でラインシェーダが生成されます
// aColorに対応してるので一応線に色を付けられます
// またRenderingSystemにcameraのhelperの生成関数を追加
// createHelper(helperName, cameraName)でOKです
// 基本的には動的更新を想定しています。いわゆるイミディエイトですが、こんなののためにRetainedを持ち出すのは
// 仰々しいので却下しました。変化させる場合は毎フレーム同じ名前で呼び出してね。所詮ヘルパーなので。
// 20241018
// defaultPaintersについては
// 使う場合にその都度作成することにしました
// RenderNodeにoptionを設けてコンストラクタでというのも考えたんですが
// デフォルトがtrueになるなら同じことだし
// falseならfalseであとで作るのが面倒だし
// そういうわけで使う場合に即時生成という結論に至りました
// 20241021
// 構造体のuniformについて
// いくつか実験した結果、配列の仕様が特殊であることが判明
// https://webglfundamentals.org/webgl/lessons/ja/webgl-shaders-and-glsl.html
// sample: https://openprocessing.org/sketch/2405155
// MyStruct:{vec2 position, vec2 velocity} でuMyStruct[2]の場合
// uMyStruct[0].position, uMyStruct[0].velocity, uMyStruct[1].position, uMyStruct[1].velocity
// 全部バラバラで現れるので個数がsizeなどの方法で把握できないのだ(まじかよ)
// なので...
// 当然だが.でsplitすれば構造体かどうかは一目瞭然だし、そのあとで[でsplitをかければ
// 長さから配列の一部かどうかわかる。長さ2だとして]でsplitして0番を取れば番号が分かる
// そんな感じで構造体なのか、構造体の配列なのかといったメタデータを用意する
// 一方、通常のuniformsは今まで通り普通に作成する
// 今まで通りのナイーブなやり方でセットすることも認める
// その一方で
// 構造体uniformに関するメタデータを用いた簡易的な処理を提供できればいい。
// つまりやることはメタデータの作成とそれを用いた簡易的なuniform登録方法の策定だけ
// 従来のメソッドを改造する必要はない
// それとは別にaddUniformにおいて構造体のuniformを指定できるようにするのと...
// そもそも構造体が作れないのでそこを何とかする感じですね
// 20241023
// PBR実装するにあたり
// multMat3,multMat4を用意
// 現在のmultMatは非推奨になるかも
// glsl内部の行列計算を先に実行したい場合に不便なので転置版が欲しいんです
// あとsetUniformでvec3の場合に限り、Vec3とVec3Arrayを許すことにしました。
// いちいちtoArray()するのめんどくさいんで。
// 20241028
// lightingまわりについて改善、仕様変更
// noLightを廃止
// forwardLight:~~~などの無駄な記述の必要性を解消しました
// angleTo,angleBetweenを実装。angleToは方向考慮、angleBetweenは絶対値のみ。
// angleToは軸を引数に取りその軸のてっぺんから見た時の角度の変化を返す
// uViewMatをfsで使うのをやめた。setLightingUniformsでcameraBaseをtrueにするとカメラベースでuniform登録される
// normalDeviceCoordinateという変数を用意。鏡面描画とかで使えそう。
// 20241103
// 今後の更新予定
// ++fisceToyBoxから大量移住
// ++modifiableTube,createAxisSystem
// ++曲線からメッシュのメソッドをより使いやすく(ベジエの繋ぎ合わせとかでもいけるように)
// ++loading関連
// 以下は余裕があれば。おそらく実行されないが.......webgl-waterが忙しいので。
// --canvas生成関連
// --drawLoop関連
// --textをキャンバスに置くメソッドを独自に用意、formatが作れるように。そんなところ
// ...
// PBRが入ってないのはいつでもできるから。デモもうできてるし。
// 20241205
// textureについて
// floatTextureをstorageとして扱う場合にそれ単体で生成できるようにする
// ただvec4の場合unpack-premultiplyAlphaが邪魔をするので(format:RGBAの場合のみ)
// レアケースですが一時的にそれを解除する処理を追加する
// 具体的にはmultiplyAlpha:falseとかしてfalseの場合に一時的にfalseにして後で戻す(デフォルトtrueは据え置き)
// separateAlpha:true/falseとかの方がいいかも。
// if(this.separateAlpha){~~~~}
// fboでテクスチャする場合は空っぽで生成するうえ、shader経由でしか更新しないので必要ないですね。
// separateAlphaのdefault:falseにしてregistTextureの際にtrueにすることで
// regist時とupdate時に解除されるようにする
// 更新完了
/*
外部から上書きするメソッドの一覧
pointerPrototype:
mouseDownAction(e):
e.offsetXとe.offsetYでそのときのマウス位置
それによりなんかしたい場合に使います
mouseMoveAction(e):
e.offsetXとe.offsetYでマウスの位置
e.movementXとe.movementYで前との位置の変化...
ただこれに相当するタッチの方のあれが見当たらないので使わない方がいいかも
mouseUpAction():
マウスアップで何かしたい場合
touchStartAction(t):
t.pageXとt.pageYでそのポインタの位置です(使わないかも)
initializeでxとyになんか入るので使わないんだよね
消しちゃうか...
touchMoveAction(t):
touchEndAction(t):
Interaction:
mouseDownDefaultAction(e):
とにかくマウスがダウンされたらなんか開始する、その処理を記述する
これをx,yで書かないのは、要するにタッチだとそれがあちこちになるので不整合、
そこら辺を考慮してる。詳しくやりたいならPointerを継承すべき。
mouseMoveDefaultAction(dx, dy, x, y):
e.clientX/Yからキャンバスの位置座標を引いて計算する -> pageじゃないとスクロールに対応できないのでpageに変更
マウスポインタが存在しなくても実行されるようにする必要があるのでそういう形になる
なるんだけど
それだけ用意してもタッチサイドでは何もできないのでう~んって感じではあるわね
mouseUpDefaultAction():
マウスアップの際の一般的な処理
上記2つもそうだがInteraction独自の処理を書くことの方が多いです
wheelAction(e):
e.offsetXやe.offsetYで位置、
e.deltaYでホイールの変化。下に回すと大きな値。
clickAction():
clickイベント。クリックは左前提なので注意。
mouseEnterAction():
マウスがキャンバスに入った時の処理
mouseLeaveAction():
マウスがキャンバスから出て行った時の処理
doubleClickAction():
ダブルクリック。まあ、非推奨って言われてるけどね。
doubleTapAction():
ダブルクリックで併用できるが、これが用意されている場合
タッチではこっちが優先される
touchStartDefaultAction(e):
とにかくタッチがスタートしたらなんかする、その場合の処理を記述
touchSwipeAction(dx, dy, x, y, px, py):
指一本で動かす場合の処理
touchPinchInOutAction(value, x, y, px, py):
指二本で動かす場合の、距離が離れた場合の処理
touchMultiSwipeAction(dx, dy, x, y, px, py):
指二本で動かす場合の、重心が移動した場合の処理
touchEndDefaultAction(e):
これもよくわからん。使わないのでは?
keyDownAction(e):
キーダウン時。これもデスクトップ用。主にデバッグ用?
blenderで多用されてる。
e.keyよりe.codeを使った方がいい
ShiftやCtrlの左右を区別してくれる優れもの
keyUpAction(e):
同様。
以上です。
*/
/*
e.codeを用いる場合のキー内容一覧(https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/code を参照)
アルファベット:KeyA,KeyB,...,KeyZ.
Kは大文字ですよ!!!!!!
ShiftRight, ShiftLeft.
Enter, CapsLock, Space, ControlLeft, ControlRight, ArrowUp, ArrowDown, ArrowLeft, ArrowRight.
Numpad0,Numpad1,Numpad2,...,Numpad9.
NumpadDecimal,NumpadEnter,NumpadAdd.
上の方の数字キー:Digit0,Digit1,Digit2,...,Digit9.
BackSpace,まあ、後は調べてください...
あんま難しいこと考えても仕方ないですね。
*/
/*
もちろん
p5のようにイベントごとにリスナーを用意する道もあるんですけど
あるんですけどね
たとえばmousePressedだけ用意するにしてもタッチにもってなった場合
そっちも用意しないといけないので
いろいろめんどくさいので
やめましょうってことになったわけです。
*/
// windowにした
// pavelさんのコードは全画面前提なので
// 全画面だとどっちでもいいんですけど
// ここをcanvasにしてしまうと画面内でインタラクションが開始された後で画面外に抜けるときに止まってしまうので
// それを回避するための処理です
// いくつか変更
// InteractionとpointerPrototypeにcanvasLeft/Topを設けました
// これがないと全画面でない場合にきちんとキャンバス上の座標を取得できません
// またtouchMovepointerActionのpreventDefaultを切りました
// スマホなどで画面外を操作したときのスクロールができなくなっていたので。
// 最後に、リサイズ時にcanvasLeft/Topが更新されるようにしました。
const foxIA = (function(){
const fox = {};
class PointerPrototype{
constructor(){
this.id = -1;
this.parent = null; // 親のInteractionクラス。KAとかいろいろ応用できそう
this.x = 0;
this.y = 0;
this.dx = 0;
this.dy = 0;
this.prevX = 0;
this.prevY = 0;
//this.canvasLeft = 0;
//this.canvasTop = 0;
this.rect = {width:0, height:0, left:0, top:0};
this.button = -1; // マウス用ボタン記録。-1:タッチですよ!の意味
}
mouseInitialize(e, rect, parent = null){
this.x = e.clientX - rect.left;
this.y = e.clientY - rect.top;
this.parent = parent;
//this.canvasLeft = left;
//this.canvasTop = top;
const {width, height, left, top} = rect;
this.rect = {width, height, left, top};
this.prevX = this.x;
this.prevY = this.y;
this.button = e.button; // 0:left, 1:center, 2:right
}
mouseDownAction(e){
}
mouseUpdate(e){
this.prevX = this.x;
this.prevY = this.y;
this.dx = (e.clientX - this.rect.left - this.x);
this.dy = (e.clientY - this.rect.top - this.y);
this.x = e.clientX - this.rect.left;
this.y = e.clientY - this.rect.top;
}
mouseMoveAction(e){
}
mouseUpAction(e){
}
touchInitialize(t, rect, parent = null){
this.id = t.identifier;
this.x = t.clientX - rect.left; // 要するにmouseX的なやつ
this.y = t.clientY - rect.top; // 要するにmouseY的なやつ
this.parent = parent;
//this.canvasLeft = left;
//this.canvasTop = top;
const {width, height, left, top} = rect;
this.rect = {width, height, left, top};
this.prevX = this.x;
this.prevY = this.y;
}
updateCanvasData(rect){
// マウスでもタッチでも実行する
const prevLeft = this.rect.left;
const prevTop = this.rect.top;
//this.canvasLeft = left;
//this.canvasTop = top;
const {width, height, left, top} = rect;
this.rect = {width, height, left, top};
this.x += prevLeft - left;
this.y += prevTop - top;
this.prevX += prevLeft - left;
this.prevY += prevTop - top;
}
touchStartAction(t){
}
touchUpdate(t){
this.prevX = this.x;
this.prevY = this.y;
this.dx = (t.clientX - this.rect.left - this.x);
this.dy = (t.clientY - this.rect.top - this.y);
this.x = t.clientX - this.rect.left;
this.y = t.clientY - this.rect.top;
}
touchMoveAction(t){
}
touchEndAction(t){
}
}
// pointerの生成関数で初期化する。なければPointerPrototypeが使われる。
// 一部のメソッドはオプションで用意するかしないか決めることにしましょう
// mouseLeaveとかdoubleClickとか場合によっては使わないでしょう
// そこらへん
// canvasで初期化できるようにするか~。で、factoryはoptionsに含めてしまおう。
// 特に指定が無ければ空っぽのoptionsでやればいい。factoryが欲しい、clickやdblclickを有効化したい場合に
// optionsを書けばいいわね。
// setFactoryは必要になったら用意しましょ
// 仕様変更(20240923): factoryがnullを返す場合はpointerを生成しない。かつ、タッチエンド/マウスアップの際に
// pointersが空の場合は処理を実行しない。これにより、factoryで分岐処理を用意することで、ポインターの生成が実行されないようにできる。
class Interaction{
constructor(canvas, options = {}){
this.pointers = [];
this.factory = ((t) => new PointerPrototype());
//this.width = 0;
//this.height = 0;
// leftとtopがwindowのサイズ変更に対応するために必要
// コンストラクタでは出来ませんね。初期化時の処理。
this.rect = {width:0, height:0, left:0, top:0};
//this.canvasWidth = 0;
//this.canvasHeight = 0;
//this.canvasLeft = 0; // touch用
//this.canvasTop = 0; // touch用
this.tapCount = 0; // ダブルタップ判定用
this.firstTapped = {x:0, y:0};
// コンストラクタで初期化しましょ
this.initialize(canvas, options);
}
initialize(canvas, options = {}){
// 念のためpointersを空にする
this.pointers = [];
// factoryを定義
const {factory = ((t) => new PointerPrototype())} = options;
this.factory = factory;
// 横幅縦幅を定義
//this.width = Number((canvas.style.width).split("px")[0]);
//this.height = Number((canvas.style.height).split("px")[0]);
// touchの場合はこうしないときちんとキャンバス上の座標が取得できない
// どうもrectからwidthとheightが出る?じゃあそれでいいですね。pixelDensityによらない、css上の値。
//const rect = canvas.getBoundingClientRect();
const {width, height, left, top} = canvas.getBoundingClientRect();
this.rect = {width, height, left, top};
//this.canvasWidth = rect.width;
//this.canvasHeight = rect.height;
//this.canvasLeft = rect.left;
//this.canvasTop = rect.top;
// 右クリック時のメニュー表示を殺す
// 一応デフォルトtrueのオプションにするか...(あんま意味ないが)
const {preventOnContextMenu = true} = options;
if(preventOnContextMenu){
document.oncontextmenu = (e) => { e.preventDefault(); }
}
// touchのデフォルトアクションを殺す
//canvas.style["touch-action"] = "none";
// イベントリスナー
// optionsになったのね。じゃあそうか。passiveの規定値はfalseのようです。指定する必要、ないのか。
// そして1回のみの場合はonceをtrueにするようです。
// たとえば警告なんかに使えるかもしれないですね。ていうか明示した方がいいのか。
// 以降はdefaultIAと名付ける、これがtrueデフォルトで、falseにするとこれらを用意しないようにできる。
// たとえば考えにくいけどホイールしか要らないよって場合とか。
const {defaultIA = true, wheel = true} = options;
if (defaultIA) {
// マウス
canvas.addEventListener('mousedown', this.mouseDownAction.bind(this), {passive:false});
window.addEventListener('mousemove', this.mouseMoveAction.bind(this), {passive:false});
window.addEventListener('mouseup', this.mouseUpAction.bind(this), {passive:false});
// タッチ(ダブルタップは無いので自前で実装)
canvas.addEventListener('touchstart', this.touchStartAction.bind(this), {passive:false});
window.addEventListener('touchmove', this.touchMoveAction.bind(this), {passive:false});
window.addEventListener('touchend', this.touchEndAction.bind(this), {passive:false});
}
// ホイールはキャンバス外で実行することはまずないですね...canvasでいいかと。
if (wheel) { canvas.addEventListener('wheel', this.wheelAction.bind(this), {passive:false}); }
// リサイズの際にleftとtopが変更されるのでそれに伴ってleftとtopを更新する
window.addEventListener('resize', (function(){
//this.updateCanvasData(newRect.left, newRect.top);
this.updateCanvasData();
}).bind(this));
window.addEventListener('scroll', (function(){
this.updateCanvasData();
}).bind(this));
// options. これらは基本パソコン環境前提なので(スマホが関係ないので)、オプションとします。
const {
mouseenter = false, mouseleave = false, click = false, dblclick = false,
keydown = false, keyup = false
} = options;
// マウスの出入り
if (mouseenter) { canvas.addEventListener('mouseenter', this.mouseEnterAction.bind(this), {passive:false}); }
if (mouseleave) { canvas.addEventListener('mouseleave', this.mouseLeaveAction.bind(this), {passive:false}); }
// クリック
if (click) { canvas.addEventListener('click', this.clickAction.bind(this), {passive:false}); }
if (dblclick) { canvas.addEventListener('dblclick', this.doubleClickAction.bind(this), {passive:false}); }
// キー(keypressは非推奨とのこと)
// いわゆる押しっぱなしの時の処理についてはフラグの切り替えのために両方必要になるわね
if (keydown) { window.addEventListener('keydown', this.keyDownAction.bind(this), {passive:false}); }
if (keyup) { window.addEventListener('keyup', this.keyUpAction.bind(this), {passive:false}); }
}
getRect(){
return this.rect;
}
updateCanvasData(){
const newRect = canvas.getBoundingClientRect();
// 対象のキャンバスを更新
const {width, height, left, top} = newRect;
this.rect = {width, height, left, top};
//this.canvasLeft = left;
//this.canvasTop = top;
for(const p of this.pointers){ p.updateCanvasData(newRect); }
}
mouseDownAction(e){
this.mouseDownPointerAction(e);
this.mouseDownDefaultAction(e);
}
mouseDownPointerAction(e){
const p = this.factory(this);
if (p === null) return; // factoryがnullを返す場合はpointerを生成しない
//p.mouseInitialize(e, this.canvasLeft, this.canvasTop);
p.mouseInitialize(e, this.rect, this);
p.mouseDownAction(e);
this.pointers.push(p);
}
mouseDownDefaultAction(e){
// Interactionサイドの実行内容を書く
}
mouseMoveAction(e){
this.mouseMovePointerAction(e);
//this.mouseMoveDefaultAction(e.movementX, e.movementY, e.clientX - this.canvasLeft, e.clientY - this.canvasTop);
// なぜmovementを使っているかというと、
// このアクションはポインターが無関係だから(ポインターが無くても実行される)
// まずいのはわかってるけどね...
// マウスダウン時のPointerの位置の計算についてはmovementが出てこないので
// マウスダウン時しか要らない場合は使わないのもあり。
this.mouseMoveDefaultAction(e.movementX, e.movementY, e.clientX - this.rect.left, e.clientY - this.rect.top);
}
mouseMovePointerAction(e){
// pointerが生成されなかった場合は処理を実行しない
if(this.pointers.length === 0){ return; }
const p = this.pointers[0];
p.mouseUpdate(e);
p.mouseMoveAction(e);
}
mouseMoveDefaultAction(dx, dy, x, y){
// Interactionサイドの実行内容を書く
}
mouseUpAction(e){
this.mouseUpPointerAction(e);
this.mouseUpDefaultAction(e);
}
mouseUpPointerAction(e){
// pointerが生成されなかった場合は処理を実行しない
if(this.pointers.length === 0){ return; }
// ここで排除するpointerに何かさせる...
const p = this.pointers[0];
p.mouseUpAction(e);
this.pointers.pop();
}
mouseUpDefaultAction(e){
// Interactionサイドの実行内容を書く
}
mouse(e){
// ホイールのイベントなどで正確なマウス座標が欲しい場合に有用
// マウス限定なのでイベント内部などマウスが関係する処理でしか使わない方がいいです
return {x:e.clientX - this.rect.left, y:e.clientY - this.rect.top};
}
wheelAction(e){
// Interactionサイドの実行内容を書く
// e.deltaXとかe.deltaYが使われる。下にホイールするとき正の数、上にホイールするとき負の数。
// 速くホイールすると大きな数字が出る。おそらく仕様によるもので-1000~1000の100の倍数になった。0.01倍して使うといいかもしれない。
// 当然だが、拡大縮小に使う場合は対数を使った方が挙動が滑らかになるしスケールにもよらないのでおすすめ。
}
clickAction(){
// Interactionサイドの実行内容を書く。クリック時。左クリック。
}
mouseEnterAction(){
// Interactionサイドの実行内容を書く。enter時。
}
mouseLeaveAction(){
// Interactionサイドの実行内容を書く。leave時。
}
doubleClickAction(){
// Interactionサイドの実行内容を書く。ダブルクリック時。
}
doubleTapAction(){
// Interactionサイドの実行内容を書く。ダブルタップ時。自前で実装するしかないようです。初めて知った。
}
touchStartAction(e){
this.touchStartPointerAction(e);
this.touchStartDefaultAction(e);
// 以下、ダブルタップ用
// マルチタップ時にはイベントキャンセル(それはダブルタップではない)
if(this.pointers.length > 1){ this.tapCount = 0; return; }
// シングルタップの場合、0ならカウントを増やしつつ350ms後に0にするカウントダウンを開始
// ただし、factoryがnullを返すなど、pointerが生成されないならば、実行しない。
// pointerが無い以上、ダブルタップの判定が出来ないので。
if(this.pointers.length === 0){ return; }
if(this.tapCount === 0){
// thisをbindしないとおかしなことになると思う
setTimeout((function(){ this.tapCount = 0; }).bind(this), 350);
this.tapCount++;
this.firstTapped.x = this.pointers[0].x;
this.firstTapped.y = this.pointers[0].y;
} else {
this.tapCount++;
// 最初のタップした場所とあまりに離れている場合はダブルとみなさない
// 25くらいあってもいい気がしてきた
const {x, y} = this.pointers[0];
if(Math.hypot(this.firstTapped.x - x, this.firstTapped.y - y) > 25){ this.tapCount = 0; return; }
if(this.tapCount === 2){
this.doubleTapAction();
this.tapCount = 0;
}
}
}
touchStartPointerAction(e){
e.preventDefault();
// targetTouchesを使わないとcanvas外のタッチオブジェクトを格納してしまう
const currentTouches = e.targetTouches; // touchオブジェクトの配列
const newPointers = [];
// 新入りがいないかどうか調べていたら増やす感じですね
// targetTouchesのうちでpointersに入ってないものを追加する処理です
// 入ってないかどうかはidで調べます
for (let i = 0; i < currentTouches.length; i++){
let equalFlag = false;
for (let j = 0; j < this.pointers.length; j++){
if (currentTouches[i].identifier === this.pointers[j].id){
equalFlag = true;
break;
}
}
if(!equalFlag){
const p = this.factory(this);
if (p === null) return; // factoryがnullを返す場合はpointerを生成しない
//p.touchInitialize(currentTouches[i], this.canvasLeft, this.canvasTop);
p.touchInitialize(currentTouches[i], this.rect, this);
p.touchStartAction(currentTouches[i]);
newPointers.push(p);
}
}
this.pointers.push(...newPointers);
}
touchStartDefaultAction(e){
// Interactionサイドの実行内容を書く。touchがスタートした時
}
touchMoveAction(e){
// pointerごとにupdateする
this.touchMovePointerAction(e);
if (this.pointers.length === 1) {
// swipe.
const p0 = this.pointers[0];
this.touchSwipeAction(
p0.x - p0.prevX, p0.y - p0.prevY, p0.x, p0.y, p0.prevX, p0.prevY
);
} else if (this.pointers.length > 1) {
// pinch in/out.
const p = this.pointers[0];
const q = this.pointers[1];
// pとqから重心の位置と変化、距離の変化を
// 計算して各種アクションを実行する
const gx = (p.x + q.x) * 0.5;
const gPrevX = (p.prevX + q.prevX) * 0.5;
const gy = (p.y + q.y) * 0.5;
const gPrevY = (p.prevY + q.prevY) * 0.5;
const gDX = gx - gPrevX;
const gDY = gy - gPrevY;
const curDistance = Math.hypot(p.x - q.x, p.y - q.y);
const prevDistance = Math.hypot(p.prevX - q.prevX, p.prevY - q.prevY)
// 今の距離 - 前の距離
const diff = curDistance - prevDistance;
// 今の距離 / 前の距離
const ratio = curDistance / prevDistance;
// 差も比も使えると思ったので仕様変更
this.touchPinchInOutAction(diff, ratio, gx, gy, gPrevX, gPrevY);
this.touchMultiSwipeAction(gDX, gDY, gx, gy, gPrevX, gPrevY);
// rotateは要検討
}
}
touchMovePointerAction(e){
// pointerが生成されなかった場合は処理を実行しない
if(this.pointers.length === 0){ return; }
//e.preventDefault();
const currentTouches = e.targetTouches;
for (let i = 0; i < currentTouches.length; i++){
const t = currentTouches[i];
for (let j = 0; j < this.pointers.length; j++){
if (t.identifier === this.pointers[j].id){
const p = this.pointers[j];
p.touchUpdate(t);
p.touchMoveAction(t);
}
}
}
}
touchSwipeAction(dx, dy, x, y, px, py){
// Interactionサイドの実行内容を書く。
// dx,dyが変位。
}
touchPinchInOutAction(diff, ratio, x, y, px, py){
// Interactionサイドの実行内容を書く。
// diffは距離の変化。正の場合大きくなる。ratioは距離の比。
}
touchMultiSwipeAction(dx, dy, x, y, px, py){
// Interactionサイドの実行内容を書く。
// dx,dyは重心の変位。
}
touchRotateAction(value, x, y, px, py){
// TODO.
}
touchEndAction(e){
// End時のアクション。
this.touchEndPointerAction(e);
this.touchEndDefaultAction(e);
}
touchEndPointerAction(e){
// pointerが生成されなかった場合は処理を実行しない
if(this.pointers.length === 0){ return; }
const changedTouches = e.changedTouches;
for (let i = 0; i < changedTouches.length; i++){
for (let j = this.pointers.length-1; j >= 0; j--){
if (changedTouches[i].identifier === this.pointers[j].id){
// ここで排除するpointerに何かさせる...
const p = this.pointers[j];
p.touchEndAction(changedTouches[i]);
this.pointers.splice(j, 1);
}
}
}
}
touchEndDefaultAction(e){
// Interactionサイドの実行内容を書く。touchEndが発生した場合。
// とはいえ難しいだろうので、おそらくpointersが空っぽの時とかそういう感じになるかと。
}
keyDownAction(e){
// Interactionサイドの実行内容を書く。
// キーが押されたとき
}
keyUpAction(e){
// Interactionサイドの実行内容を書く。
// キーが離れた時
//console.log(e.code);
}
resizeAction(){
// リサイズ時の処理。
}
getPointers(){
return this.pointers;
}
}
// addEventの方がよさそう
// add
// clear
// addとclearでよいです
// addでイベントを追加しclearですべて破棄します
// addで登録するイベント名をリスナーに合わせました(有効化オプションもこれになってるので倣った形です)
// 一応touchStartとdbltapと複数登録用意しました、が、一応デスクトップでの運用が主なので、
// 本格的にやるならCCみたいに継承してね。
class Inspector extends Interaction{
constructor(canvas, options = {}){
super(canvas, options);
this.functions = {
mousedown:[],
mousemove:[],
mouseup:[],
wheel:[],
click:[],
mouseenter:[],
mouseleave:[],
dblclick:[],
keydown:[],
keyup:[],
touchstart:[], // スマホだとclickが発動しないので代わりに。
touchend:[], // タッチエンドあった方がいい?
dbltap:[] // doubleTapですね。これも用意しておきましょ。
};
}
execute(name, args){
for (const func of this.functions[name]){
func(...args);
}
}
add(name, func){
// 複数のインタラクションを同時に設定できるようにする
if (typeof name === 'string') {
this.functions[name].push(func);
} else if (Array.isArray(name)) {
for (const functionName of name) {
this.functions[functionName].push(func);
}
}
}
clear(name){
this.functions[name] = [];
}
mouseDownDefaultAction(e){
this.execute("mousedown", arguments);
}
mouseMoveDefaultAction(dx, dy, x, y){
this.execute("mousemove", arguments);
}
mouseUpDefaultAction(e){
this.execute("mouseup", arguments);
}
wheelAction(e){
this.execute("wheel", arguments);
}
clickAction(){
this.execute("click", arguments);
}
mouseEnterAction(){
this.execute("mouseenter", arguments);
}
mouseLeaveAction(){
this.execute("mouseleave", arguments);
}
doubleClickAction(){
this.execute("dblclick", arguments);
}
doubleTapAction(){
this.execute("dbltap", arguments);
}
keyDownAction(e){
this.execute("keydown", arguments);
}
keyUpAction(e){
this.execute("keyup", arguments);
}
touchStartDefaultAction(e){
this.execute("touchstart", arguments);
}
touchEndDefaultAction(e){
this.execute("touchend", arguments);
}
}
// これクラス化しよ??Locaterがいい。
// 簡易版。毎フレームupdateする。pointersを調べて末尾を取る。末尾なので、常に新規が採用される。
// 位置情報を更新する。x,y,dx,dyを使う。また関数を導入できる。
// 発動時、移動時、activeを前提として常時、終了時のアクションが存在する。終了時はタッチの場合、
// pointersが空になるとき。なぜなら常に新規で更新されるので。
// 取得するときclampとnormalizeのoptionを設けるようにしました。
// factorを設けてすぐに値が変わらないようにできる仕組みを導入しました。
// 自由に変えられるようにするかどうかは応相談...できるだけ軽量で行きたいので。
// mouseFreeUpdateにより、マウスの場合にマウス移動で位置更新がされるようにするオプションを追加
class Locater extends Interaction{
constructor(canvas, options = {}){
super(canvas, options);
this.active = false;
this.x = 0;
this.y = 0;
this.dx = 0;
this.dy = 0;
// 位置情報を滑らかに変化させたいときはoptionsでfactorを定義する。
const {factor = 1} = options;
this.factor = factor;
// マウス操作の場合、位置情報をマウス移動に伴って変化させたい場合もあるでしょう。
// mouseFreeUpdateのoptionを設けてそれが実現されるようにします
const {mouseFreeUpdate = false} = options;
this.mouseFreeUpdate = mouseFreeUpdate;
// 関数族
this.actions = {}; // activate, inActivate, move.
// 関数のデフォルト。
this.actions.activate = (e) => {};
this.actions.move = (x, y, dx, dy) => {};
this.actions.update = (x, y, dx, dy) => {};
this.actions.inActivate = (e) => {};
// ボタン.
this.button = -1;
}
positionUpdate(x, y, dx, dy){
// 位置情報の更新を関数化する
// 急に変化させたくない場合に徐々に変化させる選択肢を設ける
const factor = this.factor;
this.x += (x - this.x) * factor;
this.y += (y - this.y) * factor;
this.dx += (dx - this.dx) * factor;
this.dy += (dy - this.dy) * factor;
}
update(){
if (this.pointers.length > 0) {
// 末尾(新規)を採用する。
// マウス操作でmouseFreeUpdateの場合これが実行されないようにするには、結局pointer.length>0ということは
// もうactivateされててbutton>=0であるから、タッチならここが-1だから、そこで判定できる。そこで、
// (this.button >= 0 && this.mouseFreeUpdate)の場合にキャンセルさせる。この場合!を使った方が分かりやすい。
// 「マウスアクションにおいてmouseFreeUpdateの場合はactive時にはpositionをupdateしない」という日本語の翻訳になる。
// buttonを使うことでタッチとマウスの処理を分けられるわけ。
if (!(this.button >= 0 && this.mouseFreeUpdate)) {
const p = this.pointers[this.pointers.length - 1];
this.positionUpdate(p.x, p.y, p.dx, p.dy);
}
}
if (this.active) {
this.actions.update(this.x, this.y, this.dx, this.dy);
}
}
setAction(name, func){
// オブジェクト記法に対応
if (typeof name === 'string') {
this.actions[name] = func;
} else if (typeof name === 'object') {
for(const _name of Object.keys(name)){
const _func = name[_name];
this.actions[_name] = _func;
}
}
}
isActive(){
return this.active;
}
getPos(options = {}){
const {clamp = false, normalize = false} = options;
const {width:w, height:h} = this.rect;
// clampのoptionsがある場合は先にclampしてから正規化する。
// dxとdyはclampの必要がない。
const result = {x:this.x, y:this.y, dx:this.dx, dy:this.dy};
if (clamp) {
result.x = Math.max(0, Math.min(w, result.x));
result.y = Math.max(0, Math.min(h, result.y));
}
// 正規化して0~1の値を返せるようにする。
if (normalize) {
result.x /= w;
result.y /= h;
result.dx /= w;
result.dy /= h;
}
return result;
}
mouseDownDefaultAction(e){
// ボタン. 0:left, 1:center, 2:right
this.button = e.button;
this.active = true;
this.actions.activate(e); // e.buttonで処理分けた方が楽だわ。タッチの場合は常に-1だけどね。
}
mouseMoveDefaultAction(dx, dy, x, y){
// mouseFreeUpdateがtrueであれば常に位置更新がされるようにする
// タッチの場合ここは実行されないため、mouseFreeUpdateがtrueでも問題ない。
if (this.mouseFreeUpdate) {
// ああここか
// xとyをそのまま使っちゃってる
// ...
this.positionUpdate(x, y, dx, dy);
}
if(this.active){
this.actions.move(x, y, dx, dy);
}
}
mouseUpDefaultAction(e){
// activateされていないなら各種の処理は不要
if (!this.active) return;
this.active = false;
this.actions.inActivate(e);
// ボタンリセット
this.button = -1;
}
touchStartDefaultAction(e){
this.active = true;
this.actions.activate(e);
}
touchSwipeAction(dx, dy, x, y, px, py){
if (this.active) {
this.actions.move(x, y, dx, dy);
}
}
touchEndDefaultAction(e){
// ここ、タッチポインタが一つでも外れるとオフになる仕様なんだけど、
// タッチポインタ、末尾採用にしたから、全部空の時だけ発動でいいよ。
// 空っぽになる場合、この時点でちゃんと空っぽだから。
// ここもactiveでないのに実行されてしまうようですね...防いでおくか。
if (this.active && this.pointers.length === 0) {
this.active = false;
this.actions.inActivate(e);
}
}
}
// キーを押したとき(activate), キーを押しているとき(update), キーを離したとき(inActivate),
// それぞれに対してイベントを設定する。
// 改変でキーコードが分かるようにするわ(どう使うか?showKeyCode:trueしたうえで使いたいキーをたたくだけ。)
// キーごとにただひとつ生成されるagent
// プロパティを持たせることで処理に柔軟性を持たせることができる。
// もちろんすべてのagentが共通のプロパティを持つ必要はないが、
// そこはメソッドで無視すればいいだけ。
class KeyAgent{
constructor(code){
this.code = code;
// tは親のKeyActionで、すなわちそれを受け取る。
// 他のキーのactive状態などを分岐処理に利用できる。
this.activateFunction = (t,a)=>{};
this.updateFunction = (t,a)=>{};
this.inActivateFunction = (t,a)=>{};
this.active = false;
}
isActive(){
return this.active;
}
activate(t){
this.activateFunction(t, this);
this.active = true;
}
update(t){
this.updateFunction(t, this);
}
inActivate(t){
this.inActivateFunction(t, this);
this.active = false;
}
registAction(actionType, func){
if(typeof actionType)
this[actionType.concat("Function")] = func;
}
}
// 改善案(同時押し対応)
// isActiveが未定義の場合nullを返しているところをfalseを返すようにする
// さらにactivate,update,inActivateの関数登録で引数を持たせられるようにする。その内容は第一引数で、
// thisである。どう使うかというとたとえば(e)=>{if(e.isActive){~~~}}といった感じで「これこれのキーが押されている場合~~」
// っていう、いわゆる同時押し対応をできるようにする。その際、たとえばBを押しながらAのときに、Bを押すだけの処理が存在しないと
// isActiveがnullを返してしまうので、先のように変更したいわけです。
// 改良版KeyAction.
// agentをクラス化することでさらに複雑な処理を可能にする.
// うん
// PointerPrototypeで遊びたいので
// オフにするのはやめましょ
class KeyAction extends Interaction{
constructor(canvas, options = {}){
// keydown,keyupは何も指定せずともlistenerが登録されるようにする
// こういう使い方もあるのだ(superの宣言箇所は任意!)
options.keydown = true;
options.keyup = true;
super(canvas, options);
this.keys = {};
this.options = {
showKeyCode:false, autoRegist:true
}
// keyAgentFactoryはcodeを引数に取る
// codeごとに異なる毛色のagentが欲しい場合に有用
const {keyAgentFactory = (code) => new KeyAgent(code)} = options;
this.keyAgentFactory = keyAgentFactory;
// showKeyCode: デフォルトはfalse. trueの場合、キーをたたくとコンソールにe.codeが表示される
// autoRegist: デフォルトはtrue. trueの場合、キーをたたくと自動的にkeyActionObjectがそれに対して生成される。
}
enable(...args){