@gjsify/canvas2d-core 0.3.21 → 0.4.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.
@@ -10,6 +10,14 @@ import PangoCairo from 'gi://PangoCairo';
10
10
  // HTMLCanvasElement type is provided by the DOM lib.
11
11
  // Our @gjsify/dom-elements HTMLCanvasElement satisfies this interface.
12
12
 
13
+ import { asCairoPattern } from './cairo-types.js';
14
+ import {
15
+ type CanvasLike,
16
+ type CanvasGlobalThis,
17
+ type DOMMatrix2DLike,
18
+ isPixbufImageSource,
19
+ isCanvasImageSource,
20
+ } from './dom-types.js';
13
21
  import { parseColor } from './color.js';
14
22
  import {
15
23
  quadraticToCubic,
@@ -26,12 +34,24 @@ import { CanvasGradient as OurCanvasGradient } from './canvas-gradient.js';
26
34
  import { CanvasPattern as OurCanvasPattern } from './canvas-pattern.js';
27
35
  import { Path2D } from './canvas-path.js';
28
36
 
37
+ /**
38
+ * Options bag passed through the `getContext('2d', options)` factory. Mirrors
39
+ * the WHATWG `CanvasRenderingContext2DSettings` dictionary; fields are
40
+ * accepted but not yet honored by this implementation.
41
+ */
42
+ export interface CanvasRenderingContext2DInit {
43
+ alpha?: boolean;
44
+ desynchronized?: boolean;
45
+ colorSpace?: PredefinedColorSpace;
46
+ willReadFrequently?: boolean;
47
+ }
48
+
29
49
  /**
30
50
  * CanvasRenderingContext2D backed by Cairo.ImageSurface.
31
51
  * Implements the Canvas 2D API for GJS.
32
52
  */
33
53
  export class CanvasRenderingContext2D {
34
- readonly canvas: any;
54
+ readonly canvas: CanvasLike;
35
55
 
36
56
  private _surface: Cairo.ImageSurface;
37
57
  private _ctx: Cairo.Context;
@@ -40,7 +60,7 @@ export class CanvasRenderingContext2D {
40
60
  private _surfaceWidth: number;
41
61
  private _surfaceHeight: number;
42
62
 
43
- constructor(canvas: any, _options?: any) {
63
+ constructor(canvas: CanvasLike, _options?: CanvasRenderingContext2DInit) {
44
64
  this.canvas = canvas;
45
65
  this._surfaceWidth = canvas.width || 300;
46
66
  this._surfaceHeight = canvas.height || 150;
@@ -115,18 +135,17 @@ export class CanvasRenderingContext2D {
115
135
  * creation — so we re-apply it on every fill/stroke.
116
136
  */
117
137
  private _applyPatternFilter(): void {
118
- const pat = (this._ctx as any).getSource?.();
119
- if (pat && typeof pat.setFilter === 'function') {
120
- let filter: number;
121
- if (!this._state.imageSmoothingEnabled) {
122
- filter = Cairo.Filter.NEAREST as unknown as number;
123
- } else if (this._state.imageSmoothingQuality === 'high') {
124
- filter = Cairo.Filter.BEST as unknown as number;
125
- } else {
126
- filter = Cairo.Filter.BILINEAR as unknown as number;
127
- }
128
- pat.setFilter(filter);
138
+ const pat = asCairoPattern(this._ctx.getSource?.());
139
+ if (!pat) return;
140
+ let filter: Cairo.Filter;
141
+ if (!this._state.imageSmoothingEnabled) {
142
+ filter = Cairo.Filter.NEAREST;
143
+ } else if (this._state.imageSmoothingQuality === 'high') {
144
+ filter = Cairo.Filter.BEST;
145
+ } else {
146
+ filter = Cairo.Filter.BILINEAR;
129
147
  }
148
+ pat.setFilter(filter);
130
149
  }
131
150
 
132
151
  /** Apply line properties to the Cairo context. */
@@ -170,9 +189,9 @@ export class CanvasRenderingContext2D {
170
189
  * regardless of any ctx.scale() or ctx.rotate() in effect.
171
190
  */
172
191
  private _deviceToUserDistance(dx: number, dy: number): [number, number] {
173
- const origin = (this._ctx as any).userToDevice(0, 0);
174
- const xAxis = (this._ctx as any).userToDevice(1, 0);
175
- const yAxis = (this._ctx as any).userToDevice(0, 1);
192
+ const origin = this._ctx.userToDevice(0, 0);
193
+ const xAxis = this._ctx.userToDevice(1, 0);
194
+ const yAxis = this._ctx.userToDevice(0, 1);
176
195
  const a = (xAxis[0] ?? 0) - (origin[0] ?? 0);
177
196
  const b = (xAxis[1] ?? 0) - (origin[1] ?? 0);
178
197
  const c = (yAxis[0] ?? 0) - (origin[0] ?? 0);
@@ -304,9 +323,9 @@ export class CanvasRenderingContext2D {
304
323
  // userToDevice(0, 0) = (e, f) — translation
305
324
  // userToDevice(1, 0) = (a + e, b + f) — first basis vector
306
325
  // userToDevice(0, 1) = (c + e, d + f) — second basis vector
307
- const origin = (this._ctx as any).userToDevice(0, 0);
308
- const xAxis = (this._ctx as any).userToDevice(1, 0);
309
- const yAxis = (this._ctx as any).userToDevice(0, 1);
326
+ const origin = this._ctx.userToDevice(0, 0);
327
+ const xAxis = this._ctx.userToDevice(1, 0);
328
+ const yAxis = this._ctx.userToDevice(0, 1);
310
329
  const e = origin[0] ?? 0;
311
330
  const f = origin[1] ?? 0;
312
331
  const a = (xAxis[0] ?? 0) - e;
@@ -314,11 +333,11 @@ export class CanvasRenderingContext2D {
314
333
  const c = (yAxis[0] ?? 0) - e;
315
334
  const d = (yAxis[1] ?? 0) - f;
316
335
 
317
- const DOMMatrixCtor = (globalThis as any).DOMMatrix;
336
+ const DOMMatrixCtor = (globalThis as CanvasGlobalThis).DOMMatrix;
318
337
  if (typeof DOMMatrixCtor === 'function') {
319
338
  return new DOMMatrixCtor([a, b, c, d, e, f]);
320
339
  }
321
- return {
340
+ const fallback: DOMMatrix2DLike = {
322
341
  a, b, c, d, e, f,
323
342
  m11: a, m12: b, m13: 0, m14: 0,
324
343
  m21: c, m22: d, m23: 0, m24: 0,
@@ -326,7 +345,8 @@ export class CanvasRenderingContext2D {
326
345
  m41: e, m42: f, m43: 0, m44: 1,
327
346
  is2D: true,
328
347
  isIdentity: (a === 1 && b === 0 && c === 0 && d === 1 && e === 0 && f === 0),
329
- } as any;
348
+ };
349
+ return fallback as unknown as DOMMatrix;
330
350
  }
331
351
 
332
352
  resetTransform(): void {
@@ -693,15 +713,15 @@ export class CanvasRenderingContext2D {
693
713
  // ---- Gradient / Pattern factories ----
694
714
 
695
715
  createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient {
696
- return new OurCanvasGradient('linear', x0, y0, x1, y1) as any;
716
+ return new OurCanvasGradient('linear', x0, y0, x1, y1) as unknown as CanvasGradient;
697
717
  }
698
718
 
699
719
  createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient {
700
- return new OurCanvasGradient('radial', x0, y0, x1, y1, r0, r1) as any;
720
+ return new OurCanvasGradient('radial', x0, y0, x1, y1, r0, r1) as unknown as CanvasGradient;
701
721
  }
702
722
 
703
- createPattern(image: any, repetition: string | null): CanvasPattern | null {
704
- return OurCanvasPattern.create(image, repetition) as any;
723
+ createPattern(image: unknown, repetition: string | null): CanvasPattern | null {
724
+ return OurCanvasPattern.create(image, repetition) as unknown as CanvasPattern | null;
705
725
  }
706
726
 
707
727
  // ---- Image data methods ----
@@ -710,9 +730,9 @@ export class CanvasRenderingContext2D {
710
730
  createImageData(imagedata: ImageData): ImageData;
711
731
  createImageData(swOrImageData: number | ImageData, sh?: number): ImageData {
712
732
  if (typeof swOrImageData === 'number') {
713
- return new OurImageData(Math.abs(swOrImageData), Math.abs(sh!)) as any;
733
+ return new OurImageData(Math.abs(swOrImageData), Math.abs(sh!)) as unknown as ImageData;
714
734
  }
715
- return new OurImageData(swOrImageData.width, swOrImageData.height) as any;
735
+ return new OurImageData(swOrImageData.width, swOrImageData.height) as unknown as ImageData;
716
736
  }
717
737
 
718
738
  getImageData(sx: number, sy: number, sw: number, sh: number): ImageData {
@@ -722,7 +742,7 @@ export class CanvasRenderingContext2D {
722
742
  // Use Gdk.pixbuf_get_from_surface to read pixels
723
743
  const pixbuf = Gdk.pixbuf_get_from_surface(this._surface, sx, sy, sw, sh);
724
744
  if (!pixbuf) {
725
- return new OurImageData(sw, sh) as any;
745
+ return new OurImageData(sw, sh) as unknown as ImageData;
726
746
  }
727
747
 
728
748
  const pixels = pixbuf.get_pixels();
@@ -742,7 +762,7 @@ export class CanvasRenderingContext2D {
742
762
  }
743
763
  }
744
764
 
745
- return new OurImageData(out, sw, sh) as any;
765
+ return new OurImageData(out, sw, sh) as unknown as ImageData;
746
766
  }
747
767
 
748
768
  putImageData(imageData: ImageData, dx: number, dy: number, dirtyX?: number, dirtyY?: number, dirtyWidth?: number, dirtyHeight?: number): void {
@@ -784,7 +804,7 @@ export class CanvasRenderingContext2D {
784
804
  // putImageData per spec ignores compositing — always uses SOURCE operator
785
805
  this._ctx.save();
786
806
  this._ctx.setOperator(Cairo.Operator.SOURCE);
787
- Gdk.cairo_set_source_pixbuf(this._ctx as any, pixbuf, dx + sx, dy + sy);
807
+ Gdk.cairo_set_source_pixbuf(this._ctx, pixbuf, dx + sx, dy + sy);
788
808
  this._ctx.rectangle(dx + sx, dy + sy, sw, sh);
789
809
  this._ctx.fill();
790
810
  this._ctx.restore();
@@ -792,11 +812,11 @@ export class CanvasRenderingContext2D {
792
812
 
793
813
  // ---- drawImage ----
794
814
 
795
- drawImage(image: any, dx: number, dy: number): void;
796
- drawImage(image: any, dx: number, dy: number, dw: number, dh: number): void;
797
- drawImage(image: any, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
815
+ drawImage(image: unknown, dx: number, dy: number): void;
816
+ drawImage(image: unknown, dx: number, dy: number, dw: number, dh: number): void;
817
+ drawImage(image: unknown, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
798
818
  drawImage(
799
- image: any,
819
+ image: unknown,
800
820
  a1: number, a2: number,
801
821
  a3?: number, a4?: number,
802
822
  a5?: number, a6?: number,
@@ -860,7 +880,7 @@ export class CanvasRenderingContext2D {
860
880
  this._ctx.scale(dw / sw, dh / sh);
861
881
  this._ctx.translate(-sx, -sy);
862
882
 
863
- Gdk.cairo_set_source_pixbuf(this._ctx as any, pixbuf, 0, 0);
883
+ Gdk.cairo_set_source_pixbuf(this._ctx, pixbuf, 0, 0);
864
884
 
865
885
  // Apply Cairo interpolation filter based on imageSmoothingEnabled +
866
886
  // imageSmoothingQuality. setSource installs a fresh SurfacePattern and
@@ -871,16 +891,17 @@ export class CanvasRenderingContext2D {
871
891
  //
872
892
  // Cairo.Filter values (verified runtime in GJS 1.86):
873
893
  // FAST=0 GOOD=1 BEST=2 NEAREST=3 BILINEAR=4 GAUSSIAN=5
874
- // GIR typings are incomplete for Cairo.SurfacePattern so we go via any.
875
- const pat = (this._ctx as any).getSource?.();
876
- if (pat && typeof pat.setFilter === 'function') {
877
- let filter: number;
894
+ // GIR typings are missing setFilter on Pattern `asCairoPattern`
895
+ // narrows to the augmented shape (see cairo-types.ts).
896
+ const pat = asCairoPattern(this._ctx.getSource?.());
897
+ if (pat) {
898
+ let filter: Cairo.Filter;
878
899
  if (!this._state.imageSmoothingEnabled) {
879
- filter = Cairo.Filter.NEAREST as unknown as number;
900
+ filter = Cairo.Filter.NEAREST;
880
901
  } else if (this._state.imageSmoothingQuality === 'high') {
881
- filter = Cairo.Filter.BEST as unknown as number;
902
+ filter = Cairo.Filter.BEST;
882
903
  } else {
883
- filter = Cairo.Filter.BILINEAR as unknown as number;
904
+ filter = Cairo.Filter.BILINEAR;
884
905
  }
885
906
  pat.setFilter(filter);
886
907
  }
@@ -891,24 +912,24 @@ export class CanvasRenderingContext2D {
891
912
  // doesn't support per-draw alpha, so paint() is the spec-correct
892
913
  // choice for drawImage. The clip above confines the paint to dx,dy,dw,dh.
893
914
  if (this._state.globalAlpha < 1) {
894
- (this._ctx as any).paintWithAlpha(this._state.globalAlpha);
915
+ this._ctx.paintWithAlpha(this._state.globalAlpha);
895
916
  } else {
896
917
  this._ctx.paint();
897
918
  }
898
919
  this._ctx.restore();
899
920
  }
900
921
 
901
- private _getDrawImageSource(image: any): { pixbuf: GdkPixbuf.Pixbuf; imgWidth: number; imgHeight: number } | null {
922
+ private _getDrawImageSource(image: unknown): { pixbuf: GdkPixbuf.Pixbuf; imgWidth: number; imgHeight: number } | null {
902
923
  // HTMLImageElement (GdkPixbuf-backed)
903
- if (typeof image?.isPixbuf === 'function' && image.isPixbuf()) {
904
- const pixbuf = image._pixbuf as GdkPixbuf.Pixbuf;
924
+ if (isPixbufImageSource(image)) {
925
+ const pixbuf = image._pixbuf;
905
926
  return { pixbuf, imgWidth: pixbuf.get_width(), imgHeight: pixbuf.get_height() };
906
927
  }
907
928
 
908
929
  // HTMLCanvasElement with a 2D context
909
- if (typeof image?.getContext === 'function') {
910
- const w = image.width;
911
- const h = image.height;
930
+ if (isCanvasImageSource(image)) {
931
+ const w = image.width ?? 0;
932
+ const h = image.height ?? 0;
912
933
  // Reject non-positive / non-finite dimensions before they reach
913
934
  // GdkPixbuf — `pixbuf_get_from_surface` logs a GLib-CRITICAL on
914
935
  // `width > 0 && height > 0` assertion failure for NaN/0 inputs.
@@ -917,7 +938,7 @@ export class CanvasRenderingContext2D {
917
938
  }
918
939
  const ctx2d = image.getContext('2d');
919
940
  if (ctx2d && typeof ctx2d._getSurface === 'function') {
920
- const surface = ctx2d._getSurface() as Cairo.ImageSurface;
941
+ const surface = ctx2d._getSurface();
921
942
  surface.flush();
922
943
  const pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0, w, h);
923
944
  if (pixbuf) {
@@ -933,7 +954,7 @@ export class CanvasRenderingContext2D {
933
954
 
934
955
  /** Create a PangoCairo layout configured with current font/text settings. */
935
956
  private _createTextLayout(text: string): Pango.Layout {
936
- const layout = PangoCairo.create_layout(this._ctx as any);
957
+ const layout = PangoCairo.create_layout(this._ctx);
937
958
  layout.set_text(text, -1);
938
959
 
939
960
  // Force LTR base direction so text is never rendered mirrored
@@ -1077,10 +1098,10 @@ export class CanvasRenderingContext2D {
1077
1098
  const aa = this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE;
1078
1099
  for (const [tx, ty, ta] of taps) {
1079
1100
  this._ctx.save();
1080
- (this._ctx as any).setAntialias(aa);
1101
+ this._ctx.setAntialias(aa);
1081
1102
  this._ctx.setSourceRGBA(sc.r, sc.g, sc.b, ta);
1082
1103
  this._ctx.moveTo(x + xOff + tx, y + yOff + ty);
1083
- PangoCairo.show_layout(this._ctx as any, layout);
1104
+ PangoCairo.show_layout(this._ctx, layout);
1084
1105
  this._ctx.restore();
1085
1106
  }
1086
1107
  }
@@ -1090,9 +1111,9 @@ export class CanvasRenderingContext2D {
1090
1111
  this._ctx.save();
1091
1112
  // Disable anti-aliasing so pixel/bitmap fonts render crisp (matching browser
1092
1113
  // behaviour for fonts with no outline hints). cairo_save/restore covers antialias.
1093
- (this._ctx as any).setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
1114
+ this._ctx.setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
1094
1115
  this._ctx.moveTo(x + xOff, y + yOff);
1095
- PangoCairo.show_layout(this._ctx as any, layout);
1116
+ PangoCairo.show_layout(this._ctx, layout);
1096
1117
  this._ctx.restore();
1097
1118
  }
1098
1119
 
@@ -1107,9 +1128,9 @@ export class CanvasRenderingContext2D {
1107
1128
  const yOff = this._getTextBaselineOffset(layout);
1108
1129
 
1109
1130
  this._ctx.save();
1110
- (this._ctx as any).setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
1131
+ this._ctx.setAntialias(this._state.imageSmoothingEnabled ? Cairo.Antialias.DEFAULT : Cairo.Antialias.NONE);
1111
1132
  this._ctx.moveTo(x + xOff, y + yOff);
1112
- PangoCairo.layout_path(this._ctx as any, layout);
1133
+ PangoCairo.layout_path(this._ctx, layout);
1113
1134
  this._ctx.stroke();
1114
1135
  this._ctx.restore();
1115
1136
  }
@@ -0,0 +1,96 @@
1
+ // Internal interface declarations for the DOM-shaped objects that
2
+ // CanvasRenderingContext2D consumes.
3
+ //
4
+ // canvas2d-core deliberately has *no* dependency on @gjsify/dom-elements (that
5
+ // would create a cycle: dom-elements → canvas2d-core → dom-elements). Instead
6
+ // it accepts duck-typed inputs that match the relevant slice of the WHATWG
7
+ // Canvas 2D API. These interfaces document those slices and let the rest of
8
+ // the package work against concrete types instead of `any`.
9
+
10
+ import type Cairo from 'cairo';
11
+ import type GdkPixbuf from 'gi://GdkPixbuf';
12
+
13
+ /**
14
+ * The HTMLCanvasElement-shaped object passed into the
15
+ * `CanvasRenderingContext2D` constructor by the registered context factory.
16
+ *
17
+ * `@gjsify/dom-elements`' `HTMLCanvasElement` satisfies this — but so does any
18
+ * lightweight `{ width, height }` mock used by unit tests. Width/height
19
+ * default to the WHATWG canvas defaults (300×150) when missing.
20
+ */
21
+ export interface CanvasLike {
22
+ width?: number;
23
+ height?: number;
24
+ }
25
+
26
+ /**
27
+ * GdkPixbuf-backed image source produced by `@gjsify/dom-elements`'
28
+ * `HTMLImageElement` (and other pixbuf-bearing wrappers).
29
+ *
30
+ * The `isPixbuf()` brand keeps us decoupled from the concrete class while
31
+ * preventing accidental matches against unrelated objects.
32
+ */
33
+ export interface PixbufImageSource {
34
+ isPixbuf(): boolean;
35
+ /** @internal — populated by HTMLImageElement once decoding completes. */
36
+ _pixbuf: GdkPixbuf.Pixbuf;
37
+ }
38
+
39
+ /**
40
+ * Canvas-like image source carrying a 2D context whose backing surface can be
41
+ * sampled (used for `drawImage(canvas, …)` and `createPattern(canvas, …)`).
42
+ */
43
+ export interface CanvasImageSource extends CanvasLike {
44
+ getContext(contextId: '2d', options?: unknown): CanvasContext2DLike | null;
45
+ getContext(contextId: string, options?: unknown): unknown;
46
+ }
47
+
48
+ /**
49
+ * The minimal slice of `CanvasRenderingContext2D` required to extract pixel
50
+ * data for `drawImage` / `createPattern`. Our own context naturally satisfies
51
+ * this through `_getSurface()`.
52
+ */
53
+ export interface CanvasContext2DLike {
54
+ /** @internal — exposes the Cairo backing surface. */
55
+ _getSurface?(): Cairo.ImageSurface;
56
+ }
57
+
58
+ /** Type guard for {@link PixbufImageSource}. */
59
+ export function isPixbufImageSource(value: unknown): value is PixbufImageSource {
60
+ if (value === null || typeof value !== 'object') return false;
61
+ const candidate = value as { isPixbuf?: unknown };
62
+ return typeof candidate.isPixbuf === 'function' && (value as PixbufImageSource).isPixbuf();
63
+ }
64
+
65
+ /** Type guard for {@link CanvasImageSource}. */
66
+ export function isCanvasImageSource(value: unknown): value is CanvasImageSource {
67
+ if (value === null || typeof value !== 'object') return false;
68
+ return typeof (value as { getContext?: unknown }).getContext === 'function';
69
+ }
70
+
71
+ /**
72
+ * Minimal `DOMMatrix` shape returned by `CanvasRenderingContext2D.getTransform()`
73
+ * when no native `DOMMatrix` constructor is registered. Mirrors the
74
+ * `is2D`-only subset of the WHATWG matrix interface.
75
+ */
76
+ export interface DOMMatrix2DLike {
77
+ a: number; b: number; c: number; d: number; e: number; f: number;
78
+ m11: number; m12: number; m13: number; m14: number;
79
+ m21: number; m22: number; m23: number; m24: number;
80
+ m31: number; m32: number; m33: number; m34: number;
81
+ m41: number; m42: number; m43: number; m44: number;
82
+ is2D: boolean;
83
+ isIdentity: boolean;
84
+ }
85
+
86
+ /**
87
+ * Constructor signature for the platform `DOMMatrix`. Lets us reach the
88
+ * runtime constructor through `globalThis` without an `any` cast when an
89
+ * embedder (e.g. `@gjsify/dom-elements`) has registered one.
90
+ */
91
+ export type DOMMatrixConstructor = new (init?: number[] | string) => DOMMatrix;
92
+
93
+ /** Subset of `globalThis` we touch inside this package. */
94
+ export interface CanvasGlobalThis {
95
+ DOMMatrix?: DOMMatrixConstructor;
96
+ }