Skip to content

Commit fc09237

Browse files
committed
add full examples of morphing between shapes
1 parent b938f60 commit fc09237

File tree

2 files changed

+151
-21
lines changed

2 files changed

+151
-21
lines changed

animate/index.ts

Lines changed: 103 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// http://www.iscriptdesign.com/?sketch=tutorial/splitbezier
44
// http://www.wikiwand.com/en/Hungarian_algorithm
55

6+
import blobs from "..";
7+
68
let ctx: CanvasRenderingContext2D;
79

810
const animationSpeed = 2;
@@ -168,6 +170,7 @@ const approxCurveLength = (a: Point, b: Point): number => {
168170
};
169171

170172
const calcOptimalOffset = (a: Coord[], b: Coord[]): number => {
173+
// TODO also reverse
171174
const count = a.length;
172175
let min = Infinity;
173176
let minIndex = 0;
@@ -185,6 +188,15 @@ const calcOptimalOffset = (a: Coord[], b: Coord[]): number => {
185188
return minIndex;
186189
};
187190

191+
const offsetShape = (offset: number, shape: Point[]): Point[] => {
192+
if (offset === 0) return shape;
193+
const out: Point[] = [];
194+
for (let i = 0; i < shape.length; i++) {
195+
out.push(shape[(i + offset) % shape.length]);
196+
}
197+
return out;
198+
};
199+
188200
const divideShape = (count: number, points: Point[]): Point[] => {
189201
if (points.length < 3) throw new Error("not enough points");
190202
if (count < points.length) throw new Error("cannot remove points");
@@ -314,6 +326,7 @@ const renderShape = (points: Point[]) => {
314326
};
315327

316328
const interpolateBetween = (percentage: number, a: Point[], b: Point[]): Point[] => {
329+
// TODO when handle length === 0, ignore/modify angle to look nice
317330
if (a.length !== b.length) throw new Error("shapes have different number of points");
318331
const points: Point[] = [];
319332
for (let i = 0; i < a.length; i++) {
@@ -332,6 +345,14 @@ const interpolateBetween = (percentage: number, a: Point[], b: Point[]): Point[]
332345
return points;
333346
};
334347

348+
const interpolateBetweenLoop = (percentage: number, a: Point[], b: Point[]): Point[] => {
349+
if (percentage < 0.5) {
350+
return interpolateBetween(2 * percentage, a, b);
351+
} else {
352+
return interpolateBetween(2 * percentage - 1, b, a);
353+
}
354+
};
355+
335356
const testSplitAt = (percentage: number) => {
336357
let points: Point[] = [
337358
point(0.15, 0.15, 135, 0.1, 315, 0.2),
@@ -366,8 +387,8 @@ const testSplitBy = () => {
366387
renderShape(
367388
splitCurveBy(
368389
i + 1,
369-
point(0.15, 0.2 + i * 0.06, 30, 0.1, -30, 0.1),
370-
point(0.45, 0.2 + i * 0.06, 135, 0.1, 225, 0.1),
390+
point(0.15, 0.2 + i * 0.06, 30, 0.04, -30, 0.04),
391+
point(0.25, 0.2 + i * 0.06, 135, 0.04, 225, 0.04),
371392
),
372393
);
373394
}
@@ -378,32 +399,93 @@ const testDivideShape = () => {
378399
for (let i = 0; i < count; i++) {
379400
renderShape(
380401
divideShape(i + 3, [
381-
point(0.6, 0.2 + i * 0.05, -10, 0.1, -45, 0.03),
382-
point(0.7, 0.2 + i * 0.05 - 0.03, 180, 0.03, 0, 0.03),
383-
point(0.8, 0.2 + i * 0.05, -135, 0.03, 170, 0.1),
402+
point(0.3, 0.2 + i * 0.05, -10, 0.04, -45, 0.02),
403+
point(0.35, 0.2 + i * 0.05 - 0.02, 180, 0.02, 0, 0.02),
404+
point(0.4, 0.2 + i * 0.05, -135, 0.02, 170, 0.04),
384405
]),
385406
);
386407
}
387408
};
388409

389410
const testInterpolateBetween = (percentage: number) => {
390411
const a = [
391-
point(0.65, 0.72, 135, 0.05, -45, 0.05),
392-
point(0.75, 0.72, -135, 0.05, 45, 0.05),
393-
point(0.75, 0.82, -45, 0.05, 135, 0.05),
394-
point(0.65, 0.82, 45, 0.05, 225, 0.05),
412+
point(0.3, 0.72, 135, 0.05, -45, 0.05),
413+
point(0.4, 0.72, -135, 0.05, 45, 0.05),
414+
point(0.4, 0.82, -45, 0.05, 135, 0.05),
415+
point(0.3, 0.82, 45, 0.05, 225, 0.05),
395416
];
396417
const b = [
397-
point(0.7, 0.72, 180, 0, 0, 0),
398-
point(0.75, 0.77, -90, 0, 90, 0),
399-
point(0.7, 0.82, 360 * 10, 0, 180, 0),
400-
point(0.65, 0.77, 90, 0, -90, 0),
418+
point(0.35, 0.72, 180, 0, 0, 0),
419+
point(0.4, 0.77, -90, 0, 90, 0),
420+
point(0.35, 0.82, 360 * 10, 0, 180, 0),
421+
point(0.3, 0.77, 90, 0, -90, 0),
401422
];
402-
if (percentage < 0.5) {
403-
renderShape(interpolateBetween(2 * percentage, a, b));
404-
} else {
405-
renderShape(interpolateBetween(2 * percentage - 1, b, a));
423+
renderShape(interpolateBetweenLoop(percentage, a, b));
424+
};
425+
426+
const testBlobMorph = (percentage: number) => {
427+
const a = genBlob("a", 0.6, 0.6, 0.3, {x: 0.5, y: 0.2});
428+
const b = genBlob("b", 1, 0.6, 0.3, {x: 0.5, y: 0.2});
429+
430+
const points = Math.max(a.length, b.length);
431+
const aNorm = divideShape(points, a);
432+
const bNorm = divideShape(points, b);
433+
const offset = calcOptimalOffset(aNorm, bNorm);
434+
const bOffset = offsetShape(offset, bNorm);
435+
436+
renderShape(interpolateBetweenLoop(percentage, aNorm, bOffset));
437+
};
438+
439+
const testShapeMorph = (percentage: number) => {
440+
const a = genBlob("a", 0.6, 0.6, 0.3, {x: 0.5, y: 0.5});
441+
const b: Point[] = [
442+
point(0.55, 0.5, 0, 0, 0, 0),
443+
point(0.55, 0.7, 0, 0, 0, 0),
444+
point(0.75, 0.7, 0, 0, 0, 0),
445+
point(0.75, 0.5, 0, 0, 0, 0),
446+
];
447+
448+
const points = Math.max(a.length, b.length);
449+
const aNorm = divideShape(points, a);
450+
const bNorm = divideShape(points, b);
451+
const offset = calcOptimalOffset(aNorm, bNorm);
452+
const bOffset = offsetShape(offset, bNorm);
453+
drawInfo("offset", offset)
454+
455+
// renderShape(a);
456+
// renderShape(b);
457+
renderShape(interpolateBetweenLoop(percentage, aNorm, bOffset));
458+
};
459+
460+
const genBlob = (
461+
seed: string,
462+
complexity: number,
463+
contrast: number,
464+
s: number,
465+
offset: Coord,
466+
): Point[] => {
467+
const original = blobs.path({
468+
complexity,
469+
contrast,
470+
size: s * size,
471+
seed,
472+
});
473+
const out: Point[] = [];
474+
for (let i = 0; i < original.length; i++) {
475+
const p = original[i];
476+
if (!p.handles) continue;
477+
out.push(
478+
point(
479+
p.x / size + offset.x,
480+
p.y / size + offset.y,
481+
p.handles.angle + 180,
482+
p.handles.in / size,
483+
p.handles.angle,
484+
p.handles.out / size,
485+
),
486+
);
406487
}
488+
return out;
407489
};
408490

409491
(() => {
@@ -419,11 +501,15 @@ const testInterpolateBetween = (percentage: number) => {
419501
let percentage = animationStart;
420502
const renderFrame = () => {
421503
ctx.clearRect(0, 0, canvas.width, canvas.height);
504+
422505
drawInfo("percentage", percentage);
423506
testSplitAt(percentage);
424507
testSplitBy();
425508
testDivideShape();
426509
testInterpolateBetween(percentage);
510+
testBlobMorph(percentage);
511+
testShapeMorph(percentage);
512+
427513
percentage += animationSpeed / 1000;
428514
percentage %= 1;
429515
if (animationSpeed > 0) requestAnimationFrame(renderFrame);

index.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {smooth} from "./internal/svg/smooth";
77
import {renderEditable} from "./internal/svg/render";
88
import {XmlElement} from "./editable";
99

10-
export interface BlobOptions {
10+
export interface PathOptions {
1111
// Bounding box dimensions.
1212
size: number;
1313

@@ -17,6 +17,11 @@ export interface BlobOptions {
1717
// Shape contrast.
1818
contrast: number;
1919

20+
// Value to seed random number generator.
21+
seed?: string;
22+
}
23+
24+
export interface BlobOptions extends PathOptions {
2025
// Fill color.
2126
color?: string;
2227

@@ -28,9 +33,6 @@ export interface BlobOptions {
2833
width: number;
2934
};
3035

31-
// Value to seed random number generator.
32-
seed?: string;
33-
3436
// Render points, handles and stroke.
3537
guides?: boolean;
3638
}
@@ -97,4 +99,46 @@ blobs.editable = (opt: BlobOptions): XmlElement => {
9799
});
98100
};
99101

102+
blobs.path = (opt: PathOptions) => {
103+
if (!opt) {
104+
throw new Error("no options specified");
105+
}
106+
107+
// Random number generator.
108+
const rgen = rand(opt.seed || String(Date.now()));
109+
110+
if (!opt.size) {
111+
throw new Error("no size specified");
112+
}
113+
114+
if (opt.complexity <= 0 || opt.complexity > 1) {
115+
throw new Error("complexity out of range ]0,1]");
116+
}
117+
118+
if (opt.contrast < 0 || opt.contrast > 1) {
119+
throw new Error("contrast out of range [0,1]");
120+
}
121+
122+
const count = 3 + Math.floor(14 * opt.complexity);
123+
const angle = 360 / count;
124+
const radius = opt.size / Math.E;
125+
126+
const points: Point[] = [];
127+
for (let i = 0; i < count; i++) {
128+
const rand = 1 - 0.8 * opt.contrast * rgen();
129+
130+
points.push({
131+
x: Math.sin(rad(i * angle)) * radius * rand + opt.size / 2,
132+
y: Math.cos(rad(i * angle)) * radius * rand + opt.size / 2,
133+
});
134+
}
135+
136+
const smoothed = smooth(points, {
137+
closed: true,
138+
strength: ((4 / 3) * Math.tan(rad(angle / 4))) / Math.sin(rad(angle / 2)),
139+
});
140+
141+
return smoothed;
142+
};
143+
100144
export default blobs;

0 commit comments

Comments
 (0)