@gjsify/canvas2d-core 0.4.0 → 0.4.4
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/package.json +50 -46
- package/src/cairo-types.ts +0 -44
- package/src/cairo-utils.ts +0 -254
- package/src/canvas-clearing.spec.ts +0 -126
- package/src/canvas-color.spec.ts +0 -113
- package/src/canvas-composite.spec.ts +0 -114
- package/src/canvas-drawimage.spec.ts +0 -334
- package/src/canvas-gradient.ts +0 -36
- package/src/canvas-imagedata.spec.ts +0 -150
- package/src/canvas-path.ts +0 -131
- package/src/canvas-pattern.ts +0 -84
- package/src/canvas-rendering-context-2d.ts +0 -1208
- package/src/canvas-state.spec.ts +0 -245
- package/src/canvas-state.ts +0 -77
- package/src/canvas-text.spec.ts +0 -241
- package/src/canvas-transform.spec.ts +0 -211
- package/src/color.ts +0 -177
- package/src/dom-types.ts +0 -96
- package/src/image-data.ts +0 -34
- package/src/index.ts +0 -14
- package/src/test.browser.mts +0 -614
- package/src/test.mts +0 -22
- package/tmp/.tsbuildinfo +0 -1
- package/tsconfig.json +0 -47
package/package.json
CHANGED
|
@@ -1,48 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"name": "@gjsify/canvas2d-core",
|
|
3
|
+
"version": "0.4.4",
|
|
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
|
+
"files": [
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clear": "rm -rf lib tmp tsconfig.tsbuildinfo test.gjs.mjs || exit 0",
|
|
19
|
+
"check": "tsc --noEmit",
|
|
20
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
21
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
22
|
+
"build:types": "tsc",
|
|
23
|
+
"build:test": "gjsify run build:test:gjs",
|
|
24
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
25
|
+
"build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
|
|
26
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
|
|
27
|
+
"test:gjs": "gjsify run test.gjs.mjs"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"gjs",
|
|
31
|
+
"canvas",
|
|
32
|
+
"canvas2d",
|
|
33
|
+
"cairo",
|
|
34
|
+
"offscreen"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@girs/gdk-4.0": "4.0.0-4.0.0-rc.15",
|
|
38
|
+
"@girs/gdkpixbuf-2.0": "2.0.0-4.0.0-rc.15",
|
|
39
|
+
"@girs/gjs": "4.0.0-rc.15",
|
|
40
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
|
|
41
|
+
"@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
|
|
42
|
+
"@girs/pango-1.0": "1.57.1-4.0.0-rc.15",
|
|
43
|
+
"@girs/pangocairo-1.0": "1.0.0-4.0.0-rc.15"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@gjsify/cli": "^0.4.4",
|
|
47
|
+
"@gjsify/dom-elements": "^0.4.4",
|
|
48
|
+
"@gjsify/unit": "^0.4.4",
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
12
51
|
}
|
|
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
|
-
"build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
|
|
23
|
-
"test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
|
|
24
|
-
"test:gjs": "gjsify run test.gjs.mjs"
|
|
25
|
-
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"gjs",
|
|
28
|
-
"canvas",
|
|
29
|
-
"canvas2d",
|
|
30
|
-
"cairo",
|
|
31
|
-
"offscreen"
|
|
32
|
-
],
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"@girs/gdk-4.0": "4.0.0-4.0.0-rc.15",
|
|
35
|
-
"@girs/gdkpixbuf-2.0": "2.0.0-4.0.0-rc.15",
|
|
36
|
-
"@girs/gjs": "4.0.0-rc.15",
|
|
37
|
-
"@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
|
|
38
|
-
"@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
|
|
39
|
-
"@girs/pango-1.0": "1.57.1-4.0.0-rc.15",
|
|
40
|
-
"@girs/pangocairo-1.0": "1.0.0-4.0.0-rc.15"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@gjsify/cli": "^0.4.0",
|
|
44
|
-
"@gjsify/unit": "^0.4.0",
|
|
45
|
-
"@types/node": "^25.6.2",
|
|
46
|
-
"typescript": "^6.0.3"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
52
|
+
}
|
package/src/cairo-types.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Local typed shape for Cairo objects whose runtime methods are missing from
|
|
2
|
-
// the GIR-generated `@girs/cairo-1.0` / `@girs/gjs/cairo` typings.
|
|
3
|
-
//
|
|
4
|
-
// `setExtend()` and `setFilter()` exist at runtime on every Cairo pattern but
|
|
5
|
-
// are absent from the .d.ts (the GIR generator emits an empty `class Pattern`
|
|
6
|
-
// for "Foreign Struct" types). Rather than reach for `(pat as any).setFilter`
|
|
7
|
-
// at every call site, we declare a thin interface here and narrow the
|
|
8
|
-
// `Cairo.Pattern` returned by `Cairo.Context.getSource()` through a single
|
|
9
|
-
// helper.
|
|
10
|
-
//
|
|
11
|
-
// Reference: https://www.cairographics.org/manual/cairo-cairo-pattern-t.html
|
|
12
|
-
//
|
|
13
|
-
// This is a pure type-level construct; runtime behavior is unchanged.
|
|
14
|
-
|
|
15
|
-
import type Cairo from 'cairo';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The `setExtend` / `setFilter` slice that exists at runtime on every
|
|
19
|
-
* `cairo_pattern_t` but is missing from the GIR types. Combined with
|
|
20
|
-
* `Cairo.Pattern` to give us a fully-typed view.
|
|
21
|
-
*/
|
|
22
|
-
export interface CairoPatternRuntime {
|
|
23
|
-
setExtend(extend: Cairo.Extend): void;
|
|
24
|
-
getExtend(): Cairo.Extend;
|
|
25
|
-
setFilter(filter: Cairo.Filter): void;
|
|
26
|
-
getFilter(): Cairo.Filter;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** A `Cairo.Pattern` augmented with the runtime methods documented above. */
|
|
30
|
-
export type CairoPattern = Cairo.Pattern & CairoPatternRuntime;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Narrow a `Cairo.Pattern` returned by the GIR API to the augmented type.
|
|
34
|
-
* Returns `null` if the input is missing the runtime methods (e.g. a future
|
|
35
|
-
* Cairo binding that reshapes the API).
|
|
36
|
-
*/
|
|
37
|
-
export function asCairoPattern(pat: Cairo.Pattern | undefined | null): CairoPattern | null {
|
|
38
|
-
if (!pat) return null;
|
|
39
|
-
const candidate = pat as CairoPattern;
|
|
40
|
-
if (typeof candidate.setFilter !== 'function' || typeof candidate.setExtend !== 'function') {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
return candidate;
|
|
44
|
-
}
|
package/src/cairo-utils.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
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
|
-
/**
|
|
202
|
-
* Map Canvas globalCompositeOperation to Cairo.Operator values.
|
|
203
|
-
*
|
|
204
|
-
* Cairo.Operator enum (verified runtime in GJS 1.86):
|
|
205
|
-
* CLEAR=0, SOURCE=1, OVER=2, IN=3, OUT=4, ATOP=5,
|
|
206
|
-
* DEST=6, DEST_OVER=7, DEST_IN=8, DEST_OUT=9, DEST_ATOP=10,
|
|
207
|
-
* XOR=11, ADD=12, SATURATE=13,
|
|
208
|
-
* MULTIPLY=14, SCREEN=15, OVERLAY=16, DARKEN=17, LIGHTEN=18,
|
|
209
|
-
* COLOR_DODGE=19, COLOR_BURN=20, HARD_LIGHT=21, SOFT_LIGHT=22,
|
|
210
|
-
* DIFFERENCE=23, EXCLUSION=24, HSL_HUE=25, HSL_SATURATION=26,
|
|
211
|
-
* HSL_COLOR=27, HSL_LUMINOSITY=28
|
|
212
|
-
*/
|
|
213
|
-
export const COMPOSITE_OP_MAP: Record<string, number> = {
|
|
214
|
-
'source-over': 2, // OVER
|
|
215
|
-
'source-in': 3, // IN
|
|
216
|
-
'source-out': 4, // OUT
|
|
217
|
-
'source-atop': 5, // ATOP
|
|
218
|
-
'destination-over': 7, // DEST_OVER
|
|
219
|
-
'destination-in': 8, // DEST_IN
|
|
220
|
-
'destination-out': 9, // DEST_OUT
|
|
221
|
-
'destination-atop': 10,// DEST_ATOP
|
|
222
|
-
'lighter': 12, // ADD
|
|
223
|
-
'copy': 1, // SOURCE
|
|
224
|
-
'xor': 11, // XOR
|
|
225
|
-
'multiply': 14, // MULTIPLY
|
|
226
|
-
'screen': 15, // SCREEN
|
|
227
|
-
'overlay': 16, // OVERLAY
|
|
228
|
-
'darken': 17, // DARKEN
|
|
229
|
-
'lighten': 18, // LIGHTEN
|
|
230
|
-
'color-dodge': 19, // COLOR_DODGE
|
|
231
|
-
'color-burn': 20, // COLOR_BURN
|
|
232
|
-
'hard-light': 21, // HARD_LIGHT
|
|
233
|
-
'soft-light': 22, // SOFT_LIGHT
|
|
234
|
-
'difference': 23, // DIFFERENCE
|
|
235
|
-
'exclusion': 24, // EXCLUSION
|
|
236
|
-
'hue': 25, // HSL_HUE
|
|
237
|
-
'saturation': 26, // HSL_SATURATION
|
|
238
|
-
'color': 27, // HSL_COLOR
|
|
239
|
-
'luminosity': 28, // HSL_LUMINOSITY
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
/** Map Canvas lineCap to Cairo.LineCap values */
|
|
243
|
-
export const LINE_CAP_MAP: Record<string, number> = {
|
|
244
|
-
'butt': 0,
|
|
245
|
-
'round': 1,
|
|
246
|
-
'square': 2,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
/** Map Canvas lineJoin to Cairo.LineJoin values */
|
|
250
|
-
export const LINE_JOIN_MAP: Record<string, number> = {
|
|
251
|
-
'miter': 0,
|
|
252
|
-
'round': 1,
|
|
253
|
-
'bevel': 2,
|
|
254
|
-
};
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Canvas 2D clearRect tests — verifies the Cairo.Operator.CLEAR path
|
|
2
|
-
// correctly clears the specified rectangle regardless of transform,
|
|
3
|
-
// clip, globalAlpha, and globalCompositeOperation.
|
|
4
|
-
//
|
|
5
|
-
// Ported from refs/wpt/html/canvas/element/drawing-rectangles-to-the-canvas/
|
|
6
|
-
// 2d.clearRect.{basic,transform,clip,globalalpha,globalcomposite,
|
|
7
|
-
// negative,path,nonfinite}.html
|
|
8
|
-
// Original: Copyright (c) Web Platform Tests contributors. 3-Clause BSD.
|
|
9
|
-
// Reimplemented for GJS using @gjsify/canvas2d-core + @gjsify/unit.
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
12
|
-
import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
|
|
13
|
-
|
|
14
|
-
function makeCtx(width = 30, height = 30): CanvasRenderingContext2D {
|
|
15
|
-
const canvas = { width, height };
|
|
16
|
-
return new CanvasRenderingContext2D(canvas);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function assertPixel(
|
|
20
|
-
ctx: CanvasRenderingContext2D,
|
|
21
|
-
x: number,
|
|
22
|
-
y: number,
|
|
23
|
-
r: number, g: number, b: number, a: number,
|
|
24
|
-
): void {
|
|
25
|
-
const data = ctx.getImageData(x, y, 1, 1).data;
|
|
26
|
-
expect(data[0]).toBe(r);
|
|
27
|
-
expect(data[1]).toBe(g);
|
|
28
|
-
expect(data[2]).toBe(b);
|
|
29
|
-
expect(data[3]).toBe(a);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default async () => {
|
|
33
|
-
await describe('CanvasRenderingContext2D — clearRect', async () => {
|
|
34
|
-
|
|
35
|
-
await it('clears a red-filled canvas to transparent black', async () => {
|
|
36
|
-
const ctx = makeCtx(20, 20);
|
|
37
|
-
ctx.fillStyle = 'rgb(255, 0, 0)';
|
|
38
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
39
|
-
assertPixel(ctx, 10, 10, 255, 0, 0, 255);
|
|
40
|
-
ctx.clearRect(0, 0, 20, 20);
|
|
41
|
-
assertPixel(ctx, 10, 10, 0, 0, 0, 0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
await it('clears only the specified sub-rectangle', async () => {
|
|
45
|
-
const ctx = makeCtx(20, 20);
|
|
46
|
-
ctx.fillStyle = 'rgb(0, 0, 255)';
|
|
47
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
48
|
-
ctx.clearRect(5, 5, 10, 10);
|
|
49
|
-
// Inside cleared region
|
|
50
|
-
assertPixel(ctx, 10, 10, 0, 0, 0, 0);
|
|
51
|
-
// Outside cleared region — still blue
|
|
52
|
-
assertPixel(ctx, 2, 2, 0, 0, 255, 255);
|
|
53
|
-
assertPixel(ctx, 17, 17, 0, 0, 255, 255);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
await it('clearRect is transformed by the current matrix', async () => {
|
|
57
|
-
const ctx = makeCtx(30, 30);
|
|
58
|
-
ctx.fillStyle = 'rgb(0, 255, 0)';
|
|
59
|
-
ctx.fillRect(0, 0, 30, 30);
|
|
60
|
-
ctx.translate(10, 10);
|
|
61
|
-
ctx.clearRect(0, 0, 5, 5);
|
|
62
|
-
// The cleared region is at (10,10)-(15,15) in canvas coords
|
|
63
|
-
assertPixel(ctx, 12, 12, 0, 0, 0, 0);
|
|
64
|
-
// Pixel just outside the cleared region — still green
|
|
65
|
-
assertPixel(ctx, 16, 16, 0, 255, 0, 255);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await it('clearRect ignores globalAlpha (spec: always 0)', async () => {
|
|
69
|
-
const ctx = makeCtx(20, 20);
|
|
70
|
-
ctx.fillStyle = 'red';
|
|
71
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
72
|
-
ctx.globalAlpha = 0.5;
|
|
73
|
-
ctx.clearRect(0, 0, 20, 20);
|
|
74
|
-
// Spec: clearRect completely clears, globalAlpha is ignored
|
|
75
|
-
assertPixel(ctx, 10, 10, 0, 0, 0, 0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await it('clearRect ignores globalCompositeOperation (spec: always CLEAR)', async () => {
|
|
79
|
-
const ctx = makeCtx(20, 20);
|
|
80
|
-
ctx.fillStyle = 'red';
|
|
81
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
82
|
-
ctx.globalCompositeOperation = 'xor';
|
|
83
|
-
ctx.clearRect(0, 0, 20, 20);
|
|
84
|
-
// Spec: clearRect uses CLEAR operator regardless of
|
|
85
|
-
// globalCompositeOperation
|
|
86
|
-
assertPixel(ctx, 10, 10, 0, 0, 0, 0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
await it('clearRect with negative width clears the normalized region', async () => {
|
|
90
|
-
const ctx = makeCtx(20, 20);
|
|
91
|
-
ctx.fillStyle = 'rgb(255, 255, 0)';
|
|
92
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
93
|
-
// clearRect(15, 5, -10, 10) ≡ clearRect(5, 5, 10, 10)
|
|
94
|
-
ctx.clearRect(15, 5, -10, 10);
|
|
95
|
-
assertPixel(ctx, 10, 10, 0, 0, 0, 0);
|
|
96
|
-
assertPixel(ctx, 2, 2, 255, 255, 0, 255);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await it('clearRect restricted by a clip region', async () => {
|
|
100
|
-
const ctx = makeCtx(30, 30);
|
|
101
|
-
ctx.fillStyle = 'rgb(255, 0, 255)';
|
|
102
|
-
ctx.fillRect(0, 0, 30, 30);
|
|
103
|
-
// Clip to a 10×10 box starting at (10,10)
|
|
104
|
-
ctx.beginPath();
|
|
105
|
-
ctx.rect(10, 10, 10, 10);
|
|
106
|
-
ctx.clip();
|
|
107
|
-
// Try to clear the full canvas — only the clipped region should
|
|
108
|
-
// actually be cleared.
|
|
109
|
-
ctx.clearRect(0, 0, 30, 30);
|
|
110
|
-
// Inside clip → cleared
|
|
111
|
-
assertPixel(ctx, 15, 15, 0, 0, 0, 0);
|
|
112
|
-
// Outside clip → still filled
|
|
113
|
-
assertPixel(ctx, 5, 5, 255, 0, 255, 255);
|
|
114
|
-
assertPixel(ctx, 25, 25, 255, 0, 255, 255);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await it('clearRect with zero width is a no-op', async () => {
|
|
118
|
-
const ctx = makeCtx(20, 20);
|
|
119
|
-
ctx.fillStyle = 'rgb(128, 128, 128)';
|
|
120
|
-
ctx.fillRect(0, 0, 20, 20);
|
|
121
|
-
ctx.clearRect(5, 5, 0, 10);
|
|
122
|
-
// Nothing cleared — pixel still grey
|
|
123
|
-
assertPixel(ctx, 10, 10, 128, 128, 128, 255);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
};
|
package/src/canvas-color.spec.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// parseColor tests — RGB, hex, named, and HSL formats
|
|
2
|
-
// Covers Excalibur's non-standard HSL output (0-1 normalized values without %)
|
|
3
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl
|
|
4
|
-
|
|
5
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
-
import { parseColor } from './color.js';
|
|
7
|
-
|
|
8
|
-
export default async () => {
|
|
9
|
-
|
|
10
|
-
await describe('parseColor — hex and named', async () => {
|
|
11
|
-
await it('parses #ffffff as white', async () => {
|
|
12
|
-
const c = parseColor('#ffffff')!;
|
|
13
|
-
expect(c.r).toBe(1); expect(c.g).toBe(1); expect(c.b).toBe(1); expect(c.a).toBe(1);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
await it('parses named "white" as white', async () => {
|
|
17
|
-
const c = parseColor('white')!;
|
|
18
|
-
expect(c.r).toBe(1); expect(c.g).toBe(1); expect(c.b).toBe(1);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
await it('parses "black" as black', async () => {
|
|
22
|
-
const c = parseColor('black')!;
|
|
23
|
-
expect(c.r).toBe(0); expect(c.g).toBe(0); expect(c.b).toBe(0);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await describe('parseColor — rgb()/rgba()', async () => {
|
|
28
|
-
await it('parses rgb(255,0,0) as red', async () => {
|
|
29
|
-
const c = parseColor('rgb(255, 0, 0)')!;
|
|
30
|
-
expect(c.r).toBe(1); expect(c.g).toBe(0); expect(c.b).toBe(0); expect(c.a).toBe(1);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await it('parses rgba(0,0,255,0.5) as semi-transparent blue', async () => {
|
|
34
|
-
const c = parseColor('rgba(0, 0, 255, 0.5)')!;
|
|
35
|
-
expect(c.r).toBe(0); expect(c.b).toBe(1); expect(c.a).toBe(0.5);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await describe('parseColor — standard CSS hsl()/hsla()', async () => {
|
|
40
|
-
await it('hsl(0, 0%, 100%) → white', async () => {
|
|
41
|
-
const c = parseColor('hsl(0, 0%, 100%)')!;
|
|
42
|
-
expect(c).toBeDefined();
|
|
43
|
-
expect(c.r).toBeGreaterThan(0.99);
|
|
44
|
-
expect(c.g).toBeGreaterThan(0.99);
|
|
45
|
-
expect(c.b).toBeGreaterThan(0.99);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await it('hsl(0, 0%, 0%) → black', async () => {
|
|
49
|
-
const c = parseColor('hsl(0, 0%, 0%)')!;
|
|
50
|
-
expect(c).toBeDefined();
|
|
51
|
-
expect(c.r).toBe(0); expect(c.g).toBe(0); expect(c.b).toBe(0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await it('hsl(120, 100%, 50%) → green', async () => {
|
|
55
|
-
const c = parseColor('hsl(120, 100%, 50%)')!;
|
|
56
|
-
expect(c).toBeDefined();
|
|
57
|
-
expect(c.g).toBeGreaterThan(0.99);
|
|
58
|
-
expect(c.r).toBeLessThan(0.01);
|
|
59
|
-
expect(c.b).toBeLessThan(0.01);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
await it('hsla(0, 100%, 50%, 0.5) → semi-transparent red', async () => {
|
|
63
|
-
const c = parseColor('hsla(0, 100%, 50%, 0.5)')!;
|
|
64
|
-
expect(c).toBeDefined();
|
|
65
|
-
expect(c.r).toBeGreaterThan(0.99);
|
|
66
|
-
expect(c.a).toBe(0.5);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await describe('parseColor — Excalibur non-standard HSL (0-1 normalized)', async () => {
|
|
71
|
-
// Excalibur's Color.toString() → HSLColor.fromRGBA().toString()
|
|
72
|
-
// uses toFixed(0) on h/s/l values stored in 0-1 range → "hsla(h, s, l, a)"
|
|
73
|
-
// without % signs and with values that are 0 or 1 after rounding.
|
|
74
|
-
|
|
75
|
-
await it('hsla(0, 0, 1, 1) → white (Color.White)', async () => {
|
|
76
|
-
// Color.White = fromHex("#FFFFFF") → r=255,g=255,b=255,a=1
|
|
77
|
-
// → HSLColor(h=0, s=0, l=1, a=1) → "hsla(0, 0, 1, 1)"
|
|
78
|
-
const c = parseColor('hsla(0, 0, 1, 1)')!;
|
|
79
|
-
expect(c).toBeDefined();
|
|
80
|
-
expect(c.r).toBeGreaterThan(0.99);
|
|
81
|
-
expect(c.g).toBeGreaterThan(0.99);
|
|
82
|
-
expect(c.b).toBeGreaterThan(0.99);
|
|
83
|
-
expect(c.a).toBe(1);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await it('hsla(0, 0, 0, 1) → black (Color.Black)', async () => {
|
|
87
|
-
const c = parseColor('hsla(0, 0, 0, 1)')!;
|
|
88
|
-
expect(c).toBeDefined();
|
|
89
|
-
expect(c.r).toBe(0); expect(c.g).toBe(0); expect(c.b).toBe(0);
|
|
90
|
-
expect(c.a).toBe(1);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
await it('hsla(0, 0, 0, 0) → transparent black', async () => {
|
|
94
|
-
const c = parseColor('hsla(0, 0, 0, 0)')!;
|
|
95
|
-
expect(c).toBeDefined();
|
|
96
|
-
expect(c.a).toBe(0);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await describe('parseColor — fillStyle round-trip via Excalibur HSL', async () => {
|
|
101
|
-
await it('returns non-null for Excalibur white string', async () => {
|
|
102
|
-
expect(parseColor('hsla(0, 0, 1, 1)')).toBeDefined();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
await it('returns non-null for Excalibur black string', async () => {
|
|
106
|
-
expect(parseColor('hsla(0, 0, 0, 1)')).toBeDefined();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
await it('returns null for completely invalid color string', async () => {
|
|
110
|
-
expect(parseColor('not-a-color')).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
};
|