canvas-emulator 1.0.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.
Files changed (147) hide show
  1. package/.gitignore +233 -0
  2. package/CLAUDE.md +61 -0
  3. package/README.md +128 -0
  4. package/dist/cjs/playground.d.ts +2 -0
  5. package/dist/cjs/playground.d.ts.map +1 -0
  6. package/dist/cjs/playground.js +37 -0
  7. package/dist/cjs/playground.js.map +1 -0
  8. package/dist/cjs/src/canvas/canvas.d.ts +41 -0
  9. package/dist/cjs/src/canvas/canvas.d.ts.map +1 -0
  10. package/dist/cjs/src/canvas/canvas.js +83 -0
  11. package/dist/cjs/src/canvas/canvas.js.map +1 -0
  12. package/dist/cjs/src/canvas/context-2d.d.ts +37 -0
  13. package/dist/cjs/src/canvas/context-2d.d.ts.map +1 -0
  14. package/dist/cjs/src/canvas/context-2d.js +245 -0
  15. package/dist/cjs/src/canvas/context-2d.js.map +1 -0
  16. package/dist/cjs/src/canvas/index.d.ts +2 -0
  17. package/dist/cjs/src/canvas/index.d.ts.map +1 -0
  18. package/dist/cjs/src/canvas/index.js +18 -0
  19. package/dist/cjs/src/canvas/index.js.map +1 -0
  20. package/dist/cjs/src/common/array.d.ts +40 -0
  21. package/dist/cjs/src/common/array.d.ts.map +1 -0
  22. package/dist/cjs/src/common/array.js +99 -0
  23. package/dist/cjs/src/common/array.js.map +1 -0
  24. package/dist/cjs/src/common/colors.d.ts +62 -0
  25. package/dist/cjs/src/common/colors.d.ts.map +1 -0
  26. package/dist/cjs/src/common/colors.js +93 -0
  27. package/dist/cjs/src/common/colors.js.map +1 -0
  28. package/dist/cjs/src/common/hash.d.ts +3 -0
  29. package/dist/cjs/src/common/hash.d.ts.map +1 -0
  30. package/dist/cjs/src/common/hash.js +44 -0
  31. package/dist/cjs/src/common/hash.js.map +1 -0
  32. package/dist/cjs/src/common/index.d.ts +7 -0
  33. package/dist/cjs/src/common/index.d.ts.map +1 -0
  34. package/dist/cjs/src/common/index.js +23 -0
  35. package/dist/cjs/src/common/index.js.map +1 -0
  36. package/dist/cjs/src/common/ppm.d.ts +2 -0
  37. package/dist/cjs/src/common/ppm.d.ts.map +1 -0
  38. package/dist/cjs/src/common/ppm.js +21 -0
  39. package/dist/cjs/src/common/ppm.js.map +1 -0
  40. package/dist/cjs/src/common/scalar.d.ts +27 -0
  41. package/dist/cjs/src/common/scalar.d.ts.map +1 -0
  42. package/dist/cjs/src/common/scalar.js +65 -0
  43. package/dist/cjs/src/common/scalar.js.map +1 -0
  44. package/dist/cjs/src/common/types.d.ts +9 -0
  45. package/dist/cjs/src/common/types.d.ts.map +1 -0
  46. package/dist/cjs/src/common/types.js +3 -0
  47. package/dist/cjs/src/common/types.js.map +1 -0
  48. package/dist/cjs/src/data/font/index.d.ts +2 -0
  49. package/dist/cjs/src/data/font/index.d.ts.map +1 -0
  50. package/dist/cjs/src/data/font/index.js +18 -0
  51. package/dist/cjs/src/data/font/index.js.map +1 -0
  52. package/dist/cjs/src/data/font/noto-mono-regular-48.d.ts +747 -0
  53. package/dist/cjs/src/data/font/noto-mono-regular-48.d.ts.map +1 -0
  54. package/dist/cjs/src/data/font/noto-mono-regular-48.js +15544 -0
  55. package/dist/cjs/src/data/font/noto-mono-regular-48.js.map +1 -0
  56. package/dist/cjs/src/data/font/noto-mono-regular-48.json +15540 -0
  57. package/dist/cjs/src/data/index.d.ts +2 -0
  58. package/dist/cjs/src/data/index.d.ts.map +1 -0
  59. package/dist/cjs/src/data/index.js +18 -0
  60. package/dist/cjs/src/data/index.js.map +1 -0
  61. package/dist/cjs/src/index.d.ts +4 -0
  62. package/dist/cjs/src/index.d.ts.map +1 -0
  63. package/dist/cjs/src/index.js +20 -0
  64. package/dist/cjs/src/index.js.map +1 -0
  65. package/dist/esm/playground.d.ts +2 -0
  66. package/dist/esm/playground.d.ts.map +1 -0
  67. package/dist/esm/playground.js +37 -0
  68. package/dist/esm/playground.js.map +1 -0
  69. package/dist/esm/src/canvas/canvas.d.ts +41 -0
  70. package/dist/esm/src/canvas/canvas.d.ts.map +1 -0
  71. package/dist/esm/src/canvas/canvas.js +83 -0
  72. package/dist/esm/src/canvas/canvas.js.map +1 -0
  73. package/dist/esm/src/canvas/context-2d.d.ts +37 -0
  74. package/dist/esm/src/canvas/context-2d.d.ts.map +1 -0
  75. package/dist/esm/src/canvas/context-2d.js +245 -0
  76. package/dist/esm/src/canvas/context-2d.js.map +1 -0
  77. package/dist/esm/src/canvas/index.d.ts +2 -0
  78. package/dist/esm/src/canvas/index.d.ts.map +1 -0
  79. package/dist/esm/src/canvas/index.js +18 -0
  80. package/dist/esm/src/canvas/index.js.map +1 -0
  81. package/dist/esm/src/common/array.d.ts +40 -0
  82. package/dist/esm/src/common/array.d.ts.map +1 -0
  83. package/dist/esm/src/common/array.js +99 -0
  84. package/dist/esm/src/common/array.js.map +1 -0
  85. package/dist/esm/src/common/colors.d.ts +62 -0
  86. package/dist/esm/src/common/colors.d.ts.map +1 -0
  87. package/dist/esm/src/common/colors.js +93 -0
  88. package/dist/esm/src/common/colors.js.map +1 -0
  89. package/dist/esm/src/common/hash.d.ts +3 -0
  90. package/dist/esm/src/common/hash.d.ts.map +1 -0
  91. package/dist/esm/src/common/hash.js +44 -0
  92. package/dist/esm/src/common/hash.js.map +1 -0
  93. package/dist/esm/src/common/index.d.ts +7 -0
  94. package/dist/esm/src/common/index.d.ts.map +1 -0
  95. package/dist/esm/src/common/index.js +23 -0
  96. package/dist/esm/src/common/index.js.map +1 -0
  97. package/dist/esm/src/common/ppm.d.ts +2 -0
  98. package/dist/esm/src/common/ppm.d.ts.map +1 -0
  99. package/dist/esm/src/common/ppm.js +21 -0
  100. package/dist/esm/src/common/ppm.js.map +1 -0
  101. package/dist/esm/src/common/scalar.d.ts +27 -0
  102. package/dist/esm/src/common/scalar.d.ts.map +1 -0
  103. package/dist/esm/src/common/scalar.js +65 -0
  104. package/dist/esm/src/common/scalar.js.map +1 -0
  105. package/dist/esm/src/common/types.d.ts +9 -0
  106. package/dist/esm/src/common/types.d.ts.map +1 -0
  107. package/dist/esm/src/common/types.js +3 -0
  108. package/dist/esm/src/common/types.js.map +1 -0
  109. package/dist/esm/src/data/font/index.d.ts +2 -0
  110. package/dist/esm/src/data/font/index.d.ts.map +1 -0
  111. package/dist/esm/src/data/font/index.js +18 -0
  112. package/dist/esm/src/data/font/index.js.map +1 -0
  113. package/dist/esm/src/data/font/noto-mono-regular-48.d.ts +747 -0
  114. package/dist/esm/src/data/font/noto-mono-regular-48.d.ts.map +1 -0
  115. package/dist/esm/src/data/font/noto-mono-regular-48.js +15544 -0
  116. package/dist/esm/src/data/font/noto-mono-regular-48.js.map +1 -0
  117. package/dist/esm/src/data/font/noto-mono-regular-48.json +15540 -0
  118. package/dist/esm/src/data/index.d.ts +2 -0
  119. package/dist/esm/src/data/index.d.ts.map +1 -0
  120. package/dist/esm/src/data/index.js +18 -0
  121. package/dist/esm/src/data/index.js.map +1 -0
  122. package/dist/esm/src/index.d.ts +4 -0
  123. package/dist/esm/src/index.d.ts.map +1 -0
  124. package/dist/esm/src/index.js +20 -0
  125. package/dist/esm/src/index.js.map +1 -0
  126. package/package.json +43 -0
  127. package/playground.ts +41 -0
  128. package/pnpm-lock.yaml +352 -0
  129. package/render_font.py +98 -0
  130. package/src/canvas/canvas.ts +123 -0
  131. package/src/canvas/context-2d.ts +305 -0
  132. package/src/canvas/index.ts +1 -0
  133. package/src/common/array.ts +176 -0
  134. package/src/common/colors.ts +93 -0
  135. package/src/common/hash.ts +46 -0
  136. package/src/common/index.ts +6 -0
  137. package/src/common/ppm.ts +28 -0
  138. package/src/common/scalar.ts +72 -0
  139. package/src/common/types.ts +10 -0
  140. package/src/data/font/index.ts +1 -0
  141. package/src/data/font/noto-mono-regular-48.json +15540 -0
  142. package/src/data/font/noto-mono-regular-48.ts +15540 -0
  143. package/src/data/index.ts +1 -0
  144. package/src/index.ts +3 -0
  145. package/tsconfig.cjs.json +10 -0
  146. package/tsconfig.esm.json +10 -0
  147. package/tsconfig.json +23 -0
@@ -0,0 +1,123 @@
1
+ import { V4 } from "../common";
2
+ import { range, toBatched } from "../common/array";
3
+ import { clamp, smoothstep } from "../common/scalar";
4
+ import { CanvasContext2DImpl, type CanvasContext2D } from "./context-2d";
5
+
6
+ export type Pixel = [number, number, number, number];
7
+ export type Point = [number, number];
8
+
9
+ export interface Canvas {
10
+ width: number;
11
+ height: number;
12
+ channels: number;
13
+ data: Array<number>;
14
+
15
+ indexAt(p: Point): number;
16
+ at(p: Point): Pixel;
17
+ atUV(p: Point): Pixel;
18
+
19
+ setAt(p: Point, px: Pixel | number): void;
20
+ setAtUV(p: Point, px: Pixel | number): void;
21
+
22
+ each(fn: (pos: Point, uv: Point) => void): void;
23
+
24
+ renderRGBA(): Array<Pixel>;
25
+ renderRGB(): Array<[number, number, number]>;
26
+
27
+ getContext2D(): CanvasContext2D;
28
+ }
29
+
30
+ export type CanvasConfig = {
31
+ width: number;
32
+ height: number;
33
+ };
34
+
35
+ export class CanvasImpl implements Canvas {
36
+ width: number;
37
+ height: number;
38
+ channels: 4 = 4;
39
+ data: Array<number>;
40
+
41
+ private readonly length: number;
42
+
43
+ constructor(cfg: CanvasConfig) {
44
+ const { width, height } = cfg;
45
+ this.width = width;
46
+ this.height = height;
47
+ this.length = width * height * this.channels;
48
+ this.data = range(this.length).fill(0);
49
+ }
50
+
51
+ indexAt(p: Point): number {
52
+ const x = Math.floor(p[0]);
53
+ const y = Math.floor(p[1]);
54
+ return clamp((y * this.width + x) * this.channels, 0, this.length - 1);
55
+ }
56
+
57
+ at(p: Point): Pixel {
58
+ const idx = this.indexAt(p);
59
+ return this.data.slice(idx, idx + this.channels) as Pixel;
60
+ }
61
+
62
+ atUV(p: Point): Pixel {
63
+ const x = p[0] * this.width;
64
+ const y = p[1] * this.height;
65
+ return this.at([x, y]);
66
+ }
67
+
68
+ setAt(p: Point, px: Pixel | number): void {
69
+ const idx = this.indexAt(p);
70
+ const nextAlpha = typeof px === "number" ? 1.0 : px[3];
71
+ const current = this.at(p);
72
+ const currentAlpha = current[3];
73
+
74
+ const nextData: V4 = typeof px === "number" ? [px, px, px, px] : px;
75
+
76
+ const mixCurrent = smoothstep(0, 1, currentAlpha - nextAlpha);
77
+ const mixNext = smoothstep(0, 1, nextAlpha);
78
+
79
+ for (let i = 0; i < nextData.length; i++) {
80
+ this.data[idx + i] =
81
+ this.data[idx + i]! * mixCurrent + nextData[i]! * mixNext;
82
+ }
83
+ }
84
+
85
+ setAtUV(p: Point, px: Pixel | number): void {
86
+ const x = p[0] * this.width;
87
+ const y = p[1] * this.height;
88
+ return this.setAt([x, y], px);
89
+ }
90
+
91
+ each(fn: (pos: Point, uv: Point) => void): void {
92
+ for (let y = 0; y < this.height; y++) {
93
+ for (let x = 0; x < this.width; x++) {
94
+ const p: Point = [x, y];
95
+ const uv: Point = [x / this.width, y / this.height];
96
+ fn(p, uv);
97
+ }
98
+ }
99
+ }
100
+
101
+ renderRGBA(): Array<Pixel> {
102
+ return toBatched(this.data, this.channels, 0);
103
+ }
104
+
105
+ renderRGB(): Array<[number, number, number]> {
106
+ const pixels: Array<[number, number, number]> = [];
107
+ for (let i = 0; i < this.data.length; i += this.channels) {
108
+ const r = this.data[i]!;
109
+ const g = this.data[i + 1]!;
110
+ const b = this.data[i + 2]!;
111
+ pixels.push([r, g, b]);
112
+ }
113
+ return pixels;
114
+ }
115
+
116
+ getContext2D(): CanvasContext2D {
117
+ return new CanvasContext2DImpl(this);
118
+ }
119
+ }
120
+
121
+ export const createCanvas = (cfg: CanvasConfig): Canvas => {
122
+ return new CanvasImpl(cfg);
123
+ };
@@ -0,0 +1,305 @@
1
+ import { clamp, remap, toRGB, V2 } from "../common";
2
+ import { type Canvas } from "./canvas";
3
+ import { NOTO_MONO_REGULAR_48 as font } from "../data/font/noto-mono-regular-48";
4
+
5
+ const FONT_WIDTH: number = 48;
6
+
7
+ export type TextMetrics = {
8
+ width: number;
9
+ };
10
+
11
+ class State {
12
+ fillStyle: string = "red";
13
+ strokeStyle: string = "red";
14
+ cursor: V2 = [0, 0];
15
+ points: V2[] = [];
16
+ paths: Array<V2[]> = [];
17
+ activePath: V2[] | null = null;
18
+ fontSize: number = FONT_WIDTH;
19
+
20
+ clone(): State {
21
+ const copy = new State();
22
+ copy.fillStyle = this.fillStyle;
23
+ copy.strokeStyle = this.strokeStyle;
24
+ copy.cursor = [this.cursor[0], this.cursor[1]];
25
+ copy.points = [...this.points.map((p): V2 => [p[0], p[1]])];
26
+ copy.paths = this.paths.map((p) => [...p]);
27
+ copy.activePath = this.activePath ? [...this.activePath] : null;
28
+ copy.fontSize = this.fontSize;
29
+ return copy;
30
+ }
31
+
32
+ get path(): V2[] {
33
+ if (this.paths.length <= 0) return this.points;
34
+ return this.paths[this.paths.length - 1]!;
35
+ }
36
+
37
+ beginPath() {
38
+ this.paths.push([]);
39
+ }
40
+
41
+ closePath() {
42
+ const path = this.paths.pop();
43
+ this.activePath = path || null;
44
+ }
45
+
46
+ addPoint(point: V2) {
47
+ if (this.activePath !== null) {
48
+ this.activePath.push([point[0], point[1]]);
49
+ return;
50
+ }
51
+ this.points.push([point[0], point[1]]);
52
+ }
53
+
54
+ addCursor() {
55
+ this.addPoint(this.cursor);
56
+ }
57
+
58
+ setCursor(x: number, y: number) {
59
+ this.cursor = [x, y];
60
+ }
61
+ }
62
+
63
+ export type CanvasContext2D = CanvasContext2DImpl;
64
+
65
+ export class CanvasContext2DImpl {
66
+ canvas: Canvas;
67
+ private stack: Array<State> = [new State()];
68
+
69
+ constructor(canvas: Canvas) {
70
+ this.canvas = canvas;
71
+ }
72
+
73
+ private get state(): State {
74
+ const states = this.stack;
75
+ if (states.length <= 0) throw new Error("Out of states");
76
+ return states[states.length - 1]!;
77
+ }
78
+
79
+ get fillStyle(): string {
80
+ return this.state.fillStyle;
81
+ }
82
+ set fillStyle(value: string) {
83
+ this.state.fillStyle = value;
84
+ }
85
+
86
+ get strokeStyle(): string {
87
+ return this.state.strokeStyle;
88
+ }
89
+ set strokeStyle(value: string) {
90
+ this.state.strokeStyle = value;
91
+ }
92
+
93
+ save() {
94
+ this.stack.push(this.state.clone());
95
+ }
96
+
97
+ restore(): void {
98
+ this.stack.pop();
99
+ }
100
+
101
+ moveTo(x: number, y: number): void {
102
+ this.state.setCursor(x, y);
103
+ }
104
+
105
+ lineTo(x: number, y: number): void {
106
+ this.state.addCursor();
107
+ this.state.addPoint([x, y]);
108
+ }
109
+
110
+ arc(x: number, y: number, radius: number, startAngle: number): void {
111
+ const segments = 32;
112
+ for (let i = 0; i < segments; i++) {
113
+ const angle1 = remap(i, [0, segments], [startAngle, Math.PI * 2]);
114
+ const angle2 = remap(i + 1, [0, segments], [startAngle, Math.PI * 2]);
115
+ const p1: V2 = [
116
+ x + Math.cos(angle1) * radius,
117
+ y + Math.sin(angle1) * radius,
118
+ ];
119
+ const p2: V2 = [
120
+ x + Math.cos(angle2) * radius,
121
+ y + Math.sin(angle2) * radius,
122
+ ];
123
+ this.state.addPoint(p1);
124
+ this.state.addPoint(p2);
125
+ }
126
+ }
127
+
128
+ rect(x: number, y: number, width: number, height: number): void {
129
+ const tl: V2 = [x, y];
130
+ const tr: V2 = [x + width, y];
131
+ const br: V2 = [x + width, y + height];
132
+ const bl: V2 = [x, y + height];
133
+ this.state.addPoint(tl);
134
+ this.state.addPoint(tr); // top
135
+ this.state.addPoint(tr);
136
+ this.state.addPoint(br); // right
137
+ this.state.addPoint(br);
138
+ this.state.addPoint(bl); // bottom
139
+ this.state.addPoint(bl);
140
+ this.state.addPoint(tl); // left
141
+ }
142
+
143
+ fill(): void {
144
+ const path = this.state.path;
145
+ if (path.length < 3) return;
146
+
147
+ const rgb = toRGB(this.fillStyle);
148
+ const pixel: [number, number, number, number] = [rgb[0], rgb[1], rgb[2], 1];
149
+
150
+ let minY = path[0]![1];
151
+ let maxY = path[0]![1];
152
+ for (const p of path) {
153
+ if (p[1] < minY) minY = p[1];
154
+ if (p[1] > maxY) maxY = p[1];
155
+ }
156
+
157
+ for (let y = Math.ceil(minY); y <= Math.floor(maxY); y++) {
158
+ const intersections: number[] = [];
159
+
160
+ for (let i = 0; i < path.length; i++) {
161
+ const a = path[i]!;
162
+ const b = path[(i + 1) % path.length]!;
163
+ const y1 = a[1];
164
+ const y2 = b[1];
165
+
166
+ if ((y1 <= y && y < y2) || (y2 <= y && y < y1)) {
167
+ const x = a[0] + ((y - y1) * (b[0] - a[0])) / (y2 - y1);
168
+ intersections.push(x);
169
+ }
170
+ }
171
+
172
+ intersections.sort((a, b) => a - b);
173
+
174
+ for (let i = 0; i < intersections.length; i += 2) {
175
+ const x1 = Math.ceil(intersections[i]!);
176
+ const x2 = Math.floor(intersections[i + 1]!);
177
+ for (let x = x1; x <= x2; x++) {
178
+ this.canvas.setAt([x, y], pixel);
179
+ }
180
+ }
181
+ }
182
+ }
183
+ stroke(): void {
184
+ const rgb = toRGB(this.strokeStyle);
185
+ const pixel: [number, number, number, number] = [rgb[0], rgb[1], rgb[2], 1];
186
+
187
+ for (let i = 0; i < this.state.path.length; i += 2) {
188
+ const a = this.state.path[(i + 0) % this.state.path.length]!;
189
+ const b = this.state.path[(i + 1) % this.state.path.length]!;
190
+ const x1 = a[0];
191
+ const y1 = a[1];
192
+ const x2 = b[0];
193
+ const y2 = b[1];
194
+ const dx = x2 - x1;
195
+ const dy = y2 - y1;
196
+
197
+ if (Math.abs(dx) >= Math.abs(dy)) {
198
+ const step = dx >= 0 ? 1 : -1;
199
+ const m = dy / dx;
200
+ for (let x = x1; step > 0 ? x < x2 : x > x2; x += step) {
201
+ const y = m * (x - x1) + y1;
202
+ this.canvas.setAt([x, y], pixel);
203
+ }
204
+ } else {
205
+ const step = dy >= 0 ? 1 : -1;
206
+ const m = dx / dy;
207
+ for (let y = y1; step > 0 ? y < y2 : y > y2; y += step) {
208
+ const x = m * (y - y1) + x1;
209
+ this.canvas.setAt([x, y], pixel);
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ strokeRect(x: number, y: number, width: number, height: number): void {
216
+ this.rect(x, y, width, height);
217
+ this.stroke();
218
+ }
219
+
220
+ fillRect(x: number, y: number, width: number, height: number): void {
221
+ this.rect(x, y, width, height);
222
+ this.fill();
223
+ }
224
+
225
+ clearRect(x: number, y: number, width: number, height: number): void {
226
+ for (let py = 0; py < height; py++) {
227
+ for (let px = 0; px < width; px++) {
228
+ this.canvas.setAt([x + px, y + py], [0, 0, 0, 0]);
229
+ }
230
+ }
231
+ }
232
+
233
+ private drawChar(
234
+ char: string,
235
+ x: number,
236
+ y: number,
237
+ width: number,
238
+ ratio: number,
239
+ ): {
240
+ advance: number;
241
+ offset_x: number;
242
+ offset_y: number;
243
+ width: number;
244
+ height: number;
245
+ } {
246
+ this.state.fontSize = width;
247
+ const code = char.charCodeAt(0);
248
+ const glyph = font[`${code}` as keyof typeof font];
249
+ if (typeof glyph === "undefined") throw new Error(`Cannot draw: ${char}`);
250
+ const pixels = glyph.data;
251
+
252
+ const height = width;
253
+
254
+ for (let py = 0; py < height; py++) {
255
+ for (let px = 0; px < width; px++) {
256
+ const pixelY = Math.floor((py / height) * FONT_WIDTH);
257
+ const pixelX = Math.floor((px / width) * FONT_WIDTH);
258
+ const idx = clamp(
259
+ pixelY * FONT_WIDTH + pixelX,
260
+ 0,
261
+ FONT_WIDTH * FONT_WIDTH - 1,
262
+ );
263
+ const alpha = pixels[idx]!;
264
+
265
+ const imgX = x + px;
266
+ const imgY = y + py + ratio * 0.5 * glyph.offset_y;
267
+
268
+ const rgb = toRGB(this.fillStyle);
269
+ this.canvas.setAt([imgX, imgY], [rgb[0], rgb[1], rgb[2], alpha]);
270
+ }
271
+ }
272
+
273
+ return glyph;
274
+ }
275
+
276
+ fillText(text: string, x: number, y: number): void;
277
+ fillText(text: string, x: number, y: number, maxWidth: number): void;
278
+ fillText(text: string, x: number, y: number, maxWidth: undefined): void;
279
+ fillText(text: string, x: number, y: number, maxWidth?: number): void {
280
+ const width = maxWidth || FONT_WIDTH;
281
+ const ratio = width / FONT_WIDTH;
282
+ for (const c of text) {
283
+ const glyph = this.drawChar(c, x, y, width, ratio);
284
+ x += ratio * glyph.advance;
285
+ }
286
+ }
287
+
288
+ strokeText(text: string, x: number, y: number): void;
289
+ strokeText(text: string, x: number, y: number, maxWidth: number): void;
290
+ strokeText(text: string, x: number, y: number, maxWidth: undefined): void;
291
+ strokeText(text: string, x: number, y: number, maxWidth?: number): void {
292
+ this.fillText(text, x, y, maxWidth || 48);
293
+ }
294
+
295
+ measureText(text: string): TextMetrics {
296
+ return { width: text.length * this.state.fontSize * 0.5 };
297
+ }
298
+
299
+ beginPath(): void {
300
+ this.state.beginPath();
301
+ }
302
+ closePath(): void {
303
+ this.state.closePath();
304
+ }
305
+ }
@@ -0,0 +1 @@
1
+ export * from "./canvas";
@@ -0,0 +1,176 @@
1
+ import { type Triple, type Pair, V2, V3 } from "./types";
2
+
3
+ export const range = (n: number): number[] =>
4
+ n <= 0 || typeof n !== "number" || isNaN(n) || !isFinite(n) ?
5
+ []
6
+ : Array.from(Array(Math.floor(n)).keys());
7
+
8
+ export const zipMin = <A, B>(a: A[], b: B[]): Array<Pair<A, B>> =>
9
+ range(Math.min(a.length, b.length)).map((i): Pair<A, B> => {
10
+ const x = a[i]!;
11
+ const y = b[i]!;
12
+ return [x, y];
13
+ });
14
+
15
+ export const zipMax = <A, B>(
16
+ a: A[],
17
+ b: B[],
18
+ ): Array<Pair<A | undefined, B | undefined>> =>
19
+ range(Math.min(a.length, b.length)).map(
20
+ (i): Pair<A | undefined, B | undefined> => {
21
+ const x = a[i];
22
+ const y = b[i];
23
+ return [x, y];
24
+ },
25
+ );
26
+
27
+ export function toBatched<T>(arr: T[], batchSize: 1, padding: T): Array<[T]>;
28
+ export function toBatched<T>(arr: T[], batchSize: 2, padding: T): Array<[T, T]>;
29
+ export function toBatched<T>(
30
+ arr: T[],
31
+ batchSize: 3,
32
+ padding: T,
33
+ ): Array<[T, T, T]>;
34
+ export function toBatched<T>(
35
+ arr: T[],
36
+ batchSize: 4,
37
+ padding: T,
38
+ ): Array<[T, T, T, T]>;
39
+ export function toBatched<T>(
40
+ arr: T[],
41
+ batchSize: 5,
42
+ padding: T,
43
+ ): Array<[T, T, T, T, T]>;
44
+ export function toBatched<T>(
45
+ arr: T[],
46
+ batchSize: 6,
47
+ padding: T,
48
+ ): Array<[T, T, T, T, T, T]>;
49
+ export function toBatched<T>(
50
+ arr: T[],
51
+ batchSize: 7,
52
+ padding: T,
53
+ ): Array<[T, T, T, T, T, T, T]>;
54
+ export function toBatched<T>(
55
+ arr: T[],
56
+ batchSize: 8,
57
+ padding: T,
58
+ ): Array<[T, T, T, T, T, T, T, T]>;
59
+ export function toBatched<T>(
60
+ arr: T[],
61
+ batchSize: number,
62
+ padding: T,
63
+ ): Array<T[]>;
64
+ export function toBatched<T>(arr: T[], batchSize: number): Array<T[]>;
65
+ export function toBatched<T>(
66
+ arr: T[],
67
+ batchSize: number,
68
+ padding?: T,
69
+ ): Array<T[]>;
70
+ export function toBatched<T>(
71
+ arr: T[],
72
+ batchSize: number,
73
+ padding?: T,
74
+ ): Array<T[]> {
75
+ const output: Array<T[]> = [];
76
+
77
+ for (let i = 0; i < arr.length; i += batchSize) {
78
+ const batch = arr.slice(i, i + batchSize);
79
+ if (padding !== undefined && batch.length < batchSize) {
80
+ batch.push(...Array<T>(batchSize - batch.length).fill(padding));
81
+ }
82
+ output.push(batch);
83
+ }
84
+
85
+ return output;
86
+ }
87
+
88
+ export const sum = (arr: number[]): number => arr.reduce((a, b) => a + b, 0);
89
+
90
+ export const average = (arr: number[]): number =>
91
+ arr.length <= 0 ? 0 : sum(arr) / arr.length;
92
+
93
+ export const magnitude = (arr: number[]): number => Math.hypot(...arr);
94
+
95
+ export const normalizeSq = <Arr extends number[]>(arr: Arr): Arr => {
96
+ const mag = magnitude(arr);
97
+ if (Math.abs(mag) < 0.00000000000000001) return [...arr] as Arr;
98
+ return arr.map((x) => x / mag) as Arr;
99
+ };
100
+
101
+ // alias for normalizeSq
102
+ export const unit = <Arr extends number[]>(arr: Arr): Arr => normalizeSq(arr);
103
+
104
+ export const dot = (a: number[], b: number[]): number =>
105
+ sum(zipMin(a, b).map(([v1, v2]) => v1 * v2));
106
+
107
+ export const cross = (
108
+ a: number[] | Triple<number, number, number>,
109
+ b: number[] | Triple<number, number, number>,
110
+ ): Triple<number, number, number> => {
111
+ if (a.length !== 3)
112
+ throw new Error("cross() can only be used on 3-dimensional vectors");
113
+ if (b.length !== a.length)
114
+ throw new Error("both vectors must have the same length");
115
+
116
+ const ax = a[0]!;
117
+ const ay = a[1]!;
118
+ const az = a[2]!;
119
+
120
+ const bx = a[0]!;
121
+ const by = a[1]!;
122
+ const bz = a[2]!;
123
+
124
+ const x = ay * bz - az * by;
125
+ const y = az * bx - ax * bz;
126
+ const z = ax * by - ay * bx;
127
+
128
+ return [x, y, z];
129
+ };
130
+
131
+ export function add(a: V2, b: V2): V2;
132
+ export function add(a: V3, b: V3): V3;
133
+ export function add(a: number[], b: number[]): number[];
134
+ export function add(a: number[], b: number[]): number[] {
135
+ return zipMin(a, b).map(([v1, v2]) => v1 + v2);
136
+ }
137
+
138
+ export function sub(a: V2, b: V2): V2;
139
+ export function sub(a: V3, b: V3): V3;
140
+ export function sub(a: number[], b: number[]): number[];
141
+ export function sub(a: number[], b: number[]): number[] {
142
+ return zipMin(a, b).map(([v1, v2]) => v1 - v2);
143
+ }
144
+
145
+ export function mul(a: V2, b: V2): V2;
146
+ export function mul(a: V3, b: V3): V3;
147
+ export function mul(a: number[], b: number[]): number[];
148
+ export function mul(a: number[], b: number[]): number[] {
149
+ return zipMin(a, b).map(([v1, v2]) => v1 * v2);
150
+ }
151
+
152
+ export function div(a: V2, b: V2): V2;
153
+ export function div(a: V3, b: V3): V3;
154
+ export function div(a: number[], b: number[]): number[];
155
+ export function div(a: number[], b: number[]): number[] {
156
+ return zipMin(a, b).map(([v1, v2]) => v1 / v2);
157
+ }
158
+
159
+ export function scale(a: V2, s: number): V2;
160
+ export function scale(a: V3, s: number): V3;
161
+ export function scale(a: number[], s: number): number[];
162
+ export function scale(a: number[], s: number): number[] {
163
+ return a.map((v) => v * s);
164
+ }
165
+
166
+ export const distance = (a: number[], b: number[]): number =>
167
+ magnitude(sub(b, a));
168
+
169
+ export const cosineDistance = (a: number[], b: number[]): number => {
170
+ const dp = dot(a, b);
171
+ const magA = magnitude(a);
172
+ const magB = magnitude(b);
173
+ const result = dp / (magA * magB);
174
+ if (isNaN(result) || !isFinite(result)) return 0;
175
+ return result;
176
+ };
@@ -0,0 +1,93 @@
1
+ import { V3 } from "./types";
2
+
3
+ export const COLOR_NAMES_TO_RGB = {
4
+ red: [1, 0, 0],
5
+ green: [0, 1, 0],
6
+ blue: [0, 0, 1],
7
+ white: [1, 1, 1],
8
+ black: [0, 0, 0],
9
+ yellow: [1, 1, 0],
10
+ cyan: [0, 1, 1],
11
+ magenta: [1, 0, 1],
12
+ orange: [1, 0.647, 0],
13
+ purple: [0.502, 0, 0.502],
14
+ pink: [1, 0.753, 0.796],
15
+ lime: [0, 1, 0],
16
+ navy: [0, 0, 0.502],
17
+ teal: [0, 0.502, 0.502],
18
+ maroon: [0.502, 0, 0],
19
+ olive: [0.502, 0.502, 0],
20
+ silver: [0.753, 0.753, 0.753],
21
+ gray: [0.502, 0.502, 0.502],
22
+ grey: [0.502, 0.502, 0.502],
23
+ lightgray: [0.827, 0.827, 0.827],
24
+ lightgrey: [0.827, 0.827, 0.827],
25
+ darkgray: [0.663, 0.663, 0.663],
26
+ darkgrey: [0.663, 0.663, 0.663],
27
+ dimgray: [0.412, 0.412, 0.412],
28
+ dimgrey: [0.412, 0.412, 0.412],
29
+ lightblue: [0.678, 0.847, 0.902],
30
+ lightcyan: [0.878, 1, 1],
31
+ lightgreen: [0.565, 0.933, 0.565],
32
+ lightpink: [1, 0.714, 0.757],
33
+ lightyellow: [1, 1, 0.878],
34
+ lightsalmon: [1, 0.627, 0.478],
35
+ darkblue: [0, 0, 0.545],
36
+ darkgreen: [0, 0.392, 0],
37
+ darkred: [0.545, 0, 0],
38
+ darkcyan: [0, 0.545, 0.545],
39
+ darkmagenta: [0.545, 0, 0.545],
40
+ darkorange: [1, 0.549, 0],
41
+ darkviolet: [0.58, 0, 0.827],
42
+ darkturquoise: [0, 0.808, 0.82],
43
+ hotpink: [1, 0.412, 0.706],
44
+ deeppink: [1, 0.078, 0.576],
45
+ coral: [1, 0.498, 0.314],
46
+ salmon: [0.98, 0.502, 0.447],
47
+ tomato: [1, 0.388, 0.278],
48
+ turquoise: [0.251, 0.878, 0.816],
49
+ violet: [0.933, 0.51, 0.933],
50
+ indigo: [0.294, 0, 0.51],
51
+ gold: [1, 0.843, 0],
52
+ khaki: [0.941, 0.902, 0.549],
53
+ beige: [0.961, 0.961, 0.863],
54
+ ivory: [1, 1, 0.941],
55
+ lavender: [0.902, 0.902, 0.98],
56
+ chocolate: [0.824, 0.412, 0.118],
57
+ peru: [0.804, 0.522, 0.247],
58
+ sienna: [0.627, 0.322, 0.176],
59
+ brown: [0.647, 0.165, 0.165],
60
+ tan: [0.824, 0.706, 0.549],
61
+ } as const;
62
+
63
+ export const toRGB = (color: string): V3 => {
64
+ if (color in COLOR_NAMES_TO_RGB) {
65
+ return COLOR_NAMES_TO_RGB[color as keyof typeof COLOR_NAMES_TO_RGB]! as V3;
66
+ }
67
+ if (color.startsWith("#")) {
68
+ const hex = color.slice(1);
69
+ if (hex.length === 3) {
70
+ const r = parseInt(hex[0]! + hex[0]!, 16) / 255;
71
+ const g = parseInt(hex[1]! + hex[1]!, 16) / 255;
72
+ const b = parseInt(hex[2]! + hex[2]!, 16) / 255;
73
+ return [r, g, b];
74
+ }
75
+ if (hex.length === 6) {
76
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
77
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
78
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
79
+ return [r, g, b];
80
+ }
81
+ }
82
+
83
+ if (color.startsWith("rgba(") || color.startsWith("rgb(")) {
84
+ const inner = color.slice(color.indexOf("(") + 1, color.lastIndexOf(")"));
85
+ const parts = inner.split(",").map((s) => s.trim());
86
+ const r = parseInt(parts[0]!) / 255;
87
+ const g = parseInt(parts[1]!) / 255;
88
+ const b = parseInt(parts[2]!) / 255;
89
+ return [r, g, b];
90
+ }
91
+
92
+ return [0, 0, 0];
93
+ };