@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,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility primitives: bezierCurve, dashedLine, arrow, strokeText, alienGlyphs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { clamp, lerp, makeStroke, splitIntoStrokes } from './helpers.mjs';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Metadata
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const METADATA = [
|
|
12
|
+
{
|
|
13
|
+
name: 'bezierCurve', description: 'Smooth Bezier curve through control points', category: 'utility',
|
|
14
|
+
parameters: {
|
|
15
|
+
points: { type: 'array', required: true, description: 'Array of {x, y} control points (max 20)' },
|
|
16
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
17
|
+
opacity: { type: 'number', min: 0.01, max: 1 },
|
|
18
|
+
pressureStyle: { type: 'string' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'dashedLine', description: 'Dashed line segment', category: 'utility',
|
|
23
|
+
parameters: {
|
|
24
|
+
startX: { type: 'number', required: true }, startY: { type: 'number', required: true },
|
|
25
|
+
endX: { type: 'number', required: true }, endY: { type: 'number', required: true },
|
|
26
|
+
dashLength: { type: 'number', min: 2, max: 50, default: 10, description: 'Dash length' },
|
|
27
|
+
gapLength: { type: 'number', min: 1, max: 50, default: 5, description: 'Gap length' },
|
|
28
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
29
|
+
pressureStyle: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'arrow', description: 'Line with arrowhead', category: 'utility',
|
|
34
|
+
parameters: {
|
|
35
|
+
startX: { type: 'number', required: true }, startY: { type: 'number', required: true },
|
|
36
|
+
endX: { type: 'number', required: true }, endY: { type: 'number', required: true },
|
|
37
|
+
headSize: { type: 'number', min: 3, max: 60, default: 15, description: 'Arrowhead size' },
|
|
38
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
39
|
+
pressureStyle: { type: 'string' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'strokeText', description: 'Draw text as single-stroke letterforms', category: 'utility',
|
|
44
|
+
parameters: {
|
|
45
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
46
|
+
text: { type: 'string', required: true, description: 'Text to draw (max 40 chars)' },
|
|
47
|
+
charHeight: { type: 'number', min: 5, max: 200, default: 30, description: 'Character height' },
|
|
48
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
49
|
+
opacity: { type: 'number', min: 0.01, max: 1 },
|
|
50
|
+
rotation: { type: 'number', description: 'Rotation in degrees' },
|
|
51
|
+
pressureStyle: { type: 'string' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'alienGlyphs', description: 'Procedural cryptic alien/AI glyphs', category: 'utility',
|
|
56
|
+
parameters: {
|
|
57
|
+
cx: { type: 'number', required: true }, cy: { type: 'number', required: true },
|
|
58
|
+
count: { type: 'number', min: 1, max: 20, default: 8, description: 'Number of glyphs' },
|
|
59
|
+
glyphSize: { type: 'number', min: 5, max: 100, default: 25, description: 'Glyph size' },
|
|
60
|
+
arrangement: { type: 'string', options: ['line', 'grid', 'scatter', 'circle'], default: 'line', description: 'Layout arrangement' },
|
|
61
|
+
color: { type: 'string' }, brushSize: { type: 'number', min: 3, max: 100 },
|
|
62
|
+
opacity: { type: 'number', min: 0.01, max: 1 },
|
|
63
|
+
pressureStyle: { type: 'string' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Glyph map for strokeText
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const GLYPH_MAP = {
|
|
73
|
+
'A': [[[0,1],[0.5,0],[1,1]],[[0.2,0.6],[0.8,0.6]]],
|
|
74
|
+
'B': [[[0,1],[0,0],[0.7,0],[0.9,0.15],[0.9,0.35],[0.7,0.5],[0,0.5]],[[0.7,0.5],[0.9,0.65],[0.9,0.85],[0.7,1],[0,1]]],
|
|
75
|
+
'C': [[[1,0.1],[0.7,0],[0.3,0],[0,0.3],[0,0.7],[0.3,1],[0.7,1],[1,0.9]]],
|
|
76
|
+
'D': [[[0,0],[0,1],[0.6,1],[0.9,0.8],[1,0.5],[0.9,0.2],[0.6,0],[0,0]]],
|
|
77
|
+
'E': [[[1,0],[0,0],[0,0.5],[0.7,0.5]],[[0,0.5],[0,1],[1,1]]],
|
|
78
|
+
'F': [[[1,0],[0,0],[0,0.5],[0.7,0.5]],[[0,0.5],[0,1]]],
|
|
79
|
+
'G': [[[1,0.1],[0.7,0],[0.3,0],[0,0.3],[0,0.7],[0.3,1],[0.7,1],[1,0.7],[1,0.5],[0.5,0.5]]],
|
|
80
|
+
'H': [[[0,0],[0,1]],[[1,0],[1,1]],[[0,0.5],[1,0.5]]],
|
|
81
|
+
'I': [[[0.3,0],[0.7,0]],[[0.5,0],[0.5,1]],[[0.3,1],[0.7,1]]],
|
|
82
|
+
'J': [[[0.3,0],[0.8,0]],[[0.6,0],[0.6,0.8],[0.4,1],[0.2,0.9]]],
|
|
83
|
+
'K': [[[0,0],[0,1]],[[1,0],[0,0.5],[1,1]]],
|
|
84
|
+
'L': [[[0,0],[0,1],[1,1]]],
|
|
85
|
+
'M': [[[0,1],[0,0],[0.5,0.4],[1,0],[1,1]]],
|
|
86
|
+
'N': [[[0,1],[0,0],[1,1],[1,0]]],
|
|
87
|
+
'O': [[[0.3,0],[0.7,0],[1,0.3],[1,0.7],[0.7,1],[0.3,1],[0,0.7],[0,0.3],[0.3,0]]],
|
|
88
|
+
'P': [[[0,1],[0,0],[0.7,0],[1,0.15],[1,0.35],[0.7,0.5],[0,0.5]]],
|
|
89
|
+
'Q': [[[0.3,0],[0.7,0],[1,0.3],[1,0.7],[0.7,1],[0.3,1],[0,0.7],[0,0.3],[0.3,0]],[[0.6,0.8],[1,1.1]]],
|
|
90
|
+
'R': [[[0,1],[0,0],[0.7,0],[1,0.15],[1,0.35],[0.7,0.5],[0,0.5]],[[0.5,0.5],[1,1]]],
|
|
91
|
+
'S': [[[1,0.1],[0.7,0],[0.3,0],[0,0.15],[0,0.35],[0.3,0.5],[0.7,0.5],[1,0.65],[1,0.85],[0.7,1],[0.3,1],[0,0.9]]],
|
|
92
|
+
'T': [[[0,0],[1,0]],[[0.5,0],[0.5,1]]],
|
|
93
|
+
'U': [[[0,0],[0,0.7],[0.3,1],[0.7,1],[1,0.7],[1,0]]],
|
|
94
|
+
'V': [[[0,0],[0.5,1],[1,0]]],
|
|
95
|
+
'W': [[[0,0],[0.25,1],[0.5,0.5],[0.75,1],[1,0]]],
|
|
96
|
+
'X': [[[0,0],[1,1]],[[1,0],[0,1]]],
|
|
97
|
+
'Y': [[[0,0],[0.5,0.5],[1,0]],[[0.5,0.5],[0.5,1]]],
|
|
98
|
+
'Z': [[[0,0],[1,0],[0,1],[1,1]]],
|
|
99
|
+
'0': [[[0.3,0],[0.7,0],[1,0.3],[1,0.7],[0.7,1],[0.3,1],[0,0.7],[0,0.3],[0.3,0]],[[0.1,0.8],[0.9,0.2]]],
|
|
100
|
+
'1': [[[0.3,0.2],[0.5,0]],[[0.5,0],[0.5,1]],[[0.3,1],[0.7,1]]],
|
|
101
|
+
'2': [[[0,0.15],[0.3,0],[0.7,0],[1,0.15],[1,0.35],[0,1],[1,1]]],
|
|
102
|
+
'3': [[[0,0.1],[0.3,0],[0.7,0],[1,0.15],[1,0.35],[0.7,0.5],[0.5,0.5]],[[0.7,0.5],[1,0.65],[1,0.85],[0.7,1],[0.3,1],[0,0.9]]],
|
|
103
|
+
'4': [[[0,0],[0,0.5],[1,0.5]],[[0.7,0],[0.7,1]]],
|
|
104
|
+
'5': [[[1,0],[0,0],[0,0.5],[0.7,0.5],[1,0.65],[1,0.85],[0.7,1],[0.3,1],[0,0.9]]],
|
|
105
|
+
'6': [[[0.7,0],[0.3,0],[0,0.3],[0,0.7],[0.3,1],[0.7,1],[1,0.7],[1,0.5],[0.7,0.4],[0,0.5]]],
|
|
106
|
+
'7': [[[0,0],[1,0],[0.3,1]]],
|
|
107
|
+
'8': [[[0.3,0],[0.7,0],[1,0.15],[1,0.35],[0.7,0.5],[0.3,0.5],[0,0.15],[0,0.35],[0.3,0.5]],[[0.3,0.5],[0,0.65],[0,0.85],[0.3,1],[0.7,1],[1,0.85],[1,0.65],[0.7,0.5]]],
|
|
108
|
+
'9': [[[1,0.5],[0.3,0.6],[0,0.3],[0,0.15],[0.3,0],[0.7,0],[1,0.3],[1,0.7],[0.7,1],[0.3,1]]],
|
|
109
|
+
' ': [],
|
|
110
|
+
'.': [[[0.4,0.9],[0.6,0.9],[0.6,1],[0.4,1],[0.4,0.9]]],
|
|
111
|
+
',': [[[0.5,0.85],[0.5,1],[0.3,1.15]]],
|
|
112
|
+
'!': [[[0.5,0],[0.5,0.7]],[[0.45,0.9],[0.55,0.9],[0.55,1],[0.45,1],[0.45,0.9]]],
|
|
113
|
+
'?': [[[0.1,0.15],[0.3,0],[0.7,0],[0.9,0.15],[0.9,0.35],[0.5,0.55],[0.5,0.7]],[[0.45,0.9],[0.55,0.9],[0.55,1],[0.45,1],[0.45,0.9]]],
|
|
114
|
+
'-': [[[0.2,0.5],[0.8,0.5]]],
|
|
115
|
+
':': [[[0.45,0.25],[0.55,0.25],[0.55,0.35],[0.45,0.35],[0.45,0.25]],[[0.45,0.65],[0.55,0.65],[0.55,0.75],[0.45,0.75],[0.45,0.65]]],
|
|
116
|
+
'/': [[[0.1,1],[0.9,0]]],
|
|
117
|
+
'\'': [[[0.45,0],[0.5,0.2]]],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Primitives
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export function bezierCurve(points, color, brushSize, opacity, pressureStyle) {
|
|
125
|
+
if (!Array.isArray(points) || points.length < 2) return [];
|
|
126
|
+
const cps = points.slice(0, 20).map(p => ({ x: Number(p.x) || 0, y: Number(p.y) || 0 }));
|
|
127
|
+
brushSize = clamp(Number(brushSize) || 5, 3, 100);
|
|
128
|
+
|
|
129
|
+
const result = [];
|
|
130
|
+
const stepsPerSeg = 12;
|
|
131
|
+
for (let i = 0; i < cps.length - 1; i++) {
|
|
132
|
+
const p0 = cps[Math.max(0, i - 1)];
|
|
133
|
+
const p1 = cps[i];
|
|
134
|
+
const p2 = cps[Math.min(cps.length - 1, i + 1)];
|
|
135
|
+
const p3 = cps[Math.min(cps.length - 1, i + 2)];
|
|
136
|
+
for (let s = 0; s < stepsPerSeg; s++) {
|
|
137
|
+
const t = s / stepsPerSeg;
|
|
138
|
+
const t2 = t * t, t3 = t2 * t;
|
|
139
|
+
result.push({
|
|
140
|
+
x: 0.5 * (2*p1.x + (-p0.x+p2.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*t2 + (-p0.x+3*p1.x-3*p2.x+p3.x)*t3),
|
|
141
|
+
y: 0.5 * (2*p1.y + (-p0.y+p2.y)*t + (2*p0.y-5*p1.y+4*p2.y-p3.y)*t2 + (-p0.y+3*p1.y-3*p2.y+p3.y)*t3),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
result.push(cps[cps.length - 1]);
|
|
146
|
+
|
|
147
|
+
return splitIntoStrokes(result, color, brushSize, opacity, pressureStyle);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function dashedLine(startX, startY, endX, endY, dashLength, gapLength, color, brushSize, pressureStyle) {
|
|
151
|
+
startX = Number(startX) || 0; startY = Number(startY) || 0;
|
|
152
|
+
endX = Number(endX) || 100; endY = Number(endY) || 0;
|
|
153
|
+
dashLength = clamp(Number(dashLength) || 10, 2, 50);
|
|
154
|
+
gapLength = clamp(Number(gapLength) || 5, 1, 50);
|
|
155
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 100);
|
|
156
|
+
|
|
157
|
+
const total = Math.hypot(endX - startX, endY - startY);
|
|
158
|
+
if (total < 1) return [];
|
|
159
|
+
const dx = (endX - startX) / total, dy = (endY - startY) / total;
|
|
160
|
+
|
|
161
|
+
const result = [];
|
|
162
|
+
let d = 0;
|
|
163
|
+
while (d < total && result.length < 200) {
|
|
164
|
+
const end = Math.min(d + dashLength, total);
|
|
165
|
+
result.push(makeStroke([
|
|
166
|
+
{ x: startX + dx * d, y: startY + dy * d },
|
|
167
|
+
{ x: startX + dx * end, y: startY + dy * end },
|
|
168
|
+
], color, brushSize, 0.9, pressureStyle));
|
|
169
|
+
d = end + gapLength;
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function arrow(startX, startY, endX, endY, headSize, color, brushSize, pressureStyle) {
|
|
175
|
+
startX = Number(startX) || 0; startY = Number(startY) || 0;
|
|
176
|
+
endX = Number(endX) || 100; endY = Number(endY) || 0;
|
|
177
|
+
headSize = clamp(Number(headSize) || 15, 3, 60);
|
|
178
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 100);
|
|
179
|
+
|
|
180
|
+
const dx = endX - startX, dy = endY - startY;
|
|
181
|
+
const angle = Math.atan2(dy, dx);
|
|
182
|
+
const ha = Math.PI * 0.8;
|
|
183
|
+
|
|
184
|
+
const result = [];
|
|
185
|
+
result.push(makeStroke([{ x: startX, y: startY }, { x: endX, y: endY }], color, brushSize, 0.9, pressureStyle));
|
|
186
|
+
result.push(makeStroke([
|
|
187
|
+
{ x: endX + Math.cos(angle + Math.PI - ha) * headSize, y: endY + Math.sin(angle + Math.PI - ha) * headSize },
|
|
188
|
+
{ x: endX, y: endY },
|
|
189
|
+
{ x: endX + Math.cos(angle + Math.PI + ha) * headSize, y: endY + Math.sin(angle + Math.PI + ha) * headSize },
|
|
190
|
+
], color, brushSize, 0.9, pressureStyle));
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function strokeText(cx, cy, text, charHeight, color, brushSize, opacity, rotation, pressureStyle) {
|
|
196
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
197
|
+
text = String(text || 'HELLO').toUpperCase().slice(0, 40);
|
|
198
|
+
charHeight = clamp(Number(charHeight) || 30, 5, 200);
|
|
199
|
+
brushSize = clamp(Number(brushSize) || 3, 3, 100);
|
|
200
|
+
opacity = clamp(Number(opacity) || 0.9, 0.01, 1);
|
|
201
|
+
rotation = (Number(rotation) || 0) * Math.PI / 180;
|
|
202
|
+
|
|
203
|
+
const charWidth = charHeight * 0.7;
|
|
204
|
+
const spacing = charHeight * 0.15;
|
|
205
|
+
const totalWidth = text.length * (charWidth + spacing) - spacing;
|
|
206
|
+
|
|
207
|
+
const result = [];
|
|
208
|
+
let cursorX = cx - totalWidth / 2;
|
|
209
|
+
|
|
210
|
+
for (const ch of text) {
|
|
211
|
+
const glyph = GLYPH_MAP[ch];
|
|
212
|
+
if (!glyph || glyph.length === 0) {
|
|
213
|
+
cursorX += charWidth + spacing;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const polyline of glyph) {
|
|
218
|
+
if (polyline.length < 2) continue;
|
|
219
|
+
const pts = polyline.map(([gx, gy]) => {
|
|
220
|
+
const lx = cursorX + gx * charWidth - cx;
|
|
221
|
+
const ly = cy + gy * charHeight - charHeight / 2 - cy;
|
|
222
|
+
return {
|
|
223
|
+
x: cx + lx * Math.cos(rotation) - ly * Math.sin(rotation),
|
|
224
|
+
y: cy + lx * Math.sin(rotation) + ly * Math.cos(rotation),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
result.push(makeStroke(pts, color, brushSize, opacity, pressureStyle));
|
|
228
|
+
if (result.length >= 200) break;
|
|
229
|
+
}
|
|
230
|
+
cursorX += charWidth + spacing;
|
|
231
|
+
if (result.length >= 200) break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result.slice(0, 200);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function alienGlyphs(cx, cy, count, glyphSize, arrangement, color, brushSize, opacity, pressureStyle) {
|
|
238
|
+
cx = Number(cx) || 0; cy = Number(cy) || 0;
|
|
239
|
+
count = clamp(Math.round(Number(count) || 8), 1, 20);
|
|
240
|
+
glyphSize = clamp(Number(glyphSize) || 25, 5, 100);
|
|
241
|
+
arrangement = String(arrangement || 'line').toLowerCase();
|
|
242
|
+
brushSize = clamp(Number(brushSize) || 2, 3, 100);
|
|
243
|
+
opacity = clamp(Number(opacity) || 0.85, 0.01, 1);
|
|
244
|
+
|
|
245
|
+
const result = [];
|
|
246
|
+
const spacing = glyphSize * 1.4;
|
|
247
|
+
|
|
248
|
+
const positions = [];
|
|
249
|
+
for (let i = 0; i < count; i++) {
|
|
250
|
+
let gx, gy;
|
|
251
|
+
if (arrangement === 'grid') {
|
|
252
|
+
const cols = Math.ceil(Math.sqrt(count));
|
|
253
|
+
const col = i % cols, row = Math.floor(i / cols);
|
|
254
|
+
const totalW = (cols - 1) * spacing, totalH = (Math.ceil(count / cols) - 1) * spacing;
|
|
255
|
+
gx = cx - totalW / 2 + col * spacing;
|
|
256
|
+
gy = cy - totalH / 2 + row * spacing;
|
|
257
|
+
} else if (arrangement === 'scatter') {
|
|
258
|
+
const r = glyphSize * 2 + Math.random() * glyphSize * 3;
|
|
259
|
+
const a = Math.random() * Math.PI * 2;
|
|
260
|
+
gx = cx + Math.cos(a) * r;
|
|
261
|
+
gy = cy + Math.sin(a) * r;
|
|
262
|
+
} else if (arrangement === 'circle') {
|
|
263
|
+
const a = (i / count) * Math.PI * 2 - Math.PI / 2;
|
|
264
|
+
const r = spacing * count / (Math.PI * 2) + glyphSize;
|
|
265
|
+
gx = cx + Math.cos(a) * r;
|
|
266
|
+
gy = cy + Math.sin(a) * r;
|
|
267
|
+
} else {
|
|
268
|
+
const totalW = (count - 1) * spacing;
|
|
269
|
+
gx = cx - totalW / 2 + i * spacing;
|
|
270
|
+
gy = cy;
|
|
271
|
+
}
|
|
272
|
+
positions.push({ x: gx, y: gy });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const pos of positions) {
|
|
276
|
+
if (result.length >= 200) break;
|
|
277
|
+
const half = glyphSize / 2;
|
|
278
|
+
|
|
279
|
+
const numElements = 2 + Math.floor(Math.random() * 3);
|
|
280
|
+
for (let e = 0; e < numElements && result.length < 200; e++) {
|
|
281
|
+
const type = Math.random();
|
|
282
|
+
|
|
283
|
+
if (type < 0.2) {
|
|
284
|
+
const x1 = pos.x + (Math.random() - 0.5) * glyphSize * 0.6;
|
|
285
|
+
const y1 = pos.y - half * (0.5 + Math.random() * 0.5);
|
|
286
|
+
const y2 = pos.y + half * (0.5 + Math.random() * 0.5);
|
|
287
|
+
const lean = (Math.random() - 0.5) * glyphSize * 0.3;
|
|
288
|
+
result.push(makeStroke([{ x: x1, y: y1 }, { x: x1 + lean, y: y2 }], color, brushSize, opacity, pressureStyle));
|
|
289
|
+
} else if (type < 0.4) {
|
|
290
|
+
const pts = [];
|
|
291
|
+
const arcSteps = 8 + Math.floor(Math.random() * 8);
|
|
292
|
+
const startA = Math.random() * Math.PI * 2;
|
|
293
|
+
const sweep = (0.5 + Math.random()) * Math.PI;
|
|
294
|
+
const r = half * (0.3 + Math.random() * 0.5);
|
|
295
|
+
const acx = pos.x + (Math.random() - 0.5) * half * 0.5;
|
|
296
|
+
const acy = pos.y + (Math.random() - 0.5) * half * 0.5;
|
|
297
|
+
for (let s = 0; s <= arcSteps; s++) {
|
|
298
|
+
const a = startA + (s / arcSteps) * sweep;
|
|
299
|
+
pts.push({ x: acx + Math.cos(a) * r, y: acy + Math.sin(a) * r });
|
|
300
|
+
}
|
|
301
|
+
result.push(makeStroke(pts, color, brushSize, opacity, pressureStyle));
|
|
302
|
+
} else if (type < 0.55) {
|
|
303
|
+
const dots = 1 + Math.floor(Math.random() * 3);
|
|
304
|
+
for (let d = 0; d < dots && result.length < 200; d++) {
|
|
305
|
+
const dx = pos.x + (Math.random() - 0.5) * glyphSize * 0.5;
|
|
306
|
+
const dy = pos.y + (Math.random() - 0.5) * glyphSize * 0.6;
|
|
307
|
+
result.push(makeStroke([{ x: dx, y: dy }, { x: dx + 1, y: dy + 1 }], color, brushSize * 1.5, opacity, pressureStyle));
|
|
308
|
+
}
|
|
309
|
+
} else if (type < 0.7) {
|
|
310
|
+
const y = pos.y + (Math.random() - 0.5) * glyphSize * 0.6;
|
|
311
|
+
const x1 = pos.x - half * (0.2 + Math.random() * 0.4);
|
|
312
|
+
const x2 = pos.x + half * (0.2 + Math.random() * 0.4);
|
|
313
|
+
result.push(makeStroke([{ x: x1, y }, { x: x2, y }], color, brushSize, opacity, pressureStyle));
|
|
314
|
+
} else if (type < 0.85) {
|
|
315
|
+
const pts = [];
|
|
316
|
+
const segs = 2 + Math.floor(Math.random() * 3);
|
|
317
|
+
let px = pos.x + (Math.random() - 0.5) * half;
|
|
318
|
+
let py = pos.y - half * 0.8;
|
|
319
|
+
pts.push({ x: px, y: py });
|
|
320
|
+
for (let s = 0; s < segs; s++) {
|
|
321
|
+
px += (Math.random() - 0.5) * glyphSize * 0.5;
|
|
322
|
+
py += glyphSize * (0.3 + Math.random() * 0.3) / segs;
|
|
323
|
+
pts.push({ x: px, y: py });
|
|
324
|
+
}
|
|
325
|
+
result.push(makeStroke(pts, color, brushSize, opacity, pressureStyle));
|
|
326
|
+
} else {
|
|
327
|
+
const r = half * (0.15 + Math.random() * 0.25);
|
|
328
|
+
const ringCx = pos.x + (Math.random() - 0.5) * half * 0.6;
|
|
329
|
+
const ringCy = pos.y + (Math.random() - 0.5) * half * 0.6;
|
|
330
|
+
const pts = [];
|
|
331
|
+
const steps = 12;
|
|
332
|
+
for (let s = 0; s <= steps; s++) {
|
|
333
|
+
const a = (s / steps) * Math.PI * 2;
|
|
334
|
+
pts.push({ x: ringCx + Math.cos(a) * r, y: ringCy + Math.sin(a) * r });
|
|
335
|
+
}
|
|
336
|
+
result.push(makeStroke(pts, color, brushSize, opacity, pressureStyle));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result.slice(0, 200);
|
|
342
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Algorithm Guide
|
|
2
|
+
|
|
3
|
+
How to write custom drawing algorithms for the ClawDraw skill.
|
|
4
|
+
|
|
5
|
+
## The Stroke Pipeline
|
|
6
|
+
|
|
7
|
+
Every drawing follows the same pipeline:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Generate points --> Create strokes --> Apply symmetry --> Send to relay
|
|
11
|
+
(your code) (makeStroke) (applySymmetry) (WebSocket)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
1. Your algorithm generates arrays of `{x, y}` points
|
|
15
|
+
2. `makeStroke()` wraps them with pressure, timestamps, brush settings, and a unique ID
|
|
16
|
+
3. Symmetry copies are generated if a symmetry mode is active
|
|
17
|
+
4. Strokes are batched and sent over WebSocket to the relay
|
|
18
|
+
|
|
19
|
+
## Available Helpers
|
|
20
|
+
|
|
21
|
+
Import from `primitives/helpers.mjs`:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import {
|
|
25
|
+
clamp, lerp, // Math
|
|
26
|
+
hexToRgb, rgbToHex, lerpColor, // Color interpolation
|
|
27
|
+
samplePalette, PALETTES, // Named color palettes
|
|
28
|
+
noise2d, // 2D Perlin noise
|
|
29
|
+
makeStroke, splitIntoStrokes, // Stroke creation
|
|
30
|
+
clipLineToRect, // Line-rect intersection
|
|
31
|
+
} from '../primitives/helpers.mjs';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pattern: Parametric Curves
|
|
35
|
+
|
|
36
|
+
Express shapes as x=f(t), y=g(t) where t goes from 0 to 1.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
function customCurve(cx, cy, radius) {
|
|
40
|
+
const points = [];
|
|
41
|
+
const steps = 100;
|
|
42
|
+
for (let i = 0; i <= steps; i++) {
|
|
43
|
+
const t = i / steps;
|
|
44
|
+
const angle = t * Math.PI * 2;
|
|
45
|
+
// Rose curve: r = cos(3*theta)
|
|
46
|
+
const r = radius * Math.cos(3 * angle);
|
|
47
|
+
points.push({
|
|
48
|
+
x: cx + r * Math.cos(angle),
|
|
49
|
+
y: cy + r * Math.sin(angle),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return splitIntoStrokes(points, '#ff0000', 3, 0.9);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This pattern works for circles, spirals, lissajous curves, rose curves, cardioids, and any curve expressible in polar or parametric form.
|
|
57
|
+
|
|
58
|
+
## Pattern: Particle Systems
|
|
59
|
+
|
|
60
|
+
Simulate particles moving through space with forces.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
function particleTrails(cx, cy, count, steps) {
|
|
64
|
+
const strokes = [];
|
|
65
|
+
for (let p = 0; p < count; p++) {
|
|
66
|
+
let x = cx + (Math.random() - 0.5) * 200;
|
|
67
|
+
let y = cy + (Math.random() - 0.5) * 200;
|
|
68
|
+
let vx = 0, vy = 0;
|
|
69
|
+
const pts = [{ x, y }];
|
|
70
|
+
|
|
71
|
+
for (let s = 0; s < steps; s++) {
|
|
72
|
+
// Attraction toward center
|
|
73
|
+
const dx = cx - x, dy = cy - y;
|
|
74
|
+
const dist = Math.hypot(dx, dy) || 1;
|
|
75
|
+
vx += (dx / dist) * 0.5;
|
|
76
|
+
vy += (dy / dist) * 0.5;
|
|
77
|
+
// Random perturbation
|
|
78
|
+
vx += (Math.random() - 0.5) * 2;
|
|
79
|
+
vy += (Math.random() - 0.5) * 2;
|
|
80
|
+
x += vx;
|
|
81
|
+
y += vy;
|
|
82
|
+
pts.push({ x, y });
|
|
83
|
+
}
|
|
84
|
+
strokes.push(makeStroke(pts, '#ffffff', 2, 0.7));
|
|
85
|
+
}
|
|
86
|
+
return strokes;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Variations: random walks, flocking (boids), attraction/repulsion, gravity simulation.
|
|
91
|
+
|
|
92
|
+
## Pattern: Fractal / Recursive
|
|
93
|
+
|
|
94
|
+
Use recursion to create self-similar branching structures.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
function fractalBranch(x, y, angle, length, depth, strokes) {
|
|
98
|
+
if (depth <= 0 || strokes.length >= 200) return;
|
|
99
|
+
|
|
100
|
+
const endX = x + Math.cos(angle) * length;
|
|
101
|
+
const endY = y + Math.sin(angle) * length;
|
|
102
|
+
strokes.push(makeStroke(
|
|
103
|
+
[{ x, y }, { x: endX, y: endY }],
|
|
104
|
+
'#ffffff', Math.max(1, depth), 0.8
|
|
105
|
+
));
|
|
106
|
+
|
|
107
|
+
const newLen = length * 0.7;
|
|
108
|
+
fractalBranch(endX, endY, angle - 0.4, newLen, depth - 1, strokes);
|
|
109
|
+
fractalBranch(endX, endY, angle + 0.4, newLen, depth - 1, strokes);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function tree(cx, cy) {
|
|
113
|
+
const strokes = [];
|
|
114
|
+
fractalBranch(cx, cy, -Math.PI / 2, 80, 7, strokes);
|
|
115
|
+
return strokes.slice(0, 200);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Always include a depth limit and a stroke count check to prevent runaway recursion.
|
|
120
|
+
|
|
121
|
+
## Pattern: Noise-Based (Perlin Noise, Flow Fields)
|
|
122
|
+
|
|
123
|
+
Use the built-in `noise2d(x, y)` function for smooth randomness.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
function noiseField(cx, cy, width, height) {
|
|
127
|
+
const strokes = [];
|
|
128
|
+
const scale = 0.005; // Lower = smoother, larger features
|
|
129
|
+
|
|
130
|
+
for (let p = 0; p < 30; p++) {
|
|
131
|
+
let x = cx + (Math.random() - 0.5) * width;
|
|
132
|
+
let y = cy + (Math.random() - 0.5) * height;
|
|
133
|
+
const pts = [{ x, y }];
|
|
134
|
+
|
|
135
|
+
for (let s = 0; s < 50; s++) {
|
|
136
|
+
const angle = noise2d(x * scale, y * scale) * Math.PI * 2;
|
|
137
|
+
x += Math.cos(angle) * 5;
|
|
138
|
+
y += Math.sin(angle) * 5;
|
|
139
|
+
pts.push({ x, y });
|
|
140
|
+
}
|
|
141
|
+
strokes.push(makeStroke(pts, '#44aaff', 2, 0.6));
|
|
142
|
+
}
|
|
143
|
+
return strokes;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Noise scale controls the feature size: 0.001 = very smooth/large, 0.1 = very noisy/small.
|
|
148
|
+
|
|
149
|
+
## Tips
|
|
150
|
+
|
|
151
|
+
### Color Gradients
|
|
152
|
+
|
|
153
|
+
Use `lerpColor()` to interpolate between two colors, or `samplePalette()` for scientific palettes:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
// Linear interpolation between two colors
|
|
157
|
+
const midColor = lerpColor('#ff0000', '#0000ff', 0.5); // purple
|
|
158
|
+
|
|
159
|
+
// Sample a named palette at position t (0 to 1)
|
|
160
|
+
const warmColor = samplePalette('magma', 0.7); // orangey
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Pressure for Line Weight Variation
|
|
164
|
+
|
|
165
|
+
Use the `pressureStyle` parameter for automatic variation, or set pressure directly on points:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
const points = myPoints.map((p, i) => ({
|
|
169
|
+
x: p.x,
|
|
170
|
+
y: p.y,
|
|
171
|
+
pressure: Math.sin((i / myPoints.length) * Math.PI), // Thick in middle
|
|
172
|
+
}));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
When a point has an explicit `pressure` property, `makeStroke()` preserves it instead of applying a pressure style.
|
|
176
|
+
|
|
177
|
+
### Jitter for Organic Feel
|
|
178
|
+
|
|
179
|
+
Add small random offsets to make shapes look hand-drawn:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const wobble = radius * (1 + (Math.random() - 0.5) * 0.04);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Performance Guidelines
|
|
186
|
+
|
|
187
|
+
| Constraint | Limit | Why |
|
|
188
|
+
|-----------|-------|-----|
|
|
189
|
+
| Points per stroke | 5,000 max | Server-enforced, `splitIntoStrokes` handles this automatically |
|
|
190
|
+
| Strokes per batch | 200 max | WebSocket message size limit |
|
|
191
|
+
| Total points per message | 10,000 max | Prevents relay overload |
|
|
192
|
+
| Iterations/recursion | Use explicit caps | Prevent infinite loops |
|
|
193
|
+
|
|
194
|
+
### Batching Strategy
|
|
195
|
+
|
|
196
|
+
If your algorithm generates more than 200 strokes, batch them:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const allStrokes = generateManyStrokes();
|
|
200
|
+
const BATCH = 200;
|
|
201
|
+
for (let i = 0; i < allStrokes.length; i += BATCH) {
|
|
202
|
+
sendBatch(allStrokes.slice(i, i + BATCH));
|
|
203
|
+
await sleep(100); // Brief pause between batches
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Keeping Point Counts Low
|
|
208
|
+
|
|
209
|
+
- For circles, scale step count with radius: `steps = clamp(radius * 0.5, 24, 200)`
|
|
210
|
+
- For recursive structures, limit total depth and check stroke count at each level
|
|
211
|
+
- For particle systems, limit trace length: 50-200 steps is usually sufficient
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Community Algorithm Contribution Guide
|
|
2
|
+
|
|
3
|
+
Add your own drawing algorithms to the ClawDraw primitive library.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. Fork the repository
|
|
8
|
+
2. Copy `community/_template.mjs` to `community/your-primitive.mjs`
|
|
9
|
+
3. Implement your algorithm following the template pattern
|
|
10
|
+
4. Test locally
|
|
11
|
+
5. Submit a pull request
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
Your file must be a single `.mjs` file in the `community/` directory:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
packages/skill/community/
|
|
19
|
+
_template.mjs # Reference template (do not modify)
|
|
20
|
+
your-primitive.mjs # Your contribution
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
### Exports
|
|
26
|
+
|
|
27
|
+
Every community primitive must export:
|
|
28
|
+
|
|
29
|
+
1. **`METADATA`** -- Object or array of objects with:
|
|
30
|
+
- `name` (string): Unique camelCase identifier
|
|
31
|
+
- `description` (string): One-line description
|
|
32
|
+
- `category`: Must be `'community'`
|
|
33
|
+
- `author` (string): Your GitHub username
|
|
34
|
+
- `parameters` (object): Parameter definitions with `type`, `required`, and `description`
|
|
35
|
+
|
|
36
|
+
2. **Named function** matching `METADATA.name` -- The drawing function itself
|
|
37
|
+
|
|
38
|
+
### Function Rules
|
|
39
|
+
|
|
40
|
+
- Accept parameters as positional arguments matching the order in `METADATA.parameters`
|
|
41
|
+
- Return an array of stroke objects (use `makeStroke` / `splitIntoStrokes` from helpers)
|
|
42
|
+
- No external dependencies -- only import from `../primitives/helpers.mjs`
|
|
43
|
+
- Maximum file size: 50KB
|
|
44
|
+
- Must not modify global state
|
|
45
|
+
- Must terminate in bounded time (no infinite loops)
|
|
46
|
+
- Respect limits: max 200 strokes, max 5000 points per stroke
|
|
47
|
+
|
|
48
|
+
### Available Imports
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import {
|
|
52
|
+
clamp, lerp,
|
|
53
|
+
hexToRgb, rgbToHex, lerpColor,
|
|
54
|
+
samplePalette, PALETTES,
|
|
55
|
+
noise2d,
|
|
56
|
+
makeStroke, splitIntoStrokes,
|
|
57
|
+
clipLineToRect,
|
|
58
|
+
} from '../primitives/helpers.mjs';
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Naming
|
|
62
|
+
|
|
63
|
+
- Use camelCase for the primitive name: `myAlgorithm`, not `my-algorithm`
|
|
64
|
+
- Choose a descriptive name that hints at the visual output
|
|
65
|
+
- Avoid names that conflict with built-in primitives (see PRIMITIVES.md)
|
|
66
|
+
|
|
67
|
+
## Submission
|
|
68
|
+
|
|
69
|
+
Submit a PR with:
|
|
70
|
+
- Your single `.mjs` file in `community/`
|
|
71
|
+
- A brief description of the algorithm and what it draws
|
|
72
|
+
- At least one example invocation showing the parameters
|