@gjsify/canvas2d 0.1.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.
@@ -0,0 +1,155 @@
1
+ import Cairo from 'cairo';
2
+ import { Path2D } from './canvas-path.js';
3
+ /**
4
+ * CanvasRenderingContext2D backed by Cairo.ImageSurface.
5
+ * Implements the Canvas 2D API for GJS.
6
+ */
7
+ export declare class CanvasRenderingContext2D {
8
+ readonly canvas: any;
9
+ private _surface;
10
+ private _ctx;
11
+ private _state;
12
+ private _stateStack;
13
+ private _surfaceWidth;
14
+ private _surfaceHeight;
15
+ constructor(canvas: any, _options?: any);
16
+ /** Ensure the surface matches the current canvas dimensions. Recreate if resized. */
17
+ private _ensureSurface;
18
+ /** Apply the current fill style (color, gradient, or pattern) to the Cairo context. */
19
+ private _applyFillStyle;
20
+ /** Apply the current stroke style to the Cairo context. */
21
+ private _applyStrokeStyle;
22
+ /** Apply line properties to the Cairo context. */
23
+ private _applyLineStyle;
24
+ /** Apply compositing operator. */
25
+ private _applyCompositing;
26
+ /** Get the Cairo ImageSurface (used by other contexts like drawImage). */
27
+ _getSurface(): Cairo.ImageSurface;
28
+ /** Check if shadow rendering is needed. */
29
+ private _hasShadow;
30
+ /**
31
+ * Render a shadow for the current path by painting to a temp surface,
32
+ * applying a simple box blur approximation, and compositing back.
33
+ * This is called before the actual fill/stroke when shadows are active.
34
+ */
35
+ private _renderShadow;
36
+ save(): void;
37
+ restore(): void;
38
+ translate(x: number, y: number): void;
39
+ rotate(angle: number): void;
40
+ scale(x: number, y: number): void;
41
+ /**
42
+ * Multiply the current transformation matrix by the given values.
43
+ * Matrix: [a c e]
44
+ * [b d f]
45
+ * [0 0 1]
46
+ */
47
+ transform(a: number, b: number, c: number, d: number, e: number, f: number): void;
48
+ /**
49
+ * Reset the transform to identity, then apply the given matrix.
50
+ */
51
+ setTransform(matrix?: DOMMatrix2DInit): void;
52
+ setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
53
+ /**
54
+ * Return the current transformation matrix as a DOMMatrix-like object.
55
+ */
56
+ getTransform(): DOMMatrix;
57
+ resetTransform(): void;
58
+ get fillStyle(): string | CanvasGradient | CanvasPattern;
59
+ set fillStyle(value: string | CanvasGradient | CanvasPattern);
60
+ get strokeStyle(): string | CanvasGradient | CanvasPattern;
61
+ set strokeStyle(value: string | CanvasGradient | CanvasPattern);
62
+ get lineWidth(): number;
63
+ set lineWidth(value: number);
64
+ get lineCap(): CanvasLineCap;
65
+ set lineCap(value: CanvasLineCap);
66
+ get lineJoin(): CanvasLineJoin;
67
+ set lineJoin(value: CanvasLineJoin);
68
+ get miterLimit(): number;
69
+ set miterLimit(value: number);
70
+ get globalAlpha(): number;
71
+ set globalAlpha(value: number);
72
+ get globalCompositeOperation(): GlobalCompositeOperation;
73
+ set globalCompositeOperation(value: GlobalCompositeOperation);
74
+ get imageSmoothingEnabled(): boolean;
75
+ set imageSmoothingEnabled(value: boolean);
76
+ get imageSmoothingQuality(): ImageSmoothingQuality;
77
+ set imageSmoothingQuality(value: ImageSmoothingQuality);
78
+ setLineDash(segments: number[]): void;
79
+ getLineDash(): number[];
80
+ get lineDashOffset(): number;
81
+ set lineDashOffset(value: number);
82
+ get shadowColor(): string;
83
+ set shadowColor(value: string);
84
+ get shadowBlur(): number;
85
+ set shadowBlur(value: number);
86
+ get shadowOffsetX(): number;
87
+ set shadowOffsetX(value: number);
88
+ get shadowOffsetY(): number;
89
+ set shadowOffsetY(value: number);
90
+ get font(): string;
91
+ set font(value: string);
92
+ get textAlign(): CanvasTextAlign;
93
+ set textAlign(value: CanvasTextAlign);
94
+ get textBaseline(): CanvasTextBaseline;
95
+ set textBaseline(value: CanvasTextBaseline);
96
+ get direction(): CanvasDirection;
97
+ set direction(value: CanvasDirection);
98
+ beginPath(): void;
99
+ moveTo(x: number, y: number): void;
100
+ lineTo(x: number, y: number): void;
101
+ closePath(): void;
102
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
103
+ quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void;
104
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
105
+ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
106
+ ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise?: boolean): void;
107
+ rect(x: number, y: number, w: number, h: number): void;
108
+ roundRect(x: number, y: number, w: number, h: number, radii?: number | number[]): void;
109
+ fill(fillRule?: CanvasFillRule): void;
110
+ fill(path: Path2D, fillRule?: CanvasFillRule): void;
111
+ stroke(): void;
112
+ stroke(path: Path2D): void;
113
+ fillRect(x: number, y: number, w: number, h: number): void;
114
+ strokeRect(x: number, y: number, w: number, h: number): void;
115
+ clearRect(x: number, y: number, w: number, h: number): void;
116
+ clip(fillRule?: CanvasFillRule): void;
117
+ clip(path: Path2D, fillRule?: CanvasFillRule): void;
118
+ isPointInPath(x: number, y: number, fillRule?: CanvasFillRule): boolean;
119
+ isPointInPath(path: Path2D, x: number, y: number, fillRule?: CanvasFillRule): boolean;
120
+ isPointInStroke(x: number, y: number): boolean;
121
+ isPointInStroke(path: Path2D, x: number, y: number): boolean;
122
+ createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
123
+ createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
124
+ createPattern(image: any, repetition: string | null): CanvasPattern | null;
125
+ createImageData(sw: number, sh: number): ImageData;
126
+ createImageData(imagedata: ImageData): ImageData;
127
+ getImageData(sx: number, sy: number, sw: number, sh: number): ImageData;
128
+ putImageData(imageData: ImageData, dx: number, dy: number, dirtyX?: number, dirtyY?: number, dirtyWidth?: number, dirtyHeight?: number): void;
129
+ drawImage(image: any, dx: number, dy: number): void;
130
+ drawImage(image: any, dx: number, dy: number, dw: number, dh: number): void;
131
+ drawImage(image: any, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
132
+ private _getDrawImageSource;
133
+ /** Create a PangoCairo layout configured with current font/text settings. */
134
+ private _createTextLayout;
135
+ /** Parse a CSS font string (e.g. "bold 16px Arial") into a Pango.FontDescription. */
136
+ private _parseFontToDescription;
137
+ /**
138
+ * Compute the x-offset for text alignment relative to the given x coordinate.
139
+ */
140
+ private _getTextAlignOffset;
141
+ /**
142
+ * Compute the y-offset for text baseline positioning.
143
+ */
144
+ private _getTextBaselineOffset;
145
+ fillText(text: string, x: number, y: number, _maxWidth?: number): void;
146
+ strokeText(text: string, x: number, y: number, _maxWidth?: number): void;
147
+ measureText(text: string): TextMetrics;
148
+ /**
149
+ * Write the canvas surface to a PNG file and return as data URL.
150
+ * Used by HTMLCanvasElement.toDataURL() when a '2d' context is active.
151
+ */
152
+ _toDataURL(type?: string, _quality?: number): string;
153
+ /** Release native Cairo resources. Call when the canvas is discarded. */
154
+ _dispose(): void;
155
+ }
@@ -0,0 +1,27 @@
1
+ import type { RGBA } from './color.js';
2
+ export interface CanvasState {
3
+ fillStyle: string | CanvasGradient | CanvasPattern;
4
+ fillColor: RGBA;
5
+ strokeStyle: string | CanvasGradient | CanvasPattern;
6
+ strokeColor: RGBA;
7
+ lineWidth: number;
8
+ lineCap: CanvasLineCap;
9
+ lineJoin: CanvasLineJoin;
10
+ miterLimit: number;
11
+ lineDash: number[];
12
+ lineDashOffset: number;
13
+ globalAlpha: number;
14
+ globalCompositeOperation: GlobalCompositeOperation;
15
+ shadowColor: string;
16
+ shadowBlur: number;
17
+ shadowOffsetX: number;
18
+ shadowOffsetY: number;
19
+ font: string;
20
+ textAlign: CanvasTextAlign;
21
+ textBaseline: CanvasTextBaseline;
22
+ direction: CanvasDirection;
23
+ imageSmoothingEnabled: boolean;
24
+ imageSmoothingQuality: ImageSmoothingQuality;
25
+ }
26
+ export declare function createDefaultState(): CanvasState;
27
+ export declare function cloneState(state: CanvasState): CanvasState;
@@ -0,0 +1,15 @@
1
+ export interface RGBA {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ a: number;
6
+ }
7
+ /**
8
+ * Parse a CSS color string into RGBA components (0-1 range).
9
+ * Supports: #rgb, #rrggbb, #rgba, #rrggbbaa, rgb(), rgba(), named colors, 'transparent'.
10
+ */
11
+ export declare function parseColor(color: string): RGBA | null;
12
+ /** Default color: opaque black */
13
+ export declare const BLACK: RGBA;
14
+ /** Transparent black */
15
+ export declare const TRANSPARENT: RGBA;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ImageData represents the pixel data of a canvas area.
3
+ * Each pixel is 4 bytes: R, G, B, A (0-255 each).
4
+ */
5
+ export declare class OurImageData {
6
+ readonly data: Uint8ClampedArray;
7
+ readonly width: number;
8
+ readonly height: number;
9
+ readonly colorSpace: PredefinedColorSpace;
10
+ constructor(sw: number, sh: number);
11
+ constructor(data: Uint8ClampedArray, sw: number, sh?: number);
12
+ }
@@ -0,0 +1,7 @@
1
+ export { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
2
+ export { CanvasGradient } from './canvas-gradient.js';
3
+ export { CanvasPattern } from './canvas-pattern.js';
4
+ export { Path2D } from './canvas-path.js';
5
+ export { OurImageData as ImageData } from './image-data.js';
6
+ export { parseColor } from './color.js';
7
+ export { Canvas2DWidget } from './canvas-drawing-area.js';
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gjsify/canvas2d",
3
+ "version": "0.1.0",
4
+ "description": "Canvas 2D rendering context for GJS, backed by Cairo",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "clear": "rm -rf lib tmp tsconfig.tsbuildinfo test.gjs.mjs || exit 0",
16
+ "check": "tsc --noEmit",
17
+ "build": "yarn build:gjsify && yarn build:types",
18
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
+ "build:types": "tsc",
20
+ "build:test": "yarn build:test:gjs",
21
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
+ "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
23
+ "test:gjs": "gjs -m test.gjs.mjs"
24
+ },
25
+ "keywords": [
26
+ "gjs",
27
+ "canvas",
28
+ "canvas2d",
29
+ "cairo"
30
+ ],
31
+ "dependencies": {
32
+ "@girs/gdk-4.0": "^4.0.0-4.0.0-beta.42",
33
+ "@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-beta.42",
34
+ "@girs/gjs": "^4.0.0-beta.42",
35
+ "@girs/glib-2.0": "^2.88.0-4.0.0-beta.42",
36
+ "@girs/gobject-2.0": "^2.88.0-4.0.0-beta.42",
37
+ "@girs/gtk-4.0": "^4.22.1-4.0.0-beta.42",
38
+ "@girs/pango-1.0": "^1.57.0-4.0.0-beta.42",
39
+ "@girs/pangocairo-1.0": "^1.0.0-4.0.0-beta.42",
40
+ "@gjsify/dom-elements": "^0.1.0",
41
+ "@gjsify/event-bridge": "^0.1.0",
42
+ "@gjsify/utils": "^0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@gjsify/cli": "^0.1.0",
46
+ "@gjsify/unit": "^0.1.0",
47
+ "@types/node": "^25.5.0",
48
+ "typescript": "^6.0.2"
49
+ }
50
+ }
@@ -0,0 +1,243 @@
1
+ // Cairo utility helpers for Canvas 2D context
2
+ // Handles format conversions and path operations not directly available in Cairo.
3
+
4
+ import type Cairo from 'cairo';
5
+
6
+ /**
7
+ * Convert quadratic Bezier control point to cubic Bezier control points.
8
+ * Canvas 2D has quadraticCurveTo but Cairo only has cubic curveTo.
9
+ *
10
+ * Given current point (cx, cy), quadratic control point (cpx, cpy), and end (x, y):
11
+ * cp1 = current + 2/3 * (cp - current)
12
+ * cp2 = end + 2/3 * (cp - end)
13
+ */
14
+ export function quadraticToCubic(
15
+ cx: number, cy: number,
16
+ cpx: number, cpy: number,
17
+ x: number, y: number,
18
+ ): { cp1x: number; cp1y: number; cp2x: number; cp2y: number } {
19
+ return {
20
+ cp1x: cx + (2 / 3) * (cpx - cx),
21
+ cp1y: cy + (2 / 3) * (cpy - cy),
22
+ cp2x: x + (2 / 3) * (cpx - x),
23
+ cp2y: y + (2 / 3) * (cpy - y),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Compute arcTo parameters.
29
+ * Canvas arcTo(x1,y1,x2,y2,radius) draws a line from current point to the tangent point,
30
+ * then an arc of the given radius tangent to both lines (current→p1 and p1→p2).
31
+ *
32
+ * Returns the two tangent points and arc center, or null if degenerate (collinear points).
33
+ */
34
+ export function computeArcTo(
35
+ x0: number, y0: number,
36
+ x1: number, y1: number,
37
+ x2: number, y2: number,
38
+ radius: number,
39
+ ): { tx0: number; ty0: number; tx1: number; ty1: number; cx: number; cy: number; startAngle: number; endAngle: number; counterclockwise: boolean } | null {
40
+ // Direction vectors
41
+ const dx0 = x0 - x1;
42
+ const dy0 = y0 - y1;
43
+ const dx1 = x2 - x1;
44
+ const dy1 = y2 - y1;
45
+
46
+ // Lengths
47
+ const len0 = Math.sqrt(dx0 * dx0 + dy0 * dy0);
48
+ const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
49
+
50
+ if (len0 === 0 || len1 === 0) return null;
51
+
52
+ // Normalize
53
+ const ux0 = dx0 / len0;
54
+ const uy0 = dy0 / len0;
55
+ const ux1 = dx1 / len1;
56
+ const uy1 = dy1 / len1;
57
+
58
+ // Cross product to determine direction
59
+ const cross = ux0 * uy1 - uy0 * ux1;
60
+ if (Math.abs(cross) < 1e-10) return null; // Collinear
61
+
62
+ // Half-angle between the two direction vectors
63
+ const dot = ux0 * ux1 + uy0 * uy1;
64
+ const halfAngle = Math.acos(Math.max(-1, Math.min(1, dot))) / 2;
65
+
66
+ // Distance from p1 to tangent point
67
+ const tanDist = radius / Math.tan(halfAngle);
68
+
69
+ // Tangent points
70
+ const tx0 = x1 + ux0 * tanDist;
71
+ const ty0 = y1 + uy0 * tanDist;
72
+ const tx1 = x1 + ux1 * tanDist;
73
+ const ty1 = y1 + uy1 * tanDist;
74
+
75
+ // Center of the arc circle: perpendicular to the bisector, distance = radius / sin(halfAngle)
76
+ const centerDist = radius / Math.sin(halfAngle);
77
+ const bisectX = (ux0 + ux1) / 2;
78
+ const bisectY = (uy0 + uy1) / 2;
79
+ const bisectLen = Math.sqrt(bisectX * bisectX + bisectY * bisectY);
80
+ const cx = x1 + (bisectX / bisectLen) * centerDist;
81
+ const cy = y1 + (bisectY / bisectLen) * centerDist;
82
+
83
+ // Start and end angles
84
+ const startAngle = Math.atan2(ty0 - cy, tx0 - cx);
85
+ const endAngle = Math.atan2(ty1 - cy, tx1 - cx);
86
+
87
+ // Counterclockwise if cross product is positive
88
+ const counterclockwise = cross > 0;
89
+
90
+ return { tx0, ty0, tx1, ty1, cx, cy, startAngle, endAngle, counterclockwise };
91
+ }
92
+
93
+ /**
94
+ * Apply an arcTo operation to a Cairo context.
95
+ */
96
+ export function cairoArcTo(
97
+ ctx: Cairo.Context,
98
+ x0: number, y0: number,
99
+ x1: number, y1: number,
100
+ x2: number, y2: number,
101
+ radius: number,
102
+ ): void {
103
+ const result = computeArcTo(x0, y0, x1, y1, x2, y2, radius);
104
+ if (!result) {
105
+ // Degenerate: just draw a line to (x1, y1)
106
+ ctx.lineTo(x1, y1);
107
+ return;
108
+ }
109
+
110
+ const { tx0, ty0, cx, cy, startAngle, endAngle, counterclockwise } = result;
111
+
112
+ // Line from current point to first tangent point
113
+ ctx.lineTo(tx0, ty0);
114
+
115
+ // Arc
116
+ if (counterclockwise) {
117
+ ctx.arcNegative(cx, cy, radius, startAngle, endAngle);
118
+ } else {
119
+ ctx.arc(cx, cy, radius, startAngle, endAngle);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Draw an ellipse on a Cairo context.
125
+ * Canvas ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise)
126
+ * is implemented via save/translate/rotate/scale/arc/restore.
127
+ */
128
+ export function cairoEllipse(
129
+ ctx: Cairo.Context,
130
+ x: number, y: number,
131
+ radiusX: number, radiusY: number,
132
+ rotation: number,
133
+ startAngle: number, endAngle: number,
134
+ counterclockwise: boolean,
135
+ ): void {
136
+ ctx.save();
137
+ ctx.translate(x, y);
138
+ ctx.rotate(rotation);
139
+ ctx.scale(radiusX, radiusY);
140
+
141
+ if (counterclockwise) {
142
+ ctx.arcNegative(0, 0, 1, startAngle, endAngle);
143
+ } else {
144
+ ctx.arc(0, 0, 1, startAngle, endAngle);
145
+ }
146
+
147
+ ctx.restore();
148
+ }
149
+
150
+ /**
151
+ * Draw a rounded rectangle path on a Cairo context.
152
+ * Implements the Canvas roundRect(x, y, w, h, radii) method.
153
+ */
154
+ export function cairoRoundRect(
155
+ ctx: Cairo.Context,
156
+ x: number, y: number,
157
+ w: number, h: number,
158
+ radii: number | number[],
159
+ ): void {
160
+ // Normalize radii to [topLeft, topRight, bottomRight, bottomLeft]
161
+ let tl: number, tr: number, br: number, bl: number;
162
+ if (typeof radii === 'number') {
163
+ tl = tr = br = bl = radii;
164
+ } else if (radii.length === 1) {
165
+ tl = tr = br = bl = radii[0];
166
+ } else if (radii.length === 2) {
167
+ tl = br = radii[0];
168
+ tr = bl = radii[1];
169
+ } else if (radii.length === 3) {
170
+ tl = radii[0];
171
+ tr = bl = radii[1];
172
+ br = radii[2];
173
+ } else {
174
+ tl = radii[0];
175
+ tr = radii[1];
176
+ br = radii[2];
177
+ bl = radii[3];
178
+ }
179
+
180
+ // Clamp radii so they don't exceed half the width/height
181
+ const maxR = Math.min(w / 2, h / 2);
182
+ tl = Math.min(tl, maxR);
183
+ tr = Math.min(tr, maxR);
184
+ br = Math.min(br, maxR);
185
+ bl = Math.min(bl, maxR);
186
+
187
+ const PI_2 = Math.PI / 2;
188
+
189
+ ctx.newSubPath();
190
+ // Top-left corner
191
+ ctx.arc(x + tl, y + tl, tl, Math.PI, Math.PI + PI_2);
192
+ // Top-right corner
193
+ ctx.arc(x + w - tr, y + tr, tr, -PI_2, 0);
194
+ // Bottom-right corner
195
+ ctx.arc(x + w - br, y + h - br, br, 0, PI_2);
196
+ // Bottom-left corner
197
+ ctx.arc(x + bl, y + h - bl, bl, PI_2, Math.PI);
198
+ ctx.closePath();
199
+ }
200
+
201
+ /** Map Canvas globalCompositeOperation to Cairo.Operator values */
202
+ export const COMPOSITE_OP_MAP: Record<string, number> = {
203
+ 'source-over': 2, // OVER
204
+ 'source-in': 5, // IN
205
+ 'source-out': 6, // OUT
206
+ 'source-atop': 7, // ATOP
207
+ 'destination-over': 8, // DEST_OVER
208
+ 'destination-in': 9, // DEST_IN
209
+ 'destination-out': 10, // DEST_OUT
210
+ 'destination-atop': 11,// DEST_ATOP
211
+ 'lighter': 12, // ADD
212
+ 'copy': 1, // SOURCE
213
+ 'xor': 13, // XOR
214
+ 'multiply': 14, // MULTIPLY
215
+ 'screen': 15, // SCREEN
216
+ 'overlay': 16, // OVERLAY
217
+ 'darken': 17, // DARKEN
218
+ 'lighten': 18, // LIGHTEN
219
+ 'color-dodge': 19, // COLOR_DODGE
220
+ 'color-burn': 20, // COLOR_BURN
221
+ 'hard-light': 21, // HARD_LIGHT
222
+ 'soft-light': 22, // SOFT_LIGHT
223
+ 'difference': 23, // DIFFERENCE
224
+ 'exclusion': 24, // EXCLUSION
225
+ 'hue': 25, // HSL_HUE
226
+ 'saturation': 26, // HSL_SATURATION
227
+ 'color': 27, // HSL_COLOR
228
+ 'luminosity': 28, // HSL_LUMINOSITY
229
+ };
230
+
231
+ /** Map Canvas lineCap to Cairo.LineCap values */
232
+ export const LINE_CAP_MAP: Record<string, number> = {
233
+ 'butt': 0,
234
+ 'round': 1,
235
+ 'square': 2,
236
+ };
237
+
238
+ /** Map Canvas lineJoin to Cairo.LineJoin values */
239
+ export const LINE_JOIN_MAP: Record<string, number> = {
240
+ 'miter': 0,
241
+ 'round': 1,
242
+ 'bevel': 2,
243
+ };
@@ -0,0 +1,164 @@
1
+ // Canvas2DWidget GTK widget for GJS — original implementation using Gtk.DrawingArea + Cairo
2
+ // Provides a Gtk.DrawingArea subclass that handles Canvas 2D bootstrapping.
3
+ // Pattern follows packages/dom/iframe/src/iframe-widget.ts (IFrameWidget)
4
+
5
+ import GObject from 'gi://GObject';
6
+ import Gdk from 'gi://Gdk?version=4.0';
7
+ import GLib from 'gi://GLib?version=2.0';
8
+ import Gtk from 'gi://Gtk?version=4.0';
9
+ import { HTMLCanvasElement as GjsifyHTMLCanvasElement } from '@gjsify/dom-elements';
10
+ import { attachEventControllers } from '@gjsify/event-bridge';
11
+ import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
12
+
13
+ type Canvas2DReadyCallback = (canvas: globalThis.HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void;
14
+
15
+ /**
16
+ * A `Gtk.DrawingArea` subclass that handles Canvas 2D bootstrapping:
17
+ * - Creates an `HTMLCanvasElement` + `CanvasRenderingContext2D` on first draw
18
+ * - Blits the Canvas 2D Cairo.ImageSurface onto the DrawingArea each frame
19
+ * - Fires `onReady()` callbacks with (canvas, ctx) once the context is available
20
+ * - Provides `requestAnimationFrame()` backed by GTK frame clock (vsync)
21
+ * - `installGlobals()` sets `globalThis.requestAnimationFrame` and `globalThis.performance`
22
+ *
23
+ * Usage:
24
+ * ```ts
25
+ * const widget = new Canvas2DWidget();
26
+ * widget.installGlobals(); // sets globalThis.requestAnimationFrame
27
+ * widget.onReady((canvas, ctx) => {
28
+ * ctx.fillStyle = 'red';
29
+ * ctx.fillRect(0, 0, 100, 100);
30
+ * });
31
+ * window.set_child(widget);
32
+ * ```
33
+ */
34
+ export const Canvas2DWidget = GObject.registerClass(
35
+ { GTypeName: 'GjsifyCanvas2DWidget' },
36
+ class Canvas2DWidget extends Gtk.DrawingArea {
37
+ _canvas: GjsifyHTMLCanvasElement | null = null;
38
+ _ctx: CanvasRenderingContext2D | null = null;
39
+ _readyCallbacks: Canvas2DReadyCallback[] = [];
40
+ _tickCallbackId: number | null = null;
41
+ _frameCallback: FrameRequestCallback | null = null;
42
+ // Time origin in microseconds (GLib monotonic clock).
43
+ // Both requestAnimationFrame timestamps and performance.now() are
44
+ // relative to this origin, matching the browser DOMHighResTimeStamp spec.
45
+ _timeOrigin: number = GLib.get_monotonic_time();
46
+
47
+ constructor(params?: Partial<Gtk.DrawingArea.ConstructorProps>) {
48
+ super(params);
49
+ this.set_draw_func(this._onDraw.bind(this));
50
+
51
+ // Bridge GTK events → DOM events on the canvas element
52
+ attachEventControllers(this, () => this._canvas);
53
+
54
+ this.connect('unrealize', () => {
55
+ if (this._tickCallbackId !== null) {
56
+ this.remove_tick_callback(this._tickCallbackId);
57
+ this._tickCallbackId = null;
58
+ }
59
+ if (this._ctx) {
60
+ this._ctx._dispose();
61
+ }
62
+ this._canvas = null;
63
+ this._ctx = null;
64
+ });
65
+ }
66
+
67
+ /** @internal Draw function called by GTK. Blits the Cairo surface to screen. */
68
+ _onDraw(_area: Gtk.DrawingArea, cr: any, width: number, height: number): void {
69
+ // Lazy init: create canvas + 2D context on first draw
70
+ if (!this._canvas) {
71
+ this._canvas = new GjsifyHTMLCanvasElement();
72
+ this._canvas.width = width;
73
+ this._canvas.height = height;
74
+ // Import side-effect registers the '2d' factory, so getContext('2d') works
75
+ this._ctx = this._canvas.getContext('2d') as unknown as CanvasRenderingContext2D;
76
+ if (this._ctx) {
77
+ for (const cb of this._readyCallbacks) {
78
+ cb(this._canvas as unknown as globalThis.HTMLCanvasElement, this._ctx);
79
+ }
80
+ this._readyCallbacks = [];
81
+ }
82
+ }
83
+
84
+ // Sync dimensions if widget was resized
85
+ if (this._canvas.width !== width || this._canvas.height !== height) {
86
+ this._canvas.width = width;
87
+ this._canvas.height = height;
88
+ }
89
+
90
+ // Blit the Canvas 2D's Cairo.ImageSurface onto the DrawingArea
91
+ if (this._ctx) {
92
+ const surface = this._ctx._getSurface();
93
+ cr.setSourceSurface(surface, 0, 0);
94
+ cr.paint();
95
+ }
96
+ }
97
+
98
+ /** The HTMLCanvasElement backing this widget. Available after the first draw. */
99
+ get canvas(): globalThis.HTMLCanvasElement | null {
100
+ return this._canvas as unknown as globalThis.HTMLCanvasElement | null;
101
+ }
102
+
103
+ /** Get the 2D rendering context. Available after the first draw. */
104
+ getContext(_id: '2d'): CanvasRenderingContext2D | null {
105
+ return this._ctx;
106
+ }
107
+
108
+ /**
109
+ * Register a callback to be invoked once the Canvas 2D context is ready.
110
+ * If the context is already available, the callback fires synchronously.
111
+ */
112
+ onReady(cb: Canvas2DReadyCallback): void {
113
+ if (this._canvas && this._ctx) {
114
+ cb(this._canvas as unknown as globalThis.HTMLCanvasElement, this._ctx);
115
+ return;
116
+ }
117
+ this._readyCallbacks.push(cb);
118
+ }
119
+
120
+ /**
121
+ * Schedules a single animation frame callback, matching the browser `requestAnimationFrame` API.
122
+ * Backed by GTK frame clock (vsync-synced, typically ~60 FPS).
123
+ * Returns 0 (handle — cancel not yet implemented).
124
+ */
125
+ requestAnimationFrame(cb: FrameRequestCallback): number {
126
+ this._frameCallback = cb;
127
+ if (this._tickCallbackId === null) {
128
+ this._tickCallbackId = this.add_tick_callback((_widget: Gtk.Widget, frameClock: Gdk.FrameClock) => {
129
+ this._tickCallbackId = null;
130
+ // DOMHighResTimeStamp: ms since time origin, matching performance.now()
131
+ const time = (frameClock.get_frame_time() - this._timeOrigin) / 1000;
132
+ this._frameCallback?.(time);
133
+ this.queue_draw();
134
+ return GLib.SOURCE_REMOVE;
135
+ });
136
+ }
137
+ // Ensure GTK schedules a new frame so the tick callback fires.
138
+ // Without this, requestAnimationFrame called during the paint phase
139
+ // (e.g. from onReady) may not trigger the frame clock to tick again.
140
+ this.queue_draw();
141
+ return 0;
142
+ }
143
+
144
+ /**
145
+ * Sets browser globals (`requestAnimationFrame`, `performance`) so that
146
+ * browser-targeted code works unchanged on GJS.
147
+ */
148
+ installGlobals(): void {
149
+ (globalThis as any).requestAnimationFrame = (cb: FrameRequestCallback) =>
150
+ this.requestAnimationFrame(cb);
151
+ // Install performance.now() on the same time origin as rAF timestamps.
152
+ // Always override to ensure consistency — native GJS performance.now()
153
+ // may use a different time origin than the frame clock.
154
+ const timeOrigin = this._timeOrigin;
155
+ (globalThis as any).performance = {
156
+ now: () => (GLib.get_monotonic_time() - timeOrigin) / 1000,
157
+ timeOrigin: Date.now(),
158
+ };
159
+ }
160
+ }
161
+ );
162
+
163
+ // Export the instance type so callers can type-annotate their Canvas2DWidget variables
164
+ export type Canvas2DWidget = InstanceType<typeof Canvas2DWidget>;