@gjsify/canvas2d 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ // Drawing state for Canvas 2D context save()/restore() stack.
2
+ // Each save() pushes a clone of the current state; restore() pops it.
3
+
4
+ import type { RGBA } from './color.js';
5
+ import { BLACK } from './color.js';
6
+
7
+ export interface CanvasState {
8
+ // Fill & stroke
9
+ fillStyle: string | CanvasGradient | CanvasPattern;
10
+ fillColor: RGBA;
11
+ strokeStyle: string | CanvasGradient | CanvasPattern;
12
+ strokeColor: RGBA;
13
+
14
+ // Line properties
15
+ lineWidth: number;
16
+ lineCap: CanvasLineCap;
17
+ lineJoin: CanvasLineJoin;
18
+ miterLimit: number;
19
+ lineDash: number[];
20
+ lineDashOffset: number;
21
+
22
+ // Compositing
23
+ globalAlpha: number;
24
+ globalCompositeOperation: GlobalCompositeOperation;
25
+
26
+ // Shadows (Phase 5 — tracked in state for save/restore correctness)
27
+ shadowColor: string;
28
+ shadowBlur: number;
29
+ shadowOffsetX: number;
30
+ shadowOffsetY: number;
31
+
32
+ // Text (Phase 4)
33
+ font: string;
34
+ textAlign: CanvasTextAlign;
35
+ textBaseline: CanvasTextBaseline;
36
+ direction: CanvasDirection;
37
+
38
+ // Image smoothing
39
+ imageSmoothingEnabled: boolean;
40
+ imageSmoothingQuality: ImageSmoothingQuality;
41
+ }
42
+
43
+ export function createDefaultState(): CanvasState {
44
+ return {
45
+ fillStyle: '#000000',
46
+ fillColor: { ...BLACK },
47
+ strokeStyle: '#000000',
48
+ strokeColor: { ...BLACK },
49
+ lineWidth: 1,
50
+ lineCap: 'butt',
51
+ lineJoin: 'miter',
52
+ miterLimit: 10,
53
+ lineDash: [],
54
+ lineDashOffset: 0,
55
+ globalAlpha: 1,
56
+ globalCompositeOperation: 'source-over',
57
+ shadowColor: 'rgba(0, 0, 0, 0)',
58
+ shadowBlur: 0,
59
+ shadowOffsetX: 0,
60
+ shadowOffsetY: 0,
61
+ font: '10px sans-serif',
62
+ textAlign: 'start',
63
+ textBaseline: 'alphabetic',
64
+ direction: 'ltr',
65
+ imageSmoothingEnabled: true,
66
+ imageSmoothingQuality: 'low',
67
+ };
68
+ }
69
+
70
+ export function cloneState(state: CanvasState): CanvasState {
71
+ return {
72
+ ...state,
73
+ fillColor: { ...state.fillColor },
74
+ strokeColor: { ...state.strokeColor },
75
+ lineDash: [...state.lineDash],
76
+ };
77
+ }
package/src/color.ts ADDED
@@ -0,0 +1,125 @@
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(), named colors, 'transparent'.
57
+ */
58
+ export function parseColor(color: string): RGBA | null {
59
+ if (!color || typeof color !== 'string') return null;
60
+
61
+ const trimmed = color.trim().toLowerCase();
62
+
63
+ // Named colors
64
+ const named = NAMED_COLORS[trimmed];
65
+ if (named) return parseHex(named);
66
+
67
+ // Hex formats
68
+ if (trimmed.startsWith('#')) return parseHex(trimmed);
69
+
70
+ // rgb()/rgba()
71
+ const rgbMatch = trimmed.match(
72
+ /^rgba?\(\s*(\d+(?:\.\d+)?%?)\s*[,\s]\s*(\d+(?:\.\d+)?%?)\s*[,\s]\s*(\d+(?:\.\d+)?%?)\s*(?:[,/]\s*(\d+(?:\.\d+)?%?))?\s*\)$/
73
+ );
74
+ if (rgbMatch) {
75
+ return {
76
+ r: parseComponent(rgbMatch[1], 255) / 255,
77
+ g: parseComponent(rgbMatch[2], 255) / 255,
78
+ b: parseComponent(rgbMatch[3], 255) / 255,
79
+ a: rgbMatch[4] !== undefined ? parseComponent(rgbMatch[4], 1) : 1,
80
+ };
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ function parseHex(hex: string): RGBA | null {
87
+ const h = hex.slice(1);
88
+ let r: number, g: number, b: number, a = 1;
89
+
90
+ if (h.length === 3) {
91
+ r = parseInt(h[0] + h[0], 16) / 255;
92
+ g = parseInt(h[1] + h[1], 16) / 255;
93
+ b = parseInt(h[2] + h[2], 16) / 255;
94
+ } else if (h.length === 4) {
95
+ r = parseInt(h[0] + h[0], 16) / 255;
96
+ g = parseInt(h[1] + h[1], 16) / 255;
97
+ b = parseInt(h[2] + h[2], 16) / 255;
98
+ a = parseInt(h[3] + h[3], 16) / 255;
99
+ } else if (h.length === 6) {
100
+ r = parseInt(h.slice(0, 2), 16) / 255;
101
+ g = parseInt(h.slice(2, 4), 16) / 255;
102
+ b = parseInt(h.slice(4, 6), 16) / 255;
103
+ } else if (h.length === 8) {
104
+ r = parseInt(h.slice(0, 2), 16) / 255;
105
+ g = parseInt(h.slice(2, 4), 16) / 255;
106
+ b = parseInt(h.slice(4, 6), 16) / 255;
107
+ a = parseInt(h.slice(6, 8), 16) / 255;
108
+ } else {
109
+ return null;
110
+ }
111
+
112
+ return { r, g, b, a };
113
+ }
114
+
115
+ function parseComponent(value: string, max: number): number {
116
+ if (value.endsWith('%')) {
117
+ return (parseFloat(value) / 100) * max;
118
+ }
119
+ return parseFloat(value);
120
+ }
121
+
122
+ /** Default color: opaque black */
123
+ export const BLACK: RGBA = { r: 0, g: 0, b: 0, a: 1 };
124
+ /** Transparent black */
125
+ export const TRANSPARENT: RGBA = { r: 0, g: 0, b: 0, a: 0 };
@@ -0,0 +1,34 @@
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
+ }