dingbatch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/dist/chunk-IAFZ3FNR.js +531 -0
- package/dist/chunk-IAFZ3FNR.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +29 -0
- package/dist/react/index.js +58 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# dingbatch
|
|
2
|
+
|
|
3
|
+
Parametric MCM (Mid-Century Modern) dingbat shapes as SVGs. Curated presets with full customization.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install dingbatch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic - Use Presets
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { boomerang } from 'dingbatch';
|
|
17
|
+
|
|
18
|
+
// Get a preset shape
|
|
19
|
+
const shape = boomerang.crescent();
|
|
20
|
+
|
|
21
|
+
// Returns:
|
|
22
|
+
// {
|
|
23
|
+
// path: "M...", // SVG path d attribute
|
|
24
|
+
// width: 100, // Bounding width
|
|
25
|
+
// height: 80, // Bounding height
|
|
26
|
+
// viewBox: "0 0 100 80",
|
|
27
|
+
// svg: "<svg>...</svg>" // Complete SVG string
|
|
28
|
+
// }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Available Presets
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
boomerang.crescent() // Curved arc/moon shape
|
|
35
|
+
boomerang.fatMoon() // Wide crescent, rounded tips
|
|
36
|
+
boomerang.sharpChevron() // Solid V-shape arrow
|
|
37
|
+
boomerang.thinArc() // Narrow curved arc
|
|
38
|
+
boomerang.horseshoe() // U-shape
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With Overrides
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// Rotate a preset
|
|
45
|
+
boomerang.crescent({ rotation: 45 })
|
|
46
|
+
|
|
47
|
+
// Change any parameter
|
|
48
|
+
boomerang.fatMoon({ thickness: 60, taper: 0.8 })
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Full Custom
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
boomerang({
|
|
55
|
+
style: 'crescent',
|
|
56
|
+
armLength: 50,
|
|
57
|
+
bendAngle: 160,
|
|
58
|
+
thickness: 35,
|
|
59
|
+
taper: 0.5,
|
|
60
|
+
tipRoundness: 0.2,
|
|
61
|
+
rotation: 0,
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## React Components
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Dingbat, Boomerang } from 'dingbatch/react';
|
|
69
|
+
import { boomerang } from 'dingbatch';
|
|
70
|
+
|
|
71
|
+
// Generic component - pass any shape
|
|
72
|
+
<Dingbat shape={boomerang.crescent()} fill="#D4A03D" />
|
|
73
|
+
|
|
74
|
+
// Shape-specific component
|
|
75
|
+
<Boomerang preset="crescent" fill="#D4A03D" />
|
|
76
|
+
<Boomerang preset="fatMoon" rotation={45} className="hero-deco" />
|
|
77
|
+
|
|
78
|
+
// Custom params
|
|
79
|
+
<Boomerang
|
|
80
|
+
params={{ style: 'chevron', armLength: 60, bendAngle: 70, thickness: 30, taper: 1, tipRoundness: 0 }}
|
|
81
|
+
fill="#1A1A1A"
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Dingbat Props
|
|
86
|
+
|
|
87
|
+
| Prop | Type | Default | Description |
|
|
88
|
+
|------|------|---------|-------------|
|
|
89
|
+
| `shape` | `DingbatResult` | required | Shape object from generator |
|
|
90
|
+
| `fill` | `string` | `'currentColor'` | Fill color |
|
|
91
|
+
| `stroke` | `string` | `'none'` | Stroke color |
|
|
92
|
+
| `strokeWidth` | `number` | `0` | Stroke width |
|
|
93
|
+
| `className` | `string` | - | CSS class |
|
|
94
|
+
| `style` | `CSSProperties` | - | Inline styles |
|
|
95
|
+
|
|
96
|
+
### Boomerang Props
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Default | Description |
|
|
99
|
+
|------|------|---------|-------------|
|
|
100
|
+
| `preset` | `string` | `'crescent'` | Preset name |
|
|
101
|
+
| `params` | `BoomerangParams` | - | Full custom params (overrides preset) |
|
|
102
|
+
| `rotation` | `number` | - | Rotation in degrees |
|
|
103
|
+
| + all Dingbat props | | | |
|
|
104
|
+
|
|
105
|
+
## Boomerang Styles
|
|
106
|
+
|
|
107
|
+
The boomerang generator supports 4 geometric styles:
|
|
108
|
+
|
|
109
|
+
- **crescent** - Curved arc/moon shape
|
|
110
|
+
- **classic** - Traditional boomerang with two diverging arms
|
|
111
|
+
- **horseshoe** - U-shape with parallel arms
|
|
112
|
+
- **chevron** - Solid V-shape or arrow
|
|
113
|
+
|
|
114
|
+
## Parameters
|
|
115
|
+
|
|
116
|
+
| Parameter | Type | Description |
|
|
117
|
+
|-----------|------|-------------|
|
|
118
|
+
| `style` | `'classic' \| 'crescent' \| 'horseshoe' \| 'chevron'` | Geometry mode |
|
|
119
|
+
| `armLength` | `number` | Length of arms or arc radius |
|
|
120
|
+
| `bendAngle` | `number` | Angle between arms or arc span |
|
|
121
|
+
| `thickness` | `number` | Shape thickness |
|
|
122
|
+
| `taper` | `number` | Tip thickness ratio (0=pointed, 1=uniform) |
|
|
123
|
+
| `tipRoundness` | `number` | How rounded the tips are (0-1) |
|
|
124
|
+
| `armCurvature` | `number` | Curve amount for classic/horseshoe (0-1) |
|
|
125
|
+
| `bendSharpness` | `number` | Bend smoothness for classic (0-1) |
|
|
126
|
+
| `armBalance` | `number` | Asymmetric arm lengths (0.5-2) |
|
|
127
|
+
| `rotation` | `number` | Rotation in degrees |
|
|
128
|
+
|
|
129
|
+
## TypeScript
|
|
130
|
+
|
|
131
|
+
Full TypeScript support with exported types:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import type {
|
|
135
|
+
DingbatResult,
|
|
136
|
+
BoomerangParams,
|
|
137
|
+
BoomerangStyle
|
|
138
|
+
} from 'dingbatch';
|
|
139
|
+
|
|
140
|
+
import type {
|
|
141
|
+
DingbatProps,
|
|
142
|
+
BoomerangProps
|
|
143
|
+
} from 'dingbatch/react';
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// src/generators/utils/svg.ts
|
|
2
|
+
function wrapPath(path, width, height) {
|
|
3
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}"><path d="${path}" fill="currentColor"/></svg>`;
|
|
4
|
+
}
|
|
5
|
+
function addSvgFields(result) {
|
|
6
|
+
const viewBox = `0 0 ${result.width} ${result.height}`;
|
|
7
|
+
const svg = wrapPath(result.path, result.width, result.height);
|
|
8
|
+
return { ...result, viewBox, svg };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/presets/boomerang.ts
|
|
12
|
+
var crescent = {
|
|
13
|
+
style: "crescent",
|
|
14
|
+
armLength: 50,
|
|
15
|
+
bendAngle: 160,
|
|
16
|
+
thickness: 35,
|
|
17
|
+
taper: 0.5,
|
|
18
|
+
tipRoundness: 0.2,
|
|
19
|
+
armCurvature: 0,
|
|
20
|
+
bendSharpness: 0
|
|
21
|
+
};
|
|
22
|
+
var fatMoon = {
|
|
23
|
+
style: "crescent",
|
|
24
|
+
armLength: 45,
|
|
25
|
+
bendAngle: 180,
|
|
26
|
+
thickness: 50,
|
|
27
|
+
taper: 0.3,
|
|
28
|
+
tipRoundness: 0.6,
|
|
29
|
+
armCurvature: 0,
|
|
30
|
+
bendSharpness: 0
|
|
31
|
+
};
|
|
32
|
+
var sharpChevron = {
|
|
33
|
+
style: "chevron",
|
|
34
|
+
armLength: 55,
|
|
35
|
+
bendAngle: 60,
|
|
36
|
+
thickness: 40,
|
|
37
|
+
taper: 1,
|
|
38
|
+
tipRoundness: 0,
|
|
39
|
+
armCurvature: 0,
|
|
40
|
+
bendSharpness: 0
|
|
41
|
+
};
|
|
42
|
+
var thinArc = {
|
|
43
|
+
style: "crescent",
|
|
44
|
+
armLength: 60,
|
|
45
|
+
bendAngle: 140,
|
|
46
|
+
thickness: 20,
|
|
47
|
+
taper: 0.7,
|
|
48
|
+
tipRoundness: 0.1,
|
|
49
|
+
armCurvature: 0,
|
|
50
|
+
bendSharpness: 0
|
|
51
|
+
};
|
|
52
|
+
var horseshoe = {
|
|
53
|
+
style: "horseshoe",
|
|
54
|
+
armLength: 80,
|
|
55
|
+
bendAngle: 50,
|
|
56
|
+
thickness: 18,
|
|
57
|
+
taper: 0.2,
|
|
58
|
+
tipRoundness: 0.3,
|
|
59
|
+
armCurvature: 0.4,
|
|
60
|
+
bendSharpness: 0
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/generators/curved/boomerang.ts
|
|
64
|
+
function boomerangBase(params) {
|
|
65
|
+
const style = params.style || "classic";
|
|
66
|
+
switch (style) {
|
|
67
|
+
case "crescent":
|
|
68
|
+
return generateCrescent(params);
|
|
69
|
+
case "horseshoe":
|
|
70
|
+
return generateHorseshoe(params);
|
|
71
|
+
case "chevron":
|
|
72
|
+
return generateChevron(params);
|
|
73
|
+
case "classic":
|
|
74
|
+
default:
|
|
75
|
+
return generateClassic(params);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
var boomerang = Object.assign(boomerangBase, {
|
|
79
|
+
crescent: (overrides) => boomerangBase({ ...crescent, ...overrides }),
|
|
80
|
+
fatMoon: (overrides) => boomerangBase({ ...fatMoon, ...overrides }),
|
|
81
|
+
sharpChevron: (overrides) => boomerangBase({ ...sharpChevron, ...overrides }),
|
|
82
|
+
thinArc: (overrides) => boomerangBase({ ...thinArc, ...overrides }),
|
|
83
|
+
horseshoe: (overrides) => boomerangBase({ ...horseshoe, ...overrides })
|
|
84
|
+
});
|
|
85
|
+
function generateClassic(params) {
|
|
86
|
+
const {
|
|
87
|
+
armLength,
|
|
88
|
+
bendAngle,
|
|
89
|
+
armCurvature,
|
|
90
|
+
bendSharpness,
|
|
91
|
+
thickness,
|
|
92
|
+
taper,
|
|
93
|
+
tipRoundness,
|
|
94
|
+
armBalance = 1,
|
|
95
|
+
thicknessBalance = 0,
|
|
96
|
+
rotation = 0
|
|
97
|
+
} = params;
|
|
98
|
+
const clampedBendAngle = Math.max(30, Math.min(180, bendAngle));
|
|
99
|
+
const clampedArmCurvature = Math.max(0, Math.min(1, armCurvature));
|
|
100
|
+
const clampedBendSharpness = Math.max(0, Math.min(1, bendSharpness));
|
|
101
|
+
const clampedTaper = Math.max(0, Math.min(1, taper));
|
|
102
|
+
const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));
|
|
103
|
+
const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));
|
|
104
|
+
const clampedThicknessBalance = Math.max(-1, Math.min(1, thicknessBalance));
|
|
105
|
+
const arm1Length = armLength * (2 / (1 + clampedArmBalance));
|
|
106
|
+
const arm2Length = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));
|
|
107
|
+
const halfAngle = clampedBendAngle / 2 * Math.PI / 180;
|
|
108
|
+
const arm1Dir = { x: -Math.sin(halfAngle), y: -Math.cos(halfAngle) };
|
|
109
|
+
const arm2Dir = { x: Math.sin(halfAngle), y: -Math.cos(halfAngle) };
|
|
110
|
+
const tip1 = { x: arm1Dir.x * arm1Length, y: arm1Dir.y * arm1Length };
|
|
111
|
+
const tip2 = { x: arm2Dir.x * arm2Length, y: arm2Dir.y * arm2Length };
|
|
112
|
+
const bendPoint = { x: 0, y: 0 };
|
|
113
|
+
const numSamples = 20;
|
|
114
|
+
const arm1Spine = generateArmSpine(tip1, bendPoint, arm1Dir, arm1Length, clampedArmCurvature, numSamples);
|
|
115
|
+
const arm2Spine = generateArmSpine(bendPoint, tip2, arm2Dir, arm2Length, clampedArmCurvature, numSamples);
|
|
116
|
+
const minThickness = thickness * clampedTaper;
|
|
117
|
+
function getThicknessAlongArm(t, isArm1) {
|
|
118
|
+
const tipFade = Math.sin(t * Math.PI / 2);
|
|
119
|
+
const baseThickness = minThickness + (thickness - minThickness) * tipFade;
|
|
120
|
+
const balanceFactor = isArm1 ? 1 - clampedThicknessBalance * 0.3 : 1 + clampedThicknessBalance * 0.3;
|
|
121
|
+
return baseThickness * balanceFactor;
|
|
122
|
+
}
|
|
123
|
+
const arm1Outer = [];
|
|
124
|
+
const arm1Inner = [];
|
|
125
|
+
const arm2Outer = [];
|
|
126
|
+
const arm2Inner = [];
|
|
127
|
+
for (let i = 0; i < arm1Spine.length; i++) {
|
|
128
|
+
const t = i / (arm1Spine.length - 1);
|
|
129
|
+
const point = arm1Spine[i];
|
|
130
|
+
const tangent = getSpineTangent(arm1Spine, i);
|
|
131
|
+
const normal = { x: -tangent.y, y: tangent.x };
|
|
132
|
+
const halfThick = getThicknessAlongArm(t, true) / 2;
|
|
133
|
+
arm1Outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });
|
|
134
|
+
arm1Inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });
|
|
135
|
+
}
|
|
136
|
+
for (let i = 0; i < arm2Spine.length; i++) {
|
|
137
|
+
const t = 1 - i / (arm2Spine.length - 1);
|
|
138
|
+
const point = arm2Spine[i];
|
|
139
|
+
const tangent = getSpineTangent(arm2Spine, i);
|
|
140
|
+
const normal = { x: -tangent.y, y: tangent.x };
|
|
141
|
+
const halfThick = getThicknessAlongArm(t, false) / 2;
|
|
142
|
+
arm2Outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });
|
|
143
|
+
arm2Inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });
|
|
144
|
+
}
|
|
145
|
+
const pathParts = [];
|
|
146
|
+
pathParts.push(`M ${arm1Outer[0].x.toFixed(2)},${arm1Outer[0].y.toFixed(2)}`);
|
|
147
|
+
const tip1Curve = clampedTipRoundness * thickness * 0.3;
|
|
148
|
+
pathParts.push(`Q ${(tip1.x + arm1Dir.x * tip1Curve).toFixed(2)},${(tip1.y + arm1Dir.y * tip1Curve).toFixed(2)} ${arm1Inner[0].x.toFixed(2)},${arm1Inner[0].y.toFixed(2)}`);
|
|
149
|
+
pathParts.push(smoothCurveThroughPoints(arm1Inner));
|
|
150
|
+
const bendInner2 = arm2Inner[0];
|
|
151
|
+
if (clampedBendSharpness < 0.3) {
|
|
152
|
+
pathParts.push(`L ${bendInner2.x.toFixed(2)},${bendInner2.y.toFixed(2)}`);
|
|
153
|
+
} else {
|
|
154
|
+
const bendInnerOffset = thickness * 0.2 * clampedBendSharpness;
|
|
155
|
+
const bendInnerCtrl = { x: 0, y: bendInnerOffset };
|
|
156
|
+
pathParts.push(`Q ${bendInnerCtrl.x.toFixed(2)},${bendInnerCtrl.y.toFixed(2)} ${bendInner2.x.toFixed(2)},${bendInner2.y.toFixed(2)}`);
|
|
157
|
+
}
|
|
158
|
+
pathParts.push(smoothCurveThroughPoints(arm2Inner));
|
|
159
|
+
const tip2Curve = clampedTipRoundness * thickness * 0.3;
|
|
160
|
+
pathParts.push(`Q ${(tip2.x + arm2Dir.x * tip2Curve).toFixed(2)},${(tip2.y + arm2Dir.y * tip2Curve).toFixed(2)} ${arm2Outer[arm2Outer.length - 1].x.toFixed(2)},${arm2Outer[arm2Outer.length - 1].y.toFixed(2)}`);
|
|
161
|
+
const arm2OuterReversed = [...arm2Outer].reverse();
|
|
162
|
+
pathParts.push(smoothCurveThroughPoints(arm2OuterReversed));
|
|
163
|
+
const bendOuter1 = arm1Outer[arm1Outer.length - 1];
|
|
164
|
+
if (clampedBendSharpness < 0.3) {
|
|
165
|
+
pathParts.push(`L ${bendOuter1.x.toFixed(2)},${bendOuter1.y.toFixed(2)}`);
|
|
166
|
+
} else {
|
|
167
|
+
const bendOuterOffset = -thickness * 0.4 * clampedBendSharpness;
|
|
168
|
+
const bendOuterCtrl = { x: 0, y: bendOuterOffset };
|
|
169
|
+
pathParts.push(`Q ${bendOuterCtrl.x.toFixed(2)},${bendOuterCtrl.y.toFixed(2)} ${bendOuter1.x.toFixed(2)},${bendOuter1.y.toFixed(2)}`);
|
|
170
|
+
}
|
|
171
|
+
const arm1OuterReversed = [...arm1Outer].reverse();
|
|
172
|
+
pathParts.push(smoothCurveThroughPoints(arm1OuterReversed));
|
|
173
|
+
pathParts.push("Z");
|
|
174
|
+
let path = pathParts.join(" ");
|
|
175
|
+
const allPoints = [...arm1Outer, ...arm1Inner, ...arm2Outer, ...arm2Inner];
|
|
176
|
+
let minX = Math.min(...allPoints.map((p) => p.x));
|
|
177
|
+
let maxX = Math.max(...allPoints.map((p) => p.x));
|
|
178
|
+
let minY = Math.min(...allPoints.map((p) => p.y));
|
|
179
|
+
let maxY = Math.max(...allPoints.map((p) => p.y));
|
|
180
|
+
const padding = thickness * 0.3 * clampedTipRoundness;
|
|
181
|
+
minX -= padding;
|
|
182
|
+
maxX += padding;
|
|
183
|
+
minY -= padding;
|
|
184
|
+
const width = maxX - minX;
|
|
185
|
+
const height = maxY - minY;
|
|
186
|
+
path = normalizePath(path, minX, minY);
|
|
187
|
+
if (rotation !== 0) {
|
|
188
|
+
path = rotatePath(path, rotation, width, height);
|
|
189
|
+
}
|
|
190
|
+
return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });
|
|
191
|
+
}
|
|
192
|
+
function generateCrescent(params) {
|
|
193
|
+
const {
|
|
194
|
+
armLength,
|
|
195
|
+
// Used as arc radius
|
|
196
|
+
bendAngle,
|
|
197
|
+
// Used as arc span (degrees, 30-300)
|
|
198
|
+
thickness,
|
|
199
|
+
taper,
|
|
200
|
+
tipRoundness,
|
|
201
|
+
armBalance = 1,
|
|
202
|
+
// Asymmetric arc thickness along length
|
|
203
|
+
rotation = 0
|
|
204
|
+
} = params;
|
|
205
|
+
const radius = Math.max(20, armLength);
|
|
206
|
+
const arcSpan = Math.max(30, Math.min(300, bendAngle)) * Math.PI / 180;
|
|
207
|
+
const clampedTaper = Math.max(0, Math.min(1, taper));
|
|
208
|
+
const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));
|
|
209
|
+
const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));
|
|
210
|
+
const numSamples = 30;
|
|
211
|
+
const startAngle = -arcSpan / 2;
|
|
212
|
+
const endAngle = arcSpan / 2;
|
|
213
|
+
const spine = [];
|
|
214
|
+
for (let i = 0; i <= numSamples; i++) {
|
|
215
|
+
const t = i / numSamples;
|
|
216
|
+
const angle = startAngle + (endAngle - startAngle) * t;
|
|
217
|
+
spine.push({
|
|
218
|
+
x: Math.sin(angle) * radius,
|
|
219
|
+
y: -Math.cos(angle) * radius + radius
|
|
220
|
+
// Shift so bottom of arc is at y=0
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const minThickness = thickness * clampedTaper;
|
|
224
|
+
function getThicknessAtT(t) {
|
|
225
|
+
const balanceShift = (t - 0.5) * (clampedArmBalance - 1) * 0.5;
|
|
226
|
+
const adjustedT = Math.max(0, Math.min(1, t + balanceShift));
|
|
227
|
+
const adjustedDist = Math.min(adjustedT, 1 - adjustedT) * 2;
|
|
228
|
+
const adjustedFactor = Math.sin(adjustedDist * Math.PI / 2);
|
|
229
|
+
return minThickness + (thickness - minThickness) * adjustedFactor;
|
|
230
|
+
}
|
|
231
|
+
const outer = [];
|
|
232
|
+
const inner = [];
|
|
233
|
+
for (let i = 0; i < spine.length; i++) {
|
|
234
|
+
const t = i / (spine.length - 1);
|
|
235
|
+
const point = spine[i];
|
|
236
|
+
const tangent = getSpineTangent(spine, i);
|
|
237
|
+
const normal = { x: -tangent.y, y: tangent.x };
|
|
238
|
+
const halfThick = getThicknessAtT(t) / 2;
|
|
239
|
+
outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });
|
|
240
|
+
inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });
|
|
241
|
+
}
|
|
242
|
+
const pathParts = [];
|
|
243
|
+
pathParts.push(`M ${outer[0].x.toFixed(2)},${outer[0].y.toFixed(2)}`);
|
|
244
|
+
const startTipCurve = clampedTipRoundness * thickness * 0.4;
|
|
245
|
+
const startTangent = getSpineTangent(spine, 0);
|
|
246
|
+
pathParts.push(`Q ${(spine[0].x - startTangent.x * startTipCurve).toFixed(2)},${(spine[0].y - startTangent.y * startTipCurve).toFixed(2)} ${inner[0].x.toFixed(2)},${inner[0].y.toFixed(2)}`);
|
|
247
|
+
pathParts.push(smoothCurveThroughPoints(inner));
|
|
248
|
+
const endTipCurve = clampedTipRoundness * thickness * 0.4;
|
|
249
|
+
const endTangent = getSpineTangent(spine, spine.length - 1);
|
|
250
|
+
pathParts.push(`Q ${(spine[spine.length - 1].x + endTangent.x * endTipCurve).toFixed(2)},${(spine[spine.length - 1].y + endTangent.y * endTipCurve).toFixed(2)} ${outer[outer.length - 1].x.toFixed(2)},${outer[outer.length - 1].y.toFixed(2)}`);
|
|
251
|
+
const outerReversed = [...outer].reverse();
|
|
252
|
+
pathParts.push(smoothCurveThroughPoints(outerReversed));
|
|
253
|
+
pathParts.push("Z");
|
|
254
|
+
let path = pathParts.join(" ");
|
|
255
|
+
const allPoints = [...outer, ...inner];
|
|
256
|
+
let minX = Math.min(...allPoints.map((p) => p.x));
|
|
257
|
+
let maxX = Math.max(...allPoints.map((p) => p.x));
|
|
258
|
+
let minY = Math.min(...allPoints.map((p) => p.y));
|
|
259
|
+
let maxY = Math.max(...allPoints.map((p) => p.y));
|
|
260
|
+
const padding = thickness * 0.3 * clampedTipRoundness;
|
|
261
|
+
minX -= padding;
|
|
262
|
+
maxX += padding;
|
|
263
|
+
minY -= padding;
|
|
264
|
+
maxY += padding;
|
|
265
|
+
const width = maxX - minX;
|
|
266
|
+
const height = maxY - minY;
|
|
267
|
+
path = normalizePath(path, minX, minY);
|
|
268
|
+
if (rotation !== 0) {
|
|
269
|
+
path = rotatePath(path, rotation, width, height);
|
|
270
|
+
}
|
|
271
|
+
return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });
|
|
272
|
+
}
|
|
273
|
+
function generateHorseshoe(params) {
|
|
274
|
+
const {
|
|
275
|
+
armLength,
|
|
276
|
+
// Length of the vertical arms
|
|
277
|
+
bendAngle,
|
|
278
|
+
// Spacing between arms (width at top)
|
|
279
|
+
armCurvature,
|
|
280
|
+
// How curved the bottom connection is
|
|
281
|
+
thickness,
|
|
282
|
+
taper,
|
|
283
|
+
tipRoundness,
|
|
284
|
+
armBalance = 1,
|
|
285
|
+
// Asymmetric arm lengths
|
|
286
|
+
rotation = 0
|
|
287
|
+
} = params;
|
|
288
|
+
const spacing = Math.max(20, bendAngle);
|
|
289
|
+
const clampedArmCurvature = Math.max(0, Math.min(1, armCurvature));
|
|
290
|
+
const clampedTaper = Math.max(0, Math.min(1, taper));
|
|
291
|
+
const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));
|
|
292
|
+
const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));
|
|
293
|
+
const arm1Len = armLength * (2 / (1 + clampedArmBalance));
|
|
294
|
+
const arm2Len = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));
|
|
295
|
+
const halfSpacing = spacing / 2;
|
|
296
|
+
const halfThick = thickness / 2;
|
|
297
|
+
const tipThick = thickness * clampedTaper / 2;
|
|
298
|
+
const leftOuterX = -halfSpacing - halfThick;
|
|
299
|
+
const leftInnerX = -halfSpacing + halfThick;
|
|
300
|
+
const rightInnerX = halfSpacing - halfThick;
|
|
301
|
+
const rightOuterX = halfSpacing + halfThick;
|
|
302
|
+
const leftTopY = 0;
|
|
303
|
+
const leftBottomY = arm1Len;
|
|
304
|
+
const rightBottomY = arm2Len;
|
|
305
|
+
const rightTopY = 0;
|
|
306
|
+
const bottomY = Math.max(arm1Len, arm2Len);
|
|
307
|
+
const curveDepth = halfSpacing * clampedArmCurvature;
|
|
308
|
+
const pathParts = [];
|
|
309
|
+
const leftTipOuterX = -halfSpacing - tipThick;
|
|
310
|
+
const leftTipInnerX = -halfSpacing + tipThick;
|
|
311
|
+
const rightTipInnerX = halfSpacing - tipThick;
|
|
312
|
+
const rightTipOuterX = halfSpacing + tipThick;
|
|
313
|
+
pathParts.push(`M ${leftTipOuterX.toFixed(2)},${leftTopY.toFixed(2)}`);
|
|
314
|
+
if (clampedTipRoundness > 0) {
|
|
315
|
+
const tipCurve = clampedTipRoundness * thickness * 0.3;
|
|
316
|
+
pathParts.push(`Q ${(-halfSpacing).toFixed(2)},${(leftTopY - tipCurve).toFixed(2)} ${leftTipInnerX.toFixed(2)},${leftTopY.toFixed(2)}`);
|
|
317
|
+
} else {
|
|
318
|
+
pathParts.push(`L ${leftTipInnerX.toFixed(2)},${leftTopY.toFixed(2)}`);
|
|
319
|
+
}
|
|
320
|
+
pathParts.push(`L ${leftInnerX.toFixed(2)},${leftBottomY.toFixed(2)}`);
|
|
321
|
+
if (clampedArmCurvature > 0.1) {
|
|
322
|
+
const innerCtrlY = bottomY + curveDepth;
|
|
323
|
+
pathParts.push(`Q ${0},${innerCtrlY.toFixed(2)} ${rightInnerX.toFixed(2)},${rightBottomY.toFixed(2)}`);
|
|
324
|
+
} else {
|
|
325
|
+
pathParts.push(`L ${rightInnerX.toFixed(2)},${rightBottomY.toFixed(2)}`);
|
|
326
|
+
}
|
|
327
|
+
pathParts.push(`L ${rightTipInnerX.toFixed(2)},${rightTopY.toFixed(2)}`);
|
|
328
|
+
if (clampedTipRoundness > 0) {
|
|
329
|
+
const tipCurve = clampedTipRoundness * thickness * 0.3;
|
|
330
|
+
pathParts.push(`Q ${halfSpacing.toFixed(2)},${(rightTopY - tipCurve).toFixed(2)} ${rightTipOuterX.toFixed(2)},${rightTopY.toFixed(2)}`);
|
|
331
|
+
} else {
|
|
332
|
+
pathParts.push(`L ${rightTipOuterX.toFixed(2)},${rightTopY.toFixed(2)}`);
|
|
333
|
+
}
|
|
334
|
+
pathParts.push(`L ${rightOuterX.toFixed(2)},${rightBottomY.toFixed(2)}`);
|
|
335
|
+
if (clampedArmCurvature > 0.1) {
|
|
336
|
+
const outerCtrlY = bottomY + curveDepth + thickness;
|
|
337
|
+
pathParts.push(`Q ${0},${outerCtrlY.toFixed(2)} ${leftOuterX.toFixed(2)},${leftBottomY.toFixed(2)}`);
|
|
338
|
+
} else {
|
|
339
|
+
pathParts.push(`L ${leftOuterX.toFixed(2)},${leftBottomY.toFixed(2)}`);
|
|
340
|
+
}
|
|
341
|
+
pathParts.push(`L ${leftTipOuterX.toFixed(2)},${leftTopY.toFixed(2)}`);
|
|
342
|
+
pathParts.push("Z");
|
|
343
|
+
let path = pathParts.join(" ");
|
|
344
|
+
const padding = thickness * 0.3;
|
|
345
|
+
let minX = leftOuterX - padding;
|
|
346
|
+
let maxX = rightOuterX + padding;
|
|
347
|
+
let minY = -padding - clampedTipRoundness * thickness * 0.3;
|
|
348
|
+
let maxY = bottomY + curveDepth + thickness + padding;
|
|
349
|
+
const width = maxX - minX;
|
|
350
|
+
const height = maxY - minY;
|
|
351
|
+
path = normalizePath(path, minX, minY);
|
|
352
|
+
if (rotation !== 0) {
|
|
353
|
+
path = rotatePath(path, rotation, width, height);
|
|
354
|
+
}
|
|
355
|
+
return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });
|
|
356
|
+
}
|
|
357
|
+
function generateChevron(params) {
|
|
358
|
+
const {
|
|
359
|
+
armLength,
|
|
360
|
+
// Length of the arms
|
|
361
|
+
bendAngle,
|
|
362
|
+
// Angle of the V (smaller = sharper point)
|
|
363
|
+
thickness,
|
|
364
|
+
// Controls arm width (proportional)
|
|
365
|
+
taper = 0,
|
|
366
|
+
// 0 = V with cutout, 1 = solid triangle (no cutout)
|
|
367
|
+
tipRoundness = 0,
|
|
368
|
+
// Roundness at the tip
|
|
369
|
+
armBalance = 1,
|
|
370
|
+
// Asymmetric arm lengths
|
|
371
|
+
rotation = 0
|
|
372
|
+
} = params;
|
|
373
|
+
const clampedBendAngle = Math.max(20, Math.min(160, bendAngle));
|
|
374
|
+
const clampedTaper = Math.max(0, Math.min(1, taper));
|
|
375
|
+
const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));
|
|
376
|
+
const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));
|
|
377
|
+
const armWidth = Math.max(armLength * 0.15, thickness * 0.5);
|
|
378
|
+
const cutoutDepth = armLength * 0.6 * (1 - clampedTaper);
|
|
379
|
+
const arm1Len = armLength * (2 / (1 + clampedArmBalance));
|
|
380
|
+
const arm2Len = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));
|
|
381
|
+
const halfAngle = clampedBendAngle / 2 * Math.PI / 180;
|
|
382
|
+
const tip = { x: 0, y: 0 };
|
|
383
|
+
const arm1End = { x: -Math.sin(halfAngle) * arm1Len, y: -Math.cos(halfAngle) * arm1Len };
|
|
384
|
+
const arm2End = { x: Math.sin(halfAngle) * arm2Len, y: -Math.cos(halfAngle) * arm2Len };
|
|
385
|
+
const arm1Perp = { x: -Math.cos(halfAngle), y: Math.sin(halfAngle) };
|
|
386
|
+
const arm2Perp = { x: Math.cos(halfAngle), y: Math.sin(halfAngle) };
|
|
387
|
+
const arm1OuterCorner = {
|
|
388
|
+
x: arm1End.x + arm1Perp.x * armWidth / 2,
|
|
389
|
+
y: arm1End.y + arm1Perp.y * armWidth / 2
|
|
390
|
+
};
|
|
391
|
+
const arm1InnerCorner = {
|
|
392
|
+
x: arm1End.x - arm1Perp.x * armWidth / 2,
|
|
393
|
+
y: arm1End.y - arm1Perp.y * armWidth / 2
|
|
394
|
+
};
|
|
395
|
+
const arm2OuterCorner = {
|
|
396
|
+
x: arm2End.x + arm2Perp.x * armWidth / 2,
|
|
397
|
+
y: arm2End.y + arm2Perp.y * armWidth / 2
|
|
398
|
+
};
|
|
399
|
+
const arm2InnerCorner = {
|
|
400
|
+
x: arm2End.x - arm2Perp.x * armWidth / 2,
|
|
401
|
+
y: arm2End.y - arm2Perp.y * armWidth / 2
|
|
402
|
+
};
|
|
403
|
+
const innerTip = {
|
|
404
|
+
x: 0,
|
|
405
|
+
y: -cutoutDepth
|
|
406
|
+
};
|
|
407
|
+
const pathParts = [];
|
|
408
|
+
if (clampedTipRoundness > 0) {
|
|
409
|
+
const roundAmount = clampedTipRoundness * armWidth * 0.3;
|
|
410
|
+
const tipStart = {
|
|
411
|
+
x: tip.x + arm1Perp.x * roundAmount,
|
|
412
|
+
y: tip.y + arm1Perp.y * roundAmount - roundAmount * 0.5
|
|
413
|
+
};
|
|
414
|
+
pathParts.push(`M ${tipStart.x.toFixed(2)},${tipStart.y.toFixed(2)}`);
|
|
415
|
+
const tipEnd = {
|
|
416
|
+
x: tip.x + arm2Perp.x * roundAmount,
|
|
417
|
+
y: tip.y + arm2Perp.y * roundAmount - roundAmount * 0.5
|
|
418
|
+
};
|
|
419
|
+
pathParts.push(`Q ${tip.x.toFixed(2)},${tip.y.toFixed(2)} ${tipEnd.x.toFixed(2)},${tipEnd.y.toFixed(2)}`);
|
|
420
|
+
} else {
|
|
421
|
+
pathParts.push(`M ${tip.x.toFixed(2)},${tip.y.toFixed(2)}`);
|
|
422
|
+
}
|
|
423
|
+
pathParts.push(`L ${arm2OuterCorner.x.toFixed(2)},${arm2OuterCorner.y.toFixed(2)}`);
|
|
424
|
+
pathParts.push(`L ${arm2InnerCorner.x.toFixed(2)},${arm2InnerCorner.y.toFixed(2)}`);
|
|
425
|
+
if (cutoutDepth > 5 && clampedTaper < 0.9) {
|
|
426
|
+
pathParts.push(`L ${innerTip.x.toFixed(2)},${innerTip.y.toFixed(2)}`);
|
|
427
|
+
}
|
|
428
|
+
pathParts.push(`L ${arm1InnerCorner.x.toFixed(2)},${arm1InnerCorner.y.toFixed(2)}`);
|
|
429
|
+
pathParts.push(`L ${arm1OuterCorner.x.toFixed(2)},${arm1OuterCorner.y.toFixed(2)}`);
|
|
430
|
+
pathParts.push("Z");
|
|
431
|
+
let path = pathParts.join(" ");
|
|
432
|
+
const allPoints = [tip, arm1End, arm2End, arm1OuterCorner, arm1InnerCorner, arm2OuterCorner, arm2InnerCorner];
|
|
433
|
+
if (cutoutDepth > 5 && clampedTaper < 0.9) {
|
|
434
|
+
allPoints.push(innerTip);
|
|
435
|
+
}
|
|
436
|
+
let minX = Math.min(...allPoints.map((p) => p.x));
|
|
437
|
+
let maxX = Math.max(...allPoints.map((p) => p.x));
|
|
438
|
+
let minY = Math.min(...allPoints.map((p) => p.y));
|
|
439
|
+
let maxY = Math.max(...allPoints.map((p) => p.y));
|
|
440
|
+
const padding = armWidth * 0.2;
|
|
441
|
+
minX -= padding;
|
|
442
|
+
maxX += padding;
|
|
443
|
+
minY -= padding;
|
|
444
|
+
maxY += padding;
|
|
445
|
+
const width = maxX - minX;
|
|
446
|
+
const height = maxY - minY;
|
|
447
|
+
path = normalizePath(path, minX, minY);
|
|
448
|
+
if (rotation !== 0) {
|
|
449
|
+
path = rotatePath(path, rotation, width, height);
|
|
450
|
+
}
|
|
451
|
+
return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });
|
|
452
|
+
}
|
|
453
|
+
function generateArmSpine(start, end, direction, length, curvature, numSamples) {
|
|
454
|
+
const points = [];
|
|
455
|
+
const perpDir = { x: direction.y, y: -direction.x };
|
|
456
|
+
const bowAmount = length * 0.4 * curvature;
|
|
457
|
+
const midPoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
458
|
+
const controlPoint = {
|
|
459
|
+
x: midPoint.x + perpDir.x * bowAmount,
|
|
460
|
+
y: midPoint.y + perpDir.y * bowAmount
|
|
461
|
+
};
|
|
462
|
+
for (let i = 0; i <= numSamples; i++) {
|
|
463
|
+
const t = i / numSamples;
|
|
464
|
+
if (curvature < 0.1) {
|
|
465
|
+
points.push({
|
|
466
|
+
x: start.x + (end.x - start.x) * t,
|
|
467
|
+
y: start.y + (end.y - start.y) * t
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
const mt = 1 - t;
|
|
471
|
+
points.push({
|
|
472
|
+
x: mt * mt * start.x + 2 * mt * t * controlPoint.x + t * t * end.x,
|
|
473
|
+
y: mt * mt * start.y + 2 * mt * t * controlPoint.y + t * t * end.y
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return points;
|
|
478
|
+
}
|
|
479
|
+
function getSpineTangent(spine, index) {
|
|
480
|
+
const prev = spine[Math.max(0, index - 1)];
|
|
481
|
+
const next = spine[Math.min(spine.length - 1, index + 1)];
|
|
482
|
+
const dx = next.x - prev.x;
|
|
483
|
+
const dy = next.y - prev.y;
|
|
484
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
485
|
+
return len > 0 ? { x: dx / len, y: dy / len } : { x: 1, y: 0 };
|
|
486
|
+
}
|
|
487
|
+
function smoothCurveThroughPoints(points) {
|
|
488
|
+
if (points.length < 2) return "";
|
|
489
|
+
const parts = [];
|
|
490
|
+
const tension = 0.3;
|
|
491
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
492
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
493
|
+
const p1 = points[i];
|
|
494
|
+
const p2 = points[i + 1];
|
|
495
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
496
|
+
const cp1x = p1.x + (p2.x - p0.x) * tension;
|
|
497
|
+
const cp1y = p1.y + (p2.y - p0.y) * tension;
|
|
498
|
+
const cp2x = p2.x - (p3.x - p1.x) * tension;
|
|
499
|
+
const cp2y = p2.y - (p3.y - p1.y) * tension;
|
|
500
|
+
parts.push(`C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`);
|
|
501
|
+
}
|
|
502
|
+
return parts.join(" ");
|
|
503
|
+
}
|
|
504
|
+
function normalizePath(path, minX, minY) {
|
|
505
|
+
return path.replace(
|
|
506
|
+
/([-\d.]+),([-\d.]+)/g,
|
|
507
|
+
(_, x, y) => `${(parseFloat(x) - minX).toFixed(2)},${(parseFloat(y) - minY).toFixed(2)}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
function rotatePath(path, rotation, width, height) {
|
|
511
|
+
const rad = rotation * Math.PI / 180;
|
|
512
|
+
const cos = Math.cos(rad);
|
|
513
|
+
const sin = Math.sin(rad);
|
|
514
|
+
const cx = width / 2;
|
|
515
|
+
const cy = height / 2;
|
|
516
|
+
return path.replace(
|
|
517
|
+
/([-\d.]+),([-\d.]+)/g,
|
|
518
|
+
(_, xStr, yStr) => {
|
|
519
|
+
const x = parseFloat(xStr) - cx;
|
|
520
|
+
const y = parseFloat(yStr) - cy;
|
|
521
|
+
const rx = x * cos - y * sin + cx;
|
|
522
|
+
const ry = x * sin + y * cos + cy;
|
|
523
|
+
return `${rx.toFixed(2)},${ry.toFixed(2)}`;
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export {
|
|
529
|
+
boomerang
|
|
530
|
+
};
|
|
531
|
+
//# sourceMappingURL=chunk-IAFZ3FNR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generators/utils/svg.ts","../src/presets/boomerang.ts","../src/generators/curved/boomerang.ts"],"sourcesContent":["// src/generators/utils/svg.ts\n\ninterface BaseResult {\n path: string;\n width: number;\n height: number;\n centerX: number;\n centerY: number;\n}\n\nexport function wrapPath(path: string, width: number, height: number): string {\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${width} ${height}\" width=\"${width}\" height=\"${height}\"><path d=\"${path}\" fill=\"currentColor\"/></svg>`;\n}\n\nexport function addSvgFields<T extends BaseResult>(result: T): T & { viewBox: string; svg: string } {\n const viewBox = `0 0 ${result.width} ${result.height}`;\n const svg = wrapPath(result.path, result.width, result.height);\n return { ...result, viewBox, svg };\n}\n","import type { BoomerangParams } from '../generators/curved/boomerang';\n\nexport const crescent: BoomerangParams = {\n style: 'crescent',\n armLength: 50,\n bendAngle: 160,\n thickness: 35,\n taper: 0.5,\n tipRoundness: 0.2,\n armCurvature: 0,\n bendSharpness: 0,\n};\n\nexport const fatMoon: BoomerangParams = {\n style: 'crescent',\n armLength: 45,\n bendAngle: 180,\n thickness: 50,\n taper: 0.3,\n tipRoundness: 0.6,\n armCurvature: 0,\n bendSharpness: 0,\n};\n\nexport const sharpChevron: BoomerangParams = {\n style: 'chevron',\n armLength: 55,\n bendAngle: 60,\n thickness: 40,\n taper: 1,\n tipRoundness: 0,\n armCurvature: 0,\n bendSharpness: 0,\n};\n\nexport const thinArc: BoomerangParams = {\n style: 'crescent',\n armLength: 60,\n bendAngle: 140,\n thickness: 20,\n taper: 0.7,\n tipRoundness: 0.1,\n armCurvature: 0,\n bendSharpness: 0,\n};\n\nexport const horseshoe: BoomerangParams = {\n style: 'horseshoe',\n armLength: 80,\n bendAngle: 50,\n thickness: 18,\n taper: 0.2,\n tipRoundness: 0.3,\n armCurvature: 0.4,\n bendSharpness: 0,\n};\n","import { GeneratorResult } from '../types';\nimport { addSvgFields } from '../utils/svg';\nimport * as presets from '../../presets/boomerang';\n\nexport type BoomerangStyle = 'classic' | 'crescent' | 'horseshoe' | 'chevron';\n\nexport interface BoomerangParams {\n // Style selection\n style?: BoomerangStyle; // 'classic' (outline V), 'crescent' (arc), 'horseshoe' (U), 'chevron' (solid V)\n\n // Core shape - interpreted based on style\n armLength: number; // Length of arms (classic/horseshoe) or arc radius (crescent)\n bendAngle: number; // Angle between arms (classic), arc span (crescent), arm spacing (horseshoe)\n armCurvature: number; // How curved: 0-1 (classic: arm bow, crescent: unused, horseshoe: bottom curve)\n bendSharpness: number; // Bend smoothness: 0-1\n\n // Thickness\n thickness: number; // Max thickness at widest point\n taper: number; // Tip thickness ratio: 0-1 (0 = pointed, 1 = uniform)\n tipRoundness: number; // How rounded the tips are: 0-1\n\n // Asymmetry (optional)\n armBalance?: number; // Ratio of arm1/arm2 length: 0.5-2 (default 1)\n thicknessBalance?: number; // Thickness bias: -1 to 1 (default 0)\n\n // Orientation\n rotation?: number; // Rotation in degrees (default 0)\n}\n\n/**\n * Boomerang function with preset methods attached.\n */\ninterface BoomerangFunction {\n (params: BoomerangParams): GeneratorResult;\n crescent: (overrides?: Partial<BoomerangParams>) => GeneratorResult;\n fatMoon: (overrides?: Partial<BoomerangParams>) => GeneratorResult;\n sharpChevron: (overrides?: Partial<BoomerangParams>) => GeneratorResult;\n thinArc: (overrides?: Partial<BoomerangParams>) => GeneratorResult;\n horseshoe: (overrides?: Partial<BoomerangParams>) => GeneratorResult;\n}\n\n/**\n * Unified boomerang generator with multiple geometry modes:\n * - 'classic': Two arms diverging from a central bend (V-shape, traditional boomerang)\n * - 'crescent': Single curved arc with thickness (moon-like, thick C-curve)\n * - 'horseshoe': Two parallel arms connected by curved bottom (U-shape)\n */\nfunction boomerangBase(params: BoomerangParams): GeneratorResult {\n const style = params.style || 'classic';\n\n switch (style) {\n case 'crescent':\n return generateCrescent(params);\n case 'horseshoe':\n return generateHorseshoe(params);\n case 'chevron':\n return generateChevron(params);\n case 'classic':\n default:\n return generateClassic(params);\n }\n}\n\nexport const boomerang: BoomerangFunction = Object.assign(boomerangBase, {\n crescent: (overrides?: Partial<BoomerangParams>) =>\n boomerangBase({ ...presets.crescent, ...overrides }),\n fatMoon: (overrides?: Partial<BoomerangParams>) =>\n boomerangBase({ ...presets.fatMoon, ...overrides }),\n sharpChevron: (overrides?: Partial<BoomerangParams>) =>\n boomerangBase({ ...presets.sharpChevron, ...overrides }),\n thinArc: (overrides?: Partial<BoomerangParams>) =>\n boomerangBase({ ...presets.thinArc, ...overrides }),\n horseshoe: (overrides?: Partial<BoomerangParams>) =>\n boomerangBase({ ...presets.horseshoe, ...overrides }),\n});\n\n// ============================================================================\n// CLASSIC STYLE - Two arms diverging from bend point (V-shape)\n// ============================================================================\n\nfunction generateClassic(params: BoomerangParams): GeneratorResult {\n const {\n armLength,\n bendAngle,\n armCurvature,\n bendSharpness,\n thickness,\n taper,\n tipRoundness,\n armBalance = 1,\n thicknessBalance = 0,\n rotation = 0,\n } = params;\n\n const clampedBendAngle = Math.max(30, Math.min(180, bendAngle));\n const clampedArmCurvature = Math.max(0, Math.min(1, armCurvature));\n const clampedBendSharpness = Math.max(0, Math.min(1, bendSharpness));\n const clampedTaper = Math.max(0, Math.min(1, taper));\n const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));\n const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));\n const clampedThicknessBalance = Math.max(-1, Math.min(1, thicknessBalance));\n\n const arm1Length = armLength * (2 / (1 + clampedArmBalance));\n const arm2Length = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));\n\n const halfAngle = (clampedBendAngle / 2) * Math.PI / 180;\n\n const arm1Dir = { x: -Math.sin(halfAngle), y: -Math.cos(halfAngle) };\n const arm2Dir = { x: Math.sin(halfAngle), y: -Math.cos(halfAngle) };\n\n const tip1 = { x: arm1Dir.x * arm1Length, y: arm1Dir.y * arm1Length };\n const tip2 = { x: arm2Dir.x * arm2Length, y: arm2Dir.y * arm2Length };\n const bendPoint = { x: 0, y: 0 };\n\n const numSamples = 20;\n\n const arm1Spine = generateArmSpine(tip1, bendPoint, arm1Dir, arm1Length, clampedArmCurvature, numSamples);\n const arm2Spine = generateArmSpine(bendPoint, tip2, arm2Dir, arm2Length, clampedArmCurvature, numSamples);\n\n const minThickness = thickness * clampedTaper;\n\n function getThicknessAlongArm(t: number, isArm1: boolean): number {\n const tipFade = Math.sin(t * Math.PI / 2);\n const baseThickness = minThickness + (thickness - minThickness) * tipFade;\n const balanceFactor = isArm1\n ? 1 - clampedThicknessBalance * 0.3\n : 1 + clampedThicknessBalance * 0.3;\n return baseThickness * balanceFactor;\n }\n\n const arm1Outer: Point[] = [];\n const arm1Inner: Point[] = [];\n const arm2Outer: Point[] = [];\n const arm2Inner: Point[] = [];\n\n for (let i = 0; i < arm1Spine.length; i++) {\n const t = i / (arm1Spine.length - 1);\n const point = arm1Spine[i];\n const tangent = getSpineTangent(arm1Spine, i);\n const normal = { x: -tangent.y, y: tangent.x };\n const halfThick = getThicknessAlongArm(t, true) / 2;\n\n arm1Outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });\n arm1Inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });\n }\n\n for (let i = 0; i < arm2Spine.length; i++) {\n const t = 1 - i / (arm2Spine.length - 1);\n const point = arm2Spine[i];\n const tangent = getSpineTangent(arm2Spine, i);\n const normal = { x: -tangent.y, y: tangent.x };\n const halfThick = getThicknessAlongArm(t, false) / 2;\n\n arm2Outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });\n arm2Inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });\n }\n\n const pathParts: string[] = [];\n\n pathParts.push(`M ${arm1Outer[0].x.toFixed(2)},${arm1Outer[0].y.toFixed(2)}`);\n\n const tip1Curve = clampedTipRoundness * thickness * 0.3;\n pathParts.push(`Q ${(tip1.x + arm1Dir.x * tip1Curve).toFixed(2)},${(tip1.y + arm1Dir.y * tip1Curve).toFixed(2)} ${arm1Inner[0].x.toFixed(2)},${arm1Inner[0].y.toFixed(2)}`);\n\n pathParts.push(smoothCurveThroughPoints(arm1Inner));\n\n const bendInner2 = arm2Inner[0];\n\n if (clampedBendSharpness < 0.3) {\n pathParts.push(`L ${bendInner2.x.toFixed(2)},${bendInner2.y.toFixed(2)}`);\n } else {\n const bendInnerOffset = thickness * 0.2 * clampedBendSharpness;\n const bendInnerCtrl = { x: 0, y: bendInnerOffset };\n pathParts.push(`Q ${bendInnerCtrl.x.toFixed(2)},${bendInnerCtrl.y.toFixed(2)} ${bendInner2.x.toFixed(2)},${bendInner2.y.toFixed(2)}`);\n }\n\n pathParts.push(smoothCurveThroughPoints(arm2Inner));\n\n const tip2Curve = clampedTipRoundness * thickness * 0.3;\n pathParts.push(`Q ${(tip2.x + arm2Dir.x * tip2Curve).toFixed(2)},${(tip2.y + arm2Dir.y * tip2Curve).toFixed(2)} ${arm2Outer[arm2Outer.length - 1].x.toFixed(2)},${arm2Outer[arm2Outer.length - 1].y.toFixed(2)}`);\n\n const arm2OuterReversed = [...arm2Outer].reverse();\n pathParts.push(smoothCurveThroughPoints(arm2OuterReversed));\n\n const bendOuter1 = arm1Outer[arm1Outer.length - 1];\n\n if (clampedBendSharpness < 0.3) {\n pathParts.push(`L ${bendOuter1.x.toFixed(2)},${bendOuter1.y.toFixed(2)}`);\n } else {\n const bendOuterOffset = -thickness * 0.4 * clampedBendSharpness;\n const bendOuterCtrl = { x: 0, y: bendOuterOffset };\n pathParts.push(`Q ${bendOuterCtrl.x.toFixed(2)},${bendOuterCtrl.y.toFixed(2)} ${bendOuter1.x.toFixed(2)},${bendOuter1.y.toFixed(2)}`);\n }\n\n const arm1OuterReversed = [...arm1Outer].reverse();\n pathParts.push(smoothCurveThroughPoints(arm1OuterReversed));\n\n pathParts.push('Z');\n\n let path = pathParts.join(' ');\n\n const allPoints = [...arm1Outer, ...arm1Inner, ...arm2Outer, ...arm2Inner];\n let minX = Math.min(...allPoints.map(p => p.x));\n let maxX = Math.max(...allPoints.map(p => p.x));\n let minY = Math.min(...allPoints.map(p => p.y));\n let maxY = Math.max(...allPoints.map(p => p.y));\n\n const padding = thickness * 0.3 * clampedTipRoundness;\n minX -= padding;\n maxX += padding;\n minY -= padding;\n\n const width = maxX - minX;\n const height = maxY - minY;\n\n path = normalizePath(path, minX, minY);\n\n if (rotation !== 0) {\n path = rotatePath(path, rotation, width, height);\n }\n\n return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });\n}\n\n// ============================================================================\n// CRESCENT STYLE - Single curved arc with thickness (moon-like shape)\n// ============================================================================\n\nfunction generateCrescent(params: BoomerangParams): GeneratorResult {\n const {\n armLength, // Used as arc radius\n bendAngle, // Used as arc span (degrees, 30-300)\n thickness,\n taper,\n tipRoundness,\n armBalance = 1, // Asymmetric arc thickness along length\n rotation = 0,\n } = params;\n\n const radius = Math.max(20, armLength);\n const arcSpan = Math.max(30, Math.min(300, bendAngle)) * Math.PI / 180;\n const clampedTaper = Math.max(0, Math.min(1, taper));\n const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));\n const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));\n\n const numSamples = 30;\n const startAngle = -arcSpan / 2;\n const endAngle = arcSpan / 2;\n\n // Generate spine points along the arc\n const spine: Point[] = [];\n for (let i = 0; i <= numSamples; i++) {\n const t = i / numSamples;\n const angle = startAngle + (endAngle - startAngle) * t;\n spine.push({\n x: Math.sin(angle) * radius,\n y: -Math.cos(angle) * radius + radius, // Shift so bottom of arc is at y=0\n });\n }\n\n // Calculate thickness at each point\n const minThickness = thickness * clampedTaper;\n\n function getThicknessAtT(t: number): number {\n // Taper at both ends, thickest in middle\n // Apply balance (shift where thickness is max)\n const balanceShift = (t - 0.5) * (clampedArmBalance - 1) * 0.5;\n const adjustedT = Math.max(0, Math.min(1, t + balanceShift));\n const adjustedDist = Math.min(adjustedT, 1 - adjustedT) * 2;\n const adjustedFactor = Math.sin(adjustedDist * Math.PI / 2);\n\n return minThickness + (thickness - minThickness) * adjustedFactor;\n }\n\n // Build inner and outer edges\n const outer: Point[] = [];\n const inner: Point[] = [];\n\n for (let i = 0; i < spine.length; i++) {\n const t = i / (spine.length - 1);\n const point = spine[i];\n const tangent = getSpineTangent(spine, i);\n const normal = { x: -tangent.y, y: tangent.x };\n const halfThick = getThicknessAtT(t) / 2;\n\n outer.push({ x: point.x + normal.x * halfThick, y: point.y + normal.y * halfThick });\n inner.push({ x: point.x - normal.x * halfThick, y: point.y - normal.y * halfThick });\n }\n\n // Build path\n const pathParts: string[] = [];\n\n // Start at first outer point\n pathParts.push(`M ${outer[0].x.toFixed(2)},${outer[0].y.toFixed(2)}`);\n\n // Rounded start tip\n const startTipCurve = clampedTipRoundness * thickness * 0.4;\n const startTangent = getSpineTangent(spine, 0);\n pathParts.push(`Q ${(spine[0].x - startTangent.x * startTipCurve).toFixed(2)},${(spine[0].y - startTangent.y * startTipCurve).toFixed(2)} ${inner[0].x.toFixed(2)},${inner[0].y.toFixed(2)}`);\n\n // Inner edge (start to end)\n pathParts.push(smoothCurveThroughPoints(inner));\n\n // Rounded end tip\n const endTipCurve = clampedTipRoundness * thickness * 0.4;\n const endTangent = getSpineTangent(spine, spine.length - 1);\n pathParts.push(`Q ${(spine[spine.length - 1].x + endTangent.x * endTipCurve).toFixed(2)},${(spine[spine.length - 1].y + endTangent.y * endTipCurve).toFixed(2)} ${outer[outer.length - 1].x.toFixed(2)},${outer[outer.length - 1].y.toFixed(2)}`);\n\n // Outer edge (end back to start)\n const outerReversed = [...outer].reverse();\n pathParts.push(smoothCurveThroughPoints(outerReversed));\n\n pathParts.push('Z');\n\n let path = pathParts.join(' ');\n\n // Calculate bounds\n const allPoints = [...outer, ...inner];\n let minX = Math.min(...allPoints.map(p => p.x));\n let maxX = Math.max(...allPoints.map(p => p.x));\n let minY = Math.min(...allPoints.map(p => p.y));\n let maxY = Math.max(...allPoints.map(p => p.y));\n\n const padding = thickness * 0.3 * clampedTipRoundness;\n minX -= padding;\n maxX += padding;\n minY -= padding;\n maxY += padding;\n\n const width = maxX - minX;\n const height = maxY - minY;\n\n path = normalizePath(path, minX, minY);\n\n if (rotation !== 0) {\n path = rotatePath(path, rotation, width, height);\n }\n\n return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });\n}\n\n// ============================================================================\n// HORSESHOE STYLE - Two parallel arms connected by curved bottom (U-shape)\n// ============================================================================\n\nfunction generateHorseshoe(params: BoomerangParams): GeneratorResult {\n const {\n armLength, // Length of the vertical arms\n bendAngle, // Spacing between arms (width at top)\n armCurvature, // How curved the bottom connection is\n thickness,\n taper,\n tipRoundness,\n armBalance = 1, // Asymmetric arm lengths\n rotation = 0,\n } = params;\n\n const spacing = Math.max(20, bendAngle);\n const clampedArmCurvature = Math.max(0, Math.min(1, armCurvature));\n const clampedTaper = Math.max(0, Math.min(1, taper));\n const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));\n const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));\n\n const arm1Len = armLength * (2 / (1 + clampedArmBalance));\n const arm2Len = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));\n\n const halfSpacing = spacing / 2;\n const halfThick = thickness / 2;\n const tipThick = thickness * clampedTaper / 2;\n\n // Use explicit geometry for straight parallel arms\n // Left arm outer/inner x-coordinates\n const leftOuterX = -halfSpacing - halfThick;\n const leftInnerX = -halfSpacing + halfThick;\n // Right arm outer/inner x-coordinates\n const rightInnerX = halfSpacing - halfThick;\n const rightOuterX = halfSpacing + halfThick;\n\n // Arm y-coordinates (with taper at tips)\n const leftTopY = 0;\n const leftBottomY = arm1Len;\n const rightBottomY = arm2Len;\n const rightTopY = 0;\n\n // Bottom curve parameters\n const bottomY = Math.max(arm1Len, arm2Len);\n const curveDepth = halfSpacing * clampedArmCurvature;\n\n // Build path directly with explicit coordinates\n const pathParts: string[] = [];\n\n // Start at left arm outer top (with tip width adjustment)\n const leftTipOuterX = -halfSpacing - tipThick;\n const leftTipInnerX = -halfSpacing + tipThick;\n const rightTipInnerX = halfSpacing - tipThick;\n const rightTipOuterX = halfSpacing + tipThick;\n\n pathParts.push(`M ${leftTipOuterX.toFixed(2)},${leftTopY.toFixed(2)}`);\n\n // Rounded left tip\n if (clampedTipRoundness > 0) {\n const tipCurve = clampedTipRoundness * thickness * 0.3;\n pathParts.push(`Q ${(-halfSpacing).toFixed(2)},${(leftTopY - tipCurve).toFixed(2)} ${leftTipInnerX.toFixed(2)},${leftTopY.toFixed(2)}`);\n } else {\n pathParts.push(`L ${leftTipInnerX.toFixed(2)},${leftTopY.toFixed(2)}`);\n }\n\n // Inner left arm - straight down\n pathParts.push(`L ${leftInnerX.toFixed(2)},${leftBottomY.toFixed(2)}`);\n\n // Inner bottom curve (from left to right)\n if (clampedArmCurvature > 0.1) {\n // Curved bottom using quadratic bezier\n const innerCtrlY = bottomY + curveDepth;\n pathParts.push(`Q ${0},${innerCtrlY.toFixed(2)} ${rightInnerX.toFixed(2)},${rightBottomY.toFixed(2)}`);\n } else {\n // Flat bottom\n pathParts.push(`L ${rightInnerX.toFixed(2)},${rightBottomY.toFixed(2)}`);\n }\n\n // Inner right arm - straight up\n pathParts.push(`L ${rightTipInnerX.toFixed(2)},${rightTopY.toFixed(2)}`);\n\n // Rounded right tip\n if (clampedTipRoundness > 0) {\n const tipCurve = clampedTipRoundness * thickness * 0.3;\n pathParts.push(`Q ${(halfSpacing).toFixed(2)},${(rightTopY - tipCurve).toFixed(2)} ${rightTipOuterX.toFixed(2)},${rightTopY.toFixed(2)}`);\n } else {\n pathParts.push(`L ${rightTipOuterX.toFixed(2)},${rightTopY.toFixed(2)}`);\n }\n\n // Outer right arm - straight down\n pathParts.push(`L ${rightOuterX.toFixed(2)},${rightBottomY.toFixed(2)}`);\n\n // Outer bottom curve (from right to left)\n if (clampedArmCurvature > 0.1) {\n const outerCtrlY = bottomY + curveDepth + thickness;\n pathParts.push(`Q ${0},${outerCtrlY.toFixed(2)} ${leftOuterX.toFixed(2)},${leftBottomY.toFixed(2)}`);\n } else {\n pathParts.push(`L ${leftOuterX.toFixed(2)},${leftBottomY.toFixed(2)}`);\n }\n\n // Outer left arm - straight up back to start\n pathParts.push(`L ${leftTipOuterX.toFixed(2)},${leftTopY.toFixed(2)}`);\n\n pathParts.push('Z');\n\n let path = pathParts.join(' ');\n\n // Calculate bounds from explicit geometry\n const padding = thickness * 0.3;\n let minX = leftOuterX - padding;\n let maxX = rightOuterX + padding;\n let minY = -padding - (clampedTipRoundness * thickness * 0.3);\n let maxY = bottomY + curveDepth + thickness + padding;\n\n const width = maxX - minX;\n const height = maxY - minY;\n\n path = normalizePath(path, minX, minY);\n\n if (rotation !== 0) {\n path = rotatePath(path, rotation, width, height);\n }\n\n return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });\n}\n\n// ============================================================================\n// CHEVRON STYLE - Solid V-shape with inner cutout (like < or > arrows)\n// ============================================================================\n\nfunction generateChevron(params: BoomerangParams): GeneratorResult {\n const {\n armLength, // Length of the arms\n bendAngle, // Angle of the V (smaller = sharper point)\n thickness, // Controls arm width (proportional)\n taper = 0, // 0 = V with cutout, 1 = solid triangle (no cutout)\n tipRoundness = 0, // Roundness at the tip\n armBalance = 1, // Asymmetric arm lengths\n rotation = 0,\n } = params;\n\n const clampedBendAngle = Math.max(20, Math.min(160, bendAngle));\n const clampedTaper = Math.max(0, Math.min(1, taper));\n const clampedTipRoundness = Math.max(0, Math.min(1, tipRoundness));\n const clampedArmBalance = Math.max(0.5, Math.min(2, armBalance));\n\n // Arm width based on thickness, with minimum for visibility\n const armWidth = Math.max(armLength * 0.15, thickness * 0.5);\n\n // Cutout depth: taper controls this. High taper = no cutout (solid triangle)\n // taper=0: full cutout based on armLength\n // taper=1: no cutout (solid triangle)\n const cutoutDepth = armLength * 0.6 * (1 - clampedTaper);\n\n // Calculate arm lengths\n const arm1Len = armLength * (2 / (1 + clampedArmBalance));\n const arm2Len = armLength * (2 * clampedArmBalance / (1 + clampedArmBalance));\n\n const halfAngle = (clampedBendAngle / 2) * Math.PI / 180;\n\n // Outer points (tip and arm ends)\n const tip = { x: 0, y: 0 };\n const arm1End = { x: -Math.sin(halfAngle) * arm1Len, y: -Math.cos(halfAngle) * arm1Len };\n const arm2End = { x: Math.sin(halfAngle) * arm2Len, y: -Math.cos(halfAngle) * arm2Len };\n\n // Perpendicular directions for arm widths\n const arm1Perp = { x: -Math.cos(halfAngle), y: Math.sin(halfAngle) };\n const arm2Perp = { x: Math.cos(halfAngle), y: Math.sin(halfAngle) };\n\n // Outer corners at arm ends\n const arm1OuterCorner = {\n x: arm1End.x + arm1Perp.x * armWidth / 2,\n y: arm1End.y + arm1Perp.y * armWidth / 2\n };\n const arm1InnerCorner = {\n x: arm1End.x - arm1Perp.x * armWidth / 2,\n y: arm1End.y - arm1Perp.y * armWidth / 2\n };\n const arm2OuterCorner = {\n x: arm2End.x + arm2Perp.x * armWidth / 2,\n y: arm2End.y + arm2Perp.y * armWidth / 2\n };\n const arm2InnerCorner = {\n x: arm2End.x - arm2Perp.x * armWidth / 2,\n y: arm2End.y - arm2Perp.y * armWidth / 2\n };\n\n // Inner cutout point\n const innerTip = {\n x: 0,\n y: -cutoutDepth\n };\n\n // Build path\n const pathParts: string[] = [];\n\n // Start at outer tip\n if (clampedTipRoundness > 0) {\n const roundAmount = clampedTipRoundness * armWidth * 0.3;\n const tipStart = {\n x: tip.x + arm1Perp.x * roundAmount,\n y: tip.y + arm1Perp.y * roundAmount - roundAmount * 0.5\n };\n pathParts.push(`M ${tipStart.x.toFixed(2)},${tipStart.y.toFixed(2)}`);\n const tipEnd = {\n x: tip.x + arm2Perp.x * roundAmount,\n y: tip.y + arm2Perp.y * roundAmount - roundAmount * 0.5\n };\n pathParts.push(`Q ${tip.x.toFixed(2)},${tip.y.toFixed(2)} ${tipEnd.x.toFixed(2)},${tipEnd.y.toFixed(2)}`);\n } else {\n pathParts.push(`M ${tip.x.toFixed(2)},${tip.y.toFixed(2)}`);\n }\n\n // Outer edge to arm2 end\n pathParts.push(`L ${arm2OuterCorner.x.toFixed(2)},${arm2OuterCorner.y.toFixed(2)}`);\n\n // Top of arm2\n pathParts.push(`L ${arm2InnerCorner.x.toFixed(2)},${arm2InnerCorner.y.toFixed(2)}`);\n\n // Inner edge back to center (the cutout) - only if taper < 0.9\n if (cutoutDepth > 5 && clampedTaper < 0.9) {\n pathParts.push(`L ${innerTip.x.toFixed(2)},${innerTip.y.toFixed(2)}`);\n }\n\n // Inner edge to arm1\n pathParts.push(`L ${arm1InnerCorner.x.toFixed(2)},${arm1InnerCorner.y.toFixed(2)}`);\n\n // Top of arm1\n pathParts.push(`L ${arm1OuterCorner.x.toFixed(2)},${arm1OuterCorner.y.toFixed(2)}`);\n\n // Close back to tip\n pathParts.push('Z');\n\n let path = pathParts.join(' ');\n\n // Calculate bounds\n const allPoints = [tip, arm1End, arm2End, arm1OuterCorner, arm1InnerCorner, arm2OuterCorner, arm2InnerCorner];\n if (cutoutDepth > 5 && clampedTaper < 0.9) {\n allPoints.push(innerTip);\n }\n let minX = Math.min(...allPoints.map(p => p.x));\n let maxX = Math.max(...allPoints.map(p => p.x));\n let minY = Math.min(...allPoints.map(p => p.y));\n let maxY = Math.max(...allPoints.map(p => p.y));\n\n const padding = armWidth * 0.2;\n minX -= padding;\n maxX += padding;\n minY -= padding;\n maxY += padding;\n\n const width = maxX - minX;\n const height = maxY - minY;\n\n path = normalizePath(path, minX, minY);\n\n if (rotation !== 0) {\n path = rotatePath(path, rotation, width, height);\n }\n\n return addSvgFields({ path, width, height, centerX: width / 2, centerY: height / 2 });\n}\n\n// ============================================================================\n// Helper types and functions\n// ============================================================================\n\ninterface Point {\n x: number;\n y: number;\n}\n\nfunction generateArmSpine(\n start: Point,\n end: Point,\n direction: Point,\n length: number,\n curvature: number,\n numSamples: number\n): Point[] {\n const points: Point[] = [];\n const perpDir = { x: direction.y, y: -direction.x };\n const bowAmount = length * 0.4 * curvature;\n const midPoint = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };\n const controlPoint = {\n x: midPoint.x + perpDir.x * bowAmount,\n y: midPoint.y + perpDir.y * bowAmount\n };\n\n for (let i = 0; i <= numSamples; i++) {\n const t = i / numSamples;\n if (curvature < 0.1) {\n points.push({\n x: start.x + (end.x - start.x) * t,\n y: start.y + (end.y - start.y) * t,\n });\n } else {\n const mt = 1 - t;\n points.push({\n x: mt * mt * start.x + 2 * mt * t * controlPoint.x + t * t * end.x,\n y: mt * mt * start.y + 2 * mt * t * controlPoint.y + t * t * end.y,\n });\n }\n }\n\n return points;\n}\n\nfunction getSpineTangent(spine: Point[], index: number): Point {\n const prev = spine[Math.max(0, index - 1)];\n const next = spine[Math.min(spine.length - 1, index + 1)];\n const dx = next.x - prev.x;\n const dy = next.y - prev.y;\n const len = Math.sqrt(dx * dx + dy * dy);\n return len > 0 ? { x: dx / len, y: dy / len } : { x: 1, y: 0 };\n}\n\nfunction smoothCurveThroughPoints(points: Point[]): string {\n if (points.length < 2) return '';\n\n const parts: string[] = [];\n const tension = 0.3;\n\n for (let i = 0; i < points.length - 1; i++) {\n const p0 = points[Math.max(0, i - 1)];\n const p1 = points[i];\n const p2 = points[i + 1];\n const p3 = points[Math.min(points.length - 1, i + 2)];\n\n const cp1x = p1.x + (p2.x - p0.x) * tension;\n const cp1y = p1.y + (p2.y - p0.y) * tension;\n const cp2x = p2.x - (p3.x - p1.x) * tension;\n const cp2y = p2.y - (p3.y - p1.y) * tension;\n\n parts.push(`C ${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`);\n }\n\n return parts.join(' ');\n}\n\nfunction normalizePath(path: string, minX: number, minY: number): string {\n return path.replace(\n /([-\\d.]+),([-\\d.]+)/g,\n (_, x, y) => `${(parseFloat(x) - minX).toFixed(2)},${(parseFloat(y) - minY).toFixed(2)}`\n );\n}\n\nfunction rotatePath(path: string, rotation: number, width: number, height: number): string {\n const rad = (rotation * Math.PI) / 180;\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const cx = width / 2;\n const cy = height / 2;\n\n return path.replace(\n /([-\\d.]+),([-\\d.]+)/g,\n (_, xStr, yStr) => {\n const x = parseFloat(xStr) - cx;\n const y = parseFloat(yStr) - cy;\n const rx = x * cos - y * sin + cx;\n const ry = x * sin + y * cos + cy;\n return `${rx.toFixed(2)},${ry.toFixed(2)}`;\n }\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface DingbatResult {
|
|
2
|
+
path: string;
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
centerX: number;
|
|
6
|
+
centerY: number;
|
|
7
|
+
viewBox: string;
|
|
8
|
+
svg: string;
|
|
9
|
+
}
|
|
10
|
+
type GeneratorResult = DingbatResult;
|
|
11
|
+
|
|
12
|
+
type BoomerangStyle = 'classic' | 'crescent' | 'horseshoe' | 'chevron';
|
|
13
|
+
interface BoomerangParams {
|
|
14
|
+
style?: BoomerangStyle;
|
|
15
|
+
armLength: number;
|
|
16
|
+
bendAngle: number;
|
|
17
|
+
armCurvature: number;
|
|
18
|
+
bendSharpness: number;
|
|
19
|
+
thickness: number;
|
|
20
|
+
taper: number;
|
|
21
|
+
tipRoundness: number;
|
|
22
|
+
armBalance?: number;
|
|
23
|
+
thicknessBalance?: number;
|
|
24
|
+
rotation?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Boomerang function with preset methods attached.
|
|
28
|
+
*/
|
|
29
|
+
interface BoomerangFunction {
|
|
30
|
+
(params: BoomerangParams): GeneratorResult;
|
|
31
|
+
crescent: (overrides?: Partial<BoomerangParams>) => GeneratorResult;
|
|
32
|
+
fatMoon: (overrides?: Partial<BoomerangParams>) => GeneratorResult;
|
|
33
|
+
sharpChevron: (overrides?: Partial<BoomerangParams>) => GeneratorResult;
|
|
34
|
+
thinArc: (overrides?: Partial<BoomerangParams>) => GeneratorResult;
|
|
35
|
+
horseshoe: (overrides?: Partial<BoomerangParams>) => GeneratorResult;
|
|
36
|
+
}
|
|
37
|
+
declare const boomerang: BoomerangFunction;
|
|
38
|
+
|
|
39
|
+
export { type BoomerangParams, type BoomerangStyle, type DingbatResult, type GeneratorResult, boomerang };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
import { DingbatResult, BoomerangParams } from '../index.js';
|
|
4
|
+
|
|
5
|
+
interface DingbatProps {
|
|
6
|
+
/** Shape result from a dingbatch generator */
|
|
7
|
+
shape: DingbatResult;
|
|
8
|
+
/** Fill color (default: currentColor) */
|
|
9
|
+
fill?: string;
|
|
10
|
+
/** Stroke color (default: none) */
|
|
11
|
+
stroke?: string;
|
|
12
|
+
/** Stroke width (default: 0) */
|
|
13
|
+
strokeWidth?: number;
|
|
14
|
+
/** CSS class name */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Inline styles */
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
}
|
|
19
|
+
declare function Dingbat({ shape, fill, stroke, strokeWidth, className, style, }: DingbatProps): react_jsx_runtime.JSX.Element;
|
|
20
|
+
|
|
21
|
+
type PresetName = 'crescent' | 'fatMoon' | 'sharpChevron' | 'thinArc' | 'horseshoe';
|
|
22
|
+
interface BoomerangProps extends Omit<DingbatProps, 'shape'> {
|
|
23
|
+
preset?: PresetName;
|
|
24
|
+
params?: BoomerangParams;
|
|
25
|
+
rotation?: number;
|
|
26
|
+
}
|
|
27
|
+
declare function Boomerang({ preset, params, rotation, ...rest }: BoomerangProps): react_jsx_runtime.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export { Boomerang, type BoomerangProps, Dingbat, type DingbatProps };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
boomerang
|
|
3
|
+
} from "../chunk-IAFZ3FNR.js";
|
|
4
|
+
|
|
5
|
+
// react/Dingbat.tsx
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
function Dingbat({
|
|
8
|
+
shape,
|
|
9
|
+
fill = "currentColor",
|
|
10
|
+
stroke = "none",
|
|
11
|
+
strokeWidth = 0,
|
|
12
|
+
className,
|
|
13
|
+
style
|
|
14
|
+
}) {
|
|
15
|
+
return /* @__PURE__ */ jsx(
|
|
16
|
+
"svg",
|
|
17
|
+
{
|
|
18
|
+
viewBox: shape.viewBox,
|
|
19
|
+
className,
|
|
20
|
+
style,
|
|
21
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
22
|
+
children: /* @__PURE__ */ jsx(
|
|
23
|
+
"path",
|
|
24
|
+
{
|
|
25
|
+
d: shape.path,
|
|
26
|
+
fill,
|
|
27
|
+
stroke,
|
|
28
|
+
strokeWidth
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// react/components/Boomerang.tsx
|
|
36
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
37
|
+
function Boomerang({
|
|
38
|
+
preset,
|
|
39
|
+
params,
|
|
40
|
+
rotation,
|
|
41
|
+
...rest
|
|
42
|
+
}) {
|
|
43
|
+
const overrides = rotation !== void 0 ? { rotation } : {};
|
|
44
|
+
let shape;
|
|
45
|
+
if (preset) {
|
|
46
|
+
shape = boomerang[preset](overrides);
|
|
47
|
+
} else if (params) {
|
|
48
|
+
shape = boomerang({ ...params, ...overrides });
|
|
49
|
+
} else {
|
|
50
|
+
shape = boomerang.crescent(overrides);
|
|
51
|
+
}
|
|
52
|
+
return /* @__PURE__ */ jsx2(Dingbat, { shape, ...rest });
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
Boomerang,
|
|
56
|
+
Dingbat
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../react/Dingbat.tsx","../../react/components/Boomerang.tsx"],"sourcesContent":["// react/Dingbat.tsx\n\nimport type { CSSProperties } from 'react';\nimport type { DingbatResult } from '../src/generators/types';\n\nexport interface DingbatProps {\n /** Shape result from a dingbatch generator */\n shape: DingbatResult;\n /** Fill color (default: currentColor) */\n fill?: string;\n /** Stroke color (default: none) */\n stroke?: string;\n /** Stroke width (default: 0) */\n strokeWidth?: number;\n /** CSS class name */\n className?: string;\n /** Inline styles */\n style?: CSSProperties;\n}\n\nexport function Dingbat({\n shape,\n fill = 'currentColor',\n stroke = 'none',\n strokeWidth = 0,\n className,\n style,\n}: DingbatProps) {\n return (\n <svg\n viewBox={shape.viewBox}\n className={className}\n style={style}\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d={shape.path}\n fill={fill}\n stroke={stroke}\n strokeWidth={strokeWidth}\n />\n </svg>\n );\n}\n","// react/components/Boomerang.tsx\n\nimport { boomerang, BoomerangParams } from '../../src';\nimport { Dingbat, DingbatProps } from '../Dingbat';\n\ntype PresetName = 'crescent' | 'fatMoon' | 'sharpChevron' | 'thinArc' | 'horseshoe';\n\nexport interface BoomerangProps extends Omit<DingbatProps, 'shape'> {\n preset?: PresetName;\n params?: BoomerangParams;\n rotation?: number;\n}\n\nexport function Boomerang({\n preset,\n params,\n rotation,\n ...rest\n}: BoomerangProps) {\n const overrides = rotation !== undefined ? { rotation } : {};\n\n let shape;\n if (preset) {\n shape = boomerang[preset](overrides);\n } else if (params) {\n shape = boomerang({ ...params, ...overrides });\n } else {\n shape = boomerang.crescent(overrides);\n }\n\n return <Dingbat shape={shape} {...rest} />;\n}\n"],"mappings":";;;;;AAmCM;AAfC,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA,OAAO;AAAA,EACP,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AACF,GAAiB;AACf,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAS,MAAM;AAAA,MACf;AAAA,MACA;AAAA,MACA,OAAM;AAAA,MAEN;AAAA,QAAC;AAAA;AAAA,UACC,GAAG,MAAM;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;;;ACbS,gBAAAA,YAAA;AAjBF,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,YAAY,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAE3D,MAAI;AACJ,MAAI,QAAQ;AACV,YAAQ,UAAU,MAAM,EAAE,SAAS;AAAA,EACrC,WAAW,QAAQ;AACjB,YAAQ,UAAU,EAAE,GAAG,QAAQ,GAAG,UAAU,CAAC;AAAA,EAC/C,OAAO;AACL,YAAQ,UAAU,SAAS,SAAS;AAAA,EACtC;AAEA,SAAO,gBAAAA,KAAC,WAAQ,OAAe,GAAG,MAAM;AAC1C;","names":["jsx"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dingbatch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Parametric MCM (Mid-Century Modern) dingbat shapes as SVGs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"import": "./dist/react/index.js",
|
|
15
|
+
"types": "./dist/react/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"svg",
|
|
29
|
+
"shapes",
|
|
30
|
+
"mcm",
|
|
31
|
+
"mid-century-modern",
|
|
32
|
+
"dingbat",
|
|
33
|
+
"react",
|
|
34
|
+
"parametric"
|
|
35
|
+
],
|
|
36
|
+
"author": "Evan Navarro",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/EvanCNavarro/dingbatch.git"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@testing-library/dom": "^10.4.0",
|
|
44
|
+
"@testing-library/react": "^16.0.0",
|
|
45
|
+
"@types/react": "^18.2.0",
|
|
46
|
+
"@types/react-dom": "^18.2.0",
|
|
47
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
48
|
+
"jsdom": "^24.0.0",
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"react-dom": "^18.2.0",
|
|
51
|
+
"tsup": "^8.0.0",
|
|
52
|
+
"typescript": "^5.0.0",
|
|
53
|
+
"vitest": "^1.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=18.0.0",
|
|
57
|
+
"react-dom": ">=18.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"react": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-dom": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|