privateboard 0.1.13 → 0.1.16

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.
@@ -0,0 +1,736 @@
1
+ /* ──────────────────────────────────────────────────────────────────
2
+ share-cover-svg-creator.js · randomized 8-bit pixel-art cover for
3
+ the All-Notes share-card modal's "boardroom" template.
4
+
5
+ Each `generate()` call rolls a fresh variation:
6
+ · sky palette (sunset / dawn / midnight / dusk / tropical / aurora)
7
+ · ground material (warm-sand / moss / snow / lavender / volcanic / desert)
8
+ · river (random control-point ys, random width 30-48 px)
9
+ · sky decorations (moon at fixed top-right, stars 12-20, clouds 2-4)
10
+
11
+ Trees / stones / flowers / dirt patches were all removed — the
12
+ composition is now ASCII logo (top) + black quote panel (centre)
13
+ + river (low ground) + sky decorations. The helper functions
14
+ (`placeStones`, `placeTrees`, `placeFlowers`, `placeDirt`) are
15
+ kept as dead code in case the user wants to bring them back.
16
+
17
+ Output is consumed by `renderShareCardHtml`'s boardroom branch in
18
+ `app.js`: the consumer pulls `skyGradient` into an inline
19
+ `style="background: …"` on the `.share-card` div and injects
20
+ `skyDeco + groundDeco` inside the `.sc-sky-deco` SVG. The module
21
+ owns NO DOM — it just returns markup strings.
22
+
23
+ Public API:
24
+ window.shareCoverSvgCreator.generate(opts?) → {
25
+ seed: number,
26
+ skyGradient: string, // CSS linear-gradient(...) value
27
+ skyDeco: string, // SVG inner markup (sky band)
28
+ groundDeco: string, // SVG inner markup (ground band)
29
+ palette: { skyName, groundName }
30
+ }
31
+
32
+ Seeded mode (for tests / reproducibility):
33
+ window.shareCoverSvgCreator.generate({ seed: 42 }) → deterministic
34
+ ────────────────────────────────────────────────────────────────── */
35
+
36
+ (function () {
37
+ "use strict";
38
+
39
+ // ── Deterministic seeded RNG · mulberry32 ────────────────────
40
+ function makeRng(seed) {
41
+ let s = (seed | 0) || 1;
42
+ return function () {
43
+ s = (s + 0x6D2B79F5) | 0;
44
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
45
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
46
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
47
+ };
48
+ }
49
+ const pick = (rng, arr) => arr[Math.floor(rng() * arr.length)];
50
+ const between = (rng, a, b) => a + rng() * (b - a);
51
+ const betweenInt = (rng, a, b) => Math.floor(between(rng, a, b + 1));
52
+
53
+ // ── Sky palettes · each is a 9-stop hard-step gradient ───────
54
+ // Stop 0 = top (deepest), stop 8 = horizon (warmest / lightest).
55
+ // The bottom two stops get overwritten with the chosen ground
56
+ // material's base color so the horizon blends into the ground.
57
+ const SKY_PALETTES = [
58
+ { name: "sunset",
59
+ stops: ["#1B0A35", "#2D1452", "#4A2070", "#6B2B6E", "#A03B5E",
60
+ "#D85F4C", "#ED8E48", "#F4C078", "#F8D898"] },
61
+ { name: "dawn",
62
+ stops: ["#1A1B3A", "#2D3460", "#4A5790", "#7A6FA8", "#C08BB0",
63
+ "#E8A088", "#F5C5A0", "#FBE2B8", "#FFEED2"] },
64
+ { name: "midnight",
65
+ stops: ["#08051A", "#100A28", "#1C1545", "#2A2058", "#3A2D68",
66
+ "#4A3878", "#5A4288", "#7050A0", "#9070B0"] },
67
+ { name: "dusk",
68
+ stops: ["#0A1538", "#152258", "#243088", "#3A4AA0", "#5C68A8",
69
+ "#88789C", "#B89090", "#DCA888", "#F0C898"] },
70
+ { name: "tropical",
71
+ stops: ["#1A0850", "#3A1070", "#702090", "#A82C82", "#D84878",
72
+ "#F06868", "#F88858", "#F8A858", "#F8D898"] },
73
+ { name: "aurora",
74
+ stops: ["#001528", "#003050", "#005078", "#208098", "#40B0A8",
75
+ "#80D8A8", "#B0E8B8", "#D8F0C8", "#F0F8E0"] },
76
+ ];
77
+
78
+ // ── Ground materials · base color + 4-tone dirt palette ──────
79
+ const GROUND_MATERIALS = [
80
+ { name: "warm-sand",
81
+ base: "#F8D898", dirt: ["#A0814F", "#8A6A48", "#5A3A22", "#3A2410"] },
82
+ { name: "moss-field",
83
+ base: "#A8C088", dirt: ["#6A8050", "#506838", "#3A5024", "#2A3818"] },
84
+ { name: "snow-drift",
85
+ base: "#E8E8EE", dirt: ["#A8A8B0", "#82828C", "#60606C", "#404048"] },
86
+ { name: "lavender-bloom",
87
+ base: "#E0C8E0", dirt: ["#9080A0", "#705C80", "#503C60", "#382848"] },
88
+ { name: "volcanic",
89
+ base: "#3A2828", dirt: ["#4A302C", "#3A2420", "#2A1814", "#1A0C08"] },
90
+ { name: "desert-sand",
91
+ base: "#F4C078", dirt: ["#A8784A", "#885828", "#684020", "#482818"] },
92
+ ];
93
+
94
+ // Each band stops at this percent (rest go in between).
95
+ const BAND_STOPS = [5, 11, 18, 25, 33, 41, 49, 57];
96
+
97
+ function gradientFromStops(stops) {
98
+ // Build hard-step bands so the gradient reads as 8-bit horizontal
99
+ // stripes (no anti-aliased fades between bands).
100
+ const parts = [];
101
+ parts.push(`${stops[0]} 0%`);
102
+ parts.push(`${stops[0]} ${BAND_STOPS[0]}%`);
103
+ for (let i = 1; i < BAND_STOPS.length; i++) {
104
+ parts.push(`${stops[i]} ${BAND_STOPS[i - 1]}%`);
105
+ parts.push(`${stops[i]} ${BAND_STOPS[i]}%`);
106
+ }
107
+ parts.push(`${stops[8]} ${BAND_STOPS[BAND_STOPS.length - 1]}%`);
108
+ parts.push(`${stops[8]} 100%`);
109
+ return `linear-gradient(180deg, ${parts.join(", ")})`;
110
+ }
111
+
112
+ // ── Sprite generators (return SVG markup strings) ────────────
113
+ function star(x, y, color) {
114
+ return `<rect x="${x}" y="${y}" width="2" height="2" fill="${color || "#FFF6D9"}"/>`;
115
+ }
116
+ function tinyStar(x, y, color) {
117
+ return `<rect x="${x}" y="${y}" width="1" height="1" fill="${color || "#FFE8A8"}"/>`;
118
+ }
119
+ function cloud(x, y) {
120
+ return `
121
+ <rect x="${x + 4}" y="${y - 2}" width="10" height="2" fill="#FFF6D9"/>
122
+ <rect x="${x}" y="${y}" width="18" height="4" fill="#FFF6D9"/>
123
+ <rect x="${x + 2}" y="${y + 4}" width="14" height="2" fill="#F4C078"/>
124
+ `;
125
+ }
126
+ function bigCloud(x, y) {
127
+ return `
128
+ <rect x="${x + 6}" y="${y - 4}" width="14" height="2" fill="#FFF6D9"/>
129
+ <rect x="${x + 2}" y="${y - 2}" width="22" height="2" fill="#FFF6D9"/>
130
+ <rect x="${x}" y="${y}" width="26" height="4" fill="#FFF6D9"/>
131
+ <rect x="${x + 2}" y="${y + 4}" width="22" height="2" fill="#F4C078"/>
132
+ `;
133
+ }
134
+ function moon(x, y) {
135
+ return `
136
+ <g transform="translate(${x} ${y})">
137
+ <rect x="8" y="0" width="20" height="4" fill="#FFF6D9"/>
138
+ <rect x="4" y="4" width="28" height="4" fill="#FFF6D9"/>
139
+ <rect x="0" y="8" width="36" height="14" fill="#FFF6D9"/>
140
+ <rect x="4" y="22" width="28" height="4" fill="#FFF6D9"/>
141
+ <rect x="8" y="26" width="20" height="4" fill="#FFF6D9"/>
142
+ <rect x="14" y="10" width="3" height="3" fill="#F4D898"/>
143
+ <rect x="22" y="14" width="2" height="2" fill="#F4D898"/>
144
+ <rect x="10" y="18" width="2" height="2" fill="#F4D898"/>
145
+ </g>
146
+ `;
147
+ }
148
+
149
+ // Top-down stones · three sizes. Mid body, bright dome highlight,
150
+ // specular spot, soft drop shadow. Same fills as the inline
151
+ // version they're replacing.
152
+ function stoneSmall(x, y) {
153
+ return `
154
+ <rect x="${x + 4}" y="${y + 14}" width="16" height="2" fill="#1B0A35" opacity="0.28"/>
155
+ <rect x="${x + 4}" y="${y}" width="12" height="2" fill="#7A6F62"/>
156
+ <rect x="${x + 2}" y="${y + 2}" width="16" height="2" fill="#7A6F62"/>
157
+ <rect x="${x}" y="${y + 4}" width="20" height="6" fill="#7A6F62"/>
158
+ <rect x="${x + 2}" y="${y + 10}" width="16" height="2" fill="#7A6F62"/>
159
+ <rect x="${x + 4}" y="${y + 12}" width="12" height="2" fill="#7A6F62"/>
160
+ <rect x="${x + 6}" y="${y + 2}" width="8" height="2" fill="#9F9388"/>
161
+ <rect x="${x + 4}" y="${y + 4}" width="12" height="4" fill="#9F9388"/>
162
+ <rect x="${x + 6}" y="${y + 8}" width="8" height="2" fill="#9F9388"/>
163
+ <rect x="${x + 7}" y="${y + 4}" width="4" height="2" fill="#B5A998"/>
164
+ <rect x="${x + 4}" y="${y + 12}" width="12" height="1" fill="#4A4038"/>
165
+ `;
166
+ }
167
+ function stoneMed(x, y) {
168
+ return `
169
+ <rect x="${x + 4}" y="${y + 20}" width="24" height="2" fill="#1B0A35" opacity="0.30"/>
170
+ <rect x="${x + 6}" y="${y + 22}" width="20" height="1" fill="#1B0A35" opacity="0.18"/>
171
+ <rect x="${x + 6}" y="${y}" width="20" height="2" fill="#7A6F62"/>
172
+ <rect x="${x + 3}" y="${y + 2}" width="26" height="2" fill="#7A6F62"/>
173
+ <rect x="${x + 1}" y="${y + 4}" width="30" height="2" fill="#7A6F62"/>
174
+ <rect x="${x}" y="${y + 6}" width="32" height="8" fill="#7A6F62"/>
175
+ <rect x="${x + 1}" y="${y + 14}" width="30" height="2" fill="#7A6F62"/>
176
+ <rect x="${x + 3}" y="${y + 16}" width="26" height="2" fill="#7A6F62"/>
177
+ <rect x="${x + 6}" y="${y + 18}" width="20" height="2" fill="#7A6F62"/>
178
+ <rect x="${x + 9}" y="${y + 2}" width="14" height="2" fill="#9F9388"/>
179
+ <rect x="${x + 6}" y="${y + 4}" width="20" height="2" fill="#9F9388"/>
180
+ <rect x="${x + 4}" y="${y + 6}" width="24" height="4" fill="#9F9388"/>
181
+ <rect x="${x + 6}" y="${y + 10}" width="20" height="2" fill="#9F9388"/>
182
+ <rect x="${x + 11}" y="${y + 5}" width="10" height="3" fill="#B5A998"/>
183
+ <rect x="${x + 4}" y="${y + 16}" width="24" height="1" fill="#5A5048"/>
184
+ <rect x="${x + 7}" y="${y + 18}" width="18" height="1" fill="#4A4038"/>
185
+ `;
186
+ }
187
+ function stoneLarge(x, y) {
188
+ return `
189
+ <rect x="${x + 6}" y="${y + 28}" width="36" height="2" fill="#1B0A35" opacity="0.32"/>
190
+ <rect x="${x + 8}" y="${y + 30}" width="32" height="1" fill="#1B0A35" opacity="0.20"/>
191
+ <rect x="${x + 10}" y="${y}" width="26" height="2" fill="#7A6F62"/>
192
+ <rect x="${x + 6}" y="${y + 2}" width="34" height="2" fill="#7A6F62"/>
193
+ <rect x="${x + 3}" y="${y + 4}" width="40" height="2" fill="#7A6F62"/>
194
+ <rect x="${x + 1}" y="${y + 6}" width="44" height="2" fill="#7A6F62"/>
195
+ <rect x="${x}" y="${y + 8}" width="46" height="10" fill="#7A6F62"/>
196
+ <rect x="${x + 1}" y="${y + 18}" width="44" height="2" fill="#7A6F62"/>
197
+ <rect x="${x + 3}" y="${y + 20}" width="40" height="2" fill="#7A6F62"/>
198
+ <rect x="${x + 6}" y="${y + 22}" width="34" height="2" fill="#7A6F62"/>
199
+ <rect x="${x + 10}" y="${y + 24}" width="26" height="2" fill="#7A6F62"/>
200
+ <rect x="${x + 14}" y="${y + 2}" width="18" height="2" fill="#9F9388"/>
201
+ <rect x="${x + 10}" y="${y + 4}" width="26" height="2" fill="#9F9388"/>
202
+ <rect x="${x + 6}" y="${y + 6}" width="34" height="2" fill="#9F9388"/>
203
+ <rect x="${x + 4}" y="${y + 8}" width="38" height="6" fill="#9F9388"/>
204
+ <rect x="${x + 6}" y="${y + 14}" width="34" height="2" fill="#9F9388"/>
205
+ <rect x="${x + 16}" y="${y + 6}" width="14" height="4" fill="#B5A998"/>
206
+ <rect x="${x + 18}" y="${y + 10}" width="10" height="2" fill="#C7BDB0"/>
207
+ <rect x="${x + 6}" y="${y + 20}" width="34" height="1" fill="#5A5048"/>
208
+ <rect x="${x + 10}" y="${y + 22}" width="26" height="1" fill="#4A4038"/>
209
+ <rect x="${x + 14}" y="${y + 24}" width="18" height="1" fill="#3A302A"/>
210
+ `;
211
+ }
212
+
213
+ // Flower · 4-petal pixel head with a stem. The "big" variant is
214
+ // ~1.4× the small. Stem and one leaf in `leaf` color.
215
+ const FLOWER_PALETTES = [
216
+ { petal: "#E26AA0", center: "#F8D848", leaf: "#3D6E2F" }, // pink
217
+ { petal: "#F0D848", center: "#E26AA0", leaf: "#3D6E2F" }, // yellow
218
+ { petal: "#FCFCFC", center: "#F0D848", leaf: "#3D6E2F" }, // white-daisy
219
+ { petal: "#C8B8E8", center: "#F0D848", leaf: "#3D6E2F" }, // lavender
220
+ { petal: "#F86868", center: "#F8D848", leaf: "#3D6E2F" }, // red
221
+ { petal: "#90C8E8", center: "#F0D848", leaf: "#3D6E2F" }, // sky-blue
222
+ ];
223
+ // Bigger flowers · sized to match the stones (was tiny ~8×11 /
224
+ // ~9×16 before, now ~18×28 / ~26×36 so the bloom reads as a
225
+ // substantial landscape element, not a sprinkled crumb). 6-7 tier
226
+ // round daisy head + 4-6 pixel center + lit-corner highlight +
227
+ // stem with a leaf or two.
228
+ function flower(x, y, p) {
229
+ return `
230
+ <!-- Daisy head · 9 stepped petal rows -->
231
+ <rect x="${x + 7}" y="${y}" width="4" height="2" fill="${p.petal}"/>
232
+ <rect x="${x + 5}" y="${y + 2}" width="8" height="2" fill="${p.petal}"/>
233
+ <rect x="${x + 3}" y="${y + 4}" width="12" height="2" fill="${p.petal}"/>
234
+ <rect x="${x + 1}" y="${y + 6}" width="16" height="2" fill="${p.petal}"/>
235
+ <rect x="${x + 1}" y="${y + 8}" width="16" height="4" fill="${p.petal}"/>
236
+ <rect x="${x + 1}" y="${y + 12}" width="16" height="2" fill="${p.petal}"/>
237
+ <rect x="${x + 3}" y="${y + 14}" width="12" height="2" fill="${p.petal}"/>
238
+ <rect x="${x + 5}" y="${y + 16}" width="8" height="2" fill="${p.petal}"/>
239
+ <rect x="${x + 7}" y="${y + 18}" width="4" height="2" fill="${p.petal}"/>
240
+ <!-- Center · 6×6 block of the center color + 2×2 sparkle -->
241
+ <rect x="${x + 6}" y="${y + 7}" width="6" height="6" fill="${p.center}"/>
242
+ <rect x="${x + 8}" y="${y + 9}" width="2" height="2" fill="#FFFFFF"/>
243
+ <!-- Stem -->
244
+ <rect x="${x + 8}" y="${y + 20}" width="2" height="8" fill="${p.leaf}"/>
245
+ <!-- Leaf branches off to the right -->
246
+ <rect x="${x + 10}" y="${y + 24}" width="4" height="2" fill="${p.leaf}"/>
247
+ <rect x="${x + 12}" y="${y + 22}" width="2" height="2" fill="${p.leaf}"/>
248
+ `;
249
+ }
250
+ function flowerBig(x, y, p) {
251
+ return `
252
+ <!-- Daisy head · 11 stepped petal rows (~24 px wide) -->
253
+ <rect x="${x + 10}" y="${y}" width="6" height="2" fill="${p.petal}"/>
254
+ <rect x="${x + 8}" y="${y + 2}" width="10" height="2" fill="${p.petal}"/>
255
+ <rect x="${x + 6}" y="${y + 4}" width="14" height="2" fill="${p.petal}"/>
256
+ <rect x="${x + 4}" y="${y + 6}" width="18" height="2" fill="${p.petal}"/>
257
+ <rect x="${x + 2}" y="${y + 8}" width="22" height="2" fill="${p.petal}"/>
258
+ <rect x="${x + 2}" y="${y + 10}" width="22" height="6" fill="${p.petal}"/>
259
+ <rect x="${x + 2}" y="${y + 16}" width="22" height="2" fill="${p.petal}"/>
260
+ <rect x="${x + 4}" y="${y + 18}" width="18" height="2" fill="${p.petal}"/>
261
+ <rect x="${x + 6}" y="${y + 20}" width="14" height="2" fill="${p.petal}"/>
262
+ <rect x="${x + 8}" y="${y + 22}" width="10" height="2" fill="${p.petal}"/>
263
+ <rect x="${x + 10}" y="${y + 24}" width="6" height="2" fill="${p.petal}"/>
264
+ <!-- Center · 8×8 in center color + 3×3 white sparkle -->
265
+ <rect x="${x + 9}" y="${y + 9}" width="8" height="8" fill="${p.center}"/>
266
+ <rect x="${x + 11}" y="${y + 11}" width="3" height="3" fill="#FFFFFF"/>
267
+ <!-- Stem (longer) -->
268
+ <rect x="${x + 12}" y="${y + 26}" width="2" height="10" fill="${p.leaf}"/>
269
+ <!-- Two leaves on opposite sides for symmetry -->
270
+ <rect x="${x + 14}" y="${y + 28}" width="6" height="2" fill="${p.leaf}"/>
271
+ <rect x="${x + 18}" y="${y + 26}" width="2" height="2" fill="${p.leaf}"/>
272
+ <rect x="${x + 6}" y="${y + 32}" width="6" height="2" fill="${p.leaf}"/>
273
+ <rect x="${x + 4}" y="${y + 30}" width="2" height="2" fill="${p.leaf}"/>
274
+ `;
275
+ }
276
+
277
+ // Pixel trees · scaled 2× from the previous version so they read
278
+ // as substantial canopy elements rather than small bushes. Multi-
279
+ // tier rounded canopy in three greens (dark silhouette / mid body
280
+ // / lit highlights), thick warm-brown trunk with a 2-px brighter
281
+ // grain stripe, and a chunky drop shadow on the ground beneath.
282
+ // Trees sit on the lower bank in front of the river.
283
+ //
284
+ // Bounding boxes used by the placement coordinator:
285
+ // treeSmall · ≈ 48 wide × 74 tall (canopy y=0-50, trunk to y=72)
286
+ // treeTall · ≈ 56 wide × 98 tall (canopy y=0-58, trunk to y=96)
287
+ function treeSmall(x, y) {
288
+ return `
289
+ <!-- Ground shadow -->
290
+ <rect x="${x + 12}" y="${y + 72}" width="24" height="2" fill="#1B0A35" opacity="0.32"/>
291
+ <!-- Trunk -->
292
+ <rect x="${x + 20}" y="${y + 48}" width="8" height="22" fill="#5A3A22"/>
293
+ <rect x="${x + 16}" y="${y + 68}" width="16" height="4" fill="#5A3A22"/>
294
+ <rect x="${x + 21}" y="${y + 48}" width="2" height="18" fill="#7A4F30"/>
295
+ <!-- Canopy outer · dark silhouette (round, 9 stepped rows) -->
296
+ <rect x="${x + 16}" y="${y}" width="16" height="4" fill="#2A4F1D"/>
297
+ <rect x="${x + 10}" y="${y + 4}" width="28" height="4" fill="#2A4F1D"/>
298
+ <rect x="${x + 6}" y="${y + 8}" width="36" height="4" fill="#2A4F1D"/>
299
+ <rect x="${x + 2}" y="${y + 12}" width="44" height="4" fill="#2A4F1D"/>
300
+ <rect x="${x}" y="${y + 16}" width="48" height="20" fill="#2A4F1D"/>
301
+ <rect x="${x + 2}" y="${y + 36}" width="44" height="4" fill="#2A4F1D"/>
302
+ <rect x="${x + 6}" y="${y + 40}" width="36" height="4" fill="#2A4F1D"/>
303
+ <rect x="${x + 10}" y="${y + 44}" width="28" height="4" fill="#2A4F1D"/>
304
+ <rect x="${x + 16}" y="${y + 48}" width="16" height="2" fill="#2A4F1D"/>
305
+ <!-- Canopy mid · lighter green body inset 4 px -->
306
+ <rect x="${x + 14}" y="${y + 4}" width="20" height="4" fill="#3D6E2F"/>
307
+ <rect x="${x + 10}" y="${y + 8}" width="28" height="4" fill="#3D6E2F"/>
308
+ <rect x="${x + 6}" y="${y + 12}" width="36" height="4" fill="#3D6E2F"/>
309
+ <rect x="${x + 4}" y="${y + 16}" width="40" height="20" fill="#3D6E2F"/>
310
+ <rect x="${x + 6}" y="${y + 36}" width="36" height="4" fill="#3D6E2F"/>
311
+ <rect x="${x + 10}" y="${y + 40}" width="28" height="2" fill="#3D6E2F"/>
312
+ <!-- Canopy highlights · brightest pixel clusters -->
313
+ <rect x="${x + 12}" y="${y + 16}" width="8" height="4" fill="#6BAA48"/>
314
+ <rect x="${x + 24}" y="${y + 12}" width="6" height="4" fill="#6BAA48"/>
315
+ <rect x="${x + 18}" y="${y + 24}" width="4" height="4" fill="#6BAA48"/>
316
+ <rect x="${x + 30}" y="${y + 20}" width="4" height="4" fill="#6BAA48"/>
317
+ <rect x="${x + 10}" y="${y + 28}" width="4" height="4" fill="#6BAA48"/>
318
+ `;
319
+ }
320
+ function treeTall(x, y) {
321
+ return `
322
+ <!-- Ground shadow -->
323
+ <rect x="${x + 14}" y="${y + 96}" width="28" height="2" fill="#1B0A35" opacity="0.34"/>
324
+ <!-- Trunk · longer than treeSmall -->
325
+ <rect x="${x + 24}" y="${y + 58}" width="10" height="36" fill="#5A3A22"/>
326
+ <rect x="${x + 20}" y="${y + 92}" width="20" height="4" fill="#5A3A22"/>
327
+ <rect x="${x + 25}" y="${y + 58}" width="2" height="32" fill="#7A4F30"/>
328
+ <!-- Canopy outer · taller, fuller body -->
329
+ <rect x="${x + 20}" y="${y}" width="16" height="4" fill="#2A4F1D"/>
330
+ <rect x="${x + 14}" y="${y + 4}" width="28" height="4" fill="#2A4F1D"/>
331
+ <rect x="${x + 10}" y="${y + 8}" width="36" height="4" fill="#2A4F1D"/>
332
+ <rect x="${x + 4}" y="${y + 12}" width="48" height="4" fill="#2A4F1D"/>
333
+ <rect x="${x + 2}" y="${y + 16}" width="52" height="28" fill="#2A4F1D"/>
334
+ <rect x="${x + 4}" y="${y + 44}" width="48" height="4" fill="#2A4F1D"/>
335
+ <rect x="${x + 10}" y="${y + 48}" width="36" height="4" fill="#2A4F1D"/>
336
+ <rect x="${x + 14}" y="${y + 52}" width="28" height="4" fill="#2A4F1D"/>
337
+ <rect x="${x + 20}" y="${y + 56}" width="16" height="2" fill="#2A4F1D"/>
338
+ <!-- Canopy mid -->
339
+ <rect x="${x + 18}" y="${y + 4}" width="20" height="4" fill="#3D6E2F"/>
340
+ <rect x="${x + 14}" y="${y + 8}" width="28" height="4" fill="#3D6E2F"/>
341
+ <rect x="${x + 8}" y="${y + 12}" width="40" height="4" fill="#3D6E2F"/>
342
+ <rect x="${x + 6}" y="${y + 16}" width="44" height="28" fill="#3D6E2F"/>
343
+ <rect x="${x + 8}" y="${y + 44}" width="40" height="4" fill="#3D6E2F"/>
344
+ <rect x="${x + 14}" y="${y + 48}" width="28" height="2" fill="#3D6E2F"/>
345
+ <!-- Canopy highlights -->
346
+ <rect x="${x + 16}" y="${y + 18}" width="8" height="4" fill="#6BAA48"/>
347
+ <rect x="${x + 28}" y="${y + 12}" width="8" height="4" fill="#6BAA48"/>
348
+ <rect x="${x + 22}" y="${y + 30}" width="4" height="4" fill="#6BAA48"/>
349
+ <rect x="${x + 36}" y="${y + 24}" width="6" height="4" fill="#6BAA48"/>
350
+ <rect x="${x + 12}" y="${y + 36}" width="4" height="4" fill="#6BAA48"/>
351
+ <!-- Small fruit · 3 red pixel flecks scattered through canopy -->
352
+ <rect x="${x + 30}" y="${y + 22}" width="3" height="3" fill="#F86868"/>
353
+ <rect x="${x + 18}" y="${y + 28}" width="3" height="3" fill="#F86868"/>
354
+ <rect x="${x + 38}" y="${y + 38}" width="3" height="3" fill="#F86868"/>
355
+ `;
356
+ }
357
+
358
+ // Dirt patches · two sizes. Colors are sampled per call from the
359
+ // chosen ground material's 4-tone dirt palette.
360
+ function dirtPatch(x, y, c) {
361
+ return `
362
+ <rect x="${x}" y="${y}" width="14" height="1" fill="${c[0]}"/>
363
+ <rect x="${x}" y="${y + 1}" width="16" height="2" fill="${c[1]}"/>
364
+ <rect x="${x}" y="${y + 3}" width="14" height="2" fill="${c[2]}"/>
365
+ <rect x="${x + 2}" y="${y + 5}" width="10" height="1" fill="${c[3]}"/>
366
+ <rect x="${x + 4}" y="${y + 1}" width="2" height="1" fill="${c[0]}"/>
367
+ <rect x="${x + 9}" y="${y + 2}" width="2" height="1" fill="${c[0]}"/>
368
+ `;
369
+ }
370
+ function dirtPatchBig(x, y, c) {
371
+ return `
372
+ <rect x="${x + 1}" y="${y}" width="22" height="1" fill="${c[0]}"/>
373
+ <rect x="${x}" y="${y + 1}" width="26" height="2" fill="${c[1]}"/>
374
+ <rect x="${x}" y="${y + 3}" width="26" height="2" fill="${c[2]}"/>
375
+ <rect x="${x + 2}" y="${y + 5}" width="22" height="2" fill="${c[3]}"/>
376
+ <rect x="${x + 4}" y="${y + 7}" width="18" height="1" fill="${c[3]}"/>
377
+ <rect x="${x + 5}" y="${y + 1}" width="3" height="1" fill="${c[0]}"/>
378
+ <rect x="${x + 14}" y="${y + 2}" width="2" height="1" fill="${c[0]}"/>
379
+ <rect x="${x + 20}" y="${y + 4}" width="2" height="1" fill="${c[0]}"/>
380
+ <rect x="${x + 8}" y="${y + 4}" width="1" height="1" fill="${c[1]}"/>
381
+ `;
382
+ }
383
+
384
+ // ── River generator ─────────────────────────────────────────
385
+ // 7 anchor xs at -10 / 80 / 180 / 260 / 340 / 420 / 550.
386
+ // Top edge y wanders ±12px around y≈685 with a tighter cap on the
387
+ // outer endpoints. River shifted down ~100px from the original
388
+ // 585 baseline so the ASCII PRIVATE BOARD logo (y=80-260) and the
389
+ // black quote panel (centred at y=460, worst-case bottom y=630)
390
+ // both have ample vertical room. Trees and stones are gone, so
391
+ // the river is now the sole ground element and sits low in the
392
+ // card to balance the watermark band at y=770+.
393
+ const RIVER_ANCHOR_XS = [-10, 80, 180, 260, 340, 420, 550];
394
+
395
+ function generateRiverPath(rng) {
396
+ // Base top-edge y for each anchor · shifted +100 vs. the original
397
+ // [583, 571, 603, 583, 563, 595, 575] so the river sits low in
398
+ // the card, just above the watermark. We perturb each control
399
+ // point a few pixels so the curve is fresh per roll but stays
400
+ // within safe vertical bounds (max bottom ~735, clear of the
401
+ // 770+ watermark band).
402
+ const baseTopYs = [683, 671, 703, 683, 663, 695, 675];
403
+ const widthVar = betweenInt(rng, 30, 48);
404
+
405
+ const topYs = baseTopYs.map((y, i) => {
406
+ // Less wiggle at the endpoints (so the river enters / exits
407
+ // the card at a predictable height), more at the controls.
408
+ const isEndpoint = (i === 0 || i === 6);
409
+ const jitter = isEndpoint ? betweenInt(rng, -6, 6) : betweenInt(rng, -12, 12);
410
+ return y + jitter;
411
+ });
412
+ const bottomYs = topYs.map((y) => y + widthVar + betweenInt(rng, -4, 4));
413
+
414
+ const fmt = (xs, ys) => xs.map((x, i) => `${x} ${ys[i]}`).join(", ");
415
+ // Body · solid cyan, closed path running left-to-right along
416
+ // the top edge then right-to-left along the bottom edge.
417
+ const bodyD = [
418
+ `M -10 ${topYs[0]}`,
419
+ `C 80 ${topYs[1]}, 180 ${topYs[2]}, 260 ${topYs[3]}`,
420
+ `C 340 ${topYs[4]}, 420 ${topYs[5]}, 550 ${topYs[6]}`,
421
+ `L 550 ${bottomYs[6]}`,
422
+ `C 420 ${bottomYs[5]}, 340 ${bottomYs[4]}, 260 ${bottomYs[3]}`,
423
+ `C 180 ${bottomYs[2]}, 80 ${bottomYs[1]}, -10 ${bottomYs[0]} Z`,
424
+ ].join(" ");
425
+ const topHiD = [
426
+ `M -10 ${topYs[0] + 2}`,
427
+ `C 80 ${topYs[1] + 2}, 180 ${topYs[2] + 2}, 260 ${topYs[3] + 2}`,
428
+ `C 340 ${topYs[4] + 2}, 420 ${topYs[5] + 2}, 550 ${topYs[6] + 2}`,
429
+ ].join(" ");
430
+ const botShadowD = [
431
+ `M -10 ${bottomYs[0] - 2}`,
432
+ `C 80 ${bottomYs[1] - 2}, 180 ${bottomYs[2] - 2}, 260 ${bottomYs[3] - 2}`,
433
+ `C 340 ${bottomYs[4] - 2}, 420 ${bottomYs[5] - 2}, 550 ${bottomYs[6] - 2}`,
434
+ ].join(" ");
435
+
436
+ // Sparkle pixels along the river spine.
437
+ const sparkles = [];
438
+ const sparkleXs = [44, 116, 184, 244, 312, 368, 436, 496, 72, 160, 284, 396];
439
+ for (const sx of sparkleXs) {
440
+ const topAt = approxAt(sx, RIVER_ANCHOR_XS, topYs);
441
+ const botAt = approxAt(sx, RIVER_ANCHOR_XS, bottomYs);
442
+ const spineY = ((topAt + botAt) / 2) | 0;
443
+ const sy = spineY + betweenInt(rng, -8, 8);
444
+ const w = rng() > 0.5 ? 3 : 2;
445
+ sparkles.push(`<rect x="${sx}" y="${sy}" width="${w}" height="1" fill="#FFFFFF"/>`);
446
+ }
447
+
448
+ return {
449
+ topYs,
450
+ bottomYs,
451
+ widthVar,
452
+ svg: `
453
+ <path d="${bodyD}" fill="#5BC0EB" shape-rendering="geometricPrecision"/>
454
+ <path d="${topHiD}" stroke="#9ADEFA" stroke-width="2" fill="none" shape-rendering="geometricPrecision"/>
455
+ <path d="${botShadowD}" stroke="#2A6FA5" stroke-width="2" fill="none" shape-rendering="geometricPrecision"/>
456
+ ${sparkles.join("")}
457
+ `,
458
+ };
459
+ }
460
+
461
+ function approxAt(x, anchorXs, ys) {
462
+ for (let i = 0; i < anchorXs.length - 1; i++) {
463
+ if (x >= anchorXs[i] && x <= anchorXs[i + 1]) {
464
+ const t = (x - anchorXs[i]) / (anchorXs[i + 1] - anchorXs[i]);
465
+ return ys[i] + (ys[i + 1] - ys[i]) * t;
466
+ }
467
+ }
468
+ return x < anchorXs[0] ? ys[0] : ys[ys.length - 1];
469
+ }
470
+ const riverTopAt = (x, river) => approxAt(x, RIVER_ANCHOR_XS, river.topYs);
471
+ const riverBotAt = (x, river) => approxAt(x, RIVER_ANCHOR_XS, river.bottomYs);
472
+
473
+ // ── Placement helpers ───────────────────────────────────────
474
+ // Shared placement coordinator · tracks rectangular "occupied"
475
+ // regions claimed by earlier sprites so later sprites can land
476
+ // somewhere else. Each entry is `{xMin, yMin, xMax, yMax}` in
477
+ // the 540×800 card coordinate system.
478
+ //
479
+ // Two fixed containers to dodge (nothing else is placed on the
480
+ // ground any more — trees/stones/flowers/dirt all removed):
481
+ // · the ASCII PRIVATE BOARD logo · anchored at top:130, span
482
+ // y=130-250 (10-line block at 12px line-height).
483
+ // · the BLACK QUOTE PANEL · centre-anchored at vertical y=460
484
+ // (CSS `top: 460; transform: translateY(-50%)` on the 800-
485
+ // tall card). Content drives the height up to a 340-px cap,
486
+ // so worst case spans y=290-630 (4-px drop-shadow extends
487
+ // below). Short notes occupy less but stay centred at y=460.
488
+ function initOccupied() {
489
+ return [
490
+ { xMin: 32, yMin: 130, xMax: 508, yMax: 250 },
491
+ { xMin: 32, yMin: 290, xMax: 508, yMax: 630 },
492
+ ];
493
+ }
494
+ function overlapsAny(xMin, yMin, xMax, yMax, occupied, pad) {
495
+ pad = pad || 0;
496
+ for (const o of occupied) {
497
+ if (xMin < o.xMax + pad
498
+ && xMax > o.xMin - pad
499
+ && yMin < o.yMax + pad
500
+ && yMax > o.yMin - pad) {
501
+ return true;
502
+ }
503
+ }
504
+ return false;
505
+ }
506
+ function claim(occupied, xMin, yMin, xMax, yMax) {
507
+ occupied.push({ xMin, yMin, xMax, yMax });
508
+ }
509
+
510
+ function placeStones(rng, river, occupied) {
511
+ const out = [];
512
+ // Upper bank (above the river) keeps NO stones — the cream
513
+ // space between the black quote panel and the riverbank belongs
514
+ // to the content. All boulders now sit on the lower bank.
515
+ // Lower bank · 2-3 stones · medium / large only.
516
+ const lowerCount = betweenInt(rng, 2, 3);
517
+ for (let i = 0; i < lowerCount; i++) {
518
+ const slot = (i + 0.5) * (540 / lowerCount);
519
+ let x = Math.max(8, Math.min(490, slot + betweenInt(rng, -30, 30)));
520
+ const sizes = [stoneMed, stoneMed, stoneLarge];
521
+ const size = pick(rng, sizes);
522
+ const isLarge = size === stoneLarge;
523
+ const w = isLarge ? 46 : 32;
524
+ const h = isLarge ? 30 : 24;
525
+ const botAt = riverBotAt(x + w / 2, river);
526
+ let y = Math.min(770 - h - 6, botAt + betweenInt(rng, 4, 14));
527
+ for (let k = 0; k < 5 && overlapsAny(x, y, x + w, y + h, occupied, 4); k++) {
528
+ if (x > 270) x = Math.max(8, x - 40);
529
+ else x = Math.min(490, x + 40);
530
+ y = Math.min(770 - h - 6, riverBotAt(x + w / 2, river) + 6);
531
+ }
532
+ claim(occupied, x, y, x + w, y + h);
533
+ out.push(size(x, y));
534
+ }
535
+ return out.join("");
536
+ }
537
+
538
+ function placeFlowers(rng, river, occupied) {
539
+ const out = [];
540
+ // Count `[2, 5]` (was 4-8) · now that flowers are stone-sized
541
+ // the cream ground reads cleaner with fewer, larger blooms
542
+ // rather than a crowded daisy patch.
543
+ const total = betweenInt(rng, 2, 5);
544
+ let placed = 0;
545
+ for (let attempt = 0; attempt < total * 4 && placed < total; attempt++) {
546
+ const big = rng() < 0.5;
547
+ const fn = big ? flowerBig : flower;
548
+ const h = big ? 36 : 28;
549
+ const w = big ? 26 : 18;
550
+ const x = betweenInt(rng, 8, 540 - w - 8);
551
+ const palette = pick(rng, FLOWER_PALETTES);
552
+ // Flowers ONLY on the upper bank (between bubble bottom and
553
+ // river top). The lower bank (below the river) gets none —
554
+ // it looked busy and competed with trees / stones for the
555
+ // eye when the river curves low.
556
+ const topAt = riverTopAt(x + w / 2, river);
557
+ const y = Math.max(525, topAt - h - betweenInt(rng, 2, 12));
558
+ // Flowers are smaller than trees · accept some overlap with
559
+ // dirt (which gets placed later anyway) but skip if they'd
560
+ // step on the bubble / plaque / trees / stones.
561
+ if (overlapsAny(x, y, x + w, y + h, occupied, 2)) continue;
562
+ claim(occupied, x, y, x + w, y + h);
563
+ out.push(fn(x, y, palette));
564
+ placed++;
565
+ }
566
+ return out.join("");
567
+ }
568
+
569
+ // Trees · 1-3 per cover, scattered on the lower bank in front of
570
+ // the river. Now twice the size from the previous version so
571
+ // they read as proper canopy elements, not bushes. Placed FIRST
572
+ // among ground sprites so they claim space and downstream
573
+ // placements (stones, flowers, dirt) route around them.
574
+ function placeTrees(rng, river, occupied) {
575
+ const out = [];
576
+ // Bumped from 1-3 to 2-4 · with all stones gone from the upper
577
+ // bank, the lower band needs more foliage to keep the bottom
578
+ // half from reading empty.
579
+ const total = betweenInt(rng, 2, 4);
580
+ // Five candidate x slots spread evenly across the card width.
581
+ // Each is wide enough for the 56-px treeTall canopy plus a
582
+ // little breathing room.
583
+ const slots = [
584
+ { x: 20 }, { x: 130 }, { x: 240 }, { x: 350 }, { x: 470 },
585
+ ];
586
+ for (let i = slots.length - 1; i > 0; i--) {
587
+ const j = Math.floor(rng() * (i + 1));
588
+ const tmp = slots[i]; slots[i] = slots[j]; slots[j] = tmp;
589
+ }
590
+ let placed = 0;
591
+ for (let i = 0; i < slots.length && placed < total; i++) {
592
+ const slot = slots[i];
593
+ const tall = rng() < 0.4;
594
+ const fn = tall ? treeTall : treeSmall;
595
+ const w = tall ? 56 : 48;
596
+ const h = tall ? 98 : 74;
597
+ const x = Math.max(4, Math.min(540 - w - 4, slot.x + betweenInt(rng, -10, 10)));
598
+ const botAt = riverBotAt(x + w / 2, river);
599
+ // Plant the trunk just below the riverbank so the tree looks
600
+ // rooted in the cream ground. Clamp so the tree's foliage
601
+ // doesn't extend past the watermark band.
602
+ const y = Math.min(770 - h - 2, botAt + betweenInt(rng, 6, 18));
603
+ if (overlapsAny(x, y, x + w, y + h, occupied, 6)) continue;
604
+ claim(occupied, x, y, x + w, y + h);
605
+ out.push(fn(x, y));
606
+ placed++;
607
+ }
608
+ return out.join("");
609
+ }
610
+
611
+ function placeDirt(rng, river, dirtColors) {
612
+ const out = [];
613
+ // Slightly fewer but BIGGER patches so the cream ground doesn't
614
+ // feel sprinkled with tiny crumbs. 85 % chance of the big patch.
615
+ const total = betweenInt(rng, 3, 6);
616
+ for (let i = 0; i < total; i++) {
617
+ const x = betweenInt(rng, 8, 510);
618
+ const big = rng() < 0.85;
619
+ const fn = big ? dirtPatchBig : dirtPatch;
620
+ const height = big ? 8 : 6;
621
+ const onUpper = rng() < 0.5;
622
+ let y;
623
+ if (onUpper) {
624
+ const topAt = riverTopAt(x, river);
625
+ y = Math.max(525, topAt - height - betweenInt(rng, 4, 18));
626
+ } else {
627
+ const botAt = riverBotAt(x, river);
628
+ y = Math.min(770 - height - 4, botAt + betweenInt(rng, 6, 22));
629
+ }
630
+ out.push(fn(x, y, dirtColors));
631
+ }
632
+ return out.join("");
633
+ }
634
+
635
+ // ── Sky decorations ─────────────────────────────────────────
636
+ // Moon stays at the same upper-right anchor (clears the
637
+ // "Privateboard.ai" header text). Stars wander but explicitly
638
+ // dodge the header text-bounds. Clouds float in the middle bands.
639
+ function placeSkyDeco(rng) {
640
+ const out = [];
641
+ out.push(moon(440, 78));
642
+
643
+ const starCount = betweenInt(rng, 12, 20);
644
+ for (let i = 0; i < starCount; i++) {
645
+ let x = betweenInt(rng, 20, 510);
646
+ let y = betweenInt(rng, 20, 180);
647
+ // Header text band (top-right "Privateboard.ai") · push y down
648
+ // a bit if the random landed inside it.
649
+ if (y >= 22 && y <= 34 && x >= 400 && x <= 512) {
650
+ y = betweenInt(rng, 44, 180);
651
+ }
652
+ // Skip stars that landed inside the moon's bounding box
653
+ // (40 px right inset, y=78-108).
654
+ if (x >= 432 && x <= 480 && y >= 76 && y <= 112) continue;
655
+ out.push(rng() < 0.55 ? star(x, y) : tinyStar(x, y));
656
+ }
657
+ const cloudCount = betweenInt(rng, 2, 4);
658
+ for (let i = 0; i < cloudCount; i++) {
659
+ const x = betweenInt(rng, 40, 480);
660
+ const y = betweenInt(rng, 130, 195);
661
+ out.push(rng() < 0.5 ? bigCloud(x, y) : cloud(x, y));
662
+ }
663
+ return out.join("");
664
+ }
665
+
666
+ // ── Public API ──────────────────────────────────────────────
667
+ function generate(opts) {
668
+ opts = opts || {};
669
+ const seed = opts.seed != null
670
+ ? (opts.seed | 0) || 1
671
+ : ((Date.now() ^ ((Math.random() * 0x7FFFFFFF) | 0)) | 0) || 1;
672
+ const rng = makeRng(seed);
673
+
674
+ const sky = pick(rng, SKY_PALETTES);
675
+ const ground = pick(rng, GROUND_MATERIALS);
676
+
677
+ // Bottom two sky-gradient stops blend into the ground material's
678
+ // base color so the horizon transitions smoothly into the
679
+ // chosen ground.
680
+ const adjustedStops = sky.stops.slice();
681
+ adjustedStops[8] = ground.base;
682
+ adjustedStops[7] = ground.base;
683
+ const skyGradient = gradientFromStops(adjustedStops);
684
+
685
+ // Pick a watermark/stamp foreground color that stays legible
686
+ // against whichever ground material rolled. Tiny luminance
687
+ // check on the base color · dark grounds (volcanic, etc.) get
688
+ // a warm cream foreground; light grounds (sand / snow /
689
+ // lavender / moss / desert) stay with the dark purple. The
690
+ // renderer emits this as a CSS variable on the card so the
691
+ // watermark + stamp rules can pick it up.
692
+ const groundFg = (function () {
693
+ const hex = ground.base.replace("#", "");
694
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
695
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
696
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
697
+ const lum = 0.299 * r + 0.587 * g + 0.114 * b;
698
+ return lum < 0.5 ? "#F4D898" : "#4A2070";
699
+ })();
700
+
701
+ const river = generateRiverPath(rng);
702
+
703
+ const skyDeco = placeSkyDeco(rng);
704
+ // Ground composition: the river is the only foreground element
705
+ // left. Trees, stones, flowers, and dirt patches were all
706
+ // removed — the lower band reads cleaner as pure cream ground +
707
+ // a single curving river, with the ASCII PRIVATE BOARD logo and
708
+ // the black quote panel carrying the composition above.
709
+ const groundDeco = river.svg;
710
+
711
+ // Per-roll ASCII-logo TYPEFACE · the renderer ships three
712
+ // pre-baked "PRIVATE BOARD" letter-form designs, all drawn with
713
+ // the same FULL BLOCK char (█) but at different widths /
714
+ // proportions. Texture (glyph + colour + shadow) stays fixed
715
+ // across rolls; only the letter shapes change.
716
+ // · block — 4-cell-wide letters, 5-row body (the original)
717
+ // · slim — 3-cell-wide letters, sleek/elegant rhythm
718
+ // · thick — 5-cell-wide letters, chunky/imposing rhythm
719
+ const logoFont = pick(rng, ["block", "slim", "thick"]);
720
+
721
+ return {
722
+ seed,
723
+ skyGradient,
724
+ skyDeco,
725
+ groundDeco,
726
+ groundFg,
727
+ logoFont,
728
+ palette: { skyName: sky.name, groundName: ground.name },
729
+ };
730
+ }
731
+
732
+ // Expose on `window`. App.js calls `window.shareCoverSvgCreator?.generate()`
733
+ // once per `openShareCard()` and caches the result across template
734
+ // chip-switches in the same modal session.
735
+ window.shareCoverSvgCreator = { generate };
736
+ })();