ntk 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -151
- package/lib/app.js +54 -21
- package/lib/cleanup.js +14 -0
- package/lib/drawable.js +12 -18
- package/lib/events_map.js +92 -98
- package/lib/fontconfig.js +123 -17
- package/lib/glyphset.js +40 -21
- package/lib/image.js +138 -0
- package/lib/index.js +123 -57
- package/lib/path.js +734 -0
- package/lib/picture.js +84 -41
- package/lib/pixmap.js +50 -39
- package/lib/rasterize.js +178 -0
- package/lib/renderingcontext_2d.js +1078 -695
- package/lib/renderingcontext_opengl.js +53 -46
- package/lib/renderingcontext_x11.js +59 -67
- package/lib/text/font.js +196 -0
- package/lib/text/fontmanager.js +233 -0
- package/lib/text/fontsource.js +179 -0
- package/lib/text/glyphs.js +456 -0
- package/lib/text/layout.js +356 -0
- package/lib/text/shape.js +141 -0
- package/lib/trapezoid.js +131 -0
- package/lib/widgets/css.js +676 -0
- package/lib/widgets/highlight.js +97 -0
- package/lib/widgets/htmlview.js +879 -0
- package/lib/widgets/markdown.js +148 -0
- package/lib/widgets/markdownview.js +487 -0
- package/lib/widgets/mermaid.js +720 -0
- package/lib/widgets/svgview.js +528 -0
- package/lib/widgets/tex.js +681 -0
- package/lib/window.js +642 -308
- package/package.json +60 -56
- package/examples/example-await.js +0 -23
- package/examples/example-co.js +0 -13
- package/examples/example.js +0 -14
- package/examples/fill-styles.js +0 -39
- package/examples/glclock.js +0 -100
- package/examples/glxpixmap.js +0 -12
- package/examples/gradient.js +0 -41
- package/examples/package.json +0 -17
- package/examples/simpletex.js +0 -72
- package/examples/teapot.js +0 -104
- package/examples/teapot.json +0 -6
- package/examples/text-rendering.js +0 -44
- package/lib/fontface.js +0 -103
- package/lib/widgets/webview.js +0 -33
|
@@ -1,695 +1,1078 @@
|
|
|
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
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
this.
|
|
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
|
-
this.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
|
|
1
|
+
import parseFontStyle from 'canvas-fontstyle';
|
|
2
|
+
import extrudePolyline from 'extrude-polyline';
|
|
3
|
+
import parseColorRaw from 'parse-color';
|
|
4
|
+
|
|
5
|
+
import Drawable from './drawable.js';
|
|
6
|
+
import { Image } from './image.js';
|
|
7
|
+
import {
|
|
8
|
+
Path2D,
|
|
9
|
+
flattenPath,
|
|
10
|
+
transformCommands,
|
|
11
|
+
ellipseSegments,
|
|
12
|
+
polysContain,
|
|
13
|
+
matApply,
|
|
14
|
+
matInvert,
|
|
15
|
+
matIsIdentity,
|
|
16
|
+
matMultiply
|
|
17
|
+
} from './path.js';
|
|
18
|
+
import Picture from './picture.js';
|
|
19
|
+
import Pixmap from './pixmap.js';
|
|
20
|
+
import { drawGlyphRuns } from './text/glyphs.js';
|
|
21
|
+
import { TextLayout } from './text/layout.js';
|
|
22
|
+
import { reorderRuns } from './text/shape.js';
|
|
23
|
+
import { trapezoidize } from './trapezoid.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_FONT = '20px sans-serif';
|
|
26
|
+
|
|
27
|
+
// canvas globalCompositeOperation -> XRender PictOp name. Porter-Duff ops
|
|
28
|
+
// map directly; with a clip/shape mask active the op only applies inside
|
|
29
|
+
// the mask coverage (outside pixels are left untouched).
|
|
30
|
+
const GCO_TO_PICTOP = {
|
|
31
|
+
'source-over': 'Over',
|
|
32
|
+
copy: 'Src',
|
|
33
|
+
'destination-over': 'OverReverse',
|
|
34
|
+
'source-in': 'In',
|
|
35
|
+
'destination-in': 'InReverse',
|
|
36
|
+
'source-out': 'Out',
|
|
37
|
+
'destination-out': 'OutReverse',
|
|
38
|
+
'source-atop': 'Atop',
|
|
39
|
+
'destination-atop': 'AtopReverse',
|
|
40
|
+
xor: 'Xor',
|
|
41
|
+
lighter: 'Add'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// extrude-polyline has no round caps/joins; degrade to the closest shape
|
|
45
|
+
const LINE_CAP = { butt: 'butt', square: 'square', round: 'square' };
|
|
46
|
+
const LINE_JOIN = { miter: 'miter', bevel: 'bevel', round: 'bevel' };
|
|
47
|
+
|
|
48
|
+
function parseColor(value) {
|
|
49
|
+
if (Array.isArray(value)) return value;
|
|
50
|
+
const c = parseColorRaw(value);
|
|
51
|
+
return [c.rgba[0] / 255, c.rgba[1] / 255, c.rgba[2] / 255, c.rgba[3]];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Canvas-like 2d rendering context backed by the XRender extension: most
|
|
56
|
+
* operations (composition, gradients, text composition) happen server-side.
|
|
57
|
+
*
|
|
58
|
+
* Paths follow the HTML canvas model: the default path records points with
|
|
59
|
+
* the current transform applied at command time; `Path2D` objects are
|
|
60
|
+
* transformed when filled/stroked/clipped. See docs/context-2d.md for the
|
|
61
|
+
* supported surface and the differences from the browser canvas.
|
|
62
|
+
*/
|
|
63
|
+
class RenderingContext2d {
|
|
64
|
+
constructor(window) {
|
|
65
|
+
const X = window.X;
|
|
66
|
+
this.X = X;
|
|
67
|
+
|
|
68
|
+
this.window = window;
|
|
69
|
+
this.Render = window.app.display.Render;
|
|
70
|
+
|
|
71
|
+
// draw into the window's backing pixmap when it has one (double
|
|
72
|
+
// buffering); re-bind when the backing pixmap is reallocated on resize
|
|
73
|
+
this._target = null;
|
|
74
|
+
this._gc = null;
|
|
75
|
+
this.picture = null;
|
|
76
|
+
this._bindTarget();
|
|
77
|
+
if (typeof window.on === 'function') {
|
|
78
|
+
window.on('_backing', () => this._bindTarget());
|
|
79
|
+
// masks are sized to the drawable — recreate them after a resize
|
|
80
|
+
window.on('resize', () => this._dropMasks());
|
|
81
|
+
// window-backed pictures are freed server-side with the window
|
|
82
|
+
window.on('_destroyed', () => {
|
|
83
|
+
if (this.picture && this._target === this.window) {
|
|
84
|
+
this.picture.forget();
|
|
85
|
+
this.picture = null;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this._solidPictures = new Map();
|
|
91
|
+
this.fillMask = null;
|
|
92
|
+
this.fillMaskDrawable = null;
|
|
93
|
+
this.clipMask = null;
|
|
94
|
+
this.clipMaskDrawable = null;
|
|
95
|
+
this._textStyle = null;
|
|
96
|
+
this._lastFontString = null;
|
|
97
|
+
this.textAlign = 'start';
|
|
98
|
+
this.textBaseline = 'alphabetic';
|
|
99
|
+
|
|
100
|
+
this._path = new Path2D();
|
|
101
|
+
this._m = [1, 0, 0, 1, 0, 0];
|
|
102
|
+
this._stack = [];
|
|
103
|
+
this._clips = []; // [{ polys, rule }] in device space, already stacked
|
|
104
|
+
this._gco = 'source-over';
|
|
105
|
+
this.globalAlpha = 1;
|
|
106
|
+
this.lineCap = 'butt';
|
|
107
|
+
this.lineJoin = 'miter';
|
|
108
|
+
this.miterLimit = 10;
|
|
109
|
+
|
|
110
|
+
this.fillStyle = 'white';
|
|
111
|
+
this.strokeStyle = 'black';
|
|
112
|
+
this.lineWidth = 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_bindTarget() {
|
|
116
|
+
const target = this.window._backing || this.window;
|
|
117
|
+
if (this._target === target) return;
|
|
118
|
+
this._target = target;
|
|
119
|
+
|
|
120
|
+
if (!this._gc) {
|
|
121
|
+
// one GC is enough: it stays valid for any drawable of the same
|
|
122
|
+
// screen and depth (the backing pixmap matches the window depth)
|
|
123
|
+
this._gc = this.X.AllocID();
|
|
124
|
+
this.X.CreateGC(this._gc, target.id);
|
|
125
|
+
}
|
|
126
|
+
if (this.picture) this.picture.destroy();
|
|
127
|
+
// TODO: support a8
|
|
128
|
+
const depth = target.depth ?? this.window.depth;
|
|
129
|
+
const format = depth === 32 ? this.Render.rgba32 : this.Render.rgb24;
|
|
130
|
+
this.picture = new Picture(this.window.app, {
|
|
131
|
+
drawable: target,
|
|
132
|
+
format,
|
|
133
|
+
polyEdge: 1,
|
|
134
|
+
polyMode: 1
|
|
135
|
+
});
|
|
136
|
+
this._dropMasks();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_dropMasks() {
|
|
140
|
+
if (this.fillMask) {
|
|
141
|
+
this.fillMask.destroy();
|
|
142
|
+
this.fillMaskDrawable.destroy();
|
|
143
|
+
this.fillMask = this.fillMaskDrawable = null;
|
|
144
|
+
}
|
|
145
|
+
if (this.clipMask) {
|
|
146
|
+
this.clipMask.destroy();
|
|
147
|
+
this.clipMaskDrawable.destroy();
|
|
148
|
+
this.clipMask = this.clipMaskDrawable = null;
|
|
149
|
+
}
|
|
150
|
+
// clip paths are device-space and survive the resize; rebuild the mask
|
|
151
|
+
if (this._clips && this._clips.length) this._rebuildClipMask();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// notify a double-buffered window that its backing content changed
|
|
155
|
+
_markDirty() {
|
|
156
|
+
if (typeof this.window._markDirty === 'function') this.window._markDirty();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// html context2d compatibility: canvas.getContext('2d').canvas == canvas
|
|
160
|
+
get canvas() {
|
|
161
|
+
return this.window;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get width() {
|
|
165
|
+
return this.window.width;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get height() {
|
|
169
|
+
return this.window.height;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_op() {
|
|
173
|
+
return this.Render.PictOp[GCO_TO_PICTOP[this._gco] || 'Over'];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
set globalCompositeOperation(value) {
|
|
177
|
+
if (value in GCO_TO_PICTOP) this._gco = value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get globalCompositeOperation() {
|
|
181
|
+
return this._gco;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
createSolidPicture(r, g, b, a) {
|
|
185
|
+
const key = [r, g, b, a].join('|');
|
|
186
|
+
let p = this._solidPictures.get(key);
|
|
187
|
+
if (p) return p;
|
|
188
|
+
|
|
189
|
+
// TODO: use window.createPixmap
|
|
190
|
+
const pixmap = new Pixmap(this.window.app, { depth: 32, width: 1, height: 1 });
|
|
191
|
+
const pictSolidPix = this.window.X.AllocID();
|
|
192
|
+
this.Render.CreatePicture(pictSolidPix, pixmap.id, this.Render.rgba32, { repeat: 1 });
|
|
193
|
+
this.Render.FillRectangles(1, pictSolidPix, [r, g, b, a], [0, 0, 100, 100]);
|
|
194
|
+
p = new Picture(this.window.app, { id: pictSolidPix });
|
|
195
|
+
p._sourcePixmap = pixmap; // keep the 1x1 pixmap alive alongside the picture
|
|
196
|
+
this._solidPictures.set(key, p);
|
|
197
|
+
return p;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_stylePicture(value) {
|
|
201
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
202
|
+
const c = parseColor(value);
|
|
203
|
+
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
204
|
+
}
|
|
205
|
+
if (value instanceof Picture || value instanceof CanvasGradient) {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Unknown fill style');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
set fillStyle(value) {
|
|
212
|
+
this._fillStyle = value;
|
|
213
|
+
this._backgroundPicture = this._stylePicture(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get fillStyle() {
|
|
217
|
+
return this._fillStyle;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
set strokeStyle(value) {
|
|
221
|
+
this._strokeStyle = value;
|
|
222
|
+
this._strokePicture = this._stylePicture(value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get strokeStyle() {
|
|
226
|
+
return this._strokeStyle;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ------------------------------------------------------------------
|
|
230
|
+
// state
|
|
231
|
+
|
|
232
|
+
save() {
|
|
233
|
+
this._stack.push({
|
|
234
|
+
fillStyle: this._fillStyle,
|
|
235
|
+
strokeStyle: this._strokeStyle,
|
|
236
|
+
lineWidth: this.lineWidth,
|
|
237
|
+
lineCap: this.lineCap,
|
|
238
|
+
lineJoin: this.lineJoin,
|
|
239
|
+
miterLimit: this.miterLimit,
|
|
240
|
+
globalAlpha: this.globalAlpha,
|
|
241
|
+
gco: this._gco,
|
|
242
|
+
textStyle: this._textStyle,
|
|
243
|
+
fontString: this._lastFontString,
|
|
244
|
+
textAlign: this.textAlign,
|
|
245
|
+
textBaseline: this.textBaseline,
|
|
246
|
+
m: this._m.slice(),
|
|
247
|
+
clips: this._clips
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
restore() {
|
|
252
|
+
const s = this._stack.pop();
|
|
253
|
+
if (!s) return;
|
|
254
|
+
this.fillStyle = s.fillStyle;
|
|
255
|
+
this.strokeStyle = s.strokeStyle;
|
|
256
|
+
this.lineWidth = s.lineWidth;
|
|
257
|
+
this.lineCap = s.lineCap;
|
|
258
|
+
this.lineJoin = s.lineJoin;
|
|
259
|
+
this.miterLimit = s.miterLimit;
|
|
260
|
+
this.globalAlpha = s.globalAlpha;
|
|
261
|
+
this._gco = s.gco;
|
|
262
|
+
this._textStyle = s.textStyle;
|
|
263
|
+
this._lastFontString = s.fontString;
|
|
264
|
+
this.textAlign = s.textAlign;
|
|
265
|
+
this.textBaseline = s.textBaseline;
|
|
266
|
+
this._m = s.m;
|
|
267
|
+
if (s.clips !== this._clips) {
|
|
268
|
+
this._clips = s.clips;
|
|
269
|
+
this._rebuildClipMask();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ------------------------------------------------------------------
|
|
274
|
+
// transform
|
|
275
|
+
|
|
276
|
+
translate(x, y) {
|
|
277
|
+
this._m = matMultiply(this._m, [1, 0, 0, 1, x, y]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
rotate(angle) {
|
|
281
|
+
const c = Math.cos(angle);
|
|
282
|
+
const s = Math.sin(angle);
|
|
283
|
+
this._m = matMultiply(this._m, [c, s, -s, c, 0, 0]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
scale(x, y = x) {
|
|
287
|
+
this._m = matMultiply(this._m, [x, 0, 0, y, 0, 0]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
transform(a, b, c, d, e, f) {
|
|
291
|
+
this._m = matMultiply(this._m, [a, b, c, d, e, f]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setTransform(a, b, c, d, e, f) {
|
|
295
|
+
if (typeof a === 'object' && a !== null) {
|
|
296
|
+
const m = a;
|
|
297
|
+
this._m = Array.isArray(m) ? m.slice(0, 6) : [m.a, m.b, m.c, m.d, m.e, m.f];
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (a === undefined) return this.resetTransform();
|
|
301
|
+
this._m = [a, b, c, d, e, f];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
resetTransform() {
|
|
305
|
+
this._m = [1, 0, 0, 1, 0, 0];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getTransform() {
|
|
309
|
+
const [a, b, c, d, e, f] = this._m;
|
|
310
|
+
return { a, b, c, d, e, f };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ------------------------------------------------------------------
|
|
314
|
+
// path building (default path is recorded in device space, per spec)
|
|
315
|
+
|
|
316
|
+
beginPath() {
|
|
317
|
+
this._path = new Path2D();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
closePath() {
|
|
321
|
+
this._path.closePath();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
moveTo(x, y) {
|
|
325
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
326
|
+
this._path.moveTo(dx, dy);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
lineTo(x, y) {
|
|
330
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
331
|
+
this._path.lineTo(dx, dy);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
bezierCurveTo(x1, y1, x2, y2, x, y) {
|
|
335
|
+
const [dx1, dy1] = matApply(this._m, x1, y1);
|
|
336
|
+
const [dx2, dy2] = matApply(this._m, x2, y2);
|
|
337
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
338
|
+
this._path.bezierCurveTo(dx1, dy1, dx2, dy2, dx, dy);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
quadraticCurveTo(x1, y1, x, y) {
|
|
342
|
+
const [dx1, dy1] = matApply(this._m, x1, y1);
|
|
343
|
+
const [dx, dy] = matApply(this._m, x, y);
|
|
344
|
+
this._path.quadraticCurveTo(dx1, dy1, dx, dy);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// append user-space segments ({start, cmds}) through the current transform
|
|
348
|
+
_appendUserSegments({ start, cmds }) {
|
|
349
|
+
const [dsx, dsy] = matApply(this._m, start.x, start.y);
|
|
350
|
+
if (this._path._x === null) this._path.moveTo(dsx, dsy);
|
|
351
|
+
else this._path.lineTo(dsx, dsy);
|
|
352
|
+
this._path._append(transformCommands(cmds, this._m));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
arc(x, y, r, startAngle, endAngle, counterclockwise = false) {
|
|
356
|
+
if (r < 0) throw new RangeError('arc: negative radius');
|
|
357
|
+
this._appendUserSegments(ellipseSegments(x, y, r, r, 0, startAngle, endAngle, counterclockwise));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
ellipse(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise = false) {
|
|
361
|
+
if (rx < 0 || ry < 0) throw new RangeError('ellipse: negative radius');
|
|
362
|
+
this._appendUserSegments(ellipseSegments(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
arcTo(x1, y1, x2, y2, r) {
|
|
366
|
+
if (this._path._x === null) return this.moveTo(x1, y1);
|
|
367
|
+
const inv = matInvert(this._m);
|
|
368
|
+
if (!inv) return;
|
|
369
|
+
const [ux, uy] = matApply(inv, this._path._x, this._path._y);
|
|
370
|
+
const tmp = new Path2D();
|
|
371
|
+
tmp.moveTo(ux, uy);
|
|
372
|
+
tmp.arcTo(x1, y1, x2, y2, r);
|
|
373
|
+
// drop the seed moveTo: the current point is already there
|
|
374
|
+
this._path._append(transformCommands(tmp._cmds.slice(1), this._m));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
rect(x, y, w, h) {
|
|
378
|
+
const tmp = new Path2D();
|
|
379
|
+
tmp.rect(x, y, w, h);
|
|
380
|
+
this._path.addPath(tmp, this._m);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
roundRect(x, y, w, h, radii) {
|
|
384
|
+
const tmp = new Path2D();
|
|
385
|
+
tmp.roundRect(x, y, w, h, radii);
|
|
386
|
+
this._path.addPath(tmp, this._m);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// args: ([path], [fillRule]) — returns device-space polys + rule
|
|
390
|
+
_polysFor(args) {
|
|
391
|
+
if (args[0] instanceof Path2D) {
|
|
392
|
+
return {
|
|
393
|
+
polys: flattenPath(args[0]._cmds, this._m),
|
|
394
|
+
rule: args[1] === 'evenodd' ? 'evenodd' : 'nonzero'
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
polys: flattenPath(this._path._cmds, null),
|
|
399
|
+
rule: args[0] === 'evenodd' ? 'evenodd' : 'nonzero'
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ------------------------------------------------------------------
|
|
404
|
+
// rasterization plumbing
|
|
405
|
+
|
|
406
|
+
_ensureFillMask() {
|
|
407
|
+
if (this.fillMask) return;
|
|
408
|
+
this.fillMaskDrawable = new Pixmap(this.window.app, {
|
|
409
|
+
depth: 8,
|
|
410
|
+
width: this.width,
|
|
411
|
+
height: this.height
|
|
412
|
+
});
|
|
413
|
+
this.fillMask = new Picture(this.window.app, {
|
|
414
|
+
drawable: this.fillMaskDrawable,
|
|
415
|
+
format: this.Render.a8
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_ensureClipMask() {
|
|
420
|
+
if (this.clipMask) return;
|
|
421
|
+
this.clipMaskDrawable = new Pixmap(this.window.app, {
|
|
422
|
+
depth: 8,
|
|
423
|
+
width: this.width,
|
|
424
|
+
height: this.height
|
|
425
|
+
});
|
|
426
|
+
this.clipMask = new Picture(this.window.app, {
|
|
427
|
+
drawable: this.clipMaskDrawable,
|
|
428
|
+
format: this.Render.a8
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_rasterizePolys(picture, polys, rule) {
|
|
433
|
+
const flat = [];
|
|
434
|
+
for (const p of polys) if (p.pts.length >= 6) flat.push(p.pts);
|
|
435
|
+
if (!flat.length) return;
|
|
436
|
+
const traps = trapezoidize(flat, 0, 0, [], rule);
|
|
437
|
+
// stay under the server's maximum request size
|
|
438
|
+
const chunk = 4000 * 6;
|
|
439
|
+
for (let i = 0; i < traps.length; i += chunk) {
|
|
440
|
+
this.Render.AddTraps(picture.id, 0, 0, traps.slice(i, i + chunk));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Core fill: rasterize device-space polys into the scratch a8 mask,
|
|
446
|
+
* scale by globalAlpha, intersect with the clip, composite the source.
|
|
447
|
+
*/
|
|
448
|
+
_fillPolys(polys, rule, { src = null, op = null, alpha = null } = {}) {
|
|
449
|
+
if (!polys.length) return;
|
|
450
|
+
src = src ?? this._backgroundPicture;
|
|
451
|
+
op = op ?? this._op();
|
|
452
|
+
alpha = alpha ?? this.globalAlpha;
|
|
453
|
+
if (alpha <= 0) return;
|
|
454
|
+
|
|
455
|
+
this._ensureFillMask();
|
|
456
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
457
|
+
this._rasterizePolys(this.fillMask, polys, rule);
|
|
458
|
+
|
|
459
|
+
if (alpha < 1) {
|
|
460
|
+
// In with a constant color scales the a8 coverage by that alpha
|
|
461
|
+
this.Render.FillRectangles(this.Render.PictOp.In, this.fillMask.id, [0, 0, 0, alpha], [0, 0, this.width, this.height]);
|
|
462
|
+
}
|
|
463
|
+
if (this.clipMask) {
|
|
464
|
+
this.Render.Composite(
|
|
465
|
+
this.Render.PictOp.In,
|
|
466
|
+
this.clipMask.id,
|
|
467
|
+
0,
|
|
468
|
+
this.fillMask.id,
|
|
469
|
+
0, 0, 0, 0, 0, 0,
|
|
470
|
+
this.width,
|
|
471
|
+
this.height
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
this.Render.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
475
|
+
this._markDirty();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_strokePolys(polys, { src = null } = {}) {
|
|
479
|
+
src = src ?? this._strokePicture;
|
|
480
|
+
if (this.globalAlpha <= 0) return;
|
|
481
|
+
// approximate transform-aware line width by the average scale factor
|
|
482
|
+
const det = this._m[0] * this._m[3] - this._m[1] * this._m[2];
|
|
483
|
+
const thickness = this.lineWidth * (Math.sqrt(Math.abs(det)) || 1);
|
|
484
|
+
const stroke = extrudePolyline({
|
|
485
|
+
thickness,
|
|
486
|
+
cap: LINE_CAP[this.lineCap] || 'butt',
|
|
487
|
+
join: LINE_JOIN[this.lineJoin] || 'miter',
|
|
488
|
+
miterLimit: this.miterLimit
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const tris = [];
|
|
492
|
+
for (const poly of polys) {
|
|
493
|
+
// drop consecutive (near-)duplicate points — zero-length segments make
|
|
494
|
+
// extrude-polyline emit NaN joins that turn into spikes at the origin
|
|
495
|
+
const pts = [];
|
|
496
|
+
let lx = Infinity;
|
|
497
|
+
let ly = Infinity;
|
|
498
|
+
for (let i = 0; i < poly.pts.length; i += 2) {
|
|
499
|
+
const x = poly.pts[i];
|
|
500
|
+
const y = poly.pts[i + 1];
|
|
501
|
+
if (!(Math.abs(x - lx) > 1e-6 || Math.abs(y - ly) > 1e-6)) continue;
|
|
502
|
+
pts.push([x, y]);
|
|
503
|
+
lx = x;
|
|
504
|
+
ly = y;
|
|
505
|
+
}
|
|
506
|
+
if (pts.length >= 2 && poly.closed) {
|
|
507
|
+
const [fx, fy] = pts[0];
|
|
508
|
+
if (Math.abs(fx - lx) > 1e-6 || Math.abs(fy - ly) > 1e-6) pts.push([fx, fy]);
|
|
509
|
+
}
|
|
510
|
+
if (pts.length < 2) continue;
|
|
511
|
+
const mesh = stroke.build(pts);
|
|
512
|
+
for (const tri of mesh.cells) {
|
|
513
|
+
for (let i = 0; i < 3; ++i) {
|
|
514
|
+
tris.push(mesh.positions[tri[i]][0], mesh.positions[tri[i]][1]);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!tris.length) return;
|
|
519
|
+
|
|
520
|
+
const op = this._op();
|
|
521
|
+
const direct = this.globalAlpha >= 1 && !this.clipMask && op === this.Render.PictOp.Over;
|
|
522
|
+
const chunk = 4000 * 6;
|
|
523
|
+
if (direct) {
|
|
524
|
+
for (let i = 0; i < tris.length; i += chunk) {
|
|
525
|
+
this.Render.Triangles(op, src.id, 0, 0, this.picture.id, this.Render.a8, tris.slice(i, i + chunk));
|
|
526
|
+
}
|
|
527
|
+
this._markDirty();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// render coverage into the scratch mask, then composite through it so
|
|
532
|
+
// the stroke honors clip / globalAlpha / composite op
|
|
533
|
+
this._ensureFillMask();
|
|
534
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
535
|
+
const opaque = this.createSolidPicture(0, 0, 0, 1);
|
|
536
|
+
for (let i = 0; i < tris.length; i += chunk) {
|
|
537
|
+
this.Render.Triangles(this.Render.PictOp.Add, opaque.id, 0, 0, this.fillMask.id, this.Render.a8, tris.slice(i, i + chunk));
|
|
538
|
+
}
|
|
539
|
+
if (this.globalAlpha < 1) {
|
|
540
|
+
this.Render.FillRectangles(this.Render.PictOp.In, this.fillMask.id, [0, 0, 0, this.globalAlpha], [0, 0, this.width, this.height]);
|
|
541
|
+
}
|
|
542
|
+
if (this.clipMask) {
|
|
543
|
+
this.Render.Composite(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
544
|
+
}
|
|
545
|
+
this.Render.Composite(op, src.id, this.fillMask.id, this.picture.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
546
|
+
this._markDirty();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// combined clip ∩ globalAlpha mask for direct composites (rect/image
|
|
550
|
+
// fast paths); returns a picture id or 0. Reuses the fill scratch mask.
|
|
551
|
+
_compositeMask() {
|
|
552
|
+
if (this.globalAlpha >= 1) return this.clipMask ? this.clipMask.id : 0;
|
|
553
|
+
this._ensureFillMask();
|
|
554
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.fillMask.id, [0, 0, 0, this.globalAlpha], [0, 0, this.width, this.height]);
|
|
555
|
+
if (this.clipMask) {
|
|
556
|
+
this.Render.Composite(this.Render.PictOp.In, this.clipMask.id, 0, this.fillMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
557
|
+
}
|
|
558
|
+
return this.fillMask.id;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ------------------------------------------------------------------
|
|
562
|
+
// drawing
|
|
563
|
+
|
|
564
|
+
clearRect(x, y, w, h) {
|
|
565
|
+
if (matIsIdentity(this._m) && !this.clipMask) {
|
|
566
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.picture.id, [1, 1, 1, 1], [x, y, w, h]);
|
|
567
|
+
this._markDirty();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const tmp = new Path2D();
|
|
571
|
+
tmp.rect(x, y, w, h);
|
|
572
|
+
// clear = opaque white, ignoring alpha and the composite op (but
|
|
573
|
+
// honoring the clip), matching the identity fast path above
|
|
574
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), 'nonzero', {
|
|
575
|
+
src: this.createSolidPicture(1, 1, 1, 1),
|
|
576
|
+
op: this.Render.PictOp.Over,
|
|
577
|
+
alpha: 1
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
fillRect(x, y, w, h) {
|
|
582
|
+
if (matIsIdentity(this._m)) {
|
|
583
|
+
const mask = this._compositeMask();
|
|
584
|
+
this.Render.Composite(
|
|
585
|
+
this._op(),
|
|
586
|
+
this._backgroundPicture.id,
|
|
587
|
+
mask,
|
|
588
|
+
this.picture.id,
|
|
589
|
+
x, y,
|
|
590
|
+
x, y,
|
|
591
|
+
x, y,
|
|
592
|
+
w, h
|
|
593
|
+
);
|
|
594
|
+
this._markDirty();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const tmp = new Path2D();
|
|
598
|
+
tmp.rect(x, y, w, h);
|
|
599
|
+
this._fillPolys(flattenPath(tmp._cmds, this._m), 'nonzero');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
strokeRect(x, y, w, h) {
|
|
603
|
+
const tmp = new Path2D();
|
|
604
|
+
tmp.rect(x, y, w, h);
|
|
605
|
+
this._strokePolys(flattenPath(tmp._cmds, this._m));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
fill(...args) {
|
|
609
|
+
const { polys, rule } = this._polysFor(args);
|
|
610
|
+
this._fillPolys(polys, rule);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
stroke(...args) {
|
|
614
|
+
const polys =
|
|
615
|
+
args[0] instanceof Path2D
|
|
616
|
+
? flattenPath(args[0]._cmds, this._m)
|
|
617
|
+
: flattenPath(this._path._cmds, null);
|
|
618
|
+
this._strokePolys(polys);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Intersect the clip region with the given path (or the current path).
|
|
623
|
+
* Cleared by restore() to the state at the matching save() — like the
|
|
624
|
+
* browser canvas, there is no other way to widen the clip again.
|
|
625
|
+
*/
|
|
626
|
+
clip(...args) {
|
|
627
|
+
const { polys, rule } = this._polysFor(args);
|
|
628
|
+
const entry = { polys, rule };
|
|
629
|
+
// copy-on-write: earlier save() snapshots keep their own clip list
|
|
630
|
+
this._clips = this._clips.concat([entry]);
|
|
631
|
+
|
|
632
|
+
if (this._clips.length === 1) {
|
|
633
|
+
this._ensureClipMask();
|
|
634
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
635
|
+
this._rasterizePolys(this.clipMask, polys, rule);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
this._intersectClip(entry);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// rasterize one clip entry into a temp a8 and In-composite it onto the mask
|
|
642
|
+
_intersectClip(entry) {
|
|
643
|
+
const tmpPixmap = new Pixmap(this.window.app, { depth: 8, width: this.width, height: this.height });
|
|
644
|
+
const tmpMask = new Picture(this.window.app, { drawable: tmpPixmap, format: this.Render.a8 });
|
|
645
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, tmpMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
646
|
+
this._rasterizePolys(tmpMask, entry.polys, entry.rule);
|
|
647
|
+
this.Render.Composite(this.Render.PictOp.In, tmpMask.id, 0, this.clipMask.id, 0, 0, 0, 0, 0, 0, this.width, this.height);
|
|
648
|
+
tmpMask.destroy();
|
|
649
|
+
tmpPixmap.destroy();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
_rebuildClipMask() {
|
|
653
|
+
if (!this._clips.length) {
|
|
654
|
+
if (this.clipMask) {
|
|
655
|
+
this.clipMask.destroy();
|
|
656
|
+
this.clipMaskDrawable.destroy();
|
|
657
|
+
this.clipMask = this.clipMaskDrawable = null;
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this._ensureClipMask();
|
|
662
|
+
this.Render.FillRectangles(this.Render.PictOp.Src, this.clipMask.id, [0, 0, 0, 0], [0, 0, this.width, this.height]);
|
|
663
|
+
this._rasterizePolys(this.clipMask, this._clips[0].polys, this._clips[0].rule);
|
|
664
|
+
for (let i = 1; i < this._clips.length; i++) this._intersectClip(this._clips[i]);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
isPointInPath(...args) {
|
|
668
|
+
let path = null;
|
|
669
|
+
let rest = args;
|
|
670
|
+
if (args[0] instanceof Path2D) {
|
|
671
|
+
path = args[0];
|
|
672
|
+
rest = args.slice(1);
|
|
673
|
+
}
|
|
674
|
+
const [x, y, rule] = rest;
|
|
675
|
+
const polys = path ? flattenPath(path._cmds, this._m) : flattenPath(this._path._cmds, null);
|
|
676
|
+
return polysContain(polys, x, y, rule === 'evenodd' ? 'evenodd' : 'nonzero');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ------------------------------------------------------------------
|
|
680
|
+
// text
|
|
681
|
+
|
|
682
|
+
_resolvedTextStyle() {
|
|
683
|
+
if (!this._textStyle) this.font = DEFAULT_FONT;
|
|
684
|
+
return this._textStyle;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// horizontal offset of the alignment point, given the shaped width
|
|
688
|
+
_alignOffset(shaped) {
|
|
689
|
+
let align = this.textAlign;
|
|
690
|
+
if (align === 'start') align = shaped.baseLevel & 1 ? 'right' : 'left';
|
|
691
|
+
if (align === 'end') align = shaped.baseLevel & 1 ? 'left' : 'right';
|
|
692
|
+
if (align === 'center') return -shaped.width / 2;
|
|
693
|
+
if (align === 'right') return -shaped.width;
|
|
694
|
+
return 0;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// vertical distance from the requested y to the baseline
|
|
698
|
+
_baselineOffset(metrics) {
|
|
699
|
+
switch (this.textBaseline) {
|
|
700
|
+
case 'top':
|
|
701
|
+
return metrics.ascent;
|
|
702
|
+
case 'hanging':
|
|
703
|
+
return metrics.ascent * 0.8;
|
|
704
|
+
case 'middle':
|
|
705
|
+
return (metrics.ascent - metrics.descent) / 2;
|
|
706
|
+
case 'bottom':
|
|
707
|
+
case 'ideographic':
|
|
708
|
+
return -metrics.descent;
|
|
709
|
+
default:
|
|
710
|
+
// 'alphabetic'
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Draw text with full shaping: OpenType kerning/ligatures, complex-script
|
|
717
|
+
* contextual forms, bidi reordering and automatic font fallback.
|
|
718
|
+
* Glyphs are uploaded to the server once per (face, size) and referenced
|
|
719
|
+
* by 1–2 byte ids afterwards — see docs/text.md.
|
|
720
|
+
*
|
|
721
|
+
* The current transform's translation applies to the anchor point;
|
|
722
|
+
* glyphs themselves are not rotated/scaled by the transform (size the
|
|
723
|
+
* font via `ctx.font` instead).
|
|
724
|
+
*/
|
|
725
|
+
fillText(text, x, y) {
|
|
726
|
+
text = String(text ?? '');
|
|
727
|
+
if (!text) return;
|
|
728
|
+
const style = this._resolvedTextStyle();
|
|
729
|
+
const app = this.window.app;
|
|
730
|
+
const shaped = app.fonts.shape(text, style);
|
|
731
|
+
const [tx, ty] = matApply(this._m, x, y);
|
|
732
|
+
const ox = tx + this._alignOffset(shaped);
|
|
733
|
+
const oy = ty + this._baselineOffset(style.font.metrics(style.size));
|
|
734
|
+
|
|
735
|
+
const positioned = [];
|
|
736
|
+
let cursor = ox;
|
|
737
|
+
for (const run of reorderRuns(shaped.runs)) {
|
|
738
|
+
positioned.push({ run, x: cursor, y: oy });
|
|
739
|
+
cursor += run.width;
|
|
740
|
+
}
|
|
741
|
+
drawGlyphRuns(app, this.Render.PictOp.Over, this._backgroundPicture.id, this.picture.id, positioned);
|
|
742
|
+
this._markDirty();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Measure shaped text. Returns a canvas-style TextMetrics object:
|
|
747
|
+
* `width` (advance), actual bounding box (ink extents relative to the
|
|
748
|
+
* origin), and font bounding box (from font metrics).
|
|
749
|
+
*/
|
|
750
|
+
measureText(text) {
|
|
751
|
+
const style = this._resolvedTextStyle();
|
|
752
|
+
const shaped = this.window.app.fonts.shape(String(text ?? ''), style);
|
|
753
|
+
let minX = 0;
|
|
754
|
+
let maxX = 0;
|
|
755
|
+
let minY = 0;
|
|
756
|
+
let maxY = 0;
|
|
757
|
+
let cursor = 0;
|
|
758
|
+
for (const run of reorderRuns(shaped.runs)) {
|
|
759
|
+
for (const g of run.glyphs) {
|
|
760
|
+
const e = run.font.glyphExtents(g.id, run.size);
|
|
761
|
+
const gx = cursor + g.dx;
|
|
762
|
+
const gy = -g.dy;
|
|
763
|
+
if (gx + e.minX < minX) minX = gx + e.minX;
|
|
764
|
+
if (gx + e.maxX > maxX) maxX = gx + e.maxX;
|
|
765
|
+
if (gy + e.minY < minY) minY = gy + e.minY;
|
|
766
|
+
if (gy + e.maxY > maxY) maxY = gy + e.maxY;
|
|
767
|
+
cursor += g.ax;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const m = style.font.metrics(style.size);
|
|
771
|
+
return {
|
|
772
|
+
width: shaped.width,
|
|
773
|
+
actualBoundingBoxLeft: -minX,
|
|
774
|
+
actualBoundingBoxRight: maxX,
|
|
775
|
+
actualBoundingBoxAscent: -minY,
|
|
776
|
+
actualBoundingBoxDescent: maxY,
|
|
777
|
+
fontBoundingBoxAscent: m.ascent,
|
|
778
|
+
fontBoundingBoxDescent: m.descent,
|
|
779
|
+
emHeightAscent: m.ascent,
|
|
780
|
+
emHeightDescent: m.descent,
|
|
781
|
+
// legacy ntk field: ink height
|
|
782
|
+
height: maxY - minY
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Lay out (possibly styled) text for a target width without drawing it —
|
|
788
|
+
* returns a TextLayout (lines, metrics) that can be inspected and then
|
|
789
|
+
* drawn with `layout.draw(ctx, x, y)`. The current `ctx.font` is the base
|
|
790
|
+
* style; see docs/text.md for spans and options.
|
|
791
|
+
*
|
|
792
|
+
* @param {string|Array} content plain text or spans [{ text, ...style }]
|
|
793
|
+
* @param {object} [options] { maxWidth, align, lineHeight, direction }
|
|
794
|
+
*/
|
|
795
|
+
layoutText(content, options = {}) {
|
|
796
|
+
const style = this._resolvedTextStyle();
|
|
797
|
+
return new TextLayout(this.window.app.fonts, content, style, options);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* CSS-style font shorthand, e.g. `'bold italic 40px "DejaVu Sans", serif'`.
|
|
802
|
+
* Resolution goes through `app.fonts`: fonts registered with
|
|
803
|
+
* `app.fonts.load()` win over system (fontconfig) lookup.
|
|
804
|
+
*/
|
|
805
|
+
set font(val) {
|
|
806
|
+
if (!val || typeof val !== 'string') return;
|
|
807
|
+
const parsed = parseFontStyle(val);
|
|
808
|
+
if (!parsed) return;
|
|
809
|
+
const style = {
|
|
810
|
+
family: parsed.family,
|
|
811
|
+
weight: parsed.weight,
|
|
812
|
+
style: parsed.style,
|
|
813
|
+
size: parsed.size
|
|
814
|
+
};
|
|
815
|
+
style.font = this.window.app.fonts.match(style.family, style);
|
|
816
|
+
this._lastFontString = val;
|
|
817
|
+
this._textStyle = style;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
get font() {
|
|
821
|
+
return this._lastFontString || DEFAULT_FONT;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ------------------------------------------------------------------
|
|
825
|
+
// gradients / images
|
|
826
|
+
|
|
827
|
+
createLinearGradient(x0, y0, x1, y1) {
|
|
828
|
+
return new CanvasGradient('linear', this, x0, y0, x1, y1);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
createRadialGradient(x0, y0, r0, x1, y1, r1) {
|
|
832
|
+
return new CanvasGradient('radial', this, x0, y0, x1, y1, r0, r1);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
createConicalGradient(x0, y0, angle) {
|
|
836
|
+
return new CanvasGradient('conical', this, x0, y0, angle);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
createImageData(w, h) {
|
|
840
|
+
return {
|
|
841
|
+
width: w,
|
|
842
|
+
height: h,
|
|
843
|
+
data: Buffer.alloc(w * h * 4)
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// TODO: remove, add drawImage
|
|
848
|
+
putImageData(data, x, y) {
|
|
849
|
+
// todo: use constants
|
|
850
|
+
this.X.PutImage(2, this._target.id, this._gc, data.width, data.height, x, y, 0, 24, data.data);
|
|
851
|
+
this._markDirty();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
getImageData(x, y, w, h, cb) {
|
|
855
|
+
// reads the backing pixmap on double-buffered windows: always valid,
|
|
856
|
+
// even where the window is occluded on screen
|
|
857
|
+
this.X.GetImage(2, this._target.id, x, y, w, h, 0xffffffff, cb);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Draw an image, with the three canvas signatures:
|
|
862
|
+
*
|
|
863
|
+
* ctx.drawImage(image, dx, dy)
|
|
864
|
+
* ctx.drawImage(image, dx, dy, dWidth, dHeight)
|
|
865
|
+
* ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
|
|
866
|
+
*
|
|
867
|
+
* `image` is an ntk `Image` (see `loadImage()` — PNG/JPEG decoded
|
|
868
|
+
* client-side, uploaded to the server once and composited from there;
|
|
869
|
+
* scaling is server-side with bilinear filtering), another
|
|
870
|
+
* RenderingContext2d, or a node-canvas-like object. Images honor the
|
|
871
|
+
* current transform (server-side, via the picture transform).
|
|
872
|
+
*/
|
|
873
|
+
drawImage(image, ...args) {
|
|
874
|
+
if (image instanceof Image) {
|
|
875
|
+
let sx = 0;
|
|
876
|
+
let sy = 0;
|
|
877
|
+
let sw = image.width;
|
|
878
|
+
let sh = image.height;
|
|
879
|
+
let dx = 0;
|
|
880
|
+
let dy = 0;
|
|
881
|
+
let dw;
|
|
882
|
+
let dh;
|
|
883
|
+
if (args.length >= 8) {
|
|
884
|
+
[sx, sy, sw, sh, dx, dy, dw, dh] = args;
|
|
885
|
+
} else if (args.length >= 4) {
|
|
886
|
+
[dx, dy, dw, dh] = args;
|
|
887
|
+
} else {
|
|
888
|
+
[dx = 0, dy = 0] = args;
|
|
889
|
+
dw = sw;
|
|
890
|
+
dh = sh;
|
|
891
|
+
}
|
|
892
|
+
if (!(dw > 0) || !(dh > 0) || !(sw > 0) || !(sh > 0)) return;
|
|
893
|
+
|
|
894
|
+
const picture = image.picture(this.window.app);
|
|
895
|
+
const op = this._op();
|
|
896
|
+
|
|
897
|
+
if (!matIsIdentity(this._m)) {
|
|
898
|
+
this._drawImageTransformed(picture, sx, sy, sw, sh, dx, dy, dw, dh, op);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const mask = this._compositeMask();
|
|
903
|
+
const scaled = dw !== sw || dh !== sh;
|
|
904
|
+
if (scaled) {
|
|
905
|
+
// the picture transform maps composite coordinates into source
|
|
906
|
+
// samples: dest pixel (i, j) reads (sx + i*sw/dw, sy + j*sh/dh)
|
|
907
|
+
this.Render.SetPictureTransform(picture.id, [sw / dw, 0, sx, 0, sh / dh, sy, 0, 0, 1]);
|
|
908
|
+
picture.setFilter('bilinear');
|
|
909
|
+
this.Render.Composite(
|
|
910
|
+
op,
|
|
911
|
+
picture.id,
|
|
912
|
+
mask,
|
|
913
|
+
this.picture.id,
|
|
914
|
+
0, 0,
|
|
915
|
+
dx, dy,
|
|
916
|
+
dx, dy,
|
|
917
|
+
dw, dh
|
|
918
|
+
);
|
|
919
|
+
// restore defaults so the cached upload can be reused as-is
|
|
920
|
+
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
921
|
+
picture.setFilter('nearest');
|
|
922
|
+
} else {
|
|
923
|
+
this.Render.Composite(
|
|
924
|
+
op,
|
|
925
|
+
picture.id,
|
|
926
|
+
mask,
|
|
927
|
+
this.picture.id,
|
|
928
|
+
sx, sy,
|
|
929
|
+
dx, dy,
|
|
930
|
+
dx, dy,
|
|
931
|
+
dw, dh
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
this._markDirty();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const sWidth = image.width;
|
|
939
|
+
const sHeight = image.height;
|
|
940
|
+
const sx = 0;
|
|
941
|
+
const sy = 0;
|
|
942
|
+
|
|
943
|
+
// TODO: allow to draw Window, Pixmap, Picture
|
|
944
|
+
if (image instanceof RenderingContext2d) {
|
|
945
|
+
// TODO: if need to scale, set transform
|
|
946
|
+
// TODO: respect global compositing blend mode
|
|
947
|
+
this.Render.Composite(
|
|
948
|
+
this.Render.PictOp.Over,
|
|
949
|
+
image.picture.id,
|
|
950
|
+
this._compositeMask(),
|
|
951
|
+
this.picture.id,
|
|
952
|
+
0, 0, 0, 0, 0, 0,
|
|
953
|
+
this.width,
|
|
954
|
+
this.height
|
|
955
|
+
);
|
|
956
|
+
this._markDirty();
|
|
957
|
+
} else if (image && image.context && typeof image.context.getImageData === 'function') {
|
|
958
|
+
// node-canvas Canvas ( or any ctx with ctx.canvas.getImageData returning pixels)
|
|
959
|
+
const imageData = image.context.getImageData(sx, sy, sWidth, sHeight);
|
|
960
|
+
// rgba -> bgra
|
|
961
|
+
const data = Buffer.alloc(imageData.data.length);
|
|
962
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
963
|
+
data[i + 2] = imageData.data[i];
|
|
964
|
+
data[i + 1] = imageData.data[i + 1];
|
|
965
|
+
data[i] = imageData.data[i + 2];
|
|
966
|
+
data[i + 3] = imageData.data[i + 3]; // multiply source alpha by context globalAlpha?
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const pixmap = new Pixmap(this.window.app, { depth: 32, width: sWidth, height: sHeight });
|
|
970
|
+
const picture = new Picture(this.window.app, { drawable: pixmap, format: this.Render.rgba32 });
|
|
971
|
+
pixmap._gc = this.X.AllocID();
|
|
972
|
+
this.X.CreateGC(pixmap._gc, pixmap.id);
|
|
973
|
+
this.X.PutImage(2, pixmap.id, pixmap._gc, sWidth, sHeight, sx, sy, 0, 32, data);
|
|
974
|
+
|
|
975
|
+
this.Render.Composite(
|
|
976
|
+
this.Render.PictOp.Over,
|
|
977
|
+
picture.id,
|
|
978
|
+
this._compositeMask(),
|
|
979
|
+
this.picture.id,
|
|
980
|
+
0, 0, 0, 0, 0, 0,
|
|
981
|
+
this.width,
|
|
982
|
+
this.height
|
|
983
|
+
);
|
|
984
|
+
this._markDirty();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// general-affine drawImage: fold CTM + dest rect + source offset into the
|
|
989
|
+
// source picture transform and composite over the transformed bbox
|
|
990
|
+
_drawImageTransformed(picture, sx, sy, sw, sh, dx, dy, dw, dh, op) {
|
|
991
|
+
// maps source-local (u, v) in [0..sw)x[0..sh) to device coordinates
|
|
992
|
+
const M = matMultiply(this._m, [dw / sw, 0, 0, dh / sh, dx, dy]);
|
|
993
|
+
const inv = matInvert(M);
|
|
994
|
+
if (!inv) return;
|
|
995
|
+
|
|
996
|
+
// device bbox of the transformed dest rect, clamped to the canvas
|
|
997
|
+
const corners = [
|
|
998
|
+
matApply(this._m, dx, dy),
|
|
999
|
+
matApply(this._m, dx + dw, dy),
|
|
1000
|
+
matApply(this._m, dx, dy + dh),
|
|
1001
|
+
matApply(this._m, dx + dw, dy + dh)
|
|
1002
|
+
];
|
|
1003
|
+
const x0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[0]))));
|
|
1004
|
+
const y0 = Math.max(0, Math.floor(Math.min(...corners.map((c) => c[1]))));
|
|
1005
|
+
const x1 = Math.min(this.width, Math.ceil(Math.max(...corners.map((c) => c[0]))));
|
|
1006
|
+
const y1 = Math.min(this.height, Math.ceil(Math.max(...corners.map((c) => c[1]))));
|
|
1007
|
+
if (x1 <= x0 || y1 <= y0) return;
|
|
1008
|
+
|
|
1009
|
+
// device -> source sample (adding the source-rect origin)
|
|
1010
|
+
const srcM = [inv[0], inv[1], inv[2], inv[3], inv[4] + sx, inv[5] + sy];
|
|
1011
|
+
this.Render.SetPictureTransform(picture.id, [srcM[0], srcM[2], srcM[4], srcM[1], srcM[3], srcM[5], 0, 0, 1]);
|
|
1012
|
+
picture.setFilter('bilinear');
|
|
1013
|
+
const mask = this._compositeMask();
|
|
1014
|
+
this.Render.Composite(op, picture.id, mask, this.picture.id, x0, y0, x0, y0, x0, y0, x1 - x0, y1 - y0);
|
|
1015
|
+
this.Render.SetPictureTransform(picture.id, [1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
1016
|
+
picture.setFilter('nearest');
|
|
1017
|
+
this._markDirty();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
class CanvasGradient {
|
|
1022
|
+
constructor(type, ctx, p0, p1, p2, p3, p4, p5) {
|
|
1023
|
+
this.type = type;
|
|
1024
|
+
this.stops = [];
|
|
1025
|
+
this.ctx = ctx;
|
|
1026
|
+
this._id = null;
|
|
1027
|
+
this._picture = null;
|
|
1028
|
+
|
|
1029
|
+
this.x0 = p0;
|
|
1030
|
+
this.y0 = p1;
|
|
1031
|
+
if (type === 'linear' || type === 'radial') {
|
|
1032
|
+
this.x1 = p2;
|
|
1033
|
+
this.y1 = p3;
|
|
1034
|
+
this.r0 = p4;
|
|
1035
|
+
this.r1 = p5;
|
|
1036
|
+
} else {
|
|
1037
|
+
this.angle = p2;
|
|
1038
|
+
}
|
|
1039
|
+
// TODO: check if ChangePictureAttribute works on gradients ( set repeat edge pixels flag )
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
addColorStop(offset, color) {
|
|
1043
|
+
this.stops.push([offset, parseColor(color)]);
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// gradient pictures are created lazily, on first use as a fill/stroke style
|
|
1048
|
+
get id() {
|
|
1049
|
+
if (this._id !== null) return this._id;
|
|
1050
|
+
|
|
1051
|
+
const Render = this.ctx.Render;
|
|
1052
|
+
const X = this.ctx.X;
|
|
1053
|
+
|
|
1054
|
+
this._id = X.AllocID();
|
|
1055
|
+
switch (this.type) {
|
|
1056
|
+
case 'linear':
|
|
1057
|
+
Render.LinearGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.stops);
|
|
1058
|
+
break;
|
|
1059
|
+
case 'radial':
|
|
1060
|
+
Render.RadialGradient(this._id, [this.x0, this.y0], [this.x1, this.y1], this.r0, this.r1, this.stops);
|
|
1061
|
+
break;
|
|
1062
|
+
case 'conical':
|
|
1063
|
+
Render.ConicalGradient(this._id, [this.x0, this.y0], this.angle, this.stops);
|
|
1064
|
+
break;
|
|
1065
|
+
default:
|
|
1066
|
+
throw new Error('unknown gradient type');
|
|
1067
|
+
}
|
|
1068
|
+
// wrap with picture so FreePicture is invoked on gc via FinalizationRegistry
|
|
1069
|
+
this._picture = new Picture(this.ctx.window.app, { id: this._id });
|
|
1070
|
+
return this._id;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// register context
|
|
1075
|
+
Drawable.renderingContextFactory['2d'] = (window) => new RenderingContext2d(window);
|
|
1076
|
+
|
|
1077
|
+
export default RenderingContext2d;
|
|
1078
|
+
export { CanvasGradient };
|