Skip to content

Commit 045c137

Browse files
committed
add random blob generator
1 parent e7ba2de commit 045c137

File tree

5 files changed

+118
-107
lines changed

5 files changed

+118
-107
lines changed

blob.svg

Lines changed: 1 addition & 1 deletion
Loading

index.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {distance, rad, loopAccess} from "./util";
2+
import {Point, renderClosed} from "./render";
3+
4+
export interface BlobOptions {
5+
size: number;
6+
color: string;
7+
complexity: number;
8+
contrast: number;
9+
}
10+
11+
// Generates a random rounded shape.
12+
export const blob = (opt: BlobOptions): string => {
13+
opt = Object.assign({}, opt);
14+
15+
if (opt.complexity <= 0 || opt.complexity > 1) {
16+
throw new Error("complexity out of range ]0,1]");
17+
}
18+
19+
if (opt.contrast < 0 || opt.contrast > 1) {
20+
throw new Error("contrast out of range [0,1]");
21+
}
22+
23+
const count = 3 + Math.floor(14 * opt.complexity);
24+
const angle = 360 / count;
25+
const radius = opt.size / 3;
26+
const handle = radius * (4 / 3) * Math.tan(rad(angle / 4));
27+
28+
const points: Point[] = [];
29+
for (let i = 0; i < count; i++) {
30+
const rand = 1 - 0.8 * opt.contrast * Math.random();
31+
32+
points.push({
33+
x: Math.sin(rad(i * angle)) * radius * rand + opt.size / 2,
34+
y: Math.cos(rad(i * angle)) * radius * rand + opt.size / 2,
35+
handles: {
36+
angle: -i * angle,
37+
in: handle,
38+
out: handle,
39+
},
40+
});
41+
}
42+
43+
// Adjust handle lengths according to proximity with adjacent points.
44+
const expected = 2 * radius * Math.sin(rad(angle / 2));
45+
for (let i = 0; i < count; i++) {
46+
const point = loopAccess(points)(i);
47+
if (!point.handles) continue; // Should not happen.
48+
49+
const {handles} = point;
50+
handles.in = (handles.in * distance(point, loopAccess(points)(i - 1))) / expected;
51+
handles.out = (handles.out * distance(point, loopAccess(points)(i + 1))) / expected;
52+
}
53+
54+
return renderClosed(points, {
55+
width: opt.size,
56+
height: opt.size,
57+
fill: opt.color,
58+
transform: `rotate(${Math.random() * angle},${opt.size / 2},${opt.size / 2})`,
59+
stroke: "red",
60+
strokeWidth: 2,
61+
guides: true,
62+
});
63+
};
64+
65+
console.log(
66+
blob({
67+
color: "pink",
68+
complexity: 0.2,
69+
contrast: 1,
70+
size: 600,
71+
}),
72+
);
73+
74+
// console.log(
75+
// renderClosed(
76+
// [
77+
// {x: 700, y: 200, handles: {angle: -135, out: 80, in: 80}},
78+
// {x: 300, y: 200, handles: {angle: 135, out: 80, in: 80}},
79+
// {x: 300, y: 600, handles: {angle: 45, out: 80, in: 80}},
80+
// {x: 700, y: 600, handles: {angle: -45, out: 80, in: 80}},
81+
// ],
82+
// {
83+
// width: 1000,
84+
// height: 800,
85+
// fill: "pink",
86+
// stroke: "red",
87+
// strokeWidth: 2,
88+
// guides: true,
89+
// },
90+
// ),
91+
// );

render.ts

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {loopAccess, rad} from "./util";
2+
13
export interface Point {
24
// Cartesian coordinates (starting at [0,0] in the bottom left).
35
x: number;
@@ -50,16 +52,11 @@ export interface RenderOptions {
5052
boundingBox?: boolean;
5153
}
5254

53-
// Safe array access at any index using a modulo operation that will always be positive.
54-
const loopAccess = <T>(arr: T[]) => (i: number): T => {
55-
return arr[((i % arr.length) + arr.length) % arr.length];
56-
};
57-
5855
// Translates a point's [x,y] cartesian coordinates into values relative to the viewport.
5956
// Translates the angle from degrees to radians and moves the start angle a half rotation.
6057
const cleanupPoint = (point: Point, opt: RenderOptions): InternalPoint => {
6158
const handles = point.handles || {angle: 0, out: 0, in: 0};
62-
handles.angle = Math.PI + (2 * Math.PI * handles.angle) / 360;
59+
handles.angle = Math.PI + rad(handles.angle);
6360
return {
6461
x: point.x,
6562
y: opt.height - point.y,
@@ -68,7 +65,7 @@ const cleanupPoint = (point: Point, opt: RenderOptions): InternalPoint => {
6865
};
6966

7067
// Renders a closed shape made up of the input points.
71-
const renderClosed = (p: Point[], opt: RenderOptions): string => {
68+
export const renderClosed = (p: Point[], opt: RenderOptions): string => {
7269
const points = p.map((point) => cleanupPoint(point, opt));
7370

7471
// Compute guides from input point data.
@@ -119,7 +116,7 @@ const renderClosed = (p: Point[], opt: RenderOptions): string => {
119116
if (opt.boundingBox) {
120117
guides += `
121118
<rect x="0" y="0" width="${opt.width}" height="${opt.height}" fill="none"
122-
stroke="${color}" stroke-width="${2*size}" stroke-dasharray="${2 * size}" />`;
119+
stroke="${color}" stroke-width="${2 * size}" stroke-dasharray="${2 * size}" />`;
123120
}
124121

125122
// Points and handles.
@@ -132,7 +129,7 @@ const renderClosed = (p: Point[], opt: RenderOptions): string => {
132129
<line x1="${x}" y1="${y}" x2="${hands.x1}" y2="${hands.y1}"
133130
stroke-width="${size}" stroke="${color}" />
134131
<line x1="${nextPoint.x}" y1="${nextPoint.y}" x2="${hands.x2}" y2="${hands.y2}"
135-
stroke-width="${size}" stroke="${color}" stroke-dasharray="${2* size}" />
132+
stroke-width="${size}" stroke="${color}" stroke-dasharray="${2 * size}" />
136133
<circle cx="${hands.x1}" cy="${hands.y1}" r="${size}"
137134
fill="${color}" />
138135
<circle cx="${hands.x2}" cy="${hands.y2}" r="${size}"
@@ -142,7 +139,7 @@ const renderClosed = (p: Point[], opt: RenderOptions): string => {
142139
}
143140

144141
const stroke = opt.stroke || (opt.guides ? "black" : "none");
145-
const strokeWidth = opt.strokeWidth || (opt.guides ? 1 : 0 );
142+
const strokeWidth = opt.strokeWidth || (opt.guides ? 1 : 0);
146143

147144
return `
148145
<svg
@@ -163,22 +160,3 @@ const renderClosed = (p: Point[], opt: RenderOptions): string => {
163160
</svg>
164161
`.replace(/\s+/g, " ");
165162
};
166-
167-
console.log(
168-
renderClosed(
169-
[
170-
{x: 700, y: 200, handles: {angle: -135, out: 80, in: 80}},
171-
{x: 300, y: 200, handles: {angle: 135, out: 80, in: 80}},
172-
{x: 300, y: 600, handles: {angle: 45, out: 80, in: 80}},
173-
{x: 700, y: 600, handles: {angle: -45, out: 80, in: 80}},
174-
],
175-
{
176-
width: 1000,
177-
height: 800,
178-
fill: "pink",
179-
stroke: "red",
180-
strokeWidth: 2,
181-
guides: true,
182-
},
183-
),
184-
);

svg.ts

Lines changed: 0 additions & 77 deletions
This file was deleted.

util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
interface Point {
2+
x: number;
3+
y: number;
4+
}
5+
6+
// Safe array access at any index using a modulo operation that will always be positive.
7+
export const loopAccess = <T>(arr: T[]) => (i: number): T => {
8+
return arr[((i % arr.length) + arr.length) % arr.length];
9+
};
10+
11+
// Converts degrees to radians.
12+
export const rad = (deg: number) => {
13+
return (deg / 360) * 2 * Math.PI;
14+
};
15+
16+
// Calculates distance between two points.
17+
export const distance = (p1: Point, p2: Point): number => {
18+
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
19+
};

0 commit comments

Comments
 (0)