Skip to content

Commit 3a9f170

Browse files
committed
add util to add points to shape while keeping the most even possible segment lengths
1 parent 81e8fa2 commit 3a9f170

File tree

1 file changed

+113
-99
lines changed

1 file changed

+113
-99
lines changed

animate/index.ts

Lines changed: 113 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ export const distance = (a: Coordinates, b: Coordinates): number => {
6969
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
7070
};
7171

72+
const point = (x: number, y: number, ia: number, il: number, oa: number, ol: number): Point => {
73+
return {
74+
x: x * size,
75+
y: y * size,
76+
handleIn: {angle: rad(ia), length: il * size},
77+
handleOut: {angle: rad(oa), length: ol * size},
78+
};
79+
};
80+
81+
const copyPoint = (p: Point): Point => ({
82+
x: p.x,
83+
y: p.y,
84+
handleIn: {...p.handleIn},
85+
handleOut: {...p.handleOut},
86+
});
87+
7288
const expandHandle = (origin: Coordinates, handle: Handle): Coordinates => {
7389
return {
7490
x: origin.x + handle.length * Math.cos(handle.angle),
@@ -132,6 +148,60 @@ const approxCurveLength = (a: Point, b: Point): number => {
132148
return (ab + abHandle + a.handleOut.length + b.handleIn.length) / 2;
133149
};
134150

151+
const divideShape = (count: number, points: Point[]): Point[] => {
152+
if (points.length < 3) throw new Error("not enough points");
153+
if (count < points.length) throw new Error("cannot remove points");
154+
if (count === points.length) return points.slice();
155+
156+
const lengths = [];
157+
for (let i = 0; i < points.length; i++) {
158+
lengths.push(approxCurveLength(points[i], points[(i + 1) % points.length]));
159+
}
160+
161+
const divisors = divideLengths(lengths, count - points.length);
162+
const out: Point[] = [];
163+
for (let i = 0; i < points.length; i++) {
164+
const curr: Point = out[out.length - 1] || points[i];
165+
const next = points[(i + 1) % points.length];
166+
out.pop();
167+
out.push(...splitCurveBy(divisors[i], curr, next));
168+
}
169+
const last = out.pop();
170+
out[0].handleIn = last!.handleIn;
171+
172+
return out;
173+
};
174+
175+
const divideLengths = (lengths: number[], add: number): number[] => {
176+
const divisors = lengths.map(() => 1);
177+
const sizes = lengths.slice();
178+
for (let i = 0; i < add; i++) {
179+
let maxSizeIndex = 0;
180+
for (let j = 1; j < sizes.length; j++) {
181+
if (sizes[j] > sizes[maxSizeIndex]) {
182+
maxSizeIndex = j;
183+
continue;
184+
}
185+
if (sizes[j] === sizes[maxSizeIndex]) {
186+
if (lengths[j] > lengths[maxSizeIndex]) {
187+
maxSizeIndex = j;
188+
}
189+
}
190+
}
191+
divisors[maxSizeIndex]++;
192+
sizes[maxSizeIndex] = lengths[maxSizeIndex] / divisors[maxSizeIndex];
193+
}
194+
return divisors;
195+
};
196+
197+
const splitCurveBy = (count: number, a: Point, b: Point): Point[] => {
198+
if (count < 2) return [a, b];
199+
const percentage = 1 / count;
200+
const [c, d, e] = splitCurveAt(percentage, a, b);
201+
if (count === 2) return [c, d, e];
202+
return [c, ...splitCurveBy(count - 1, d, e)];
203+
};
204+
135205
// Add a control point to the curve between a and b.
136206
// Percentage [0, 1] from a to b.
137207
// a: original first point.
@@ -142,31 +212,12 @@ const approxCurveLength = (a: Point, b: Point): number => {
142212
// f: split point between a and b's handles.
143213
// g: split point between c's handle and f.
144214
// h: split point between e's handle and f.
145-
const splitCurve = (percentage: number, a: Point, b: Point): [Point, Point, Point] => {
146-
const c: Point = {
147-
x: a.x,
148-
y: a.y,
149-
handleIn: {
150-
angle: a.handleIn.angle,
151-
length: a.handleIn.length,
152-
},
153-
handleOut: {
154-
angle: a.handleOut.angle,
155-
length: a.handleOut.length * percentage,
156-
},
157-
};
158-
const e: Point = {
159-
x: b.x,
160-
y: b.y,
161-
handleIn: {
162-
angle: b.handleIn.angle,
163-
length: b.handleIn.length * (1 - percentage),
164-
},
165-
handleOut: {
166-
angle: b.handleOut.angle,
167-
length: b.handleOut.length,
168-
},
169-
};
215+
const splitCurveAt = (percentage: number, a: Point, b: Point): [Point, Point, Point] => {
216+
const c = copyPoint(a);
217+
c.handleOut.length *= percentage;
218+
219+
const e = copyPoint(b);
220+
e.handleIn.length *= 1 - percentage;
170221

171222
const aHandle = expandHandle(a, a.handleOut);
172223
const bHandle = expandHandle(b, b.handleIn);
@@ -200,8 +251,8 @@ const splitCurve = (percentage: number, a: Point, b: Point): [Point, Point, Poin
200251
return [c, d, e];
201252
};
202253

203-
const render = (points: Point[]) => {
204-
if (points.length < 3) throw new Error("not enough points");
254+
const renderShape = (points: Point[]) => {
255+
if (points.length < 2) throw new Error("not enough points");
205256

206257
// Draw points.
207258
for (let i = 0; i < points.length; i++) {
@@ -225,64 +276,20 @@ const render = (points: Point[]) => {
225276
}
226277
};
227278

228-
const renderTestShape = (percentage: number) => {
279+
const testSplitAt = (percentage: number) => {
229280
let points: Point[] = [
230-
{
231-
x: 0.2 * size,
232-
y: 0.2 * size,
233-
handleIn: {
234-
angle: rad(135),
235-
length: 0.1 * size,
236-
},
237-
handleOut: {
238-
angle: rad(315),
239-
length: 0.2 * size,
240-
},
241-
},
242-
{
243-
x: 0.8 * size,
244-
y: 0.2 * size,
245-
handleIn: {
246-
angle: rad(225),
247-
length: 0.1 * size,
248-
},
249-
handleOut: {
250-
angle: rad(45),
251-
length: 0.2 * size,
252-
},
253-
},
254-
{
255-
x: 0.8 * size,
256-
y: 0.8 * size,
257-
handleIn: {
258-
angle: rad(315),
259-
length: 0.1 * size,
260-
},
261-
handleOut: {
262-
angle: rad(135),
263-
length: 0.2 * size,
264-
},
265-
},
266-
{
267-
x: 0.2 * size,
268-
y: 0.8 * size,
269-
handleIn: {
270-
angle: rad(45),
271-
length: 0.1 * size,
272-
},
273-
handleOut: {
274-
angle: rad(225),
275-
length: 0.2 * size,
276-
},
277-
},
281+
point(0.15, 0.15, 135, 0.1, 315, 0.2),
282+
point(0.85, 0.15, 225, 0.1, 45, 0.2),
283+
point(0.85, 0.85, 315, 0.1, 135, 0.2),
284+
point(0.15, 0.85, 45, 0.1, 225, 0.2),
278285
];
279286

280287
const count = points.length;
281288
const stop = 2 * count - 1;
282289
for (let i = 0; i < count; i++) {
283290
const double = i * 2;
284291
const next = (double + 1) % stop;
285-
points.splice(double, 2, ...splitCurve(percentage, points[double], points[next]));
292+
points.splice(double, 2, ...splitCurveAt(percentage, points[double], points[next]));
286293
}
287294
points.splice(0, 1);
288295

@@ -292,29 +299,35 @@ const renderTestShape = (percentage: number) => {
292299
const next = points[(i + 1) % points.length];
293300
length += approxCurveLength(curr, next);
294301
}
295-
drawInfo("shape path lengths sum", length);
302+
drawInfo("split at lengths sum", length);
303+
304+
renderShape(points);
305+
};
296306

297-
render(points);
307+
const testSplitBy = () => {
308+
const count = 10;
309+
for (let i = 0; i < count; i++) {
310+
renderShape(
311+
splitCurveBy(
312+
i + 1,
313+
point(0.15, 0.2 + i * 0.06, 30, 0.1, -30, 0.1),
314+
point(0.45, 0.2 + i * 0.06, 135, 0.1, 225, 0.1),
315+
),
316+
);
317+
}
298318
};
299319

300-
const renderTestCurve = (percentage: number) => {
301-
render(
302-
splitCurve(
303-
percentage,
304-
{
305-
x: 0.3 * size,
306-
y: 0.3 * size,
307-
handleIn: {angle: 0, length: 0},
308-
handleOut: {angle: 0, length: 0.4 * size},
309-
},
310-
{
311-
x: 0.7 * size,
312-
y: 0.7 * size,
313-
handleIn: {angle: Math.PI, length: 0.4 * size},
314-
handleOut: {angle: 0, length: 0},
315-
},
316-
),
317-
);
320+
const testDivideShape = () => {
321+
const count = 10;
322+
for (let i = 0; i < count; i++) {
323+
renderShape(
324+
divideShape(i + 3, [
325+
point(0.6, 0.2 + i * 0.05, -10, 0.1, -45, 0.03),
326+
point(0.7, 0.2 + i * 0.05 - 0.03, 180, 0.03, 0, 0.03),
327+
point(0.8, 0.2 + i * 0.05, -135, 0.03, 170, 0.1),
328+
]),
329+
);
330+
}
318331
};
319332

320333
(() => {
@@ -331,8 +344,9 @@ const renderTestCurve = (percentage: number) => {
331344
const renderFrame = () => {
332345
ctx.clearRect(0, 0, canvas.width, canvas.height);
333346
drawInfo("percentage", percentage);
334-
renderTestCurve(percentage);
335-
renderTestShape(percentage);
347+
testSplitAt(percentage);
348+
testSplitBy();
349+
testDivideShape();
336350
percentage += animationSpeed / 1000;
337351
percentage %= 1;
338352
if (animationSpeed > 0) requestAnimationFrame(renderFrame);

0 commit comments

Comments
 (0)