Skip to content

Commit 52451b4

Browse files
committed
move bezier logic into own directory
1 parent a36f02d commit 52451b4

File tree

9 files changed

+249
-242
lines changed

9 files changed

+249
-242
lines changed

animate/animate/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {Shape} from "../bezier/types";
2+
3+
export interface EasingFunc {
4+
(progress: number): number;
5+
}
6+
7+
export interface Keyframe {
8+
shape: Shape;
9+
easeIn: EasingFunc;
10+
easeOut: EasingFunc;
11+
}

animate/interpolate.ts renamed to animate/bezier/interpolate.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {Point} from "./types";
2-
import {split, splitLine} from "./util";
1+
import {Shape} from "./types";
2+
import {split, splitLine} from "../util";
33

4-
const splitAngle = (percentage: number, a: number, b: number): number => {
4+
const interpolateAngle = (percentage: number, a: number, b: number): number => {
55
const tau = Math.PI * 2;
66
let aNorm = ((a % tau) + tau) % tau;
77
let bNorm = ((b % tau) + tau) % tau;
@@ -15,26 +15,26 @@ const splitAngle = (percentage: number, a: number, b: number): number => {
1515
return split(percentage, aNorm, bNorm);
1616
};
1717

18-
const interpolateBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {
18+
const interpolateBetween = (percentage: number, a: Shape, b: Shape): Shape => {
1919
if (a.length !== b.length) throw new Error("shapes have different number of points");
20-
const points: Point[] = [];
20+
const shape: Shape = [];
2121
for (let i = 0; i < a.length; i++) {
22-
points.push({
22+
shape.push({
2323
...splitLine(percentage, a[i], b[i]),
2424
handleIn: {
25-
angle: splitAngle(percentage, a[i].handleIn.angle, b[i].handleIn.angle),
25+
angle: interpolateAngle(percentage, a[i].handleIn.angle, b[i].handleIn.angle),
2626
length: split(percentage, a[i].handleIn.length, b[i].handleIn.length),
2727
},
2828
handleOut: {
29-
angle: splitAngle(percentage, a[i].handleOut.angle, b[i].handleOut.angle),
29+
angle: interpolateAngle(percentage, a[i].handleOut.angle, b[i].handleOut.angle),
3030
length: split(percentage, a[i].handleOut.length, b[i].handleOut.length),
3131
},
3232
});
3333
}
34-
return points;
34+
return shape;
3535
};
3636

37-
export const interpolateBetweenLoop = (percentage: number, a: Point[], b: Point[]): Point[] => {
37+
export const interpolateBetweenLoop = (percentage: number, a: Shape, b: Shape): Shape => {
3838
if (percentage < 0.5) {
3939
return interpolateBetween(2 * percentage, a, b);
4040
} else {

animate/bezier/prepare.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {copyPoint, length, reverse, shift, split} from "./util";
2+
import {Point, Shape} from "./types";
3+
import {distance} from "../util";
4+
5+
const optimizeOrder = (a: Shape, b: Shape): Shape => {
6+
const count = a.length;
7+
8+
let minSum = Infinity;
9+
let minOffset = 0;
10+
let minShape: Shape = [];
11+
12+
const setMinOffset = (shape: Shape) => {
13+
for (let i = 0; i < count; i++) {
14+
let sum = 0;
15+
for (let j = 0; j < count; j++) {
16+
sum += distance(a[j], shape[(j + i) % count]);
17+
if (sum > minSum) break;
18+
}
19+
if (sum <= minSum) {
20+
minSum = sum;
21+
minOffset = i;
22+
minShape = shape;
23+
}
24+
}
25+
};
26+
setMinOffset(b);
27+
setMinOffset(reverse(b));
28+
29+
return shift(minOffset, minShape);
30+
};
31+
32+
export const divideShape = (count: number, points: Shape): Shape => {
33+
if (points.length < 3) throw new Error("not enough points");
34+
if (count < points.length) throw new Error("cannot remove points");
35+
if (count === points.length) return points.slice();
36+
37+
const lengths = [];
38+
for (let i = 0; i < points.length; i++) {
39+
lengths.push(length(points[i], points[(i + 1) % points.length]));
40+
}
41+
42+
const divisors = divideLengths(lengths, count - points.length);
43+
const out: Shape = [];
44+
for (let i = 0; i < points.length; i++) {
45+
const curr: Point = out[out.length - 1] || points[i];
46+
const next = points[(i + 1) % points.length];
47+
out.pop();
48+
out.push(...splitCurveBy(divisors[i], curr, next));
49+
}
50+
const last = out.pop();
51+
out[0].handleIn = last!.handleIn;
52+
53+
return out;
54+
};
55+
56+
const fixAngles = (a: Shape, b: Shape): Shape => {
57+
const out: Shape = [];
58+
for (let i = 0; i < a.length; i++) {
59+
const point = copyPoint(b[i]);
60+
if (point.handleIn.length === 0) {
61+
point.handleIn.angle = a[i].handleIn.angle;
62+
}
63+
if (point.handleOut.length === 0) {
64+
point.handleOut.angle = a[i].handleOut.angle;
65+
}
66+
out.push(point);
67+
}
68+
return out;
69+
};
70+
71+
const divideLengths = (lengths: number[], add: number): number[] => {
72+
const divisors = lengths.map(() => 1);
73+
const sizes = lengths.slice();
74+
for (let i = 0; i < add; i++) {
75+
let maxSizeIndex = 0;
76+
for (let j = 1; j < sizes.length; j++) {
77+
if (sizes[j] > sizes[maxSizeIndex]) {
78+
maxSizeIndex = j;
79+
continue;
80+
}
81+
if (sizes[j] === sizes[maxSizeIndex]) {
82+
if (lengths[j] > lengths[maxSizeIndex]) {
83+
maxSizeIndex = j;
84+
}
85+
}
86+
}
87+
divisors[maxSizeIndex]++;
88+
sizes[maxSizeIndex] = lengths[maxSizeIndex] / divisors[maxSizeIndex];
89+
}
90+
return divisors;
91+
};
92+
93+
export const splitCurveBy = (count: number, a: Point, b: Point): Shape => {
94+
if (count < 2) return [a, b];
95+
const percentage = 1 / count;
96+
const [c, d, e] = split(percentage, a, b);
97+
if (count === 2) return [c, d, e];
98+
return [c, ...splitCurveBy(count - 1, d, e)];
99+
};
100+
101+
export const prepShapes = (a: Shape, b: Shape): [Shape, Shape] => {
102+
const points = Math.max(a.length, b.length);
103+
const aNorm = divideShape(points, a);
104+
const bNorm = divideShape(points, b);
105+
const bOpt = optimizeOrder(aNorm, bNorm);
106+
const bFix = fixAngles(aNorm, bOpt);
107+
return [aNorm, bFix];
108+
};

animate/types.ts renamed to animate/bezier/types.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,4 @@ export interface Point extends Coord {
1818
handleOut: Handle;
1919
}
2020

21-
export interface EasingFunc {
22-
(progress: number): number;
23-
}
24-
25-
export interface Keyframe {
26-
points: Point[];
27-
easeIn: EasingFunc;
28-
easeOut: EasingFunc;
29-
}
21+
export type Shape = Point[];

animate/bezier/util.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {Coord, Handle, Point, Shape} from "./types";
2+
import {distance, splitLine} from "../util";
3+
4+
export const copyPoint = (p: Point): Point => ({
5+
x: p.x,
6+
y: p.y,
7+
handleIn: {...p.handleIn},
8+
handleOut: {...p.handleOut},
9+
});
10+
11+
export const expandHandle = (point: Coord, handle: Handle): Coord => {
12+
return {
13+
x: point.x + handle.length * Math.cos(handle.angle),
14+
y: point.y + handle.length * Math.sin(handle.angle),
15+
};
16+
};
17+
18+
const collapseHandle = (point: Coord, handle: Coord): Handle => {
19+
const dx = handle.x - point.x;
20+
const dy = -handle.y + point.y;
21+
let angle = Math.atan2(dy, dx);
22+
return {
23+
angle: angle < 0 ? Math.abs(angle) : 2 * Math.PI - angle,
24+
length: Math.sqrt(dx ** 2 + dy ** 2),
25+
};
26+
};
27+
28+
export const length = (a: Point, b: Point): number => {
29+
const aHandle = expandHandle(a, a.handleOut);
30+
const bHandle = expandHandle(b, b.handleIn);
31+
const ab = distance(a, b);
32+
const abHandle = distance(aHandle, bHandle);
33+
return (ab + abHandle + a.handleOut.length + b.handleIn.length) / 2;
34+
};
35+
36+
export const reverse = (shape: Shape): Shape => {
37+
const inverted: Shape = [];
38+
for (let i = 0; i < shape.length; i++) {
39+
const j = shape.length - i - 1;
40+
const p = copyPoint(shape[j]);
41+
p.handleIn.angle += Math.PI;
42+
p.handleOut.angle += Math.PI;
43+
inverted.push(p);
44+
}
45+
return inverted;
46+
};
47+
48+
export const shift = (offset: number, shape: Shape): Shape => {
49+
if (offset === 0) return shape;
50+
const out: Shape = [];
51+
for (let i = 0; i < shape.length; i++) {
52+
out.push(shape[(i + offset) % shape.length]);
53+
}
54+
return out;
55+
};
56+
57+
// Add a control point to the curve between a and b.
58+
// Percentage [0, 1] from a to b.
59+
// a: original first point.
60+
// b: original last point.
61+
// c: new first point.
62+
// d: new added point.
63+
// e: new last point.
64+
// f: split point between a and b's handles.
65+
// g: split point between c's handle and f.
66+
// h: split point between e's handle and f.
67+
export const split = (percentage: number, a: Point, b: Point): [Point, Point, Point] => {
68+
const c = copyPoint(a);
69+
c.handleOut.length *= percentage;
70+
71+
const e = copyPoint(b);
72+
e.handleIn.length *= 1 - percentage;
73+
74+
const aHandle = expandHandle(a, a.handleOut);
75+
const bHandle = expandHandle(b, b.handleIn);
76+
const cHandle = expandHandle(c, c.handleOut);
77+
const eHandle = expandHandle(e, e.handleIn);
78+
const f = splitLine(percentage, aHandle, bHandle);
79+
const g = splitLine(percentage, cHandle, f);
80+
const h = splitLine(1 - percentage, eHandle, f);
81+
const dCoord = splitLine(percentage, g, h);
82+
83+
const d: Point = {
84+
x: dCoord.x,
85+
y: dCoord.y,
86+
handleIn: collapseHandle(dCoord, g),
87+
handleOut: collapseHandle(dCoord, h),
88+
};
89+
return [c, d, e];
90+
};

animate/canvas/draw.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1-
import {Coord, Point} from "../types";
2-
import {expandHandle} from "../util";
1+
import {Coord, Point, Shape} from "../bezier/types";
2+
import {expandHandle} from "../bezier/util";
33

44
const pointSize = 2;
55
const infoSpacing = 20;
66

7-
export function clear(ctx: CanvasRenderingContext2D) {
7+
export const clear = (ctx: CanvasRenderingContext2D) => {
88
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
9-
}
9+
};
1010

11-
export function drawInfo(ctx: CanvasRenderingContext2D, pos: number, label: string, value: any) {
11+
export const drawInfo = (ctx: CanvasRenderingContext2D, pos: number, label: string, value: any) => {
1212
ctx.fillText(`${label}: ${value}`, infoSpacing, (pos + 1) * infoSpacing);
13-
}
13+
};
1414

15-
function drawLine(ctx: CanvasRenderingContext2D, a: Coord, b: Coord, style: string) {
15+
const drawLine = (ctx: CanvasRenderingContext2D, a: Coord, b: Coord, style: string) => {
1616
const backupStrokeStyle = ctx.strokeStyle;
1717
ctx.beginPath();
1818
ctx.moveTo(a.x, a.y);
1919
ctx.lineTo(b.x, b.y);
2020
ctx.strokeStyle = style;
2121
ctx.stroke();
2222
ctx.strokeStyle = backupStrokeStyle;
23-
}
23+
};
2424

25-
function drawPoint(ctx: CanvasRenderingContext2D, p: Coord, style: string) {
25+
const drawPoint = (ctx: CanvasRenderingContext2D, p: Coord, style: string) => {
2626
const backupFillStyle = ctx.fillStyle;
2727
ctx.beginPath();
2828
ctx.arc(p.x, p.y, pointSize, 0, 2 * Math.PI);
2929
ctx.fillStyle = style;
3030
ctx.fill();
3131
ctx.fillStyle = backupFillStyle;
32-
}
32+
};
3333

34-
export function drawShape(ctx: CanvasRenderingContext2D, debug: boolean, points: Point[]) {
35-
if (points.length < 2) throw new Error("not enough points");
34+
export const drawShape = (ctx: CanvasRenderingContext2D, debug: boolean, shape: Shape) => {
35+
if (shape.length < 2) throw new Error("not enough points");
3636

37-
for (let i = 0; i < points.length; i++) {
37+
for (let i = 0; i < shape.length; i++) {
3838
// Compute coordinates of handles.
39-
const curr = points[i];
40-
const next = points[(i + 1) % points.length];
39+
const curr = shape[i];
40+
const next = shape[(i + 1) % shape.length];
4141
const currHandle = expandHandle(curr, curr.handleOut);
4242
const nextHandle = expandHandle(next, next.handleIn);
4343

@@ -53,4 +53,4 @@ export function drawShape(ctx: CanvasRenderingContext2D, debug: boolean, points:
5353
ctx.bezierCurveTo(currHandle.x, currHandle.y, nextHandle.x, nextHandle.y, next.x, next.y);
5454
ctx.stroke();
5555
}
56-
}
56+
};

0 commit comments

Comments
 (0)