@gjsify/canvas2d-core 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.
- package/lib/esm/cairo-utils.js +172 -0
- package/lib/esm/canvas-gradient.js +23 -0
- package/lib/esm/canvas-path.js +104 -0
- package/lib/esm/canvas-pattern.js +58 -0
- package/lib/esm/canvas-rendering-context-2d.js +885 -0
- package/lib/esm/canvas-state.js +39 -0
- package/lib/esm/color.js +209 -0
- package/lib/esm/image-data.js +22 -0
- package/lib/esm/index.js +14 -0
- package/lib/types/cairo-utils.d.ts +54 -0
- package/lib/types/canvas-gradient.d.ts +11 -0
- package/lib/types/canvas-path.d.ts +80 -0
- package/lib/types/canvas-pattern.d.ts +12 -0
- package/lib/types/canvas-rendering-context-2d.d.ts +164 -0
- package/lib/types/canvas-state.d.ts +27 -0
- package/lib/types/color.d.ts +15 -0
- package/lib/types/image-data.d.ts +12 -0
- package/lib/types/index.d.ts +6 -0
- package/package.json +47 -0
- package/src/cairo-utils.ts +243 -0
- package/src/canvas-gradient.ts +36 -0
- package/src/canvas-path.ts +131 -0
- package/src/canvas-pattern.ts +75 -0
- package/src/canvas-rendering-context-2d.ts +1007 -0
- package/src/canvas-state.ts +77 -0
- package/src/canvas-text.spec.ts +241 -0
- package/src/color.ts +125 -0
- package/src/image-data.ts +34 -0
- package/src/index.ts +14 -0
- package/src/test.mts +6 -0
- package/tmp/.tsbuildinfo +1 -0
- package/tsconfig.json +47 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/canvas2d-core",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Cairo-backed Canvas 2D core (CanvasRenderingContext2D, Path2D, ImageData) — no GTK dependency",
|
|
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
|
+
"offscreen"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@girs/gdk-4.0": "^4.0.0-4.0.0-rc.1",
|
|
34
|
+
"@girs/gdkpixbuf-2.0": "^2.0.0-4.0.0-rc.1",
|
|
35
|
+
"@girs/gjs": "^4.0.0-rc.1",
|
|
36
|
+
"@girs/glib-2.0": "^2.88.0-4.0.0-rc.1",
|
|
37
|
+
"@girs/gobject-2.0": "^2.88.0-4.0.0-rc.1",
|
|
38
|
+
"@girs/pango-1.0": "^1.57.1-4.0.0-rc.1",
|
|
39
|
+
"@girs/pangocairo-1.0": "^1.0.0-4.0.0-rc.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@gjsify/cli": "^0.1.3",
|
|
43
|
+
"@gjsify/unit": "^0.1.3",
|
|
44
|
+
"@types/node": "^25.5.2",
|
|
45
|
+
"typescript": "^6.0.2"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -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,36 @@
|
|
|
1
|
+
// CanvasGradient implementation backed by Cairo gradient patterns
|
|
2
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient
|
|
3
|
+
|
|
4
|
+
import Cairo from 'cairo';
|
|
5
|
+
import { parseColor } from './color.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CanvasGradient wrapping a Cairo LinearGradient or RadialGradient.
|
|
9
|
+
*/
|
|
10
|
+
export class CanvasGradient {
|
|
11
|
+
private _pattern: Cairo.LinearGradient | Cairo.RadialGradient;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
type: 'linear' | 'radial',
|
|
15
|
+
x0: number, y0: number,
|
|
16
|
+
x1: number, y1: number,
|
|
17
|
+
r0?: number, r1?: number,
|
|
18
|
+
) {
|
|
19
|
+
if (type === 'radial') {
|
|
20
|
+
this._pattern = new Cairo.RadialGradient(x0, y0, r0!, x1, y1, r1!);
|
|
21
|
+
} else {
|
|
22
|
+
this._pattern = new Cairo.LinearGradient(x0, y0, x1, y1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addColorStop(offset: number, color: string): void {
|
|
27
|
+
const parsed = parseColor(color);
|
|
28
|
+
if (!parsed) return;
|
|
29
|
+
this._pattern.addColorStopRGBA(offset, parsed.r, parsed.g, parsed.b, parsed.a);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @internal Get the underlying Cairo pattern for rendering. */
|
|
33
|
+
_getCairoPattern(): Cairo.LinearGradient | Cairo.RadialGradient {
|
|
34
|
+
return this._pattern;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Path2D implementation for Canvas 2D context
|
|
2
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/Path2D
|
|
3
|
+
// Records path operations and replays them on a Cairo context.
|
|
4
|
+
|
|
5
|
+
import { quadraticToCubic, cairoRoundRect } from './cairo-utils.js';
|
|
6
|
+
|
|
7
|
+
/** A recorded path operation. */
|
|
8
|
+
type PathOp =
|
|
9
|
+
| { type: 'moveTo'; x: number; y: number }
|
|
10
|
+
| { type: 'lineTo'; x: number; y: number }
|
|
11
|
+
| { type: 'closePath' }
|
|
12
|
+
| { type: 'bezierCurveTo'; cp1x: number; cp1y: number; cp2x: number; cp2y: number; x: number; y: number }
|
|
13
|
+
| { type: 'quadraticCurveTo'; cpx: number; cpy: number; x: number; y: number }
|
|
14
|
+
| { type: 'arc'; x: number; y: number; radius: number; startAngle: number; endAngle: number; ccw: boolean }
|
|
15
|
+
| { type: 'ellipse'; x: number; y: number; rx: number; ry: number; rotation: number; startAngle: number; endAngle: number; ccw: boolean }
|
|
16
|
+
| { type: 'rect'; x: number; y: number; w: number; h: number }
|
|
17
|
+
| { type: 'roundRect'; x: number; y: number; w: number; h: number; radii: number | number[] };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Path2D records path operations for later replay on a CanvasRenderingContext2D.
|
|
21
|
+
*/
|
|
22
|
+
export class Path2D {
|
|
23
|
+
/** @internal Recorded operations */
|
|
24
|
+
_ops: PathOp[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(pathOrSvg?: Path2D | string) {
|
|
27
|
+
if (pathOrSvg instanceof Path2D) {
|
|
28
|
+
this._ops = [...pathOrSvg._ops];
|
|
29
|
+
}
|
|
30
|
+
// SVG path string parsing is not implemented (complex, rarely needed)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addPath(path: Path2D): void {
|
|
34
|
+
this._ops.push(...path._ops);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
moveTo(x: number, y: number): void {
|
|
38
|
+
this._ops.push({ type: 'moveTo', x, y });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lineTo(x: number, y: number): void {
|
|
42
|
+
this._ops.push({ type: 'lineTo', x, y });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
closePath(): void {
|
|
46
|
+
this._ops.push({ type: 'closePath' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void {
|
|
50
|
+
this._ops.push({ type: 'bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void {
|
|
54
|
+
this._ops.push({ type: 'quadraticCurveTo', cpx, cpy, x, y });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, counterclockwise = false): void {
|
|
58
|
+
this._ops.push({ type: 'arc', x, y, radius, startAngle, endAngle, ccw: counterclockwise });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, counterclockwise = false): void {
|
|
62
|
+
if (radiusX < 0 || radiusY < 0) throw new RangeError('The radii provided are negative');
|
|
63
|
+
this._ops.push({ type: 'ellipse', x, y, rx: radiusX, ry: radiusY, rotation, startAngle, endAngle, ccw: counterclockwise });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rect(x: number, y: number, w: number, h: number): void {
|
|
67
|
+
this._ops.push({ type: 'rect', x, y, w, h });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
roundRect(x: number, y: number, w: number, h: number, radii: number | number[] = 0): void {
|
|
71
|
+
this._ops.push({ type: 'roundRect', x, y, w, h, radii });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @internal Replay all recorded path operations onto a Cairo context.
|
|
76
|
+
*/
|
|
77
|
+
_replayOnCairo(ctx: import('cairo').default.Context): void {
|
|
78
|
+
let lastX = 0, lastY = 0;
|
|
79
|
+
|
|
80
|
+
for (const op of this._ops) {
|
|
81
|
+
switch (op.type) {
|
|
82
|
+
case 'moveTo':
|
|
83
|
+
ctx.moveTo(op.x, op.y);
|
|
84
|
+
lastX = op.x; lastY = op.y;
|
|
85
|
+
break;
|
|
86
|
+
case 'lineTo':
|
|
87
|
+
ctx.lineTo(op.x, op.y);
|
|
88
|
+
lastX = op.x; lastY = op.y;
|
|
89
|
+
break;
|
|
90
|
+
case 'closePath':
|
|
91
|
+
ctx.closePath();
|
|
92
|
+
break;
|
|
93
|
+
case 'bezierCurveTo':
|
|
94
|
+
ctx.curveTo(op.cp1x, op.cp1y, op.cp2x, op.cp2y, op.x, op.y);
|
|
95
|
+
lastX = op.x; lastY = op.y;
|
|
96
|
+
break;
|
|
97
|
+
case 'quadraticCurveTo': {
|
|
98
|
+
const { cp1x, cp1y, cp2x, cp2y } = quadraticToCubic(lastX, lastY, op.cpx, op.cpy, op.x, op.y);
|
|
99
|
+
ctx.curveTo(cp1x, cp1y, cp2x, cp2y, op.x, op.y);
|
|
100
|
+
lastX = op.x; lastY = op.y;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'arc':
|
|
104
|
+
if (op.ccw) {
|
|
105
|
+
ctx.arcNegative(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
106
|
+
} else {
|
|
107
|
+
ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'ellipse':
|
|
111
|
+
ctx.save();
|
|
112
|
+
ctx.translate(op.x, op.y);
|
|
113
|
+
ctx.rotate(op.rotation);
|
|
114
|
+
ctx.scale(op.rx, op.ry);
|
|
115
|
+
if (op.ccw) {
|
|
116
|
+
ctx.arcNegative(0, 0, 1, op.startAngle, op.endAngle);
|
|
117
|
+
} else {
|
|
118
|
+
ctx.arc(0, 0, 1, op.startAngle, op.endAngle);
|
|
119
|
+
}
|
|
120
|
+
ctx.restore();
|
|
121
|
+
break;
|
|
122
|
+
case 'rect':
|
|
123
|
+
ctx.rectangle(op.x, op.y, op.w, op.h);
|
|
124
|
+
break;
|
|
125
|
+
case 'roundRect':
|
|
126
|
+
cairoRoundRect(ctx, op.x, op.y, op.w, op.h, op.radii);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// CanvasPattern implementation backed by Cairo SurfacePattern
|
|
2
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern
|
|
3
|
+
|
|
4
|
+
import Cairo from 'cairo';
|
|
5
|
+
import Gdk from 'gi://Gdk?version=4.0';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CanvasPattern wrapping a Cairo SurfacePattern.
|
|
9
|
+
*/
|
|
10
|
+
export class CanvasPattern {
|
|
11
|
+
private _pattern: Cairo.SurfacePattern;
|
|
12
|
+
|
|
13
|
+
private constructor(surface: Cairo.ImageSurface, repetition: string | null) {
|
|
14
|
+
this._pattern = new Cairo.SurfacePattern(surface);
|
|
15
|
+
|
|
16
|
+
// Set extend mode based on repetition
|
|
17
|
+
// setExtend exists at runtime on SurfacePattern but is missing from GIR types
|
|
18
|
+
const pat = this._pattern as any;
|
|
19
|
+
switch (repetition) {
|
|
20
|
+
case 'repeat':
|
|
21
|
+
case '':
|
|
22
|
+
case null:
|
|
23
|
+
pat.setExtend(Cairo.Extend.REPEAT);
|
|
24
|
+
break;
|
|
25
|
+
case 'repeat-x':
|
|
26
|
+
case 'repeat-y':
|
|
27
|
+
// Cairo doesn't have separate x/y repeat — use REPEAT as approximation
|
|
28
|
+
pat.setExtend(Cairo.Extend.REPEAT);
|
|
29
|
+
break;
|
|
30
|
+
case 'no-repeat':
|
|
31
|
+
pat.setExtend(Cairo.Extend.NONE);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Create a CanvasPattern from a supported image source. Returns null if unsupported. */
|
|
37
|
+
static create(image: any, repetition: string | null): CanvasPattern | null {
|
|
38
|
+
// HTMLImageElement (GdkPixbuf-backed)
|
|
39
|
+
if ('isPixbuf' in image && typeof (image as any).isPixbuf === 'function' && (image as any).isPixbuf()) {
|
|
40
|
+
const pixbuf = (image as any)._pixbuf as import('@girs/gdkpixbuf-2.0').default.Pixbuf;
|
|
41
|
+
// Create a Cairo surface from the pixbuf
|
|
42
|
+
const w = pixbuf.get_width();
|
|
43
|
+
const h = pixbuf.get_height();
|
|
44
|
+
const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
|
|
45
|
+
const ctx = new Cairo.Context(surface);
|
|
46
|
+
Gdk.cairo_set_source_pixbuf(ctx as any, pixbuf, 0, 0);
|
|
47
|
+
ctx.paint();
|
|
48
|
+
ctx.$dispose();
|
|
49
|
+
return new CanvasPattern(surface, repetition);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// HTMLCanvasElement with a 2D context
|
|
53
|
+
if (typeof image?.getContext === 'function') {
|
|
54
|
+
const ctx2d = image.getContext('2d');
|
|
55
|
+
if (ctx2d && typeof ctx2d._getSurface === 'function') {
|
|
56
|
+
const sourceSurface = ctx2d._getSurface() as Cairo.ImageSurface;
|
|
57
|
+
const w = sourceSurface.getWidth();
|
|
58
|
+
const h = sourceSurface.getHeight();
|
|
59
|
+
const surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, w, h);
|
|
60
|
+
const ctx = new Cairo.Context(surface);
|
|
61
|
+
ctx.setSourceSurface(sourceSurface, 0, 0);
|
|
62
|
+
ctx.paint();
|
|
63
|
+
ctx.$dispose();
|
|
64
|
+
return new CanvasPattern(surface, repetition);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @internal Get the underlying Cairo pattern for rendering. */
|
|
72
|
+
_getCairoPattern(): Cairo.SurfacePattern {
|
|
73
|
+
return this._pattern;
|
|
74
|
+
}
|
|
75
|
+
}
|