Skip to content

Commit 086a754

Browse files
committed
add points type and renderer for points
1 parent 78aa4cc commit 086a754

File tree

3 files changed

+172
-26
lines changed

3 files changed

+172
-26
lines changed

blob.svg

Lines changed: 47 additions & 18 deletions
Loading

render.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
3+
TODO
4+
- points relative to (bottom-right/center)? by default
5+
- angles in degrees
6+
- angle relative to horizontal (3 o'clock + positive is counterclockwise)
7+
- draw path
8+
- convert size to x/y
9+
10+
*/
11+
12+
interface Point {
13+
x: number;
14+
y: number;
15+
handles?: {
16+
angle: number;
17+
in: number;
18+
out: number;
19+
};
20+
}
21+
22+
interface RenderOptions {
23+
size: number;
24+
center?: boolean;
25+
rotation?: number;
26+
fill?: string;
27+
stroke?: string;
28+
strokeWidth?: number;
29+
handles?: boolean;
30+
}
31+
32+
const loop = <T>(arr: T[]) => (i: number): T => {
33+
return arr[((i%arr.length)+arr.length)%arr.length];
34+
}
35+
36+
// Renders a closed shape made up of the input points.
37+
const render = (points: Point[], opt: RenderOptions): string => {
38+
const count = points.length;
39+
const handles: {x1: number, y1: number, x2: number, y2: number}[] = [];
40+
41+
for (let i = 0; i < count; i++) {
42+
const {x, y, handles: hands} = points[i];
43+
44+
const next = loop(points)(i+1);
45+
const nextHandles = next.handles || {angle: 0, in: 0, out: 0};
46+
47+
if (hands === undefined) {
48+
handles.push({x1: x, y1: y, x2: next.x, y2: next.y});
49+
continue;
50+
}
51+
52+
handles.push({
53+
x1: x - Math.cos(hands.angle) * hands.out,
54+
y1: y + Math.sin(hands.angle) * hands.out,
55+
x2: next.x + Math.cos(nextHandles.angle) * nextHandles.in,
56+
y2: next.y - Math.sin(nextHandles.angle) * nextHandles.in,
57+
});
58+
}
59+
60+
let path = "";
61+
for (let i = 0; i <= count; i++) {
62+
const point = loop(points)(i);
63+
const hands = loop(handles)(i-1);
64+
65+
// Start at the first point's coordinates.
66+
if (i === 0) {
67+
path += `M${point.x},${point.y}`;
68+
continue;
69+
}
70+
71+
// Add cubic bezier coordinates using the computed handle positions.
72+
path += `C${hands.x1},${hands.y1},${hands.x2},${hands.y2},${point.x},${point.y}`;
73+
}
74+
75+
return `
76+
<svg width="${opt.size}" height="${opt.size}" viewBox="0 0 ${opt.size} ${opt.size}" xmlns="http://www.w3.org/2000/svg">
77+
<g transform="
78+
${opt.center ? `translate(${opt.size / 2}, ${opt.size / 2})` : ""}
79+
rotate(${opt.rotation || 0})
80+
">
81+
<path
82+
stroke="${opt.stroke || "none"}"
83+
stroke-width="${opt.strokeWidth || 0}"
84+
fill="${opt.fill || "none"}"
85+
d="${path}"
86+
/>
87+
${!opt.handles ? "" : points.map(({x, y}, i) => {
88+
const color = i === 0 ? "red" : "grey";
89+
const handle = handles[i];
90+
const nextPoint = loop(points)(i+1);
91+
return `
92+
<g id="point-handle-${i}">
93+
<line x1="${x}" y1="${y}" x2="${handle.x1}" y2="${handle.y1}" stroke-width="1" stroke="${color}" />
94+
<line x1="${nextPoint.x}" y1="${nextPoint.y}" x2="${handle.x2}" y2="${handle.y2}" stroke-width="1" stroke="${color}" stroke-dasharray="2" />
95+
<circle cx="${handle.x1}" cy="${handle.y1}" r="1" fill="${color}" />
96+
<circle cx="${handle.x2}" cy="${handle.y2}" r="1" fill="${color}" />
97+
<circle cx="${x}" cy="${y}" r="2" fill="${color}" />
98+
</g>
99+
`;
100+
}).join("")}
101+
</g>
102+
</svg>
103+
`;
104+
};
105+
106+
console.log(render([
107+
{x: 200, y: 200, handles: {angle: -Math.PI* 7/4, in: 60, out: 80}},
108+
{x: -200, y: 200, handles: {angle: Math.PI* 7/4, in: 60, out: 80}},
109+
{x: -200, y: -200, handles: {angle: Math.PI* 5/4, in: 60, out: 80}},
110+
{x: 200, y: -200, handles: {angle: -Math.PI* 5/4, in: 60, out: 80}},
111+
], {
112+
size: 1000,
113+
center: true,
114+
handles: true,
115+
stroke: "green",
116+
strokeWidth: 1,
117+
}));

svg.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const rand = (a, b) => Math.min(a, b) + (Math.abs(a - b) * Math.random());
22

33
const size = 600;
44
const count = 5;
5-
const randomness = 0.6;
5+
const randomness = 0.2;
66
const color = "grey";
77

88
const angle = 2 * Math.PI/count;
@@ -11,7 +11,7 @@ const ctrlDistance = distance * 4/3 * Math.tan(angle/4);
1111

1212
const points: {x: number, y: number}[] = [];
1313
for (let i = 0; i < count; i++) {
14-
const randomizedDistance = rand(distance * randomness, distance);
14+
const randomizedDistance = rand(distance * (1-randomness), distance);
1515
points.push({
1616
x: Math.sin(i*angle) * randomizedDistance,
1717
y: Math.cos(i*angle) * randomizedDistance,
@@ -54,6 +54,12 @@ console.log(`
5454
translate(${size / 2}, ${size / 2})
5555
rotate(${rand(0, 360 / count)})
5656
">
57+
<path
58+
stroke="cyan"
59+
stroke-width="1"
60+
fill="${"none" || color}"
61+
d="${paths.join("")}"
62+
/>
5763
${points.map(({x, y}, i) => {
5864
return `<line x1="${x}" y1="${y}" x2="${controls[i].x1}" y2="${controls[i].y1}" stroke-width="1" stroke="green" />`;
5965
}).join("")}
@@ -66,12 +72,6 @@ console.log(`
6672
${controls.map(({x2: x, y2: y}, i) => {
6773
return `<circle cx="${x}" cy="${y}" r="2" fill="${i === 0 ? "black" : "blue"}" />`;
6874
}).join("")}
69-
<path
70-
stroke="cyan"
71-
stroke-width="1"
72-
fill="${"none" || color}"
73-
d="${paths.join("")}"
74-
/>
7575
</g>
7676
</svg>
7777
`);

0 commit comments

Comments
 (0)