@clawdraw/skill 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/LICENSE +21 -0
- package/README.md +63 -0
- package/SKILL.md +245 -0
- package/community/README.md +69 -0
- package/community/_template.mjs +39 -0
- package/community/helpers.mjs +1 -0
- package/package.json +44 -0
- package/primitives/basic-shapes.mjs +176 -0
- package/primitives/community-palettes.json +1 -0
- package/primitives/decorative.mjs +373 -0
- package/primitives/fills.mjs +217 -0
- package/primitives/flow-abstract.mjs +276 -0
- package/primitives/helpers.mjs +291 -0
- package/primitives/index.mjs +154 -0
- package/primitives/organic.mjs +514 -0
- package/primitives/utility.mjs +342 -0
- package/references/ALGORITHM_GUIDE.md +211 -0
- package/references/COMMUNITY.md +72 -0
- package/references/EXAMPLES.md +165 -0
- package/references/PALETTES.md +46 -0
- package/references/PRIMITIVES.md +301 -0
- package/references/PRO_TIPS.md +114 -0
- package/references/SECURITY.md +58 -0
- package/references/STROKE_FORMAT.md +78 -0
- package/references/SYMMETRY.md +59 -0
- package/references/WEBSOCKET.md +83 -0
- package/scripts/auth.mjs +145 -0
- package/scripts/clawdraw.mjs +882 -0
- package/scripts/connection.mjs +330 -0
- package/scripts/symmetry.mjs +217 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organic/natural primitives: lSystem, flower, leaf, vine, spaceColonization, mycelium, barnsleyFern.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { clamp, lerp, lerpColor, makeStroke, splitIntoStrokes, samplePalette } from './helpers.mjs';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Metadata
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const METADATA = [
|
|
12
|
+
{
|
|
13
|
+
name: 'lSystem', description: 'L-System branching structures (fern, tree, bush, coral, seaweed)', category: 'organic',
|
|
14
|
+
parameters: {
|
|
15
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
16
|
+
preset: { type: 'string', required: true, options: ['fern', 'tree', 'bush', 'coral', 'seaweed'], description: 'L-System preset (controls shape rules)' },
|
|
17
|
+
iterations: { type: 'number', min: 1, max: 5, description: 'Iteration depth (max varies by preset)' },
|
|
18
|
+
scale: { type: 'number', min: 0.1, max: 5, default: 1, description: 'Size multiplier (0.5=small, 2=large)' },
|
|
19
|
+
rotation: { type: 'number', description: 'Starting rotation in degrees' },
|
|
20
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
21
|
+
palette: { type: 'string', options: ['magma', 'plasma', 'viridis', 'turbo', 'inferno'], description: 'Color palette (gradient)' },
|
|
22
|
+
pressureStyle: { type: 'string' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'flower', description: 'Multi-petal flower with filled center spiral', category: 'organic',
|
|
27
|
+
parameters: {
|
|
28
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
29
|
+
petals: { type: 'number', min: 3, max: 20, default: 8, description: 'Number of petals' },
|
|
30
|
+
petalLength: { type: 'number', min: 10, max: 300, default: 60, description: 'Petal length' },
|
|
31
|
+
petalWidth: { type: 'number', min: 5, max: 150, default: 25, description: 'Petal width' },
|
|
32
|
+
centerRadius: { type: 'number', min: 5, max: 100, default: 20, description: 'Center circle size' },
|
|
33
|
+
petalColor: { type: 'string', description: 'Petal color (hex)' },
|
|
34
|
+
centerColor: { type: 'string', description: 'Center color (hex)' },
|
|
35
|
+
brushSize: { type: 'number', min: 3, max: 100 },
|
|
36
|
+
pressureStyle: { type: 'string' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'leaf', description: 'Single leaf with midrib and veins', category: 'organic',
|
|
41
|
+
parameters: {
|
|
42
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
43
|
+
length: { type: 'number', min: 10, max: 300, default: 80, description: 'Leaf length' },
|
|
44
|
+
width: { type: 'number', min: 5, max: 150, default: 30, description: 'Leaf width' },
|
|
45
|
+
rotation: { type: 'number', description: 'Rotation in degrees' },
|
|
46
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
47
|
+
veinCount: { type: 'number', min: 0, max: 12, default: 4, description: 'Number of veins' },
|
|
48
|
+
pressureStyle: { type: 'string' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'vine', description: 'Curving vine with small leaves', category: 'organic',
|
|
53
|
+
parameters: {
|
|
54
|
+
startX: { type: 'number', required: true }, startY: { type: 'number', required: true },
|
|
55
|
+
endX: { type: 'number', required: true }, endY: { type: 'number', required: true },
|
|
56
|
+
curveAmount: { type: 'number', min: 0, max: 300, default: 50, description: 'Curve intensity' },
|
|
57
|
+
leafCount: { type: 'number', min: 0, max: 20, default: 5, description: 'Number of leaves' },
|
|
58
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
59
|
+
pressureStyle: { type: 'string' },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'spaceColonization', description: 'Space colonization algorithm (roots, veins, lightning)', category: 'organic',
|
|
64
|
+
parameters: {
|
|
65
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
66
|
+
width: { type: 'number', min: 20, max: 600, default: 200, description: 'Area width' },
|
|
67
|
+
height: { type: 'number', min: 20, max: 600, default: 200, description: 'Area height' },
|
|
68
|
+
density: { type: 'number', min: 0.1, max: 1, default: 0.5, description: 'Attractor density' },
|
|
69
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
70
|
+
palette: { type: 'string', description: 'Color palette' },
|
|
71
|
+
stepLength: { type: 'number', min: 2, max: 30, default: 8, description: 'Growth step length' },
|
|
72
|
+
pressureStyle: { type: 'string' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'mycelium', description: 'Organic branching mycelium network', category: 'organic',
|
|
77
|
+
parameters: {
|
|
78
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
79
|
+
radius: { type: 'number', min: 20, max: 500, default: 150, description: 'Spread radius' },
|
|
80
|
+
density: { type: 'number', min: 0.1, max: 1, default: 0.5, description: 'Branch density' },
|
|
81
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
82
|
+
palette: { type: 'string', description: 'Color palette' },
|
|
83
|
+
branchiness: { type: 'number', min: 0.1, max: 1, default: 0.5, description: 'Branch probability' },
|
|
84
|
+
pressureStyle: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'barnsleyFern', description: 'Barnsley Fern IFS fractal', category: 'organic',
|
|
89
|
+
parameters: {
|
|
90
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
91
|
+
scale: { type: 'number', min: 3, max: 100, default: 20, description: 'Size scale' },
|
|
92
|
+
iterations: { type: 'number', min: 500, max: 8000, default: 2000, description: 'Point count' },
|
|
93
|
+
lean: { type: 'number', min: -30, max: 30, default: 0, description: 'Lean angle in degrees' },
|
|
94
|
+
curl: { type: 'number', min: 0.5, max: 1.5, default: 1, description: 'Curl factor' },
|
|
95
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
96
|
+
palette: { type: 'string', description: 'Color palette' },
|
|
97
|
+
pressureStyle: { type: 'string' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Primitives
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
const L_SYSTEM_PRESETS = {
|
|
107
|
+
fern: { axiom: 'X', rules: { X: 'F+[[X]-X]-F[-FX]+X', F: 'FF' }, angle: 25, maxIter: 5 },
|
|
108
|
+
tree: { axiom: 'F', rules: { F: 'FF+[+F-F-F]-[-F+F+F]' }, angle: 22, maxIter: 4 },
|
|
109
|
+
bush: { axiom: 'F', rules: { F: 'F[+F]F[-F]F' }, angle: 25, maxIter: 4 },
|
|
110
|
+
coral: { axiom: 'F', rules: { F: 'FF-[-F+F+F]+[+F-F-F]' }, angle: 22, maxIter: 4 },
|
|
111
|
+
seaweed: { axiom: 'F', rules: { F: 'F[+F]F[-F]+F' }, angle: 20, maxIter: 5 },
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function lSystem(cx, cy, preset, iterations, scale, rotation, color, brushSize, palette, pressureStyle) {
|
|
115
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
116
|
+
const cfg = L_SYSTEM_PRESETS[preset] || L_SYSTEM_PRESETS.tree;
|
|
117
|
+
iterations = clamp(Math.round(Number(iterations) || cfg.maxIter), 1, cfg.maxIter);
|
|
118
|
+
scale = clamp(Number(scale) || 1, 0.1, 5);
|
|
119
|
+
const startAngle = (Number(rotation) || -90);
|
|
120
|
+
|
|
121
|
+
let str = cfg.axiom;
|
|
122
|
+
for (let i = 0; i < iterations; i++) {
|
|
123
|
+
let next = '';
|
|
124
|
+
for (const ch of str) next += cfg.rules[ch] ?? ch;
|
|
125
|
+
str = next;
|
|
126
|
+
if (str.length > 50000) break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseStepLen = 4 * scale;
|
|
130
|
+
const baseTurnRad = cfg.angle * Math.PI / 180;
|
|
131
|
+
const strokes = [];
|
|
132
|
+
const stack = [];
|
|
133
|
+
let x = cx, y = cy, angle = startAngle * Math.PI / 180;
|
|
134
|
+
let cur = [{ x, y }];
|
|
135
|
+
let depth = 0;
|
|
136
|
+
let maxDepth = 0;
|
|
137
|
+
|
|
138
|
+
let tempDepth = 0;
|
|
139
|
+
for (const ch of str) {
|
|
140
|
+
if (ch === '[') { tempDepth++; if (tempDepth > maxDepth) maxDepth = tempDepth; }
|
|
141
|
+
else if (ch === ']') tempDepth--;
|
|
142
|
+
}
|
|
143
|
+
maxDepth = maxDepth || 1;
|
|
144
|
+
|
|
145
|
+
for (const ch of str) {
|
|
146
|
+
if (ch === 'F') {
|
|
147
|
+
const stepJitter = baseStepLen * (1 + (Math.random() - 0.5) * 0.3);
|
|
148
|
+
x += Math.cos(angle) * stepJitter;
|
|
149
|
+
y += Math.sin(angle) * stepJitter;
|
|
150
|
+
cur.push({ x, y });
|
|
151
|
+
} else if (ch === '+') { angle += baseTurnRad + (Math.random() - 0.5) * 0.087; }
|
|
152
|
+
else if (ch === '-') { angle -= baseTurnRad + (Math.random() - 0.5) * 0.087; }
|
|
153
|
+
else if (ch === '[') {
|
|
154
|
+
stack.push({ x, y, angle, depth });
|
|
155
|
+
depth++;
|
|
156
|
+
} else if (ch === ']') {
|
|
157
|
+
if (cur.length > 1) {
|
|
158
|
+
const t = depth / maxDepth;
|
|
159
|
+
const c = palette ? samplePalette(palette, t) : color;
|
|
160
|
+
strokes.push(makeStroke(cur, c, brushSize, 0.9, pressureStyle));
|
|
161
|
+
}
|
|
162
|
+
if (stack.length > 0) {
|
|
163
|
+
const s = stack.pop();
|
|
164
|
+
x = s.x; y = s.y; angle = s.angle; depth = s.depth;
|
|
165
|
+
}
|
|
166
|
+
cur = [{ x, y }];
|
|
167
|
+
}
|
|
168
|
+
if (strokes.length >= 200) break;
|
|
169
|
+
}
|
|
170
|
+
if (cur.length > 1) {
|
|
171
|
+
const t = depth / maxDepth;
|
|
172
|
+
const c = palette ? samplePalette(palette, t) : color;
|
|
173
|
+
strokes.push(makeStroke(cur, c, brushSize, 0.9, pressureStyle));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return strokes.slice(0, 200);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function flower(cx, cy, petals, petalLength, petalWidth, centerRadius, petalColor, centerColor, brushSize, pressureStyle) {
|
|
180
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
181
|
+
petals = clamp(Math.round(Number(petals) || 8), 3, 20);
|
|
182
|
+
petalLength = clamp(Number(petalLength) || 60, 10, 300);
|
|
183
|
+
petalWidth = clamp(Number(petalWidth) || 25, 5, 150);
|
|
184
|
+
centerRadius = clamp(Number(centerRadius) || 20, 5, 100);
|
|
185
|
+
brushSize = clamp(Number(brushSize) || 8, 3, 100);
|
|
186
|
+
|
|
187
|
+
const result = [];
|
|
188
|
+
|
|
189
|
+
// Petals — each petal gets a two-color gradient from petalColor (base) to centerColor (tip)
|
|
190
|
+
for (let i = 0; i < petals; i++) {
|
|
191
|
+
const a = (i / petals) * Math.PI * 2;
|
|
192
|
+
const dx = Math.cos(a), dy = Math.sin(a);
|
|
193
|
+
const nx = -dy, ny = dx;
|
|
194
|
+
const thisLength = petalLength * (1 + (Math.random() - 0.5) * 0.2);
|
|
195
|
+
|
|
196
|
+
const bands = 3;
|
|
197
|
+
for (let b = 0; b < bands; b++) {
|
|
198
|
+
const tStart = b / bands;
|
|
199
|
+
const tEnd = (b + 1) / bands;
|
|
200
|
+
const gradT = (b + 0.5) / bands;
|
|
201
|
+
const c = lerpColor(petalColor || '#ff6688', centerColor || '#ffcc00', gradT);
|
|
202
|
+
|
|
203
|
+
const pts = [];
|
|
204
|
+
const stepsPerBand = 8;
|
|
205
|
+
for (let j = 0; j <= stepsPerBand; j++) {
|
|
206
|
+
const localT = j / stepsPerBand;
|
|
207
|
+
const t = tStart + localT * (tEnd - tStart);
|
|
208
|
+
const along = t * thisLength;
|
|
209
|
+
const across = Math.sin(t * Math.PI) * petalWidth * 0.5;
|
|
210
|
+
pts.push({ x: cx + dx * along + nx * across, y: cy + dy * along + ny * across });
|
|
211
|
+
}
|
|
212
|
+
for (let j = stepsPerBand; j >= 0; j--) {
|
|
213
|
+
const localT = j / stepsPerBand;
|
|
214
|
+
const t = tStart + localT * (tEnd - tStart);
|
|
215
|
+
const along = t * thisLength;
|
|
216
|
+
const across = -Math.sin(t * Math.PI) * petalWidth * 0.5;
|
|
217
|
+
pts.push({ x: cx + dx * along + nx * across, y: cy + dy * along + ny * across });
|
|
218
|
+
}
|
|
219
|
+
result.push(makeStroke(pts, c, brushSize, 0.85, pressureStyle));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const centerPts = [];
|
|
224
|
+
const cSteps = 40;
|
|
225
|
+
for (let i = 0; i <= cSteps; i++) {
|
|
226
|
+
const t = i / cSteps;
|
|
227
|
+
const a = t * Math.PI * 4;
|
|
228
|
+
const r = centerRadius * (1 - t * 0.8);
|
|
229
|
+
centerPts.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r });
|
|
230
|
+
}
|
|
231
|
+
result.push(makeStroke(centerPts, centerColor, clamp(centerRadius * 0.8, 5, 60), 0.9, pressureStyle));
|
|
232
|
+
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function leaf(cx, cy, length, width, rotation, color, brushSize, veinCount, pressureStyle) {
|
|
237
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
238
|
+
length = clamp(Number(length) || 80, 10, 300);
|
|
239
|
+
width = clamp(Number(width) || 30, 5, 150);
|
|
240
|
+
rotation = (Number(rotation) || 0) * Math.PI / 180;
|
|
241
|
+
veinCount = clamp(Math.round(Number(veinCount) || 4), 0, 12);
|
|
242
|
+
brushSize = clamp(Number(brushSize) || 5, 3, 100);
|
|
243
|
+
|
|
244
|
+
const result = [];
|
|
245
|
+
|
|
246
|
+
const pts = [];
|
|
247
|
+
const steps = 24;
|
|
248
|
+
for (let i = 0; i <= steps; i++) {
|
|
249
|
+
const t = i / steps;
|
|
250
|
+
let lx, ly;
|
|
251
|
+
if (t < 0.5) {
|
|
252
|
+
const s = t * 2;
|
|
253
|
+
lx = s * length;
|
|
254
|
+
ly = Math.sin(s * Math.PI) * width * 0.5;
|
|
255
|
+
} else {
|
|
256
|
+
const s = (1 - t) * 2;
|
|
257
|
+
lx = s * length;
|
|
258
|
+
ly = -Math.sin(s * Math.PI) * width * 0.5;
|
|
259
|
+
}
|
|
260
|
+
const rx = lx * Math.cos(rotation) - ly * Math.sin(rotation);
|
|
261
|
+
const ry = lx * Math.sin(rotation) + ly * Math.cos(rotation);
|
|
262
|
+
pts.push({ x: cx + rx, y: cy + ry });
|
|
263
|
+
}
|
|
264
|
+
result.push(makeStroke(pts, color, brushSize, 0.85, pressureStyle));
|
|
265
|
+
|
|
266
|
+
const midPts = [
|
|
267
|
+
{ x: cx, y: cy },
|
|
268
|
+
{ x: cx + Math.cos(rotation) * length, y: cy + Math.sin(rotation) * length },
|
|
269
|
+
];
|
|
270
|
+
result.push(makeStroke(midPts, color, clamp(brushSize * 0.6, 3, 20), 0.7, pressureStyle));
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < veinCount; i++) {
|
|
273
|
+
const t = (i + 1) / (veinCount + 1);
|
|
274
|
+
const baseX = cx + Math.cos(rotation) * length * t;
|
|
275
|
+
const baseY = cy + Math.sin(rotation) * length * t;
|
|
276
|
+
const veinLen = width * 0.4 * Math.sin(t * Math.PI);
|
|
277
|
+
const perpAngle = rotation + Math.PI / 2;
|
|
278
|
+
const sign = i % 2 === 0 ? 1 : -1;
|
|
279
|
+
const tipX = baseX + Math.cos(perpAngle) * veinLen * sign + Math.cos(rotation) * veinLen * 0.3;
|
|
280
|
+
const tipY = baseY + Math.sin(perpAngle) * veinLen * sign + Math.sin(rotation) * veinLen * 0.3;
|
|
281
|
+
result.push(makeStroke([{ x: baseX, y: baseY }, { x: tipX, y: tipY }], color, clamp(brushSize * 0.4, 3, 10), 0.5, pressureStyle));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function vine(startX, startY, endX, endY, curveAmount, leafCount, color, brushSize, pressureStyle) {
|
|
288
|
+
startX = Number(startX) || 0; startY = Number(startY) || 0;
|
|
289
|
+
endX = Number(endX) || 100; endY = Number(endY) || 0;
|
|
290
|
+
curveAmount = clamp(Number(curveAmount) || 50, 0, 300);
|
|
291
|
+
leafCount = clamp(Math.round(Number(leafCount) || 5), 0, 20);
|
|
292
|
+
brushSize = clamp(Number(brushSize) || 4, 3, 100);
|
|
293
|
+
|
|
294
|
+
const result = [];
|
|
295
|
+
|
|
296
|
+
const mx = (startX + endX) / 2 + (Math.random() - 0.5) * curveAmount;
|
|
297
|
+
const my = (startY + endY) / 2 + (Math.random() - 0.5) * curveAmount;
|
|
298
|
+
const vinePts = [];
|
|
299
|
+
const steps = 30;
|
|
300
|
+
for (let i = 0; i <= steps; i++) {
|
|
301
|
+
const t = i / steps;
|
|
302
|
+
const x = (1-t)*(1-t)*startX + 2*(1-t)*t*mx + t*t*endX;
|
|
303
|
+
const y = (1-t)*(1-t)*startY + 2*(1-t)*t*my + t*t*endY;
|
|
304
|
+
vinePts.push({ x, y });
|
|
305
|
+
}
|
|
306
|
+
result.push(makeStroke(vinePts, color, brushSize, 0.9, pressureStyle));
|
|
307
|
+
|
|
308
|
+
for (let i = 0; i < leafCount; i++) {
|
|
309
|
+
const t = (i + 0.5) / leafCount;
|
|
310
|
+
const idx = Math.floor(t * steps);
|
|
311
|
+
const base = vinePts[idx];
|
|
312
|
+
const next = vinePts[Math.min(idx + 1, steps)];
|
|
313
|
+
const dx = next.x - base.x, dy = next.y - base.y;
|
|
314
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
315
|
+
const perpX = -dy / len, perpY = dx / len;
|
|
316
|
+
const side = i % 2 === 0 ? 1 : -1;
|
|
317
|
+
const leafLen = 12 + Math.random() * 10;
|
|
318
|
+
const lPts = [
|
|
319
|
+
{ x: base.x, y: base.y },
|
|
320
|
+
{ x: base.x + perpX * leafLen * side * 0.5 + dx / len * leafLen * 0.3, y: base.y + perpY * leafLen * side * 0.5 + dy / len * leafLen * 0.3 },
|
|
321
|
+
{ x: base.x + perpX * leafLen * side, y: base.y + perpY * leafLen * side },
|
|
322
|
+
];
|
|
323
|
+
result.push(makeStroke(lPts, color, clamp(brushSize * 0.6, 3, 20), 0.7, pressureStyle));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function spaceColonization(cx, cy, width, height, density, color, brushSize, palette, stepLength, pressureStyle) {
|
|
330
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
331
|
+
width = clamp(Number(width) || 200, 20, 600);
|
|
332
|
+
height = clamp(Number(height) || 200, 20, 600);
|
|
333
|
+
density = clamp(Number(density) || 0.5, 0.1, 1);
|
|
334
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 100);
|
|
335
|
+
|
|
336
|
+
const numAttractors = Math.round(30 * density);
|
|
337
|
+
const attractors = [];
|
|
338
|
+
for (let i = 0; i < numAttractors; i++) {
|
|
339
|
+
attractors.push({
|
|
340
|
+
x: cx + (Math.random() - 0.5) * width,
|
|
341
|
+
y: cy + (Math.random() - 0.5) * height,
|
|
342
|
+
alive: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
stepLength = clamp(Number(stepLength) || 8, 2, 30);
|
|
347
|
+
const nodes = [{ x: cx, y: cy + height * 0.4, parent: -1 }];
|
|
348
|
+
const killDist = 10, attractDist = width * 0.4, stepLen = stepLength;
|
|
349
|
+
|
|
350
|
+
for (let iter = 0; iter < 100; iter++) {
|
|
351
|
+
const dirs = new Array(nodes.length).fill(null).map(() => ({ x: 0, y: 0, count: 0 }));
|
|
352
|
+
|
|
353
|
+
for (const att of attractors) {
|
|
354
|
+
if (!att.alive) continue;
|
|
355
|
+
let closest = -1, closestDist = Infinity;
|
|
356
|
+
for (let n = 0; n < nodes.length; n++) {
|
|
357
|
+
const d = Math.hypot(att.x - nodes[n].x, att.y - nodes[n].y);
|
|
358
|
+
if (d < closestDist) { closestDist = d; closest = n; }
|
|
359
|
+
}
|
|
360
|
+
if (closestDist < killDist) { att.alive = false; continue; }
|
|
361
|
+
if (closestDist < attractDist && closest >= 0) {
|
|
362
|
+
const d = closestDist || 1;
|
|
363
|
+
dirs[closest].x += (att.x - nodes[closest].x) / d;
|
|
364
|
+
dirs[closest].y += (att.y - nodes[closest].y) / d;
|
|
365
|
+
dirs[closest].count++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let grew = false;
|
|
370
|
+
for (let n = 0; n < dirs.length; n++) {
|
|
371
|
+
if (dirs[n].count === 0) continue;
|
|
372
|
+
const len = Math.hypot(dirs[n].x, dirs[n].y) || 1;
|
|
373
|
+
nodes.push({
|
|
374
|
+
x: nodes[n].x + (dirs[n].x / len) * stepLen,
|
|
375
|
+
y: nodes[n].y + (dirs[n].y / len) * stepLen,
|
|
376
|
+
parent: n,
|
|
377
|
+
});
|
|
378
|
+
grew = true;
|
|
379
|
+
if (nodes.length >= 500) break;
|
|
380
|
+
}
|
|
381
|
+
if (!grew || nodes.length >= 500) break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const result = [];
|
|
385
|
+
const isLeaf = new Array(nodes.length).fill(true);
|
|
386
|
+
for (const n of nodes) if (n.parent >= 0) isLeaf[n.parent] = false;
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < nodes.length && result.length < 200; i++) {
|
|
389
|
+
if (!isLeaf[i]) continue;
|
|
390
|
+
const pts = [];
|
|
391
|
+
let idx = i;
|
|
392
|
+
while (idx >= 0 && pts.length < 100) {
|
|
393
|
+
pts.unshift({ x: nodes[idx].x, y: nodes[idx].y });
|
|
394
|
+
idx = nodes[idx].parent;
|
|
395
|
+
}
|
|
396
|
+
if (pts.length > 1) {
|
|
397
|
+
const t = palette ? clamp(1 - pts.length / 20, 0, 1) : 0;
|
|
398
|
+
const c = palette ? samplePalette(palette, t) : color;
|
|
399
|
+
result.push(makeStroke(pts, c, brushSize, 0.8, pressureStyle));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return result.slice(0, 200);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function mycelium(cx, cy, radius, density, color, brushSize, palette, branchiness, pressureStyle) {
|
|
407
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
408
|
+
radius = clamp(Number(radius) || 150, 20, 500);
|
|
409
|
+
density = clamp(Number(density) || 0.5, 0.1, 1);
|
|
410
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 10);
|
|
411
|
+
branchiness = clamp(Number(branchiness) || 0.5, 0.1, 1.0);
|
|
412
|
+
|
|
413
|
+
const numSeeds = Math.round(3 + density * 2);
|
|
414
|
+
const seeds = [];
|
|
415
|
+
for (let i = 0; i < numSeeds; i++) {
|
|
416
|
+
const a = (i / numSeeds) * Math.PI * 2 + Math.random() * 0.5;
|
|
417
|
+
const r = radius * (0.1 + Math.random() * 0.2);
|
|
418
|
+
seeds.push({ x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const result = [];
|
|
422
|
+
const stepLen = 4 + Math.random() * 3;
|
|
423
|
+
const branchProb = 0.04 + branchiness * 0.12;
|
|
424
|
+
const maxBranches = Math.round(150 * density);
|
|
425
|
+
|
|
426
|
+
for (const seed of seeds) {
|
|
427
|
+
const tips = [{ x: seed.x, y: seed.y, angle: Math.random() * Math.PI * 2, depth: 0 }];
|
|
428
|
+
let branches = 0;
|
|
429
|
+
|
|
430
|
+
while (tips.length > 0 && branches < maxBranches && result.length < 200) {
|
|
431
|
+
const tip = tips.shift();
|
|
432
|
+
const pts = [{ x: tip.x, y: tip.y }];
|
|
433
|
+
let { x, y, angle, depth } = tip;
|
|
434
|
+
const steps = 5 + Math.floor(Math.random() * 10);
|
|
435
|
+
|
|
436
|
+
for (let s = 0; s < steps; s++) {
|
|
437
|
+
angle += (Math.random() - 0.5) * 0.6;
|
|
438
|
+
x += Math.cos(angle) * stepLen;
|
|
439
|
+
y += Math.sin(angle) * stepLen;
|
|
440
|
+
pts.push({ x, y });
|
|
441
|
+
|
|
442
|
+
if (Math.hypot(x - cx, y - cy) > radius) break;
|
|
443
|
+
|
|
444
|
+
if (Math.random() < branchProb && depth < 6) {
|
|
445
|
+
tips.push({ x, y, angle: angle + (Math.random() - 0.5) * 1.2, depth: depth + 1 });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (pts.length > 1) {
|
|
450
|
+
const t = palette ? clamp(depth / 5, 0, 1) : 0;
|
|
451
|
+
const c = palette ? samplePalette(palette, t) : color;
|
|
452
|
+
const size = Math.max(3, brushSize - depth * 0.3);
|
|
453
|
+
result.push(makeStroke(pts, c, size, 0.6 + Math.random() * 0.2, pressureStyle));
|
|
454
|
+
branches++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return result.slice(0, 200);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function barnsleyFern(cx, cy, scale, iterations, lean, curl, color, brushSize, palette, pressureStyle) {
|
|
463
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
464
|
+
scale = clamp(Number(scale) || 20, 3, 100);
|
|
465
|
+
iterations = clamp(Math.round(Number(iterations) || 2000), 500, 8000);
|
|
466
|
+
lean = clamp(Number(lean) || 0, -30, 30) * Math.PI / 180;
|
|
467
|
+
curl = clamp(Number(curl) || 1.0, 0.5, 1.5);
|
|
468
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 100);
|
|
469
|
+
|
|
470
|
+
let x = 0, y = 0;
|
|
471
|
+
const allPts = [];
|
|
472
|
+
for (let i = 0; i < iterations; i++) {
|
|
473
|
+
const r = Math.random();
|
|
474
|
+
let nx, ny;
|
|
475
|
+
if (r < 0.01) {
|
|
476
|
+
nx = 0;
|
|
477
|
+
ny = 0.16 * y;
|
|
478
|
+
} else if (r < 0.86) {
|
|
479
|
+
nx = 0.85 * x + 0.04 * y;
|
|
480
|
+
ny = -0.04 * x + (0.85 * curl) * y + 1.6;
|
|
481
|
+
} else if (r < 0.93) {
|
|
482
|
+
nx = 0.2 * x - 0.26 * y;
|
|
483
|
+
ny = 0.23 * x + 0.22 * y + 1.6;
|
|
484
|
+
} else {
|
|
485
|
+
nx = -0.15 * x + 0.28 * y;
|
|
486
|
+
ny = 0.26 * x + 0.24 * y + 0.44;
|
|
487
|
+
}
|
|
488
|
+
x = nx; y = ny;
|
|
489
|
+
|
|
490
|
+
const rx = (x * Math.cos(lean) - y * Math.sin(lean)) * scale + cx;
|
|
491
|
+
const ry = (-y * Math.cos(lean) - x * Math.sin(lean)) * scale + cy;
|
|
492
|
+
allPts.push({ x: rx, y: ry, rawY: y });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
allPts.sort((a, b) => a.rawY - b.rawY);
|
|
496
|
+
|
|
497
|
+
const result = [];
|
|
498
|
+
const segSize = Math.max(10, Math.floor(allPts.length / 60));
|
|
499
|
+
const maxRawY = allPts[allPts.length - 1]?.rawY || 1;
|
|
500
|
+
|
|
501
|
+
for (let i = 0; i < allPts.length && result.length < 200; i += segSize) {
|
|
502
|
+
const seg = allPts.slice(i, i + segSize);
|
|
503
|
+
if (seg.length < 2) continue;
|
|
504
|
+
seg.sort((a, b) => a.x - b.x || a.y - b.y);
|
|
505
|
+
const t = palette ? clamp(seg[0].rawY / maxRawY, 0, 1) : 0;
|
|
506
|
+
const c = palette ? samplePalette(palette, t) : color;
|
|
507
|
+
result.push(makeStroke(
|
|
508
|
+
seg.map(p => ({ x: p.x, y: p.y })),
|
|
509
|
+
c, brushSize, 0.75, pressureStyle
|
|
510
|
+
));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result.slice(0, 200);
|
|
514
|
+
}
|