@gjsify/canvas2d 0.1.2 → 0.1.3

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.
@@ -1,865 +1,4 @@
1
- import Cairo from "cairo";
2
- import Gdk from "gi://Gdk?version=4.0";
3
- import GdkPixbuf from "gi://GdkPixbuf";
4
- import Pango from "gi://Pango";
5
- import PangoCairo from "gi://PangoCairo";
6
- import { parseColor } from "./color.js";
7
- import {
8
- quadraticToCubic,
9
- cairoArcTo,
10
- cairoEllipse,
11
- cairoRoundRect,
12
- COMPOSITE_OP_MAP,
13
- LINE_CAP_MAP,
14
- LINE_JOIN_MAP
15
- } from "./cairo-utils.js";
16
- import { createDefaultState, cloneState } from "./canvas-state.js";
17
- import { OurImageData } from "./image-data.js";
18
- import { CanvasGradient as OurCanvasGradient } from "./canvas-gradient.js";
19
- import { CanvasPattern as OurCanvasPattern } from "./canvas-pattern.js";
20
- import { Path2D } from "./canvas-path.js";
21
- class CanvasRenderingContext2D {
22
- constructor(canvas, _options) {
23
- this._stateStack = [];
24
- this.canvas = canvas;
25
- this._surfaceWidth = canvas.width || 300;
26
- this._surfaceHeight = canvas.height || 150;
27
- this._surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, this._surfaceWidth, this._surfaceHeight);
28
- this._ctx = new Cairo.Context(this._surface);
29
- this._state = createDefaultState();
30
- }
31
- // ---- Internal helpers ----
32
- /** Ensure the surface matches the current canvas dimensions. Recreate if resized. */
33
- _ensureSurface() {
34
- const w = this.canvas.width || 300;
35
- const h = this.canvas.height || 150;
36
- if (w !== this._surfaceWidth || h !== this._surfaceHeight) {
37
- this._ctx.$dispose();
38
- this._surface.finish();
39
- this._surfaceWidth = w;
40
- this._surfaceHeight = h;
41
- this._surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
42
- this._ctx = new Cairo.Context(this._surface);
43
- this._state = createDefaultState();
44
- this._stateStack = [];
45
- }
46
- }
47
- /** Apply the current fill style (color, gradient, or pattern) to the Cairo context. */
48
- _applyFillStyle() {
49
- const style = this._state.fillStyle;
50
- if (typeof style === "string") {
51
- const c = this._state.fillColor;
52
- const a = c.a * this._state.globalAlpha;
53
- this._ctx.setSourceRGBA(c.r, c.g, c.b, a);
54
- } else if (style instanceof OurCanvasGradient) {
55
- this._ctx.setSource(style._getCairoPattern());
56
- } else if (style instanceof OurCanvasPattern) {
57
- this._ctx.setSource(style._getCairoPattern());
58
- }
59
- }
60
- /** Apply the current stroke style to the Cairo context. */
61
- _applyStrokeStyle() {
62
- const style = this._state.strokeStyle;
63
- if (typeof style === "string") {
64
- const c = this._state.strokeColor;
65
- const a = c.a * this._state.globalAlpha;
66
- this._ctx.setSourceRGBA(c.r, c.g, c.b, a);
67
- } else if (style instanceof OurCanvasGradient) {
68
- this._ctx.setSource(style._getCairoPattern());
69
- } else if (style instanceof OurCanvasPattern) {
70
- this._ctx.setSource(style._getCairoPattern());
71
- }
72
- }
73
- /** Apply line properties to the Cairo context. */
74
- _applyLineStyle() {
75
- this._ctx.setLineWidth(this._state.lineWidth);
76
- this._ctx.setLineCap(LINE_CAP_MAP[this._state.lineCap]);
77
- this._ctx.setLineJoin(LINE_JOIN_MAP[this._state.lineJoin]);
78
- this._ctx.setMiterLimit(this._state.miterLimit);
79
- this._ctx.setDash(this._state.lineDash, this._state.lineDashOffset);
80
- }
81
- /** Apply compositing operator. */
82
- _applyCompositing() {
83
- const op = COMPOSITE_OP_MAP[this._state.globalCompositeOperation];
84
- if (op !== void 0) {
85
- this._ctx.setOperator(op);
86
- }
87
- }
88
- /** Get the Cairo ImageSurface (used by other contexts like drawImage). */
89
- _getSurface() {
90
- return this._surface;
91
- }
92
- /** Check if shadow rendering is needed. */
93
- _hasShadow() {
94
- if (this._state.shadowBlur === 0 && this._state.shadowOffsetX === 0 && this._state.shadowOffsetY === 0) {
95
- return false;
96
- }
97
- const c = parseColor(this._state.shadowColor);
98
- return c !== null && c.a > 0;
99
- }
100
- /**
101
- * Render a shadow for the current path by painting to a temp surface,
102
- * applying a simple box blur approximation, and compositing back.
103
- * This is called before the actual fill/stroke when shadows are active.
104
- */
105
- _renderShadow(drawOp) {
106
- const blur = this._state.shadowBlur;
107
- const offX = this._state.shadowOffsetX;
108
- const offY = this._state.shadowOffsetY;
109
- const color = parseColor(this._state.shadowColor);
110
- if (!color) return;
111
- const pad = Math.ceil(blur * 2);
112
- const w = this._surfaceWidth + pad * 2;
113
- const h = this._surfaceHeight + pad * 2;
114
- const shadowSurface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
115
- const shadowCtx = new Cairo.Context(shadowSurface);
116
- shadowCtx.translate(pad, pad);
117
- shadowCtx.setSourceRGBA(color.r, color.g, color.b, color.a * this._state.globalAlpha);
118
- drawOp.call(this);
119
- shadowCtx.$dispose();
120
- shadowSurface.finish();
121
- this._ctx.save();
122
- this._applyCompositing();
123
- this._ctx.setSourceRGBA(color.r, color.g, color.b, color.a * this._state.globalAlpha);
124
- this._ctx.translate(offX, offY);
125
- drawOp();
126
- this._ctx.restore();
127
- }
128
- // ---- State ----
129
- save() {
130
- this._ensureSurface();
131
- this._stateStack.push(cloneState(this._state));
132
- this._ctx.save();
133
- }
134
- restore() {
135
- this._ensureSurface();
136
- const prev = this._stateStack.pop();
137
- if (prev) {
138
- this._state = prev;
139
- this._ctx.restore();
140
- }
141
- }
142
- // ---- Transforms ----
143
- translate(x, y) {
144
- this._ensureSurface();
145
- this._ctx.translate(x, y);
146
- }
147
- rotate(angle) {
148
- this._ensureSurface();
149
- this._ctx.rotate(angle);
150
- }
151
- scale(x, y) {
152
- this._ensureSurface();
153
- this._ctx.scale(x, y);
154
- }
155
- /**
156
- * Multiply the current transformation matrix by the given values.
157
- * Matrix: [a c e]
158
- * [b d f]
159
- * [0 0 1]
160
- */
161
- transform(a, b, c, d, e, f) {
162
- this._ensureSurface();
163
- const matrix = new Cairo.Matrix();
164
- matrix.init(a, b, c, d, e, f);
165
- this._ctx.transform(matrix);
166
- }
167
- setTransform(a, b, c, d, e, f) {
168
- this._ensureSurface();
169
- if (typeof a === "object" && a !== null) {
170
- const m = a;
171
- this._ctx.identityMatrix();
172
- this.transform(
173
- m.a ?? m.m11 ?? 1,
174
- m.b ?? m.m12 ?? 0,
175
- m.c ?? m.m21 ?? 0,
176
- m.d ?? m.m22 ?? 1,
177
- m.e ?? m.m41 ?? 0,
178
- m.f ?? m.m42 ?? 0
179
- );
180
- } else if (typeof a === "number") {
181
- this._ctx.identityMatrix();
182
- this.transform(a, b, c, d, e, f);
183
- } else {
184
- this._ctx.identityMatrix();
185
- }
186
- }
187
- /**
188
- * Return the current transformation matrix as a DOMMatrix-like object.
189
- */
190
- getTransform() {
191
- const m = this._ctx.getMatrix?.();
192
- if (m) {
193
- return {
194
- a: m.xx ?? 1,
195
- b: m.yx ?? 0,
196
- c: m.xy ?? 0,
197
- d: m.yy ?? 1,
198
- e: m.x0 ?? 0,
199
- f: m.y0 ?? 0,
200
- m11: m.xx ?? 1,
201
- m12: m.yx ?? 0,
202
- m13: 0,
203
- m14: 0,
204
- m21: m.xy ?? 0,
205
- m22: m.yy ?? 1,
206
- m23: 0,
207
- m24: 0,
208
- m31: 0,
209
- m32: 0,
210
- m33: 1,
211
- m34: 0,
212
- m41: m.x0 ?? 0,
213
- m42: m.y0 ?? 0,
214
- m43: 0,
215
- m44: 1,
216
- is2D: true,
217
- isIdentity: m.xx === 1 && m.yx === 0 && m.xy === 0 && m.yy === 1 && m.x0 === 0 && m.y0 === 0
218
- };
219
- }
220
- return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0, is2D: true, isIdentity: true };
221
- }
222
- resetTransform() {
223
- this._ensureSurface();
224
- this._ctx.identityMatrix();
225
- }
226
- // ---- Style properties ----
227
- get fillStyle() {
228
- return this._state.fillStyle;
229
- }
230
- set fillStyle(value) {
231
- if (typeof value === "string") {
232
- const parsed = parseColor(value);
233
- if (parsed) {
234
- this._state.fillStyle = value;
235
- this._state.fillColor = parsed;
236
- }
237
- } else {
238
- this._state.fillStyle = value;
239
- }
240
- }
241
- get strokeStyle() {
242
- return this._state.strokeStyle;
243
- }
244
- set strokeStyle(value) {
245
- if (typeof value === "string") {
246
- const parsed = parseColor(value);
247
- if (parsed) {
248
- this._state.strokeStyle = value;
249
- this._state.strokeColor = parsed;
250
- }
251
- } else {
252
- this._state.strokeStyle = value;
253
- }
254
- }
255
- get lineWidth() {
256
- return this._state.lineWidth;
257
- }
258
- set lineWidth(value) {
259
- if (value > 0 && isFinite(value)) this._state.lineWidth = value;
260
- }
261
- get lineCap() {
262
- return this._state.lineCap;
263
- }
264
- set lineCap(value) {
265
- if (value === "butt" || value === "round" || value === "square") {
266
- this._state.lineCap = value;
267
- }
268
- }
269
- get lineJoin() {
270
- return this._state.lineJoin;
271
- }
272
- set lineJoin(value) {
273
- if (value === "miter" || value === "round" || value === "bevel") {
274
- this._state.lineJoin = value;
275
- }
276
- }
277
- get miterLimit() {
278
- return this._state.miterLimit;
279
- }
280
- set miterLimit(value) {
281
- if (value > 0 && isFinite(value)) this._state.miterLimit = value;
282
- }
283
- get globalAlpha() {
284
- return this._state.globalAlpha;
285
- }
286
- set globalAlpha(value) {
287
- if (value >= 0 && value <= 1 && isFinite(value)) this._state.globalAlpha = value;
288
- }
289
- get globalCompositeOperation() {
290
- return this._state.globalCompositeOperation;
291
- }
292
- set globalCompositeOperation(value) {
293
- if (COMPOSITE_OP_MAP[value] !== void 0) {
294
- this._state.globalCompositeOperation = value;
295
- }
296
- }
297
- get imageSmoothingEnabled() {
298
- return this._state.imageSmoothingEnabled;
299
- }
300
- set imageSmoothingEnabled(value) {
301
- this._state.imageSmoothingEnabled = !!value;
302
- }
303
- get imageSmoothingQuality() {
304
- return this._state.imageSmoothingQuality;
305
- }
306
- set imageSmoothingQuality(value) {
307
- if (value === "low" || value === "medium" || value === "high") {
308
- this._state.imageSmoothingQuality = value;
309
- }
310
- }
311
- // Line dash
312
- setLineDash(segments) {
313
- if (segments.some((v) => v < 0 || !isFinite(v))) return;
314
- this._state.lineDash = [...segments];
315
- }
316
- getLineDash() {
317
- return [...this._state.lineDash];
318
- }
319
- get lineDashOffset() {
320
- return this._state.lineDashOffset;
321
- }
322
- set lineDashOffset(value) {
323
- if (isFinite(value)) this._state.lineDashOffset = value;
324
- }
325
- // ---- Shadow properties (stored in state, rendering in Phase 5) ----
326
- get shadowColor() {
327
- return this._state.shadowColor;
328
- }
329
- set shadowColor(value) {
330
- this._state.shadowColor = value;
331
- }
332
- get shadowBlur() {
333
- return this._state.shadowBlur;
334
- }
335
- set shadowBlur(value) {
336
- if (value >= 0 && isFinite(value)) this._state.shadowBlur = value;
337
- }
338
- get shadowOffsetX() {
339
- return this._state.shadowOffsetX;
340
- }
341
- set shadowOffsetX(value) {
342
- if (isFinite(value)) this._state.shadowOffsetX = value;
343
- }
344
- get shadowOffsetY() {
345
- return this._state.shadowOffsetY;
346
- }
347
- set shadowOffsetY(value) {
348
- if (isFinite(value)) this._state.shadowOffsetY = value;
349
- }
350
- // ---- Text properties (stored in state, rendering in Phase 4) ----
351
- get font() {
352
- return this._state.font;
353
- }
354
- set font(value) {
355
- this._state.font = value;
356
- }
357
- get textAlign() {
358
- return this._state.textAlign;
359
- }
360
- set textAlign(value) {
361
- this._state.textAlign = value;
362
- }
363
- get textBaseline() {
364
- return this._state.textBaseline;
365
- }
366
- set textBaseline(value) {
367
- this._state.textBaseline = value;
368
- }
369
- get direction() {
370
- return this._state.direction;
371
- }
372
- set direction(value) {
373
- this._state.direction = value;
374
- }
375
- // ---- Path methods ----
376
- beginPath() {
377
- this._ensureSurface();
378
- this._ctx.newPath();
379
- }
380
- moveTo(x, y) {
381
- this._ensureSurface();
382
- this._ctx.moveTo(x, y);
383
- }
384
- lineTo(x, y) {
385
- this._ensureSurface();
386
- this._ctx.lineTo(x, y);
387
- }
388
- closePath() {
389
- this._ensureSurface();
390
- this._ctx.closePath();
391
- }
392
- bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
393
- this._ensureSurface();
394
- this._ctx.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
395
- }
396
- quadraticCurveTo(cpx, cpy, x, y) {
397
- this._ensureSurface();
398
- let cx, cy;
399
- if (this._ctx.hasCurrentPoint()) {
400
- [cx, cy] = this._ctx.getCurrentPoint();
401
- } else {
402
- cx = cpx;
403
- cy = cpy;
404
- }
405
- const { cp1x, cp1y, cp2x, cp2y } = quadraticToCubic(cx, cy, cpx, cpy, x, y);
406
- this._ctx.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
407
- }
408
- arc(x, y, radius, startAngle, endAngle, counterclockwise = false) {
409
- this._ensureSurface();
410
- if (Math.abs(endAngle - startAngle) >= 2 * Math.PI) {
411
- this._ctx.arc(x, y, radius, 0, 2 * Math.PI);
412
- return;
413
- }
414
- if (counterclockwise) {
415
- this._ctx.arcNegative(x, y, radius, startAngle, endAngle);
416
- } else {
417
- this._ctx.arc(x, y, radius, startAngle, endAngle);
418
- }
419
- }
420
- arcTo(x1, y1, x2, y2, radius) {
421
- this._ensureSurface();
422
- let x0, y0;
423
- if (this._ctx.hasCurrentPoint()) {
424
- [x0, y0] = this._ctx.getCurrentPoint();
425
- } else {
426
- x0 = x1;
427
- y0 = y1;
428
- this._ctx.moveTo(x1, y1);
429
- }
430
- cairoArcTo(this._ctx, x0, y0, x1, y1, x2, y2, radius);
431
- }
432
- ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise = false) {
433
- this._ensureSurface();
434
- if (radiusX < 0 || radiusY < 0) {
435
- throw new RangeError("The radii provided are negative");
436
- }
437
- cairoEllipse(this._ctx, x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise);
438
- }
439
- rect(x, y, w, h) {
440
- this._ensureSurface();
441
- this._ctx.rectangle(x, y, w, h);
442
- }
443
- roundRect(x, y, w, h, radii = 0) {
444
- this._ensureSurface();
445
- cairoRoundRect(this._ctx, x, y, w, h, radii);
446
- }
447
- fill(pathOrRule, fillRule) {
448
- this._ensureSurface();
449
- this._applyCompositing();
450
- this._applyFillStyle();
451
- let rule;
452
- if (pathOrRule instanceof Path2D) {
453
- this._ctx.newPath();
454
- pathOrRule._replayOnCairo(this._ctx);
455
- rule = fillRule;
456
- } else {
457
- rule = pathOrRule;
458
- }
459
- this._ctx.setFillRule(rule === "evenodd" ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING);
460
- this._ctx.fillPreserve();
461
- }
462
- stroke(path) {
463
- this._ensureSurface();
464
- this._applyCompositing();
465
- this._applyStrokeStyle();
466
- this._applyLineStyle();
467
- if (path instanceof Path2D) {
468
- this._ctx.newPath();
469
- path._replayOnCairo(this._ctx);
470
- }
471
- this._ctx.strokePreserve();
472
- }
473
- fillRect(x, y, w, h) {
474
- this._ensureSurface();
475
- this._applyCompositing();
476
- const savedPath = this._ctx.copyPath();
477
- if (this._hasShadow()) {
478
- this._renderShadow(() => {
479
- this._ctx.newPath();
480
- this._ctx.rectangle(x, y, w, h);
481
- this._ctx.fill();
482
- });
483
- }
484
- this._applyFillStyle();
485
- this._ctx.newPath();
486
- this._ctx.rectangle(x, y, w, h);
487
- this._ctx.fill();
488
- this._ctx.newPath();
489
- this._ctx.appendPath(savedPath);
490
- }
491
- strokeRect(x, y, w, h) {
492
- this._ensureSurface();
493
- this._applyCompositing();
494
- const savedPath = this._ctx.copyPath();
495
- if (this._hasShadow()) {
496
- this._renderShadow(() => {
497
- this._ctx.newPath();
498
- this._ctx.rectangle(x, y, w, h);
499
- this._ctx.stroke();
500
- });
501
- }
502
- this._applyStrokeStyle();
503
- this._applyLineStyle();
504
- this._ctx.newPath();
505
- this._ctx.rectangle(x, y, w, h);
506
- this._ctx.stroke();
507
- this._ctx.newPath();
508
- this._ctx.appendPath(savedPath);
509
- }
510
- clearRect(x, y, w, h) {
511
- this._ensureSurface();
512
- const savedPath = this._ctx.copyPath();
513
- this._ctx.save();
514
- this._ctx.setOperator(Cairo.Operator.CLEAR);
515
- this._ctx.newPath();
516
- this._ctx.rectangle(x, y, w, h);
517
- this._ctx.fill();
518
- this._ctx.restore();
519
- this._ctx.newPath();
520
- this._ctx.appendPath(savedPath);
521
- }
522
- clip(pathOrRule, fillRule) {
523
- this._ensureSurface();
524
- let rule;
525
- if (pathOrRule instanceof Path2D) {
526
- this._ctx.newPath();
527
- pathOrRule._replayOnCairo(this._ctx);
528
- rule = fillRule;
529
- } else {
530
- rule = pathOrRule;
531
- }
532
- this._ctx.setFillRule(rule === "evenodd" ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING);
533
- this._ctx.clip();
534
- }
535
- isPointInPath(pathOrX, xOrY, fillRuleOrY, fillRule) {
536
- this._ensureSurface();
537
- let x, y, rule;
538
- if (pathOrX instanceof Path2D) {
539
- this._ctx.newPath();
540
- pathOrX._replayOnCairo(this._ctx);
541
- x = xOrY;
542
- y = fillRuleOrY;
543
- rule = fillRule;
544
- } else {
545
- x = pathOrX;
546
- y = xOrY;
547
- rule = fillRuleOrY;
548
- }
549
- this._ctx.setFillRule(rule === "evenodd" ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING);
550
- return this._ctx.inFill(x, y);
551
- }
552
- isPointInStroke(pathOrX, xOrY, y) {
553
- this._ensureSurface();
554
- this._applyLineStyle();
555
- if (pathOrX instanceof Path2D) {
556
- this._ctx.newPath();
557
- pathOrX._replayOnCairo(this._ctx);
558
- return this._ctx.inStroke(xOrY, y);
559
- }
560
- return this._ctx.inStroke(pathOrX, xOrY);
561
- }
562
- // ---- Gradient / Pattern factories ----
563
- createLinearGradient(x0, y0, x1, y1) {
564
- return new OurCanvasGradient("linear", x0, y0, x1, y1);
565
- }
566
- createRadialGradient(x0, y0, r0, x1, y1, r1) {
567
- return new OurCanvasGradient("radial", x0, y0, x1, y1, r0, r1);
568
- }
569
- createPattern(image, repetition) {
570
- return OurCanvasPattern.create(image, repetition);
571
- }
572
- createImageData(swOrImageData, sh) {
573
- if (typeof swOrImageData === "number") {
574
- return new OurImageData(Math.abs(swOrImageData), Math.abs(sh));
575
- }
576
- return new OurImageData(swOrImageData.width, swOrImageData.height);
577
- }
578
- getImageData(sx, sy, sw, sh) {
579
- this._ensureSurface();
580
- this._surface.flush();
581
- const pixbuf = Gdk.pixbuf_get_from_surface(this._surface, sx, sy, sw, sh);
582
- if (!pixbuf) {
583
- return new OurImageData(sw, sh);
584
- }
585
- const pixels = pixbuf.get_pixels();
586
- const hasAlpha = pixbuf.get_has_alpha();
587
- const rowstride = pixbuf.get_rowstride();
588
- const nChannels = pixbuf.get_n_channels();
589
- const out = new Uint8ClampedArray(sw * sh * 4);
590
- for (let y = 0; y < sh; y++) {
591
- for (let x = 0; x < sw; x++) {
592
- const srcIdx = y * rowstride + x * nChannels;
593
- const dstIdx = (y * sw + x) * 4;
594
- out[dstIdx] = pixels[srcIdx];
595
- out[dstIdx + 1] = pixels[srcIdx + 1];
596
- out[dstIdx + 2] = pixels[srcIdx + 2];
597
- out[dstIdx + 3] = hasAlpha ? pixels[srcIdx + 3] : 255;
598
- }
599
- }
600
- return new OurImageData(out, sw, sh);
601
- }
602
- putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {
603
- this._ensureSurface();
604
- const sx = dirtyX ?? 0;
605
- const sy = dirtyY ?? 0;
606
- const sw = dirtyWidth ?? imageData.width;
607
- const sh = dirtyHeight ?? imageData.height;
608
- const srcData = imageData.data;
609
- const srcWidth = imageData.width;
610
- const regionData = new Uint8Array(sw * sh * 4);
611
- for (let y = 0; y < sh; y++) {
612
- for (let x = 0; x < sw; x++) {
613
- const srcIdx = ((sy + y) * srcWidth + (sx + x)) * 4;
614
- const dstIdx = (y * sw + x) * 4;
615
- regionData[dstIdx] = srcData[srcIdx];
616
- regionData[dstIdx + 1] = srcData[srcIdx + 1];
617
- regionData[dstIdx + 2] = srcData[srcIdx + 2];
618
- regionData[dstIdx + 3] = srcData[srcIdx + 3];
619
- }
620
- }
621
- const pixbuf = GdkPixbuf.Pixbuf.new_from_bytes(
622
- regionData,
623
- GdkPixbuf.Colorspace.RGB,
624
- true,
625
- // has_alpha
626
- 8,
627
- // bits_per_sample
628
- sw,
629
- sh,
630
- sw * 4
631
- // rowstride
632
- );
633
- this._ctx.save();
634
- this._ctx.setOperator(Cairo.Operator.SOURCE);
635
- Gdk.cairo_set_source_pixbuf(this._ctx, pixbuf, dx + sx, dy + sy);
636
- this._ctx.rectangle(dx + sx, dy + sy, sw, sh);
637
- this._ctx.fill();
638
- this._ctx.restore();
639
- }
640
- drawImage(image, a1, a2, a3, a4, a5, a6, a7, a8) {
641
- this._ensureSurface();
642
- this._applyCompositing();
643
- let sx, sy, sw, sh;
644
- let dx, dy, dw, dh;
645
- const sourceInfo = this._getDrawImageSource(image);
646
- if (!sourceInfo) return;
647
- const { pixbuf, imgWidth, imgHeight } = sourceInfo;
648
- if (a3 === void 0) {
649
- sx = 0;
650
- sy = 0;
651
- sw = imgWidth;
652
- sh = imgHeight;
653
- dx = a1;
654
- dy = a2;
655
- dw = imgWidth;
656
- dh = imgHeight;
657
- } else if (a5 === void 0) {
658
- sx = 0;
659
- sy = 0;
660
- sw = imgWidth;
661
- sh = imgHeight;
662
- dx = a1;
663
- dy = a2;
664
- dw = a3;
665
- dh = a4;
666
- } else {
667
- sx = a1;
668
- sy = a2;
669
- sw = a3;
670
- sh = a4;
671
- dx = a5;
672
- dy = a6;
673
- dw = a7;
674
- dh = a8;
675
- }
676
- this._ctx.save();
677
- this._ctx.translate(dx, dy);
678
- this._ctx.scale(dw / sw, dh / sh);
679
- this._ctx.translate(-sx, -sy);
680
- Gdk.cairo_set_source_pixbuf(this._ctx, pixbuf, 0, 0);
681
- this._ctx.rectangle(sx, sy, sw, sh);
682
- this._ctx.fill();
683
- this._ctx.restore();
684
- }
685
- _getDrawImageSource(image) {
686
- if (typeof image?.isPixbuf === "function" && image.isPixbuf()) {
687
- const pixbuf = image._pixbuf;
688
- return { pixbuf, imgWidth: pixbuf.get_width(), imgHeight: pixbuf.get_height() };
689
- }
690
- if (typeof image?.getContext === "function") {
691
- const ctx2d = image.getContext("2d");
692
- if (ctx2d && typeof ctx2d._getSurface === "function") {
693
- const surface = ctx2d._getSurface();
694
- surface.flush();
695
- const pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, image.width, image.height);
696
- if (pixbuf) {
697
- return { pixbuf, imgWidth: image.width, imgHeight: image.height };
698
- }
699
- }
700
- }
701
- return null;
702
- }
703
- // ---- Text methods (PangoCairo) ----
704
- /** Create a PangoCairo layout configured with current font/text settings. */
705
- _createTextLayout(text) {
706
- const layout = PangoCairo.create_layout(this._ctx);
707
- layout.set_text(text, -1);
708
- const fontDesc = this._parseFontToDescription(this._state.font);
709
- layout.set_font_description(fontDesc);
710
- return layout;
711
- }
712
- /** Parse a CSS font string (e.g. "bold 16px Arial") into a Pango.FontDescription. */
713
- _parseFontToDescription(cssFont) {
714
- const match = cssFont.match(
715
- /^\s*(italic|oblique|normal)?\s*(small-caps|normal)?\s*(bold|bolder|lighter|[1-9]00|normal)?\s*(\d+(?:\.\d+)?)(px|pt|em|rem|%)?\s*(?:\/\S+)?\s*(.+)?$/i
716
- );
717
- if (!match) {
718
- return Pango.font_description_from_string(cssFont);
719
- }
720
- const style = match[1] || "";
721
- const weight = match[3] || "";
722
- let size = parseFloat(match[4]) || 10;
723
- const unit = match[5] || "px";
724
- const family = (match[6] || "sans-serif").replace(/['"]/g, "").trim();
725
- if (unit === "px") size = size * 0.75;
726
- else if (unit === "em" || unit === "rem") size = size * 12;
727
- else if (unit === "%") size = size / 100 * 12;
728
- let pangoStr = family;
729
- if (style === "italic") pangoStr += " Italic";
730
- else if (style === "oblique") pangoStr += " Oblique";
731
- if (weight === "bold" || weight === "bolder" || parseInt(weight) >= 600) pangoStr += " Bold";
732
- else if (weight === "lighter" || parseInt(weight) > 0 && parseInt(weight) <= 300) pangoStr += " Light";
733
- pangoStr += ` ${Math.round(size)}`;
734
- return Pango.font_description_from_string(pangoStr);
735
- }
736
- /**
737
- * Compute the x-offset for text alignment relative to the given x coordinate.
738
- */
739
- _getTextAlignOffset(layout) {
740
- const [, logicalRect] = layout.get_pixel_extents();
741
- const width = logicalRect.width;
742
- switch (this._state.textAlign) {
743
- case "center":
744
- return -width / 2;
745
- case "right":
746
- case "end":
747
- return -width;
748
- case "left":
749
- case "start":
750
- default:
751
- return 0;
752
- }
753
- }
754
- /**
755
- * Compute the y-offset for text baseline positioning.
756
- */
757
- _getTextBaselineOffset(layout) {
758
- const fontDesc = layout.get_font_description() || this._parseFontToDescription(this._state.font);
759
- const context = layout.get_context();
760
- const metrics = context.get_metrics(fontDesc, null);
761
- const ascent = metrics.get_ascent() / Pango.SCALE;
762
- const descent = metrics.get_descent() / Pango.SCALE;
763
- const height = ascent + descent;
764
- switch (this._state.textBaseline) {
765
- case "top":
766
- return ascent;
767
- case "hanging":
768
- return ascent * 0.8;
769
- case "middle":
770
- return ascent - height / 2;
771
- case "alphabetic":
772
- return 0;
773
- case "ideographic":
774
- return -descent * 0.5;
775
- case "bottom":
776
- return -descent;
777
- default:
778
- return 0;
779
- }
780
- }
781
- fillText(text, x, y, _maxWidth) {
782
- this._ensureSurface();
783
- this._applyCompositing();
784
- this._applyFillStyle();
785
- const layout = this._createTextLayout(text);
786
- const xOff = this._getTextAlignOffset(layout);
787
- const yOff = this._getTextBaselineOffset(layout);
788
- this._ctx.save();
789
- this._ctx.moveTo(x + xOff, y + yOff);
790
- PangoCairo.show_layout(this._ctx, layout);
791
- this._ctx.restore();
792
- }
793
- strokeText(text, x, y, _maxWidth) {
794
- this._ensureSurface();
795
- this._applyCompositing();
796
- this._applyStrokeStyle();
797
- this._applyLineStyle();
798
- const layout = this._createTextLayout(text);
799
- const xOff = this._getTextAlignOffset(layout);
800
- const yOff = this._getTextBaselineOffset(layout);
801
- this._ctx.save();
802
- this._ctx.moveTo(x + xOff, y + yOff);
803
- PangoCairo.layout_path(this._ctx, layout);
804
- this._ctx.stroke();
805
- this._ctx.restore();
806
- }
807
- measureText(text) {
808
- this._ensureSurface();
809
- const layout = this._createTextLayout(text);
810
- const [inkRect, logicalRect] = layout.get_pixel_extents();
811
- const fontDesc = layout.get_font_description() || this._parseFontToDescription(this._state.font);
812
- const context = layout.get_context();
813
- const metrics = context.get_metrics(fontDesc, null);
814
- const ascent = metrics.get_ascent() / Pango.SCALE;
815
- const descent = metrics.get_descent() / Pango.SCALE;
816
- return {
817
- width: logicalRect.width,
818
- actualBoundingBoxAscent: ascent,
819
- actualBoundingBoxDescent: descent,
820
- actualBoundingBoxLeft: -inkRect.x,
821
- actualBoundingBoxRight: inkRect.x + inkRect.width,
822
- fontBoundingBoxAscent: ascent,
823
- fontBoundingBoxDescent: descent,
824
- alphabeticBaseline: 0,
825
- emHeightAscent: ascent,
826
- emHeightDescent: descent,
827
- hangingBaseline: ascent * 0.8,
828
- ideographicBaseline: -descent
829
- };
830
- }
831
- // ---- toDataURL/toBlob support ----
832
- /**
833
- * Write the canvas surface to a PNG file and return as data URL.
834
- * Used by HTMLCanvasElement.toDataURL() when a '2d' context is active.
835
- */
836
- _toDataURL(type, _quality) {
837
- if (type && type !== "image/png") {
838
- }
839
- this._surface.flush();
840
- const Gio = imports.gi.Gio;
841
- const GLib = imports.gi.GLib;
842
- const [, tempPath] = GLib.file_open_tmp("canvas-XXXXXX.png");
843
- try {
844
- this._surface.writeToPNG(tempPath);
845
- const file = Gio.File.new_for_path(tempPath);
846
- const [, contents] = file.load_contents(null);
847
- const base64 = GLib.base64_encode(contents);
848
- return `data:image/png;base64,${base64}`;
849
- } finally {
850
- try {
851
- GLib.unlink(tempPath);
852
- } catch (_e) {
853
- }
854
- }
855
- }
856
- // ---- Cleanup ----
857
- /** Release native Cairo resources. Call when the canvas is discarded. */
858
- _dispose() {
859
- this._ctx.$dispose();
860
- this._surface.finish();
861
- }
862
- }
1
+ import { CanvasRenderingContext2D } from "@gjsify/canvas2d-core";
863
2
  export {
864
3
  CanvasRenderingContext2D
865
4
  };