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.
- package/dist/cli.js +2623 -333
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +6 -6
- package/public/agent-build-bgm.js +292 -0
- package/public/agent-overlay.css +14 -14
- package/public/agent-profile.css +408 -87
- package/public/agent-profile.js +254 -0
- package/public/app.js +2486 -384
- package/public/home.html +26 -26
- package/public/i18n.js +1890 -21
- package/public/icons/logo2.png +0 -0
- package/public/icons/private-board-vi.html +1716 -0
- package/public/index.html +2954 -1018
- package/public/magazine.html +12 -12
- package/public/new-agent.css +29 -29
- package/public/newspaper.html +20 -20
- package/public/onboarding.css +350 -272
- package/public/onboarding.js +614 -323
- package/public/quote-cta.css +4 -4
- package/public/report.html +2008 -1673
- package/public/room-settings.css +192 -24
- package/public/room-settings.js +5 -0
- package/public/share-cover-svg-creator.js +736 -0
- package/public/themes.css +0 -34
- package/public/typing-sfx.js +176 -3
- package/public/user-settings.css +50 -27
- package/public/user-settings.js +43 -14
- package/public/voice-onboarding.css +425 -0
- package/public/voice-onboarding.js +144 -0
- package/public/voice-replay.css +31 -38
- package/public/voice-replay.js +12 -11
|
@@ -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
|
+
})();
|