ridgescape 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1574 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod3) => __copyProps(__defProp({}, "__esModule", { value: true }), mod3);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ ANCHOR_HEIGHTS: () => ANCHOR_HEIGHTS,
24
+ CLOUD_DEFAULTS: () => CLOUD_DEFAULTS,
25
+ DEPTH_DEFAULTS: () => DEPTH_DEFAULTS,
26
+ LAKE_DEFAULTS: () => LAKE_DEFAULTS,
27
+ PARALLAX_DEFAULTS: () => PARALLAX_DEFAULTS,
28
+ PROFILE_DEFAULTS: () => PROFILE_DEFAULTS,
29
+ SAMPLE_STEP: () => SAMPLE_STEP,
30
+ SCENE_DEFAULTS: () => SCENE_DEFAULTS,
31
+ SKY_DEFAULTS: () => SKY_DEFAULTS,
32
+ TREE_DEFAULTS: () => TREE_DEFAULTS,
33
+ TREE_GROUPS: () => TREE_GROUPS,
34
+ TREE_SHAPES: () => TREE_SHAPES,
35
+ attachParallax: () => attachParallax,
36
+ buildOutline: () => buildOutline,
37
+ buildProfile: () => buildProfile,
38
+ capsulePath: () => capsulePath,
39
+ circlePath: () => circlePath,
40
+ clipId: () => clipId,
41
+ clipStyle: () => clipStyle,
42
+ createRandom: () => createRandom,
43
+ defsMarkup: () => defsMarkup,
44
+ drawLayer: () => drawLayer,
45
+ drawShape: () => drawShape,
46
+ drawToCanvas: () => drawToCanvas,
47
+ formatColor: () => format,
48
+ formatNumber: () => formatNumber,
49
+ gradientMarkup: () => gradientMarkup,
50
+ groundPath: () => groundPath,
51
+ hashString: () => hashString,
52
+ layerColor: () => layerColor,
53
+ layerMarkup: () => layerMarkup,
54
+ mirrorClip: () => mirrorClip,
55
+ mirrorClipId: () => mirrorClipId,
56
+ mix: () => mix,
57
+ octaves: () => octaves,
58
+ palettes: () => palettes,
59
+ parallaxDelta: () => parallaxDelta,
60
+ parallaxOffset: () => parallaxOffset,
61
+ parseColor: () => parseColor,
62
+ periodicNoise: () => periodicNoise,
63
+ placeTrees: () => placeTrees,
64
+ presets: () => presets,
65
+ rectPath: () => rectPath,
66
+ reflectionLayers: () => reflectionLayers,
67
+ render: () => render,
68
+ resolve: () => resolve,
69
+ resolveLength: () => resolveLength,
70
+ resolvePalette: () => resolvePalette,
71
+ ridgescapeParentStyle: () => ridgescapeParentStyle,
72
+ scaleLength: () => scaleLength,
73
+ serialize: () => serialize,
74
+ shapeMarkup: () => shapeMarkup,
75
+ simplify: () => simplify,
76
+ toSvg: () => toSvg,
77
+ treeHeightAt: () => treeHeightAt,
78
+ treeKeyPoints: () => treeKeyPoints,
79
+ waterBase: () => waterBase,
80
+ waterSurface: () => waterSurface
81
+ });
82
+ module.exports = __toCommonJS(src_exports);
83
+
84
+ // src/color/parse.ts
85
+ var NAMED_SOURCE = "aliceblue f0f8ff antiquewhite faebd7 aqua 00ffff aquamarine 7fffd4 azure f0ffff beige f5f5dc bisque ffe4c4 black 000000 blanchedalmond ffebcd blue 0000ff blueviolet 8a2be2 brown a52a2a burlywood deb887 cadetblue 5f9ea0 chartreuse 7fff00 chocolate d2691e coral ff7f50 cornflowerblue 6495ed cornsilk fff8dc crimson dc143c cyan 00ffff darkblue 00008b darkcyan 008b8b darkgoldenrod b8860b darkgray a9a9a9 darkgreen 006400 darkgrey a9a9a9 darkkhaki bdb76b darkmagenta 8b008b darkolivegreen 556b2f darkorange ff8c00 darkorchid 9932cc darkred 8b0000 darksalmon e9967a darkseagreen 8fbc8f darkslateblue 483d8b darkslategray 2f4f4f darkslategrey 2f4f4f darkturquoise 00ced1 darkviolet 9400d3 deeppink ff1493 deepskyblue 00bfff dimgray 696969 dimgrey 696969 dodgerblue 1e90ff firebrick b22222 floralwhite fffaf0 forestgreen 228b22 fuchsia ff00ff gainsboro dcdcdc ghostwhite f8f8ff gold ffd700 goldenrod daa520 gray 808080 green 008000 greenyellow adff2f grey 808080 honeydew f0fff0 hotpink ff69b4 indianred cd5c5c indigo 4b0082 ivory fffff0 khaki f0e68c lavender e6e6fa lavenderblush fff0f5 lawngreen 7cfc00 lemonchiffon fffacd lightblue add8e6 lightcoral f08080 lightcyan e0ffff lightgoldenrodyellow fafad2 lightgray d3d3d3 lightgreen 90ee90 lightgrey d3d3d3 lightpink ffb6c1 lightsalmon ffa07a lightseagreen 20b2aa lightskyblue 87cefa lightslategray 778899 lightslategrey 778899 lightsteelblue b0c4de lightyellow ffffe0 lime 00ff00 limegreen 32cd32 linen faf0e6 magenta ff00ff maroon 800000 mediumaquamarine 66cdaa mediumblue 0000cd mediumorchid ba55d3 mediumpurple 9370db mediumseagreen 3cb371 mediumslateblue 7b68ee mediumspringgreen 00fa9a mediumturquoise 48d1cc mediumvioletred c71585 midnightblue 191970 mintcream f5fffa mistyrose ffe4e1 moccasin ffe4b5 navajowhite ffdead navy 000080 oldlace fdf5e6 olive 808000 olivedrab 6b8e23 orange ffa500 orangered ff4500 orchid da70d6 palegoldenrod eee8aa palegreen 98fb98 paleturquoise afeeee palevioletred db7093 papayawhip ffefd5 peachpuff ffdab9 peru cd853f pink ffc0cb plum dda0dd powderblue b0e0e6 purple 800080 rebeccapurple 663399 red ff0000 rosybrown bc8f8f royalblue 4169e1 saddlebrown 8b4513 salmon fa8072 sandybrown f4a460 seagreen 2e8b57 seashell fff5ee sienna a0522d silver c0c0c0 skyblue 87ceeb slateblue 6a5acd slategray 708090 slategrey 708090 snow fffafa springgreen 00ff7f steelblue 4682b4 tan d2b48c teal 008080 thistle d8bfd8 tomato ff6347 turquoise 40e0d0 violet ee82ee wheat f5deb3 white ffffff whitesmoke f5f5f5 yellow ffff00 yellowgreen 9acd32";
86
+ var named;
87
+ function namedColor(name) {
88
+ if (!named) {
89
+ named = /* @__PURE__ */ new Map();
90
+ const parts = NAMED_SOURCE.split(" ");
91
+ for (let i = 0; i < parts.length; i += 2) named.set(parts[i], parts[i + 1]);
92
+ }
93
+ return named.get(name);
94
+ }
95
+ var clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
96
+ function hex(h) {
97
+ if (!/^[0-9a-f]+$/i.test(h)) return null;
98
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
99
+ if (h.length !== 6 && h.length !== 8) return null;
100
+ const n = (i) => parseInt(h.slice(i, i + 2), 16);
101
+ return { r: n(0), g: n(2), b: n(4), a: h.length === 8 ? n(6) / 255 : 1 };
102
+ }
103
+ function channel(token, max) {
104
+ return token.endsWith("%") ? parseFloat(token) / 100 * max : parseFloat(token);
105
+ }
106
+ function alpha(token) {
107
+ if (token === void 0) return 1;
108
+ return clamp(token.endsWith("%") ? parseFloat(token) / 100 : parseFloat(token), 0, 1);
109
+ }
110
+ function hslToRgb(h, s, l) {
111
+ const k = (n) => (n + h / 30) % 12;
112
+ const a = s * Math.min(l, 1 - l);
113
+ const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
114
+ return [f(0) * 255, f(8) * 255, f(4) * 255];
115
+ }
116
+ function parseColor(input) {
117
+ const c = input.trim().toLowerCase();
118
+ if (c === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
119
+ if (c.startsWith("#")) return hex(c.slice(1));
120
+ const name = namedColor(c);
121
+ if (name) return hex(name);
122
+ const m = /^(rgba?|hsla?)\((.*)\)$/.exec(c);
123
+ if (!m) return null;
124
+ const parts = m[2].replace(/\s*\/\s*/, " / ").split(/[\s,]+/).filter((p) => p && p !== "/");
125
+ if (parts.length < 3 || parts.length > 4 || parts.some((p) => !/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?(%|deg)?$/.test(p))) return null;
126
+ if (m[1].startsWith("rgb")) {
127
+ return { r: clamp(channel(parts[0], 255), 0, 255), g: clamp(channel(parts[1], 255), 0, 255), b: clamp(channel(parts[2], 255), 0, 255), a: alpha(parts[3]) };
128
+ }
129
+ const h = (parseFloat(parts[0]) % 360 + 360) % 360;
130
+ const [r, g, b] = hslToRgb(h, clamp(parseFloat(parts[1]) / 100, 0, 1), clamp(parseFloat(parts[2]) / 100, 0, 1));
131
+ return { r, g, b, a: alpha(parts[3]) };
132
+ }
133
+
134
+ // src/color/index.ts
135
+ var clamp2 = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
136
+ var toLinear = (c) => {
137
+ const v = c / 255;
138
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
139
+ };
140
+ var fromLinear = (v) => (v <= 31308e-7 ? 12.92 * Math.max(0, v) : 1.055 * v ** (1 / 2.4) - 0.055) * 255;
141
+ function toOklab({ r, g, b, a }) {
142
+ const lr = toLinear(r), lg = toLinear(g), lb = toLinear(b);
143
+ const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
144
+ const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
145
+ const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
146
+ return [
147
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
148
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
149
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
150
+ a
151
+ ];
152
+ }
153
+ function fromOklab([L, A, B, alpha2]) {
154
+ const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
155
+ const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
156
+ const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
157
+ return {
158
+ r: fromLinear(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
159
+ g: fromLinear(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
160
+ b: fromLinear(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
161
+ a: alpha2
162
+ };
163
+ }
164
+ function parse(color) {
165
+ const c = parseColor(color);
166
+ if (!c) throw new TypeError(`ridgescape: can't mix ${JSON.stringify(color)}; palette and haze colours must be hex, rgb(), hsl() or named`);
167
+ return c;
168
+ }
169
+ function mix(a, b, t) {
170
+ const la = toOklab(parse(a)), lb = toOklab(parse(b));
171
+ const u = clamp2(t, 0, 1);
172
+ return format(fromOklab(la.map((v, i) => v + (lb[i] - v) * u)));
173
+ }
174
+ function format({ r, g, b, a }) {
175
+ const h = (n) => Math.round(clamp2(n, 0, 255)).toString(16).padStart(2, "0");
176
+ if (a >= 1) return `#${h(r)}${h(g)}${h(b)}`;
177
+ const c = (n) => Math.round(clamp2(n, 0, 255));
178
+ return `rgba(${c(r)}, ${c(g)}, ${c(b)}, ${Number(clamp2(a, 0, 1).toFixed(3))})`;
179
+ }
180
+ function layerColor(palette, haze, depth) {
181
+ return mix(mix(palette.near, palette.far, depth), haze.color, haze.strength * depth ** haze.curve);
182
+ }
183
+
184
+ // src/random/index.ts
185
+ function hashString(str, seed = 0) {
186
+ let h1 = 3735928559 ^ seed;
187
+ let h2 = 1103547991 ^ seed;
188
+ for (let i = 0; i < str.length; i++) {
189
+ const ch = str.charCodeAt(i);
190
+ h1 = Math.imul(h1 ^ ch, 2654435761);
191
+ h2 = Math.imul(h2 ^ ch, 1597334677);
192
+ }
193
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
194
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
195
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
196
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
197
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
198
+ }
199
+ function mulberry32(seed) {
200
+ let a = seed >>> 0;
201
+ return () => {
202
+ a = a + 1831565813 >>> 0;
203
+ let t = a;
204
+ t = Math.imul(t ^ t >>> 15, t | 1);
205
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
206
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
207
+ };
208
+ }
209
+ var SEP = "\0";
210
+ function createRandom(seed) {
211
+ return fromKey(String(seed));
212
+ }
213
+ function fromKey(key) {
214
+ const gen = mulberry32(hashString(key));
215
+ const random = {
216
+ key,
217
+ next: gen,
218
+ range: (min, max) => min + gen() * (max - min),
219
+ int: (min, max) => min + Math.floor(gen() * (max - min + 1)),
220
+ fork: (...path) => fromKey(key + SEP + path.join(SEP)),
221
+ value: (...path) => random.fork(...path).next()
222
+ };
223
+ return random;
224
+ }
225
+
226
+ // src/render/paths.ts
227
+ function circlePath(cx, cy, r, fmt) {
228
+ const R = fmt(r);
229
+ return `M${fmt(cx - r)} ${fmt(cy)}A${R} ${R} 0 1 1 ${fmt(cx + r)} ${fmt(cy)}A${R} ${R} 0 1 1 ${fmt(cx - r)} ${fmt(cy)}Z`;
230
+ }
231
+ function capsulePath(x0, y0, x1, y1, fmt) {
232
+ const r = Math.min((y1 - y0) / 2, (x1 - x0) / 2);
233
+ const R = fmt(r);
234
+ return `M${fmt(x0 + r)} ${fmt(y0)}H${fmt(x1 - r)}A${R} ${R} 0 0 1 ${fmt(x1 - r)} ${fmt(y1)}H${fmt(x0 + r)}A${R} ${R} 0 0 1 ${fmt(x0 + r)} ${fmt(y0)}Z`;
235
+ }
236
+ function rectPath(x0, y0, x1, y1, fmt) {
237
+ return `M${fmt(x0)} ${fmt(y0)}H${fmt(x1)}V${fmt(y1)}H${fmt(x0)}Z`;
238
+ }
239
+ function groundPath(points, bottom, fmt) {
240
+ let d = "M" + fmt(points[0].x) + " " + fmt(points[0].y);
241
+ for (let i = 1; i < points.length; i++) d += "L" + fmt(points[i].x) + " " + fmt(points[i].y);
242
+ return d + "L" + fmt(points[points.length - 1].x) + " " + fmt(bottom) + "L" + fmt(points[0].x) + " " + fmt(bottom) + "Z";
243
+ }
244
+
245
+ // src/spec/length.ts
246
+ var PERCENT = /^\s*([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)%\s*$/i;
247
+ function isLength(value) {
248
+ return typeof value === "number" && Number.isFinite(value) || typeof value === "string" && PERCENT.test(value);
249
+ }
250
+ function resolveLength(value, total) {
251
+ if (typeof value === "number") return value;
252
+ const m = PERCENT.exec(value);
253
+ if (!m) throw new TypeError(`ridgescape: expected px or a percentage, got ${JSON.stringify(value)}`);
254
+ return Number(m[1]) / 100 * total;
255
+ }
256
+ function scaleLength(value, k) {
257
+ if (typeof value === "number") return round(value * k);
258
+ return `${round(resolveLength(value, 100) * k)}%`;
259
+ }
260
+ var round = (n) => Number(n.toFixed(4));
261
+
262
+ // src/clouds/index.ts
263
+ function cloudLayers(clouds, palette, ctx) {
264
+ const top = resolveLength(clouds.top, ctx.height);
265
+ const bottom = resolveLength(clouds.bottom, ctx.height);
266
+ const layers = [];
267
+ if (clouds.cover <= 0) return layers;
268
+ for (let i = 0; i < clouds.bands; i++) {
269
+ const t = clouds.bands > 1 ? i / (clouds.bands - 1) : 1;
270
+ const random = createRandom(clouds.seed).fork("band", i);
271
+ const size = clouds.size * (0.55 + 0.45 * t);
272
+ const base = top + (bottom - top) * t;
273
+ const period = 2400 + 300 * i;
274
+ const count2 = Math.max(1, Math.round(clouds.cover * period / size));
275
+ const slot = period / count2;
276
+ const tile = Array.from({ length: count2 }, (_, k) => ({
277
+ x: (k + 0.5 + random.range(-0.35, 0.35)) * slot,
278
+ y: base + random.range(-0.15, 0.15) * size,
279
+ width: size * random.range(0.6, 1.4),
280
+ random: random.fork("cloud", k)
281
+ }));
282
+ let d = "";
283
+ let highest = Infinity;
284
+ for (let copy = -1; copy * period < ctx.width + size * 2; copy++) {
285
+ for (const c of tile) {
286
+ const x = c.x + copy * period;
287
+ if (x + c.width < 0 || x - c.width > ctx.width) continue;
288
+ const piece = clouds.style === "streaks" ? streaks({ ...c, x, random: c.random.fork("shape") }, ctx) : puffy({ ...c, x, random: c.random.fork("shape") }, ctx);
289
+ d += piece.d;
290
+ highest = Math.min(highest, piece.top);
291
+ }
292
+ }
293
+ if (!d) continue;
294
+ const color = mix(clouds.color, palette.haze, 0.35 * (1 - t));
295
+ const content = {
296
+ y1: ctx.round(base - size * 0.45),
297
+ y2: ctx.round(base + size * 0.05),
298
+ stops: [
299
+ { offset: 0, color },
300
+ { offset: 1, color: mix(color, palette.far, clouds.shade) }
301
+ ]
302
+ };
303
+ const fill = clouds.shade > 0 ? { type: "linear", id: ctx.id(content), x1: 0, x2: 0, ...content } : color;
304
+ layers.push({
305
+ key: `clouds-${i}`,
306
+ kind: "clouds",
307
+ index: i,
308
+ depth: Number((1 - 0.15 * t).toFixed(4)),
309
+ shapes: [{ d, fill, ...clouds.opacity < 1 ? { opacity: clouds.opacity } : {} }],
310
+ top: ctx.round(highest)
311
+ });
312
+ }
313
+ return layers;
314
+ }
315
+ function puffy({ x, y, width, random }, ctx) {
316
+ const base = width * 0.14;
317
+ const x0 = x - width / 2;
318
+ let d = capsulePath(x0, y - base, x0 + width, y, ctx.fmt);
319
+ let top = y - base;
320
+ const puffs = 3 + Math.floor(random.next() * 3);
321
+ for (let j = 0; j < puffs; j++) {
322
+ const u = (j + 0.5) / puffs;
323
+ const r = width * (0.09 + 0.12 * Math.sin(Math.PI * u)) * random.range(0.85, 1.15);
324
+ const cx = x0 + width * (0.12 + 0.76 * u) + random.range(-0.04, 0.04) * width;
325
+ const cy = y - base / 2 - r * 0.75;
326
+ d += circlePath(cx, cy, r, ctx.fmt);
327
+ top = Math.min(top, cy - r);
328
+ }
329
+ return { d, top };
330
+ }
331
+ function streaks({ x, y, width, random }, ctx) {
332
+ const { fmt } = ctx;
333
+ let d = "";
334
+ let top = y;
335
+ const count2 = 1 + Math.floor(random.next() * 3);
336
+ for (let j = 0; j < count2; j++) {
337
+ const length2 = width * random.range(0.5, 1.2);
338
+ const h = width * random.range(0.018, 0.04);
339
+ const cx = x + random.range(-0.25, 0.25) * width;
340
+ const by = y - j * width * 0.07;
341
+ const peak = cx + random.range(-0.2, 0.2) * length2;
342
+ const x0 = cx - length2 / 2, x1 = cx + length2 / 2;
343
+ d += `M${fmt(x0)} ${fmt(by)}Q${fmt(peak)} ${fmt(by - 2 * h)} ${fmt(x1)} ${fmt(by)}Q${fmt(peak)} ${fmt(by + 0.5 * h)} ${fmt(x0)} ${fmt(by)}Z`;
344
+ top = Math.min(top, by - h);
345
+ }
346
+ return { d, top };
347
+ }
348
+
349
+ // src/lakes/index.ts
350
+ var CLARITY = 0.6;
351
+ var RIPPLES = 0.5;
352
+ function waterBase(spans, bottom, ctx) {
353
+ return spans.map(({ span, lake }) => ({ d: rectPath(span.x0, span.y, span.x1, bottom, ctx.fmt), fill: lake.color }));
354
+ }
355
+ function reflectionLayers(spans, behind, bottom, key, index, depth, ctx) {
356
+ const out = [];
357
+ const byLevel = /* @__PURE__ */ new Map();
358
+ for (const { span, lake } of spans) {
359
+ if (!lake.reflection) continue;
360
+ const rects = byLevel.get(span.y) ?? [];
361
+ rects.push([span.x0, span.y, span.x1, bottom]);
362
+ byLevel.set(span.y, rects);
363
+ }
364
+ for (const [y, water] of byLevel) {
365
+ const mirror = [1, 0, 0, -1, 0, ctx.round(2 * y)];
366
+ const runs = [];
367
+ for (const layer of behind) {
368
+ if (layer.mirror) continue;
369
+ const shapes2 = [];
370
+ for (const s of layer.shapes) {
371
+ if (s.clip || s.transform) continue;
372
+ s.id ?? (s.id = ctx.id({ shape: s.d, fill: s.fill, opacity: s.opacity }));
373
+ shapes2.push({ d: s.d, fill: s.fill, ...s.opacity !== void 0 ? { opacity: s.opacity } : {}, transform: mirror, href: s.id });
374
+ }
375
+ if (shapes2.length === 0) continue;
376
+ const run = runs[runs.length - 1];
377
+ if (run && run.source === layer.depth) run.shapes.push(...shapes2);
378
+ else runs.push({ source: layer.depth, shapes: shapes2 });
379
+ }
380
+ for (const { source, shapes: shapes2 } of runs) {
381
+ out.push({
382
+ key: `${key}-${out.length}`,
383
+ kind: "reflection",
384
+ index,
385
+ depth,
386
+ shapes: shapes2,
387
+ top: ctx.round(y),
388
+ mirror: { depth: source, water: water.map((r) => r.map(ctx.round)) }
389
+ });
390
+ }
391
+ }
392
+ return out;
393
+ }
394
+ function waterSurface(spans, bottom, ctx) {
395
+ const shapes2 = [];
396
+ for (const { span, lake, seed } of spans) {
397
+ const { x0, x1, y } = span;
398
+ const water = rectPath(x0, y, x1, bottom, ctx.fmt);
399
+ const depth = Math.max(40, (ctx.height - y) * 0.6);
400
+ const clear = lake.reflection ? CLARITY : 0;
401
+ const tint = {
402
+ y1: ctx.round(y),
403
+ y2: ctx.round(y + depth),
404
+ stops: [
405
+ { offset: 0, color: lake.color, opacity: ctx.round(0.5 * (1 - clear) + 0.1) },
406
+ { offset: 1, color: lake.color, opacity: ctx.round(Math.min(1, 1 - 0.45 * clear)) }
407
+ ]
408
+ };
409
+ shapes2.push({ d: water, fill: { type: "linear", id: ctx.id(tint), x1: 0, x2: 0, ...tint } });
410
+ const { light, alpha: alpha2 } = shine(lake.color);
411
+ shapes2.push({ d: capsulePath(x0, y - 0.6, x1, y + 0.9, ctx.fmt), fill: light, opacity: ctx.round(0.55 * alpha2), clip: water });
412
+ if (lake.ripples) {
413
+ const random = createRandom(seed);
414
+ const width = x1 - x0;
415
+ const count2 = Math.round(RIPPLES * width / 22);
416
+ let d = "";
417
+ for (let i = 0; i < count2; i++) {
418
+ const down = 3 + random.next() ** 1.6 * Math.min(depth, 140);
419
+ const length2 = random.range(8, 46) * (1 + down / 70);
420
+ const cx = x0 + random.next() * width;
421
+ const thick = random.range(0.9, 1.6);
422
+ d += capsulePath(cx - length2 / 2, y + down, cx + length2 / 2, y + down + thick, ctx.fmt);
423
+ }
424
+ if (d) shapes2.push({ d, fill: light, opacity: ctx.round((0.2 + 0.35 * RIPPLES) * alpha2), clip: water });
425
+ }
426
+ }
427
+ return shapes2;
428
+ }
429
+ function shine(color) {
430
+ try {
431
+ return { light: mix(color, "#ffffff", 0.55), alpha: 1 };
432
+ } catch {
433
+ return { light: "#ffffff", alpha: 0.55 };
434
+ }
435
+ }
436
+ function mirrorClip(water) {
437
+ return water.map(([x0, y0, x1, y1]) => rectPath(x0, y0, x1, y1, (n) => String(n + 0))).join("");
438
+ }
439
+
440
+ // src/profile/noise.ts
441
+ var mod = (a, n) => (a % n + n) % n;
442
+ function periodicNoise(random, cells) {
443
+ const n = Math.max(1, Math.round(cells));
444
+ const values = Array.from({ length: n }, () => random.next() * 2 - 1);
445
+ const at = (i) => values[mod(i, n)];
446
+ return (u) => {
447
+ const i = Math.floor(u);
448
+ const t = u - i;
449
+ const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
450
+ return p1 + 0.5 * t * (p2 - p0 + t * (2 * p0 - 5 * p1 + 4 * p2 - p3 + t * (3 * (p1 - p2) + p3 - p0)));
451
+ };
452
+ }
453
+ function octaves(random, { cells, count: count2, gain }, shape = (v) => v) {
454
+ const layers = Array.from({ length: count2 }, (_, k) => ({
455
+ k,
456
+ cells: Math.round(cells) * 2 ** k,
457
+ weight: gain ** k,
458
+ noise: periodicNoise(random.fork("octave", k), Math.round(cells) * 2 ** k)
459
+ }));
460
+ return (x) => {
461
+ let sum = 0;
462
+ for (const o of layers) sum += o.weight * shape(o.noise(x * o.cells), o.k);
463
+ return sum;
464
+ };
465
+ }
466
+
467
+ // src/profile/index.ts
468
+ var SAMPLE_STEP = 4;
469
+ var smoothstep = (a, b, x) => {
470
+ const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
471
+ return t * t * (3 - 2 * t);
472
+ };
473
+ function octavesFor(roughness, cells) {
474
+ return { cells, count: 2 + Math.round(roughness * 4), gain: 0.3 + 0.3 * roughness };
475
+ }
476
+ var shapes = {
477
+ rolling(seed, cells, roughness) {
478
+ return octaves(createRandom(seed), octavesFor(roughness, cells));
479
+ },
480
+ peaks(seed, cells, roughness) {
481
+ return octaves(createRandom(seed), octavesFor(roughness, cells), (v, k) => k < 2 ? (1 - Math.abs(v)) ** 2 : 0.6 * v);
482
+ },
483
+ mesa(seed, cells, roughness) {
484
+ return octaves(createRandom(seed), octavesFor(roughness * 0.5, cells));
485
+ },
486
+ dunes(seed, cells, roughness) {
487
+ const random = createRandom(seed);
488
+ const slow = Math.max(1, Math.round(cells / 2));
489
+ const warp = octaves(random.fork("warp"), { cells: slow, count: 2, gain: 0.5 });
490
+ const swell = octaves(random.fork("swell"), { cells: slow, count: 1, gain: 1 });
491
+ const detail = octaves(random.fork("detail"), { cells: cells * 4, count: 2, gain: 0.5 });
492
+ const crest = 0.78;
493
+ return (x) => {
494
+ const p = x * cells + 0.35 * warp(x);
495
+ const s = p - Math.floor(p);
496
+ const wave = s < crest ? (1 - Math.cos(Math.PI * s / crest)) / 2 : (1 + Math.cos(Math.PI * (s - crest) / (1 - crest))) / 2;
497
+ return wave * (0.65 + 0.35 * swell(x)) + 0.08 * roughness * detail(x);
498
+ };
499
+ }
500
+ };
501
+ var finish = {
502
+ // Wider valleys, so the peaks stand out.
503
+ peaks: (v) => v ** 1.3,
504
+ // Plateau curve: flat tops and floors joined by steep sides.
505
+ mesa: (v) => 0.82 * smoothstep(0.38, 0.62, v) + 0.18 * v
506
+ };
507
+ var cache = /* @__PURE__ */ new Map();
508
+ var CACHE_SIZE = 64;
509
+ function buildProfile(options) {
510
+ const key = JSON.stringify([options.profile, options.roughness, options.wavelength, options.period, options.seed]);
511
+ const cached = cache.get(key);
512
+ if (cached) return cached;
513
+ const { profile, roughness, wavelength, period, seed } = options;
514
+ const count2 = Math.max(8, Math.round(period / SAMPLE_STEP));
515
+ const cells = Math.max(1, Math.round(period / wavelength));
516
+ const shape = shapes[profile](seed, cells, roughness);
517
+ const values = new Float64Array(count2);
518
+ for (let i = 0; i < count2; i++) values[i] = shape(i / count2);
519
+ normalise(values);
520
+ const f = finish[profile];
521
+ if (f) {
522
+ for (let i = 0; i < count2; i++) values[i] = f(values[i]);
523
+ normalise(values);
524
+ }
525
+ const result = { period, step: period / count2, values };
526
+ if (cache.size >= CACHE_SIZE) cache.delete(cache.keys().next().value);
527
+ cache.set(key, result);
528
+ return result;
529
+ }
530
+ function normalise(values) {
531
+ let min = Infinity, max = -Infinity;
532
+ for (const v of values) {
533
+ if (v < min) min = v;
534
+ if (v > max) max = v;
535
+ }
536
+ const range = max - min || 1;
537
+ for (let i = 0; i < values.length; i++) values[i] = (values[i] - min) / range;
538
+ }
539
+
540
+ // src/trees/index.ts
541
+ var lean = (half, r) => {
542
+ const s = r.range(-0.1, 0.1);
543
+ return { left: half * (1 + s), right: half * (1 - s) };
544
+ };
545
+ var SHAPES = {
546
+ // A narrow triangle with jagged sides: short tiers of branches, each one's tips poking out past
547
+ // the line of the tree, more of them on bigger trees.
548
+ spruce: {
549
+ kind: "conifer",
550
+ width: 0.42,
551
+ scale: 1,
552
+ build(h, r) {
553
+ const half = h * 0.2 * r.range(0.85, 1.15);
554
+ const sides = lean(half, r);
555
+ const n = Math.max(1, Math.min(9, Math.round(h / 8)));
556
+ const step = h * 0.85 / n;
557
+ const parts = [];
558
+ for (let k = 0; k < n; k++) {
559
+ const base = step * k;
560
+ const out = (1 - base / h) * r.range(1.08, 1.25);
561
+ parts.push({ type: "cone", offset: 0, left: sides.left * out, right: sides.right * out, base, apex: Math.min(h, base + step * 2.4), bend: 1 });
562
+ }
563
+ parts.push({ type: "cone", offset: 0, left: sides.left * 0.3, right: sides.right * 0.3, base: h * 0.7, apex: h, bend: 1 });
564
+ return parts;
565
+ }
566
+ },
567
+ // One smooth, narrow spire with gently curved sides.
568
+ fir: {
569
+ kind: "conifer",
570
+ width: 0.36,
571
+ scale: 1.05,
572
+ build: (h, r) => [{ type: "cone", offset: 0, ...lean(h * 0.2 * r.range(0.85, 1.15), r), base: 0, apex: h, bend: r.range(1.05, 1.2) }]
573
+ },
574
+ // Ragged and round-shouldered: a broad body with flat-topped clumps along its top. Each clump
575
+ // stays inside the body where it stands (anything wider would drop to the ground as a column).
576
+ pine: {
577
+ kind: "conifer",
578
+ width: 0.62,
579
+ scale: 0.95,
580
+ build(h, r) {
581
+ const radius = h * 0.28 * r.range(0.85, 1.15);
582
+ const body = h * 0.8;
583
+ const parts = [{ type: "dome", offset: 0, radius, base: 0, height: body, power: 1 }];
584
+ const halfAt = (y) => y >= body ? 0 : radius * Math.sqrt(1 - y / body);
585
+ const clumps = h < 16 ? 1 : r.int(3, 5);
586
+ for (let k = 0; k < clumps; k++) {
587
+ const top = k === 0 ? h : h * r.range(0.62, 0.9);
588
+ const base = top - h * (k === 0 ? 0.3 : r.range(0.14, 0.2));
589
+ const room = halfAt(base);
590
+ const offset = k === 0 ? room * r.range(-0.25, 0.25) : room * r.range(0.2, 0.6) * (r.next() < 0.5 ? -1 : 1);
591
+ const size = (room - Math.abs(offset)) * r.range(0.8, 0.95);
592
+ if (size > 0.8) parts.push({ type: "dome", offset, radius: size, base, height: top, power: 0.3 });
593
+ }
594
+ return parts;
595
+ }
596
+ },
597
+ // A tall, narrow column with a rounded point.
598
+ cypress: {
599
+ kind: "conifer",
600
+ width: 0.26,
601
+ scale: 1.1,
602
+ build(h, r) {
603
+ const half = h * 0.13 * r.range(0.85, 1.15);
604
+ return [
605
+ { type: "dome", offset: 0, radius: half, base: 0, height: h * 0.9, power: 0.4 },
606
+ { type: "cone", offset: 0, left: half * 0.4, right: half * 0.4, base: h * 0.82, apex: h, bend: 1 }
607
+ ];
608
+ }
609
+ },
610
+ // A round crown between two lower, sloping shoulders (a crown's own sides would drop straight down).
611
+ maple: {
612
+ kind: "deciduous",
613
+ width: 0.9,
614
+ scale: 0.85,
615
+ build(h, r) {
616
+ const w = h * 0.9 * r.range(0.85, 1.15);
617
+ return [
618
+ { type: "dome", offset: 0, radius: w * 0.35, base: 0, height: h, power: 0.5 },
619
+ { type: "dome", offset: -w * r.range(0.15, 0.28), radius: w * 0.3 * r.range(0.85, 1.15), base: 0, height: h * r.range(0.65, 0.85), power: 1 },
620
+ { type: "dome", offset: w * r.range(0.15, 0.28), radius: w * 0.3 * r.range(0.85, 1.15), base: 0, height: h * r.range(0.65, 0.85), power: 1 }
621
+ ];
622
+ }
623
+ },
624
+ // Broad and lumpy: a cluster of round crowns over wide shoulders.
625
+ oak: {
626
+ kind: "deciduous",
627
+ width: 1.25,
628
+ scale: 0.8,
629
+ build(h, r) {
630
+ const w = h * 1.25 * r.range(0.85, 1.15);
631
+ const n = h < 14 ? 2 : r.int(3, 5);
632
+ const parts = [];
633
+ for (let k = 0; k < n; k++) {
634
+ const offset = n === 1 ? 0 : w * (-0.26 + 0.52 * k / (n - 1) + r.range(-0.05, 0.05));
635
+ const middle = 1 - Math.abs(offset) / (w * 0.4);
636
+ parts.push({ type: "dome", offset, radius: w * r.range(0.17, 0.24), base: 0, height: h * (0.72 + 0.28 * middle) * r.range(0.92, 1), power: 0.5 });
637
+ }
638
+ const top = Math.max(...parts.map((p) => p.type === "dome" ? p.height : 0));
639
+ for (const p of parts) if (p.type === "dome") p.height *= h / top;
640
+ parts.push(
641
+ { type: "dome", offset: -w * 0.24, radius: w * 0.27, base: 0, height: h * r.range(0.5, 0.62), power: 1 },
642
+ { type: "dome", offset: w * 0.24, radius: w * 0.27, base: 0, height: h * r.range(0.5, 0.62), power: 1 }
643
+ );
644
+ return parts;
645
+ }
646
+ },
647
+ // Tall and narrow, with a couple of bumps down its sides.
648
+ poplar: {
649
+ kind: "deciduous",
650
+ width: 0.4,
651
+ scale: 1.05,
652
+ build(h, r) {
653
+ const half = h * 0.18 * r.range(0.85, 1.15);
654
+ return [
655
+ { type: "dome", offset: 0, radius: half, base: 0, height: h, power: 0.6 },
656
+ { type: "dome", offset: -half * r.range(0.3, 0.5), radius: half * 0.75, base: 0, height: h * r.range(0.5, 0.7), power: 0.5 },
657
+ { type: "dome", offset: half * r.range(0.3, 0.5), radius: half * 0.75, base: 0, height: h * r.range(0.5, 0.7), power: 0.5 }
658
+ ];
659
+ }
660
+ }
661
+ };
662
+ var cache2 = /* @__PURE__ */ new Map();
663
+ var CACHE_SIZE2 = 64;
664
+ function placeTrees({ trees, height, period }) {
665
+ const key = JSON.stringify([trees, height, period]);
666
+ const cached = cache2.get(key);
667
+ if (cached) return cached;
668
+ const mix2 = Object.entries(trees.kind);
669
+ const mean = (f) => mix2.reduce((t, [shape, w]) => t + f(SHAPES[shape]) * w, 0);
670
+ const scale = mean((s) => s.scale);
671
+ const random = createRandom(trees.seed);
672
+ const typicalWidth = height * mean((s) => s.width * (s.scale / scale));
673
+ const gap = typicalWidth * (2.4 + (0.55 - 2.4) * trees.density);
674
+ const spots = [];
675
+ for (let x = random.next() * gap, i = 0; x < period; i++) {
676
+ spots.push(x);
677
+ x += gap * random.fork("gap", i).range(0.6, 1.4);
678
+ }
679
+ const cells = Math.max(4, Math.round(period / 400));
680
+ const patches = periodicNoise(random.fork("patches"), cells);
681
+ const patch = spots.map((x) => patches(x / period * cells));
682
+ const ranked = [...patch].sort((a, b) => a - b);
683
+ const keep = Math.round(trees.cover * spots.length);
684
+ const threshold = keep === 0 ? -Infinity : ranked[keep - 1];
685
+ const list = [];
686
+ for (let i = 0; i < spots.length; i++) {
687
+ if (patch[i] > threshold) continue;
688
+ const x = spots[i];
689
+ const r = random.fork("tree", i);
690
+ const shape = pick(mix2, r.next());
691
+ const spec = SHAPES[shape];
692
+ const h = height * (1 + (r.next() * 2 - 1) * trees.variation) * (spec.scale / scale);
693
+ const parts = spec.build(h, r.fork("shape"));
694
+ const reach = Math.max(...parts.map((p) => p.type === "cone" ? Math.max(p.left - p.offset, p.right + p.offset) : Math.abs(p.offset) + p.radius));
695
+ list.push({ x, kind: spec.kind, shape, height: h, width: reach * 2, parts });
696
+ }
697
+ if (cache2.size >= CACHE_SIZE2) cache2.delete(cache2.keys().next().value);
698
+ cache2.set(key, list);
699
+ return list;
700
+ }
701
+ function pick(mix2, u) {
702
+ for (const [shape, w] of mix2) {
703
+ if (u < w) return shape;
704
+ u -= w;
705
+ }
706
+ return mix2[mix2.length - 1][0];
707
+ }
708
+ function treeHeightAt(tree, dx) {
709
+ let best = -Infinity;
710
+ for (const p of tree.parts) {
711
+ const a = dx - p.offset;
712
+ if (p.type === "cone") {
713
+ const t = Math.abs(a) / (a < 0 ? p.left : p.right);
714
+ if (t < 1) best = Math.max(best, p.base + (p.apex - p.base) * (1 - t) ** p.bend);
715
+ } else {
716
+ const u = a / p.radius;
717
+ if (u > -1 && u < 1) best = Math.max(best, p.base + (p.height - p.base) * (1 - u * u) ** p.power);
718
+ }
719
+ }
720
+ return best;
721
+ }
722
+ function treeKeyPoints(tree) {
723
+ const points = [];
724
+ for (const p of tree.parts) {
725
+ points.push(p.offset);
726
+ if (p.type === "cone" && p.base > 0) points.push(p.offset - p.left * 0.999, p.offset + p.right * 0.999);
727
+ if (p.type === "dome" && p.base > 0) points.push(p.offset - p.radius * 0.999, p.offset + p.radius * 0.999);
728
+ }
729
+ return points;
730
+ }
731
+
732
+ // src/outline/index.ts
733
+ var mod2 = (a, n) => (a % n + n) % n;
734
+ function buildOutline(layer, size, { overscan = 0, tolerance = 0.25 } = {}) {
735
+ const profile = buildProfile(layer);
736
+ const { step, values, period } = profile;
737
+ const count2 = values.length;
738
+ const baseline = resolveLength(layer.baseline, size.height);
739
+ const amplitude = resolveLength(layer.amplitude, size.height);
740
+ const profileAt = (x) => {
741
+ const u = x / step;
742
+ const i = Math.floor(u);
743
+ const a = values[mod2(i, count2)], b = values[mod2(i + 1, count2)];
744
+ return baseline + amplitude * (a + (b - a) * (u - i));
745
+ };
746
+ const pointed = layer.profile === "peaks";
747
+ const anchors = layer.anchors.map((a) => anchorShape(a, size, period, profileAt, pointed));
748
+ const lakes = layer.lakes.map((l) => lakeShape(l, size, period, baseline));
749
+ const adjust = (x, h) => {
750
+ for (const a of anchors) h = a(x, h, baseline);
751
+ for (const l of lakes) h = l.adjust(x, h);
752
+ return h;
753
+ };
754
+ const water = waterSpans(lakes, size, period);
755
+ const shores = water.filter((w) => !lakes[w.lake].endless).flatMap((w) => [w.x0 - SHORE, w.x0 + SHORE, w.x1 - SHORE, w.x1 + SHORE]);
756
+ const bottom = size.height + Math.max(0, overscan);
757
+ const last = Math.ceil((size.width + step) / step);
758
+ if (layer.trees) {
759
+ const groundAt = (x) => adjust(x, profileAt(x));
760
+ const dry = (x) => lakes.every((l) => !l.wet(x));
761
+ const { crest: crest2, ground } = withTrees(layer, size, step, last, groundAt, dry, period, tolerance, shores);
762
+ return { crest: crest2, ground, top: topOf(crest2), baseline: size.height - baseline, bottom, water };
763
+ }
764
+ const first = -1;
765
+ const points = [];
766
+ for (let i = first; i <= last; i++) {
767
+ const x = i * step;
768
+ const h = adjust(x, baseline + amplitude * values[mod2(i, count2)]);
769
+ points.push({ x, y: size.height - h });
770
+ }
771
+ for (const x of shores) if (x > first * step && x < last * step) points.push({ x, y: size.height - adjust(x, profileAt(x)) });
772
+ points.sort((a, b) => a.x - b.x);
773
+ const crest = simplifyKeeping(points, tolerance, new Set(shores));
774
+ return { crest, top: topOf(crest), baseline: size.height - baseline, bottom, water };
775
+ }
776
+ var LAKE_BED = 3;
777
+ var BANK = 3;
778
+ var SHORE = 1e-3;
779
+ var MIN_LAND = 60;
780
+ function lakeShape(lake, size, period, baseline) {
781
+ const center2 = resolveLength(lake.at, size.width);
782
+ const width = resolveLength(lake.width, size.width);
783
+ const half = Math.min(period / 2 - 1, Math.max(1, width / 2));
784
+ const level = baseline + resolveLength(lake.level, size.height);
785
+ const bank = Math.min(Math.max(half * 0.3, 10), 80);
786
+ const endless = center2 - width / 2 <= 0 && center2 + width / 2 >= size.width || period - 2 * half < 2 * bank + MIN_LAND;
787
+ const distance = (x) => {
788
+ const d = mod2(x - center2, period);
789
+ return Math.min(d, period - d);
790
+ };
791
+ return {
792
+ center: center2,
793
+ half,
794
+ endless,
795
+ level,
796
+ adjust(x, h) {
797
+ if (endless) return level - LAKE_BED;
798
+ const d = distance(x);
799
+ if (d <= half) return level - LAKE_BED;
800
+ if (d >= half + bank) return h;
801
+ const t = (d - half) / bank;
802
+ const ease = t * t * (3 - 2 * t);
803
+ return level + BANK + (h - level - BANK) * ease;
804
+ },
805
+ wet: (x) => endless || distance(x) < half + bank * 0.5
806
+ };
807
+ }
808
+ function waterSpans(lakes, size, period) {
809
+ const spans = [];
810
+ lakes.forEach((l, lake) => {
811
+ if (l.endless) {
812
+ spans.push({ lake, copy: 0, x0: -1, x1: size.width + 1, y: size.height - l.level });
813
+ return;
814
+ }
815
+ const start = Math.floor((-l.half - l.center) / period);
816
+ for (let copy = start; l.center + copy * period - l.half < size.width; copy++) {
817
+ const x0 = l.center + copy * period - l.half;
818
+ const x1 = l.center + copy * period + l.half;
819
+ if (x1 < 0) continue;
820
+ spans.push({ lake, copy, x0, x1, y: size.height - l.level });
821
+ }
822
+ });
823
+ return spans.sort((a, b) => a.x0 - b.x0);
824
+ }
825
+ function topOf(points) {
826
+ let top = Infinity;
827
+ for (const p of points) if (p.y < top) top = p.y;
828
+ return top;
829
+ }
830
+ var TREE_SUBSTEPS = 4;
831
+ function withTrees(layer, size, step, last, groundAt, dry, period, tolerance, shores) {
832
+ const trees = placeTrees({ trees: layer.trees, height: resolveLength(layer.trees.height, size.height), period });
833
+ const x0 = -step, x1 = last * step;
834
+ const placed = [];
835
+ const widest = trees.reduce((m, t) => Math.max(m, t.width), 0);
836
+ for (let copy = Math.floor((x0 - widest) / period); copy * period < x1 + widest; copy++) {
837
+ for (const tree of trees) {
838
+ const x = tree.x + copy * period;
839
+ const half = tree.width / 2;
840
+ if (x + half < x0 || x - half > x1) continue;
841
+ if (!dry(x - half) || !dry(x) || !dry(x + half)) continue;
842
+ if (Math.abs(groundAt(x + half) - groundAt(x - half)) > tree.width) continue;
843
+ placed.push({ tree, x, foot: groundAt(x) - 0.08 * tree.height });
844
+ }
845
+ }
846
+ const xs = [];
847
+ for (let i = -TREE_SUBSTEPS; i <= last * TREE_SUBSTEPS; i++) xs.push(i / TREE_SUBSTEPS * step);
848
+ for (const p of placed) for (const dx of treeKeyPoints(p.tree)) if (p.x + dx > x0 && p.x + dx < x1) xs.push(p.x + dx);
849
+ for (const x of shores) if (x > x0 && x < x1) xs.push(x);
850
+ xs.sort((a, b) => a - b);
851
+ const ground = xs.map(groundAt);
852
+ const heights = ground.slice();
853
+ for (const p of placed) {
854
+ const half = p.tree.width / 2;
855
+ for (let j = lowerBound(xs, p.x - half); j < xs.length && xs[j] <= p.x + half; j++) {
856
+ const h = p.foot + treeHeightAt(p.tree, xs[j] - p.x);
857
+ if (h > heights[j]) heights[j] = h;
858
+ }
859
+ }
860
+ const fixed = new Set(shores);
861
+ const toPoints = (hs) => simplifyKeeping(xs.map((x, j) => ({ x, y: size.height - hs[j] })), tolerance, fixed);
862
+ return { crest: toPoints(heights), ground: toPoints(ground) };
863
+ }
864
+ function lowerBound(xs, x) {
865
+ let lo = 0, hi = xs.length;
866
+ while (lo < hi) {
867
+ const mid = lo + hi >> 1;
868
+ if (xs[mid] < x) lo = mid + 1;
869
+ else hi = mid;
870
+ }
871
+ return lo;
872
+ }
873
+ function anchorShape(anchor, size, period, profileAt, pointed) {
874
+ const at = resolveLength(anchor.at, size.width);
875
+ const radius = Math.min(period / 2, Math.max(1, resolveLength(anchor.width, size.width) / 2));
876
+ const height = resolveLength(anchor.height, size.height);
877
+ const flat = anchor.kind === "flat";
878
+ const inner = flat ? radius / 2 : 0;
879
+ const lift = pointed && anchor.kind === "peak";
880
+ const center2 = profileAt(at);
881
+ return (x, h, baseline) => {
882
+ let d = mod2(x - at, period);
883
+ d = Math.min(d, period - d);
884
+ if (d >= radius) return h;
885
+ const t = d <= inner ? 0 : (d - inner) / (radius - inner);
886
+ const w = lift ? (1 - t) ** 2 * (1 + 2 * t) * (1 - t) ** 0.6 : 0.5 * (1 + Math.cos(Math.PI * t));
887
+ return flat ? h + w * (baseline + height - h) : h + w * (baseline + height - center2);
888
+ };
889
+ }
890
+ function simplifyKeeping(points, tolerance, xs) {
891
+ if (xs.size === 0 || points.length < 3) return simplify(points, tolerance);
892
+ const out = [];
893
+ let start = 0;
894
+ for (let i = 1; i < points.length; i++) {
895
+ if (i < points.length - 1 && !xs.has(points[i].x)) continue;
896
+ const piece = simplify(points.slice(start, i + 1), tolerance);
897
+ out.push(...out.length > 0 ? piece.slice(1) : piece);
898
+ start = i;
899
+ }
900
+ return out;
901
+ }
902
+ function simplify(points, tolerance) {
903
+ if (points.length < 3 || tolerance <= 0) return points;
904
+ const keep = new Uint8Array(points.length);
905
+ keep[0] = keep[points.length - 1] = 1;
906
+ const stack = [[0, points.length - 1]];
907
+ while (stack.length > 0) {
908
+ const [a, b] = stack.pop();
909
+ const p = points[a], q = points[b];
910
+ const dx = q.x - p.x, dy = q.y - p.y;
911
+ const len = Math.hypot(dx, dy) || 1;
912
+ let worst = -1, index = -1;
913
+ for (let i = a + 1; i < b; i++) {
914
+ const r = points[i];
915
+ const dist = Math.abs(dy * (r.x - p.x) - dx * (r.y - p.y)) / len;
916
+ if (dist > worst) {
917
+ worst = dist;
918
+ index = i;
919
+ }
920
+ }
921
+ if (worst > tolerance) {
922
+ keep[index] = 1;
923
+ stack.push([a, index], [index, b]);
924
+ }
925
+ }
926
+ return points.filter((_, i) => keep[i]);
927
+ }
928
+
929
+ // src/sky/index.ts
930
+ var STAR_TILE = 1024;
931
+ function skyLayers(sky, ctx) {
932
+ const layers = [];
933
+ if (sky.gradient) {
934
+ const [top, bottom] = sky.gradient;
935
+ const content = { y1: 0, y2: ctx.height, stops: [{ offset: 0, color: top }, { offset: 1, color: bottom }] };
936
+ layers.push({
937
+ key: "sky",
938
+ kind: "sky",
939
+ index: 0,
940
+ depth: 0,
941
+ shapes: [{ d: rectPath(0, 0, ctx.width, ctx.height + ctx.overscan, ctx.fmt), fill: { type: "linear", id: ctx.id(content), x1: 0, x2: 0, ...content } }],
942
+ top: 0
943
+ });
944
+ }
945
+ if (sky.stars) layers.push(starsLayer(sky.stars, ctx));
946
+ if (sky.sun) layers.push(discLayer("sun", sky.sun, ctx, discPath(sky.sun, ctx)));
947
+ if (sky.moon) layers.push(discLayer("moon", sky.moon, ctx, moonPath(sky.moon, ctx)));
948
+ return layers;
949
+ }
950
+ function starsLayer(stars, ctx) {
951
+ const area = resolveLength(stars.area, ctx.height);
952
+ const random = createRandom(stars.seed);
953
+ const count2 = Math.round(stars.density * STAR_TILE * STAR_TILE / 1e4);
954
+ const tile = Array.from({ length: count2 }, () => ({
955
+ u: random.next() * STAR_TILE,
956
+ v: random.next() * STAR_TILE,
957
+ r: (0.5 + random.next() * 1.1) * stars.size,
958
+ bucket: Math.floor(random.next() * 3),
959
+ keep: random.next()
960
+ }));
961
+ const paths = ["", "", ""];
962
+ for (let ty = 0; ty * STAR_TILE < area; ty++) {
963
+ for (let tx = 0; tx * STAR_TILE < ctx.width; tx++) {
964
+ for (const s of tile) {
965
+ const x = tx * STAR_TILE + s.u;
966
+ const y = ty * STAR_TILE + s.v;
967
+ if (x > ctx.width + 2 || y > area || s.keep < (y / area) ** 2) continue;
968
+ paths[s.bucket] += circlePath(x, y, s.r, ctx.fmt);
969
+ }
970
+ }
971
+ }
972
+ const opacities = [0.45, 0.7, 1];
973
+ const shapes2 = paths.flatMap((d, i) => d ? [{ d, fill: stars.color, opacity: opacities[i] }] : []);
974
+ return { key: "stars", kind: "stars", index: 0, depth: 1, shapes: shapes2, top: 0 };
975
+ }
976
+ function center(disc, ctx) {
977
+ return { x: resolveLength(disc.at[0], ctx.width), y: resolveLength(disc.at[1], ctx.height) };
978
+ }
979
+ function discPath(sun, ctx) {
980
+ const c = center(sun, ctx);
981
+ return circlePath(c.x, c.y, sun.radius, ctx.fmt);
982
+ }
983
+ function moonPath(moon, ctx) {
984
+ const { x, y } = center(moon, ctx);
985
+ const r = moon.radius;
986
+ const offset = r * (2 - 1.7 * moon.crescent);
987
+ if (offset >= 2 * r) return circlePath(x, y, r, ctx.fmt);
988
+ const ix = x + offset / 2;
989
+ const h = Math.sqrt(r * r - (offset / 2) ** 2);
990
+ const { fmt } = ctx;
991
+ const R = fmt(r);
992
+ return `M${fmt(ix)} ${fmt(y - h)}A${R} ${R} 0 1 0 ${fmt(ix)} ${fmt(y + h)}A${R} ${R} 0 0 1 ${fmt(ix)} ${fmt(y - h)}Z`;
993
+ }
994
+ function discLayer(kind, disc, ctx, d) {
995
+ const c = center(disc, ctx);
996
+ const shapes2 = [];
997
+ if (disc.glow > 0) {
998
+ const R = disc.radius * (1 + 3.5 * disc.glow);
999
+ const content = {
1000
+ cx: ctx.round(c.x),
1001
+ cy: ctx.round(c.y),
1002
+ r: ctx.round(R),
1003
+ stops: [
1004
+ { offset: 0, color: disc.color, opacity: 0.6 * disc.glow },
1005
+ { offset: ctx.round(disc.radius / R), color: disc.color, opacity: 0.45 * disc.glow },
1006
+ { offset: 1, color: disc.color, opacity: 0 }
1007
+ ]
1008
+ };
1009
+ shapes2.push({ d: circlePath(c.x, c.y, R, ctx.fmt), fill: { type: "radial", id: ctx.id(content), ...content } });
1010
+ }
1011
+ shapes2.push({ d, fill: disc.color });
1012
+ return { key: kind, kind, index: 0, depth: 1, shapes: shapes2, top: ctx.round(c.y - disc.radius * (1 + 3.5 * disc.glow)) };
1013
+ }
1014
+
1015
+ // src/palettes/index.ts
1016
+ var palettes = {
1017
+ dusk: { near: "#2b1e3f", far: "#8a5a83", haze: "#f3b38a", sky: ["#3d3466", "#f6c49a"], sun: "#ffd9a8", cloud: "#f2c4b8" },
1018
+ dawn: { near: "#3c3a5e", far: "#c49bb0", haze: "#ffd9c7", sky: ["#a9b8de", "#ffe2cf"], sun: "#fff1d6", cloud: "#ffe6e0" },
1019
+ alpine: { near: "#1f3446", far: "#6f8ea6", haze: "#e4eef5", sky: ["#8fb6d8", "#eaf3f9"], sun: "#fffbe8", cloud: "#ffffff" },
1020
+ forest: { near: "#173a2b", far: "#5f8a6a", haze: "#dfe9d8", sky: ["#bfd8d2", "#eef3e4"], mist: 0.25, sun: "#fff6d8", cloud: "#f7faf4" },
1021
+ desert: { near: "#7a3b24", far: "#d29a6a", haze: "#f6dcb8", sky: ["#f2a65e", "#fbe3c0"], sun: "#fff0c2", cloud: "#fde9cf" },
1022
+ fog: { near: "#3a4148", far: "#9aa3aa", haze: "#e9ecee", sky: ["#d5dadd", "#eff1f2"], mist: 0.6, sun: "#f8f8f6", cloud: "#f4f5f6" },
1023
+ night: { near: "#0b1020", far: "#2b3a5e", haze: "#4a5a86", sky: ["#05070f", "#24325a"], sun: "#f1ecd9", cloud: "#36446b" },
1024
+ mono: { near: "#262626", far: "#a3a3a3", haze: "#e5e5e5", sky: ["#f5f5f5", "#e5e5e5"], sun: "#ffffff", cloud: "#fafafa" }
1025
+ };
1026
+
1027
+ // src/spec/resolve.ts
1028
+ var SCENE_DEFAULTS = {
1029
+ seed: "ridgescape",
1030
+ palette: "dusk",
1031
+ layers: 4,
1032
+ height: "45%",
1033
+ haze: { strength: 0.5, curve: 1.2 }
1034
+ };
1035
+ var PROFILES = ["rolling", "peaks", "mesa", "dunes"];
1036
+ var ANCHOR_KINDS = ["peak", "dip", "flat"];
1037
+ var PROFILE_DEFAULTS = {
1038
+ rolling: { roughness: 0.35, wavelength: 1 },
1039
+ peaks: { roughness: 0.75, wavelength: 0.8 },
1040
+ mesa: { roughness: 0.3, wavelength: 1.3 },
1041
+ dunes: { roughness: 0.25, wavelength: 0.55 }
1042
+ };
1043
+ var DEPTH_DEFAULTS = {
1044
+ /** Valleys: low at the front, high at the back. */
1045
+ baseline: (d) => 0.04 + 0.48 * d,
1046
+ /** Hills: gentle at the front, tall at the back. */
1047
+ amplitude: (d) => 0.28 + 0.3 * d,
1048
+ /** Hill width in px: broad at the front. */
1049
+ wavelength: (d) => 900 - 380 * d,
1050
+ /** Repeat width in px, different per layer so repeats don't line up. */
1051
+ period: (d) => 1800 + 700 * d,
1052
+ /** Far layers are smoother. */
1053
+ roughness: (d) => 1 - 0.3 * d
1054
+ };
1055
+ var SKY_DEFAULTS = {
1056
+ sun: { at: ["72%", "28%"], radius: 56, glow: 0.6 },
1057
+ moon: { at: ["24%", "22%"], radius: 36, glow: 0.35, crescent: 0 },
1058
+ stars: { density: 1, size: 1, area: "70%", color: "#ffffff" }
1059
+ };
1060
+ var CLOUD_DEFAULTS = {
1061
+ style: "puffy",
1062
+ bands: 2,
1063
+ cover: 0.4,
1064
+ top: "14%",
1065
+ bottom: "40%",
1066
+ size: 240,
1067
+ shade: 0.2,
1068
+ opacity: 0.92
1069
+ };
1070
+ var TREE_DEFAULTS = {
1071
+ kind: "conifer",
1072
+ density: 0.6,
1073
+ cover: 1,
1074
+ variation: 0.3,
1075
+ /** Height in px by depth: 44 at the front, 12 at the horizon. */
1076
+ height: (d) => Math.round(44 * (1 - 0.72 * d))
1077
+ };
1078
+ var TREE_GROUPS = {
1079
+ conifer: { spruce: 0.4, fir: 0.32, pine: 0.16, cypress: 0.12 },
1080
+ deciduous: { maple: 0.4, oak: 0.38, poplar: 0.22 },
1081
+ mixed: { spruce: 0.2, fir: 0.16, pine: 0.08, cypress: 0.06, maple: 0.2, oak: 0.19, poplar: 0.11 }
1082
+ };
1083
+ var TREE_SHAPES = ["spruce", "fir", "pine", "cypress", "maple", "oak", "poplar"];
1084
+ var LAKE_DEFAULTS = { width: "30%", level: 0.12, reflection: true, ripples: true };
1085
+ var ANCHOR_HEIGHTS = { peak: 1.15, dip: 0, flat: 0.2 };
1086
+ function resolve(input = {}) {
1087
+ const seed = String(input.seed ?? SCENE_DEFAULTS.seed);
1088
+ const palette = resolvePalette(input.palette ?? SCENE_DEFAULTS.palette);
1089
+ const height = input.height ?? SCENE_DEFAULTS.height;
1090
+ if (!isLength(height)) throw new TypeError(`ridgescape: height must be px or a percentage, got ${JSON.stringify(height)}`);
1091
+ const haze = {
1092
+ color: input.haze?.color ?? palette.haze,
1093
+ strength: unit(input.haze?.strength ?? SCENE_DEFAULTS.haze.strength, "haze.strength"),
1094
+ curve: positive(input.haze?.curve ?? SCENE_DEFAULTS.haze.curve, "haze.curve")
1095
+ };
1096
+ const raw = input.layers ?? SCENE_DEFAULTS.layers;
1097
+ const list = typeof raw === "number" ? Array.from({ length: count(raw) }, () => ({})) : raw;
1098
+ const n = list.length;
1099
+ const layers = list.map((layer, i) => resolveLayer(layer, { index: i, depth: n > 1 ? (n - 1 - i) / (n - 1) : 0, seed, palette, haze, height }));
1100
+ const sky = input.sky ? resolveSky(input.sky === true ? {} : input.sky, palette, seed) : null;
1101
+ const clouds = input.clouds ? resolveClouds(input.clouds === true ? {} : input.clouds, palette, seed) : null;
1102
+ return { seed, palette, height, haze, sky, clouds, layers };
1103
+ }
1104
+ function resolveLayer(input, ctx) {
1105
+ const profile = input.profile ?? "rolling";
1106
+ if (!PROFILES.includes(profile)) throw new TypeError(`ridgescape: unknown profile ${JSON.stringify(profile)}; expected ${PROFILES.join(", ")}`);
1107
+ const depth = unit(input.depth ?? ctx.depth, "depth");
1108
+ const p = PROFILE_DEFAULTS[profile];
1109
+ const amplitude = length(input.amplitude ?? scaleLength(ctx.height, DEPTH_DEFAULTS.amplitude(depth)), "amplitude");
1110
+ const seed = String(input.seed ?? `${ctx.seed}:${ctx.index}`);
1111
+ const color = input.color ?? layerColor(ctx.palette, ctx.haze, depth);
1112
+ return {
1113
+ profile,
1114
+ depth,
1115
+ baseline: length(input.baseline ?? scaleLength(ctx.height, DEPTH_DEFAULTS.baseline(depth)), "baseline"),
1116
+ amplitude,
1117
+ roughness: unit(input.roughness ?? round2(p.roughness * DEPTH_DEFAULTS.roughness(depth)), "roughness"),
1118
+ wavelength: positive(input.wavelength ?? Math.round(DEPTH_DEFAULTS.wavelength(depth) * p.wavelength), "wavelength"),
1119
+ period: positive(input.period ?? Math.round(DEPTH_DEFAULTS.period(depth)), "period"),
1120
+ color,
1121
+ mist: unit(input.mist ?? ctx.palette.mist, "mist"),
1122
+ anchors: (input.anchors ?? []).map((a) => resolveAnchor(a, amplitude)),
1123
+ trees: input.trees ? resolveTrees(input.trees === true ? {} : input.trees, depth, seed) : null,
1124
+ lakes: (input.lakes ?? []).map((l) => resolveLake(l, amplitude, color, ctx.palette)),
1125
+ seed
1126
+ };
1127
+ }
1128
+ function flag(value, name) {
1129
+ if (typeof value !== "boolean") throw new TypeError(`ridgescape: ${name} must be true or false, got ${JSON.stringify(value)}`);
1130
+ return value;
1131
+ }
1132
+ function resolveLake(input, amplitude, layerColor2, palette) {
1133
+ return {
1134
+ at: length(input.at, "lake at"),
1135
+ width: length(input.width ?? LAKE_DEFAULTS.width, "lake width"),
1136
+ level: length(input.level ?? scaleLength(amplitude, LAKE_DEFAULTS.level), "lake level"),
1137
+ color: input.color ?? waterColor(palette.sky[1], layerColor2),
1138
+ reflection: flag(input.reflection ?? LAKE_DEFAULTS.reflection, "lake reflection"),
1139
+ ripples: flag(input.ripples ?? LAKE_DEFAULTS.ripples, "lake ripples")
1140
+ };
1141
+ }
1142
+ function waterColor(sky, land) {
1143
+ try {
1144
+ return mix(sky, land, 0.35);
1145
+ } catch {
1146
+ return land;
1147
+ }
1148
+ }
1149
+ function resolveTrees(input, depth, layerSeed) {
1150
+ return {
1151
+ kind: resolveTreeKind(input.kind ?? TREE_DEFAULTS.kind),
1152
+ density: unit(input.density ?? TREE_DEFAULTS.density, "trees.density"),
1153
+ height: positiveLength(input.height ?? TREE_DEFAULTS.height(depth), "trees.height"),
1154
+ cover: unit(input.cover ?? TREE_DEFAULTS.cover, "trees.cover"),
1155
+ variation: unit(input.variation ?? TREE_DEFAULTS.variation, "trees.variation"),
1156
+ color: input.color ?? null,
1157
+ seed: String(input.seed ?? `${layerSeed}:trees`)
1158
+ };
1159
+ }
1160
+ function resolveTreeKind(input) {
1161
+ const fail = (kind) => {
1162
+ throw new TypeError(`ridgescape: unknown tree kind ${JSON.stringify(kind)}; expected one of ${[...Object.keys(TREE_GROUPS), ...TREE_SHAPES].join(", ")}`);
1163
+ };
1164
+ const weights = {};
1165
+ const add = (name, weight) => {
1166
+ if (name in TREE_GROUPS) {
1167
+ for (const [shape, w] of Object.entries(TREE_GROUPS[name])) weights[shape] = (weights[shape] ?? 0) + w * weight;
1168
+ } else if (TREE_SHAPES.includes(name)) {
1169
+ weights[name] = (weights[name] ?? 0) + weight;
1170
+ } else fail(name);
1171
+ };
1172
+ if (typeof input === "string") add(input, 1);
1173
+ else if (Array.isArray(input)) for (const name of input) add(name, 1);
1174
+ else if (input && typeof input === "object") {
1175
+ for (const [name, w] of Object.entries(input)) {
1176
+ if (!TREE_SHAPES.includes(name)) fail(name);
1177
+ if (typeof w !== "number" || !(w >= 0) || !Number.isFinite(w)) throw new TypeError(`ridgescape: trees.kind.${name} must be a number \u2265 0`);
1178
+ add(name, w);
1179
+ }
1180
+ } else fail(input);
1181
+ const total = Object.values(weights).reduce((t, w) => t + w, 0);
1182
+ if (!(total > 0)) fail(input);
1183
+ const out = {};
1184
+ for (const shape of TREE_SHAPES) if (weights[shape]) out[shape] = Number((weights[shape] / total).toFixed(6));
1185
+ return out;
1186
+ }
1187
+ function resolveAnchor(input, amplitude) {
1188
+ const kind = input.kind ?? "peak";
1189
+ if (!ANCHOR_KINDS.includes(kind)) throw new TypeError(`ridgescape: unknown anchor kind ${JSON.stringify(kind)}; expected ${ANCHOR_KINDS.join(", ")}`);
1190
+ return {
1191
+ at: length(input.at, "anchor at"),
1192
+ kind,
1193
+ height: length(input.height ?? scaleLength(amplitude, ANCHOR_HEIGHTS[kind]), "anchor height"),
1194
+ width: length(input.width ?? "20%", "anchor width")
1195
+ };
1196
+ }
1197
+ function resolveSky(input, palette, seed) {
1198
+ const gradient = input.gradient === false || input.gradient === null ? null : input.gradient ?? palette.sky;
1199
+ const disc = (value, defaults, name) => {
1200
+ if (!value) return null;
1201
+ const v = value === true ? {} : value;
1202
+ const at = v.at ?? defaults.at;
1203
+ if (!Array.isArray(at) || at.length !== 2 || !isLength(at[0]) || !isLength(at[1])) throw new TypeError(`ridgescape: ${name}.at must be [x, y] in px or percentages, got ${JSON.stringify(at)}`);
1204
+ return { at: [at[0], at[1]], radius: positive(v.radius ?? defaults.radius, `${name}.radius`), color: v.color ?? palette.sun, glow: unit(v.glow ?? defaults.glow, `${name}.glow`) };
1205
+ };
1206
+ const sun = disc(input.sun, SKY_DEFAULTS.sun, "sun");
1207
+ const moonDisc = disc(input.moon, SKY_DEFAULTS.moon, "moon");
1208
+ const moonInput = typeof input.moon === "object" && input.moon ? input.moon : {};
1209
+ const moon = moonDisc && { ...moonDisc, crescent: unit(moonInput.crescent ?? SKY_DEFAULTS.moon.crescent, "moon.crescent") };
1210
+ let stars = null;
1211
+ if (input.stars) {
1212
+ const s = input.stars === true ? {} : input.stars;
1213
+ stars = {
1214
+ density: positive(s.density ?? SKY_DEFAULTS.stars.density, "stars.density"),
1215
+ size: positive(s.size ?? SKY_DEFAULTS.stars.size, "stars.size"),
1216
+ area: length(s.area ?? SKY_DEFAULTS.stars.area, "stars.area"),
1217
+ color: s.color ?? SKY_DEFAULTS.stars.color,
1218
+ seed: String(s.seed ?? `${seed}:stars`)
1219
+ };
1220
+ }
1221
+ return { gradient: gradient ? [gradient[0], gradient[1]] : null, sun, moon, stars };
1222
+ }
1223
+ var CLOUD_STYLES = ["puffy", "streaks"];
1224
+ function resolveClouds(input, palette, seed) {
1225
+ const style = input.style ?? CLOUD_DEFAULTS.style;
1226
+ if (!CLOUD_STYLES.includes(style)) throw new TypeError(`ridgescape: unknown cloud style ${JSON.stringify(style)}; expected ${CLOUD_STYLES.join(", ")}`);
1227
+ const bands = input.bands ?? CLOUD_DEFAULTS.bands;
1228
+ if (!Number.isInteger(bands) || bands < 1 || bands > 8) throw new RangeError(`ridgescape: clouds.bands must be a whole number from 1 to 8, got ${bands}`);
1229
+ return {
1230
+ style,
1231
+ bands,
1232
+ cover: unit(input.cover ?? CLOUD_DEFAULTS.cover, "clouds.cover"),
1233
+ top: length(input.top ?? CLOUD_DEFAULTS.top, "clouds.top"),
1234
+ bottom: length(input.bottom ?? CLOUD_DEFAULTS.bottom, "clouds.bottom"),
1235
+ size: positive(input.size ?? CLOUD_DEFAULTS.size, "clouds.size"),
1236
+ color: input.color ?? palette.cloud,
1237
+ shade: unit(input.shade ?? CLOUD_DEFAULTS.shade, "clouds.shade"),
1238
+ opacity: unit(input.opacity ?? CLOUD_DEFAULTS.opacity, "clouds.opacity"),
1239
+ seed: String(input.seed ?? `${seed}:clouds`)
1240
+ };
1241
+ }
1242
+ function resolvePalette(input) {
1243
+ const p = typeof input === "string" ? palettes[input] : input;
1244
+ if (!p) throw new TypeError(`ridgescape: unknown palette ${JSON.stringify(input)}; expected ${Object.keys(palettes).join(", ")} or your own colours`);
1245
+ const haze = p.haze ?? p.far;
1246
+ return { near: p.near, far: p.far, haze, sky: p.sky ?? [haze, haze], mist: p.mist ?? 0, sun: p.sun ?? lighten(haze, 0.5), cloud: p.cloud ?? lighten(haze, 0.6) };
1247
+ }
1248
+ function lighten(color, t) {
1249
+ try {
1250
+ return mix(color, "#ffffff", t);
1251
+ } catch {
1252
+ return "#ffffff";
1253
+ }
1254
+ }
1255
+ function count(n) {
1256
+ if (!Number.isInteger(n) || n < 1 || n > 32) throw new RangeError(`ridgescape: layers must be a whole number from 1 to 32, got ${n}`);
1257
+ return n;
1258
+ }
1259
+ function unit(n, name) {
1260
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 0 || n > 1) throw new RangeError(`ridgescape: ${name} must be from 0 to 1, got ${n}`);
1261
+ return n;
1262
+ }
1263
+ function positive(n, name) {
1264
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) throw new RangeError(`ridgescape: ${name} must be greater than 0, got ${n}`);
1265
+ return n;
1266
+ }
1267
+ function length(value, name) {
1268
+ if (!isLength(value)) throw new TypeError(`ridgescape: ${name} must be px or a percentage, got ${JSON.stringify(value)}`);
1269
+ return value;
1270
+ }
1271
+ function positiveLength(value, name) {
1272
+ const l = length(value, name);
1273
+ if (!(resolveLength(l, 100) > 0)) throw new RangeError(`ridgescape: ${name} must be greater than 0, got ${JSON.stringify(value)}`);
1274
+ return l;
1275
+ }
1276
+ var round2 = (n) => Number(n.toFixed(4));
1277
+
1278
+ // src/render/index.ts
1279
+ function render(scene, { width, height, overscan = 0, idPrefix = "ridgescape", precision = 2 }) {
1280
+ if (!(width > 0) || !(height > 0)) throw new RangeError(`ridgescape: width and height must be greater than 0, got ${width} \xD7 ${height}`);
1281
+ const resolved = resolve(scene);
1282
+ const fmt = (n) => formatNumber(n, precision);
1283
+ const ctx = {
1284
+ width,
1285
+ height,
1286
+ overscan: Math.max(0, overscan),
1287
+ fmt,
1288
+ round: (n) => Number(fmt(n)),
1289
+ // Ids from the content, so two scenes on one page never pick up each other's gradients.
1290
+ id: (content) => `${idPrefix}-${hashString(JSON.stringify(content)).toString(36)}`
1291
+ };
1292
+ const layers = [];
1293
+ if (resolved.sky) layers.push(...skyLayers(resolved.sky, ctx));
1294
+ if (resolved.clouds) layers.push(...cloudLayers(resolved.clouds, resolved.palette, ctx));
1295
+ resolved.layers.forEach((layer, index) => {
1296
+ const outline = buildOutline(layer, { width, height }, { overscan: ctx.overscan });
1297
+ let fill = layer.color;
1298
+ if (layer.mist > 0) {
1299
+ const content = {
1300
+ y1: ctx.round(outline.top),
1301
+ y2: ctx.round(Math.max(outline.baseline, outline.top + 1)),
1302
+ stops: [
1303
+ { offset: 0, color: layer.color },
1304
+ { offset: 1, color: mix(layer.color, resolved.haze.color, layer.mist) }
1305
+ ]
1306
+ };
1307
+ fill = { type: "linear", id: ctx.id(content), x1: 0, x2: 0, ...content };
1308
+ }
1309
+ const shapes2 = layer.trees?.color && outline.ground ? [{ d: groundPath(outline.crest, outline.bottom, fmt), fill: layer.trees.color }, { d: groundPath(outline.ground, outline.bottom, fmt), fill }] : [{ d: groundPath(outline.crest, outline.bottom, fmt), fill }];
1310
+ const spans = outline.water.map((span) => ({ span, lake: layer.lakes[span.lake], seed: `${layer.seed}:lake:${span.lake}` }));
1311
+ shapes2.push(...waterBase(spans, outline.bottom, ctx));
1312
+ const reflections = reflectionLayers(spans, layers, outline.bottom, `reflection-${index}`, index, layer.depth, ctx);
1313
+ layers.push({ key: `ground-${index}`, kind: "ground", index, depth: layer.depth, shapes: shapes2, top: ctx.round(outline.top) });
1314
+ if (spans.length > 0) {
1315
+ layers.push(...reflections);
1316
+ const surface = waterSurface(spans, outline.bottom, ctx);
1317
+ layers.push({ key: `water-${index}`, kind: "water", index, depth: layer.depth, shapes: surface, top: ctx.round(Math.min(...spans.map((s) => s.span.y))) });
1318
+ }
1319
+ });
1320
+ return { width, height, overscan: ctx.overscan, layers };
1321
+ }
1322
+ function formatNumber(n, precision = 2) {
1323
+ const s = String(Number(n.toFixed(precision)));
1324
+ return s === "-0" ? "0" : s;
1325
+ }
1326
+ var escape = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1327
+ function stopsMarkup(fill) {
1328
+ return fill.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escape(s.color)}"${s.opacity !== void 0 && s.opacity !== 1 ? ` stop-opacity="${s.opacity}"` : ""}/>`).join("");
1329
+ }
1330
+ function gradientMarkup(fill) {
1331
+ if (fill.type === "radial") {
1332
+ return `<radialGradient id="${escape(fill.id)}" gradientUnits="userSpaceOnUse" cx="${fill.cx}" cy="${fill.cy}" r="${fill.r}">${stopsMarkup(fill)}</radialGradient>`;
1333
+ }
1334
+ return `<linearGradient id="${escape(fill.id)}" gradientUnits="userSpaceOnUse" x1="${fill.x1}" y1="${fill.y1}" x2="${fill.x2}" y2="${fill.y2}">${stopsMarkup(fill)}</linearGradient>`;
1335
+ }
1336
+ function clipId(shape, idPrefix) {
1337
+ return `${idPrefix}-clip-${hashString(shape.clip).toString(36)}`;
1338
+ }
1339
+ function mirrorClipId(layer, idPrefix) {
1340
+ return `${idPrefix}-clip-${hashString(mirrorClip(layer.mirror.water)).toString(36)}`;
1341
+ }
1342
+ function defsMarkup(layers, idPrefix = "ridgescape", { clipMirrors = true } = {}) {
1343
+ const defs = /* @__PURE__ */ new Map();
1344
+ for (const layer of layers) {
1345
+ if (layer.mirror && clipMirrors) {
1346
+ const id = mirrorClipId(layer, idPrefix);
1347
+ if (!defs.has(id)) defs.set(id, `<clipPath id="${id}"><path d="${mirrorClip(layer.mirror.water)}"/></clipPath>`);
1348
+ }
1349
+ for (const shape of layer.shapes) {
1350
+ if (typeof shape.fill !== "string" && !defs.has(shape.fill.id)) defs.set(shape.fill.id, gradientMarkup(shape.fill));
1351
+ if (shape.clip) {
1352
+ const id = clipId(shape, idPrefix);
1353
+ if (!defs.has(id)) defs.set(id, `<clipPath id="${id}"><path d="${shape.clip}"/></clipPath>`);
1354
+ }
1355
+ }
1356
+ }
1357
+ return defs.size > 0 ? `<defs>${[...defs.values()].join("")}</defs>` : "";
1358
+ }
1359
+ function shapeMarkup(shape, idPrefix = "ridgescape") {
1360
+ const fill = typeof shape.fill === "string" ? escape(shape.fill) : `url(#${escape(shape.fill.id)})`;
1361
+ const opacity = shape.opacity !== void 0 && shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : "";
1362
+ const transform = shape.transform ? ` transform="matrix(${shape.transform.join(" ")})"` : "";
1363
+ const path = shape.href ? `<use href="#${escape(shape.href)}"${transform}/>` : `<path${shape.id ? ` id="${escape(shape.id)}"` : ""} d="${shape.d}" fill="${fill}"${opacity}${transform}/>`;
1364
+ return shape.clip ? `<g clip-path="url(#${clipId(shape, idPrefix)})">${path}</g>` : path;
1365
+ }
1366
+ function layerMarkup(layer, idPrefix = "ridgescape", { clipMirrors = true } = {}) {
1367
+ const shapes2 = layer.shapes.map((s) => shapeMarkup(s, idPrefix)).join("");
1368
+ return layer.mirror && clipMirrors ? `<g clip-path="url(#${mirrorClipId(layer, idPrefix)})">${shapes2}</g>` : shapes2;
1369
+ }
1370
+ function serialize(rendered, idPrefix = "ridgescape") {
1371
+ const { width, height, layers } = rendered;
1372
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">` + defsMarkup(layers, idPrefix) + layers.map((l) => layerMarkup(l, idPrefix)).join("") + `</svg>`;
1373
+ }
1374
+ function toSvg(scene, options) {
1375
+ return serialize(render(scene, options), options.idPrefix);
1376
+ }
1377
+
1378
+ // src/presets/index.ts
1379
+ var presets = {
1380
+ /** Mountains at dusk behind rolling foothills. */
1381
+ sierra: { seed: 1, palette: "dusk", layers: [{ profile: "peaks" }, { profile: "peaks" }, {}, {}, {}] },
1382
+ /** A low, green footer. */
1383
+ meadow: { seed: 2, palette: "forest", height: "30%", layers: 3 },
1384
+ /** Many soft ridges sinking into fog. */
1385
+ misty: { seed: 3, palette: "fog", layers: 7, haze: { strength: 0.9 } },
1386
+ /** Mesas behind dunes. */
1387
+ mesa: { seed: 4, palette: "desert", layers: [{ profile: "mesa" }, { profile: "dunes" }, { profile: "dunes" }] },
1388
+ /** Snowy peaks, tall and pale. */
1389
+ glacier: { seed: 5, palette: "alpine", height: "60%", layers: [{ profile: "peaks" }, { profile: "peaks" }, { profile: "peaks" }, {}] },
1390
+ /** Dark hills under a night sky. */
1391
+ nightfall: { seed: 6, palette: "night", layers: 4 },
1392
+ /** A bright day: sun and puffy clouds over green hills. Draws its own sky. */
1393
+ summer: { seed: 7, palette: "alpine", sky: { sun: { at: ["78%", "22%"] } }, clouds: { bands: 2, cover: 0.45 }, layers: [{ profile: "peaks" }, {}, { color: "#4f7a4a" }, { color: "#355c37" }] },
1394
+ /** A crescent moon, stars and wisps of cloud. Draws its own sky. */
1395
+ starlight: { seed: 8, palette: "night", sky: { moon: { crescent: 0.55 }, stars: true }, clouds: { style: "streaks", cover: 0.3, opacity: 0.7 }, layers: [{ profile: "peaks" }, { trees: true }, { trees: { density: 0.8 } }] },
1396
+ /** A mountain lake mirroring the peaks behind it. Draws its own sky. */
1397
+ lakeside: {
1398
+ seed: 5,
1399
+ palette: "alpine",
1400
+ height: "60%",
1401
+ sky: { sun: { at: ["70%", "25%"] } },
1402
+ clouds: { bands: 1 },
1403
+ layers: [{ profile: "peaks" }, { profile: "peaks" }, { lakes: [{ at: "50%", width: "100%", level: "18%" }] }]
1404
+ },
1405
+ /** Pine forest climbing the hills, with misty valleys. */
1406
+ pinewood: { seed: 9, palette: "forest", layers: [{ profile: "peaks" }, { trees: true }, { trees: true }, { trees: { density: 0.85 } }] }
1407
+ };
1408
+
1409
+ // src/canvas/index.ts
1410
+ function fillStyle(ctx, fill) {
1411
+ if (typeof fill === "string") return fill;
1412
+ const gradient = fill.type === "radial" ? ctx.createRadialGradient(fill.cx, fill.cy, 0, fill.cx, fill.cy, fill.r) : ctx.createLinearGradient(fill.x1, fill.y1, fill.x2, fill.y2);
1413
+ for (const stop of fill.stops) gradient.addColorStop(stop.offset, stop.opacity === void 0 || stop.opacity === 1 ? stop.color : withAlpha(stop.color, stop.opacity));
1414
+ return gradient;
1415
+ }
1416
+ function withAlpha(color, alpha2) {
1417
+ const c = parseColor(color);
1418
+ if (c) return format({ ...c, a: c.a * alpha2 });
1419
+ return `color-mix(in srgb, ${color} ${Number((alpha2 * 100).toFixed(2))}%, transparent)`;
1420
+ }
1421
+ function drawShape(ctx, shape) {
1422
+ ctx.save();
1423
+ if (shape.clip) ctx.clip(new Path2D(shape.clip));
1424
+ if (shape.transform) ctx.transform(...shape.transform);
1425
+ if (shape.opacity !== void 0) ctx.globalAlpha *= shape.opacity;
1426
+ ctx.fillStyle = fillStyle(ctx, shape.fill);
1427
+ ctx.fill(new Path2D(shape.d));
1428
+ ctx.restore();
1429
+ }
1430
+ function drawLayer(ctx, layer, { clipMirrors = true } = {}) {
1431
+ const clip = layer.mirror && clipMirrors;
1432
+ if (clip) {
1433
+ ctx.save();
1434
+ ctx.clip(new Path2D(mirrorClip(layer.mirror.water)));
1435
+ }
1436
+ for (const shape of layer.shapes) drawShape(ctx, shape);
1437
+ if (clip) ctx.restore();
1438
+ }
1439
+ function drawToCanvas(ctx, rendered, { pixelRatio = 1, clear = true } = {}) {
1440
+ const layers = Array.isArray(rendered) ? rendered : rendered.layers;
1441
+ ctx.save();
1442
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1443
+ if (clear) ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
1444
+ ctx.scale(pixelRatio, pixelRatio);
1445
+ for (const layer of layers) drawLayer(ctx, layer);
1446
+ ctx.restore();
1447
+ }
1448
+
1449
+ // src/parallax/index.ts
1450
+ var PARALLAX_DEFAULTS = { strength: 0.3 };
1451
+ function parallaxDelta(rect, viewportHeight) {
1452
+ if (rect.bottom > viewportHeight) return rect.bottom - viewportHeight;
1453
+ return Math.max(Math.min(rect.top, 0), rect.bottom - viewportHeight);
1454
+ }
1455
+ function parallaxOffset(depth, strength, delta, viewportHeight) {
1456
+ const d = Math.min(viewportHeight, Math.max(-viewportHeight, delta));
1457
+ const k = Math.min(1, Math.max(0, strength * depth));
1458
+ return round3(-k * d);
1459
+ }
1460
+ function attachParallax(container, { strength = PARALLAX_DEFAULTS.strength } = {}) {
1461
+ if (typeof window === "undefined") return () => {
1462
+ };
1463
+ const depth = (el, name) => Number(el.getAttribute(name)) || 0;
1464
+ const layers = Array.from(container.querySelectorAll("[data-depth]"), (el) => ({ el, depth: depth(el, "data-depth") }));
1465
+ const mirrors = Array.from(container.querySelectorAll("[data-mirror]"), (el) => {
1466
+ const lake = el.parentElement?.closest("[data-depth]");
1467
+ return { el, depth: depth(el, "data-mirror"), lake: lake && container.contains(lake) ? depth(lake, "data-depth") : 0 };
1468
+ });
1469
+ const all = [...layers, ...mirrors];
1470
+ const reset = () => {
1471
+ for (const { el } of all) {
1472
+ el.style.transform = "";
1473
+ el.style.willChange = "";
1474
+ }
1475
+ };
1476
+ const reduced = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
1477
+ const move = (el, offset) => {
1478
+ el.style.willChange = "transform";
1479
+ el.style.transform = offset === 0 ? "" : `translate3d(0, ${offset}px, 0)`;
1480
+ };
1481
+ let frame = 0;
1482
+ const update = () => {
1483
+ frame = 0;
1484
+ if (reduced?.matches) return reset();
1485
+ const vh = window.innerHeight;
1486
+ const delta = parallaxDelta(container.getBoundingClientRect(), vh);
1487
+ const offset = (d) => parallaxOffset(d, strength, delta, vh);
1488
+ for (const l of layers) move(l.el, offset(l.depth));
1489
+ for (const m of mirrors) move(m.el, round3(offset(m.lake) - offset(m.depth)));
1490
+ };
1491
+ const schedule = () => {
1492
+ if (!frame) frame = requestAnimationFrame(update);
1493
+ };
1494
+ update();
1495
+ window.addEventListener("scroll", schedule, { passive: true });
1496
+ window.addEventListener("resize", schedule);
1497
+ reduced?.addEventListener?.("change", schedule);
1498
+ return () => {
1499
+ window.removeEventListener("scroll", schedule);
1500
+ window.removeEventListener("resize", schedule);
1501
+ reduced?.removeEventListener?.("change", schedule);
1502
+ if (frame) cancelAnimationFrame(frame);
1503
+ reset();
1504
+ };
1505
+ }
1506
+ var round3 = (n) => Math.round(n * 100) / 100 + 0;
1507
+ function clipStyle(water) {
1508
+ return `path("${mirrorClip(water)}")`;
1509
+ }
1510
+
1511
+ // src/parent.ts
1512
+ var ridgescapeParentStyle = { position: "relative", isolation: "isolate" };
1513
+ // Annotate the CommonJS export names for ESM import in node:
1514
+ 0 && (module.exports = {
1515
+ ANCHOR_HEIGHTS,
1516
+ CLOUD_DEFAULTS,
1517
+ DEPTH_DEFAULTS,
1518
+ LAKE_DEFAULTS,
1519
+ PARALLAX_DEFAULTS,
1520
+ PROFILE_DEFAULTS,
1521
+ SAMPLE_STEP,
1522
+ SCENE_DEFAULTS,
1523
+ SKY_DEFAULTS,
1524
+ TREE_DEFAULTS,
1525
+ TREE_GROUPS,
1526
+ TREE_SHAPES,
1527
+ attachParallax,
1528
+ buildOutline,
1529
+ buildProfile,
1530
+ capsulePath,
1531
+ circlePath,
1532
+ clipId,
1533
+ clipStyle,
1534
+ createRandom,
1535
+ defsMarkup,
1536
+ drawLayer,
1537
+ drawShape,
1538
+ drawToCanvas,
1539
+ formatColor,
1540
+ formatNumber,
1541
+ gradientMarkup,
1542
+ groundPath,
1543
+ hashString,
1544
+ layerColor,
1545
+ layerMarkup,
1546
+ mirrorClip,
1547
+ mirrorClipId,
1548
+ mix,
1549
+ octaves,
1550
+ palettes,
1551
+ parallaxDelta,
1552
+ parallaxOffset,
1553
+ parseColor,
1554
+ periodicNoise,
1555
+ placeTrees,
1556
+ presets,
1557
+ rectPath,
1558
+ reflectionLayers,
1559
+ render,
1560
+ resolve,
1561
+ resolveLength,
1562
+ resolvePalette,
1563
+ ridgescapeParentStyle,
1564
+ scaleLength,
1565
+ serialize,
1566
+ shapeMarkup,
1567
+ simplify,
1568
+ toSvg,
1569
+ treeHeightAt,
1570
+ treeKeyPoints,
1571
+ waterBase,
1572
+ waterSurface
1573
+ });
1574
+ //# sourceMappingURL=index.cjs.map