Skip to content

Commit 4438496

Browse files
committed
fix path drawing + normalize coordinates and angles to be more familiar
1 parent 086a754 commit 4438496

File tree

2 files changed

+111
-106
lines changed

2 files changed

+111
-106
lines changed

blob.svg

Lines changed: 1 addition & 48 deletions
Loading

render.ts

Lines changed: 110 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,82 @@
1-
/*
1+
export interface Point {
2+
// Cartesian coordinates (starting at [0,0] in the bottom left).
3+
x: number;
4+
y: number;
25

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
6+
// Optional cubic bezier handle configuration.
7+
handles?: {
8+
// Direction of the outgoing path in degrees. Value is relative to the 3:00 position
9+
// on a clock and the positive direction is counter-clockwise.
10+
angle: number;
911

10-
*/
12+
// Distance between each handle and the point.
13+
out: number;
14+
in: number;
15+
};
16+
}
1117

12-
interface Point {
18+
interface InternalPoint {
19+
// Coordinates of the point in the SVG viewport.
1320
x: number;
1421
y: number;
15-
handles?: {
22+
23+
// Cubic bezier handle configuration.
24+
handles: {
25+
// Direction of the outgoing path in radians. Value is relative to the 9:00 position
26+
// on a clock and the positive direction is counter-clockwise.
1627
angle: number;
17-
in: number;
28+
29+
// Distance between each handle and the point.
1830
out: number;
31+
in: number;
1932
};
2033
}
2134

22-
interface RenderOptions {
23-
size: number;
24-
center?: boolean;
25-
rotation?: number;
35+
export interface RenderOptions {
36+
// Viewport size.
37+
width: number;
38+
height: number;
39+
40+
// Transformation applied to all drawn points.
41+
transform?: string;
42+
43+
// Output path styling.
2644
fill?: string;
2745
stroke?: string;
2846
strokeWidth?: number;
29-
handles?: boolean;
47+
48+
// Option to render guides (points, handles and viewport).
49+
guides?: boolean;
3050
}
3151

32-
const loop = <T>(arr: T[]) => (i: number): T => {
52+
// Safe array access at any index using a modulo operation that will always be positive.
53+
const loopAccess = <T>(arr: T[]) => (i: number): T => {
3354
return arr[((i%arr.length)+arr.length)%arr.length];
34-
}
55+
};
56+
57+
// Translates a point's [x,y] cartesian coordinates into values relative to the viewport.
58+
// Translates the angle from degrees to radians and moves the start angle a half rotation.
59+
const cleanupPoint = (point: Point, opt: RenderOptions): InternalPoint => {
60+
const handles = point.handles || {angle: 0, out: 0, in: 0};
61+
handles.angle = Math.PI + (2*Math.PI * handles.angle/360);
62+
return {
63+
x: point.x,
64+
y: opt.height - point.y,
65+
handles,
66+
};
67+
};
3568

3669
// 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}[] = [];
70+
const renderClosed = (p: Point[], opt: RenderOptions): string => {
71+
const points = p.map((point) => cleanupPoint(point, opt));
4072

41-
for (let i = 0; i < count; i++) {
42-
const {x, y, handles: hands} = points[i];
73+
// Compute guides from input point data.
74+
const handles: {x1: number, y1: number, x2: number, y2: number}[] = [];
75+
for (let i = 0; i < points.length; i++) {
76+
const {x, y, handles: hands} = loopAccess(points)(i);
4377

44-
const next = loop(points)(i+1);
45-
const nextHandles = next.handles || {angle: 0, in: 0, out: 0};
78+
const next = loopAccess(points)(i+1);
79+
const nextHandles = next.handles;
4680

4781
if (hands === undefined) {
4882
handles.push({x1: x, y1: y, x2: next.x, y2: next.y});
@@ -57,10 +91,12 @@ const render = (points: Point[], opt: RenderOptions): string => {
5791
});
5892
}
5993

94+
// Render path data attribute from points and handles. Must loop more times than the
95+
// number of points in order to correctly close the path.
6096
let path = "";
61-
for (let i = 0; i <= count; i++) {
62-
const point = loop(points)(i);
63-
const hands = loop(handles)(i-1);
97+
for (let i = 0; i <= points.length; i++) {
98+
const point = loopAccess(points)(i);
99+
const hands = loopAccess(handles)(i-1);
64100

65101
// Start at the first point's coordinates.
66102
if (i === 0) {
@@ -72,46 +108,62 @@ const render = (points: Point[], opt: RenderOptions): string => {
72108
path += `C${hands.x1},${hands.y1},${hands.x2},${hands.y2},${point.x},${point.y}`;
73109
}
74110

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-
">
111+
// Render guides if configured to do so.
112+
let guides = "";
113+
if (opt.guides) {
114+
// Bounding box.
115+
guides += `
116+
<rect x="0" y="0" width="${opt.width}" height="${opt.height}"
117+
fill="none" stroke="black" stroke-width="1" stroke-dasharray="2" />`;
118+
119+
// Points and handles.
120+
for (let i = 0; i < points.length; i++) {
121+
const {x, y} = loopAccess(points)(i);
122+
const hands = loopAccess(handles)(i);
123+
const nextPoint = loopAccess(points)(i+1);
124+
125+
guides += `
126+
<line x1="${x}" y1="${y}" x2="${hands.x1}" y2="${hands.y1}"
127+
stroke-width="1" stroke="black" />
128+
<line x1="${nextPoint.x}" y1="${nextPoint.y}" x2="${hands.x2}" y2="${hands.y2}"
129+
stroke-width="1" stroke="black" stroke-dasharray="2" />
130+
<circle cx="${hands.x1}" cy="${hands.y1}" r="1"
131+
fill="black" />
132+
<circle cx="${hands.x2}" cy="${hands.y2}" r="1"
133+
fill="black" />
134+
<circle cx="${x}" cy="${y}" r="2" fill="black" />`;
135+
}
136+
}
137+
138+
return (`
139+
<svg
140+
width="${opt.width}"
141+
height="${opt.height}"
142+
viewBox="0 0 ${opt.width} ${opt.height}"
143+
xmlns="http://www.w3.org/2000/svg"
144+
>
145+
<g transform="${opt.transform || ""}">
81146
<path
82147
stroke="${opt.stroke || "none"}"
83148
stroke-width="${opt.strokeWidth || 0}"
84149
fill="${opt.fill || "none"}"
85150
d="${path}"
86151
/>
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("")}
152+
${guides}
101153
</g>
102154
</svg>
103-
`;
155+
`).replace(/\s+/g, " ");
104156
};
105157

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}},
158+
console.log(renderClosed([
159+
{x: 700, y: 200, handles: {angle: -135, out: 80, in: 80}},
160+
{x: 300, y: 200, handles: {angle: 135, out: 80, in: 80}},
161+
{x: 300, y: 600, handles: {angle: 45, out: 80, in: 80}},
162+
{x: 700, y: 600, handles: {angle: -45, out: 80, in: 80}},
111163
], {
112-
size: 1000,
113-
center: true,
114-
handles: true,
115-
stroke: "green",
164+
width: 1000,
165+
height: 800,
166+
stroke: "blue",
116167
strokeWidth: 1,
168+
guides: true,
117169
}));

0 commit comments

Comments
 (0)