@gjsify/canvas2d-core 0.1.7 → 0.1.9

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,245 @@
1
+ // Canvas 2D state stack tests — save/restore must round-trip every
2
+ // state field (fillStyle, strokeStyle, globalAlpha, lineWidth, transform,
3
+ // text properties, clip region, imageSmoothingEnabled).
4
+ //
5
+ // Ported from refs/wpt/html/canvas/element/the-canvas-state/
6
+ // 2d.state.saverestore.{fillStyle,strokeStyle,globalAlpha,
7
+ // globalCompositeOperation,lineWidth,lineCap,lineJoin,miterLimit,
8
+ // transformation,path,underflow,clip,stackdepth}.html
9
+ // Original: Copyright (c) Web Platform Tests contributors. 3-Clause BSD.
10
+ // Reimplemented for GJS using @gjsify/canvas2d-core + @gjsify/unit.
11
+
12
+ import { describe, it, expect } from '@gjsify/unit';
13
+ import { CanvasRenderingContext2D } from './canvas-rendering-context-2d.js';
14
+
15
+ function makeCtx(width = 50, height = 50): CanvasRenderingContext2D {
16
+ const canvas = { width, height };
17
+ return new CanvasRenderingContext2D(canvas);
18
+ }
19
+
20
+ function nearlyEqual(a: number, b: number, eps = 1e-9): boolean {
21
+ return Math.abs(a - b) < eps;
22
+ }
23
+
24
+ export default async () => {
25
+ await describe('CanvasRenderingContext2D — state save/restore', async () => {
26
+
27
+ await describe('style properties', async () => {
28
+ await it('save/restore round-trips fillStyle string colors', async () => {
29
+ const ctx = makeCtx();
30
+ ctx.fillStyle = '#ff0000';
31
+ ctx.save();
32
+ ctx.fillStyle = '#00ff00';
33
+ expect(ctx.fillStyle).toBe('#00ff00');
34
+ ctx.restore();
35
+ expect(ctx.fillStyle).toBe('#ff0000');
36
+ });
37
+
38
+ await it('save/restore round-trips strokeStyle', async () => {
39
+ const ctx = makeCtx();
40
+ ctx.strokeStyle = '#112233';
41
+ ctx.save();
42
+ ctx.strokeStyle = '#445566';
43
+ ctx.restore();
44
+ expect(ctx.strokeStyle).toBe('#112233');
45
+ });
46
+
47
+ await it('save/restore round-trips globalAlpha', async () => {
48
+ const ctx = makeCtx();
49
+ ctx.globalAlpha = 0.3;
50
+ ctx.save();
51
+ ctx.globalAlpha = 0.9;
52
+ ctx.restore();
53
+ expect(nearlyEqual(ctx.globalAlpha, 0.3)).toBe(true);
54
+ });
55
+
56
+ await it('save/restore round-trips globalCompositeOperation', async () => {
57
+ const ctx = makeCtx();
58
+ ctx.globalCompositeOperation = 'source-over';
59
+ ctx.save();
60
+ ctx.globalCompositeOperation = 'destination-over';
61
+ ctx.restore();
62
+ expect(ctx.globalCompositeOperation).toBe('source-over');
63
+ });
64
+ });
65
+
66
+ await describe('line properties', async () => {
67
+ await it('save/restore round-trips lineWidth', async () => {
68
+ const ctx = makeCtx();
69
+ ctx.lineWidth = 3;
70
+ ctx.save();
71
+ ctx.lineWidth = 10;
72
+ ctx.restore();
73
+ expect(ctx.lineWidth).toBe(3);
74
+ });
75
+
76
+ await it('save/restore round-trips lineCap and lineJoin', async () => {
77
+ const ctx = makeCtx();
78
+ ctx.lineCap = 'round';
79
+ ctx.lineJoin = 'bevel';
80
+ ctx.save();
81
+ ctx.lineCap = 'square';
82
+ ctx.lineJoin = 'miter';
83
+ ctx.restore();
84
+ expect(ctx.lineCap).toBe('round');
85
+ expect(ctx.lineJoin).toBe('bevel');
86
+ });
87
+
88
+ await it('save/restore round-trips miterLimit', async () => {
89
+ const ctx = makeCtx();
90
+ ctx.miterLimit = 5;
91
+ ctx.save();
92
+ ctx.miterLimit = 20;
93
+ ctx.restore();
94
+ expect(ctx.miterLimit).toBe(5);
95
+ });
96
+
97
+ await it('save/restore round-trips lineDash and lineDashOffset', async () => {
98
+ const ctx = makeCtx();
99
+ ctx.setLineDash([5, 2]);
100
+ ctx.lineDashOffset = 3;
101
+ ctx.save();
102
+ ctx.setLineDash([10, 10]);
103
+ ctx.lineDashOffset = 7;
104
+ ctx.restore();
105
+ const restored = ctx.getLineDash();
106
+ expect(restored.length).toBe(2);
107
+ expect(restored[0]).toBe(5);
108
+ expect(restored[1]).toBe(2);
109
+ expect(ctx.lineDashOffset).toBe(3);
110
+ });
111
+ });
112
+
113
+ await describe('text properties', async () => {
114
+ await it('save/restore round-trips font, textAlign, textBaseline', async () => {
115
+ const ctx = makeCtx();
116
+ ctx.font = 'bold 20px sans-serif';
117
+ ctx.textAlign = 'center';
118
+ ctx.textBaseline = 'middle';
119
+ ctx.save();
120
+ ctx.font = '10px serif';
121
+ ctx.textAlign = 'left';
122
+ ctx.textBaseline = 'top';
123
+ ctx.restore();
124
+ expect(ctx.font).toBe('bold 20px sans-serif');
125
+ expect(ctx.textAlign).toBe('center');
126
+ expect(ctx.textBaseline).toBe('middle');
127
+ });
128
+ });
129
+
130
+ await describe('shadow properties', async () => {
131
+ await it('save/restore round-trips shadow state', async () => {
132
+ const ctx = makeCtx();
133
+ ctx.shadowColor = '#abcdef';
134
+ ctx.shadowBlur = 5;
135
+ ctx.shadowOffsetX = 3;
136
+ ctx.shadowOffsetY = 7;
137
+ ctx.save();
138
+ ctx.shadowColor = '#000000';
139
+ ctx.shadowBlur = 0;
140
+ ctx.shadowOffsetX = 0;
141
+ ctx.shadowOffsetY = 0;
142
+ ctx.restore();
143
+ expect(ctx.shadowColor).toBe('#abcdef');
144
+ expect(ctx.shadowBlur).toBe(5);
145
+ expect(ctx.shadowOffsetX).toBe(3);
146
+ expect(ctx.shadowOffsetY).toBe(7);
147
+ });
148
+ });
149
+
150
+ await describe('imageSmoothing properties — Excalibur pixel-art regression', async () => {
151
+ // Locks the Phase D imageSmoothingEnabled=false fix. If any
152
+ // future refactor drops imageSmoothingEnabled from cloneState,
153
+ // save/restore would silently reset it to the default (true)
154
+ // and pixel-art sprites would render blurry again.
155
+ await it('save/restore round-trips imageSmoothingEnabled', async () => {
156
+ const ctx = makeCtx();
157
+ ctx.imageSmoothingEnabled = false;
158
+ ctx.save();
159
+ ctx.imageSmoothingEnabled = true;
160
+ ctx.restore();
161
+ expect(ctx.imageSmoothingEnabled).toBe(false);
162
+ });
163
+
164
+ await it('save/restore round-trips imageSmoothingQuality', async () => {
165
+ const ctx = makeCtx();
166
+ ctx.imageSmoothingQuality = 'high';
167
+ ctx.save();
168
+ ctx.imageSmoothingQuality = 'low';
169
+ ctx.restore();
170
+ expect(ctx.imageSmoothingQuality).toBe('high');
171
+ });
172
+ });
173
+
174
+ await describe('transform matrix', async () => {
175
+ await it('save/restore round-trips translation', async () => {
176
+ const ctx = makeCtx();
177
+ ctx.translate(10, 20);
178
+ ctx.save();
179
+ ctx.translate(50, 50);
180
+ const inner = ctx.getTransform();
181
+ expect(nearlyEqual(inner.e, 60)).toBe(true);
182
+ expect(nearlyEqual(inner.f, 70)).toBe(true);
183
+ ctx.restore();
184
+ const outer = ctx.getTransform();
185
+ expect(nearlyEqual(outer.e, 10)).toBe(true);
186
+ expect(nearlyEqual(outer.f, 20)).toBe(true);
187
+ });
188
+
189
+ await it('save/restore round-trips scale', async () => {
190
+ const ctx = makeCtx();
191
+ ctx.scale(2, 3);
192
+ ctx.save();
193
+ ctx.scale(5, 5);
194
+ ctx.restore();
195
+ const m = ctx.getTransform();
196
+ expect(nearlyEqual(m.a, 2)).toBe(true);
197
+ expect(nearlyEqual(m.d, 3)).toBe(true);
198
+ });
199
+ });
200
+
201
+ await describe('nested save/restore stacks', async () => {
202
+ await it('triple save + triple restore returns to default', async () => {
203
+ const ctx = makeCtx();
204
+ ctx.save();
205
+ ctx.fillStyle = '#111111';
206
+ ctx.save();
207
+ ctx.fillStyle = '#222222';
208
+ ctx.save();
209
+ ctx.fillStyle = '#333333';
210
+ ctx.restore();
211
+ expect(ctx.fillStyle).toBe('#222222');
212
+ ctx.restore();
213
+ expect(ctx.fillStyle).toBe('#111111');
214
+ ctx.restore();
215
+ // Default fillStyle per spec is #000000
216
+ expect(ctx.fillStyle).toBe('#000000');
217
+ });
218
+
219
+ await it('restore() on empty stack is a no-op', async () => {
220
+ const ctx = makeCtx();
221
+ // Should not throw — Canvas 2D spec explicitly allows
222
+ // unbalanced restores.
223
+ expect(() => {
224
+ ctx.restore();
225
+ ctx.restore();
226
+ ctx.restore();
227
+ }).not.toThrow();
228
+ });
229
+
230
+ await it('deep stack (50 saves) restores correctly', async () => {
231
+ const ctx = makeCtx();
232
+ const fills: string[] = [];
233
+ for (let i = 0; i < 50; i++) {
234
+ ctx.save();
235
+ ctx.fillStyle = '#' + (i * 7).toString(16).padStart(6, '0');
236
+ fills.push(ctx.fillStyle as string);
237
+ }
238
+ for (let i = 49; i >= 0; i--) {
239
+ expect(ctx.fillStyle).toBe(fills[i]);
240
+ ctx.restore();
241
+ }
242
+ });
243
+ });
244
+ });
245
+ };
@@ -0,0 +1,211 @@
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 CHANGED
@@ -53,7 +53,11 @@ const NAMED_COLORS: Record<string, string> = {
53
53
 
54
54
  /**
55
55
  * Parse a CSS color string into RGBA components (0-1 range).
56
- * Supports: #rgb, #rrggbb, #rgba, #rrggbbaa, rgb(), rgba(), named colors, 'transparent'.
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)").
57
61
  */
58
62
  export function parseColor(color: string): RGBA | null {
59
63
  if (!color || typeof color !== 'string') return null;
@@ -80,6 +84,31 @@ export function parseColor(color: string): RGBA | null {
80
84
  };
81
85
  }
82
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
+
83
112
  return null;
84
113
  }
85
114
 
@@ -119,6 +148,29 @@ function parseComponent(value: string, max: number): number {
119
148
  return parseFloat(value);
120
149
  }
121
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
+
122
174
  /** Default color: opaque black */
123
175
  export const BLACK: RGBA = { r: 0, g: 0, b: 0, a: 1 };
124
176
  /** Transparent black */
package/src/test.mts CHANGED
@@ -2,5 +2,21 @@
2
2
  import { run } from '@gjsify/unit';
3
3
 
4
4
  import canvasTextSuite from './canvas-text.spec.js';
5
+ import canvasTransformSuite from './canvas-transform.spec.js';
6
+ import canvasDrawimageSuite from './canvas-drawimage.spec.js';
7
+ import canvasStateSuite from './canvas-state.spec.js';
8
+ import canvasClearingSuite from './canvas-clearing.spec.js';
9
+ import canvasImagedataSuite from './canvas-imagedata.spec.js';
10
+ import canvasCompositeSuite from './canvas-composite.spec.js';
11
+ import canvasColorSuite from './canvas-color.spec.js';
5
12
 
6
- run({ testSuite: canvasTextSuite });
13
+ run({
14
+ canvasTextSuite,
15
+ canvasTransformSuite,
16
+ canvasDrawimageSuite,
17
+ canvasStateSuite,
18
+ canvasClearingSuite,
19
+ canvasImagedataSuite,
20
+ canvasCompositeSuite,
21
+ canvasColorSuite,
22
+ });