card-gate-frame 1.0.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 +152 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core-DzM1ds_q.js +2536 -0
- package/dist/core-DzM1ds_q.js.map +1 -0
- package/dist/index-B0WElJq8.d.ts +142 -0
- package/dist/index-B0WElJq8.d.ts.map +1 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +728 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,2536 @@
|
|
|
1
|
+
//#region src/core/geometry-utils.ts
|
|
2
|
+
const COORDINATE_SCALE$1 = 1e4;
|
|
3
|
+
const canonicalNumber = (value) => {
|
|
4
|
+
const rounded = Math.round(value * COORDINATE_SCALE$1) / COORDINATE_SCALE$1;
|
|
5
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
6
|
+
};
|
|
7
|
+
const canonicalizeCommand = (command) => {
|
|
8
|
+
switch (command.kind) {
|
|
9
|
+
case "M":
|
|
10
|
+
case "L": return {
|
|
11
|
+
kind: command.kind,
|
|
12
|
+
x: canonicalNumber(command.x),
|
|
13
|
+
y: canonicalNumber(command.y)
|
|
14
|
+
};
|
|
15
|
+
case "C": return {
|
|
16
|
+
kind: "C",
|
|
17
|
+
x1: canonicalNumber(command.x1),
|
|
18
|
+
y1: canonicalNumber(command.y1),
|
|
19
|
+
x2: canonicalNumber(command.x2),
|
|
20
|
+
y2: canonicalNumber(command.y2),
|
|
21
|
+
x: canonicalNumber(command.x),
|
|
22
|
+
y: canonicalNumber(command.y)
|
|
23
|
+
};
|
|
24
|
+
case "Q": return {
|
|
25
|
+
kind: "Q",
|
|
26
|
+
x1: canonicalNumber(command.x1),
|
|
27
|
+
y1: canonicalNumber(command.y1),
|
|
28
|
+
x: canonicalNumber(command.x),
|
|
29
|
+
y: canonicalNumber(command.y)
|
|
30
|
+
};
|
|
31
|
+
case "Z": return { kind: "Z" };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const canonicalizePath = (path) => path.map(canonicalizeCommand);
|
|
35
|
+
const mirrorCommand = (command, width) => {
|
|
36
|
+
switch (command.kind) {
|
|
37
|
+
case "M":
|
|
38
|
+
case "L": return {
|
|
39
|
+
kind: command.kind,
|
|
40
|
+
x: canonicalNumber(width - command.x),
|
|
41
|
+
y: command.y
|
|
42
|
+
};
|
|
43
|
+
case "C": return {
|
|
44
|
+
kind: "C",
|
|
45
|
+
x1: canonicalNumber(width - command.x1),
|
|
46
|
+
y1: command.y1,
|
|
47
|
+
x2: canonicalNumber(width - command.x2),
|
|
48
|
+
y2: command.y2,
|
|
49
|
+
x: canonicalNumber(width - command.x),
|
|
50
|
+
y: command.y
|
|
51
|
+
};
|
|
52
|
+
case "Q": return {
|
|
53
|
+
kind: "Q",
|
|
54
|
+
x1: canonicalNumber(width - command.x1),
|
|
55
|
+
y1: command.y1,
|
|
56
|
+
x: canonicalNumber(width - command.x),
|
|
57
|
+
y: command.y
|
|
58
|
+
};
|
|
59
|
+
case "Z": return { kind: "Z" };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const mirrorPath = (path, width) => path.map((command) => mirrorCommand(command, width));
|
|
63
|
+
const deepFreeze = (value) => {
|
|
64
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
65
|
+
for (const nested of Object.values(value)) deepFreeze(nested);
|
|
66
|
+
Object.freeze(value);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/core/utf8.ts
|
|
73
|
+
const Encoder = globalThis.TextEncoder;
|
|
74
|
+
const encoder = new Encoder();
|
|
75
|
+
const encodeUtf8 = (value) => encoder.encode(value);
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/core/courtyard.ts
|
|
79
|
+
const DEFAULT_INSET = 8;
|
|
80
|
+
const DEFAULT_STROKE_WIDTH = 2;
|
|
81
|
+
const DEFAULT_DENSITY = .55;
|
|
82
|
+
const DEFAULT_CURVATURE = .65;
|
|
83
|
+
const normalizeDimensions$1 = (dimensions) => {
|
|
84
|
+
const { width, height } = dimensions;
|
|
85
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) throw new TypeError("Gate dimensions must be finite numbers");
|
|
86
|
+
if (width < 160 || width > 1600 || height < 96 || height > 1200) throw new RangeError("Gate dimensions are outside the supported range");
|
|
87
|
+
const aspectRatio = width / height;
|
|
88
|
+
if (aspectRatio < .5 || aspectRatio > 6) throw new RangeError("Gate dimensions have an unsupported aspect ratio");
|
|
89
|
+
return {
|
|
90
|
+
width: canonicalNumber(width),
|
|
91
|
+
height: canonicalNumber(height)
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const normalizeSeed$1 = (seed) => {
|
|
95
|
+
if (typeof seed === "number") {
|
|
96
|
+
if (!Number.isFinite(seed)) throw new TypeError("Gate seed numbers must be finite");
|
|
97
|
+
return Object.is(seed, -0) ? 0 : seed;
|
|
98
|
+
}
|
|
99
|
+
if (typeof seed !== "string") throw new TypeError("Gate seeds must be strings or finite numbers");
|
|
100
|
+
if (seed.length === 0) throw new TypeError("Gate seed strings must not be empty");
|
|
101
|
+
return seed;
|
|
102
|
+
};
|
|
103
|
+
const validateGenerationOptions = (options) => {
|
|
104
|
+
if (options.family !== "courtyard") throw new TypeError("GF-01 requires family: \"courtyard\"");
|
|
105
|
+
if (options.generationVersion !== void 0 && options.generationVersion !== 1) throw new TypeError("GF-01 supports only generationVersion 1");
|
|
106
|
+
};
|
|
107
|
+
const normalizeShapeControl = (value, fallback, name) => {
|
|
108
|
+
const normalized = value === void 0 ? fallback : value;
|
|
109
|
+
if (!Number.isFinite(normalized)) throw new TypeError(`${name} must be a finite number`);
|
|
110
|
+
if (normalized < 0 || normalized > 1) throw new RangeError(`${name} must be between 0 and 1 inclusive`);
|
|
111
|
+
return canonicalNumber(normalized);
|
|
112
|
+
};
|
|
113
|
+
const hashByte = (hash, byte) => Math.imul(hash ^ byte, 16777619) >>> 0;
|
|
114
|
+
const hashUtf8 = (value) => {
|
|
115
|
+
let hash = 2166136261;
|
|
116
|
+
for (const byte of encodeUtf8(value)) hash = hashByte(hash, byte);
|
|
117
|
+
return hash;
|
|
118
|
+
};
|
|
119
|
+
const seedState = (seed) => hashUtf8(`${typeof seed === "number" ? "number" : "string"}:${String(seed)}`);
|
|
120
|
+
const nextUint32 = (state) => {
|
|
121
|
+
let next = state || 1831565813;
|
|
122
|
+
next ^= next << 13;
|
|
123
|
+
next ^= next >>> 17;
|
|
124
|
+
next ^= next << 5;
|
|
125
|
+
return next >>> 0;
|
|
126
|
+
};
|
|
127
|
+
function generateGate$1(dimensions, options) {
|
|
128
|
+
validateGenerationOptions(options);
|
|
129
|
+
const { width, height } = normalizeDimensions$1(dimensions);
|
|
130
|
+
const seed = normalizeSeed$1(options.seed);
|
|
131
|
+
const density = normalizeShapeControl(options.density, DEFAULT_DENSITY, "density");
|
|
132
|
+
const curvature = normalizeShapeControl(options.curvature, DEFAULT_CURVATURE, "curvature");
|
|
133
|
+
const edge = canonicalNumber(9);
|
|
134
|
+
const railY = canonicalNumber(Math.max(edge + 24, height * .275));
|
|
135
|
+
const bottomY = canonicalNumber(height - edge);
|
|
136
|
+
const frameBand = canonicalNumber(Math.max(12, Math.min(24, Math.min(width, height) * .06)));
|
|
137
|
+
const safeLeft = canonicalNumber(edge + frameBand);
|
|
138
|
+
const safeRight = canonicalNumber(width - safeLeft);
|
|
139
|
+
const safeTop = canonicalNumber(railY + frameBand);
|
|
140
|
+
const safeBottom = canonicalNumber(bottomY - frameBand);
|
|
141
|
+
const centerX = canonicalNumber(width / 2);
|
|
142
|
+
const surfacePath = [
|
|
143
|
+
{
|
|
144
|
+
kind: "M",
|
|
145
|
+
x: edge,
|
|
146
|
+
y: railY
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
kind: "L",
|
|
150
|
+
x: width - edge,
|
|
151
|
+
y: railY
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
kind: "L",
|
|
155
|
+
x: width - edge,
|
|
156
|
+
y: bottomY
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
kind: "L",
|
|
160
|
+
x: edge,
|
|
161
|
+
y: bottomY
|
|
162
|
+
},
|
|
163
|
+
{ kind: "Z" },
|
|
164
|
+
{
|
|
165
|
+
kind: "M",
|
|
166
|
+
x: safeLeft,
|
|
167
|
+
y: safeTop
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
kind: "L",
|
|
171
|
+
x: safeLeft,
|
|
172
|
+
y: safeBottom
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
kind: "L",
|
|
176
|
+
x: safeRight,
|
|
177
|
+
y: safeBottom
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
kind: "L",
|
|
181
|
+
x: safeRight,
|
|
182
|
+
y: safeTop
|
|
183
|
+
},
|
|
184
|
+
{ kind: "Z" }
|
|
185
|
+
];
|
|
186
|
+
const structuralPath = [
|
|
187
|
+
{
|
|
188
|
+
kind: "M",
|
|
189
|
+
x: edge,
|
|
190
|
+
y: bottomY
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: "L",
|
|
194
|
+
x: edge,
|
|
195
|
+
y: railY
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
kind: "L",
|
|
199
|
+
x: width - edge,
|
|
200
|
+
y: railY
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: "L",
|
|
204
|
+
x: width - edge,
|
|
205
|
+
y: bottomY
|
|
206
|
+
},
|
|
207
|
+
{ kind: "Z" }
|
|
208
|
+
];
|
|
209
|
+
const seedVariation = nextUint32(seedState(seed)) / 4294967295;
|
|
210
|
+
const availableScrollLift = canonicalNumber(railY - edge - 4);
|
|
211
|
+
const scrollLift = canonicalNumber(Math.min(availableScrollLift, 27 + seedVariation * 6));
|
|
212
|
+
const scrollStart = safeLeft;
|
|
213
|
+
const availableScrollReach = canonicalNumber(centerX - 24 - scrollStart);
|
|
214
|
+
const baseScrollReach = Math.min(width * .22, 66 + seedVariation * 8, availableScrollReach);
|
|
215
|
+
const scrollReach = canonicalNumber(baseScrollReach * (.86 + density * .14));
|
|
216
|
+
const scrollEnd = canonicalNumber(scrollStart + scrollReach);
|
|
217
|
+
const curvatureScale = .8 + curvature * .3;
|
|
218
|
+
const leftScroll = [
|
|
219
|
+
{
|
|
220
|
+
kind: "M",
|
|
221
|
+
x: scrollStart,
|
|
222
|
+
y: railY
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "C",
|
|
226
|
+
x1: scrollStart + 5 * curvatureScale,
|
|
227
|
+
y1: railY - 20 * curvatureScale,
|
|
228
|
+
x2: scrollEnd - 18 * curvatureScale,
|
|
229
|
+
y2: railY - scrollLift,
|
|
230
|
+
x: scrollEnd,
|
|
231
|
+
y: railY - scrollLift
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
kind: "C",
|
|
235
|
+
x1: scrollEnd + 18 * curvatureScale,
|
|
236
|
+
y1: railY - scrollLift,
|
|
237
|
+
x2: scrollEnd + 22 * curvatureScale,
|
|
238
|
+
y2: railY - 9 * curvatureScale,
|
|
239
|
+
x: scrollEnd + 10,
|
|
240
|
+
y: railY - 6
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
kind: "C",
|
|
244
|
+
x1: scrollEnd,
|
|
245
|
+
y1: railY - 3,
|
|
246
|
+
x2: scrollEnd - 8 * curvatureScale,
|
|
247
|
+
y2: railY - 10 * curvatureScale,
|
|
248
|
+
x: scrollEnd - 3,
|
|
249
|
+
y: railY - 16
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
const spear = [
|
|
253
|
+
{
|
|
254
|
+
kind: "M",
|
|
255
|
+
x: centerX,
|
|
256
|
+
y: railY
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
kind: "L",
|
|
260
|
+
x: centerX,
|
|
261
|
+
y: edge + 18
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
kind: "M",
|
|
265
|
+
x: centerX,
|
|
266
|
+
y: edge
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
kind: "L",
|
|
270
|
+
x: centerX + 7,
|
|
271
|
+
y: edge + 12
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
kind: "L",
|
|
275
|
+
x: centerX,
|
|
276
|
+
y: edge + 19
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
kind: "L",
|
|
280
|
+
x: centerX - 7,
|
|
281
|
+
y: edge + 12
|
|
282
|
+
},
|
|
283
|
+
{ kind: "Z" }
|
|
284
|
+
];
|
|
285
|
+
const geometry = {
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
generationVersion: 1,
|
|
288
|
+
seed,
|
|
289
|
+
dimensions: {
|
|
290
|
+
width,
|
|
291
|
+
height
|
|
292
|
+
},
|
|
293
|
+
viewBox: {
|
|
294
|
+
minX: 0,
|
|
295
|
+
minY: 0,
|
|
296
|
+
width,
|
|
297
|
+
height
|
|
298
|
+
},
|
|
299
|
+
options: {
|
|
300
|
+
generationVersion: 1,
|
|
301
|
+
family: "courtyard",
|
|
302
|
+
density,
|
|
303
|
+
curvature,
|
|
304
|
+
symmetry: 1,
|
|
305
|
+
crest: "spear",
|
|
306
|
+
inset: DEFAULT_INSET,
|
|
307
|
+
strokeWidth: DEFAULT_STROKE_WIDTH
|
|
308
|
+
},
|
|
309
|
+
bounds: {
|
|
310
|
+
left: edge,
|
|
311
|
+
right: canonicalNumber(width - edge),
|
|
312
|
+
railY,
|
|
313
|
+
bottomY
|
|
314
|
+
},
|
|
315
|
+
contentInsets: {
|
|
316
|
+
top: safeTop,
|
|
317
|
+
right: canonicalNumber(width - safeRight),
|
|
318
|
+
bottom: canonicalNumber(height - safeBottom),
|
|
319
|
+
left: safeLeft
|
|
320
|
+
},
|
|
321
|
+
surfacePath: canonicalizePath(surfacePath),
|
|
322
|
+
structuralPaths: [canonicalizePath(structuralPath)],
|
|
323
|
+
decorativePaths: [
|
|
324
|
+
canonicalizePath(leftScroll),
|
|
325
|
+
mirrorPath(canonicalizePath(leftScroll), width),
|
|
326
|
+
canonicalizePath(spear)
|
|
327
|
+
]
|
|
328
|
+
};
|
|
329
|
+
return deepFreeze(geometry);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/core/labelled-sampler.ts
|
|
334
|
+
function createLabelledSampler(version) {
|
|
335
|
+
const UINT32_RANGE = 4294967296;
|
|
336
|
+
const SAMPLE_DOMAIN = `gate-frame/v${version}/sample\0`;
|
|
337
|
+
const LABEL_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*$/;
|
|
338
|
+
const pushUint32Le = (bytes, value) => {
|
|
339
|
+
bytes.push(value & 255, value >>> 8 & 255, value >>> 16 & 255, value >>> 24);
|
|
340
|
+
};
|
|
341
|
+
function sampleKeyBytes(seed, label, index) {
|
|
342
|
+
if (typeof seed === "number" && !Number.isFinite(seed)) throw new TypeError("Sample seed numbers must be finite");
|
|
343
|
+
if (typeof seed !== "string" && typeof seed !== "number") throw new TypeError("Sample seeds must be strings or finite numbers");
|
|
344
|
+
if (!LABEL_PATTERN.test(label)) throw new TypeError(`Sample labels must use the generation-v${version} labelled domain`);
|
|
345
|
+
if (index !== void 0 && (!Number.isInteger(index) || index < 0 || index > 4294967295)) throw new RangeError("Sample indices must be unsigned 32-bit integers");
|
|
346
|
+
const normalizedSeed = typeof seed === "number" && Object.is(seed, -0) ? 0 : seed;
|
|
347
|
+
const seedBytes = encodeUtf8(String(normalizedSeed));
|
|
348
|
+
const labelBytes = encodeUtf8(label);
|
|
349
|
+
const bytes = Array.from(encodeUtf8(SAMPLE_DOMAIN));
|
|
350
|
+
bytes.push(typeof normalizedSeed === "number" ? 110 : 115);
|
|
351
|
+
pushUint32Le(bytes, seedBytes.length);
|
|
352
|
+
bytes.push(...seedBytes);
|
|
353
|
+
pushUint32Le(bytes, labelBytes.length);
|
|
354
|
+
bytes.push(...labelBytes);
|
|
355
|
+
bytes.push(index === void 0 ? 0 : 1);
|
|
356
|
+
if (index !== void 0) pushUint32Le(bytes, index);
|
|
357
|
+
return Uint8Array.from(bytes);
|
|
358
|
+
}
|
|
359
|
+
function sampleUint32(seed, label, index) {
|
|
360
|
+
let hash = 2166136261;
|
|
361
|
+
for (const byte of sampleKeyBytes(seed, label, index)) hash = Math.imul(hash ^ byte, 16777619) >>> 0;
|
|
362
|
+
hash ^= hash >>> 16;
|
|
363
|
+
hash = Math.imul(hash, 2246822507) >>> 0;
|
|
364
|
+
hash ^= hash >>> 13;
|
|
365
|
+
hash = Math.imul(hash, 3266489909) >>> 0;
|
|
366
|
+
hash ^= hash >>> 16;
|
|
367
|
+
return hash >>> 0;
|
|
368
|
+
}
|
|
369
|
+
function sampleScalar(seed, label, index) {
|
|
370
|
+
return sampleUint32(seed, label, index) / UINT32_RANGE;
|
|
371
|
+
}
|
|
372
|
+
function sampleIndex(seed, label, count, index) {
|
|
373
|
+
if (!Number.isInteger(count) || count < 1 || count > 65536) throw new RangeError("Sample counts must be integers from 1 through 65536");
|
|
374
|
+
return Math.floor(sampleUint32(seed, label, index) * count / UINT32_RANGE);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
sampleKeyBytes,
|
|
378
|
+
sampleUint32,
|
|
379
|
+
sampleScalar,
|
|
380
|
+
sampleIndex
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region src/core/v2/sampler.ts
|
|
386
|
+
const sampler$1 = createLabelledSampler(2);
|
|
387
|
+
const sampleKeyBytes$1 = sampler$1.sampleKeyBytes;
|
|
388
|
+
const sampleUint32$1 = sampler$1.sampleUint32;
|
|
389
|
+
const sampleScalar$1 = sampler$1.sampleScalar;
|
|
390
|
+
const sampleIndex$1 = sampler$1.sampleIndex;
|
|
391
|
+
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/core/v3/sampler.ts
|
|
394
|
+
const sampler = createLabelledSampler(3);
|
|
395
|
+
const sampleKeyBytes = sampler.sampleKeyBytes;
|
|
396
|
+
const sampleUint32 = sampler.sampleUint32;
|
|
397
|
+
const sampleScalar = sampler.sampleScalar;
|
|
398
|
+
const sampleIndex = sampler.sampleIndex;
|
|
399
|
+
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/core/family-options.ts
|
|
402
|
+
const INSET = 8;
|
|
403
|
+
const STROKE_WIDTH = 2;
|
|
404
|
+
const FAMILY_ORDER = [
|
|
405
|
+
"courtyard",
|
|
406
|
+
"fleuron",
|
|
407
|
+
"arcade",
|
|
408
|
+
"vine",
|
|
409
|
+
"fan",
|
|
410
|
+
"volute"
|
|
411
|
+
];
|
|
412
|
+
const SPEAR_DIAMOND_CRESTS = [
|
|
413
|
+
"spear",
|
|
414
|
+
"diamond",
|
|
415
|
+
"none"
|
|
416
|
+
];
|
|
417
|
+
const FLEUR_SPEAR_CRESTS = [
|
|
418
|
+
"fleur",
|
|
419
|
+
"spear",
|
|
420
|
+
"none"
|
|
421
|
+
];
|
|
422
|
+
const DIAMOND_SPEAR_CRESTS = [
|
|
423
|
+
"diamond",
|
|
424
|
+
"spear",
|
|
425
|
+
"none"
|
|
426
|
+
];
|
|
427
|
+
const FLEUR_DIAMOND_CRESTS = [
|
|
428
|
+
"fleur",
|
|
429
|
+
"diamond",
|
|
430
|
+
"none"
|
|
431
|
+
];
|
|
432
|
+
const isGateFamily = (value) => typeof value === "string" && FAMILY_ORDER.includes(value);
|
|
433
|
+
const compatibleCrestsForFamily = (family) => family === "courtyard" ? SPEAR_DIAMOND_CRESTS : family === "vine" ? FLEUR_DIAMOND_CRESTS : family === "arcade" || family === "fan" ? DIAMOND_SPEAR_CRESTS : FLEUR_SPEAR_CRESTS;
|
|
434
|
+
const isCompatibleCrest = (family, crest) => isGateFamily(family) && compatibleCrestsForFamily(family).includes(crest);
|
|
435
|
+
const FAMILY_OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
436
|
+
"seed",
|
|
437
|
+
"generationVersion",
|
|
438
|
+
"family",
|
|
439
|
+
"density",
|
|
440
|
+
"curvature",
|
|
441
|
+
"symmetry",
|
|
442
|
+
"peakHeight",
|
|
443
|
+
"sideComplexity",
|
|
444
|
+
"crest"
|
|
445
|
+
]);
|
|
446
|
+
const requirePlainObject$1 = (value, version) => {
|
|
447
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`generation-v${version} options must be a plain object`);
|
|
448
|
+
return value;
|
|
449
|
+
};
|
|
450
|
+
const ownValue = (input, key) => Object.hasOwn(input, key) ? input[key] : void 0;
|
|
451
|
+
const normalizeDimensions = (dimensions) => {
|
|
452
|
+
if (dimensions === null || typeof dimensions !== "object") throw new TypeError("Gate dimensions must be a plain object");
|
|
453
|
+
const { width, height } = dimensions;
|
|
454
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) throw new TypeError("Gate dimensions must be finite numbers");
|
|
455
|
+
if (width < 160 || width > 1600 || height < 96 || height > 1200) throw new RangeError("Gate dimensions are outside the supported range");
|
|
456
|
+
const aspectRatio = width / height;
|
|
457
|
+
if (aspectRatio < .5 || aspectRatio > 6) throw new RangeError("Gate dimensions have an unsupported aspect ratio");
|
|
458
|
+
return {
|
|
459
|
+
width: canonicalNumber(width),
|
|
460
|
+
height: canonicalNumber(height)
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
const normalizeSeed = (value) => {
|
|
464
|
+
if (typeof value === "number") {
|
|
465
|
+
if (!Number.isFinite(value)) throw new TypeError("Gate seed numbers must be finite");
|
|
466
|
+
return Object.is(value, -0) ? 0 : value;
|
|
467
|
+
}
|
|
468
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError("Gate seeds must be non-empty strings or finite numbers");
|
|
469
|
+
return value;
|
|
470
|
+
};
|
|
471
|
+
const normalizeControl = (value, fallback, name) => {
|
|
472
|
+
const candidate = value === void 0 ? fallback : value;
|
|
473
|
+
if (typeof candidate !== "number" || !Number.isFinite(candidate)) throw new TypeError(`${name} must be a finite number`);
|
|
474
|
+
if (candidate < 0 || candidate > 1) throw new RangeError(`${name} must be between 0 and 1 inclusive`);
|
|
475
|
+
return canonicalNumber(candidate);
|
|
476
|
+
};
|
|
477
|
+
function normalizeFamilyOptions(value, version) {
|
|
478
|
+
const sampleIndex$2 = version === 2 ? sampleIndex$1 : sampleIndex;
|
|
479
|
+
const input = requirePlainObject$1(value, version);
|
|
480
|
+
const unsupportedKeys = Reflect.ownKeys(input).filter((key) => typeof key !== "string" || !FAMILY_OPTION_KEYS.has(key)).map(String).sort();
|
|
481
|
+
if (unsupportedKeys.length > 0) throw new TypeError(`Unsupported generation-v${version} option keys: ${unsupportedKeys.join(", ")}`);
|
|
482
|
+
if (ownValue(input, "generationVersion") !== version) throw new TypeError(`generation-v${version} options require generationVersion ${version}`);
|
|
483
|
+
const seed = normalizeSeed(ownValue(input, "seed"));
|
|
484
|
+
const familyInput = ownValue(input, "family");
|
|
485
|
+
const requestedFamily = familyInput === void 0 ? "auto" : familyInput;
|
|
486
|
+
if (requestedFamily !== "auto" && !isGateFamily(requestedFamily)) throw new TypeError(`generation-v${version} family is invalid`);
|
|
487
|
+
const family = requestedFamily === "auto" ? FAMILY_ORDER[sampleIndex$2(seed, "family", FAMILY_ORDER.length)] : requestedFamily;
|
|
488
|
+
const density = normalizeControl(ownValue(input, "density"), .55, "density");
|
|
489
|
+
const curvature = normalizeControl(ownValue(input, "curvature"), .65, "curvature");
|
|
490
|
+
const symmetry = normalizeControl(ownValue(input, "symmetry"), 1, "symmetry");
|
|
491
|
+
const peakHeight = normalizeControl(ownValue(input, "peakHeight"), .35, "peakHeight");
|
|
492
|
+
const sideComplexity = normalizeControl(ownValue(input, "sideComplexity"), .5, "sideComplexity");
|
|
493
|
+
const requestedCrest = ownValue(input, "crest");
|
|
494
|
+
const compatibleCrests = compatibleCrestsForFamily(family);
|
|
495
|
+
if (requestedCrest !== void 0 && (typeof requestedCrest !== "string" || !compatibleCrests.includes(requestedCrest))) throw new TypeError(`crest is incompatible with generation-v${version} family ${family}`);
|
|
496
|
+
return {
|
|
497
|
+
seed,
|
|
498
|
+
options: {
|
|
499
|
+
generationVersion: version,
|
|
500
|
+
family,
|
|
501
|
+
density,
|
|
502
|
+
curvature,
|
|
503
|
+
symmetry,
|
|
504
|
+
peakHeight,
|
|
505
|
+
sideComplexity,
|
|
506
|
+
crest: requestedCrest === void 0 ? compatibleCrests[version === 3 ? 0 : sampleIndex$2(seed, `${family}/crest`, compatibleCrests.length)] : requestedCrest,
|
|
507
|
+
inset: INSET,
|
|
508
|
+
strokeWidth: STROKE_WIDTH
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/core/v2/generate.ts
|
|
515
|
+
const COORDINATE_SCALE = 1e4;
|
|
516
|
+
const centerPairForWidth = (width) => {
|
|
517
|
+
const widthLatticeUnits = Math.round(width * COORDINATE_SCALE);
|
|
518
|
+
const leftLatticeUnits = Math.floor(widthLatticeUnits / 2);
|
|
519
|
+
return {
|
|
520
|
+
left: leftLatticeUnits / COORDINATE_SCALE,
|
|
521
|
+
right: (widthLatticeUnits - leftLatticeUnits) / COORDINATE_SCALE
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
const pairedCenteredPaths = (path, width) => {
|
|
525
|
+
const centerPair = centerPairForWidth(width);
|
|
526
|
+
return centerPair.left === centerPair.right ? [path] : [path, mirrorPath(path, width)];
|
|
527
|
+
};
|
|
528
|
+
const displaceInteriorY = (path, displacement) => canonicalizePath(path.map((command, index) => {
|
|
529
|
+
if (command.kind === "C") return {
|
|
530
|
+
...command,
|
|
531
|
+
y1: command.y1 + displacement,
|
|
532
|
+
y2: command.y2 + displacement
|
|
533
|
+
};
|
|
534
|
+
if (command.kind === "Q") return {
|
|
535
|
+
...command,
|
|
536
|
+
y1: command.y1 + displacement
|
|
537
|
+
};
|
|
538
|
+
if (command.kind === "L" && index < path.length - 1) return {
|
|
539
|
+
...command,
|
|
540
|
+
y: command.y + displacement
|
|
541
|
+
};
|
|
542
|
+
return command;
|
|
543
|
+
}));
|
|
544
|
+
const quantizedSymmetryDisplacement = (symmetry, signedSample, maximumSpan) => {
|
|
545
|
+
if (symmetry === 1) return 0;
|
|
546
|
+
const maximumLatticeUnits = Math.max(1, Math.floor(maximumSpan * COORDINATE_SCALE / 2));
|
|
547
|
+
const sampledLatticeUnits = Math.round((1 - symmetry) * signedSample * maximumSpan * COORDINATE_SCALE);
|
|
548
|
+
return (signedSample < 0 ? -1 : 1) * Math.min(maximumLatticeUnits, Math.max(1, Math.abs(sampledLatticeUnits))) / COORDINATE_SCALE;
|
|
549
|
+
};
|
|
550
|
+
const createCrestPath = (crest, centerX, edge, railY, peakHeight) => {
|
|
551
|
+
if (crest === "none") return;
|
|
552
|
+
const upperHeight = railY - edge;
|
|
553
|
+
const tipY = edge;
|
|
554
|
+
const shoulderY = canonicalNumber(edge + Math.min(18, upperHeight * (.28 + peakHeight * .16)));
|
|
555
|
+
const halfWidth = canonicalNumber(Math.min(8, upperHeight * .18));
|
|
556
|
+
if (crest === "spear") return canonicalizePath([
|
|
557
|
+
{
|
|
558
|
+
kind: "M",
|
|
559
|
+
x: centerX,
|
|
560
|
+
y: railY
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
kind: "L",
|
|
564
|
+
x: centerX,
|
|
565
|
+
y: tipY
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
kind: "M",
|
|
569
|
+
x: centerX,
|
|
570
|
+
y: tipY
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
kind: "L",
|
|
574
|
+
x: centerX + halfWidth,
|
|
575
|
+
y: shoulderY
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
kind: "L",
|
|
579
|
+
x: centerX,
|
|
580
|
+
y: shoulderY + halfWidth
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
kind: "L",
|
|
584
|
+
x: centerX - halfWidth,
|
|
585
|
+
y: shoulderY
|
|
586
|
+
},
|
|
587
|
+
{ kind: "Z" }
|
|
588
|
+
]);
|
|
589
|
+
if (crest === "fleur") {
|
|
590
|
+
const crestHeight = canonicalNumber(upperHeight * (.44 + peakHeight * .04));
|
|
591
|
+
const crestTopY = canonicalNumber(railY - crestHeight);
|
|
592
|
+
const sidePetalHalfWidth = canonicalNumber(crestHeight * .39);
|
|
593
|
+
const collarHalfWidth = canonicalNumber(crestHeight * .26);
|
|
594
|
+
const centralPetalHalfWidth = canonicalNumber(crestHeight * .18);
|
|
595
|
+
const lowerSepalHalfWidth = canonicalNumber(crestHeight * .37);
|
|
596
|
+
const sidePetalY = canonicalNumber(crestTopY + crestHeight * .38);
|
|
597
|
+
const collarY = canonicalNumber(crestTopY + crestHeight * .7);
|
|
598
|
+
const centralPetal = canonicalizePath([
|
|
599
|
+
{
|
|
600
|
+
kind: "M",
|
|
601
|
+
x: centerX,
|
|
602
|
+
y: collarY
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
kind: "C",
|
|
606
|
+
x1: centerX - centralPetalHalfWidth,
|
|
607
|
+
y1: crestTopY + crestHeight * .52,
|
|
608
|
+
x2: centerX - centralPetalHalfWidth,
|
|
609
|
+
y2: crestTopY + crestHeight * .16,
|
|
610
|
+
x: centerX,
|
|
611
|
+
y: crestTopY
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
kind: "C",
|
|
615
|
+
x1: centerX + centralPetalHalfWidth,
|
|
616
|
+
y1: crestTopY + crestHeight * .16,
|
|
617
|
+
x2: centerX + centralPetalHalfWidth,
|
|
618
|
+
y2: crestTopY + crestHeight * .52,
|
|
619
|
+
x: centerX,
|
|
620
|
+
y: collarY
|
|
621
|
+
},
|
|
622
|
+
{ kind: "Z" }
|
|
623
|
+
]);
|
|
624
|
+
const leftSidePetal = canonicalizePath([
|
|
625
|
+
{
|
|
626
|
+
kind: "M",
|
|
627
|
+
x: centerX,
|
|
628
|
+
y: collarY
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
kind: "C",
|
|
632
|
+
x1: centerX - collarHalfWidth * .35,
|
|
633
|
+
y1: crestTopY + crestHeight * .56,
|
|
634
|
+
x2: centerX - sidePetalHalfWidth * .72,
|
|
635
|
+
y2: crestTopY + crestHeight * .23,
|
|
636
|
+
x: centerX - sidePetalHalfWidth,
|
|
637
|
+
y: sidePetalY
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
kind: "C",
|
|
641
|
+
x1: centerX - sidePetalHalfWidth * 1.02,
|
|
642
|
+
y1: crestTopY + crestHeight * .52,
|
|
643
|
+
x2: centerX - collarHalfWidth * 1.25,
|
|
644
|
+
y2: crestTopY + crestHeight * .66,
|
|
645
|
+
x: centerX - collarHalfWidth,
|
|
646
|
+
y: collarY
|
|
647
|
+
}
|
|
648
|
+
]);
|
|
649
|
+
const leftSepal = canonicalizePath([{
|
|
650
|
+
kind: "M",
|
|
651
|
+
x: centerX - collarHalfWidth,
|
|
652
|
+
y: collarY
|
|
653
|
+
}, {
|
|
654
|
+
kind: "C",
|
|
655
|
+
x1: centerX - lowerSepalHalfWidth,
|
|
656
|
+
y1: crestTopY + crestHeight * .79,
|
|
657
|
+
x2: centerX - lowerSepalHalfWidth,
|
|
658
|
+
y2: crestTopY + crestHeight * .91,
|
|
659
|
+
x: centerX,
|
|
660
|
+
y: railY
|
|
661
|
+
}]);
|
|
662
|
+
return canonicalizePath([
|
|
663
|
+
{
|
|
664
|
+
kind: "M",
|
|
665
|
+
x: centerX,
|
|
666
|
+
y: railY
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
kind: "L",
|
|
670
|
+
x: centerX,
|
|
671
|
+
y: collarY
|
|
672
|
+
},
|
|
673
|
+
...leftSidePetal,
|
|
674
|
+
...mirrorPath(leftSidePetal, centerX * 2),
|
|
675
|
+
{
|
|
676
|
+
kind: "M",
|
|
677
|
+
x: centerX - collarHalfWidth,
|
|
678
|
+
y: collarY
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
kind: "L",
|
|
682
|
+
x: centerX + collarHalfWidth,
|
|
683
|
+
y: collarY
|
|
684
|
+
},
|
|
685
|
+
...leftSepal,
|
|
686
|
+
...mirrorPath(leftSepal, centerX * 2),
|
|
687
|
+
...centralPetal
|
|
688
|
+
]);
|
|
689
|
+
}
|
|
690
|
+
return canonicalizePath([
|
|
691
|
+
{
|
|
692
|
+
kind: "M",
|
|
693
|
+
x: centerX,
|
|
694
|
+
y: railY
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
kind: "L",
|
|
698
|
+
x: centerX,
|
|
699
|
+
y: tipY
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
kind: "M",
|
|
703
|
+
x: centerX,
|
|
704
|
+
y: tipY
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
kind: "L",
|
|
708
|
+
x: centerX + halfWidth,
|
|
709
|
+
y: shoulderY
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
kind: "L",
|
|
713
|
+
x: centerX,
|
|
714
|
+
y: shoulderY + halfWidth * 2
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
kind: "L",
|
|
718
|
+
x: centerX - halfWidth,
|
|
719
|
+
y: shoulderY
|
|
720
|
+
},
|
|
721
|
+
{ kind: "Z" }
|
|
722
|
+
]);
|
|
723
|
+
};
|
|
724
|
+
const composeCourtyard = (seed, options, layout) => {
|
|
725
|
+
const { width, edge, railY, bottomY, frameBand, safeLeft } = layout;
|
|
726
|
+
const composition = sampleIndex$1(seed, "courtyard/composition", 3);
|
|
727
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "courtyard/motif-count")));
|
|
728
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "courtyard/sides")));
|
|
729
|
+
const scrollPairs = [
|
|
730
|
+
1,
|
|
731
|
+
2,
|
|
732
|
+
3
|
|
733
|
+
][motifLevel];
|
|
734
|
+
const upperHeight = railY - edge;
|
|
735
|
+
const halfSpan = width / 2 - edge;
|
|
736
|
+
const scrollReach = canonicalNumber(halfSpan * (.42 + .26 * sampleScalar$1(seed, "courtyard/anchor/scroll-reach")));
|
|
737
|
+
const lift = canonicalNumber(upperHeight * (.42 + options.curvature * .18 + options.peakHeight * .12));
|
|
738
|
+
const structural = [];
|
|
739
|
+
if (composition === 1) {
|
|
740
|
+
const bayWidth = (width - edge * 2) / 3;
|
|
741
|
+
for (let index = 0; index < 3; index += 1) {
|
|
742
|
+
const left = canonicalNumber(edge + bayWidth * index);
|
|
743
|
+
const right = canonicalNumber(edge + bayWidth * (index + 1));
|
|
744
|
+
const bayLift = canonicalNumber(lift * (index === 1 ? 1 : .72));
|
|
745
|
+
const bayPath = canonicalizePath([{
|
|
746
|
+
kind: "M",
|
|
747
|
+
x: left,
|
|
748
|
+
y: railY
|
|
749
|
+
}, {
|
|
750
|
+
kind: "Q",
|
|
751
|
+
x1: (left + right) / 2,
|
|
752
|
+
y1: railY - bayLift,
|
|
753
|
+
x: right,
|
|
754
|
+
y: railY
|
|
755
|
+
}]);
|
|
756
|
+
structural.push(...index === 1 ? pairedCenteredPaths(bayPath, width) : [bayPath]);
|
|
757
|
+
}
|
|
758
|
+
const centerPair = centerPairForWidth(width);
|
|
759
|
+
if (centerPair.left !== centerPair.right) {
|
|
760
|
+
const leftBay = structural[0];
|
|
761
|
+
structural[structural.length - 1] = mirrorPath(leftBay, width);
|
|
762
|
+
}
|
|
763
|
+
} else if (composition === 2) {
|
|
764
|
+
const shoulder = canonicalNumber(width / 2 - Math.min(scrollReach * .38, halfSpan * .34));
|
|
765
|
+
const crownY = canonicalNumber(edge + upperHeight * (.28 - options.peakHeight * .08));
|
|
766
|
+
const bridgePath = canonicalizePath([
|
|
767
|
+
{
|
|
768
|
+
kind: "M",
|
|
769
|
+
x: edge,
|
|
770
|
+
y: railY
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
kind: "L",
|
|
774
|
+
x: shoulder,
|
|
775
|
+
y: railY - lift * .48
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
kind: "Q",
|
|
779
|
+
x1: width / 2,
|
|
780
|
+
y1: crownY,
|
|
781
|
+
x: width - shoulder,
|
|
782
|
+
y: railY - lift * .48
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
kind: "L",
|
|
786
|
+
x: width - edge,
|
|
787
|
+
y: railY
|
|
788
|
+
}
|
|
789
|
+
]);
|
|
790
|
+
structural.push(...pairedCenteredPaths(bridgePath, width));
|
|
791
|
+
const centerStem = canonicalizePath([{
|
|
792
|
+
kind: "M",
|
|
793
|
+
x: width / 2,
|
|
794
|
+
y: railY
|
|
795
|
+
}, {
|
|
796
|
+
kind: "L",
|
|
797
|
+
x: width / 2,
|
|
798
|
+
y: crownY
|
|
799
|
+
}]);
|
|
800
|
+
structural.push(...pairedCenteredPaths(centerStem, width));
|
|
801
|
+
}
|
|
802
|
+
for (let index = 0; index < scrollPairs; index += 1) {
|
|
803
|
+
const nesting = 1 - index * .17;
|
|
804
|
+
const startX = canonicalNumber(edge + index * Math.min(5, frameBand * .22));
|
|
805
|
+
const endX = canonicalNumber(edge + scrollReach * nesting);
|
|
806
|
+
const scrollLift = canonicalNumber(lift * nesting);
|
|
807
|
+
const leftScroll = canonicalizePath([
|
|
808
|
+
{
|
|
809
|
+
kind: "M",
|
|
810
|
+
x: startX,
|
|
811
|
+
y: railY
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
kind: "C",
|
|
815
|
+
x1: startX + (endX - startX) * .18,
|
|
816
|
+
y1: railY - scrollLift * .52,
|
|
817
|
+
x2: startX + (endX - startX) * .54,
|
|
818
|
+
y2: railY - scrollLift,
|
|
819
|
+
x: endX,
|
|
820
|
+
y: railY - scrollLift * .58
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
kind: "C",
|
|
824
|
+
x1: endX + (endX - startX) * .08,
|
|
825
|
+
y1: railY - scrollLift * .26,
|
|
826
|
+
x2: endX - (endX - startX) * .12,
|
|
827
|
+
y2: railY - scrollLift * .08,
|
|
828
|
+
x: endX - (endX - startX) * .28,
|
|
829
|
+
y: railY
|
|
830
|
+
}
|
|
831
|
+
]);
|
|
832
|
+
const rightDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "courtyard/detail", index) - .5, lift * .08);
|
|
833
|
+
structural.push(leftScroll, displaceInteriorY(mirrorPath(leftScroll, width), rightDisplacement));
|
|
834
|
+
}
|
|
835
|
+
const sideMiddle = canonicalNumber((railY + bottomY) / 2);
|
|
836
|
+
const sideReach = canonicalNumber(safeLeft - edge - 1.0001);
|
|
837
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([
|
|
838
|
+
{
|
|
839
|
+
kind: "M",
|
|
840
|
+
x: edge,
|
|
841
|
+
y: railY
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
kind: "L",
|
|
845
|
+
x: edge + sideReach * .52,
|
|
846
|
+
y: sideMiddle
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
kind: "L",
|
|
850
|
+
x: edge,
|
|
851
|
+
y: bottomY
|
|
852
|
+
}
|
|
853
|
+
]) : canonicalizePath([
|
|
854
|
+
{
|
|
855
|
+
kind: "M",
|
|
856
|
+
x: edge,
|
|
857
|
+
y: railY
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
kind: "C",
|
|
861
|
+
x1: edge + sideReach,
|
|
862
|
+
y1: railY + (sideMiddle - railY) * .42,
|
|
863
|
+
x2: edge + sideReach,
|
|
864
|
+
y2: sideMiddle - frameBand * .35,
|
|
865
|
+
x: edge + sideReach * .5,
|
|
866
|
+
y: sideMiddle
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
kind: "C",
|
|
870
|
+
x1: edge,
|
|
871
|
+
y1: sideMiddle + frameBand * .35,
|
|
872
|
+
x2: edge,
|
|
873
|
+
y2: bottomY - (bottomY - sideMiddle) * .42,
|
|
874
|
+
x: edge,
|
|
875
|
+
y: bottomY
|
|
876
|
+
}
|
|
877
|
+
]);
|
|
878
|
+
return {
|
|
879
|
+
structural,
|
|
880
|
+
decorative: [leftSide, mirrorPath(leftSide, width)]
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
const composeFleuron = (seed, options, layout) => {
|
|
884
|
+
const { width, edge, railY, bottomY, frameBand, safeLeft } = layout;
|
|
885
|
+
const composition = sampleIndex$1(seed, "fleuron/composition", 3);
|
|
886
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "fleuron/motif-count")));
|
|
887
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "fleuron/sides")));
|
|
888
|
+
const clusterCount = [
|
|
889
|
+
1,
|
|
890
|
+
2,
|
|
891
|
+
3
|
|
892
|
+
][motifLevel];
|
|
893
|
+
const upperHeight = railY - edge;
|
|
894
|
+
const upperRight = canonicalNumber(width - edge);
|
|
895
|
+
const upperWidth = canonicalNumber(upperRight - edge);
|
|
896
|
+
const petalSpan = canonicalNumber(upperWidth * (.12 + .1 * sampleScalar$1(seed, "fleuron/proportion/petal-span")));
|
|
897
|
+
const lift = canonicalNumber(upperHeight * (.5 + options.curvature * .16 + options.peakHeight * .16));
|
|
898
|
+
const centerX = width / 2;
|
|
899
|
+
const structural = [];
|
|
900
|
+
const clusterTier = (index) => Math.min(index, clusterCount - 1 - index) % 2;
|
|
901
|
+
if (composition === 0) {
|
|
902
|
+
const leftBrace = canonicalizePath([
|
|
903
|
+
{
|
|
904
|
+
kind: "M",
|
|
905
|
+
x: edge,
|
|
906
|
+
y: railY
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
kind: "C",
|
|
910
|
+
x1: edge + petalSpan * .3,
|
|
911
|
+
y1: railY - lift * .5,
|
|
912
|
+
x2: centerX - petalSpan,
|
|
913
|
+
y2: railY - lift,
|
|
914
|
+
x: centerX,
|
|
915
|
+
y: railY - lift * .58
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
kind: "C",
|
|
919
|
+
x1: centerX - petalSpan * .3,
|
|
920
|
+
y1: railY - lift * .34,
|
|
921
|
+
x2: centerX - petalSpan * .18,
|
|
922
|
+
y2: railY - lift * .12,
|
|
923
|
+
x: centerX,
|
|
924
|
+
y: railY
|
|
925
|
+
}
|
|
926
|
+
]);
|
|
927
|
+
structural.push(leftBrace, mirrorPath(leftBrace, width));
|
|
928
|
+
const centerStem = canonicalizePath([{
|
|
929
|
+
kind: "M",
|
|
930
|
+
x: centerX,
|
|
931
|
+
y: railY
|
|
932
|
+
}, {
|
|
933
|
+
kind: "L",
|
|
934
|
+
x: centerX,
|
|
935
|
+
y: edge + upperHeight * .2
|
|
936
|
+
}]);
|
|
937
|
+
structural.push(...pairedCenteredPaths(centerStem, width));
|
|
938
|
+
} else if (composition === 1) {
|
|
939
|
+
const heartPath = canonicalizePath([
|
|
940
|
+
{
|
|
941
|
+
kind: "M",
|
|
942
|
+
x: edge,
|
|
943
|
+
y: railY
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
kind: "C",
|
|
947
|
+
x1: edge + petalSpan * .55,
|
|
948
|
+
y1: railY - lift * .58,
|
|
949
|
+
x2: centerX - petalSpan * .72,
|
|
950
|
+
y2: railY - lift,
|
|
951
|
+
x: centerX,
|
|
952
|
+
y: railY - lift * .42
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
kind: "C",
|
|
956
|
+
x1: centerX + petalSpan * .72,
|
|
957
|
+
y1: railY - lift,
|
|
958
|
+
x2: width - edge - petalSpan * .55,
|
|
959
|
+
y2: railY - lift * .58,
|
|
960
|
+
x: width - edge,
|
|
961
|
+
y: railY
|
|
962
|
+
}
|
|
963
|
+
]);
|
|
964
|
+
structural.push(...pairedCenteredPaths(heartPath, width));
|
|
965
|
+
}
|
|
966
|
+
const decorative = [];
|
|
967
|
+
const stemPairSources = /* @__PURE__ */ new Map();
|
|
968
|
+
const petalPairSources = /* @__PURE__ */ new Map();
|
|
969
|
+
for (let index = 0; index < clusterCount; index += 1) {
|
|
970
|
+
const mirrorIndex = clusterCount - 1 - index;
|
|
971
|
+
if (index > mirrorIndex) {
|
|
972
|
+
const stemSource = stemPairSources.get(mirrorIndex);
|
|
973
|
+
const source = petalPairSources.get(mirrorIndex);
|
|
974
|
+
structural.push(mirrorPath(stemSource, width));
|
|
975
|
+
decorative.push(mirrorPath(source, width));
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
const offset = (index - (clusterCount - 1) / 2) * petalSpan * .72;
|
|
979
|
+
const rootX = canonicalNumber(centerX + offset * 1.45);
|
|
980
|
+
const flowerX = canonicalNumber(centerX + offset);
|
|
981
|
+
const flowerY = canonicalNumber(railY - lift * (.58 + (clusterTier(index) === 0 ? .16 : 0)));
|
|
982
|
+
const halfPetal = canonicalNumber(Math.min(petalSpan * .22, upperHeight * .2));
|
|
983
|
+
const stemPath = canonicalizePath([{
|
|
984
|
+
kind: "M",
|
|
985
|
+
x: rootX,
|
|
986
|
+
y: railY
|
|
987
|
+
}, {
|
|
988
|
+
kind: "Q",
|
|
989
|
+
x1: rootX + (flowerX - rootX) * .35,
|
|
990
|
+
y1: railY - lift * .36,
|
|
991
|
+
x: flowerX,
|
|
992
|
+
y: flowerY + halfPetal
|
|
993
|
+
}]);
|
|
994
|
+
const petalPath = canonicalizePath([
|
|
995
|
+
{
|
|
996
|
+
kind: "M",
|
|
997
|
+
x: flowerX,
|
|
998
|
+
y: flowerY + halfPetal
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
kind: "Q",
|
|
1002
|
+
x1: flowerX - halfPetal,
|
|
1003
|
+
y1: flowerY,
|
|
1004
|
+
x: flowerX,
|
|
1005
|
+
y: flowerY - halfPetal
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
kind: "Q",
|
|
1009
|
+
x1: flowerX + halfPetal,
|
|
1010
|
+
y1: flowerY,
|
|
1011
|
+
x: flowerX,
|
|
1012
|
+
y: flowerY + halfPetal
|
|
1013
|
+
},
|
|
1014
|
+
{ kind: "Z" }
|
|
1015
|
+
]);
|
|
1016
|
+
if (index === mirrorIndex) {
|
|
1017
|
+
structural.push(...pairedCenteredPaths(stemPath, width));
|
|
1018
|
+
decorative.push(...pairedCenteredPaths(petalPath, width));
|
|
1019
|
+
} else {
|
|
1020
|
+
stemPairSources.set(index, stemPath);
|
|
1021
|
+
petalPairSources.set(index, petalPath);
|
|
1022
|
+
structural.push(stemPath);
|
|
1023
|
+
decorative.push(petalPath);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const sideMiddle = canonicalNumber((railY + bottomY) / 2);
|
|
1027
|
+
const sideReach = canonicalNumber(safeLeft - edge - 1.0001);
|
|
1028
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([
|
|
1029
|
+
{
|
|
1030
|
+
kind: "M",
|
|
1031
|
+
x: edge,
|
|
1032
|
+
y: bottomY
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
kind: "L",
|
|
1036
|
+
x: edge + sideReach * .72,
|
|
1037
|
+
y: sideMiddle
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
kind: "L",
|
|
1041
|
+
x: edge,
|
|
1042
|
+
y: railY
|
|
1043
|
+
}
|
|
1044
|
+
]) : sideLevel === 1 ? canonicalizePath([
|
|
1045
|
+
{
|
|
1046
|
+
kind: "M",
|
|
1047
|
+
x: edge,
|
|
1048
|
+
y: sideMiddle + frameBand * .42
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
kind: "Q",
|
|
1052
|
+
x1: edge + sideReach,
|
|
1053
|
+
y1: sideMiddle,
|
|
1054
|
+
x: edge,
|
|
1055
|
+
y: sideMiddle - frameBand * .42
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
kind: "Q",
|
|
1059
|
+
x1: edge + sideReach * .45,
|
|
1060
|
+
y1: sideMiddle,
|
|
1061
|
+
x: edge,
|
|
1062
|
+
y: sideMiddle + frameBand * .42
|
|
1063
|
+
},
|
|
1064
|
+
{ kind: "Z" }
|
|
1065
|
+
]) : canonicalizePath([
|
|
1066
|
+
{
|
|
1067
|
+
kind: "M",
|
|
1068
|
+
x: edge,
|
|
1069
|
+
y: railY
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
kind: "C",
|
|
1073
|
+
x1: edge + sideReach,
|
|
1074
|
+
y1: railY + (sideMiddle - railY) * .34,
|
|
1075
|
+
x2: edge + sideReach,
|
|
1076
|
+
y2: sideMiddle - frameBand * .28,
|
|
1077
|
+
x: edge + sideReach * .35,
|
|
1078
|
+
y: sideMiddle
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
kind: "C",
|
|
1082
|
+
x1: edge + sideReach,
|
|
1083
|
+
y1: sideMiddle + frameBand * .28,
|
|
1084
|
+
x2: edge + sideReach,
|
|
1085
|
+
y2: bottomY - (bottomY - sideMiddle) * .34,
|
|
1086
|
+
x: edge,
|
|
1087
|
+
y: bottomY
|
|
1088
|
+
}
|
|
1089
|
+
]);
|
|
1090
|
+
const rightSideDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "fleuron/detail", clusterCount) - .5, frameBand * .12);
|
|
1091
|
+
decorative.push(leftSide, displaceInteriorY(mirrorPath(leftSide, width), rightSideDisplacement));
|
|
1092
|
+
return {
|
|
1093
|
+
structural,
|
|
1094
|
+
decorative
|
|
1095
|
+
};
|
|
1096
|
+
};
|
|
1097
|
+
const composeVine = (seed, options, layout) => {
|
|
1098
|
+
const { width, edge, railY, bottomY, frameBand, safeLeft } = layout;
|
|
1099
|
+
const composition = sampleIndex$1(seed, "vine/composition", 3);
|
|
1100
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "vine/motif-count")));
|
|
1101
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "vine/sides")));
|
|
1102
|
+
const leafPairs = [
|
|
1103
|
+
2,
|
|
1104
|
+
4,
|
|
1105
|
+
6
|
|
1106
|
+
][motifLevel];
|
|
1107
|
+
const curlPairCount = [
|
|
1108
|
+
1,
|
|
1109
|
+
1,
|
|
1110
|
+
2
|
|
1111
|
+
][motifLevel];
|
|
1112
|
+
const upperHeight = railY - edge;
|
|
1113
|
+
const halfSpan = width / 2 - edge;
|
|
1114
|
+
const leafNode = .22 + .56 * sampleScalar$1(seed, "vine/anchor/leaf-node");
|
|
1115
|
+
const lift = canonicalNumber(upperHeight * (.52 + options.curvature * .16 + options.peakHeight * .14));
|
|
1116
|
+
const structural = [];
|
|
1117
|
+
if (composition === 0) {
|
|
1118
|
+
const leftStem = canonicalizePath([
|
|
1119
|
+
{
|
|
1120
|
+
kind: "M",
|
|
1121
|
+
x: edge,
|
|
1122
|
+
y: railY
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
kind: "C",
|
|
1126
|
+
x1: edge + halfSpan * .12,
|
|
1127
|
+
y1: railY - lift * .28,
|
|
1128
|
+
x2: edge + halfSpan * .28,
|
|
1129
|
+
y2: railY - lift * .78,
|
|
1130
|
+
x: edge + halfSpan * .62,
|
|
1131
|
+
y: railY - lift
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
kind: "C",
|
|
1135
|
+
x1: edge + halfSpan * .78,
|
|
1136
|
+
y1: railY - lift * .96,
|
|
1137
|
+
x2: width / 2 - halfSpan * .08,
|
|
1138
|
+
y2: railY - lift * .5,
|
|
1139
|
+
x: width / 2,
|
|
1140
|
+
y: railY
|
|
1141
|
+
}
|
|
1142
|
+
]);
|
|
1143
|
+
structural.push(leftStem, mirrorPath(leftStem, width));
|
|
1144
|
+
} else if (composition === 1) {
|
|
1145
|
+
const leftCanopy = canonicalizePath([
|
|
1146
|
+
{
|
|
1147
|
+
kind: "M",
|
|
1148
|
+
x: edge,
|
|
1149
|
+
y: railY
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
kind: "C",
|
|
1153
|
+
x1: edge + halfSpan * .18,
|
|
1154
|
+
y1: railY - lift * .72,
|
|
1155
|
+
x2: edge + halfSpan * .52,
|
|
1156
|
+
y2: railY - lift,
|
|
1157
|
+
x: width / 2,
|
|
1158
|
+
y: railY - lift * .62
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
kind: "C",
|
|
1162
|
+
x1: width / 2 + halfSpan * .18,
|
|
1163
|
+
y1: railY - lift * .38,
|
|
1164
|
+
x2: width / 2 + halfSpan * .3,
|
|
1165
|
+
y2: railY - lift * .82,
|
|
1166
|
+
x: width / 2 + halfSpan * .48,
|
|
1167
|
+
y: railY - lift * .54
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
kind: "C",
|
|
1171
|
+
x1: width / 2 + halfSpan * .62,
|
|
1172
|
+
y1: railY - lift * .3,
|
|
1173
|
+
x2: width - edge - halfSpan * .08,
|
|
1174
|
+
y2: railY - lift * .16,
|
|
1175
|
+
x: width - edge,
|
|
1176
|
+
y: railY
|
|
1177
|
+
}
|
|
1178
|
+
]);
|
|
1179
|
+
structural.push(leftCanopy, mirrorPath(leftCanopy, width));
|
|
1180
|
+
} else {
|
|
1181
|
+
const leftBranch = canonicalizePath([
|
|
1182
|
+
{
|
|
1183
|
+
kind: "M",
|
|
1184
|
+
x: edge,
|
|
1185
|
+
y: railY
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
kind: "C",
|
|
1189
|
+
x1: edge + halfSpan * .08,
|
|
1190
|
+
y1: railY - lift * .22,
|
|
1191
|
+
x2: edge + halfSpan * .2,
|
|
1192
|
+
y2: railY - lift * .86,
|
|
1193
|
+
x: edge + halfSpan * .48,
|
|
1194
|
+
y: railY - lift * .82
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
kind: "C",
|
|
1198
|
+
x1: edge + halfSpan * .7,
|
|
1199
|
+
y1: railY - lift * .78,
|
|
1200
|
+
x2: width / 2 - halfSpan * .12,
|
|
1201
|
+
y2: railY - lift * .34,
|
|
1202
|
+
x: width / 2,
|
|
1203
|
+
y: railY - lift * .56
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
kind: "C",
|
|
1207
|
+
x1: width / 2 - halfSpan * .04,
|
|
1208
|
+
y1: railY - lift * .28,
|
|
1209
|
+
x2: width / 2 - halfSpan * .02,
|
|
1210
|
+
y2: railY - lift * .12,
|
|
1211
|
+
x: width / 2,
|
|
1212
|
+
y: railY
|
|
1213
|
+
}
|
|
1214
|
+
]);
|
|
1215
|
+
structural.push(leftBranch, mirrorPath(leftBranch, width));
|
|
1216
|
+
const centerStem = canonicalizePath([{
|
|
1217
|
+
kind: "M",
|
|
1218
|
+
x: width / 2,
|
|
1219
|
+
y: railY
|
|
1220
|
+
}, {
|
|
1221
|
+
kind: "L",
|
|
1222
|
+
x: width / 2,
|
|
1223
|
+
y: railY - lift * .56
|
|
1224
|
+
}]);
|
|
1225
|
+
structural.push(...pairedCenteredPaths(centerStem, width));
|
|
1226
|
+
}
|
|
1227
|
+
for (let index = 0; index < curlPairCount; index += 1) {
|
|
1228
|
+
const rootX = canonicalNumber(edge + halfSpan * (.16 + index * .11));
|
|
1229
|
+
const reach = canonicalNumber(halfSpan * (.28 - index * .06));
|
|
1230
|
+
const curlLift = canonicalNumber(lift * (.68 - index * .12));
|
|
1231
|
+
const leftCurl = canonicalizePath([
|
|
1232
|
+
{
|
|
1233
|
+
kind: "M",
|
|
1234
|
+
x: rootX,
|
|
1235
|
+
y: railY
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
kind: "C",
|
|
1239
|
+
x1: rootX + reach * .12,
|
|
1240
|
+
y1: railY - curlLift * .48,
|
|
1241
|
+
x2: rootX + reach * .54,
|
|
1242
|
+
y2: railY - curlLift,
|
|
1243
|
+
x: rootX + reach * .82,
|
|
1244
|
+
y: railY - curlLift * .72
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
kind: "C",
|
|
1248
|
+
x1: rootX + reach * 1.08,
|
|
1249
|
+
y1: railY - curlLift * .52,
|
|
1250
|
+
x2: rootX + reach,
|
|
1251
|
+
y2: railY - curlLift * .18,
|
|
1252
|
+
x: rootX + reach * .76,
|
|
1253
|
+
y: railY - curlLift * .28
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
kind: "C",
|
|
1257
|
+
x1: rootX + reach * .56,
|
|
1258
|
+
y1: railY - curlLift * .38,
|
|
1259
|
+
x2: rootX + reach * .6,
|
|
1260
|
+
y2: railY - curlLift * .58,
|
|
1261
|
+
x: rootX + reach * .73,
|
|
1262
|
+
y: railY - curlLift * .5
|
|
1263
|
+
}
|
|
1264
|
+
]);
|
|
1265
|
+
const rightDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "vine/detail", leafPairs * 2 + index) - .5, curlLift * .06);
|
|
1266
|
+
structural.push(leftCurl, displaceInteriorY(mirrorPath(leftCurl, width), rightDisplacement));
|
|
1267
|
+
}
|
|
1268
|
+
const decorative = [];
|
|
1269
|
+
const leafHalfWidth = canonicalNumber(Math.min(halfSpan * .035, upperHeight * .12));
|
|
1270
|
+
const leafHalfHeight = canonicalNumber(Math.min(upperHeight * .075, 7));
|
|
1271
|
+
for (let index = 0; index < leafPairs; index += 1) {
|
|
1272
|
+
const detail = sampleScalar$1(seed, "vine/detail", index);
|
|
1273
|
+
const along = .18 + .64 * (index + leafNode) / (leafPairs + 1);
|
|
1274
|
+
const leafX = canonicalNumber(edge + halfSpan * along);
|
|
1275
|
+
const leftLeafDisplacement = quantizedSymmetryDisplacement(options.symmetry, .5 - detail, lift * .08);
|
|
1276
|
+
const leafY = canonicalNumber(railY - lift * (.28 + along * .56) + leftLeafDisplacement);
|
|
1277
|
+
const leftLeaf = canonicalizePath([
|
|
1278
|
+
{
|
|
1279
|
+
kind: "M",
|
|
1280
|
+
x: leafX,
|
|
1281
|
+
y: leafY + leafHalfHeight
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
kind: "Q",
|
|
1285
|
+
x1: leafX - leafHalfWidth,
|
|
1286
|
+
y1: leafY,
|
|
1287
|
+
x: leafX,
|
|
1288
|
+
y: leafY - leafHalfHeight
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
kind: "Q",
|
|
1292
|
+
x1: leafX + leafHalfWidth,
|
|
1293
|
+
y1: leafY,
|
|
1294
|
+
x: leafX,
|
|
1295
|
+
y: leafY + leafHalfHeight
|
|
1296
|
+
},
|
|
1297
|
+
{ kind: "Z" }
|
|
1298
|
+
]);
|
|
1299
|
+
const leftTwig = canonicalizePath([{
|
|
1300
|
+
kind: "M",
|
|
1301
|
+
x: leafX,
|
|
1302
|
+
y: railY
|
|
1303
|
+
}, {
|
|
1304
|
+
kind: "Q",
|
|
1305
|
+
x1: leafX + (index % 2 === 0 ? -leafHalfWidth : leafHalfWidth),
|
|
1306
|
+
y1: (railY + leafY) / 2,
|
|
1307
|
+
x: leafX,
|
|
1308
|
+
y: leafY + leafHalfHeight
|
|
1309
|
+
}]);
|
|
1310
|
+
const rightLeafDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "vine/detail", index + leafPairs) - .5, leafHalfHeight * .24);
|
|
1311
|
+
structural.push(leftTwig, mirrorPath(leftTwig, width));
|
|
1312
|
+
decorative.push(leftLeaf, displaceInteriorY(mirrorPath(leftLeaf, width), rightLeafDisplacement));
|
|
1313
|
+
}
|
|
1314
|
+
const sideMiddle = canonicalNumber((railY + bottomY) / 2);
|
|
1315
|
+
const sideReach = canonicalNumber(safeLeft - edge - 1.0001);
|
|
1316
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([{
|
|
1317
|
+
kind: "M",
|
|
1318
|
+
x: edge,
|
|
1319
|
+
y: bottomY
|
|
1320
|
+
}, {
|
|
1321
|
+
kind: "L",
|
|
1322
|
+
x: edge + sideReach * .62,
|
|
1323
|
+
y: railY
|
|
1324
|
+
}]) : sideLevel === 1 ? canonicalizePath([
|
|
1325
|
+
{
|
|
1326
|
+
kind: "M",
|
|
1327
|
+
x: edge,
|
|
1328
|
+
y: bottomY
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
kind: "Q",
|
|
1332
|
+
x1: edge + sideReach,
|
|
1333
|
+
y1: sideMiddle,
|
|
1334
|
+
x: edge,
|
|
1335
|
+
y: railY
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
kind: "L",
|
|
1339
|
+
x: edge + sideReach * .72,
|
|
1340
|
+
y: sideMiddle
|
|
1341
|
+
}
|
|
1342
|
+
]) : canonicalizePath([
|
|
1343
|
+
{
|
|
1344
|
+
kind: "M",
|
|
1345
|
+
x: edge,
|
|
1346
|
+
y: bottomY
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
kind: "C",
|
|
1350
|
+
x1: edge + sideReach,
|
|
1351
|
+
y1: bottomY - (bottomY - sideMiddle) * .46,
|
|
1352
|
+
x2: edge,
|
|
1353
|
+
y2: sideMiddle + frameBand * .3,
|
|
1354
|
+
x: edge + sideReach * .58,
|
|
1355
|
+
y: sideMiddle
|
|
1356
|
+
},
|
|
1357
|
+
{
|
|
1358
|
+
kind: "C",
|
|
1359
|
+
x1: edge + sideReach,
|
|
1360
|
+
y1: sideMiddle - frameBand * .3,
|
|
1361
|
+
x2: edge,
|
|
1362
|
+
y2: railY + (sideMiddle - railY) * .46,
|
|
1363
|
+
x: edge,
|
|
1364
|
+
y: railY
|
|
1365
|
+
}
|
|
1366
|
+
]);
|
|
1367
|
+
decorative.push(leftSide, mirrorPath(leftSide, width));
|
|
1368
|
+
return {
|
|
1369
|
+
structural,
|
|
1370
|
+
decorative
|
|
1371
|
+
};
|
|
1372
|
+
};
|
|
1373
|
+
const composeFan = (seed, options, layout) => {
|
|
1374
|
+
const { width, edge, railY, bottomY, frameBand, safeLeft } = layout;
|
|
1375
|
+
const composition = sampleIndex$1(seed, "fan/composition", 3);
|
|
1376
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "fan/motif-count")));
|
|
1377
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "fan/sides")));
|
|
1378
|
+
const raysPerHalf = [
|
|
1379
|
+
3,
|
|
1380
|
+
5,
|
|
1381
|
+
7
|
|
1382
|
+
][motifLevel];
|
|
1383
|
+
const upperHeight = railY - edge;
|
|
1384
|
+
const halfSpan = width / 2 - edge;
|
|
1385
|
+
const originRatio = .58 + .24 * sampleScalar$1(seed, "fan/anchor/origin-height");
|
|
1386
|
+
const originY = canonicalNumber(railY + (bottomY - railY) * originRatio);
|
|
1387
|
+
const lift = canonicalNumber(upperHeight * (.48 + options.peakHeight * .22 + options.curvature * .1));
|
|
1388
|
+
const structural = [];
|
|
1389
|
+
for (let index = 0; index < raysPerHalf; index += 1) {
|
|
1390
|
+
const fraction = (index + 1) / (raysPerHalf + 1);
|
|
1391
|
+
const detail = sampleScalar$1(seed, "fan/detail", index);
|
|
1392
|
+
const targetX = canonicalNumber(edge + halfSpan * fraction);
|
|
1393
|
+
const leftTargetDisplacement = quantizedSymmetryDisplacement(options.symmetry, .5 - detail, lift * .06);
|
|
1394
|
+
const targetY = canonicalNumber(railY - lift * (.34 + fraction * .62) + leftTargetDisplacement);
|
|
1395
|
+
const leftRay = composition === 0 ? canonicalizePath([
|
|
1396
|
+
{
|
|
1397
|
+
kind: "M",
|
|
1398
|
+
x: edge,
|
|
1399
|
+
y: originY
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
kind: "L",
|
|
1403
|
+
x: edge,
|
|
1404
|
+
y: railY
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
kind: "L",
|
|
1408
|
+
x: targetX,
|
|
1409
|
+
y: targetY
|
|
1410
|
+
}
|
|
1411
|
+
]) : composition === 1 ? canonicalizePath([
|
|
1412
|
+
{
|
|
1413
|
+
kind: "M",
|
|
1414
|
+
x: edge,
|
|
1415
|
+
y: originY
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
kind: "L",
|
|
1419
|
+
x: edge,
|
|
1420
|
+
y: railY
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
kind: "L",
|
|
1424
|
+
x: edge + halfSpan * fraction * .24,
|
|
1425
|
+
y: railY + frameBand * (.72 - fraction * .24)
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
kind: "L",
|
|
1429
|
+
x: edge + halfSpan * fraction * .58,
|
|
1430
|
+
y: railY - lift * fraction * .34
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
kind: "L",
|
|
1434
|
+
x: targetX,
|
|
1435
|
+
y: targetY
|
|
1436
|
+
}
|
|
1437
|
+
]) : canonicalizePath([
|
|
1438
|
+
{
|
|
1439
|
+
kind: "M",
|
|
1440
|
+
x: edge,
|
|
1441
|
+
y: originY
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
kind: "L",
|
|
1445
|
+
x: edge,
|
|
1446
|
+
y: railY
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
kind: "Q",
|
|
1450
|
+
x1: edge + halfSpan * fraction * .38,
|
|
1451
|
+
y1: railY - lift * fraction * .18,
|
|
1452
|
+
x: targetX,
|
|
1453
|
+
y: targetY
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
kind: "L",
|
|
1457
|
+
x: width / 2,
|
|
1458
|
+
y: railY
|
|
1459
|
+
}
|
|
1460
|
+
]);
|
|
1461
|
+
const mirroredRightRay = mirrorPath(leftRay, width);
|
|
1462
|
+
const rightTargetIndex = composition === 2 ? mirroredRightRay.length - 2 : mirroredRightRay.length - 1;
|
|
1463
|
+
const rightTargetDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "fan/detail", index + raysPerHalf) - .5, lift * .05);
|
|
1464
|
+
const rightRay = canonicalizePath(mirroredRightRay.map((command, commandIndex) => commandIndex === rightTargetIndex && (command.kind === "L" || command.kind === "Q") ? {
|
|
1465
|
+
...command,
|
|
1466
|
+
y: command.y + rightTargetDisplacement
|
|
1467
|
+
} : command));
|
|
1468
|
+
structural.push(leftRay, rightRay);
|
|
1469
|
+
}
|
|
1470
|
+
const decorative = [];
|
|
1471
|
+
const sideMiddle = canonicalNumber((railY + bottomY) / 2);
|
|
1472
|
+
const sideReach = canonicalNumber(safeLeft - edge - 1.0001);
|
|
1473
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([{
|
|
1474
|
+
kind: "M",
|
|
1475
|
+
x: edge + sideReach * .32,
|
|
1476
|
+
y: railY
|
|
1477
|
+
}, {
|
|
1478
|
+
kind: "L",
|
|
1479
|
+
x: edge + sideReach * .32,
|
|
1480
|
+
y: bottomY
|
|
1481
|
+
}]) : sideLevel === 1 ? canonicalizePath([
|
|
1482
|
+
{
|
|
1483
|
+
kind: "M",
|
|
1484
|
+
x: edge,
|
|
1485
|
+
y: railY
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
kind: "L",
|
|
1489
|
+
x: edge + sideReach * .42,
|
|
1490
|
+
y: sideMiddle - frameBand * .48
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
kind: "L",
|
|
1494
|
+
x: edge + sideReach,
|
|
1495
|
+
y: sideMiddle
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
kind: "L",
|
|
1499
|
+
x: edge,
|
|
1500
|
+
y: bottomY
|
|
1501
|
+
}
|
|
1502
|
+
]) : canonicalizePath([
|
|
1503
|
+
{
|
|
1504
|
+
kind: "M",
|
|
1505
|
+
x: edge,
|
|
1506
|
+
y: railY
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
kind: "L",
|
|
1510
|
+
x: edge + sideReach * .72,
|
|
1511
|
+
y: sideMiddle
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
kind: "L",
|
|
1515
|
+
x: edge,
|
|
1516
|
+
y: bottomY
|
|
1517
|
+
}
|
|
1518
|
+
]);
|
|
1519
|
+
decorative.push(leftSide, mirrorPath(leftSide, width));
|
|
1520
|
+
return {
|
|
1521
|
+
structural,
|
|
1522
|
+
decorative
|
|
1523
|
+
};
|
|
1524
|
+
};
|
|
1525
|
+
const composeVolute = (seed, options, layout) => {
|
|
1526
|
+
const { width, edge, railY, bottomY, frameBand, safeLeft } = layout;
|
|
1527
|
+
const composition = sampleIndex$1(seed, "volute/composition", 3);
|
|
1528
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "volute/motif-count")));
|
|
1529
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "volute/sides")));
|
|
1530
|
+
const pairCount = [
|
|
1531
|
+
1,
|
|
1532
|
+
2,
|
|
1533
|
+
3
|
|
1534
|
+
][motifLevel];
|
|
1535
|
+
const upperHeight = railY - edge;
|
|
1536
|
+
const halfSpan = width / 2 - edge;
|
|
1537
|
+
const upperRight = canonicalNumber(width - edge);
|
|
1538
|
+
const upperWidth = canonicalNumber(upperRight - edge);
|
|
1539
|
+
const coilRadius = canonicalNumber(upperWidth * (.1 + .08 * sampleScalar$1(seed, "volute/proportion/coil-radius")));
|
|
1540
|
+
const lift = canonicalNumber(upperHeight * (.5 + options.curvature * .2 + options.peakHeight * .14));
|
|
1541
|
+
const structural = [];
|
|
1542
|
+
if (composition === 0) for (let index = 0; index < pairCount; index += 1) {
|
|
1543
|
+
const nesting = 1 - index * .18;
|
|
1544
|
+
const startX = canonicalNumber(edge + index * Math.min(5, frameBand * .2));
|
|
1545
|
+
const centerX = canonicalNumber(edge + Math.min(halfSpan * .78, coilRadius * (1.3 + index * .2)));
|
|
1546
|
+
const coilLift = canonicalNumber(lift * nesting);
|
|
1547
|
+
const leftVolute = canonicalizePath([
|
|
1548
|
+
{
|
|
1549
|
+
kind: "M",
|
|
1550
|
+
x: startX,
|
|
1551
|
+
y: railY
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
kind: "C",
|
|
1555
|
+
x1: startX + coilRadius * .18,
|
|
1556
|
+
y1: railY - coilLift * .54,
|
|
1557
|
+
x2: centerX - coilRadius * .5,
|
|
1558
|
+
y2: railY - coilLift,
|
|
1559
|
+
x: centerX,
|
|
1560
|
+
y: railY - coilLift * .66
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
kind: "C",
|
|
1564
|
+
x1: centerX + coilRadius * .32,
|
|
1565
|
+
y1: railY - coilLift * .4,
|
|
1566
|
+
x2: centerX + coilRadius * .16,
|
|
1567
|
+
y2: railY - coilLift * .18,
|
|
1568
|
+
x: centerX - coilRadius * .08,
|
|
1569
|
+
y: railY - coilLift * .28
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
kind: "C",
|
|
1573
|
+
x1: centerX - coilRadius * .2,
|
|
1574
|
+
y1: railY - coilLift * .36,
|
|
1575
|
+
x2: centerX - coilRadius * .24,
|
|
1576
|
+
y2: railY - coilLift * .1,
|
|
1577
|
+
x: centerX - coilRadius * .42,
|
|
1578
|
+
y: railY
|
|
1579
|
+
}
|
|
1580
|
+
]);
|
|
1581
|
+
structural.push(leftVolute, mirrorPath(leftVolute, width));
|
|
1582
|
+
}
|
|
1583
|
+
else if (composition === 1) {
|
|
1584
|
+
const crownPath = canonicalizePath([
|
|
1585
|
+
{
|
|
1586
|
+
kind: "M",
|
|
1587
|
+
x: edge,
|
|
1588
|
+
y: railY
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
kind: "C",
|
|
1592
|
+
x1: edge + coilRadius * .4,
|
|
1593
|
+
y1: railY - lift * .66,
|
|
1594
|
+
x2: width / 2 - coilRadius * 1.15,
|
|
1595
|
+
y2: railY - lift,
|
|
1596
|
+
x: width / 2 - coilRadius * .42,
|
|
1597
|
+
y: railY - lift * .6
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
kind: "C",
|
|
1601
|
+
x1: width / 2 - coilRadius * .08,
|
|
1602
|
+
y1: railY - lift * .36,
|
|
1603
|
+
x2: width / 2 - coilRadius * .28,
|
|
1604
|
+
y2: railY - lift * .14,
|
|
1605
|
+
x: width / 2,
|
|
1606
|
+
y: railY - lift * .72
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
kind: "C",
|
|
1610
|
+
x1: width / 2 + coilRadius * .28,
|
|
1611
|
+
y1: railY - lift * .14,
|
|
1612
|
+
x2: width / 2 + coilRadius * .08,
|
|
1613
|
+
y2: railY - lift * .36,
|
|
1614
|
+
x: width / 2 + coilRadius * .42,
|
|
1615
|
+
y: railY - lift * .6
|
|
1616
|
+
},
|
|
1617
|
+
{
|
|
1618
|
+
kind: "C",
|
|
1619
|
+
x1: width / 2 + coilRadius * 1.15,
|
|
1620
|
+
y1: railY - lift,
|
|
1621
|
+
x2: width - edge - coilRadius * .4,
|
|
1622
|
+
y2: railY - lift * .66,
|
|
1623
|
+
x: width - edge,
|
|
1624
|
+
y: railY
|
|
1625
|
+
}
|
|
1626
|
+
]);
|
|
1627
|
+
structural.push(...pairedCenteredPaths(crownPath, width));
|
|
1628
|
+
for (let index = 0; index < pairCount; index += 1) {
|
|
1629
|
+
const spread = canonicalNumber(coilRadius * (.45 + index * .32));
|
|
1630
|
+
const bracePath = canonicalizePath([{
|
|
1631
|
+
kind: "M",
|
|
1632
|
+
x: width / 2 - spread,
|
|
1633
|
+
y: railY
|
|
1634
|
+
}, {
|
|
1635
|
+
kind: "Q",
|
|
1636
|
+
x1: width / 2,
|
|
1637
|
+
y1: railY - lift * (.28 + index * .08),
|
|
1638
|
+
x: width / 2 + spread,
|
|
1639
|
+
y: railY
|
|
1640
|
+
}]);
|
|
1641
|
+
structural.push(...pairedCenteredPaths(bracePath, width));
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
for (let index = 0; index < pairCount; index += 1) {
|
|
1645
|
+
const nesting = 1 - index * .16;
|
|
1646
|
+
const startX = canonicalNumber(edge + index * Math.min(6, frameBand * .22));
|
|
1647
|
+
const reach = canonicalNumber(Math.min(halfSpan * .86, coilRadius * (1.25 + index * .28)));
|
|
1648
|
+
const cascadeLift = canonicalNumber(lift * nesting);
|
|
1649
|
+
const leftCascade = canonicalizePath([
|
|
1650
|
+
{
|
|
1651
|
+
kind: "M",
|
|
1652
|
+
x: startX,
|
|
1653
|
+
y: railY
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
kind: "C",
|
|
1657
|
+
x1: startX + reach * .22,
|
|
1658
|
+
y1: railY - cascadeLift * .84,
|
|
1659
|
+
x2: startX + reach * .68,
|
|
1660
|
+
y2: railY - cascadeLift,
|
|
1661
|
+
x: startX + reach,
|
|
1662
|
+
y: railY - cascadeLift * .54
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
kind: "C",
|
|
1666
|
+
x1: startX + reach * .88,
|
|
1667
|
+
y1: railY - cascadeLift * .22,
|
|
1668
|
+
x2: startX + reach * .58,
|
|
1669
|
+
y2: railY - cascadeLift * .14,
|
|
1670
|
+
x: startX + reach * .42,
|
|
1671
|
+
y: railY
|
|
1672
|
+
}
|
|
1673
|
+
]);
|
|
1674
|
+
structural.push(leftCascade, mirrorPath(leftCascade, width));
|
|
1675
|
+
}
|
|
1676
|
+
const centerStem = canonicalizePath([{
|
|
1677
|
+
kind: "M",
|
|
1678
|
+
x: width / 2,
|
|
1679
|
+
y: railY
|
|
1680
|
+
}, {
|
|
1681
|
+
kind: "L",
|
|
1682
|
+
x: width / 2,
|
|
1683
|
+
y: railY - lift * .64
|
|
1684
|
+
}]);
|
|
1685
|
+
structural.push(...pairedCenteredPaths(centerStem, width));
|
|
1686
|
+
}
|
|
1687
|
+
const decorative = [];
|
|
1688
|
+
const sideMiddle = canonicalNumber((railY + bottomY) / 2);
|
|
1689
|
+
const sideReach = canonicalNumber(safeLeft - edge - 1.0001);
|
|
1690
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([{
|
|
1691
|
+
kind: "M",
|
|
1692
|
+
x: edge,
|
|
1693
|
+
y: railY
|
|
1694
|
+
}, {
|
|
1695
|
+
kind: "C",
|
|
1696
|
+
x1: edge + sideReach,
|
|
1697
|
+
y1: railY + (sideMiddle - railY) * .42,
|
|
1698
|
+
x2: edge + sideReach,
|
|
1699
|
+
y2: sideMiddle - frameBand * .24,
|
|
1700
|
+
x: edge,
|
|
1701
|
+
y: sideMiddle
|
|
1702
|
+
}]) : sideLevel === 1 ? canonicalizePath([
|
|
1703
|
+
{
|
|
1704
|
+
kind: "M",
|
|
1705
|
+
x: edge,
|
|
1706
|
+
y: railY
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
kind: "C",
|
|
1710
|
+
x1: edge + sideReach,
|
|
1711
|
+
y1: railY + (sideMiddle - railY) * .38,
|
|
1712
|
+
x2: edge,
|
|
1713
|
+
y2: sideMiddle - frameBand * .28,
|
|
1714
|
+
x: edge + sideReach * .62,
|
|
1715
|
+
y: sideMiddle
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
kind: "C",
|
|
1719
|
+
x1: edge + sideReach,
|
|
1720
|
+
y1: sideMiddle + frameBand * .28,
|
|
1721
|
+
x2: edge,
|
|
1722
|
+
y2: bottomY - (bottomY - sideMiddle) * .38,
|
|
1723
|
+
x: edge,
|
|
1724
|
+
y: bottomY
|
|
1725
|
+
}
|
|
1726
|
+
]) : canonicalizePath([
|
|
1727
|
+
{
|
|
1728
|
+
kind: "M",
|
|
1729
|
+
x: edge,
|
|
1730
|
+
y: railY
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
kind: "C",
|
|
1734
|
+
x1: edge + sideReach,
|
|
1735
|
+
y1: railY + (sideMiddle - railY) * .28,
|
|
1736
|
+
x2: edge + sideReach,
|
|
1737
|
+
y2: sideMiddle - frameBand * .4,
|
|
1738
|
+
x: edge + sideReach * .46,
|
|
1739
|
+
y: sideMiddle - frameBand * .18
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
kind: "C",
|
|
1743
|
+
x1: edge,
|
|
1744
|
+
y1: sideMiddle,
|
|
1745
|
+
x2: edge + sideReach,
|
|
1746
|
+
y2: sideMiddle + frameBand * .12,
|
|
1747
|
+
x: edge + sideReach * .68,
|
|
1748
|
+
y: sideMiddle + frameBand * .3
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
kind: "C",
|
|
1752
|
+
x1: edge + sideReach,
|
|
1753
|
+
y1: sideMiddle + frameBand * .5,
|
|
1754
|
+
x2: edge,
|
|
1755
|
+
y2: bottomY - (bottomY - sideMiddle) * .28,
|
|
1756
|
+
x: edge,
|
|
1757
|
+
y: bottomY
|
|
1758
|
+
}
|
|
1759
|
+
]);
|
|
1760
|
+
const rightSideDisplacement = quantizedSymmetryDisplacement(options.symmetry, sampleScalar$1(seed, "volute/detail", pairCount) - .5, frameBand * .12);
|
|
1761
|
+
decorative.push(leftSide, displaceInteriorY(mirrorPath(leftSide, width), rightSideDisplacement));
|
|
1762
|
+
return {
|
|
1763
|
+
structural,
|
|
1764
|
+
decorative
|
|
1765
|
+
};
|
|
1766
|
+
};
|
|
1767
|
+
function generateGateV2(dimensionsInput, optionsInput) {
|
|
1768
|
+
const dimensions = normalizeDimensions(dimensionsInput);
|
|
1769
|
+
const { seed, options } = normalizeFamilyOptions(optionsInput, 2);
|
|
1770
|
+
const { width, height } = dimensions;
|
|
1771
|
+
const edge = canonicalNumber(9);
|
|
1772
|
+
const railY = canonicalNumber(Math.max(edge + 32, height * .3));
|
|
1773
|
+
const bottomY = canonicalNumber(height - edge);
|
|
1774
|
+
const frameBand = canonicalNumber(Math.max(12, Math.min(24, Math.min(width, height) * .06)));
|
|
1775
|
+
const safeLeft = canonicalNumber(edge + frameBand);
|
|
1776
|
+
const safeRight = canonicalNumber(width - safeLeft);
|
|
1777
|
+
const safeTop = canonicalNumber(railY + frameBand);
|
|
1778
|
+
const safeBottom = canonicalNumber(bottomY - frameBand);
|
|
1779
|
+
const centerX = canonicalNumber(width / 2);
|
|
1780
|
+
const surfacePath = canonicalizePath([
|
|
1781
|
+
{
|
|
1782
|
+
kind: "M",
|
|
1783
|
+
x: edge,
|
|
1784
|
+
y: railY
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
kind: "L",
|
|
1788
|
+
x: width - edge,
|
|
1789
|
+
y: railY
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
kind: "L",
|
|
1793
|
+
x: width - edge,
|
|
1794
|
+
y: bottomY
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
kind: "L",
|
|
1798
|
+
x: edge,
|
|
1799
|
+
y: bottomY
|
|
1800
|
+
},
|
|
1801
|
+
{ kind: "Z" },
|
|
1802
|
+
{
|
|
1803
|
+
kind: "M",
|
|
1804
|
+
x: safeLeft,
|
|
1805
|
+
y: safeTop
|
|
1806
|
+
},
|
|
1807
|
+
{
|
|
1808
|
+
kind: "L",
|
|
1809
|
+
x: safeLeft,
|
|
1810
|
+
y: safeBottom
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
kind: "L",
|
|
1814
|
+
x: safeRight,
|
|
1815
|
+
y: safeBottom
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
kind: "L",
|
|
1819
|
+
x: safeRight,
|
|
1820
|
+
y: safeTop
|
|
1821
|
+
},
|
|
1822
|
+
{ kind: "Z" }
|
|
1823
|
+
]);
|
|
1824
|
+
const framePath = canonicalizePath([
|
|
1825
|
+
{
|
|
1826
|
+
kind: "M",
|
|
1827
|
+
x: edge,
|
|
1828
|
+
y: bottomY
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
kind: "L",
|
|
1832
|
+
x: edge,
|
|
1833
|
+
y: railY
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
kind: "L",
|
|
1837
|
+
x: width - edge,
|
|
1838
|
+
y: railY
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
kind: "L",
|
|
1842
|
+
x: width - edge,
|
|
1843
|
+
y: bottomY
|
|
1844
|
+
},
|
|
1845
|
+
{ kind: "Z" }
|
|
1846
|
+
]);
|
|
1847
|
+
if (options.family === "courtyard" || options.family === "fleuron" || options.family === "vine" || options.family === "fan" || options.family === "volute") {
|
|
1848
|
+
const familyLayout = {
|
|
1849
|
+
width,
|
|
1850
|
+
edge,
|
|
1851
|
+
railY,
|
|
1852
|
+
bottomY,
|
|
1853
|
+
frameBand,
|
|
1854
|
+
safeLeft
|
|
1855
|
+
};
|
|
1856
|
+
const composed = options.family === "courtyard" ? composeCourtyard(seed, options, familyLayout) : options.family === "fleuron" ? composeFleuron(seed, options, familyLayout) : options.family === "vine" ? composeVine(seed, options, familyLayout) : options.family === "fan" ? composeFan(seed, options, familyLayout) : composeVolute(seed, options, familyLayout);
|
|
1857
|
+
const decorativePaths = [...composed.decorative];
|
|
1858
|
+
const crestPath = createCrestPath(options.crest, centerX, edge, railY, options.peakHeight);
|
|
1859
|
+
if (crestPath !== void 0) decorativePaths.push(...pairedCenteredPaths(crestPath, width));
|
|
1860
|
+
return deepFreeze({
|
|
1861
|
+
schemaVersion: 1,
|
|
1862
|
+
generationVersion: 2,
|
|
1863
|
+
seed,
|
|
1864
|
+
dimensions,
|
|
1865
|
+
viewBox: {
|
|
1866
|
+
minX: 0,
|
|
1867
|
+
minY: 0,
|
|
1868
|
+
width,
|
|
1869
|
+
height
|
|
1870
|
+
},
|
|
1871
|
+
options,
|
|
1872
|
+
bounds: {
|
|
1873
|
+
left: edge,
|
|
1874
|
+
right: canonicalNumber(width - edge),
|
|
1875
|
+
railY,
|
|
1876
|
+
bottomY
|
|
1877
|
+
},
|
|
1878
|
+
contentInsets: {
|
|
1879
|
+
top: safeTop,
|
|
1880
|
+
right: canonicalNumber(width - safeRight),
|
|
1881
|
+
bottom: canonicalNumber(height - safeBottom),
|
|
1882
|
+
left: safeLeft
|
|
1883
|
+
},
|
|
1884
|
+
surfacePath,
|
|
1885
|
+
structuralPaths: [framePath, ...composed.structural],
|
|
1886
|
+
decorativePaths
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
const composition = sampleIndex$1(seed, "arcade/composition", 3);
|
|
1890
|
+
const motifLevel = Math.min(2, Math.floor(3 * options.density + sampleScalar$1(seed, "arcade/motif-count")));
|
|
1891
|
+
const sideLevel = Math.min(2, Math.floor(3 * options.sideComplexity + sampleScalar$1(seed, "arcade/sides")));
|
|
1892
|
+
const bayCount = [
|
|
1893
|
+
2,
|
|
1894
|
+
3,
|
|
1895
|
+
5
|
|
1896
|
+
][motifLevel];
|
|
1897
|
+
const span = width - edge * 2;
|
|
1898
|
+
const upperHeight = railY - edge;
|
|
1899
|
+
const springRatio = .32 + .26 * sampleScalar$1(seed, "arcade/anchor/spring-height");
|
|
1900
|
+
const archLift = canonicalNumber(upperHeight * springRatio * (.72 + options.curvature * .28) * (.78 + options.peakHeight * .22));
|
|
1901
|
+
const boundaries = Array.from({ length: bayCount + 1 }, (_, index) => canonicalNumber(edge + span * index / bayCount));
|
|
1902
|
+
const arches = [];
|
|
1903
|
+
for (let index = 0; index < bayCount; index += 1) {
|
|
1904
|
+
const left = boundaries[index];
|
|
1905
|
+
const right = boundaries[index + 1];
|
|
1906
|
+
const detail = sampleScalar$1(seed, "arcade/detail", index);
|
|
1907
|
+
const displacement = quantizedSymmetryDisplacement(options.symmetry, detail - .5, (right - left) * .16);
|
|
1908
|
+
const roundBay = canonicalizePath([{
|
|
1909
|
+
kind: "M",
|
|
1910
|
+
x: left,
|
|
1911
|
+
y: railY
|
|
1912
|
+
}, {
|
|
1913
|
+
kind: "Q",
|
|
1914
|
+
x1: (left + right) / 2 + displacement,
|
|
1915
|
+
y1: railY - archLift,
|
|
1916
|
+
x: right,
|
|
1917
|
+
y: railY
|
|
1918
|
+
}]);
|
|
1919
|
+
const center = (left + right) / 2;
|
|
1920
|
+
const pointedBay = canonicalizePath([
|
|
1921
|
+
{
|
|
1922
|
+
kind: "M",
|
|
1923
|
+
x: left,
|
|
1924
|
+
y: railY
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
kind: "C",
|
|
1928
|
+
x1: left + (right - left) * .2 + displacement,
|
|
1929
|
+
y1: railY - archLift * .42,
|
|
1930
|
+
x2: center - (right - left) * .12 + displacement,
|
|
1931
|
+
y2: railY - archLift,
|
|
1932
|
+
x: center + displacement,
|
|
1933
|
+
y: railY - archLift
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
kind: "C",
|
|
1937
|
+
x1: center + (right - left) * .12 + displacement,
|
|
1938
|
+
y1: railY - archLift,
|
|
1939
|
+
x2: right - (right - left) * .2 + displacement,
|
|
1940
|
+
y2: railY - archLift * .42,
|
|
1941
|
+
x: right,
|
|
1942
|
+
y: railY
|
|
1943
|
+
}
|
|
1944
|
+
]);
|
|
1945
|
+
const mirroredPairIndex = Math.min(index, bayCount - 1 - index);
|
|
1946
|
+
arches.push(composition === 0 || composition === 2 && mirroredPairIndex % 2 === 0 ? roundBay : pointedBay);
|
|
1947
|
+
}
|
|
1948
|
+
const centerPair = centerPairForWidth(width);
|
|
1949
|
+
if (centerPair.left !== centerPair.right) {
|
|
1950
|
+
if (options.symmetry === 1) for (let index = 0; index < Math.floor(bayCount / 2); index += 1) {
|
|
1951
|
+
const leftBay = arches[index];
|
|
1952
|
+
arches[bayCount - 1 - index] = mirrorPath(leftBay, width);
|
|
1953
|
+
}
|
|
1954
|
+
if (bayCount % 2 === 1) {
|
|
1955
|
+
const middleIndex = Math.floor(bayCount / 2);
|
|
1956
|
+
const middleBay = arches[middleIndex];
|
|
1957
|
+
arches.splice(middleIndex + 1, 0, mirrorPath(middleBay, width));
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const decorativePaths = [];
|
|
1961
|
+
const crestPath = createCrestPath(options.crest, centerX, edge, railY, options.peakHeight);
|
|
1962
|
+
if (crestPath !== void 0) decorativePaths.push(...pairedCenteredPaths(crestPath, width));
|
|
1963
|
+
const sideCenterY = canonicalNumber((railY + bottomY) / 2);
|
|
1964
|
+
const sideReach = canonicalNumber(safeLeft - edge - 2);
|
|
1965
|
+
const leftSide = sideLevel === 0 ? canonicalizePath([{
|
|
1966
|
+
kind: "M",
|
|
1967
|
+
x: edge + sideReach * .34,
|
|
1968
|
+
y: railY
|
|
1969
|
+
}, {
|
|
1970
|
+
kind: "L",
|
|
1971
|
+
x: edge + sideReach * .34,
|
|
1972
|
+
y: bottomY
|
|
1973
|
+
}]) : sideLevel === 1 ? canonicalizePath([{
|
|
1974
|
+
kind: "M",
|
|
1975
|
+
x: edge,
|
|
1976
|
+
y: railY
|
|
1977
|
+
}, {
|
|
1978
|
+
kind: "Q",
|
|
1979
|
+
x1: edge + sideReach * (.65 + options.curvature * .25),
|
|
1980
|
+
y1: sideCenterY,
|
|
1981
|
+
x: edge,
|
|
1982
|
+
y: bottomY
|
|
1983
|
+
}]) : canonicalizePath([
|
|
1984
|
+
{
|
|
1985
|
+
kind: "M",
|
|
1986
|
+
x: edge,
|
|
1987
|
+
y: railY
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
kind: "L",
|
|
1991
|
+
x: edge + sideReach * .55,
|
|
1992
|
+
y: sideCenterY - frameBand * .5
|
|
1993
|
+
},
|
|
1994
|
+
{
|
|
1995
|
+
kind: "L",
|
|
1996
|
+
x: edge + sideReach,
|
|
1997
|
+
y: sideCenterY
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
kind: "L",
|
|
2001
|
+
x: edge + sideReach * .55,
|
|
2002
|
+
y: sideCenterY + frameBand * .5
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
kind: "L",
|
|
2006
|
+
x: edge,
|
|
2007
|
+
y: bottomY
|
|
2008
|
+
}
|
|
2009
|
+
]);
|
|
2010
|
+
decorativePaths.push(leftSide, mirrorPath(leftSide, width));
|
|
2011
|
+
return deepFreeze({
|
|
2012
|
+
schemaVersion: 1,
|
|
2013
|
+
generationVersion: 2,
|
|
2014
|
+
seed,
|
|
2015
|
+
dimensions,
|
|
2016
|
+
viewBox: {
|
|
2017
|
+
minX: 0,
|
|
2018
|
+
minY: 0,
|
|
2019
|
+
width,
|
|
2020
|
+
height
|
|
2021
|
+
},
|
|
2022
|
+
options,
|
|
2023
|
+
bounds: {
|
|
2024
|
+
left: edge,
|
|
2025
|
+
right: canonicalNumber(width - edge),
|
|
2026
|
+
railY,
|
|
2027
|
+
bottomY
|
|
2028
|
+
},
|
|
2029
|
+
contentInsets: {
|
|
2030
|
+
top: safeTop,
|
|
2031
|
+
right: canonicalNumber(width - safeRight),
|
|
2032
|
+
bottom: canonicalNumber(height - safeBottom),
|
|
2033
|
+
left: safeLeft
|
|
2034
|
+
},
|
|
2035
|
+
surfacePath,
|
|
2036
|
+
structuralPaths: [framePath, ...arches],
|
|
2037
|
+
decorativePaths
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/core/v3/generate.ts
|
|
2043
|
+
function generateGateV3(dimensions, optionsInput) {
|
|
2044
|
+
const { seed, options } = normalizeFamilyOptions(optionsInput, 3);
|
|
2045
|
+
dimensions = normalizeDimensions(dimensions);
|
|
2046
|
+
const { width, height } = dimensions;
|
|
2047
|
+
const edge = 9;
|
|
2048
|
+
const railY = canonicalNumber(Math.max(41, height * .3));
|
|
2049
|
+
const bottomY = canonicalNumber(height - edge);
|
|
2050
|
+
const frameBand = canonicalNumber(Math.max(12, Math.min(24, Math.min(width, height) * .06)));
|
|
2051
|
+
const safeLeft = canonicalNumber(edge + frameBand);
|
|
2052
|
+
const safeRight = canonicalNumber(width - safeLeft);
|
|
2053
|
+
const safeTop = canonicalNumber(railY + frameBand);
|
|
2054
|
+
const safeBottom = canonicalNumber(bottomY - frameBand);
|
|
2055
|
+
const source = {
|
|
2056
|
+
seed,
|
|
2057
|
+
options,
|
|
2058
|
+
dimensions,
|
|
2059
|
+
bounds: {
|
|
2060
|
+
left: edge,
|
|
2061
|
+
right: canonicalNumber(width - edge),
|
|
2062
|
+
railY,
|
|
2063
|
+
bottomY
|
|
2064
|
+
},
|
|
2065
|
+
contentInsets: {
|
|
2066
|
+
left: safeLeft,
|
|
2067
|
+
right: canonicalNumber(width - safeRight),
|
|
2068
|
+
top: safeTop,
|
|
2069
|
+
bottom: canonicalNumber(height - safeBottom)
|
|
2070
|
+
},
|
|
2071
|
+
surfacePath: canonicalizePath([
|
|
2072
|
+
m(edge, railY),
|
|
2073
|
+
l(width - edge, railY),
|
|
2074
|
+
l(width - edge, bottomY),
|
|
2075
|
+
l(edge, bottomY),
|
|
2076
|
+
z,
|
|
2077
|
+
m(safeLeft, safeTop),
|
|
2078
|
+
l(safeLeft, safeBottom),
|
|
2079
|
+
l(safeRight, safeBottom),
|
|
2080
|
+
l(safeRight, safeTop),
|
|
2081
|
+
z
|
|
2082
|
+
]),
|
|
2083
|
+
structuralPaths: [canonicalizePath([
|
|
2084
|
+
m(edge, bottomY),
|
|
2085
|
+
l(edge, railY),
|
|
2086
|
+
l(width - edge, railY),
|
|
2087
|
+
l(width - edge, bottomY),
|
|
2088
|
+
z
|
|
2089
|
+
])]
|
|
2090
|
+
};
|
|
2091
|
+
const { railY: rail, bottomY: bottom } = source.bounds;
|
|
2092
|
+
const { family, crest, density, curvature, peakHeight, sideComplexity } = source.options;
|
|
2093
|
+
const upper = (rail - edge) * (.94 + peakHeight * .06);
|
|
2094
|
+
const center = width / 2;
|
|
2095
|
+
const half = center - edge;
|
|
2096
|
+
const band = source.contentInsets.left - edge;
|
|
2097
|
+
const structure = [source.structuralPaths[0]];
|
|
2098
|
+
const ornaments = [];
|
|
2099
|
+
const fine = [];
|
|
2100
|
+
const solid = [];
|
|
2101
|
+
const centered = (collection, paths) => {
|
|
2102
|
+
for (const path of paths) {
|
|
2103
|
+
const canonical = canonicalizePath(path);
|
|
2104
|
+
collection.push(canonical);
|
|
2105
|
+
if (Math.round(width * 1e4) % 2 === 1) collection.push(mirrorPath(canonical, width));
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
const rightHeightFactor = 1 - (1 - options.symmetry) * (.06 + .04 * sampleScalar(seed, `${family}/proportion/right-lift`));
|
|
2109
|
+
const pair = (collection, path) => {
|
|
2110
|
+
const canonical = canonicalizePath(path);
|
|
2111
|
+
const right = mirrorPath(canonical, width);
|
|
2112
|
+
const isUpper = path.every((command) => Object.entries(command).every(([key, value]) => !key.startsWith("y") || Number(value) <= rail));
|
|
2113
|
+
collection.push(canonical, isUpper && options.symmetry < 1 ? place(right, 0, rail * (1 - rightHeightFactor), 1, rightHeightFactor) : right);
|
|
2114
|
+
};
|
|
2115
|
+
const barX = edge + Math.min(7, band * .36);
|
|
2116
|
+
pair(fine, [m(barX, rail), l(barX, bottom)]);
|
|
2117
|
+
fine.push(canonicalizePath([m(edge, bottom - 7), l(width - edge, bottom - 7)]));
|
|
2118
|
+
for (const fraction of sideComplexity > .65 ? [
|
|
2119
|
+
.2,
|
|
2120
|
+
.5,
|
|
2121
|
+
.8
|
|
2122
|
+
] : [.28, .72]) {
|
|
2123
|
+
const y = rail + (bottom - rail) * fraction;
|
|
2124
|
+
pair(solid, [
|
|
2125
|
+
m(edge, y - 1.5),
|
|
2126
|
+
l(barX + 2, y - 1.5),
|
|
2127
|
+
l(barX + 2, y + 1.5),
|
|
2128
|
+
l(edge, y + 1.5),
|
|
2129
|
+
z
|
|
2130
|
+
]);
|
|
2131
|
+
}
|
|
2132
|
+
const crestHeight = Math.min(upper * (.7 + peakHeight * .12), width * .29);
|
|
2133
|
+
const crestTop = rail - crestHeight - 4;
|
|
2134
|
+
const reserve = crest === "fleur" ? crestHeight * .44 + 7 : crest === "none" ? 9 : 16;
|
|
2135
|
+
if (crest === "fleur") {
|
|
2136
|
+
const fleurHeight = crestHeight * .84;
|
|
2137
|
+
const [crown, collar, foot] = fleurDeLys(center, rail - fleurHeight - 4, fleurHeight);
|
|
2138
|
+
centered(fine, [crown, foot]);
|
|
2139
|
+
centered(solid, [collar]);
|
|
2140
|
+
centered(structure, [[m(center, rail), l(center, rail - 4)]]);
|
|
2141
|
+
} else if (crest !== "none") {
|
|
2142
|
+
const capHeight = Math.min(22, crestHeight * .35);
|
|
2143
|
+
const capWidth = capHeight * (crest === "diamond" ? .32 : .22);
|
|
2144
|
+
centered(solid, [placeEmblem([
|
|
2145
|
+
m(0, 0),
|
|
2146
|
+
l(capWidth, capHeight * .58),
|
|
2147
|
+
l(0, capHeight),
|
|
2148
|
+
l(-capWidth, capHeight * .58),
|
|
2149
|
+
z
|
|
2150
|
+
], center, crestTop, 1)]);
|
|
2151
|
+
centered(structure, [[m(center, crestTop + capHeight), l(center, rail)]]);
|
|
2152
|
+
centered(fine, [canonicalizePath([m(center - 4, crestTop + capHeight + 4), l(center + 4, crestTop + capHeight + 4)])]);
|
|
2153
|
+
}
|
|
2154
|
+
const usable = half - reserve - 9;
|
|
2155
|
+
const variation = sampleScalar(source.seed, `${family}/proportion/study-lift`);
|
|
2156
|
+
const count = Math.max(1, Math.min(family === "arcade" ? 2 + Math.round(density * 2) : 1 + Math.round(density * 2), Math.floor(usable / 46)));
|
|
2157
|
+
const cell = usable / count;
|
|
2158
|
+
if (family === "fan") {
|
|
2159
|
+
const span = half - reserve;
|
|
2160
|
+
const lift = Math.min(upper * (.6 + variation * .12) * (.86 + curvature * .14), span * .9);
|
|
2161
|
+
const a = {
|
|
2162
|
+
x: edge,
|
|
2163
|
+
y: rail
|
|
2164
|
+
};
|
|
2165
|
+
const b = {
|
|
2166
|
+
x: edge + span * .2,
|
|
2167
|
+
y: rail - lift * .7
|
|
2168
|
+
};
|
|
2169
|
+
const d = {
|
|
2170
|
+
x: edge + span,
|
|
2171
|
+
y: rail - lift
|
|
2172
|
+
};
|
|
2173
|
+
const p = {
|
|
2174
|
+
x: edge + span * .65,
|
|
2175
|
+
y: rail - lift
|
|
2176
|
+
};
|
|
2177
|
+
pair(structure, [
|
|
2178
|
+
m(a.x, a.y),
|
|
2179
|
+
c(b.x, b.y, p.x, p.y, d.x, d.y),
|
|
2180
|
+
l(d.x, rail)
|
|
2181
|
+
]);
|
|
2182
|
+
const rays = Math.min(3 + Math.round(density * 3), Math.max(2, Math.floor(span / 16)));
|
|
2183
|
+
for (let index = 1; index < rays; index++) {
|
|
2184
|
+
const tip = cubicPoint(a, b, p, d, index / rays);
|
|
2185
|
+
pair(ornaments, [m(edge + span, rail), l(tip.x, tip.y)]);
|
|
2186
|
+
}
|
|
2187
|
+
} else if (family === "arcade") for (let index = 0; index < count; index++) {
|
|
2188
|
+
const x = 16 + cell * index;
|
|
2189
|
+
const w = cell - 9;
|
|
2190
|
+
const h = Math.min(upper * (.6 + variation * .1 + .1 * index / count) * (.86 + curvature * .14), w * 1.15);
|
|
2191
|
+
const shoulder = rail - h * .5;
|
|
2192
|
+
pair(ornaments, [
|
|
2193
|
+
m(x, rail),
|
|
2194
|
+
l(x, shoulder),
|
|
2195
|
+
c(x, rail - h * .82, x + w * .22, rail - h, x + w * .5, rail - h),
|
|
2196
|
+
c(x + w * .78, rail - h, x + w, rail - h * .82, x + w, shoulder),
|
|
2197
|
+
l(x + w, rail)
|
|
2198
|
+
]);
|
|
2199
|
+
}
|
|
2200
|
+
else if (family !== "vine") {
|
|
2201
|
+
const span = half - reserve;
|
|
2202
|
+
const coilWidth = Math.min(span * .57, upper * (family === "volute" ? 1.12 : .9));
|
|
2203
|
+
const coilX = edge + span - coilWidth;
|
|
2204
|
+
const height = Math.min(upper * (family === "courtyard" ? .61 : family === "fleuron" ? .79 : .88) * (.94 + curvature * .06 + variation * .035), coilWidth * 1.25);
|
|
2205
|
+
const top = rail - height;
|
|
2206
|
+
const curl = scroll(coilX, top, coilWidth, height);
|
|
2207
|
+
pair(ornaments, [
|
|
2208
|
+
m(edge, rail),
|
|
2209
|
+
c(edge, rail - height * .72, coilX + coilWidth * .18, top, coilX + coilWidth * .53, top),
|
|
2210
|
+
...curl.slice(2, coilWidth < 34 ? 5 : void 0)
|
|
2211
|
+
]);
|
|
2212
|
+
if (span > 120 && density > .25) {
|
|
2213
|
+
const smallWidth = Math.min(upper * .49, span * .25);
|
|
2214
|
+
const smallHeight = height * (family === "volute" ? .36 : .29);
|
|
2215
|
+
pair(fine, scroll(edge + span * .24, rail - smallHeight, smallWidth, smallHeight).slice(0, 5));
|
|
2216
|
+
}
|
|
2217
|
+
} else for (let index = 0; index < count; index++) {
|
|
2218
|
+
const w = Math.min(cell - 10, upper * .94);
|
|
2219
|
+
const x = 16 + cell * index + (cell - 10 - w) / 2;
|
|
2220
|
+
const hierarchy = count === 1 ? .78 : .54 + .24 * index / (count - 1);
|
|
2221
|
+
const h = Math.min(upper * hierarchy * (.88 + curvature * .12 + variation * .08), w * 1.25);
|
|
2222
|
+
const top = rail - h;
|
|
2223
|
+
const curl = scroll(x, top, w, h);
|
|
2224
|
+
pair(ornaments, w < 34 ? curl.slice(0, 5) : curl);
|
|
2225
|
+
if (family === "vine") for (const t of w > 50 ? [.24, .45] : [.34]) {
|
|
2226
|
+
const node = cubicPoint({
|
|
2227
|
+
x,
|
|
2228
|
+
y: rail
|
|
2229
|
+
}, {
|
|
2230
|
+
x,
|
|
2231
|
+
y: top + h * .48
|
|
2232
|
+
}, {
|
|
2233
|
+
x: x + w * .18,
|
|
2234
|
+
y: top
|
|
2235
|
+
}, {
|
|
2236
|
+
x: x + w * .53,
|
|
2237
|
+
y: top
|
|
2238
|
+
}, t);
|
|
2239
|
+
const size = Math.min(11, w * .16);
|
|
2240
|
+
pair(solid, [
|
|
2241
|
+
m(node.x, node.y),
|
|
2242
|
+
c(node.x - size * .6, node.y - size * .05, node.x - size, node.y - size * .75, node.x - size * .7, node.y - size * 1.5),
|
|
2243
|
+
c(node.x - size * .05, node.y - size, node.x + size * .12, node.y - size * .5, node.x, node.y),
|
|
2244
|
+
z
|
|
2245
|
+
]);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return deepFreeze({
|
|
2249
|
+
schemaVersion: 1,
|
|
2250
|
+
generationVersion: 3,
|
|
2251
|
+
seed,
|
|
2252
|
+
options,
|
|
2253
|
+
dimensions,
|
|
2254
|
+
viewBox: {
|
|
2255
|
+
minX: 0,
|
|
2256
|
+
minY: 0,
|
|
2257
|
+
width,
|
|
2258
|
+
height
|
|
2259
|
+
},
|
|
2260
|
+
bounds: source.bounds,
|
|
2261
|
+
contentInsets: source.contentInsets,
|
|
2262
|
+
surfacePath: source.surfacePath,
|
|
2263
|
+
structuralPaths: structure,
|
|
2264
|
+
finePaths: fine,
|
|
2265
|
+
decorativePaths: ornaments,
|
|
2266
|
+
solidPaths: solid
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
const cubicPoint = (a, b, c, d, t) => {
|
|
2270
|
+
const u = 1 - t;
|
|
2271
|
+
return {
|
|
2272
|
+
x: u ** 3 * a.x + 3 * u ** 2 * t * b.x + 3 * u * t ** 2 * c.x + t ** 3 * d.x,
|
|
2273
|
+
y: u ** 3 * a.y + 3 * u ** 2 * t * b.y + 3 * u * t ** 2 * c.y + t ** 3 * d.y
|
|
2274
|
+
};
|
|
2275
|
+
};
|
|
2276
|
+
const m = (x, y) => ({
|
|
2277
|
+
kind: "M",
|
|
2278
|
+
x,
|
|
2279
|
+
y
|
|
2280
|
+
});
|
|
2281
|
+
const l = (x, y) => ({
|
|
2282
|
+
kind: "L",
|
|
2283
|
+
x,
|
|
2284
|
+
y
|
|
2285
|
+
});
|
|
2286
|
+
const c = (x1, y1, x2, y2, x, y) => ({
|
|
2287
|
+
kind: "C",
|
|
2288
|
+
x1,
|
|
2289
|
+
y1,
|
|
2290
|
+
x2,
|
|
2291
|
+
y2,
|
|
2292
|
+
x,
|
|
2293
|
+
y
|
|
2294
|
+
});
|
|
2295
|
+
const z = { kind: "Z" };
|
|
2296
|
+
const place = (path, x, y, sx, sy = sx) => canonicalizePath(path.map((command) => {
|
|
2297
|
+
if (command.kind === "Z") return command;
|
|
2298
|
+
const point = {
|
|
2299
|
+
x: x + command.x * sx,
|
|
2300
|
+
y: y + command.y * sy
|
|
2301
|
+
};
|
|
2302
|
+
if (command.kind === "C") return {
|
|
2303
|
+
kind: "C",
|
|
2304
|
+
...point,
|
|
2305
|
+
x1: x + command.x1 * sx,
|
|
2306
|
+
y1: y + command.y1 * sy,
|
|
2307
|
+
x2: x + command.x2 * sx,
|
|
2308
|
+
y2: y + command.y2 * sy
|
|
2309
|
+
};
|
|
2310
|
+
if (command.kind === "Q") return {
|
|
2311
|
+
kind: "Q",
|
|
2312
|
+
...point,
|
|
2313
|
+
x1: x + command.x1 * sx,
|
|
2314
|
+
y1: y + command.y1 * sy
|
|
2315
|
+
};
|
|
2316
|
+
return {
|
|
2317
|
+
kind: command.kind,
|
|
2318
|
+
...point
|
|
2319
|
+
};
|
|
2320
|
+
}));
|
|
2321
|
+
/** Heraldic silhouette selected in ART-01. */
|
|
2322
|
+
const placeEmblem = (path, center, top, scale) => {
|
|
2323
|
+
const centerUnits = Math.round(center * 1e4);
|
|
2324
|
+
return canonicalizePath(path.map((command) => Object.fromEntries(Object.entries(command).map(([key, value]) => [key, key.startsWith("x") ? (centerUnits + Math.sign(Number(value)) * Math.round(Math.abs(Number(value)) * scale * 1e4)) / 1e4 : key.startsWith("y") ? top + Number(value) * scale : value]))));
|
|
2325
|
+
};
|
|
2326
|
+
function fleurDeLys(x, top, height) {
|
|
2327
|
+
const crown = [
|
|
2328
|
+
m(-.12, .682),
|
|
2329
|
+
c(-.14, .55, -.25, .39, -.3, .44),
|
|
2330
|
+
c(-.35, .49, -.3, .55, -.25, .5),
|
|
2331
|
+
c(-.28, .68, -.47, .55, -.36, .36),
|
|
2332
|
+
c(-.29, .25, -.15, .32, -.055, .51),
|
|
2333
|
+
c(-.13, .3, -.14, .16, 0, 0),
|
|
2334
|
+
c(.14, .16, .13, .3, .055, .51),
|
|
2335
|
+
c(.15, .32, .29, .25, .36, .36),
|
|
2336
|
+
c(.47, .55, .28, .68, .25, .5),
|
|
2337
|
+
c(.3, .55, .35, .49, .3, .44),
|
|
2338
|
+
c(.25, .39, .14, .55, .12, .682),
|
|
2339
|
+
l(-.12, .682),
|
|
2340
|
+
z
|
|
2341
|
+
];
|
|
2342
|
+
const collar = [
|
|
2343
|
+
m(-.13, .682),
|
|
2344
|
+
l(.13, .682),
|
|
2345
|
+
l(.13, .708),
|
|
2346
|
+
l(-.13, .708),
|
|
2347
|
+
z
|
|
2348
|
+
];
|
|
2349
|
+
const foot = [
|
|
2350
|
+
m(-.075, .708),
|
|
2351
|
+
c(-.08, .81, -.14, .85, -.2, .86),
|
|
2352
|
+
c(-.14, .93, -.07, .91, -.035, .86),
|
|
2353
|
+
c(-.04, .93, -.03, .97, 0, 1),
|
|
2354
|
+
c(.03, .97, .04, .93, .035, .86),
|
|
2355
|
+
c(.07, .91, .14, .93, .2, .86),
|
|
2356
|
+
c(.14, .85, .08, .81, .075, .708),
|
|
2357
|
+
l(-.075, .708),
|
|
2358
|
+
z
|
|
2359
|
+
];
|
|
2360
|
+
return [
|
|
2361
|
+
placeEmblem(crown, x, top, height),
|
|
2362
|
+
placeEmblem(collar, x, top, height),
|
|
2363
|
+
placeEmblem(foot, x, top, height)
|
|
2364
|
+
];
|
|
2365
|
+
}
|
|
2366
|
+
function scroll(x, top, width, height) {
|
|
2367
|
+
return place([
|
|
2368
|
+
m(0, 1),
|
|
2369
|
+
c(0, .48, .18, 0, .53, 0),
|
|
2370
|
+
c(.82, 0, 1, .18, 1, .44),
|
|
2371
|
+
c(1, .68, .83, .83, .65, .83),
|
|
2372
|
+
c(.49, .83, .39, .72, .39, .6),
|
|
2373
|
+
c(.39, .49, .47, .42, .56, .42),
|
|
2374
|
+
c(.64, .42, .69, .48, .69, .54)
|
|
2375
|
+
], x, top, width, height);
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
//#endregion
|
|
2379
|
+
//#region src/core/dispatch.ts
|
|
2380
|
+
const V2_ONLY_OPTION_KEYS = [
|
|
2381
|
+
"symmetry",
|
|
2382
|
+
"peakHeight",
|
|
2383
|
+
"sideComplexity",
|
|
2384
|
+
"crest"
|
|
2385
|
+
];
|
|
2386
|
+
function generateGate(dimensions, options) {
|
|
2387
|
+
if (options !== null && typeof options === "object" && Object.hasOwn(options, "generationVersion") && options.generationVersion === 3) return generateGateV3(dimensions, options);
|
|
2388
|
+
if (options !== null && typeof options === "object" && Object.hasOwn(options, "generationVersion") && options.generationVersion === 2) return generateGateV2(dimensions, options);
|
|
2389
|
+
if (options !== null && typeof options === "object") {
|
|
2390
|
+
for (const key of V2_ONLY_OPTION_KEYS) if (Object.hasOwn(options, key)) throw new TypeError(`${key} requires generationVersion 2`);
|
|
2391
|
+
}
|
|
2392
|
+
return generateGate$1(dimensions, options);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
//#endregion
|
|
2396
|
+
//#region src/core/svg.ts
|
|
2397
|
+
const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
2398
|
+
const COLOR_KEYWORD = /^(?:transparent|currentcolor)$/i;
|
|
2399
|
+
const FUNCTION_COLOR = /^(rgb|rgba|hsl|hsla)\((.*)\)$/i;
|
|
2400
|
+
const DECIMAL_NUMBER = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
2401
|
+
const XML_DELIMITER = /[&<>"']/;
|
|
2402
|
+
const sourceDimensions = (dimensions) => {
|
|
2403
|
+
const { width, height } = dimensions;
|
|
2404
|
+
for (const sourceWidth of [
|
|
2405
|
+
width,
|
|
2406
|
+
width - 499e-7,
|
|
2407
|
+
width + 499e-7
|
|
2408
|
+
]) for (const sourceHeight of [
|
|
2409
|
+
height,
|
|
2410
|
+
height - 499e-7,
|
|
2411
|
+
height + 499e-7
|
|
2412
|
+
]) {
|
|
2413
|
+
const ratio = sourceWidth / sourceHeight;
|
|
2414
|
+
if (ratio >= .5 && ratio <= 6) return {
|
|
2415
|
+
width: sourceWidth,
|
|
2416
|
+
height: sourceHeight
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
return dimensions;
|
|
2420
|
+
};
|
|
2421
|
+
const requirePlainObject = (value, name) => {
|
|
2422
|
+
if (value === null || typeof value !== "object") throw new TypeError(`${name} must be a plain object`);
|
|
2423
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2424
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${name} must be a plain object`);
|
|
2425
|
+
return value;
|
|
2426
|
+
};
|
|
2427
|
+
const structurallyEqual = (left, right) => {
|
|
2428
|
+
if (left === right) return true;
|
|
2429
|
+
if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => structurallyEqual(value, right[index]));
|
|
2430
|
+
if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
|
|
2431
|
+
const leftRecord = left;
|
|
2432
|
+
const rightRecord = right;
|
|
2433
|
+
const leftKeys = Reflect.ownKeys(leftRecord);
|
|
2434
|
+
const rightKeys = Reflect.ownKeys(rightRecord);
|
|
2435
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(rightRecord, key) && structurallyEqual(Reflect.get(leftRecord, key), Reflect.get(rightRecord, key)));
|
|
2436
|
+
};
|
|
2437
|
+
const regenerateGeometry = (geometry, sourceDimensions) => {
|
|
2438
|
+
const options = geometry.options;
|
|
2439
|
+
if (geometry.generationVersion === 1) {
|
|
2440
|
+
const publicOptions = {
|
|
2441
|
+
generationVersion: 1,
|
|
2442
|
+
seed: geometry.seed,
|
|
2443
|
+
family: "courtyard",
|
|
2444
|
+
density: options.density,
|
|
2445
|
+
curvature: options.curvature
|
|
2446
|
+
};
|
|
2447
|
+
return generateGate(sourceDimensions, publicOptions);
|
|
2448
|
+
}
|
|
2449
|
+
const publicOptions = {
|
|
2450
|
+
generationVersion: geometry.generationVersion === 3 ? 3 : 2,
|
|
2451
|
+
seed: geometry.seed,
|
|
2452
|
+
family: options.family,
|
|
2453
|
+
density: options.density,
|
|
2454
|
+
curvature: options.curvature,
|
|
2455
|
+
symmetry: options.symmetry,
|
|
2456
|
+
peakHeight: options.peakHeight,
|
|
2457
|
+
sideComplexity: options.sideComplexity,
|
|
2458
|
+
crest: options.crest
|
|
2459
|
+
};
|
|
2460
|
+
return generateGate(sourceDimensions, publicOptions);
|
|
2461
|
+
};
|
|
2462
|
+
const validateGeometryForSerialization = (value) => {
|
|
2463
|
+
const geometry = requirePlainObject(value, "geometry");
|
|
2464
|
+
const dimensions = requirePlainObject(geometry.dimensions, "geometry.dimensions");
|
|
2465
|
+
const canonicalGeometry = regenerateGeometry(geometry, sourceDimensions(dimensions));
|
|
2466
|
+
if (!structurallyEqual(geometry, canonicalGeometry)) throw new TypeError("geometry must exactly match canonical generated geometry");
|
|
2467
|
+
return canonicalGeometry;
|
|
2468
|
+
};
|
|
2469
|
+
const hasXmlBreakingCharacter = (value) => {
|
|
2470
|
+
if (XML_DELIMITER.test(value)) return true;
|
|
2471
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2472
|
+
const codeUnit = value.charCodeAt(index);
|
|
2473
|
+
if (codeUnit <= 8 || codeUnit === 11 || codeUnit === 12 || codeUnit >= 14 && codeUnit <= 31) return true;
|
|
2474
|
+
if (codeUnit === 127) return true;
|
|
2475
|
+
}
|
|
2476
|
+
return false;
|
|
2477
|
+
};
|
|
2478
|
+
const parseChannel = (token, maximum, percentOnly = false) => {
|
|
2479
|
+
const percent = token.endsWith("%");
|
|
2480
|
+
const numberText = percent ? token.slice(0, -1) : token;
|
|
2481
|
+
if (percentOnly && !percent || !DECIMAL_NUMBER.test(numberText)) return false;
|
|
2482
|
+
const value = Number(numberText);
|
|
2483
|
+
const upperBound = percent ? 100 : maximum;
|
|
2484
|
+
return Number.isFinite(value) && value >= 0 && value <= upperBound;
|
|
2485
|
+
};
|
|
2486
|
+
const isFunctionalColor = (paint) => {
|
|
2487
|
+
const match = FUNCTION_COLOR.exec(paint);
|
|
2488
|
+
if (match === null) return false;
|
|
2489
|
+
const functionName = match[1]?.toLowerCase();
|
|
2490
|
+
const channels = match[2]?.split(",").map((channel) => channel.trim()) ?? [];
|
|
2491
|
+
const hasAlpha = functionName === "rgba" || functionName === "hsla";
|
|
2492
|
+
if (channels.length !== (hasAlpha ? 4 : 3) || channels.some((channel) => channel === "")) return false;
|
|
2493
|
+
if (functionName === "rgb" || functionName === "rgba") {
|
|
2494
|
+
if (!channels.slice(0, 3).every((channel) => parseChannel(channel, 255))) return false;
|
|
2495
|
+
} else {
|
|
2496
|
+
const [hue, saturation, lightness] = channels;
|
|
2497
|
+
if (hue === void 0 || saturation === void 0 || lightness === void 0 || !DECIMAL_NUMBER.test(hue) || !Number.isFinite(Number(hue)) || !parseChannel(saturation, 100, true) || !parseChannel(lightness, 100, true)) return false;
|
|
2498
|
+
}
|
|
2499
|
+
const alpha = channels[3];
|
|
2500
|
+
return alpha === void 0 || parseChannel(alpha, 1);
|
|
2501
|
+
};
|
|
2502
|
+
const normalizePaint = (value, name) => {
|
|
2503
|
+
if (typeof value !== "string") throw new TypeError(`${name} paint must be a string`);
|
|
2504
|
+
const paint = value.trim();
|
|
2505
|
+
if (paint.length === 0 || hasXmlBreakingCharacter(paint) || !HEX_COLOR.test(paint) && !COLOR_KEYWORD.test(paint) && !isFunctionalColor(paint)) throw new TypeError(`${name} paint must be a safe literal CSS color`);
|
|
2506
|
+
return paint;
|
|
2507
|
+
};
|
|
2508
|
+
const escapeXmlAttribute = (value) => value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">").replaceAll("'", "'");
|
|
2509
|
+
const serializeCommand = (command) => {
|
|
2510
|
+
switch (command.kind) {
|
|
2511
|
+
case "M":
|
|
2512
|
+
case "L": return `${command.kind}${command.x} ${command.y}`;
|
|
2513
|
+
case "C": return `C${command.x1} ${command.y1} ${command.x2} ${command.y2} ${command.x} ${command.y}`;
|
|
2514
|
+
case "Q": return `Q${command.x1} ${command.y1} ${command.x} ${command.y}`;
|
|
2515
|
+
case "Z": return "Z";
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
const serializePath = (path) => path.map(serializeCommand).join(" ");
|
|
2519
|
+
function renderGateSVG(geometry, paint = {}) {
|
|
2520
|
+
const canonicalGeometry = validateGeometryForSerialization(geometry);
|
|
2521
|
+
const stroke = escapeXmlAttribute(normalizePaint(paint.stroke ?? "currentColor", "stroke"));
|
|
2522
|
+
const surface = escapeXmlAttribute(normalizePaint(paint.surface ?? "transparent", "surface"));
|
|
2523
|
+
const { width, height } = canonicalGeometry.dimensions;
|
|
2524
|
+
const viewBox = canonicalGeometry.viewBox;
|
|
2525
|
+
const surfacePath = `<path d="${serializePath(canonicalGeometry.surfacePath)}" fill="${surface}" fill-rule="evenodd" stroke="none"/>`;
|
|
2526
|
+
if (canonicalGeometry.generationVersion === 3) {
|
|
2527
|
+
const paths = (items) => items.map((path) => `<path d="${serializePath(path)}"/>`).join("");
|
|
2528
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}" data-gate-frame-generation-version="3">${surfacePath}<g stroke="${stroke}" fill="none" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="2">${paths(canonicalGeometry.structuralPaths)}</g><g stroke-width="1.8">${paths(canonicalGeometry.decorativePaths)}</g><g stroke-width="1.2">${paths(canonicalGeometry.finePaths)}</g><g stroke-width="0.8" fill="${stroke}">${paths(canonicalGeometry.solidPaths)}</g></g></svg>`;
|
|
2529
|
+
}
|
|
2530
|
+
const ironPaths = [...canonicalGeometry.structuralPaths.map((path) => `<path d="${serializePath(path)}" fill="none"/>`), ...canonicalGeometry.decorativePaths.map((path) => `<path d="${serializePath(path)}" fill="none"/>`)].join("");
|
|
2531
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}" data-gate-frame-generation-version="${canonicalGeometry.generationVersion}">${surfacePath}<g stroke="${stroke}" stroke-width="${canonicalGeometry.options.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">${ironPaths}</g></svg>`;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
//#endregion
|
|
2535
|
+
export { isGateFamily as i, generateGate as n, isCompatibleCrest as r, renderGateSVG as t };
|
|
2536
|
+
//# sourceMappingURL=core-DzM1ds_q.js.map
|