Skip to content

Commit e039a86

Browse files
committed
consolidate old and new point types into one
1 parent 77d135b commit e039a86

File tree

8 files changed

+95
-181
lines changed

8 files changed

+95
-181
lines changed

index.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// https://www.blobmaker.app/
22

33
import {rand} from "./internal/math/rand";
4-
import {Coord} from "./internal/types";
4+
import {Shape} from "./internal/types";
55
import {rad} from "./internal/math/unit";
66
import {smooth} from "./internal/svg/smooth";
77
import {renderEditable} from "./internal/svg/render";
@@ -72,22 +72,20 @@ blobs.editable = (opt: BlobOptions): XmlElement => {
7272
const angle = 360 / count;
7373
const radius = opt.size / Math.E;
7474

75-
const points: Coord[] = [];
75+
const points: Shape = [];
7676
for (let i = 0; i < count; i++) {
7777
const rand = 1 - 0.8 * opt.contrast * rgen();
78-
7978
points.push({
8079
x: Math.sin(rad(i * angle)) * radius * rand + opt.size / 2,
81-
y: Math.cos(rad(i * angle)) * radius * rand + opt.size / 2,
80+
y: opt.size - (Math.cos(rad(i * angle)) * radius * rand + opt.size / 2),
81+
handleIn: {angle: 0, length: 0},
82+
handleOut: {angle: 0, length: 0},
8283
});
8384
}
8485

8586
// https://math.stackexchange.com/a/873589/235756
8687
const smoothingStrength = ((4 / 3) * Math.tan(rad(angle / 4))) / Math.sin(rad(angle / 2));
87-
const smoothed = smooth(points, {
88-
closed: true,
89-
strength: smoothingStrength,
90-
});
88+
const smoothed = smooth(points, smoothingStrength);
9189

9290
return renderEditable(smoothed, {
9391
closed: true,
@@ -101,6 +99,7 @@ blobs.editable = (opt: BlobOptions): XmlElement => {
10199
});
102100
};
103101

102+
// TODO remove
104103
blobs.path = (opt: PathOptions) => {
105104
if (!opt) {
106105
throw new Error("no options specified");
@@ -125,20 +124,20 @@ blobs.path = (opt: PathOptions) => {
125124
const angle = 360 / count;
126125
const radius = opt.size / Math.E;
127126

128-
const points: Coord[] = [];
127+
const points: Shape = [];
129128
for (let i = 0; i < count; i++) {
130129
const rand = 1 - 0.8 * opt.contrast * rgen();
131-
132130
points.push({
133131
x: Math.sin(rad(i * angle)) * radius * rand + opt.size / 2,
134132
y: Math.cos(rad(i * angle)) * radius * rand + opt.size / 2,
133+
handleIn: {angle: 0, length: 0},
134+
handleOut: {angle: 0, length: 0},
135135
});
136136
}
137137

138-
const smoothed = smooth(points, {
139-
closed: true,
140-
strength: ((4 / 3) * Math.tan(rad(angle / 4))) / Math.sin(rad(angle / 2)),
141-
});
138+
// https://math.stackexchange.com/a/873589/235756
139+
const smoothingStrength = ((4 / 3) * Math.tan(rad(angle / 4))) / Math.sin(rad(angle / 2));
140+
const smoothed = smooth(points, smoothingStrength);
142141

143142
return smoothed;
144143
};

internal/shape/prepare.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const divideShape = (count: number, points: Shape): Shape => {
5454
};
5555

5656
const fixAngles = (a: Shape, b: Shape): Shape => {
57+
// TODO fix in first shape too
5758
const out: Shape = [];
5859
for (let i = 0; i < a.length; i++) {
5960
const point = copyPoint(b[i]);

internal/shape/util.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,26 @@ export const copyPoint = (p: Point): Point => ({
88
handleOut: {...p.handleOut},
99
});
1010

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-
};
11+
export const angleBetween = (a: Coord, b: Coord): number => {
12+
const dx = b.x - a.x;
13+
const dy = -b.y + a.y;
14+
const angle = Math.atan2(dy, dx);
15+
if (angle < 0) {
16+
return Math.abs(angle);
17+
} else {
18+
return 2 * Math.PI - angle;
19+
}
1620
};
1721

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-
};
22+
export const expandHandle = (point: Coord, handle: Handle): Coord => ({
23+
x: point.x + handle.length * Math.cos(handle.angle),
24+
y: point.y + handle.length * Math.sin(handle.angle),
25+
});
26+
27+
const collapseHandle = (point: Coord, handle: Coord): Handle => ({
28+
angle: angleBetween(point, handle),
29+
length: Math.sqrt((handle.x - point.x) ** 2 + (handle.y - point.y) ** 2),
30+
});
2731

2832
export const length = (a: Point, b: Point): number => {
2933
const aHandle = expandHandle(a, a.handleOut);

internal/svg/point.ts

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

internal/svg/render.ts

Lines changed: 37 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {loopAccess} from "./util";
2-
import {Point, interpolate} from "./point";
32
import {xml, XmlElement} from "../../editable";
3+
import {Shape} from "../types";
4+
import {expandHandle} from "../shape/util";
45

56
export interface RenderOptions {
67
// Viewport size.
@@ -26,45 +27,15 @@ export interface RenderOptions {
2627

2728
// Renders a shape made up of the input points to an editable data structure
2829
// which can be rendered to svg.
29-
export const renderEditable = (p: Point[], opt: RenderOptions): XmlElement => {
30-
const points = p.map((point) => interpolate(point, opt.height));
31-
32-
// Compute guides from input point data.
33-
const handles: {x1: number; y1: number; x2: number; y2: number}[] = [];
30+
export const renderEditable = (points: Shape, opt: RenderOptions): XmlElement => {
31+
// Render path data attribute from points and handles.
32+
let path = `M${points[0].x},${points[0].y}`;
3433
for (let i = 0; i < points.length; i++) {
35-
const {x, y, handles: hands} = loopAccess(points)(i);
36-
34+
const curr = loopAccess(points)(i);
3735
const next = loopAccess(points)(i + 1);
38-
const nextHandles = next.handles;
39-
40-
if (hands === undefined) {
41-
handles.push({x1: x, y1: y, x2: next.x, y2: next.y});
42-
continue;
43-
}
44-
45-
handles.push({
46-
x1: x - Math.cos(hands.angle) * hands.out,
47-
y1: y + Math.sin(hands.angle) * hands.out,
48-
x2: next.x + Math.cos(nextHandles.angle) * nextHandles.in,
49-
y2: next.y - Math.sin(nextHandles.angle) * nextHandles.in,
50-
});
51-
}
52-
53-
// Render path data attribute from points and handles. Must loop more times
54-
// than the number of points in order to correctly close the path.
55-
let path = "";
56-
for (let i = 0; i <= points.length; i++) {
57-
const point = loopAccess(points)(i);
58-
const hands = loopAccess(handles)(i - 1);
59-
60-
// Start at the first point's coordinates.
61-
if (i === 0) {
62-
path += `M${point.x},${point.y}`;
63-
continue;
64-
}
65-
66-
// Add cubic bezier coordinates using the computed handle positions.
67-
path += `C${hands.x1},${hands.y1},${hands.x2},${hands.y2},${point.x},${point.y}`;
36+
const currControl = expandHandle(curr, curr.handleOut);
37+
const nextControl = expandHandle(next, next.handleIn);
38+
path += `C${currControl.x},${currControl.y},${nextControl.x},${nextControl.y},${next.x},${next.y}`;
6839
}
6940

7041
const stroke = opt.stroke || (opt.guides ? "black" : "none");
@@ -109,49 +80,50 @@ export const renderEditable = (p: Point[], opt: RenderOptions): XmlElement => {
10980

11081
// Points and handles.
11182
for (let i = 0; i < points.length; i++) {
112-
const {x, y} = loopAccess(points)(i);
113-
const hands = loopAccess(handles)(i);
114-
const nextPoint = loopAccess(points)(i + 1);
115-
116-
const xmlIncomingHandleLine = xml("line");
117-
xmlIncomingHandleLine.attributes.x1 = x;
118-
xmlIncomingHandleLine.attributes.y1 = y;
119-
xmlIncomingHandleLine.attributes.x2 = hands.x1;
120-
xmlIncomingHandleLine.attributes.y2 = hands.y1;
121-
xmlIncomingHandleLine.attributes["stroke-width"] = size;
122-
xmlIncomingHandleLine.attributes.stroke = color;
83+
const curr = loopAccess(points)(i);
84+
const next = loopAccess(points)(i + 1);
85+
const currControl = expandHandle(curr, curr.handleOut);
86+
const nextControl = expandHandle(next, next.handleIn);
12387

12488
const xmlOutgoingHandleLine = xml("line");
125-
xmlOutgoingHandleLine.attributes.x1 = nextPoint.x;
126-
xmlOutgoingHandleLine.attributes.y1 = nextPoint.y;
127-
xmlOutgoingHandleLine.attributes.x2 = hands.x2;
128-
xmlOutgoingHandleLine.attributes.y2 = hands.y2;
89+
xmlOutgoingHandleLine.attributes.x1 = curr.x;
90+
xmlOutgoingHandleLine.attributes.y1 = curr.y;
91+
xmlOutgoingHandleLine.attributes.x2 = currControl.x;
92+
xmlOutgoingHandleLine.attributes.y2 = currControl.y;
12993
xmlOutgoingHandleLine.attributes["stroke-width"] = size;
13094
xmlOutgoingHandleLine.attributes.stroke = color;
131-
xmlOutgoingHandleLine.attributes["stroke-dasharray"] = 2 * size;
13295

133-
const xmlIncomingHandleCircle = xml("circle");
134-
xmlIncomingHandleCircle.attributes.cx = hands.x1;
135-
xmlIncomingHandleCircle.attributes.cy = hands.y1;
136-
xmlIncomingHandleCircle.attributes.r = size;
137-
xmlIncomingHandleCircle.attributes.fill = color;
96+
const xmlIncomingHandleLine = xml("line");
97+
xmlIncomingHandleLine.attributes.x1 = next.x;
98+
xmlIncomingHandleLine.attributes.y1 = next.y;
99+
xmlIncomingHandleLine.attributes.x2 = nextControl.x;
100+
xmlIncomingHandleLine.attributes.y2 = nextControl.y;
101+
xmlIncomingHandleLine.attributes["stroke-width"] = size;
102+
xmlIncomingHandleLine.attributes.stroke = color;
103+
xmlIncomingHandleLine.attributes["stroke-dasharray"] = 2 * size;
138104

139105
const xmlOutgoingHandleCircle = xml("circle");
140-
xmlOutgoingHandleCircle.attributes.cx = hands.x2;
141-
xmlOutgoingHandleCircle.attributes.cy = hands.y2;
106+
xmlOutgoingHandleCircle.attributes.cx = currControl.x;
107+
xmlOutgoingHandleCircle.attributes.cy = currControl.y;
142108
xmlOutgoingHandleCircle.attributes.r = size;
143109
xmlOutgoingHandleCircle.attributes.fill = color;
144110

111+
const xmlIncomingHandleCircle = xml("circle");
112+
xmlIncomingHandleCircle.attributes.cx = nextControl.x;
113+
xmlIncomingHandleCircle.attributes.cy = nextControl.y;
114+
xmlIncomingHandleCircle.attributes.r = size;
115+
xmlIncomingHandleCircle.attributes.fill = color;
116+
145117
const xmlPointCircle = xml("circle");
146-
xmlPointCircle.attributes.cx = x;
147-
xmlPointCircle.attributes.cy = y;
118+
xmlPointCircle.attributes.cx = curr.x;
119+
xmlPointCircle.attributes.cy = curr.y;
148120
xmlPointCircle.attributes.r = 2 * size;
149121
xmlPointCircle.attributes.fill = color;
150122

151-
xmlContentGroup.children.push(xmlIncomingHandleLine);
152123
xmlContentGroup.children.push(xmlOutgoingHandleLine);
153-
xmlContentGroup.children.push(xmlIncomingHandleCircle);
124+
xmlContentGroup.children.push(xmlIncomingHandleLine);
154125
xmlContentGroup.children.push(xmlOutgoingHandleCircle);
126+
xmlContentGroup.children.push(xmlIncomingHandleCircle);
155127
xmlContentGroup.children.push(xmlPointCircle);
156128
}
157129
}

internal/svg/smooth.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,31 @@
1-
import {Point} from "./point";
1+
import {Shape} from "../types";
22
import {loopAccess} from "./util";
3-
import {angle, distance} from "../math/geometry";
4-
5-
export interface SmoothingOptions {
6-
// Declare whether the path is closed.
7-
// This option is currently always true.
8-
closed: true;
9-
10-
// Smoothing strength as ratio [0,1].
11-
strength: number;
12-
}
3+
import {distance} from "../math/geometry";
4+
import {angleBetween} from "../shape/util";
135

146
// Smooths out the path made up of the given points.
157
// Existing handles are ignored.
16-
export const smooth = (points: Point[], opt: SmoothingOptions): Point[] => {
17-
if (points.length < 3) throw new Error("not enough points to smooth shape");
8+
export const smooth = (shape: Shape, strength: number): Shape => {
9+
if (shape.length < 3) throw new Error("not enough points to smooth shape");
1810

19-
const out: Point[] = [];
11+
const out: Shape = [];
2012

21-
for (let i = 0; i < points.length; i++) {
22-
const curr = loopAccess(points)(i);
23-
const before = loopAccess(points)(i - 1);
24-
const after = loopAccess(points)(i + 1);
13+
for (let i = 0; i < shape.length; i++) {
14+
const curr = loopAccess(shape)(i);
15+
const before = loopAccess(shape)(i - 1);
16+
const after = loopAccess(shape)(i + 1);
17+
const angle = angleBetween(before, after);
2518

2619
out.push({
2720
x: curr.x,
2821
y: curr.y,
29-
handles: {
30-
angle: angle(before, after),
31-
in: opt.strength * (1 / 2) * distance(curr, before),
32-
out: opt.strength * (1 / 2) * distance(curr, after),
22+
handleIn: {
23+
angle: angle + Math.PI,
24+
length: strength * (1 / 2) * distance(curr, before),
25+
},
26+
handleOut: {
27+
angle,
28+
length: strength * (1 / 2) * distance(curr, after),
3329
},
3430
});
3531
}

0 commit comments

Comments
 (0)