@guinetik/gcanvas 1.0.5 → 2.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.
- package/dist/aizawa.html +27 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/gcanvas.es.js +5130 -372
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/halvorsen.html +27 -0
- package/dist/index.html +96 -48
- package/dist/js/aizawa.js +425 -0
- package/dist/js/bezier.js +5 -5
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/isometric.js +34 -46
- package/dist/js/lorenz.js +425 -0
- package/dist/js/painter.js +8 -8
- package/dist/js/rossler.js +480 -0
- package/dist/js/schrodinger.js +314 -18
- package/dist/js/thomas.js +394 -0
- package/dist/lorenz.html +27 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/thomas.html +27 -0
- package/package.json +1 -1
- package/readme.md +30 -22
- package/src/game/objects/go.js +7 -0
- package/src/game/objects/index.js +2 -0
- package/src/game/objects/isometric-scene.js +53 -3
- package/src/game/objects/layoutscene.js +57 -0
- package/src/game/objects/mask.js +241 -0
- package/src/game/objects/scene.js +19 -0
- package/src/game/objects/wrapper.js +14 -2
- package/src/game/pipeline.js +17 -0
- package/src/game/ui/button.js +101 -16
- package/src/game/ui/theme.js +0 -6
- package/src/game/ui/togglebutton.js +25 -14
- package/src/game/ui/tooltip.js +12 -4
- package/src/index.js +3 -0
- package/src/io/gesture.js +409 -0
- package/src/io/index.js +4 -1
- package/src/io/keys.js +9 -1
- package/src/io/screen.js +476 -0
- package/src/math/attractors.js +664 -0
- package/src/math/heat.js +106 -0
- package/src/math/index.js +1 -0
- package/src/mixins/draggable.js +15 -19
- package/src/painter/painter.shapes.js +11 -5
- package/src/particle/particle-system.js +165 -1
- package/src/physics/index.js +26 -0
- package/src/physics/physics-updaters.js +333 -0
- package/src/physics/physics.js +375 -0
- package/src/shapes/image.js +5 -5
- package/src/shapes/index.js +2 -0
- package/src/shapes/parallelogram.js +147 -0
- package/src/shapes/righttriangle.js +115 -0
- package/src/shapes/svg.js +281 -100
- package/src/shapes/text.js +22 -6
- package/src/shapes/transformable.js +5 -0
- package/src/sound/effects.js +807 -0
- package/src/sound/index.js +13 -0
- package/src/webgl/index.js +7 -0
- package/src/webgl/shaders/clifford-point-shaders.js +131 -0
- package/src/webgl/shaders/dejong-point-shaders.js +131 -0
- package/src/webgl/shaders/point-sprite-shaders.js +152 -0
- package/src/webgl/webgl-clifford-renderer.js +477 -0
- package/src/webgl/webgl-dejong-renderer.js +472 -0
- package/src/webgl/webgl-line-renderer.js +391 -0
- package/src/webgl/webgl-particle-renderer.js +410 -0
- package/types/index.d.ts +30 -2
- package/types/io.d.ts +217 -0
- package/types/physics.d.ts +299 -0
- package/types/shapes.d.ts +8 -0
- package/types/webgl.d.ts +188 -109
package/src/shapes/svg.js
CHANGED
|
@@ -5,6 +5,34 @@ import { Shape } from "./shape";
|
|
|
5
5
|
* SVGShape - A specialized Shape class that can render SVG path data
|
|
6
6
|
*/
|
|
7
7
|
export class SVGShape extends Shape {
|
|
8
|
+
/**
|
|
9
|
+
* Load an SVG file from a URL and create an SVGShape
|
|
10
|
+
* @param {string} url - URL to the SVG file
|
|
11
|
+
* @param {object} options - Standard shape options plus SVG-specific options
|
|
12
|
+
* @returns {Promise<SVGShape>} Promise that resolves to the SVGShape instance
|
|
13
|
+
*/
|
|
14
|
+
static async fromURL(url, options = {}) {
|
|
15
|
+
const response = await fetch(url);
|
|
16
|
+
const svgText = await response.text();
|
|
17
|
+
|
|
18
|
+
// Parse SVG to extract path data
|
|
19
|
+
const parser = new DOMParser();
|
|
20
|
+
const doc = parser.parseFromString(svgText, 'image/svg+xml');
|
|
21
|
+
|
|
22
|
+
// Get the first path element's d attribute
|
|
23
|
+
const pathElement = doc.querySelector('path');
|
|
24
|
+
if (!pathElement) {
|
|
25
|
+
throw new Error('No path element found in SVG');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const pathData = pathElement.getAttribute('d');
|
|
29
|
+
if (!pathData) {
|
|
30
|
+
throw new Error('Path element has no d attribute');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return new SVGShape(pathData, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
/**
|
|
9
37
|
* @param {number} x - Center X position
|
|
10
38
|
* @param {number} y - Center Y position
|
|
@@ -15,9 +43,10 @@ export class SVGShape extends Shape {
|
|
|
15
43
|
* @param {number} [options.animationProgress=1] - Animation progress (0-1)
|
|
16
44
|
*/
|
|
17
45
|
constructor(svgPathData, options = {}) {
|
|
18
|
-
console.log("SVGShape", options.x)
|
|
19
46
|
super(options);
|
|
20
47
|
// SVG specific options
|
|
48
|
+
// Note: 'scale' is for pre-scaling the path data
|
|
49
|
+
// scaleX/scaleY are inherited from Transformable for runtime transforms (e.g., flipping)
|
|
21
50
|
this.scale = options.scale || 1;
|
|
22
51
|
this.centerPath =
|
|
23
52
|
options.centerPath !== undefined ? options.centerPath : true;
|
|
@@ -46,76 +75,201 @@ export class SVGShape extends Shape {
|
|
|
46
75
|
|
|
47
76
|
/**
|
|
48
77
|
* Parse an SVG path string into a command array for rendering
|
|
78
|
+
* Parses commands in sequential order to maintain path integrity
|
|
49
79
|
* @param {string} svgPath - SVG path data string
|
|
50
80
|
* @returns {Array} Array of path commands
|
|
51
81
|
*/
|
|
52
82
|
parseSVGPath(svgPath) {
|
|
53
|
-
// Regular expressions to match SVG path commands
|
|
54
|
-
const moveRegex = /M\s*([-\d.]+)[,\s]*([-\d.]+)/g;
|
|
55
|
-
const lineRegex = /L\s*([-\d.]+)[,\s]*([-\d.]+)/g;
|
|
56
|
-
const curveRegex =
|
|
57
|
-
/C\s*([-\d.]+)[,\s]*([-\d.]+)\s*([-\d.]+)[,\s]*([-\d.]+)\s*([-\d.]+)[,\s]*([-\d.]+)/g;
|
|
58
|
-
const zRegex = /Z/g;
|
|
59
|
-
|
|
60
|
-
// Arrays to store the parsed commands
|
|
61
83
|
const commands = [];
|
|
62
|
-
|
|
63
|
-
//
|
|
84
|
+
|
|
85
|
+
// Track current position for converting lines to curves
|
|
86
|
+
let currentX = 0;
|
|
87
|
+
let currentY = 0;
|
|
88
|
+
let subpathStartX = 0;
|
|
89
|
+
let subpathStartY = 0;
|
|
90
|
+
|
|
91
|
+
// Single regex to match all commands in order
|
|
92
|
+
// Matches: M, m, L, l, H, h, V, v, C, c, S, s, Q, q, T, t, A, a, Z, z
|
|
93
|
+
const commandRegex = /([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)/g;
|
|
94
|
+
|
|
64
95
|
let match;
|
|
65
|
-
while ((match =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
while ((match = commandRegex.exec(svgPath)) !== null) {
|
|
97
|
+
const command = match[1];
|
|
98
|
+
const argsStr = match[2].trim();
|
|
99
|
+
|
|
100
|
+
// Parse numbers from arguments
|
|
101
|
+
const args = argsStr.length > 0
|
|
102
|
+
? argsStr.split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n))
|
|
103
|
+
: [];
|
|
104
|
+
|
|
105
|
+
switch (command) {
|
|
106
|
+
case 'M': // Absolute moveto
|
|
107
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
108
|
+
currentX = args[i];
|
|
109
|
+
currentY = args[i + 1];
|
|
110
|
+
if (i === 0) {
|
|
111
|
+
subpathStartX = currentX;
|
|
112
|
+
subpathStartY = currentY;
|
|
113
|
+
commands.push(['M', currentX, currentY]);
|
|
114
|
+
} else {
|
|
115
|
+
// Subsequent coordinate pairs are implicit lineto
|
|
116
|
+
commands.push(['L', currentX, currentY]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'm': // Relative moveto
|
|
122
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
123
|
+
currentX += args[i];
|
|
124
|
+
currentY += args[i + 1];
|
|
125
|
+
if (i === 0) {
|
|
126
|
+
subpathStartX = currentX;
|
|
127
|
+
subpathStartY = currentY;
|
|
128
|
+
commands.push(['M', currentX, currentY]);
|
|
129
|
+
} else {
|
|
130
|
+
commands.push(['L', currentX, currentY]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
83
133
|
break;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
|
|
135
|
+
case 'L': // Absolute lineto
|
|
136
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
137
|
+
const x = args[i];
|
|
138
|
+
const y = args[i + 1];
|
|
139
|
+
commands.push(['L', x, y]);
|
|
140
|
+
currentX = x;
|
|
141
|
+
currentY = y;
|
|
142
|
+
}
|
|
87
143
|
break;
|
|
88
|
-
|
|
144
|
+
|
|
145
|
+
case 'l': // Relative lineto
|
|
146
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
147
|
+
currentX += args[i];
|
|
148
|
+
currentY += args[i + 1];
|
|
149
|
+
commands.push(['L', currentX, currentY]);
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'H': // Absolute horizontal lineto
|
|
154
|
+
for (let i = 0; i < args.length; i++) {
|
|
155
|
+
currentX = args[i];
|
|
156
|
+
commands.push(['L', currentX, currentY]);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'h': // Relative horizontal lineto
|
|
161
|
+
for (let i = 0; i < args.length; i++) {
|
|
162
|
+
currentX += args[i];
|
|
163
|
+
commands.push(['L', currentX, currentY]);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'V': // Absolute vertical lineto
|
|
168
|
+
for (let i = 0; i < args.length; i++) {
|
|
169
|
+
currentY = args[i];
|
|
170
|
+
commands.push(['L', currentX, currentY]);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 'v': // Relative vertical lineto
|
|
175
|
+
for (let i = 0; i < args.length; i++) {
|
|
176
|
+
currentY += args[i];
|
|
177
|
+
commands.push(['L', currentX, currentY]);
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'C': // Absolute cubic bezier
|
|
182
|
+
for (let i = 0; i < args.length; i += 6) {
|
|
183
|
+
commands.push(['C', args[i], args[i+1], args[i+2], args[i+3], args[i+4], args[i+5]]);
|
|
184
|
+
currentX = args[i + 4];
|
|
185
|
+
currentY = args[i + 5];
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'c': // Relative cubic bezier
|
|
190
|
+
for (let i = 0; i < args.length; i += 6) {
|
|
191
|
+
commands.push(['C',
|
|
192
|
+
currentX + args[i], currentY + args[i+1],
|
|
193
|
+
currentX + args[i+2], currentY + args[i+3],
|
|
194
|
+
currentX + args[i+4], currentY + args[i+5]
|
|
195
|
+
]);
|
|
196
|
+
currentX += args[i + 4];
|
|
197
|
+
currentY += args[i + 5];
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'S': // Absolute smooth cubic bezier
|
|
202
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
203
|
+
// Reflect previous control point
|
|
204
|
+
const lastCmd = commands[commands.length - 1];
|
|
205
|
+
let cp1x = currentX, cp1y = currentY;
|
|
206
|
+
if (lastCmd && lastCmd[0] === 'C') {
|
|
207
|
+
cp1x = 2 * currentX - lastCmd[3];
|
|
208
|
+
cp1y = 2 * currentY - lastCmd[4];
|
|
209
|
+
}
|
|
210
|
+
commands.push(['C', cp1x, cp1y, args[i], args[i+1], args[i+2], args[i+3]]);
|
|
211
|
+
currentX = args[i + 2];
|
|
212
|
+
currentY = args[i + 3];
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 's': // Relative smooth cubic bezier
|
|
217
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
218
|
+
const lastCmd = commands[commands.length - 1];
|
|
219
|
+
let cp1x = currentX, cp1y = currentY;
|
|
220
|
+
if (lastCmd && lastCmd[0] === 'C') {
|
|
221
|
+
cp1x = 2 * currentX - lastCmd[3];
|
|
222
|
+
cp1y = 2 * currentY - lastCmd[4];
|
|
223
|
+
}
|
|
224
|
+
commands.push(['C', cp1x, cp1y,
|
|
225
|
+
currentX + args[i], currentY + args[i+1],
|
|
226
|
+
currentX + args[i+2], currentY + args[i+3]
|
|
227
|
+
]);
|
|
228
|
+
currentX += args[i + 2];
|
|
229
|
+
currentY += args[i + 3];
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'Q': // Absolute quadratic bezier - convert to cubic
|
|
234
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
235
|
+
const qx = args[i], qy = args[i+1];
|
|
236
|
+
const x = args[i+2], y = args[i+3];
|
|
237
|
+
// Convert quadratic to cubic control points
|
|
238
|
+
const cp1x = currentX + 2/3 * (qx - currentX);
|
|
239
|
+
const cp1y = currentY + 2/3 * (qy - currentY);
|
|
240
|
+
const cp2x = x + 2/3 * (qx - x);
|
|
241
|
+
const cp2y = y + 2/3 * (qy - y);
|
|
242
|
+
commands.push(['C', cp1x, cp1y, cp2x, cp2y, x, y]);
|
|
243
|
+
currentX = x;
|
|
244
|
+
currentY = y;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case 'q': // Relative quadratic bezier - convert to cubic
|
|
249
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
250
|
+
const qx = currentX + args[i], qy = currentY + args[i+1];
|
|
251
|
+
const x = currentX + args[i+2], y = currentY + args[i+3];
|
|
252
|
+
const cp1x = currentX + 2/3 * (qx - currentX);
|
|
253
|
+
const cp1y = currentY + 2/3 * (qy - currentY);
|
|
254
|
+
const cp2x = x + 2/3 * (qx - x);
|
|
255
|
+
const cp2y = y + 2/3 * (qy - y);
|
|
256
|
+
commands.push(['C', cp1x, cp1y, cp2x, cp2y, x, y]);
|
|
257
|
+
currentX = x;
|
|
258
|
+
currentY = y;
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'Z': // Close path
|
|
263
|
+
case 'z':
|
|
264
|
+
commands.push(['Z']);
|
|
265
|
+
currentX = subpathStartX;
|
|
266
|
+
currentY = subpathStartY;
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
// T, t (smooth quadratic) and A, a (arc) could be added if needed
|
|
89
270
|
}
|
|
90
|
-
|
|
91
|
-
// Calculate control points for an approximated curve
|
|
92
|
-
// For a line, we can use control points that are 1/3 and 2/3 along the line
|
|
93
|
-
const cp1x = prevX + (x - prevX) / 3;
|
|
94
|
-
const cp1y = prevY + (y - prevY) / 3;
|
|
95
|
-
const cp2x = prevX + (2 * (x - prevX)) / 3;
|
|
96
|
-
const cp2y = prevY + (2 * (y - prevY)) / 3;
|
|
97
|
-
|
|
98
|
-
commands.push(["C", cp1x, cp1y, cp2x, cp2y, x, y]);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Match and process Curve commands (C)
|
|
102
|
-
while ((match = curveRegex.exec(svgPath)) !== null) {
|
|
103
|
-
commands.push([
|
|
104
|
-
"C",
|
|
105
|
-
parseFloat(match[1]),
|
|
106
|
-
parseFloat(match[2]),
|
|
107
|
-
parseFloat(match[3]),
|
|
108
|
-
parseFloat(match[4]),
|
|
109
|
-
parseFloat(match[5]),
|
|
110
|
-
parseFloat(match[6]),
|
|
111
|
-
]);
|
|
112
271
|
}
|
|
113
|
-
|
|
114
|
-
// Match and process Close commands (Z)
|
|
115
|
-
if (zRegex.test(svgPath)) {
|
|
116
|
-
commands.push(["Z"]);
|
|
117
|
-
}
|
|
118
|
-
|
|
272
|
+
|
|
119
273
|
return commands;
|
|
120
274
|
}
|
|
121
275
|
|
|
@@ -133,7 +287,7 @@ export class SVGShape extends Shape {
|
|
|
133
287
|
maxY = -Infinity;
|
|
134
288
|
|
|
135
289
|
for (const cmd of path) {
|
|
136
|
-
if (cmd[0] === "M") {
|
|
290
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
137
291
|
minX = Math.min(minX, cmd[1]);
|
|
138
292
|
minY = Math.min(minY, cmd[2]);
|
|
139
293
|
maxX = Math.max(maxX, cmd[1]);
|
|
@@ -156,9 +310,9 @@ export class SVGShape extends Shape {
|
|
|
156
310
|
|
|
157
311
|
// Translate the center to the origin and scale
|
|
158
312
|
return path.map((cmd) => {
|
|
159
|
-
if (cmd[0] === "M") {
|
|
313
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
160
314
|
return [
|
|
161
|
-
|
|
315
|
+
cmd[0],
|
|
162
316
|
(cmd[1] - pathCenterX) * scale,
|
|
163
317
|
(cmd[2] - pathCenterY) * scale,
|
|
164
318
|
];
|
|
@@ -192,7 +346,7 @@ export class SVGShape extends Shape {
|
|
|
192
346
|
maxY = -Infinity;
|
|
193
347
|
|
|
194
348
|
for (const cmd of path) {
|
|
195
|
-
if (cmd[0] === "M") {
|
|
349
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
196
350
|
minX = Math.min(minX, cmd[1]);
|
|
197
351
|
minY = Math.min(minY, cmd[2]);
|
|
198
352
|
maxX = Math.max(maxX, cmd[1]);
|
|
@@ -211,8 +365,8 @@ export class SVGShape extends Shape {
|
|
|
211
365
|
|
|
212
366
|
// Scale without centering
|
|
213
367
|
return path.map((cmd) => {
|
|
214
|
-
if (cmd[0] === "M") {
|
|
215
|
-
return [
|
|
368
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
369
|
+
return [cmd[0], cmd[1] * scale, cmd[2] * scale];
|
|
216
370
|
} else if (cmd[0] === "C") {
|
|
217
371
|
return [
|
|
218
372
|
"C",
|
|
@@ -230,15 +384,20 @@ export class SVGShape extends Shape {
|
|
|
230
384
|
}
|
|
231
385
|
|
|
232
386
|
/**
|
|
233
|
-
* Calculate a point along a
|
|
387
|
+
* Calculate a point along a segment at time t
|
|
234
388
|
* @param {Array} segment - Path segment command
|
|
235
389
|
* @param {number} t - Time parameter (0-1)
|
|
236
390
|
* @returns {Object} Point coordinates {x, y}
|
|
237
391
|
*/
|
|
238
|
-
|
|
392
|
+
getPointOnSegment(segment, t) {
|
|
239
393
|
if (segment[0] === "M") {
|
|
240
394
|
// For move commands, just return the point
|
|
241
395
|
return { x: segment[1], y: segment[2] };
|
|
396
|
+
} else if (segment[0] === "L") {
|
|
397
|
+
// For line commands, linear interpolation
|
|
398
|
+
const x = this.prevX + (segment[1] - this.prevX) * t;
|
|
399
|
+
const y = this.prevY + (segment[2] - this.prevY) * t;
|
|
400
|
+
return { x, y };
|
|
242
401
|
} else if (segment[0] === "C") {
|
|
243
402
|
// For Cubic Bezier curves, calculate the point at t
|
|
244
403
|
const startX = this.prevX;
|
|
@@ -268,6 +427,13 @@ export class SVGShape extends Shape {
|
|
|
268
427
|
|
|
269
428
|
return { x: 0, y: 0 };
|
|
270
429
|
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @deprecated Use getPointOnSegment instead
|
|
433
|
+
*/
|
|
434
|
+
getBezierPoint(segment, t) {
|
|
435
|
+
return this.getPointOnSegment(segment, t);
|
|
436
|
+
}
|
|
271
437
|
|
|
272
438
|
/**
|
|
273
439
|
* Get a subset of the path up to the current animation progress
|
|
@@ -279,7 +445,7 @@ export class SVGShape extends Shape {
|
|
|
279
445
|
let segmentIndex = Math.floor(this.animationProgress * totalSegments);
|
|
280
446
|
let segmentProgress = (this.animationProgress * totalSegments) % 1;
|
|
281
447
|
|
|
282
|
-
// Initialize
|
|
448
|
+
// Initialize previous point tracking
|
|
283
449
|
let hasPrevPoint = false;
|
|
284
450
|
this.prevX = 0;
|
|
285
451
|
this.prevY = 0;
|
|
@@ -287,18 +453,14 @@ export class SVGShape extends Shape {
|
|
|
287
453
|
// Add all completed segments
|
|
288
454
|
for (let i = 0; i < segmentIndex; i++) {
|
|
289
455
|
const cmd = this.pathCommands[i];
|
|
290
|
-
|
|
291
|
-
// Add to result
|
|
292
456
|
result.push([...cmd]);
|
|
293
457
|
|
|
294
|
-
// Track
|
|
295
|
-
if (cmd[0] === "M") {
|
|
296
|
-
// For Move commands, just update position
|
|
458
|
+
// Track current position
|
|
459
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
297
460
|
this.prevX = cmd[1];
|
|
298
461
|
this.prevY = cmd[2];
|
|
299
|
-
hasPrevPoint = true;
|
|
462
|
+
hasPrevPoint = true;
|
|
300
463
|
} else if (cmd[0] === "C") {
|
|
301
|
-
// For Bezier commands, update to end point
|
|
302
464
|
this.prevX = cmd[5];
|
|
303
465
|
this.prevY = cmd[6];
|
|
304
466
|
hasPrevPoint = true;
|
|
@@ -315,31 +477,23 @@ export class SVGShape extends Shape {
|
|
|
315
477
|
this.prevX = currentSegment[1];
|
|
316
478
|
this.prevY = currentSegment[2];
|
|
317
479
|
this.currentPoint = { x: currentSegment[1], y: currentSegment[2] };
|
|
318
|
-
|
|
480
|
+
} else if (currentSegment[0] === "L") {
|
|
481
|
+
// For Line commands, interpolate position
|
|
482
|
+
if (!hasPrevPoint) {
|
|
483
|
+
this.findPreviousPoint(segmentIndex);
|
|
484
|
+
}
|
|
485
|
+
const point = this.getPointOnSegment(currentSegment, segmentProgress);
|
|
486
|
+
result.push(['L', point.x, point.y]);
|
|
487
|
+
this.currentPoint = point;
|
|
319
488
|
} else if (currentSegment[0] === "C") {
|
|
489
|
+
// For Curve commands, interpolate along bezier
|
|
320
490
|
if (!hasPrevPoint) {
|
|
321
|
-
|
|
322
|
-
// we need to find the closest previous Move command
|
|
323
|
-
for (let i = segmentIndex - 1; i >= 0; i--) {
|
|
324
|
-
if (this.pathCommands[i][0] === "M") {
|
|
325
|
-
this.prevX = this.pathCommands[i][1];
|
|
326
|
-
this.prevY = this.pathCommands[i][2];
|
|
327
|
-
hasPrevPoint = true;
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// If still no previous point, use (0,0)
|
|
333
|
-
if (!hasPrevPoint) {
|
|
334
|
-
this.prevX = 0;
|
|
335
|
-
this.prevY = 0;
|
|
336
|
-
}
|
|
491
|
+
this.findPreviousPoint(segmentIndex);
|
|
337
492
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// Add partial curve to result
|
|
493
|
+
const point = this.getPointOnSegment(currentSegment, segmentProgress);
|
|
494
|
+
|
|
495
|
+
// For partial bezier, we need to split the curve
|
|
496
|
+
// Simplified: just draw to the current point
|
|
343
497
|
result.push([
|
|
344
498
|
"C",
|
|
345
499
|
currentSegment[1],
|
|
@@ -349,13 +503,40 @@ export class SVGShape extends Shape {
|
|
|
349
503
|
point.x,
|
|
350
504
|
point.y,
|
|
351
505
|
]);
|
|
352
|
-
|
|
353
506
|
this.currentPoint = point;
|
|
507
|
+
} else if (currentSegment[0] === "Z") {
|
|
508
|
+
// Close path - add if we've progressed past it
|
|
509
|
+
if (segmentProgress > 0.5) {
|
|
510
|
+
result.push(['Z']);
|
|
511
|
+
}
|
|
512
|
+
this.currentPoint = { x: this.prevX, y: this.prevY };
|
|
354
513
|
}
|
|
355
514
|
}
|
|
356
515
|
|
|
357
516
|
return result;
|
|
358
517
|
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Find the previous point by looking back through commands
|
|
521
|
+
* @param {number} fromIndex - Index to search backwards from
|
|
522
|
+
*/
|
|
523
|
+
findPreviousPoint(fromIndex) {
|
|
524
|
+
for (let i = fromIndex - 1; i >= 0; i--) {
|
|
525
|
+
const cmd = this.pathCommands[i];
|
|
526
|
+
if (cmd[0] === "M" || cmd[0] === "L") {
|
|
527
|
+
this.prevX = cmd[1];
|
|
528
|
+
this.prevY = cmd[2];
|
|
529
|
+
return;
|
|
530
|
+
} else if (cmd[0] === "C") {
|
|
531
|
+
this.prevX = cmd[5];
|
|
532
|
+
this.prevY = cmd[6];
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Default to origin
|
|
537
|
+
this.prevX = 0;
|
|
538
|
+
this.prevY = 0;
|
|
539
|
+
}
|
|
359
540
|
|
|
360
541
|
/**
|
|
361
542
|
* Draw the SVG path
|
package/src/shapes/text.js
CHANGED
|
@@ -32,6 +32,10 @@ export class TextShape extends Shape {
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Draw the text using Painter
|
|
35
|
+
*
|
|
36
|
+
* Text is drawn at an offset to center it within its bounding box.
|
|
37
|
+
* This ensures that regardless of alignment setting, the text's
|
|
38
|
+
* bounding box is centered at the object's (x, y) position.
|
|
35
39
|
*/
|
|
36
40
|
draw() {
|
|
37
41
|
super.draw();
|
|
@@ -39,7 +43,8 @@ export class TextShape extends Shape {
|
|
|
39
43
|
Painter.text.setFont(this.font);
|
|
40
44
|
Painter.text.setTextAlign(this.align);
|
|
41
45
|
Painter.text.setTextBaseline(this.baseline);
|
|
42
|
-
|
|
46
|
+
// Apply alignment offset to center text within its bounding box
|
|
47
|
+
Painter.text.fillText(this.text, -this._centerOffsetX, -this._centerOffsetY, this.color);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
_calculateAlignmentOffsets() {
|
|
@@ -47,7 +52,13 @@ export class TextShape extends Shape {
|
|
|
47
52
|
if (!Painter.text) return;
|
|
48
53
|
// Measure text dimensions
|
|
49
54
|
const metrics = Painter.text.measureTextDimensions(this.text, this.font);
|
|
55
|
+
|
|
50
56
|
// Calculate horizontal center point offset
|
|
57
|
+
// Goal: center the text bounding box at (0, 0) in local coordinates
|
|
58
|
+
// We draw at (-_centerOffsetX), so:
|
|
59
|
+
// left: draw at -width/2 so text spans [-width/2, +width/2]
|
|
60
|
+
// center: draw at 0 (already centered)
|
|
61
|
+
// right: draw at +width/2 so text spans [-width/2, +width/2]
|
|
51
62
|
switch (this._align) {
|
|
52
63
|
case "left":
|
|
53
64
|
this._centerOffsetX = metrics.width / 2;
|
|
@@ -56,22 +67,27 @@ export class TextShape extends Shape {
|
|
|
56
67
|
this._centerOffsetX = 0;
|
|
57
68
|
break;
|
|
58
69
|
case "right":
|
|
59
|
-
this._centerOffsetX = -metrics.width / 2
|
|
70
|
+
this._centerOffsetX = -metrics.width / 2;
|
|
60
71
|
break;
|
|
61
72
|
}
|
|
73
|
+
|
|
62
74
|
// Calculate vertical center point offset
|
|
75
|
+
// Goal: center the text bounding box at (0, 0) in local coordinates
|
|
76
|
+
// We draw at (-_centerOffsetY), so:
|
|
77
|
+
// top: draw at -height/2 so text spans [-height/2, +height/2]
|
|
78
|
+
// middle: draw at 0 (already centered)
|
|
79
|
+
// bottom: draw at +height/2 so text spans [-height/2, +height/2]
|
|
63
80
|
switch (this._baseline) {
|
|
64
81
|
case "top":
|
|
65
|
-
this._centerOffsetY = metrics.height/
|
|
82
|
+
this._centerOffsetY = metrics.height / 2;
|
|
66
83
|
break;
|
|
67
84
|
case "middle":
|
|
68
|
-
this._centerOffsetY =
|
|
85
|
+
this._centerOffsetY = 0;
|
|
69
86
|
break;
|
|
70
87
|
case "bottom":
|
|
71
|
-
this._centerOffsetY = -metrics.height;
|
|
88
|
+
this._centerOffsetY = -metrics.height / 2;
|
|
72
89
|
break;
|
|
73
90
|
}
|
|
74
|
-
//console.log("calculateAlignmentOffsets", this._centerOffsetY, this._centerOffsetX);
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
getTextBounds() {
|
|
@@ -70,6 +70,11 @@ export class Transformable extends Renderable {
|
|
|
70
70
|
*/
|
|
71
71
|
draw() {
|
|
72
72
|
this.applyTransforms();
|
|
73
|
+
// Flush any stale canvas fill state before shape draws
|
|
74
|
+
// This prevents color bleeding between consecutive shape renders
|
|
75
|
+
const ctx = Painter.ctx;
|
|
76
|
+
ctx.beginPath();
|
|
77
|
+
ctx.fill();
|
|
73
78
|
this.drawDebug();
|
|
74
79
|
}
|
|
75
80
|
|