@gjsify/canvas2d-core 0.3.21 → 0.4.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/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/cairo-types.js +1 -0
- package/lib/esm/cairo-utils.js +1 -1
- package/lib/esm/canvas-gradient.js +1 -1
- package/lib/esm/canvas-path.js +1 -1
- package/lib/esm/canvas-pattern.js +1 -1
- package/lib/esm/canvas-rendering-context-2d.js +1 -1
- package/lib/esm/canvas-state.js +1 -1
- package/lib/esm/color.js +1 -1
- package/lib/esm/dom-types.js +1 -0
- package/lib/esm/image-data.js +1 -1
- package/lib/types/cairo-types.d.ts +20 -0
- package/lib/types/canvas-pattern.d.ts +1 -1
- package/lib/types/canvas-rendering-context-2d.d.ts +18 -6
- package/lib/types/dom-types.d.ts +88 -0
- package/package.json +50 -46
- 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 -75
- package/src/canvas-rendering-context-2d.ts +0 -1187
- 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/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
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// Canvas 2D transform tests — verifies Cairo.Context affine matrix
|
|
2
|
-
// decomposition (translate + rotate + scale) and getTransform round-trip
|
|
3
|
-
// via userToDevice. Cairo.Context in GJS does not expose a generic
|
|
4
|
-
// transform(matrix) call, so canvas2d-core decomposes the supplied 2D
|
|
5
|
-
// affine matrix [a,b,c,d,e,f] into primitive translate/rotate/scale calls.
|
|
6
|
-
//
|
|
7
|
-
// These tests lock in the fix made for the excalibur-jelly-jumper showcase,
|
|
8
|
-
// which exercises multiply/transform/setTransform inside its Canvas 2D
|
|
9
|
-
// fallback rendering path.
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from '@gjsify/unit';
|
|
12
|
-
import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
|
|
13
|
-
|
|
14
|
-
function makeCtx(width = 100, height = 100): CanvasRenderingContext2D {
|
|
15
|
-
const canvas = { width, height };
|
|
16
|
-
return new CanvasRenderingContext2D(canvas);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Assert two numbers are equal to within an epsilon. */
|
|
20
|
-
function nearlyEqual(actual: number, expected: number, eps = 1e-9): boolean {
|
|
21
|
-
return Math.abs(actual - expected) < eps;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export default async () => {
|
|
25
|
-
await describe('CanvasRenderingContext2D — transforms', async () => {
|
|
26
|
-
|
|
27
|
-
await describe('getTransform initial state', async () => {
|
|
28
|
-
await it('returns identity for a fresh context', async () => {
|
|
29
|
-
const ctx = makeCtx();
|
|
30
|
-
const m = ctx.getTransform();
|
|
31
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
32
|
-
expect(nearlyEqual(m.b, 0)).toBe(true);
|
|
33
|
-
expect(nearlyEqual(m.c, 0)).toBe(true);
|
|
34
|
-
expect(nearlyEqual(m.d, 1)).toBe(true);
|
|
35
|
-
expect(nearlyEqual(m.e, 0)).toBe(true);
|
|
36
|
-
expect(nearlyEqual(m.f, 0)).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await describe('translate', async () => {
|
|
41
|
-
await it('updates e and f in getTransform', async () => {
|
|
42
|
-
const ctx = makeCtx();
|
|
43
|
-
ctx.translate(10, 20);
|
|
44
|
-
const m = ctx.getTransform();
|
|
45
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
46
|
-
expect(nearlyEqual(m.d, 1)).toBe(true);
|
|
47
|
-
expect(nearlyEqual(m.e, 10)).toBe(true);
|
|
48
|
-
expect(nearlyEqual(m.f, 20)).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
await it('composes translations cumulatively', async () => {
|
|
52
|
-
const ctx = makeCtx();
|
|
53
|
-
ctx.translate(10, 20);
|
|
54
|
-
ctx.translate(5, 7);
|
|
55
|
-
const m = ctx.getTransform();
|
|
56
|
-
expect(nearlyEqual(m.e, 15)).toBe(true);
|
|
57
|
-
expect(nearlyEqual(m.f, 27)).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await describe('scale', async () => {
|
|
62
|
-
await it('updates a and d in getTransform', async () => {
|
|
63
|
-
const ctx = makeCtx();
|
|
64
|
-
ctx.scale(2, 3);
|
|
65
|
-
const m = ctx.getTransform();
|
|
66
|
-
expect(nearlyEqual(m.a, 2)).toBe(true);
|
|
67
|
-
expect(nearlyEqual(m.d, 3)).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await it('composes with a prior translate (translate then scale)', async () => {
|
|
71
|
-
const ctx = makeCtx();
|
|
72
|
-
ctx.translate(10, 20);
|
|
73
|
-
ctx.scale(2, 2);
|
|
74
|
-
const m = ctx.getTransform();
|
|
75
|
-
expect(nearlyEqual(m.a, 2)).toBe(true);
|
|
76
|
-
expect(nearlyEqual(m.d, 2)).toBe(true);
|
|
77
|
-
expect(nearlyEqual(m.e, 10)).toBe(true);
|
|
78
|
-
expect(nearlyEqual(m.f, 20)).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await describe('transform (multiply-by-affine)', async () => {
|
|
83
|
-
await it('pure translate via transform(1,0,0,1,tx,ty)', async () => {
|
|
84
|
-
const ctx = makeCtx();
|
|
85
|
-
ctx.transform(1, 0, 0, 1, 15, 25);
|
|
86
|
-
const m = ctx.getTransform();
|
|
87
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
88
|
-
expect(nearlyEqual(m.d, 1)).toBe(true);
|
|
89
|
-
expect(nearlyEqual(m.e, 15)).toBe(true);
|
|
90
|
-
expect(nearlyEqual(m.f, 25)).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
await it('pure scale via transform(sx,0,0,sy,0,0)', async () => {
|
|
94
|
-
const ctx = makeCtx();
|
|
95
|
-
ctx.transform(3, 0, 0, 4, 0, 0);
|
|
96
|
-
const m = ctx.getTransform();
|
|
97
|
-
expect(nearlyEqual(m.a, 3)).toBe(true);
|
|
98
|
-
expect(nearlyEqual(m.d, 4)).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
await it('ignores NaN / Infinity values without crashing', async () => {
|
|
102
|
-
const ctx = makeCtx();
|
|
103
|
-
// Excalibur can emit non-finite matrices during scene
|
|
104
|
-
// transitions — our Cairo binding must not crash on them.
|
|
105
|
-
ctx.transform(NaN, 0, 0, 1, 0, 0);
|
|
106
|
-
ctx.transform(1, 0, 0, 1, Infinity, 0);
|
|
107
|
-
// Transform was no-op'd, so state is still identity
|
|
108
|
-
const m = ctx.getTransform();
|
|
109
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await describe('setTransform', async () => {
|
|
114
|
-
await it('resets the matrix then applies the new transform (numeric)', async () => {
|
|
115
|
-
const ctx = makeCtx();
|
|
116
|
-
ctx.translate(100, 100);
|
|
117
|
-
ctx.setTransform(2, 0, 0, 2, 5, 10);
|
|
118
|
-
const m = ctx.getTransform();
|
|
119
|
-
expect(nearlyEqual(m.a, 2)).toBe(true);
|
|
120
|
-
expect(nearlyEqual(m.d, 2)).toBe(true);
|
|
121
|
-
expect(nearlyEqual(m.e, 5)).toBe(true);
|
|
122
|
-
expect(nearlyEqual(m.f, 10)).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await it('accepts a DOMMatrix-like object (a,b,c,d,e,f)', async () => {
|
|
126
|
-
const ctx = makeCtx();
|
|
127
|
-
ctx.setTransform({ a: 2, b: 0, c: 0, d: 2, e: 5, f: 10 });
|
|
128
|
-
const m = ctx.getTransform();
|
|
129
|
-
expect(nearlyEqual(m.a, 2)).toBe(true);
|
|
130
|
-
expect(nearlyEqual(m.d, 2)).toBe(true);
|
|
131
|
-
expect(nearlyEqual(m.e, 5)).toBe(true);
|
|
132
|
-
expect(nearlyEqual(m.f, 10)).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
await it('no-arg resets to identity', async () => {
|
|
136
|
-
const ctx = makeCtx();
|
|
137
|
-
ctx.translate(10, 10);
|
|
138
|
-
ctx.scale(5, 5);
|
|
139
|
-
ctx.setTransform();
|
|
140
|
-
const m = ctx.getTransform();
|
|
141
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
142
|
-
expect(nearlyEqual(m.d, 1)).toBe(true);
|
|
143
|
-
expect(nearlyEqual(m.e, 0)).toBe(true);
|
|
144
|
-
expect(nearlyEqual(m.f, 0)).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
await describe('save / restore', async () => {
|
|
149
|
-
await it('restore reverts the matrix set inside a save block', async () => {
|
|
150
|
-
const ctx = makeCtx();
|
|
151
|
-
ctx.translate(10, 20);
|
|
152
|
-
ctx.save();
|
|
153
|
-
ctx.scale(5, 5);
|
|
154
|
-
ctx.translate(100, 200);
|
|
155
|
-
ctx.restore();
|
|
156
|
-
const m = ctx.getTransform();
|
|
157
|
-
expect(nearlyEqual(m.a, 1)).toBe(true);
|
|
158
|
-
expect(nearlyEqual(m.d, 1)).toBe(true);
|
|
159
|
-
expect(nearlyEqual(m.e, 10)).toBe(true);
|
|
160
|
-
expect(nearlyEqual(m.f, 20)).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
await describe('Excalibur-style multiply pattern', async () => {
|
|
165
|
-
await it('ctx.setTransform(ctx.getTransform().multiply(other)) round-trips', async () => {
|
|
166
|
-
// getTransform returns a real DOMMatrix instance only when
|
|
167
|
-
// globalThis.DOMMatrix is installed (via @gjsify/dom-elements).
|
|
168
|
-
// Inject a minimal DOMMatrix for this test so the multiply
|
|
169
|
-
// code path runs end-to-end.
|
|
170
|
-
if (typeof (globalThis as any).DOMMatrix === 'undefined') {
|
|
171
|
-
class TestDOMMatrix {
|
|
172
|
-
a = 1; b = 0; c = 0; d = 1; e = 0; f = 0;
|
|
173
|
-
constructor(init?: number[]) {
|
|
174
|
-
if (Array.isArray(init) && init.length === 6) {
|
|
175
|
-
this.a = init[0]; this.b = init[1];
|
|
176
|
-
this.c = init[2]; this.d = init[3];
|
|
177
|
-
this.e = init[4]; this.f = init[5];
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
multiply(o: { a: number; b: number; c: number; d: number; e: number; f: number }) {
|
|
181
|
-
const a = this.a * o.a + this.c * o.b;
|
|
182
|
-
const b = this.b * o.a + this.d * o.b;
|
|
183
|
-
const c = this.a * o.c + this.c * o.d;
|
|
184
|
-
const d = this.b * o.c + this.d * o.d;
|
|
185
|
-
const e = this.a * o.e + this.c * o.f + this.e;
|
|
186
|
-
const f = this.b * o.e + this.d * o.f + this.f;
|
|
187
|
-
return new TestDOMMatrix([a, b, c, d, e, f]);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
(globalThis as any).DOMMatrix = TestDOMMatrix;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const ctx = makeCtx();
|
|
194
|
-
ctx.translate(50, 50);
|
|
195
|
-
// Simulate Excalibur's GraphicsContext2DCanvas.multiply():
|
|
196
|
-
// ctx.setTransform(ctx.getTransform().multiply(otherMatrix))
|
|
197
|
-
const current = ctx.getTransform() as any;
|
|
198
|
-
expect(typeof current.multiply).toBe('function');
|
|
199
|
-
const other = { a: 2, b: 0, c: 0, d: 2, e: 0, f: 0 } as any;
|
|
200
|
-
const composed = current.multiply(other);
|
|
201
|
-
ctx.setTransform(composed);
|
|
202
|
-
const final = ctx.getTransform();
|
|
203
|
-
// translate(50,50) then scale(2,2) → matrix [2,0,0,2,50,50]
|
|
204
|
-
expect(nearlyEqual(final.a, 2)).toBe(true);
|
|
205
|
-
expect(nearlyEqual(final.d, 2)).toBe(true);
|
|
206
|
-
expect(nearlyEqual(final.e, 50)).toBe(true);
|
|
207
|
-
expect(nearlyEqual(final.f, 50)).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
};
|
package/src/color.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// CSS color parser for Canvas 2D context
|
|
2
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
|
|
3
|
-
|
|
4
|
-
export interface RGBA {
|
|
5
|
-
r: number; // 0-1
|
|
6
|
-
g: number; // 0-1
|
|
7
|
-
b: number; // 0-1
|
|
8
|
-
a: number; // 0-1
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// CSS named colors (all 148 standard colors)
|
|
12
|
-
const NAMED_COLORS: Record<string, string> = {
|
|
13
|
-
aliceblue: '#f0f8ff', antiquewhite: '#faebd7', aqua: '#00ffff', aquamarine: '#7fffd4',
|
|
14
|
-
azure: '#f0ffff', beige: '#f5f5dc', bisque: '#ffe4c4', black: '#000000',
|
|
15
|
-
blanchedalmond: '#ffebcd', blue: '#0000ff', blueviolet: '#8a2be2', brown: '#a52a2a',
|
|
16
|
-
burlywood: '#deb887', cadetblue: '#5f9ea0', chartreuse: '#7fff00', chocolate: '#d2691e',
|
|
17
|
-
coral: '#ff7f50', cornflowerblue: '#6495ed', cornsilk: '#fff8dc', crimson: '#dc143c',
|
|
18
|
-
cyan: '#00ffff', darkblue: '#00008b', darkcyan: '#008b8b', darkgoldenrod: '#b8860b',
|
|
19
|
-
darkgray: '#a9a9a9', darkgreen: '#006400', darkgrey: '#a9a9a9', darkkhaki: '#bdb76b',
|
|
20
|
-
darkmagenta: '#8b008b', darkolivegreen: '#556b2f', darkorange: '#ff8c00', darkorchid: '#9932cc',
|
|
21
|
-
darkred: '#8b0000', darksalmon: '#e9967a', darkseagreen: '#8fbc8f', darkslateblue: '#483d8b',
|
|
22
|
-
darkslategray: '#2f4f4f', darkslategrey: '#2f4f4f', darkturquoise: '#00ced1', darkviolet: '#9400d3',
|
|
23
|
-
deeppink: '#ff1493', deepskyblue: '#00bfff', dimgray: '#696969', dimgrey: '#696969',
|
|
24
|
-
dodgerblue: '#1e90ff', firebrick: '#b22222', floralwhite: '#fffaf0', forestgreen: '#228b22',
|
|
25
|
-
fuchsia: '#ff00ff', gainsboro: '#dcdcdc', ghostwhite: '#f8f8ff', gold: '#ffd700',
|
|
26
|
-
goldenrod: '#daa520', gray: '#808080', green: '#008000', greenyellow: '#adff2f',
|
|
27
|
-
grey: '#808080', honeydew: '#f0fff0', hotpink: '#ff69b4', indianred: '#cd5c5c',
|
|
28
|
-
indigo: '#4b0082', ivory: '#fffff0', khaki: '#f0e68c', lavender: '#e6e6fa',
|
|
29
|
-
lavenderblush: '#fff0f5', lawngreen: '#7cfc00', lemonchiffon: '#fffacd', lightblue: '#add8e6',
|
|
30
|
-
lightcoral: '#f08080', lightcyan: '#e0ffff', lightgoldenrodyellow: '#fafad2', lightgray: '#d3d3d3',
|
|
31
|
-
lightgreen: '#90ee90', lightgrey: '#d3d3d3', lightpink: '#ffb6c1', lightsalmon: '#ffa07a',
|
|
32
|
-
lightseagreen: '#20b2aa', lightskyblue: '#87cefa', lightslategray: '#778899', lightslategrey: '#778899',
|
|
33
|
-
lightsteelblue: '#b0c4de', lightyellow: '#ffffe0', lime: '#00ff00', limegreen: '#32cd32',
|
|
34
|
-
linen: '#faf0e6', magenta: '#ff00ff', maroon: '#800000', mediumaquamarine: '#66cdaa',
|
|
35
|
-
mediumblue: '#0000cd', mediumorchid: '#ba55d3', mediumpurple: '#9370db', mediumseagreen: '#3cb371',
|
|
36
|
-
mediumslateblue: '#7b68ee', mediumspringgreen: '#00fa9a', mediumturquoise: '#48d1cc',
|
|
37
|
-
mediumvioletred: '#c71585', midnightblue: '#191970', mintcream: '#f5fffa', mistyrose: '#ffe4e1',
|
|
38
|
-
moccasin: '#ffe4b5', navajowhite: '#ffdead', navy: '#000080', oldlace: '#fdf5e6',
|
|
39
|
-
olive: '#808000', olivedrab: '#6b8e23', orange: '#ffa500', orangered: '#ff4500',
|
|
40
|
-
orchid: '#da70d6', palegoldenrod: '#eee8aa', palegreen: '#98fb98', paleturquoise: '#afeeee',
|
|
41
|
-
palevioletred: '#db7093', papayawhip: '#ffefd5', peachpuff: '#ffdab9', peru: '#cd853f',
|
|
42
|
-
pink: '#ffc0cb', plum: '#dda0dd', powderblue: '#b0e0e6', purple: '#800080',
|
|
43
|
-
rebeccapurple: '#663399', red: '#ff0000', rosybrown: '#bc8f8f', royalblue: '#4169e1',
|
|
44
|
-
saddlebrown: '#8b4513', salmon: '#fa8072', sandybrown: '#f4a460', seagreen: '#2e8b57',
|
|
45
|
-
seashell: '#fff5ee', sienna: '#a0522d', silver: '#c0c0c0', skyblue: '#87ceeb',
|
|
46
|
-
slateblue: '#6a5acd', slategray: '#708090', slategrey: '#708090', snow: '#fffafa',
|
|
47
|
-
springgreen: '#00ff7f', steelblue: '#4682b4', tan: '#d2b48c', teal: '#008080',
|
|
48
|
-
thistle: '#d8bfd8', tomato: '#ff6347', turquoise: '#40e0d0', violet: '#ee82ee',
|
|
49
|
-
wheat: '#f5deb3', white: '#ffffff', whitesmoke: '#f5f5f5', yellow: '#ffff00',
|
|
50
|
-
yellowgreen: '#9acd32',
|
|
51
|
-
transparent: '#00000000',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Parse a CSS color string into RGBA components (0-1 range).
|
|
56
|
-
* Supports: #rgb, #rrggbb, #rgba, #rrggbbaa, rgb(), rgba(), hsl(), hsla(), named colors, 'transparent'.
|
|
57
|
-
*
|
|
58
|
-
* Also handles Excalibur's non-standard HSL format where h/s/l are all in 0-1 range (not degrees/%).
|
|
59
|
-
* Excalibur's Color.toString() returns `hsla(h, s, l, a)` with values in 0-1 normalized form
|
|
60
|
-
* (e.g. Color.White → "hsla(0, 0, 1, 1)", Color.Black → "hsla(0, 0, 0, 1)").
|
|
61
|
-
*/
|
|
62
|
-
export function parseColor(color: string): RGBA | null {
|
|
63
|
-
if (!color || typeof color !== 'string') return null;
|
|
64
|
-
|
|
65
|
-
const trimmed = color.trim().toLowerCase();
|
|
66
|
-
|
|
67
|
-
// Named colors
|
|
68
|
-
const named = NAMED_COLORS[trimmed];
|
|
69
|
-
if (named) return parseHex(named);
|
|
70
|
-
|
|
71
|
-
// Hex formats
|
|
72
|
-
if (trimmed.startsWith('#')) return parseHex(trimmed);
|
|
73
|
-
|
|
74
|
-
// rgb()/rgba()
|
|
75
|
-
const rgbMatch = trimmed.match(
|
|
76
|
-
/^rgba?\(\s*(\d+(?:\.\d+)?%?)\s*[,\s]\s*(\d+(?:\.\d+)?%?)\s*[,\s]\s*(\d+(?:\.\d+)?%?)\s*(?:[,/]\s*(\d+(?:\.\d+)?%?))?\s*\)$/
|
|
77
|
-
);
|
|
78
|
-
if (rgbMatch) {
|
|
79
|
-
return {
|
|
80
|
-
r: parseComponent(rgbMatch[1], 255) / 255,
|
|
81
|
-
g: parseComponent(rgbMatch[2], 255) / 255,
|
|
82
|
-
b: parseComponent(rgbMatch[3], 255) / 255,
|
|
83
|
-
a: rgbMatch[4] !== undefined ? parseComponent(rgbMatch[4], 1) : 1,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// hsl()/hsla() — handles both standard CSS (degrees, %) and Excalibur's 0-1 normalized form.
|
|
88
|
-
// Heuristic: if s/l have no % and are ≤ 1, treat as 0-1 normalized; if h > 1, treat as degrees.
|
|
89
|
-
const hslMatch = trimmed.match(
|
|
90
|
-
/^hsla?\(\s*(\d+(?:\.\d+)?)\s*[,\s]\s*(\d+(?:\.\d+)?)(%)?\s*[,\s]\s*(\d+(?:\.\d+)?)(%)?\s*(?:[,/]\s*(\d+(?:\.\d+)?%?))?\s*\)$/
|
|
91
|
-
);
|
|
92
|
-
if (hslMatch) {
|
|
93
|
-
let h = parseFloat(hslMatch[1]);
|
|
94
|
-
let s = parseFloat(hslMatch[2]);
|
|
95
|
-
const sPct = hslMatch[3] === '%';
|
|
96
|
-
let l = parseFloat(hslMatch[4]);
|
|
97
|
-
const lPct = hslMatch[5] === '%';
|
|
98
|
-
const a = hslMatch[6] !== undefined ? parseComponent(hslMatch[6], 1) : 1;
|
|
99
|
-
|
|
100
|
-
// Normalize h to 0-1 range: if > 1, it's degrees (0-360)
|
|
101
|
-
if (h > 1) h /= 360;
|
|
102
|
-
// Normalize s to 0-1 range
|
|
103
|
-
if (sPct) s /= 100;
|
|
104
|
-
else if (s > 1) s /= 100;
|
|
105
|
-
// Normalize l to 0-1 range
|
|
106
|
-
if (lPct) l /= 100;
|
|
107
|
-
else if (l > 1) l /= 100;
|
|
108
|
-
|
|
109
|
-
return hslToRGBA(h, s, l, Math.max(0, Math.min(1, a)));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseHex(hex: string): RGBA | null {
|
|
116
|
-
const h = hex.slice(1);
|
|
117
|
-
let r: number, g: number, b: number, a = 1;
|
|
118
|
-
|
|
119
|
-
if (h.length === 3) {
|
|
120
|
-
r = parseInt(h[0] + h[0], 16) / 255;
|
|
121
|
-
g = parseInt(h[1] + h[1], 16) / 255;
|
|
122
|
-
b = parseInt(h[2] + h[2], 16) / 255;
|
|
123
|
-
} else if (h.length === 4) {
|
|
124
|
-
r = parseInt(h[0] + h[0], 16) / 255;
|
|
125
|
-
g = parseInt(h[1] + h[1], 16) / 255;
|
|
126
|
-
b = parseInt(h[2] + h[2], 16) / 255;
|
|
127
|
-
a = parseInt(h[3] + h[3], 16) / 255;
|
|
128
|
-
} else if (h.length === 6) {
|
|
129
|
-
r = parseInt(h.slice(0, 2), 16) / 255;
|
|
130
|
-
g = parseInt(h.slice(2, 4), 16) / 255;
|
|
131
|
-
b = parseInt(h.slice(4, 6), 16) / 255;
|
|
132
|
-
} else if (h.length === 8) {
|
|
133
|
-
r = parseInt(h.slice(0, 2), 16) / 255;
|
|
134
|
-
g = parseInt(h.slice(2, 4), 16) / 255;
|
|
135
|
-
b = parseInt(h.slice(4, 6), 16) / 255;
|
|
136
|
-
a = parseInt(h.slice(6, 8), 16) / 255;
|
|
137
|
-
} else {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { r, g, b, a };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseComponent(value: string, max: number): number {
|
|
145
|
-
if (value.endsWith('%')) {
|
|
146
|
-
return (parseFloat(value) / 100) * max;
|
|
147
|
-
}
|
|
148
|
-
return parseFloat(value);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function hue2rgb(p: number, q: number, t: number): number {
|
|
152
|
-
if (t < 0) t += 1;
|
|
153
|
-
if (t > 1) t -= 1;
|
|
154
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
155
|
-
if (t < 1 / 2) return q;
|
|
156
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
157
|
-
return p;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function hslToRGBA(h: number, s: number, l: number, a: number): RGBA {
|
|
161
|
-
let r: number, g: number, b: number;
|
|
162
|
-
if (s === 0) {
|
|
163
|
-
r = g = b = l;
|
|
164
|
-
} else {
|
|
165
|
-
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
166
|
-
const p = 2 * l - q;
|
|
167
|
-
r = hue2rgb(p, q, h + 1 / 3);
|
|
168
|
-
g = hue2rgb(p, q, h);
|
|
169
|
-
b = hue2rgb(p, q, h - 1 / 3);
|
|
170
|
-
}
|
|
171
|
-
return { r, g, b, a };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Default color: opaque black */
|
|
175
|
-
export const BLACK: RGBA = { r: 0, g: 0, b: 0, a: 1 };
|
|
176
|
-
/** Transparent black */
|
|
177
|
-
export const TRANSPARENT: RGBA = { r: 0, g: 0, b: 0, a: 0 };
|
package/src/image-data.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// ImageData implementation for Canvas 2D context
|
|
2
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/ImageData
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ImageData represents the pixel data of a canvas area.
|
|
6
|
-
* Each pixel is 4 bytes: R, G, B, A (0-255 each).
|
|
7
|
-
*/
|
|
8
|
-
export class OurImageData {
|
|
9
|
-
readonly data: Uint8ClampedArray;
|
|
10
|
-
readonly width: number;
|
|
11
|
-
readonly height: number;
|
|
12
|
-
readonly colorSpace: PredefinedColorSpace = 'srgb';
|
|
13
|
-
|
|
14
|
-
constructor(sw: number, sh: number);
|
|
15
|
-
constructor(data: Uint8ClampedArray, sw: number, sh?: number);
|
|
16
|
-
constructor(swOrData: number | Uint8ClampedArray, sh: number, maybeHeight?: number) {
|
|
17
|
-
if (typeof swOrData === 'number') {
|
|
18
|
-
// new ImageData(width, height)
|
|
19
|
-
this.width = swOrData;
|
|
20
|
-
this.height = sh;
|
|
21
|
-
this.data = new Uint8ClampedArray(this.width * this.height * 4);
|
|
22
|
-
} else {
|
|
23
|
-
// new ImageData(data, width[, height])
|
|
24
|
-
this.data = swOrData;
|
|
25
|
-
this.width = sh;
|
|
26
|
-
this.height = maybeHeight ?? (this.data.length / (4 * this.width));
|
|
27
|
-
if (this.data.length !== this.width * this.height * 4) {
|
|
28
|
-
throw new RangeError(
|
|
29
|
-
`Source data length ${this.data.length} is not a multiple of (4 * width=${this.width})`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Cairo-backed Canvas 2D core — no @gjsify/dom-elements dependency.
|
|
2
|
-
// Extracted from @gjsify/canvas2d so @gjsify/dom-elements can depend on it
|
|
3
|
-
// without creating a circular dependency.
|
|
4
|
-
//
|
|
5
|
-
// @gjsify/dom-elements imports this package to auto-register the '2d' context
|
|
6
|
-
// factory on HTMLCanvasElement, mirroring browser behavior where
|
|
7
|
-
// canvas.getContext('2d') works without any explicit import.
|
|
8
|
-
|
|
9
|
-
export { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
|
|
10
|
-
export { CanvasGradient } from './canvas-gradient.js';
|
|
11
|
-
export { CanvasPattern } from './canvas-pattern.js';
|
|
12
|
-
export { Path2D } from './canvas-path.js';
|
|
13
|
-
export { OurImageData as ImageData } from './image-data.js';
|
|
14
|
-
export { parseColor } from './color.js';
|