impact-ui 5.0.1-beta.1 → 5.0.1-beta.2

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.
Files changed (65) hide show
  1. package/dist/assets/halloween_devil_morning_final.html.js +1304 -0
  2. package/dist/assets/halloween_devil_night_Final.html.js +1299 -0
  3. package/dist/components/HalloweenStyleIntro/HalloweenStyleIntroProvider.d.ts.map +1 -1
  4. package/dist/components/HalloweenStyleIntro/HalloweenStyleIntroProvider.js +23 -4
  5. package/dist/components/HalloweenStyleIntro/halloweenIntro.constants.d.ts +5 -0
  6. package/dist/components/HalloweenStyleIntro/halloweenIntro.constants.d.ts.map +1 -1
  7. package/dist/components/HalloweenStyleIntro/halloweenIntro.constants.js +5 -0
  8. package/dist/components/HalloweenStyleIntro/halloweenIntroHtml.d.ts +10 -3
  9. package/dist/components/HalloweenStyleIntro/halloweenIntroHtml.d.ts.map +1 -1
  10. package/dist/components/HalloweenStyleIntro/halloweenIntroHtml.js +129 -9
  11. package/dist/components/HalloweenStyleIntro/halloweenIntroHtml.test.d.ts +2 -0
  12. package/dist/components/HalloweenStyleIntro/halloweenIntroHtml.test.d.ts.map +1 -0
  13. package/dist/components/Header/Header.types.d.ts +5 -0
  14. package/dist/components/Header/Header.types.d.ts.map +1 -1
  15. package/dist/components/Header/index.d.ts +1 -1
  16. package/dist/components/Header/index.d.ts.map +1 -1
  17. package/dist/components/Header/index.js +4 -0
  18. package/dist/components/Input/index.js +1 -1
  19. package/dist/components/Notification/NotificationBody/NotificationList/NotificationChipsList.d.ts.map +1 -1
  20. package/dist/components/Notification/NotificationBody/NotificationList/NotificationChipsList.js +3 -1
  21. package/dist/components/Notification/NotificationBody/NotificationList/NotificationList.js +1 -1
  22. package/dist/components/Notification/NotificationBody/NotificationTaskList.js +4 -2
  23. package/dist/components/Notification/index.js +1 -1
  24. package/dist/components/ProfileMenu/ProfileMenu.types.d.ts +5 -0
  25. package/dist/components/ProfileMenu/ProfileMenu.types.d.ts.map +1 -1
  26. package/dist/components/ProfileMenu/index.d.ts +1 -1
  27. package/dist/components/ProfileMenu/index.d.ts.map +1 -1
  28. package/dist/components/ProfileMenu/index.js +5 -1
  29. package/dist/components/TableV36/index.d.ts.map +1 -1
  30. package/dist/components/TableV36/index.js +8 -1
  31. package/dist/components/ThemingPanel/StylesSection.d.ts.map +1 -1
  32. package/dist/components/ThemingPanel/StylesSection.js +5 -4
  33. package/dist/components/ThemingPanel/ThemingPanel.d.ts +1 -1
  34. package/dist/components/ThemingPanel/ThemingPanel.d.ts.map +1 -1
  35. package/dist/components/ThemingPanel/ThemingPanel.js +24 -11
  36. package/dist/components/ThemingPanel/ThemingPanel.types.d.ts +5 -3
  37. package/dist/components/ThemingPanel/ThemingPanel.types.d.ts.map +1 -1
  38. package/dist/components/ThemingPanel/themingPanel.constants.d.ts +4 -0
  39. package/dist/components/ThemingPanel/themingPanel.constants.d.ts.map +1 -1
  40. package/dist/components/ThemingPanel/themingPanel.constants.js +11 -3
  41. package/dist/locales/de-DE.json.d.ts +4 -1
  42. package/dist/locales/de-DE.json.js +4 -1
  43. package/dist/locales/en-US.json.d.ts +4 -1
  44. package/dist/locales/en-US.json.js +4 -1
  45. package/dist/locales/es-ES.json.d.ts +4 -1
  46. package/dist/locales/es-ES.json.js +4 -1
  47. package/dist/mcp-component-registry.json +14 -53
  48. package/dist/stories/designTokensStoryHelpers.d.ts.map +1 -1
  49. package/dist/tokens/ImpactUiThemeProvider.js +2 -2
  50. package/dist/tokens/impactUiPreferences.d.ts +23 -0
  51. package/dist/tokens/impactUiPreferences.d.ts.map +1 -0
  52. package/dist/tokens/impactUiPreferences.js +92 -0
  53. package/dist/tokens/impactUiPreferences.test.d.ts +2 -0
  54. package/dist/tokens/impactUiPreferences.test.d.ts.map +1 -0
  55. package/dist/tokens/index.d.ts +1 -0
  56. package/dist/tokens/index.d.ts.map +1 -1
  57. package/dist/tokens/interfaceStyleDom.d.ts +7 -2
  58. package/dist/tokens/interfaceStyleDom.d.ts.map +1 -1
  59. package/dist/tokens/interfaceStyleDom.js +4 -10
  60. package/dist/tokens/themeDom.d.ts +4 -1
  61. package/dist/tokens/themeDom.d.ts.map +1 -1
  62. package/dist/tokens/themeDom.js +4 -10
  63. package/package.json +1 -1
  64. package/dist/assets/halloween_candle_light.html.js +0 -2320
  65. package/dist/assets/halloween_night.html.js +0 -2307
@@ -1,2320 +0,0 @@
1
- const halloweenCandleLightHtml = `<!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
- <title>The Last Candle</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Creepster&display=swap" />
10
- <style>
11
- html,
12
- body {
13
- margin: 0;
14
- height: 100%;
15
- background: #000;
16
- overflow: hidden;
17
- }
18
- canvas {
19
- display: block;
20
- }
21
- #hint {
22
- position: fixed;
23
- inset: 0;
24
- display: grid;
25
- place-items: center;
26
- margin: 0;
27
- pointer-events: none;
28
- user-select: none;
29
- font:
30
- 400 15px/1.5 'Iowan Old Style',
31
- 'Palatino Linotype',
32
- Palatino,
33
- Georgia,
34
- serif;
35
- letter-spacing: 0.05em;
36
- color: #7f766a;
37
- transition: opacity 1.4s ease;
38
- }
39
- #hint.gone {
40
- opacity: 0;
41
- }
42
- </style>
43
- <script type="importmap">
44
- {
45
- "imports": {
46
- "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
47
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
48
- }
49
- }
50
- <\/script>
51
- </head>
52
- <body>
53
- <p id="hint">Best experienced with sound.</p>
54
-
55
- <script type="module">
56
- import * as THREE from 'three';
57
- import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
58
- import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
59
- import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
60
- import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';
61
- import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
62
- import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
63
- import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
64
- import { mergeGeometries, mergeVertices } from 'three/addons/utils/BufferGeometryUtils.js';
65
-
66
- /* ==========================================================================
67
- THE LAST CANDLE
68
- An empty graveyard at night. The camera makes one slow push down the aisle
69
- while the low morning sun, haze and carved pumpkins fade up from black; at the end
70
- every light goes out at once.
71
-
72
- Timeline (seconds)
73
- 0.0 - 0.8 pitch black, every light at 0
74
- 0.8 - 6.5 morning sun, sky, haze and environment fade up; pumpkins kindle
75
- 10 - 15.5 the pumpkins gutter
76
- 2.5 - 7.5 the stone monument at the end of the aisle wakes: its edges and
77
- veins glow blue to violet; a witch's hat hovers above it
78
- 9.8 - 16.9 "Happy Halloween" burns in at the top-left corner: fire-lit letters,
79
- a hot igniting edge, heat shimmer and rising embers
80
- 1.0 - 16.9 a small "Applying Style...." status line at the bottom centre, dots loading
81
- 16.9 every light hits exactly 0
82
- 16.9 - 20.0 pitch black
83
- ========================================================================== */
84
-
85
- const RENDER = new URLSearchParams(location.search).has('render');
86
- const EMBED = new URLSearchParams(location.search).has('embed');
87
- const T = {
88
- revealIn: 0.8,
89
- revealFull: 6.5,
90
- devilGo: 2.2,
91
- devilArrive: 14.4,
92
- lift0: 15.0,
93
- lift1: 16.1,
94
- blow0: 16.25,
95
- out: 16.9,
96
- end: 20.0,
97
- loop: 22.0,
98
- };
99
- const Z_START = -31.0,
100
- Z_END = 0.4; // the point the camera looks at and focuses on
101
- const CAM_Z0 = 9.0,
102
- CAM_Z1 = 3.4; // dolly path
103
- const CAM_Y0 = 1.74,
104
- CAM_Y1 = 1.45;
105
- const FOG_DENSITY = 0.026; // light morning haze
106
- // light levels at full reveal (physical units: lux for the sun, candela for point lights)
107
- const K = {
108
- sun: 3.8,
109
- hemi: 1.05,
110
- env: 0.4,
111
- pump: 0.42,
112
- glow: 1.6,
113
- ap0: 0.00025,
114
- ap1: 0.0013,
115
- };
116
-
117
- /* ---------- deterministic randomness ---------- */
118
- function mulberry32(a) {
119
- return function () {
120
- a |= 0;
121
- a = (a + 0x6d2b79f5) | 0;
122
- let t = Math.imul(a ^ (a >>> 15), 1 | a);
123
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
124
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
125
- };
126
- }
127
- const rng = mulberry32(31102026);
128
- const rand = (a = 0, b = 1) => a + (b - a) * rng();
129
- function hash1(n) {
130
- const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
131
- return x - Math.floor(x);
132
- }
133
- function noise1(x) {
134
- const i = Math.floor(x),
135
- f = x - i,
136
- u = f * f * (3 - 2 * f);
137
- return hash1(i) * (1 - u) + hash1(i + 1) * u;
138
- }
139
- function fbm1(x) {
140
- return 0.5 * noise1(x) + 0.3 * noise1(x * 2.03 + 7.1) + 0.2 * noise1(x * 4.11 + 3.3);
141
- }
142
-
143
- /* ---------- easing ---------- */
144
- const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
145
- const lerp = (a, b, t) => a + (b - a) * t;
146
- const smooth = (e0, e1, x) => {
147
- const t = clamp((x - e0) / (e1 - e0));
148
- return t * t * (3 - 2 * t);
149
- };
150
- const smoother = (e0, e1, x) => {
151
- const t = clamp((x - e0) / (e1 - e0));
152
- return t * t * t * (t * (t * 6 - 15) + 10);
153
- };
154
- // long-tailed ease: very gentle start and an extremely soft landing
155
- function easeLong(u) {
156
- u = clamp(u);
157
- const p = 2.3,
158
- a = Math.pow(u, p),
159
- b = Math.pow(1 - u, p);
160
- return a / (a + b);
161
- }
162
- // glide profile: speed ~ u^1.6 (1-u)^2.6, integrated once (a drifting body that bleeds speed slowly)
163
- const TRAVEL = (() => {
164
- const N = 2048,
165
- arr = new Float32Array(N + 1);
166
- let s = 0;
167
- for (let i = 1; i <= N; i++) {
168
- const u = (i - 0.5) / N;
169
- s += Math.pow(u, 1.6) * Math.pow(1 - u, 2.6);
170
- arr[i] = s;
171
- }
172
- for (let i = 0; i <= N; i++) arr[i] /= s;
173
- return arr;
174
- })();
175
- function travel(u) {
176
- u = clamp(u);
177
- const f = u * 2048,
178
- i = Math.min(2047, Math.floor(f));
179
- return lerp(TRAVEL[i], TRAVEL[i + 1], f - i);
180
- }
181
- function devilZ(t) {
182
- return lerp(Z_START, Z_END, travel((t - T.devilGo) / (T.devilArrive - T.devilGo)));
183
- }
184
-
185
- /* ---------- sound: UI-style, procedural, scheduled on the same timeline ----------
186
- 0.8 - 16.9 a very quiet cinematic bed: low drone, dark pad, soft wind
187
- 0.8 - 16.9 slight wind, gusting and drifting across the speakers
188
- 1.5 a ghostly, theremin-like wail drifts across
189
- 8.0 the wail again, drifting back the other way
190
- 5.2, 10.6, 15.0 bats flit past, squeaking softly
191
- 11.8 a wolf howls far off to the right, very quietly
192
- 16.9 power-off blip, then silence
193
- ------------------------------------------------------------------------- */
194
- const sndCache = new WeakMap();
195
- function noiseBuf(ctx, seconds, kind, seed) {
196
- const sr = ctx.sampleRate,
197
- fade = Math.floor(0.05 * sr),
198
- n = Math.floor(seconds * sr) + fade,
199
- b = ctx.createBuffer(2, n, sr);
200
- for (let ch = 0; ch < 2; ch++) {
201
- const r = mulberry32(seed + ch * 7919),
202
- d = b.getChannelData(ch);
203
- let b0 = 0,
204
- b1 = 0,
205
- b2 = 0,
206
- last = 0;
207
- for (let i = 0; i < n; i++) {
208
- const w = r() * 2 - 1;
209
- if (kind === 'white') d[i] = w * 0.7;
210
- else if (kind === 'pink') {
211
- b0 = 0.99765 * b0 + w * 0.099046;
212
- b1 = 0.963 * b1 + w * 0.2965164;
213
- b2 = 0.57 * b2 + w * 1.0526913;
214
- d[i] = (b0 + b1 + b2 + w * 0.1848) * 0.2;
215
- } else {
216
- last = (last + 0.02 * w) / 1.02;
217
- d[i] = last * 3.2;
218
- }
219
- }
220
- // seamless loop: the head fades in from the tail that will precede it
221
- for (let i = 0; i < fade; i++) {
222
- const k = i / fade;
223
- d[i] = d[i] * k + d[n - fade + i] * (1 - k);
224
- }
225
- }
226
- b.__loopEnd = (n - fade) / sr;
227
- return b;
228
- }
229
- function impulseBuf(ctx, seconds, seed) {
230
- const sr = ctx.sampleRate,
231
- n = Math.floor(seconds * sr),
232
- b = ctx.createBuffer(2, n, sr),
233
- pre = Math.floor(0.018 * sr);
234
- for (let ch = 0; ch < 2; ch++) {
235
- const r = mulberry32(seed + ch * 31),
236
- d = b.getChannelData(ch);
237
- let lp = 0;
238
- for (let i = 0; i < n; i++) {
239
- const x = i / n,
240
- w = r() * 2 - 1;
241
- lp += (w - lp) * (0.6 - 0.5 * x);
242
- d[i] = i < pre ? 0 : lp * Math.pow(1 - x, 1.6) * Math.exp(-3.0 * x);
243
- }
244
- }
245
- return b;
246
- }
247
- function buildSound(ctx, t0, dest) {
248
- const S =
249
- sndCache.get(ctx) ||
250
- (() => {
251
- const c = {
252
- white: noiseBuf(ctx, 3, 'white', 101),
253
- brown: noiseBuf(ctx, 8, 'brown', 303),
254
- ir: impulseBuf(ctx, 1.1, 404),
255
- };
256
- sndCache.set(ctx, c);
257
- return c;
258
- })();
259
- const master = ctx.createGain();
260
- master.gain.value = 0;
261
- master.gain.setValueAtTime(2.2, t0);
262
- master.gain.setValueAtTime(2.2, t0 + T.out + 0.22);
263
- master.gain.linearRampToValueAtTime(0, t0 + T.out + 0.25);
264
- const comp = ctx.createDynamicsCompressor();
265
- comp.threshold.value = -14;
266
- comp.ratio.value = 3;
267
- comp.attack.value = 0.004;
268
- comp.release.value = 0.2;
269
- master.connect(comp);
270
- comp.connect(dest);
271
- const verb = ctx.createConvolver();
272
- verb.buffer = S.ir;
273
- const verbOut = ctx.createGain();
274
- verbOut.gain.value = 0.18;
275
- verb.connect(verbOut).connect(master);
276
- const bus = ctx.createGain();
277
- bus.connect(master);
278
- bus.connect(verb);
279
-
280
- const tone = (t, f0, f1, dur, type = 'sine', amp = 0.2, pan = 0, attack = 0.004) => {
281
- const o = ctx.createOscillator();
282
- o.type = type;
283
- o.frequency.setValueAtTime(f0, t0 + t);
284
- if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(f1, t0 + t + dur);
285
- const g = ctx.createGain();
286
- g.gain.value = 0;
287
- g.gain.setValueAtTime(0, t0 + t);
288
- g.gain.linearRampToValueAtTime(amp, t0 + t + attack);
289
- g.gain.exponentialRampToValueAtTime(0.0001, t0 + t + dur);
290
- const p = ctx.createStereoPanner();
291
- p.pan.value = pan;
292
- o.connect(g).connect(p).connect(bus);
293
- o.start(t0 + t);
294
- o.stop(t0 + t + dur + 0.02);
295
- };
296
- const whoosh = (t, dur, fA, fB, amp, pan0 = 0, pan1 = 0) => {
297
- const s = ctx.createBufferSource();
298
- s.buffer = S.white;
299
- s.loop = true;
300
- s.loopEnd = S.white.__loopEnd;
301
- const bp = ctx.createBiquadFilter();
302
- bp.type = 'bandpass';
303
- bp.Q.value = 1.2;
304
- bp.frequency.setValueAtTime(fA, t0 + t);
305
- bp.frequency.exponentialRampToValueAtTime(fB, t0 + t + dur);
306
- const g = ctx.createGain();
307
- g.gain.value = 0;
308
- g.gain.setValueAtTime(0, t0 + t);
309
- g.gain.linearRampToValueAtTime(amp, t0 + t + dur * 0.45);
310
- g.gain.exponentialRampToValueAtTime(0.0001, t0 + t + dur);
311
- const p = ctx.createStereoPanner();
312
- p.pan.setValueAtTime(pan0, t0 + t);
313
- p.pan.linearRampToValueAtTime(pan1, t0 + t + dur);
314
- s.connect(bp).connect(g).connect(p).connect(bus);
315
- s.start(t0 + t, 0);
316
- s.stop(t0 + t + dur + 0.02);
317
- };
318
-
319
- // cinematic bed, very low: fades up with the picture, swells slowly, cuts with the lights
320
- {
321
- const bed = ctx.createGain();
322
- bed.connect(bus);
323
- const lvl = (t) => smoother(0.8, 6.0, t) * (0.75 + 0.25 * smoother(8, 15.5, t));
324
- const n = Math.ceil(T.out * 30),
325
- env = new Float32Array(n);
326
- for (let i = 0; i < n; i++) env[i] = 0.1 * lvl((T.out * i) / (n - 1)); // 0.1 keeps it far under everything else
327
- bed.gain.value = 0;
328
- bed.gain.setValueCurveAtTime(env, t0, T.out - 0.02);
329
- bed.gain.linearRampToValueAtTime(0, t0 + T.out);
330
- const stopAt = t0 + T.out + 0.05;
331
- const osc = (type, f, det, amp, dest) => {
332
- const o = ctx.createOscillator();
333
- o.type = type;
334
- o.frequency.value = f;
335
- o.detune.value = det;
336
- const g = ctx.createGain();
337
- g.gain.value = amp;
338
- o.connect(g).connect(dest);
339
- o.start(t0);
340
- o.stop(stopAt);
341
- };
342
- // low drone (A and E)
343
- const droneLP = ctx.createBiquadFilter();
344
- droneLP.type = 'lowpass';
345
- droneLP.frequency.value = 320;
346
- droneLP.connect(bed);
347
- osc('sine', 55, 0, 0.05, droneLP);
348
- osc('sine', 55.3, 0, 0.04, droneLP);
349
- osc('triangle', 82.41, 0, 0.02, droneLP);
350
- osc('triangle', 110, 3, 0.012, droneLP);
351
- // dark A-minor pad that slowly opens
352
- const padLP = ctx.createBiquadFilter();
353
- padLP.type = 'lowpass';
354
- padLP.Q.value = 0.8;
355
- padLP.connect(bed);
356
- const m = Math.ceil(T.out * 20),
357
- cut = new Float32Array(m);
358
- for (let i = 0; i < m; i++) {
359
- const t = (T.out * i) / (m - 1);
360
- cut[i] = 380 + 520 * smoother(4, 15.5, t) + 60 * Math.sin(t * 0.7);
361
- }
362
- padLP.frequency.value = cut[0];
363
- padLP.frequency.setValueCurveAtTime(cut, t0, T.out);
364
- [
365
- [110, -6],
366
- [110, 6],
367
- [130.81, -5],
368
- [130.81, 5],
369
- [164.81, -4],
370
- [164.81, 4],
371
- ].forEach(([f, d]) => osc('sawtooth', f, d, 0.006, padLP));
372
- // soft wind
373
- const wind = ctx.createBufferSource();
374
- wind.buffer = S.brown;
375
- wind.loop = true;
376
- wind.loopEnd = S.brown.__loopEnd;
377
- const windLP = ctx.createBiquadFilter();
378
- windLP.type = 'lowpass';
379
- windLP.Q.value = 0.5;
380
- const wc = new Float32Array(m),
381
- wg = new Float32Array(m);
382
- for (let i = 0; i < m; i++) {
383
- const t = (T.out * i) / (m - 1);
384
- wc[i] = 260 + 380 * fbm1(t * 0.4 + 3.1);
385
- wg[i] = 0.05 * (0.6 + 0.8 * (fbm1(t * 0.45 + 7.3) - 0.3));
386
- }
387
- windLP.frequency.value = wc[0];
388
- windLP.frequency.setValueCurveAtTime(wc, t0, T.out);
389
- const windG = ctx.createGain();
390
- windG.gain.value = wg[0];
391
- windG.gain.setValueCurveAtTime(wg, t0, T.out);
392
- wind.connect(windLP).connect(windG).connect(bed);
393
- wind.start(t0, 1.3);
394
- wind.stop(stopAt);
395
- }
396
- // slight wind in the background: a low rush plus an airy gusting top, drifting left and right
397
- {
398
- const WIND = 0.07; // overall level: slight
399
- const m = Math.ceil(T.out * 30),
400
- at = (i) => (T.out * i) / (m - 1);
401
- const curve = (fn) => {
402
- const c = new Float32Array(m);
403
- for (let i = 0; i < m; i++) c[i] = fn(at(i));
404
- return c;
405
- };
406
- const gust = (t) => 0.55 + 0.75 * Math.max(0, fbm1(t * 0.5 + 11.7) - 0.25);
407
- const level = (t) => smoother(0.8, 5.0, t) * gust(t);
408
- const out = ctx.createGain();
409
- out.gain.value = 0;
410
- out.gain.setValueCurveAtTime(
411
- curve((t) => WIND * level(t)),
412
- t0,
413
- T.out - 0.02
414
- );
415
- out.gain.linearRampToValueAtTime(0, t0 + T.out);
416
- const pan = ctx.createStereoPanner();
417
- pan.pan.value = 0;
418
- pan.pan.setValueCurveAtTime(
419
- curve((t) => 0.55 * Math.sin(t * 0.33 + 0.8)),
420
- t0,
421
- T.out
422
- );
423
- out.connect(pan).connect(bus);
424
- const src = (buf, offset) => {
425
- const s = ctx.createBufferSource();
426
- s.buffer = buf;
427
- s.loop = true;
428
- s.loopEnd = buf.__loopEnd;
429
- s.start(t0, offset);
430
- s.stop(t0 + T.out + 0.05);
431
- return s;
432
- };
433
- // low rush
434
- const lp = ctx.createBiquadFilter();
435
- lp.type = 'lowpass';
436
- lp.Q.value = 0.6;
437
- lp.frequency.value = 500;
438
- lp.frequency.setValueCurveAtTime(
439
- curve((t) => 380 + 520 * fbm1(t * 0.5 + 11.7)),
440
- t0,
441
- T.out
442
- );
443
- src(S.brown, 4.2).connect(lp).connect(out);
444
- // airy, whistling top that rises with each gust
445
- const bp = ctx.createBiquadFilter();
446
- bp.type = 'bandpass';
447
- bp.Q.value = 1.6;
448
- bp.frequency.value = 900;
449
- bp.frequency.setValueCurveAtTime(
450
- curve((t) => 700 + 900 * fbm1(t * 0.5 + 11.7) + 150 * Math.sin(t * 0.9)),
451
- t0,
452
- T.out
453
- );
454
- const airG = ctx.createGain();
455
- airG.gain.value = 0.35;
456
- src(S.white, 1.1).connect(bp).connect(airG).connect(out);
457
- }
458
- // bats: a few quiet passes of high squeaks and soft wing flutter, flying across the stereo field
459
- const batPass = (tStart, dur, panFrom, panTo, seed, amp) => {
460
- const R = mulberry32(seed);
461
- for (let t = tStart + 0.05; t < tStart + dur - 0.05;) {
462
- const u = (t - tStart) / dur,
463
- near = Math.sin(Math.PI * u); // louder as it passes closest
464
- const pan = lerp(panFrom, panTo, u),
465
- f0 = 7000 + R() * 2500;
466
- tone(
467
- t,
468
- f0,
469
- f0 * (0.45 + R() * 0.1),
470
- 0.018 + R() * 0.02,
471
- 'sine',
472
- amp * (0.4 + 0.6 * near) * (0.6 + 0.4 * R()),
473
- pan,
474
- 0.002
475
- );
476
- t += R() < 0.3 ? 0.035 + R() * 0.03 : 0.09 + R() * 0.16; // short clusters, then gaps
477
- }
478
- const n = ctx.createBufferSource();
479
- n.buffer = S.white;
480
- n.loop = true;
481
- n.loopEnd = S.white.__loopEnd;
482
- const bp = ctx.createBiquadFilter();
483
- bp.type = 'bandpass';
484
- bp.frequency.value = 900;
485
- bp.Q.value = 0.9;
486
- const g = ctx.createGain(),
487
- k = Math.ceil(dur * 200),
488
- env = new Float32Array(k);
489
- for (let i = 0; i < k; i++) {
490
- const u = i / (k - 1);
491
- env[i] =
492
- amp *
493
- 0.9 *
494
- Math.sin(Math.PI * u) *
495
- Math.pow(0.5 + 0.5 * Math.sin(u * dur * 2 * Math.PI * 11), 3);
496
- }
497
- g.gain.value = 0;
498
- g.gain.setValueCurveAtTime(env, t0 + tStart, dur);
499
- const pn = ctx.createStereoPanner();
500
- pn.pan.setValueAtTime(panFrom, t0 + tStart);
501
- pn.pan.linearRampToValueAtTime(panTo, t0 + tStart + dur);
502
- n.connect(bp).connect(g).connect(pn).connect(bus);
503
- n.start(t0 + tStart, R() * 2);
504
- n.stop(t0 + tStart + dur + 0.02);
505
- };
506
- batPass(5.2, 1.6, -0.8, 0.8, 4101, 0.035);
507
- batPass(10.6, 1.1, 0.8, -0.5, 4202, 0.03);
508
- batPass(15.0, 1.3, -0.6, 0.9, 4303, 0.028);
509
- // a voice whose pitch and level follow curves over its life
510
- const voiceCurve = (t, dur, freqFn, ampFn, type = 'sine', pan = 0, lpf = 4000) => {
511
- const n = Math.max(8, Math.ceil(dur * 120)),
512
- fq = new Float32Array(n),
513
- am = new Float32Array(n);
514
- for (let i = 0; i < n; i++) {
515
- const u = i / (n - 1);
516
- fq[i] = freqFn(u);
517
- am[i] = ampFn(u);
518
- }
519
- am[n - 1] = 0;
520
- const o = ctx.createOscillator();
521
- o.type = type;
522
- o.frequency.value = fq[0];
523
- o.frequency.setValueCurveAtTime(fq, t0 + t, dur);
524
- const g = ctx.createGain();
525
- g.gain.value = 0;
526
- g.gain.setValueCurveAtTime(am, t0 + t, dur);
527
- const lp = ctx.createBiquadFilter();
528
- lp.type = 'lowpass';
529
- lp.frequency.value = lpf;
530
- const p = ctx.createStereoPanner();
531
- if (Array.isArray(pan)) {
532
- p.pan.setValueAtTime(pan[0], t0 + t);
533
- p.pan.linearRampToValueAtTime(pan[1], t0 + t + dur);
534
- } else p.pan.value = pan;
535
- o.connect(lp).connect(g).connect(p).connect(bus);
536
- o.start(t0 + t);
537
- o.stop(t0 + t + dur + 0.02);
538
- };
539
- // ghostly wail: theremin-like rise and fall with vibrato, drifting across, low
540
- const wail = (t, pan) => {
541
- const vib = (u) => 1 + 0.03 * Math.sin(u * 2.3 * 2 * Math.PI * 5.5),
542
- shape = (u) => Math.sin(Math.PI * Math.min(1, u * 1.3));
543
- voiceCurve(
544
- t,
545
- 2.3,
546
- (u) => (520 + 380 * shape(u)) * vib(u),
547
- (u) => 0.045 * Math.sin(Math.PI * u),
548
- 'sine',
549
- pan,
550
- 3000
551
- );
552
- voiceCurve(
553
- t,
554
- 2.3,
555
- (u) => (260 + 190 * shape(u)) * vib(u),
556
- (u) => 0.018 * Math.sin(Math.PI * u),
557
- 'triangle',
558
- pan,
559
- 1500
560
- );
561
- };
562
- wail(1.5, [0.7, -0.7]); // as the graveyard fades up, right to left
563
- wail(8.0, [-0.7, 0.7]); // again in the middle, left to right
564
- // wolf howl, far off to the right, very low
565
- voiceCurve(
566
- 11.8,
567
- 2.4,
568
- (u) =>
569
- (u < 0.25 ? 300 + 320 * (u / 0.25) : u < 0.7 ? 620 : 620 - 270 * ((u - 0.7) / 0.3)) *
570
- (1 + 0.012 * Math.sin(u * 2.4 * 2 * Math.PI * 6)),
571
- (u) => 0.025 * Math.min(1, u * 5) * Math.min(1, (1 - u) * 4),
572
- 'sine',
573
- 0.75,
574
- 1600
575
- );
576
- // lights out: power off
577
- tone(T.out, 880, 90, 0.22, 'sine', 0.2);
578
- tone(T.out, 1760, 180, 0.12, 'triangle', 0.06);
579
- tone(T.out, 4000, 4000, 0.008, 'square', 0.03);
580
- }
581
- function wavBase64(buf) {
582
- const ch = buf.numberOfChannels,
583
- n = buf.length,
584
- sr = buf.sampleRate,
585
- bytes = new Uint8Array(44 + n * ch * 2),
586
- v = new DataView(bytes.buffer);
587
- const str = (o, s) => {
588
- for (let i = 0; i < s.length; i++) v.setUint8(o + i, s.charCodeAt(i));
589
- };
590
- str(0, 'RIFF');
591
- v.setUint32(4, 36 + n * ch * 2, true);
592
- str(8, 'WAVE');
593
- str(12, 'fmt ');
594
- v.setUint32(16, 16, true);
595
- v.setUint16(20, 1, true);
596
- v.setUint16(22, ch, true);
597
- v.setUint32(24, sr, true);
598
- v.setUint32(28, sr * ch * 2, true);
599
- v.setUint16(32, ch * 2, true);
600
- v.setUint16(34, 16, true);
601
- str(36, 'data');
602
- v.setUint32(40, n * ch * 2, true);
603
- const data = [];
604
- for (let c = 0; c < ch; c++) data.push(buf.getChannelData(c));
605
- let o = 44;
606
- for (let i = 0; i < n; i++)
607
- for (let c = 0; c < ch; c++) {
608
- const s = Math.max(-1, Math.min(1, data[c][i]));
609
- v.setInt16(o, s < 0 ? s * 0x8000 : s * 0x7fff, true);
610
- o += 2;
611
- }
612
- let bin = '';
613
- for (let i = 0; i < bytes.length; i += 0x8000)
614
- bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
615
- return btoa(bin);
616
- }
617
- async function renderSoundtrack() {
618
- const ctx = new OfflineAudioContext(2, Math.ceil(T.end * 48000), 48000);
619
- buildSound(ctx, 0, ctx.destination);
620
- return wavBase64(await ctx.startRendering());
621
- }
622
- /* ---------- end sound ---------- */
623
-
624
- /* ---------- renderer ---------- */
625
- let W = RENDER ? 1920 : innerWidth,
626
- H = RENDER ? 1080 : innerHeight;
627
- const renderer = new THREE.WebGLRenderer({
628
- antialias: true,
629
- powerPreference: 'high-performance',
630
- preserveDrawingBuffer: RENDER,
631
- });
632
- let pixelRatio = RENDER ? 1 : Math.min(devicePixelRatio, 1.5);
633
- renderer.setPixelRatio(pixelRatio);
634
- renderer.setSize(W, H);
635
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
636
- renderer.toneMappingExposure = 1.0;
637
- renderer.shadowMap.enabled = true;
638
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
639
- renderer.setClearColor(0x000000, 1);
640
- document.body.appendChild(renderer.domElement);
641
- const maxAniso = renderer.capabilities.getMaxAnisotropy();
642
-
643
- const scene = new THREE.Scene();
644
- const FOG_BASE = new THREE.Color(0xb3c2d2); // pale blue morning haze
645
- scene.fog = new THREE.FogExp2(0x000000, FOG_DENSITY);
646
- scene.background = new THREE.Color(0x000000);
647
- const pmrem = new THREE.PMREMGenerator(renderer);
648
- scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
649
- scene.environmentIntensity = 0;
650
-
651
- const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100);
652
- camera.position.set(0, CAM_Y0, CAM_Z0);
653
-
654
- /* ---------- procedural textures (seeded, tileable) ---------- */
655
- function tileNoise(size, cells, seed) {
656
- const r = mulberry32(seed),
657
- g = new Float32Array(cells * cells);
658
- for (let i = 0; i < g.length; i++) g[i] = r();
659
- const out = new Float32Array(size * size);
660
- for (let y = 0; y < size; y++) {
661
- const fy = (y / size) * cells,
662
- iy = Math.floor(fy),
663
- ty = fy - iy,
664
- sy = ty * ty * (3 - 2 * ty),
665
- y0 = iy % cells,
666
- y1 = (iy + 1) % cells;
667
- for (let x = 0; x < size; x++) {
668
- const fx = (x / size) * cells,
669
- ix = Math.floor(fx),
670
- tx = fx - ix,
671
- sx = tx * tx * (3 - 2 * tx),
672
- x0 = ix % cells,
673
- x1 = (ix + 1) % cells;
674
- const a = g[y0 * cells + x0],
675
- b = g[y0 * cells + x1],
676
- c = g[y1 * cells + x0],
677
- d = g[y1 * cells + x1];
678
- out[y * size + x] = (a * (1 - sx) + b * sx) * (1 - sy) + (c * (1 - sx) + d * sx) * sy;
679
- }
680
- }
681
- return out;
682
- }
683
- function fbmField(size, base, oct, seed) {
684
- const acc = new Float32Array(size * size);
685
- let amp = 0.5,
686
- tot = 0;
687
- for (let o = 0; o < oct; o++) {
688
- const n = tileNoise(size, base << o, seed + o * 101);
689
- for (let i = 0; i < acc.length; i++) acc[i] += n[i] * amp;
690
- tot += amp;
691
- amp *= 0.5;
692
- }
693
- for (let i = 0; i < acc.length; i++) acc[i] = clamp((acc[i] / tot - 0.5) * 2.3 + 0.5);
694
- return acc;
695
- }
696
- function canvasTex(size, fill, srgb, repeat = 1) {
697
- const c = document.createElement('canvas');
698
- c.width = c.height = size;
699
- const ctx = c.getContext('2d'),
700
- img = ctx.createImageData(size, size);
701
- for (let i = 0; i < size * size; i++) {
702
- const p = fill(i);
703
- img.data[i * 4] = p[0] * 255;
704
- img.data[i * 4 + 1] = p[1] * 255;
705
- img.data[i * 4 + 2] = p[2] * 255;
706
- img.data[i * 4 + 3] = 255;
707
- }
708
- ctx.putImageData(img, 0, 0);
709
- const t = new THREE.CanvasTexture(c);
710
- t.wrapS = t.wrapT = THREE.RepeatWrapping;
711
- t.repeat.set(repeat, repeat);
712
- t.colorSpace = srgb ? THREE.SRGBColorSpace : THREE.NoColorSpace;
713
- t.anisotropy = maxAniso;
714
- return t;
715
- }
716
- const TS = 512;
717
- const nA = fbmField(TS, 4, 6, 11),
718
- nB = fbmField(TS, 8, 4, 23),
719
- nC = fbmField(TS, 32, 3, 37),
720
- nD = fbmField(TS, 4, 5, 53);
721
-
722
- const stoneMap = canvasTex(
723
- TS,
724
- (i) => {
725
- const base = 0.34 + 0.2 * nA[i] + 0.07 * (nC[i] - 0.5);
726
- let r = base,
727
- g = base * 0.985,
728
- b = base * 0.95;
729
- const lichen = smooth(0.62, 0.85, nB[i]) * 0.38;
730
- r = lerp(r, 0.24, lichen);
731
- g = lerp(g, 0.27, lichen);
732
- b = lerp(b, 0.15, lichen);
733
- const pit = smooth(0.68, 0.88, nC[i]) * 0.22;
734
- return [r * (1 - pit), g * (1 - pit), b * (1 - pit)];
735
- },
736
- true,
737
- 1.7
738
- );
739
- const stoneRough = canvasTex(
740
- TS,
741
- (i) => {
742
- const v = 0.7 + 0.28 * nB[i];
743
- return [v, v, v];
744
- },
745
- false,
746
- 1.7
747
- );
748
- const stoneBump = canvasTex(
749
- TS,
750
- (i) => {
751
- const v = 0.6 * nC[i] + 0.4 * nA[i];
752
- return [v, v, v];
753
- },
754
- false,
755
- 1.7
756
- );
757
- const soilMap = canvasTex(
758
- TS,
759
- (i) => {
760
- const a = nD[i],
761
- c = nC[i];
762
- let r = 0.13 + 0.07 * a,
763
- g = 0.105 + 0.055 * a,
764
- b = 0.085 + 0.04 * a;
765
- const leaf = smooth(0.62, 0.8, nB[i]) * 0.6;
766
- r = lerp(r, 0.24, leaf);
767
- g = lerp(g, 0.17, leaf);
768
- b = lerp(b, 0.09, leaf);
769
- const k = 0.85 + 0.3 * c;
770
- return [r * k, g * k, b * k];
771
- },
772
- true,
773
- 1
774
- );
775
- soilMap.repeat.set(22, 34);
776
- const soilBump = canvasTex(
777
- TS,
778
- (i) => {
779
- const v = 0.55 * nC[i] + 0.45 * nB[i];
780
- return [v, v, v];
781
- },
782
- false,
783
- 1
784
- );
785
- soilBump.repeat.set(22, 34);
786
- const fabricBump = canvasTex(
787
- TS,
788
- (i) => {
789
- const x = (i % TS) / TS;
790
- const v =
791
- 0.5 + 0.38 * Math.sin(Math.PI * 2 * (x * 11) + 3.2 * nA[i]) + 0.12 * (nC[i] - 0.5);
792
- return [v, v, v];
793
- },
794
- false,
795
- 1
796
- );
797
- /* ---------- materials ---------- */
798
- const P = (o) => new THREE.MeshPhysicalMaterial(o);
799
- const stoneMats = [0xbdb8ae, 0xa8a49c, 0xc9c2b4, 0x9a9890].map((c, k) =>
800
- P({
801
- color: c,
802
- map: stoneMap,
803
- roughnessMap: stoneRough,
804
- roughness: 1.0,
805
- bumpMap: stoneBump,
806
- bumpScale: 0.5 + k * 0.1,
807
- metalness: 0,
808
- specularIntensity: 0.35,
809
- envMapIntensity: 0.6,
810
- })
811
- );
812
- const groundMat = P({
813
- color: 0xffffff,
814
- vertexColors: true,
815
- map: soilMap,
816
- roughness: 0.97,
817
- bumpMap: soilBump,
818
- bumpScale: 3.0,
819
- specularIntensity: 0.25,
820
- });
821
- const barkMat = P({
822
- color: 0x3a322b,
823
- roughness: 0.93,
824
- bumpMap: stoneBump,
825
- bumpScale: 4.0,
826
- specularIntensity: 0.2,
827
- });
828
- const ironMat = P({
829
- color: 0x1d1c1b,
830
- metalness: 0.85,
831
- roughness: 0.58,
832
- envMapIntensity: 0.8,
833
- });
834
- const grassMat = P({
835
- color: 0xffffff,
836
- roughness: 0.85,
837
- specularIntensity: 0.3,
838
- sheen: 0.4,
839
- sheenColor: 0x8a8460,
840
- sheenRoughness: 0.6,
841
- });
842
-
843
- /* ---------- terrain ---------- */
844
- function groundH(x, z) {
845
- const m = smooth(0.9, 2.4, Math.abs(x));
846
- const h =
847
- 0.1 * Math.sin(0.33 * x + 0.21 * z) * Math.sin(0.27 * z - 0.13 * x + 1.0) +
848
- 0.05 * Math.sin(1.13 * x + 0.6) * Math.cos(0.87 * z + 0.3) +
849
- 0.025 * Math.sin(2.7 * x + 1.9 * z);
850
- const hill = 1.4 * smooth(-26, -70, z) * (0.6 + 0.4 * Math.sin(0.13 * x + 0.8));
851
- return h * m - 0.015 * (1 - m) + hill;
852
- }
853
- {
854
- const g = new THREE.PlaneGeometry(76, 110, 152, 220);
855
- g.rotateX(-Math.PI / 2);
856
- g.translate(0, 0, -35);
857
- const pos = g.attributes.position,
858
- col = new Float32Array(pos.count * 3);
859
- for (let i = 0; i < pos.count; i++) {
860
- const x = pos.getX(i),
861
- z = pos.getZ(i);
862
- pos.setY(i, groundH(x, z));
863
- const path = 1 - smooth(0.5, 1.6, Math.abs(x));
864
- const damp = 0.5 + 0.5 * Math.sin(x * 0.7 + z * 0.4) * Math.sin(z * 0.23);
865
- col[i * 3] = lerp(0.82 + 0.1 * damp, 1.18, path);
866
- col[i * 3 + 1] = lerp(0.86 + 0.08 * damp, 1.1, path);
867
- col[i * 3 + 2] = lerp(0.8, 1.02, path);
868
- }
869
- g.setAttribute('color', new THREE.BufferAttribute(col, 3));
870
- g.computeVertexNormals();
871
- const ground = new THREE.Mesh(g, groundMat);
872
- ground.receiveShadow = true;
873
- scene.add(ground);
874
- }
875
-
876
- /* ---------- sky dome: morning blue overhead, hazy at the horizon, warm around a low sun ---------- */
877
- const SUN_POS = new THREE.Vector3(14, 9, -40); // low, upper right of frame, behind the monument
878
- const sunDir = SUN_POS.clone().normalize();
879
- const ZENITH = new THREE.Color(0x6f9ccf),
880
- SUN_TINT = new THREE.Color(0xffc283);
881
- const skyMat = new THREE.ShaderMaterial({
882
- side: THREE.BackSide,
883
- depthWrite: false,
884
- fog: false,
885
- uniforms: {
886
- uFog: { value: new THREE.Color(0) },
887
- uZenith: { value: new THREE.Color(0) },
888
- uSunCol: { value: new THREE.Color(0) },
889
- uSun: { value: sunDir },
890
- },
891
- vertexShader: \`varying vec3 vDir; void main(){ vec4 w = modelMatrix*vec4(position,1.0); vDir = w.xyz - cameraPosition; gl_Position = projectionMatrix*viewMatrix*w; }\`,
892
- fragmentShader: \`uniform vec3 uFog, uZenith, uSunCol, uSun; varying vec3 vDir;
893
- void main(){ vec3 d = normalize(vDir); float h = d.y;
894
- vec3 col = mix(uFog, uZenith, smoothstep(0.0, 0.55, h));
895
- float m = max(dot(d, normalize(uSun)), 0.0);
896
- col += uSunCol * (0.35*pow(m, 6.0) + 0.6*pow(m, 60.0)) * smoothstep(-0.1, 0.15, h); // warm glow around the sun
897
- col += uSunCol * 4.0 * smoothstep(0.99955, 0.9998, m); // the sun's disc
898
- gl_FragColor = vec4(col, 1.0); }\`,
899
- });
900
- const sky = new THREE.Mesh(new THREE.SphereGeometry(85, 32, 16), skyMat);
901
- sky.renderOrder = -1;
902
- scene.add(sky);
903
-
904
- /* ---------- tombstones ---------- */
905
- function stoneShape(w, h, type) {
906
- const s = new THREE.Shape(),
907
- hw = w / 2;
908
- if (type === 0) {
909
- s.moveTo(-hw, 0);
910
- s.lineTo(hw, 0);
911
- s.lineTo(hw, h - hw);
912
- s.absarc(0, h - hw, hw, 0, Math.PI, false);
913
- s.lineTo(-hw, 0);
914
- } else if (type === 1) {
915
- s.moveTo(-hw, 0);
916
- s.lineTo(hw, 0);
917
- s.lineTo(hw, h * 0.62);
918
- s.quadraticCurveTo(hw, h * 0.9, 0, h);
919
- s.quadraticCurveTo(-hw, h * 0.9, -hw, h * 0.62);
920
- s.lineTo(-hw, 0);
921
- } else if (type === 2) {
922
- s.moveTo(-hw, 0);
923
- s.lineTo(hw, 0);
924
- s.lineTo(hw, h * 0.8);
925
- s.quadraticCurveTo(hw * 0.95, h * 0.88, hw * 0.58, h * 0.88);
926
- s.quadraticCurveTo(hw * 0.25, h * 0.88, 0, h);
927
- s.quadraticCurveTo(-hw * 0.25, h * 0.88, -hw * 0.58, h * 0.88);
928
- s.quadraticCurveTo(-hw * 0.95, h * 0.88, -hw, h * 0.8);
929
- s.lineTo(-hw, 0);
930
- } else {
931
- const a = w * 0.15,
932
- y0 = h * 0.6,
933
- y1 = y0 + 2 * a;
934
- s.moveTo(-a, 0);
935
- s.lineTo(a, 0);
936
- s.lineTo(a, y0);
937
- s.lineTo(hw, y0);
938
- s.lineTo(hw, y1);
939
- s.lineTo(a, y1);
940
- s.lineTo(a, h);
941
- s.lineTo(-a, h);
942
- s.lineTo(-a, y1);
943
- s.lineTo(-hw, y1);
944
- s.lineTo(-hw, y0);
945
- s.lineTo(-a, y0);
946
- s.lineTo(-a, 0);
947
- }
948
- return s;
949
- }
950
- const stoneGroup = new THREE.Group();
951
- scene.add(stoneGroup);
952
- function addTomb(x, z, scale) {
953
- const type = rng() < 0.12 ? 3 : Math.floor(rand(0, 3)),
954
- r = rng();
955
- const w = (type === 3 ? rand(0.55, 0.75) : rand(0.5, 0.78)) * scale,
956
- h = (type === 3 ? rand(1.0, 1.35) : rand(0.7, 1.15)) * scale,
957
- d = rand(0.1, 0.17) * scale;
958
- const geo = new THREE.ExtrudeGeometry(stoneShape(w, h, type), {
959
- depth: d,
960
- bevelEnabled: true,
961
- bevelThickness: 0.014,
962
- bevelSize: 0.014,
963
- bevelSegments: 2,
964
- curveSegments: 18,
965
- });
966
- geo.translate(0, 0, -d / 2);
967
- const m = new THREE.Mesh(geo, stoneMats[Math.floor(r * 4)]);
968
- const grp = new THREE.Group();
969
- grp.add(m);
970
- if (type !== 3 && rng() < 0.45) {
971
- const pl = new THREE.Mesh(
972
- new THREE.BoxGeometry(w * 1.18, 0.12 * scale, d * 2.2),
973
- m.material
974
- );
975
- pl.position.y = 0.0;
976
- m.position.y = 0.05 * scale;
977
- grp.add(pl);
978
- pl.castShadow = pl.receiveShadow = true;
979
- }
980
- grp.position.set(x, groundH(x, z) - rand(0.03, 0.1), z);
981
- grp.rotation.set(
982
- rand(-0.11, 0.07),
983
- rand(-0.3, 0.3) + (x > 0 ? -0.08 : 0.08),
984
- rand(-0.09, 0.09)
985
- );
986
- m.castShadow = m.receiveShadow = true;
987
- stoneGroup.add(grp);
988
- if (rng() < 0.5) {
989
- const mound = new THREE.Mesh(
990
- new THREE.SphereGeometry(1, 20, 12, 0, Math.PI * 2, 0, Math.PI / 2),
991
- groundMat
992
- );
993
- mound.scale.set(w * 0.62, 0.11, 0.85);
994
- mound.position.set(x + rand(-0.05, 0.05), groundH(x, z + 0.95) - 0.05, z + 0.95);
995
- mound.receiveShadow = true;
996
- scene.add(mound);
997
- }
998
- }
999
- function addObelisk(x, z) {
1000
- const h = rand(1.5, 2.1),
1001
- w = rand(0.32, 0.4),
1002
- grp = new THREE.Group(),
1003
- mat = stoneMats[Math.floor(rand(0, 4))];
1004
- const base = new THREE.Mesh(new THREE.BoxGeometry(w * 1.7, 0.28, w * 1.7), mat);
1005
- base.position.y = 0.1;
1006
- const shaft = new THREE.Mesh(new THREE.CylinderGeometry(w * 0.48, w * 0.68, h, 4, 1), mat);
1007
- shaft.rotation.y = Math.PI / 4;
1008
- shaft.position.y = 0.24 + h / 2;
1009
- const tip = new THREE.Mesh(new THREE.ConeGeometry(w * 0.48, w * 0.7, 4), mat);
1010
- tip.rotation.y = Math.PI / 4;
1011
- tip.position.y = 0.24 + h + w * 0.35;
1012
- [base, shaft, tip].forEach((o) => {
1013
- o.castShadow = o.receiveShadow = true;
1014
- grp.add(o);
1015
- });
1016
- grp.position.set(x, groundH(x, z) - 0.05, z);
1017
- grp.rotation.set(rand(-0.04, 0.04), rand(0, 1), rand(-0.05, 0.05));
1018
- stoneGroup.add(grp);
1019
- }
1020
- for (const side of [-1, 1]) {
1021
- for (let z = 6.2; z > -40; z -= rand(2.2, 3.0))
1022
- addTomb(side * rand(1.55, 2.05), z + rand(-0.4, 0.4), rand(0.9, 1.1));
1023
- for (let z = 5.0; z > -42; z -= rand(2.4, 3.4))
1024
- addTomb(side * rand(3.0, 4.2), z + rand(-0.5, 0.5), rand(0.85, 1.15));
1025
- for (let z = 3.0; z > -44; z -= rand(2.6, 4.0)) {
1026
- const x = side * rand(5.2, 8.5);
1027
- if (rng() < 0.18) addObelisk(x, z);
1028
- else addTomb(x, z, rand(0.9, 1.25));
1029
- }
1030
- }
1031
-
1032
- /* ---------- dead trees ---------- */
1033
- function makeTree(seed, scale) {
1034
- const r = mulberry32(seed),
1035
- geos = [],
1036
- Y = new THREE.Vector3(0, 1, 0);
1037
- function seg(a, b, r0, r1, joint) {
1038
- const d = new THREE.Vector3().subVectors(b, a),
1039
- L = d.length();
1040
- const g = new THREE.CylinderGeometry(r1, r0, L, 7, 1);
1041
- g.translate(0, L / 2, 0);
1042
- g.applyQuaternion(new THREE.Quaternion().setFromUnitVectors(Y, d.normalize()));
1043
- g.translate(a.x, a.y, a.z);
1044
- geos.push(g);
1045
- if (joint) {
1046
- const s = new THREE.SphereGeometry(r1 * 1.02, 8, 6);
1047
- s.translate(b.x, b.y, b.z);
1048
- geos.push(s);
1049
- }
1050
- }
1051
- function branch(p, dir, len, rad, depth) {
1052
- let cur = p.clone(),
1053
- d = dir.clone(),
1054
- r0 = rad * (depth === 0 ? 1.7 : 1);
1055
- for (let i = 0; i < 3; i++) {
1056
- d.add(
1057
- new THREE.Vector3((r() - 0.5) * 0.7, (r() - 0.45) * 0.35, (r() - 0.5) * 0.7)
1058
- ).normalize();
1059
- const nxt = cur.clone().addScaledVector(d, len / 3),
1060
- r1 = rad * (1 - (0.5 * (i + 1)) / 3);
1061
- seg(cur, nxt, r0, r1, depth < 3);
1062
- cur = nxt;
1063
- r0 = r1;
1064
- }
1065
- if (depth >= 5 || rad < 0.012) return;
1066
- const kids = depth < 1 ? 3 : 2 + (r() < 0.4 ? 1 : 0);
1067
- for (let k = 0; k < kids; k++) {
1068
- const nd = d
1069
- .clone()
1070
- .add(new THREE.Vector3((r() - 0.5) * 1.7, r() * 0.7 - 0.15, (r() - 0.5) * 1.7))
1071
- .normalize();
1072
- branch(cur, nd, len * (0.6 + r() * 0.16), r0 * 0.74, depth + 1);
1073
- }
1074
- }
1075
- branch(
1076
- new THREE.Vector3(0, -0.3, 0),
1077
- new THREE.Vector3(0, 1, 0),
1078
- 3.4 * scale,
1079
- 0.2 * scale,
1080
- 0
1081
- );
1082
- return mergeGeometries(geos);
1083
- }
1084
- [
1085
- [-5.6, -13, 1.25, 7],
1086
- [6.8, -19, 1.1, 19],
1087
- [-8.5, -27, 1.3, 33],
1088
- [4.6, -35, 1.0, 41],
1089
- [9.5, -8, 0.9, 57],
1090
- [-11, -6, 1.0, 71],
1091
- [12, -30, 1.2, 83],
1092
- ].forEach(([x, z, s, seed]) => {
1093
- const t = new THREE.Mesh(makeTree(seed, s), barkMat);
1094
- t.position.set(x, groundH(x, z), z);
1095
- t.rotation.y = seed;
1096
- t.castShadow = true;
1097
- t.receiveShadow = true;
1098
- scene.add(t);
1099
- });
1100
-
1101
- /* ---------- gate and iron fence (at the far end of the aisle) ---------- */
1102
- const GATE_Z = -27;
1103
- {
1104
- const pillarMat = stoneMats[1];
1105
- for (const side of [-1, 1]) {
1106
- const pg = new THREE.Group();
1107
- const p = new THREE.Mesh(new THREE.BoxGeometry(0.5, 2.5, 0.5), pillarMat);
1108
- p.position.y = 1.25;
1109
- const cap = new THREE.Mesh(new THREE.BoxGeometry(0.64, 0.14, 0.64), pillarMat);
1110
- cap.position.y = 2.57;
1111
- const fin = new THREE.Mesh(new THREE.SphereGeometry(0.17, 20, 14), pillarMat);
1112
- fin.position.y = 2.82;
1113
- [p, cap, fin].forEach((o) => {
1114
- o.castShadow = o.receiveShadow = true;
1115
- pg.add(o);
1116
- });
1117
- pg.position.set(side * 1.65, groundH(side * 1.65, GATE_Z), GATE_Z);
1118
- scene.add(pg);
1119
- // open gate leaf, hinged at the pillar, swung toward the camera
1120
- const bars = [];
1121
- for (let i = 0; i < 8; i++) {
1122
- const b = new THREE.CylinderGeometry(0.013, 0.013, 1.9, 6);
1123
- b.translate(0.08 + i * 0.16, 0.98, 0);
1124
- bars.push(b);
1125
- const tip = new THREE.ConeGeometry(0.03, 0.12, 6);
1126
- tip.translate(0.08 + i * 0.16, 1.98, 0);
1127
- bars.push(tip);
1128
- }
1129
- for (const y of [0.18, 1.55, 1.8]) {
1130
- const r = new THREE.BoxGeometry(1.24, 0.035, 0.035);
1131
- r.translate(0.64, y, 0);
1132
- bars.push(r);
1133
- }
1134
- const leaf = new THREE.Mesh(mergeGeometries(bars.map((g) => g.toNonIndexed())), ironMat);
1135
- leaf.castShadow = true;
1136
- const hinge = new THREE.Group();
1137
- hinge.position.set(side * 1.38, groundH(side * 1.38, GATE_Z), GATE_Z + 0.02);
1138
- hinge.rotation.y = side < 0 ? -1.15 : Math.PI + 1.15;
1139
- hinge.add(leaf);
1140
- scene.add(hinge);
1141
- }
1142
- // arch
1143
- const arch = new THREE.Mesh(new THREE.TorusGeometry(1.4, 0.022, 8, 48, Math.PI), ironMat);
1144
- arch.position.set(0, 2.45, GATE_Z);
1145
- arch.castShadow = true;
1146
- scene.add(arch);
1147
- // fence
1148
- const barGeo = mergeGeometries([
1149
- new THREE.CylinderGeometry(0.011, 0.011, 1.5, 5).translate(0, 0.75, 0).toNonIndexed(),
1150
- new THREE.ConeGeometry(0.026, 0.1, 5).translate(0, 1.55, 0).toNonIndexed(),
1151
- ]);
1152
- const xs = [];
1153
- for (const side of [-1, 1]) for (let x = 2.0; x < 16; x += 0.15) xs.push(side * x);
1154
- const fence = new THREE.InstancedMesh(barGeo, ironMat, xs.length);
1155
- const m4 = new THREE.Matrix4();
1156
- xs.forEach((x, i) => {
1157
- m4.makeRotationZ((hash1(i * 3.1) - 0.5) * 0.06);
1158
- m4.setPosition(x, groundH(x, GATE_Z) - 0.05, GATE_Z);
1159
- fence.setMatrixAt(i, m4);
1160
- });
1161
- fence.castShadow = true;
1162
- scene.add(fence);
1163
- for (const side of [-1, 1])
1164
- for (const y of [0.25, 1.32]) {
1165
- const r = new THREE.Mesh(new THREE.BoxGeometry(14, 0.03, 0.03), ironMat);
1166
- r.position.set(side * 9, y + 0.1, GATE_Z);
1167
- r.castShadow = true;
1168
- scene.add(r);
1169
- }
1170
- }
1171
-
1172
- /* ---------- dead grass ---------- */
1173
- {
1174
- const N = 5200,
1175
- blade = new THREE.ConeGeometry(0.008, 0.18, 3, 1);
1176
- blade.translate(0, 0.09, 0);
1177
- const grass = new THREE.InstancedMesh(blade, grassMat, N),
1178
- m4 = new THREE.Matrix4(),
1179
- q = new THREE.Quaternion(),
1180
- e = new THREE.Euler(),
1181
- s = new THREE.Vector3(),
1182
- p = new THREE.Vector3(),
1183
- c = new THREE.Color();
1184
- let n = 0;
1185
- while (n < N) {
1186
- const x = rand(-13, 13),
1187
- z = rand(-40, 8.5);
1188
- if (Math.abs(x) < 0.75 + rng() * 0.6) continue;
1189
- e.set(rand(-0.45, 0.45), rand(0, 6.28), rand(-0.45, 0.45));
1190
- q.setFromEuler(e);
1191
- s.set(rand(0.7, 1.3), rand(0.45, 1.5), rand(0.7, 1.3));
1192
- p.set(x, groundH(x, z) - 0.02, z);
1193
- m4.compose(p, q, s);
1194
- grass.setMatrixAt(n, m4);
1195
- c.setRGB(lerp(0.14, 0.27, rng()), lerp(0.13, 0.23, rng()), lerp(0.08, 0.12, rng()));
1196
- grass.setColorAt(n, c);
1197
- n++;
1198
- }
1199
- grass.receiveShadow = true;
1200
- scene.add(grass);
1201
- }
1202
-
1203
- /* ---------- pumpkins ---------- */
1204
- const pumpkinColorDark = new THREE.Color(0x5a1e06),
1205
- pumpkinColorLight = new THREE.Color(0xd9661c);
1206
- function pumpkinDeform(v) {
1207
- const th = Math.atan2(v.z, v.x),
1208
- lobe = Math.pow(Math.abs(Math.cos(th * 4)), 0.5);
1209
- const rf = (1 - 0.075 * (1 - lobe)) * (1 - 0.06 * v.y * v.y);
1210
- let y = v.y * 0.72 - 0.2 * smooth(0.72, 1.0, v.y) + 0.08 * smooth(-0.8, -1.0, v.y);
1211
- return { p: new THREE.Vector3(v.x * rf, y, v.z * rf), lobe };
1212
- }
1213
- const pumpkinGeo = (() => {
1214
- let g = new THREE.SphereGeometry(1, 64, 40);
1215
- g.deleteAttribute('uv');
1216
- g.deleteAttribute('normal');
1217
- g = mergeVertices(g);
1218
- const pos = g.attributes.position,
1219
- col = new Float32Array(pos.count * 3),
1220
- v = new THREE.Vector3(),
1221
- c = new THREE.Color();
1222
- for (let i = 0; i < pos.count; i++) {
1223
- v.fromBufferAttribute(pos, i).normalize();
1224
- const d = pumpkinDeform(v);
1225
- pos.setXYZ(i, d.p.x, d.p.y, d.p.z);
1226
- c.copy(pumpkinColorDark).lerp(
1227
- pumpkinColorLight,
1228
- Math.pow(d.lobe, 1.6) * (0.75 + 0.25 * (1 - Math.abs(v.y)))
1229
- );
1230
- col[i * 3] = c.r;
1231
- col[i * 3 + 1] = c.g;
1232
- col[i * 3 + 2] = c.b;
1233
- }
1234
- g.setAttribute('color', new THREE.BufferAttribute(col, 3));
1235
- g.computeVertexNormals();
1236
- return g;
1237
- })();
1238
- const pumpkinMat = P({
1239
- color: 0xffffff,
1240
- vertexColors: true,
1241
- roughness: 0.52,
1242
- clearcoat: 0.35,
1243
- clearcoatRoughness: 0.45,
1244
- sheen: 0.3,
1245
- sheenColor: 0xffa060,
1246
- specularIntensity: 0.6,
1247
- });
1248
- const stemMat = P({ color: 0x4a4426, roughness: 0.82, bumpMap: stoneBump, bumpScale: 3 });
1249
- function subdivide(geo, times) {
1250
- let g = geo.index ? geo.toNonIndexed() : geo;
1251
- for (let k = 0; k < times; k++) {
1252
- const a = g.attributes.position.array,
1253
- out = [];
1254
- for (let i = 0; i < a.length; i += 9) {
1255
- const A = [a[i], a[i + 1], a[i + 2]],
1256
- B = [a[i + 3], a[i + 4], a[i + 5]],
1257
- C = [a[i + 6], a[i + 7], a[i + 8]];
1258
- const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
1259
- const ab = mid(A, B),
1260
- bc = mid(B, C),
1261
- ca = mid(C, A);
1262
- out.push(
1263
- ...A,
1264
- ...ab,
1265
- ...ca,
1266
- ...ab,
1267
- ...B,
1268
- ...bc,
1269
- ...ca,
1270
- ...bc,
1271
- ...C,
1272
- ...ab,
1273
- ...bc,
1274
- ...ca
1275
- );
1276
- }
1277
- g = new THREE.BufferGeometry();
1278
- g.setAttribute('position', new THREE.Float32BufferAttribute(out, 3));
1279
- }
1280
- return g;
1281
- }
1282
- const faceGeo = (() => {
1283
- const shapes = [];
1284
- const tri = (pts) => {
1285
- const s = new THREE.Shape();
1286
- s.moveTo(...pts[0]);
1287
- pts.slice(1).forEach((p) => s.lineTo(...p));
1288
- s.closePath();
1289
- return s;
1290
- };
1291
- shapes.push(
1292
- tri([
1293
- [0.12, 0.08],
1294
- [0.42, 0.17],
1295
- [0.25, 0.38],
1296
- ]),
1297
- tri([
1298
- [-0.12, 0.08],
1299
- [-0.25, 0.38],
1300
- [-0.42, 0.17],
1301
- ]),
1302
- tri([
1303
- [-0.06, -0.03],
1304
- [0.06, -0.03],
1305
- [0, 0.07],
1306
- ])
1307
- );
1308
- const yu = (x) => -0.1 - 0.1 * (1 - (x / 0.5) ** 2),
1309
- yl = (x) => -0.14 - 0.26 * (1 - (x / 0.5) ** 2);
1310
- const m = new THREE.Shape();
1311
- m.moveTo(-0.5, yu(-0.5));
1312
- for (let x = -0.45; x <= 0.451; x += 0.05) {
1313
- const xr = Math.round(x * 100) / 100;
1314
- if (xr === -0.05) {
1315
- m.lineTo(-0.05, yl(-0.05));
1316
- m.lineTo(-0.05, yl(-0.05) + 0.08);
1317
- m.lineTo(0.05, yl(0.05) + 0.08);
1318
- m.lineTo(0.05, yl(0.05));
1319
- continue;
1320
- }
1321
- if (xr === 0 || xr === 0.05) continue;
1322
- m.lineTo(xr, yl(xr));
1323
- }
1324
- m.lineTo(0.5, yu(0.5));
1325
- for (let x = 0.45; x >= -0.451; x -= 0.05) {
1326
- const xr = Math.round(x * 100) / 100;
1327
- if (xr === 0.22) {
1328
- m.lineTo(0.22, yu(0.22));
1329
- m.lineTo(0.22, yu(0.22) - 0.08);
1330
- m.lineTo(0.12, yu(0.12) - 0.08);
1331
- m.lineTo(0.12, yu(0.12));
1332
- continue;
1333
- }
1334
- if (xr === -0.12) {
1335
- m.lineTo(-0.12, yu(-0.12));
1336
- m.lineTo(-0.12, yu(-0.12) - 0.08);
1337
- m.lineTo(-0.22, yu(-0.22) - 0.08);
1338
- m.lineTo(-0.22, yu(-0.22));
1339
- continue;
1340
- }
1341
- if ((xr > 0.12 && xr < 0.22) || (xr > -0.22 && xr < -0.12)) continue;
1342
- m.lineTo(xr, yu(xr));
1343
- }
1344
- m.closePath();
1345
- shapes.push(m);
1346
- const flat = subdivide(new THREE.ShapeGeometry(shapes, 1), 3),
1347
- pos = flat.attributes.position;
1348
- const col = new Float32Array(pos.count * 3),
1349
- v = new THREE.Vector3();
1350
- for (let i = 0; i < pos.count; i++) {
1351
- const x = pos.getX(i),
1352
- y = pos.getY(i);
1353
- v.set(x, y, Math.sqrt(Math.max(0, 1 - x * x - y * y))).normalize();
1354
- const d = pumpkinDeform(v);
1355
- d.p.addScaledVector(v, 0.014);
1356
- pos.setXYZ(i, d.p.x, d.p.y, d.p.z);
1357
- const k = 0.55 + 0.45 * smooth(0.55, 0.0, Math.hypot(x * 0.8, y + 0.05));
1358
- col[i * 3] = k;
1359
- col[i * 3 + 1] = k * k;
1360
- col[i * 3 + 2] = k * k * k;
1361
- }
1362
- flat.setAttribute('color', new THREE.BufferAttribute(col, 3));
1363
- return flat;
1364
- })();
1365
- const pumpkins = [];
1366
- function addPumpkin(x, z, r, carved, withLight) {
1367
- const g = new THREE.Group(),
1368
- body = new THREE.Mesh(pumpkinGeo, pumpkinMat);
1369
- body.castShadow = body.receiveShadow = true;
1370
- g.add(body);
1371
- const st = new THREE.CylinderGeometry(0.045, 0.08, 0.26, 7, 5);
1372
- const sp = st.attributes.position;
1373
- for (let i = 0; i < sp.count; i++) {
1374
- const yy = sp.getY(i) + 0.13;
1375
- sp.setX(i, sp.getX(i) + 0.1 * yy * yy);
1376
- }
1377
- st.computeVertexNormals();
1378
- const stem = new THREE.Mesh(st, stemMat);
1379
- stem.position.y = 0.58;
1380
- stem.rotation.y = rand(0, 6);
1381
- stem.castShadow = true;
1382
- g.add(stem);
1383
- let face = null,
1384
- faceMat = null,
1385
- light = null;
1386
- if (carved) {
1387
- faceMat = new THREE.MeshBasicMaterial({
1388
- color: 0x000000,
1389
- vertexColors: true,
1390
- polygonOffset: true,
1391
- polygonOffsetFactor: -2,
1392
- polygonOffsetUnits: -2,
1393
- });
1394
- face = new THREE.Mesh(faceGeo, faceMat);
1395
- g.add(face);
1396
- if (withLight) {
1397
- light = new THREE.PointLight(0xff7a2a, 0, 5, 2);
1398
- light.position.set(0, 0.25, 0.55);
1399
- g.add(light);
1400
- }
1401
- }
1402
- g.scale.setScalar(r);
1403
- g.position.set(x, groundH(x, z) + r * 0.62, z);
1404
- g.rotation.y = Math.atan2(0 - x, 3.0 - z) + rand(-0.18, 0.18);
1405
- g.rotation.z = rand(-0.08, 0.08);
1406
- scene.add(g);
1407
- pumpkins.push({ g, faceMat, light, seed: pumpkins.length * 13.7 + 2.1 });
1408
- }
1409
- addPumpkin(-0.98, -1.35, 0.3, true, true);
1410
- addPumpkin(1.18, -2.75, 0.34, true, true);
1411
- addPumpkin(1.42, -2.25, 0.2, false);
1412
- addPumpkin(-1.38, 4.6, 0.26, true, true);
1413
- addPumpkin(-2.4, 2.1, 0.22, false);
1414
- addPumpkin(1.55, -6.6, 0.27, true, false);
1415
- addPumpkin(-1.75, -11.2, 0.3, true, false);
1416
- addPumpkin(2.6, -8.4, 0.24, false);
1417
- addPumpkin(1.6, -17.5, 0.28, true, false);
1418
- addPumpkin(-2.9, -3.8, 0.2, false);
1419
-
1420
- /* ---------- ground mist (soft sheets, never in the dolly path) ---------- */
1421
- const mistUniforms = {
1422
- uTime: { value: 0 },
1423
- uColor: { value: new THREE.Color(0) },
1424
- uCandlePos: { value: new THREE.Vector3() },
1425
- uCandleCol: { value: new THREE.Color(1.0, 0.5, 0.18) },
1426
- uCandleI: { value: 0 },
1427
- uCam: { value: new THREE.Vector3() },
1428
- uFogDensity: { value: FOG_DENSITY },
1429
- };
1430
- function mistMaterial(vertical, opacity) {
1431
- return new THREE.ShaderMaterial({
1432
- transparent: true,
1433
- depthWrite: false,
1434
- fog: false,
1435
- side: THREE.DoubleSide,
1436
- uniforms: {
1437
- ...mistUniforms,
1438
- uVertical: { value: vertical ? 1 : 0 },
1439
- uOpacity: { value: opacity },
1440
- },
1441
- vertexShader: \`varying vec3 vW; void main(){ vec4 w = modelMatrix*vec4(position,1.0); vW = w.xyz; gl_Position = projectionMatrix*viewMatrix*w; }\`,
1442
- fragmentShader: \`
1443
- uniform float uTime, uOpacity, uFogDensity, uVertical, uCandleI; uniform vec3 uColor, uCandlePos, uCandleCol, uCam; varying vec3 vW;
1444
- float hash(vec2 p){ p = fract(p*vec2(123.34,456.21)); p += dot(p, p+45.32); return fract(p.x*p.y); }
1445
- float vn(vec2 p){ vec2 i = floor(p), f = fract(p), u = f*f*(3.0-2.0*f);
1446
- return mix(mix(hash(i), hash(i+vec2(1,0)), u.x), mix(hash(i+vec2(0,1)), hash(i+vec2(1,1)), u.x), u.y); }
1447
- float fbm(vec2 p){ float a = 0.5, s = 0.0; for(int i=0;i<4;i++){ s += a*vn(p); p = p*2.03 + vec2(1.7,9.2); a *= 0.5; } return s; }
1448
- void main(){
1449
- vec2 q = uVertical > 0.5 ? vec2(vW.x*0.32 + uTime*0.05, vW.y*0.75 - uTime*0.012) : vec2(vW.x*0.22 + uTime*0.04, vW.z*0.2 + uTime*0.025);
1450
- float n = fbm(q + 1.3*fbm(q*1.6 - uTime*0.03));
1451
- float a = smoothstep(0.38, 0.86, n) * uOpacity;
1452
- if (uVertical > 0.5) { a *= smoothstep(0.0, 0.6, vW.y) * smoothstep(3.0, 1.1, vW.y) * smoothstep(13.0, 5.0, abs(vW.x)); }
1453
- else { a *= smoothstep(15.0, 4.0, abs(vW.x)); }
1454
- float dc = distance(uCam, vW); a *= smoothstep(0.9, 3.5, dc);
1455
- float fogF = 1.0 - exp(-pow(uFogDensity*dc, 2.0));
1456
- float dl = distance(vW, uCandlePos); float w = uCandleI / (0.3 + dl*dl);
1457
- vec3 col = uColor + uCandleCol * w * 0.22;
1458
- a = a * (1.0 - 0.75*fogF) * (1.0 + min(w, 3.0)*0.35);
1459
- gl_FragColor = vec4(col, clamp(a, 0.0, 1.0));
1460
- }\`,
1461
- });
1462
- }
1463
- const mistGroup = new THREE.Group();
1464
- scene.add(mistGroup);
1465
- {
1466
- const h1 = new THREE.Mesh(new THREE.PlaneGeometry(36, 66), mistMaterial(false, 0.55));
1467
- h1.rotation.x = -Math.PI / 2;
1468
- h1.position.set(0, 0.2, -24);
1469
- mistGroup.add(h1);
1470
- const h2 = new THREE.Mesh(new THREE.PlaneGeometry(36, 60), mistMaterial(false, 0.3));
1471
- h2.rotation.x = -Math.PI / 2;
1472
- h2.position.set(0, 0.55, -27);
1473
- mistGroup.add(h2);
1474
- for (const z of [-3.5, -9, -15.5, -22.5]) {
1475
- const v = new THREE.Mesh(new THREE.PlaneGeometry(26, 3.2), mistMaterial(true, 0.5));
1476
- v.position.set(0, 1.5, z);
1477
- mistGroup.add(v);
1478
- }
1479
- }
1480
-
1481
- /* ==========================================================================
1482
- THE MONUMENT: three carved stone shards on a buried boulder, glowing blue
1483
- to violet along their edges and veins, with a witch's hat hovering above.
1484
- ========================================================================== */
1485
- const MON_Z = -4.5,
1486
- MON_LIFT = 0.12,
1487
- PX = 0.00685; // reference drawing: 1 px = 6.85 mm
1488
- function vnoise3(x, y, z) {
1489
- const xi = Math.floor(x),
1490
- yi = Math.floor(y),
1491
- zi = Math.floor(z),
1492
- s = (t) => t * t * (3 - 2 * t),
1493
- u = s(x - xi),
1494
- v = s(y - yi),
1495
- w = s(z - zi);
1496
- const hh = (a, b, c) => hash1(a * 127.1 + b * 311.7 + c * 74.7),
1497
- l = (a, b, t) => a + (b - a) * t;
1498
- return l(
1499
- l(
1500
- l(hh(xi, yi, zi), hh(xi + 1, yi, zi), u),
1501
- l(hh(xi, yi + 1, zi), hh(xi + 1, yi + 1, zi), u),
1502
- v
1503
- ),
1504
- l(
1505
- l(hh(xi, yi, zi + 1), hh(xi + 1, yi, zi + 1), u),
1506
- l(hh(xi, yi + 1, zi + 1), hh(xi + 1, yi + 1, zi + 1), u),
1507
- v
1508
- ),
1509
- w
1510
- );
1511
- }
1512
- const GN = \`
1513
- float gh(vec3 p){ p = fract(p*0.3183099 + 0.1); p *= 17.0; return fract(p.x*p.y*p.z*(p.x+p.y+p.z)); }
1514
- float gn(vec3 x){ vec3 i = floor(x), f = fract(x); f = f*f*(3.0-2.0*f);
1515
- return mix(mix(mix(gh(i), gh(i+vec3(1,0,0)), f.x), mix(gh(i+vec3(0,1,0)), gh(i+vec3(1,1,0)), f.x), f.y),
1516
- mix(mix(gh(i+vec3(0,0,1)), gh(i+vec3(1,0,1)), f.x), mix(gh(i+vec3(0,1,1)), gh(i+vec3(1,1,1)), f.x), f.y), f.z); }
1517
- \`;
1518
- const monUni = {
1519
- uGlow: { value: 0 },
1520
- uTime: { value: 0 },
1521
- uTop: { value: new THREE.Color(0x6aaee6) },
1522
- uBot: { value: new THREE.Color(0x6a6ee6) },
1523
- uOrigin: { value: new THREE.Vector3(0, 0, MON_Z) },
1524
- };
1525
- const glowStoneMat = P({
1526
- color: 0x4c5262,
1527
- map: stoneMap,
1528
- roughnessMap: stoneRough,
1529
- roughness: 1,
1530
- bumpMap: stoneBump,
1531
- bumpScale: 1.4,
1532
- specularIntensity: 0.45,
1533
- envMapIntensity: 0.7,
1534
- });
1535
- glowStoneMat.onBeforeCompile = (sh) => {
1536
- Object.assign(sh.uniforms, monUni);
1537
- sh.vertexShader =
1538
- 'uniform vec3 uOrigin; varying vec3 vMon;\\n' +
1539
- sh.vertexShader.replace(
1540
- '#include <begin_vertex>',
1541
- '#include <begin_vertex>\\n vMon = (modelMatrix * vec4(transformed, 1.0)).xyz - uOrigin;'
1542
- );
1543
- sh.fragmentShader =
1544
- 'uniform float uGlow, uTime; uniform vec3 uTop, uBot; varying vec3 vMon;\\n' +
1545
- GN +
1546
- sh.fragmentShader.replace(
1547
- '#include <emissivemap_fragment>',
1548
- \`#include <emissivemap_fragment>
1549
- float k = clamp(0.5 + 0.38*(vMon.y - 1.1) - 0.3*vMon.x, 0.0, 1.0); // light blue top-left, violet bottom-right
1550
- vec3 gcol = mix(uBot, uTop, k);
1551
- float rim = pow(1.0 - abs(dot(normalize(normal), normalize(vViewPosition))), 3.0);
1552
- float r1 = 1.0 - abs(gn(vMon*2.4 + vec3(0.0, uTime*0.04, 0.0))*2.0 - 1.0);
1553
- float r2 = 1.0 - abs(gn(vMon*5.5 + 11.0)*2.0 - 1.0);
1554
- float vein = smoothstep(0.982, 0.999, r1) + 0.5*smoothstep(0.99, 0.9995, r2); // sparse, thin cracks
1555
- float pulse = 0.8 + 0.2*sin(uTime*2.2 - vMon.y*3.0);
1556
- totalEmissiveRadiance += gcol * uGlow * (0.08 + 1.9*rim + 3.0*vein*pulse);\`
1557
- );
1558
- };
1559
- glowStoneMat.customProgramCacheKey = () => 'glow-stone';
1560
- const monument = new THREE.Group();
1561
- monument.position.set(0, MON_LIFT, MON_Z);
1562
- scene.add(monument);
1563
- function shard(pts, depth, zOff, seed) {
1564
- const s = new THREE.Shape(),
1565
- m2 = pts.map(([px, py]) => [(px - 227) * PX, (575 - py) * PX]);
1566
- const cx = m2.reduce((a, q) => a + q[0], 0) / m2.length,
1567
- cy = m2.reduce((a, q) => a + q[1], 0) / m2.length;
1568
- m2.forEach(([x, y], i) => {
1569
- x = cx + (x - cx) * 0.95;
1570
- y = cy + (y - cy) * 0.95;
1571
- if (i) s.lineTo(x, y);
1572
- else s.moveTo(x, y);
1573
- });
1574
- s.closePath(); // shrink a little so the gaps stay open
1575
- const g = new THREE.ExtrudeGeometry(s, {
1576
- depth,
1577
- bevelEnabled: true,
1578
- bevelThickness: 0.06,
1579
- bevelSize: 0.03,
1580
- bevelSegments: 3,
1581
- curveSegments: 1,
1582
- });
1583
- g.translate(0, 0, -depth / 2 + zOff);
1584
- const pos = g.attributes.position,
1585
- v = new THREE.Vector3();
1586
- for (let i = 0; i < pos.count; i++) {
1587
- // rough it up; shared corners move together, so no cracks
1588
- v.fromBufferAttribute(pos, i);
1589
- const n = (a, b) => vnoise3(v.x * a + seed, v.y * a + b, v.z * a) - 0.5;
1590
- v.x += 0.05 * n(5, 1) + 0.015 * n(17, 4);
1591
- v.y += 0.05 * n(5, 7) + 0.015 * n(17, 9);
1592
- v.z += 0.06 * n(5, 13);
1593
- pos.setXYZ(i, v.x, v.y, v.z);
1594
- }
1595
- g.computeVertexNormals();
1596
- const m = new THREE.Mesh(g, glowStoneMat);
1597
- m.castShadow = m.receiveShadow = true;
1598
- monument.add(m);
1599
- }
1600
- shard(
1601
- [
1602
- [166, 350],
1603
- [226, 397],
1604
- [140, 575],
1605
- [82, 528],
1606
- ],
1607
- 0.42,
1608
- 0.0,
1609
- 3.1
1610
- ); // leaning slab
1611
- shard(
1612
- [
1613
- [198, 283],
1614
- [258, 330],
1615
- [233, 383],
1616
- [172, 338],
1617
- ],
1618
- 0.36,
1619
- 0.04,
1620
- 7.7
1621
- ); // top block
1622
- shard(
1623
- [
1624
- [268, 356],
1625
- [372, 575],
1626
- [207, 488],
1627
- ],
1628
- 0.4,
1629
- -0.03,
1630
- 12.9
1631
- ); // triangle
1632
- {
1633
- // the boulder it stands on, half buried
1634
- const g = new THREE.IcosahedronGeometry(1, 4),
1635
- pos = g.attributes.position,
1636
- v = new THREE.Vector3();
1637
- for (let i = 0; i < pos.count; i++) {
1638
- v.fromBufferAttribute(pos, i);
1639
- const k =
1640
- 1 +
1641
- 0.18 * (vnoise3(v.x * 2.2, v.y * 2.2, v.z * 2.2) - 0.5) +
1642
- 0.06 * (vnoise3(v.x * 6 + 3, v.y * 6, v.z * 6) - 0.5);
1643
- pos.setXYZ(i, v.x * k * 1.45, v.y * k * 0.38, v.z * k * 0.85);
1644
- }
1645
- g.computeVertexNormals();
1646
- const base = new THREE.Mesh(
1647
- g,
1648
- P({
1649
- color: 0x5d5b58,
1650
- map: stoneMap,
1651
- roughnessMap: stoneRough,
1652
- roughness: 1,
1653
- bumpMap: stoneBump,
1654
- bumpScale: 1.6,
1655
- specularIntensity: 0.3,
1656
- })
1657
- );
1658
- base.position.set(0.02, groundH(0, MON_Z) - 0.02, MON_Z);
1659
- base.castShadow = base.receiveShadow = true;
1660
- scene.add(base);
1661
- }
1662
- // its glow on the ground, the fog and the underside of the hat
1663
- const monLights = [
1664
- [new THREE.PointLight(0x6aaee6, 0, 0, 2), [-0.5, 1.2, 1.6], 1.0],
1665
- [new THREE.PointLight(0x6a6ee6, 0, 0, 2), [0.7, 0.45, 1.5], 1.0],
1666
- [new THREE.PointLight(0x7a9ef0, 0, 0, 2), [0, 2.25, 0.35], 0.45],
1667
- ];
1668
- monLights.forEach(([l, p]) => {
1669
- l.position.set(...p);
1670
- monument.add(l);
1671
- });
1672
-
1673
- // the hat: crooked cone, curled brim, band and gold buckle
1674
- const HAT_Y = MON_LIFT + 2.0 + 0.32;
1675
- const hat = new THREE.Group();
1676
- hat.position.set(-0.08, HAT_Y, MON_Z);
1677
- scene.add(hat);
1678
- const hatRed = new THREE.Color(0x8f3434),
1679
- hatPink = new THREE.Color(0xe9b7ac);
1680
- const hatMat = P({
1681
- color: 0xffffff,
1682
- vertexColors: true,
1683
- roughness: 0.86,
1684
- sheen: 0.6,
1685
- sheenColor: 0xf0b8a8,
1686
- sheenRoughness: 0.5,
1687
- specularIntensity: 0.3,
1688
- side: THREE.DoubleSide,
1689
- bumpMap: fabricBump,
1690
- bumpScale: 0.6,
1691
- });
1692
- {
1693
- const H = 1.35,
1694
- pts = [];
1695
- for (let i = 0; i <= 48; i++) {
1696
- const t = i / 48;
1697
- pts.push(new THREE.Vector2(0.42 * Math.pow(1 - t, 1.1) + 0.012, t * H));
1698
- }
1699
- const g = new THREE.LatheGeometry(pts, 72, Math.PI, Math.PI * 2),
1700
- pos = g.attributes.position,
1701
- col = new Float32Array(pos.count * 3),
1702
- c = new THREE.Color();
1703
- for (let i = 0; i < pos.count; i++) {
1704
- let x = pos.getX(i),
1705
- y = pos.getY(i),
1706
- z = pos.getZ(i);
1707
- const hn = y / H,
1708
- phi = Math.atan2(x, z);
1709
- const wr =
1710
- 1 +
1711
- 0.06 * Math.sin(hn * 15 + phi * 2) * smooth(0.1, 0.6, hn) +
1712
- 0.03 * Math.sin(phi * 5 + hn * 9);
1713
- x *= wr;
1714
- z *= wr;
1715
- x += -0.45 * Math.pow(hn, 2.2) - 0.12 * smooth(0.75, 1, hn); // tip bends to the left
1716
- y -= 0.14 * smooth(0.72, 1, hn) * hn; // and droops
1717
- pos.setXYZ(i, x, y, z);
1718
- const hl =
1719
- smooth(0.1, 0.85, Math.sin(phi)) *
1720
- smooth(0.12, 0.45, hn) *
1721
- (1 - smooth(0.85, 1, hn)) *
1722
- 0.75;
1723
- c.copy(hatRed).lerp(hatPink, hl);
1724
- col[i * 3] = c.r;
1725
- col[i * 3 + 1] = c.g;
1726
- col[i * 3 + 2] = c.b;
1727
- }
1728
- g.setAttribute('color', new THREE.BufferAttribute(col, 3));
1729
- g.computeVertexNormals();
1730
- const cone = new THREE.Mesh(g, hatMat);
1731
- cone.castShadow = cone.receiveShadow = true;
1732
- hat.add(cone);
1733
- }
1734
- {
1735
- const prof = [
1736
- [0.3, 0.02],
1737
- [0.6, 0.0],
1738
- [0.85, 0.03],
1739
- [0.98, 0.1],
1740
- [1.0, 0.12],
1741
- [0.99, 0.1],
1742
- [0.85, 0.0],
1743
- [0.6, -0.025],
1744
- [0.3, -0.01],
1745
- ];
1746
- const g = new THREE.LatheGeometry(
1747
- prof.map((p) => new THREE.Vector2(...p)),
1748
- 96,
1749
- Math.PI,
1750
- Math.PI * 2
1751
- );
1752
- const pos = g.attributes.position,
1753
- col = new Float32Array(pos.count * 3),
1754
- c = new THREE.Color();
1755
- for (let i = 0; i < pos.count; i++) {
1756
- let x = pos.getX(i),
1757
- y = pos.getY(i),
1758
- z = pos.getZ(i);
1759
- const r = Math.hypot(x, z),
1760
- phi = Math.atan2(x, z),
1761
- top = i % prof.length < 5;
1762
- y += 0.035 * Math.sin(phi * 3 + 1) * smooth(0.4, 1, r);
1763
- y += 0.16 * smooth(0.55, 1, r) * Math.pow(Math.max(0, Math.sin(phi)), 3); // right edge curls up
1764
- y -= 0.05 * smooth(0.6, 1, r) * Math.max(0, -Math.sin(phi));
1765
- pos.setXYZ(i, x, y, z * 0.82);
1766
- c.copy(hatRed).lerp(hatPink, smooth(0.7, 1, r) * Math.max(0, Math.sin(phi)) * 0.6);
1767
- if (!top) c.multiplyScalar(0.6);
1768
- col[i * 3] = c.r;
1769
- col[i * 3 + 1] = c.g;
1770
- col[i * 3 + 2] = c.b;
1771
- }
1772
- g.setAttribute('color', new THREE.BufferAttribute(col, 3));
1773
- g.computeVertexNormals();
1774
- const brim = new THREE.Mesh(g, hatMat);
1775
- brim.castShadow = brim.receiveShadow = true;
1776
- hat.add(brim);
1777
- }
1778
- {
1779
- const band = new THREE.Mesh(
1780
- new THREE.CylinderGeometry(0.432, 0.448, 0.13, 72, 1, true),
1781
- P({
1782
- color: 0x5a1f1f,
1783
- roughness: 0.8,
1784
- sheen: 0.4,
1785
- sheenColor: 0xc08070,
1786
- side: THREE.DoubleSide,
1787
- })
1788
- );
1789
- band.position.y = 0.075;
1790
- band.castShadow = true;
1791
- hat.add(band);
1792
- const a = 0.55,
1793
- piv = new THREE.Group();
1794
- piv.position.set(Math.sin(a) * 0.455, 0.075, Math.cos(a) * 0.455);
1795
- piv.rotation.y = a;
1796
- hat.add(piv);
1797
- const buckle = new THREE.Mesh(
1798
- new THREE.TorusGeometry(0.075, 0.014, 8, 4),
1799
- P({ color: 0xf2c230, metalness: 1, roughness: 0.32, clearcoat: 0.3 })
1800
- );
1801
- buckle.rotation.z = Math.PI / 4;
1802
- piv.add(buckle);
1803
- }
1804
-
1805
- /* ---------- lights ---------- */
1806
- const sun = new THREE.DirectionalLight(0xffcf9a, 0); // low warm morning sun, backlighting the scene
1807
- sun.position.copy(SUN_POS);
1808
- sun.target.position.set(0, 0, -12);
1809
- sun.castShadow = true;
1810
- sun.shadow.mapSize.set(2048, 2048);
1811
- Object.assign(sun.shadow.camera, {
1812
- left: -27,
1813
- right: 27,
1814
- top: 27,
1815
- bottom: -27,
1816
- near: 1,
1817
- far: 90,
1818
- });
1819
- sun.shadow.bias = -0.0005;
1820
- sun.shadow.normalBias = 0.03;
1821
- scene.add(sun, sun.target);
1822
- const hemi = new THREE.HemisphereLight(0xbcd0e8, 0x5e5040, 0);
1823
- scene.add(hemi);
1824
-
1825
- /* ---------- "Happy Halloween": a burning title in the top-left corner ----------
1826
- Screen-space pass after tone mapping, placed in the lower part of the frame so it frames
1827
- the monument instead of covering it. The letters ignite left to right behind a hot edge,
1828
- then burn: fire flowing upward inside, a charred rim, an orange halo, heat shimmer and
1829
- embers rising off the words. */
1830
- // <fire-title>
1831
- const TITLE_T0 = 9.8,
1832
- TITLE_W = 0.42,
1833
- TITLE_CY = 0.867,
1834
- TITLE_LEFT = 0.05; // start, rect width, rect centre (from bottom), left margin
1835
- const TITLE_FRAG = \`
1836
- uniform sampler2D tDiffuse, tMask; uniform float uT, uAspect, uOn; uniform vec4 uRect; varying vec2 vUv;
1837
- float hh(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
1838
- float vn(vec2 p){ vec2 i = floor(p), f = fract(p); f = f*f*(3.0-2.0*f);
1839
- return mix(mix(hh(i), hh(i+vec2(1.0,0.0)), f.x), mix(hh(i+vec2(0.0,1.0)), hh(i+vec2(1.0,1.0)), f.x), f.y); }
1840
- float fbm(vec2 p){ float s = 0.0, a = 0.5; for (int i = 0; i < 4; i++){ s += a*vn(p); p = p*2.03 + vec2(1.7, 9.2); a *= 0.5; } return s; }
1841
- void main(){
1842
- vec3 col = texture2D(tDiffuse, vUv).rgb;
1843
- vec2 q = (vUv - uRect.xy)/uRect.zw + 0.5;
1844
- if (uT <= 0.0 || uOn <= 0.0 || q.x < -0.12 || q.x > 1.12 || q.y < -0.5 || q.y > 2.2) { gl_FragColor = vec4(col, 1.0); return; }
1845
- float R = uRect.z*uAspect/uRect.w;
1846
- vec2 hq = q + vec2((vn(vec2(q.x*14.0, q.y*6.0 - uT*3.0)) - 0.5)*0.003, (vn(vec2(q.x*10.0 + 3.0, q.y*5.0 - uT*2.5)) - 0.5)*0.012); // heat shimmer
1847
- vec4 M = texture2D(tMask, hq); // r sharp, g soft, b wide halo
1848
- float n = fbm(q*vec2(R*2.5, 2.5) + 3.1);
1849
- float front = uT*0.75 - q.x*0.9 + (n - 0.5)*0.5; // ignition sweeps left to right
1850
- float lit = smoothstep(0.0, 0.08, front);
1851
- float edge = smoothstep(0.0, 0.05, front)*(1.0 - smoothstep(0.05, 0.28, front));
1852
- col *= 1.0 - 0.5*M.b*smoothstep(-0.2, 0.3, front); // the night darkens behind the words
1853
- float flick = 0.86 + 0.14*vn(vec2(uT*7.0, 1.0));
1854
- float fire = fbm(vec2(q.x*R*3.0, q.y*3.0 - uT*1.6));
1855
- vec3 deep = vec3(0.78, 0.17, 0.02), hot = vec3(1.0, 0.6, 0.1), core = vec3(1.0, 0.92, 0.66);
1856
- vec3 fillc = mix(deep, hot, smoothstep(0.3, 0.75, fire + 0.3*(1.0 - clamp(q.y, 0.0, 1.0))));
1857
- fillc = mix(fillc, core, smoothstep(0.72, 0.95, fire)*0.6);
1858
- float inside = smoothstep(0.38, 0.62, M.g);
1859
- float rim = smoothstep(0.38, 0.5, M.g)*(1.0 - smoothstep(0.55, 0.85, M.g));
1860
- vec3 letters = fillc*flick*(1.0 - 0.45*rim); // charred rim, burning heart
1861
- col += vec3(1.0, 0.4, 0.07)*(M.g*0.55 + M.b*0.55)*flick*lit*0.6; // halo
1862
- col = mix(col, letters, inside*lit);
1863
- col += vec3(1.0, 0.82, 0.5)*edge*smoothstep(0.15, 0.6, M.g)*1.3; // the igniting edge flares
1864
- vec2 sp = vec2(q.x*R*6.0, q.y*6.0 - uT*1.3); // embers drifting up off the words
1865
- vec2 cid = floor(sp), cf = fract(sp) - 0.5; float hs = hh(cid);
1866
- vec2 off = vec2(hh(cid + 7.1) - 0.5, hh(cid + 3.3) - 0.5)*0.6 + vec2(sin(uT*3.0 + hs*20.0)*0.15, 0.0);
1867
- float spark = step(0.86, hs)*smoothstep(0.09, 0.0, length(cf - off))*smoothstep(2.0, 0.7, q.y)*smoothstep(0.1, 0.6, q.y)
1868
- * smoothstep(0.05, 0.3, max(texture2D(tMask, vec2(q.x, 0.34)).b, texture2D(tMask, vec2(q.x, 0.66)).b))*lit;
1869
- col += vec3(1.0, 0.55, 0.15)*spark*(0.55 + 0.45*sin(uT*9.0 + hs*40.0));
1870
- gl_FragColor = vec4(min(col, vec3(1.0)), 1.0);
1871
- }\`;
1872
- function blurInto(src, CW, CH, RB, passes) {
1873
- const b = src.slice(),
1874
- tmp = new Float32Array(src.length),
1875
- cl = (v, n) => Math.min(n - 1, Math.max(0, v)),
1876
- k = 1 / (2 * RB + 1);
1877
- for (let pass = 0; pass < passes; pass++) {
1878
- for (let y = 0; y < CH; y++) {
1879
- const row = y * CW;
1880
- let s = 0;
1881
- for (let x = -RB; x <= RB; x++) s += b[row + cl(x, CW)];
1882
- for (let x = 0; x < CW; x++) {
1883
- tmp[row + x] = s * k;
1884
- s += b[row + cl(x + RB + 1, CW)] - b[row + cl(x - RB, CW)];
1885
- }
1886
- }
1887
- for (let x = 0; x < CW; x++) {
1888
- let s = 0;
1889
- for (let y = -RB; y <= RB; y++) s += tmp[cl(y, CH) * CW + x];
1890
- for (let y = 0; y < CH; y++) {
1891
- b[y * CW + x] = s * k;
1892
- s += tmp[cl(y + RB + 1, CH) * CW + x] - tmp[cl(y - RB, CH) * CW + x];
1893
- }
1894
- }
1895
- }
1896
- return b;
1897
- }
1898
- function buildTitleMask(text) {
1899
- const CW = 2048,
1900
- CH = 512,
1901
- c = document.createElement('canvas');
1902
- c.width = CW;
1903
- c.height = CH;
1904
- const g = c.getContext('2d'),
1905
- font = (s) => \`400 \${s}px "Creepster", "Chiller", "Papyrus", Georgia, serif\`;
1906
- const lines = text.split('\\n');
1907
- g.font = font(260);
1908
- const widest = Math.max(...lines.map((l) => g.measureText(l).width));
1909
- const fs = Math.floor(Math.min((260 * 1820) / widest, 400 / (lines.length * 1.12)));
1910
- g.font = font(fs);
1911
- g.textAlign = 'left';
1912
- g.textBaseline = 'middle';
1913
- g.fillStyle = g.strokeStyle = '#fff';
1914
- g.lineJoin = 'round';
1915
- g.lineWidth = fs * 0.02;
1916
- lines.forEach((l, i) => {
1917
- const y = CH / 2 + (i - (lines.length - 1) / 2) * fs * 1.12;
1918
- g.fillText(l, 40, y);
1919
- g.strokeText(l, 40, y);
1920
- }); // left-aligned
1921
- const px = g.getImageData(0, 0, CW, CH).data,
1922
- a = new Float32Array(CW * CH);
1923
- for (let i = 0; i < a.length; i++) a[i] = px[i * 4 + 3] / 255;
1924
- const soft = blurInto(a, CW, CH, 3, 2),
1925
- wide = blurInto(a, CW, CH, 22, 3);
1926
- const data = new Uint8Array(CW * CH * 4);
1927
- for (let y = 0; y < CH; y++)
1928
- for (let x = 0; x < CW; x++) {
1929
- const i = y * CW + x,
1930
- o = ((CH - 1 - y) * CW + x) * 4; // flip: texture rows run bottom-up
1931
- data[o] = a[i] * 255;
1932
- data[o + 1] = soft[i] * 255;
1933
- data[o + 2] = Math.min(1, wide[i] * 1.8) * 255;
1934
- data[o + 3] = 255;
1935
- }
1936
- return { data, CW, CH };
1937
- }
1938
- // </fire-title>
1939
- await Promise.race([
1940
- document.fonts ? document.fonts.load('400 200px "Creepster"') : Promise.resolve(),
1941
- new Promise((res) => setTimeout(res, 3000)),
1942
- ]);
1943
- const titleMask = buildTitleMask('Happy\\nHalloween');
1944
- const titleTex = new THREE.DataTexture(
1945
- titleMask.data,
1946
- titleMask.CW,
1947
- titleMask.CH,
1948
- THREE.RGBAFormat
1949
- );
1950
- titleTex.minFilter = titleTex.magFilter = THREE.LinearFilter;
1951
- titleTex.generateMipmaps = false;
1952
- titleTex.needsUpdate = true;
1953
- const titlePass = new ShaderPass({
1954
- uniforms: {
1955
- tDiffuse: { value: null },
1956
- tMask: { value: titleTex },
1957
- uT: { value: 0 },
1958
- uOn: { value: 0 },
1959
- uAspect: { value: W / H },
1960
- uRect: {
1961
- value: new THREE.Vector4(
1962
- TITLE_LEFT + TITLE_W / 2 - (40 / 2048) * TITLE_W,
1963
- TITLE_CY,
1964
- TITLE_W,
1965
- (TITLE_W * (W / H)) / 4
1966
- ),
1967
- },
1968
- },
1969
- vertexShader: \`varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }\`,
1970
- fragmentShader: TITLE_FRAG,
1971
- });
1972
-
1973
- /* ---------- "Applying Style....": a small loading line at the bottom centre ----------
1974
- Text and dots are drawn once into a mipmapped mask (red = words, green = dots) so they stay
1975
- crisp at a small size; the dots fill in one by one and reset, and a soft shimmer sweeps the words. */
1976
- // <status-line>
1977
- const STATUS_CY = 0.045,
1978
- STATUS_H = 0.031; // centre (from bottom) and rect height, as fractions of the screen
1979
- const STATUS_FRAG = \`
1980
- uniform sampler2D tDiffuse, tStatus; uniform float uT, uOn, uDot0, uDotStep; uniform vec4 uRect; varying vec2 vUv;
1981
- void main(){
1982
- vec3 col = texture2D(tDiffuse, vUv).rgb;
1983
- vec2 q = (vUv - uRect.xy)/uRect.zw + 0.5;
1984
- if (uOn <= 0.0 || q.x < 0.0 || q.x > 1.0 || q.y < -0.2 || q.y > 1.0) { gl_FragColor = vec4(col, 1.0); return; }
1985
- float appear = smoothstep(1.0, 1.8, uT);
1986
- vec4 m = texture2D(tStatus, q);
1987
- float phase = fract(uT/1.6)*5.0, i = floor((q.x - uDot0)/uDotStep);
1988
- float dotA = smoothstep(i + 0.6, i + 1.0, phase)*(1.0 - smoothstep(4.55, 4.95, phase)); // dots fill in, then reset
1989
- float sweep = exp(-pow((q.x - (fract(uT/2.4)*1.6 - 0.3))*7.0, 2.0)); // soft shimmer
1990
- float a = (m.r*(0.6 + 0.4*sweep) + m.g*dotA*0.85)*appear;
1991
- gl_FragColor = vec4(mix(col, vec3(0.93, 0.94, 0.97), a), 1.0);
1992
- }\`;
1993
- function buildStatusMask(words, dots) {
1994
- const CW = 1024,
1995
- CH = 128,
1996
- c = document.createElement('canvas');
1997
- c.width = CW;
1998
- c.height = CH;
1999
- const g = c.getContext('2d');
2000
- g.fillStyle = '#000';
2001
- g.fillRect(0, 0, CW, CH);
2002
- g.font = '300 64px "Helvetica Neue", Helvetica, Arial, sans-serif';
2003
- if ('letterSpacing' in g) g.letterSpacing = '2px';
2004
- g.textBaseline = 'middle';
2005
- const ww = g.measureText(words).width,
2006
- dw = g.measureText('.').width + 4,
2007
- x0 = (CW - (ww + dw * dots)) / 2;
2008
- g.fillStyle = '#f00';
2009
- g.fillText(words, x0, CH / 2);
2010
- g.fillStyle = '#0f0';
2011
- for (let i = 0; i < dots; i++) g.fillText('.', x0 + ww + i * dw, CH / 2);
2012
- const tex = new THREE.CanvasTexture(c);
2013
- tex.colorSpace = THREE.NoColorSpace;
2014
- tex.generateMipmaps = true;
2015
- tex.minFilter = THREE.LinearMipmapLinearFilter;
2016
- tex.magFilter = THREE.LinearFilter;
2017
- tex.anisotropy = 4;
2018
- return { tex, dot0: (x0 + ww) / CW, dotStep: dw / CW, aspect: CW / CH };
2019
- }
2020
- // </status-line>
2021
- const status = buildStatusMask('Applying Style', 4);
2022
- const statusPass = new ShaderPass({
2023
- uniforms: {
2024
- tDiffuse: { value: null },
2025
- tStatus: { value: status.tex },
2026
- uT: { value: 0 },
2027
- uOn: { value: 0 },
2028
- uDot0: { value: status.dot0 },
2029
- uDotStep: { value: status.dotStep },
2030
- uRect: {
2031
- value: new THREE.Vector4(
2032
- 0.5,
2033
- STATUS_CY,
2034
- (STATUS_H * status.aspect) / (W / H),
2035
- STATUS_H
2036
- ),
2037
- },
2038
- },
2039
- vertexShader: \`varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }\`,
2040
- fragmentShader: STATUS_FRAG,
2041
- });
2042
-
2043
- /* ---------- post: MSAA HDR -> depth of field -> ACES -> title -> status -> film ---------- */
2044
- const rt = new THREE.WebGLRenderTarget(W * pixelRatio, H * pixelRatio, {
2045
- type: THREE.HalfFloatType,
2046
- samples: 4,
2047
- });
2048
- const composer = new EffectComposer(renderer, rt);
2049
- composer.addPass(new RenderPass(scene, camera));
2050
- const bokeh = new BokehPass(scene, camera, { focus: 10, aperture: 0.0004, maxblur: 0.009 });
2051
- const bokehRender = bokeh.render.bind(bokeh);
2052
- bokeh.render = (...a) => {
2053
- mistGroup.visible = false;
2054
- sky.visible = false;
2055
- bokehRender(...a);
2056
- mistGroup.visible = true;
2057
- sky.visible = true;
2058
- };
2059
- composer.addPass(bokeh);
2060
- const bloom = new UnrealBloomPass(
2061
- new THREE.Vector2(W * pixelRatio, H * pixelRatio),
2062
- 0.5,
2063
- 0.6,
2064
- 1.3
2065
- );
2066
- composer.addPass(bloom);
2067
- composer.addPass(new OutputPass());
2068
- const filmPass = new ShaderPass({
2069
- uniforms: {
2070
- tDiffuse: { value: null },
2071
- uTime: { value: 0 },
2072
- uFade: { value: 0 },
2073
- uRes: { value: new THREE.Vector2(W, H) },
2074
- },
2075
- vertexShader: \`varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }\`,
2076
- fragmentShader: \`uniform sampler2D tDiffuse; uniform float uTime, uFade; uniform vec2 uRes; varying vec2 vUv;
2077
- float h(vec2 p){ return fract(sin(dot(p, vec2(12.9898, 78.233)))*43758.5453); }
2078
- void main(){ vec2 c = vUv - 0.5; vec2 ca = c*vec2(uRes.x/uRes.y, 1.0); float r2 = dot(ca, ca);
2079
- vec2 off = c*r2*0.006;
2080
- vec3 col = vec3(texture2D(tDiffuse, vUv - off).r, texture2D(tDiffuse, vUv).g, texture2D(tDiffuse, vUv + off).b);
2081
- col *= 1.0 - 0.58*smoothstep(0.12, 0.95, sqrt(r2));
2082
- float g = h(floor(vUv*uRes) + fract(uTime*7.31)*vec2(91.7, 37.3)) - 0.5;
2083
- col *= 1.0 + g*0.11;
2084
- gl_FragColor = vec4(max(col, 0.0)*uFade, 1.0); }\`,
2085
- });
2086
- composer.addPass(titlePass);
2087
- composer.addPass(statusPass);
2088
- composer.addPass(filmPass);
2089
-
2090
- /* ==========================================================================
2091
- SEQUENCE — every value is a pure function of t
2092
- ========================================================================== */
2093
- const tmpV = new THREE.Vector3(),
2094
- faceW = new THREE.Vector3(),
2095
- lookFar = new THREE.Vector3(0, 1.15, -30),
2096
- lookDev = new THREE.Vector3(),
2097
- Yup = new THREE.Vector3(0, 1, 0);
2098
- const fogCol = new THREE.Color();
2099
-
2100
- function update(t) {
2101
- const out = t >= T.out;
2102
- const on = out ? 0 : 1;
2103
-
2104
- // world light: the morning fades up from absolute zero; thin drifting cloud barely varies the sun
2105
- const reveal = smoother(T.revealIn, T.revealFull, t) * on;
2106
- const cloud = 1 + 0.05 * (fbm1(t * 0.35) - 0.5);
2107
- sun.intensity = K.sun * reveal * cloud;
2108
- hemi.intensity = K.hemi * reveal;
2109
- scene.environmentIntensity = K.env * reveal;
2110
- fogCol.copy(FOG_BASE).multiplyScalar(reveal);
2111
- scene.fog.color.copy(fogCol);
2112
- scene.background.copy(fogCol);
2113
- skyMat.uniforms.uFog.value.copy(fogCol);
2114
- skyMat.uniforms.uZenith.value.copy(ZENITH).multiplyScalar(reveal);
2115
- skyMat.uniforms.uSunCol.value.copy(SUN_TINT).multiplyScalar(reveal * cloud);
2116
-
2117
- // the monument wakes: edges and veins glow, the hat hovers and sways above it
2118
- const glowLevel = smoother(2.5, 7.5, t) * on * (0.88 + 0.12 * Math.sin(t * 1.6));
2119
- monUni.uGlow.value = glowLevel;
2120
- monUni.uTime.value = t;
2121
- monLights.forEach(([l, , k]) => {
2122
- l.intensity = K.glow * k * glowLevel;
2123
- });
2124
- hat.position.y = HAT_Y + 0.06 * Math.sin(t * 1.25) + 0.02 * Math.sin(t * 2.7 + 1.0);
2125
- hat.rotation.set(
2126
- 0.04 * Math.sin(t * 0.8 + 0.5),
2127
- 0.25 * Math.sin(t * 0.35),
2128
- -0.08 + 0.05 * Math.sin(t * 0.95)
2129
- );
2130
-
2131
- // pumpkins kindle with the reveal and gutter toward the end
2132
- const fear = smoother(10, 15.5, t);
2133
- for (const p of pumpkins) {
2134
- if (!p.faceMat) continue;
2135
- const f =
2136
- (0.78 + 0.22 * fbm1(t * 3.1 + p.seed) + 0.07 * noise1(t * 11 + p.seed)) *
2137
- (1 - 0.45 * fear);
2138
- const k = smoother(1.6, 6.0, t) * on * f;
2139
- p.faceMat.color.setRGB(2.6 * k, 0.95 * k, 0.22 * k);
2140
- if (p.light) p.light.intensity = K.pump * k;
2141
- }
2142
-
2143
- // camera: one continuous dolly, eased to an almost imperceptible landing
2144
- const cu = easeLong(t / T.out);
2145
- camera.position.set(0, lerp(CAM_Y0, CAM_Y1, cu), lerp(CAM_Z0, CAM_Z1, cu));
2146
- const wLook = smoother(3.5, 13.8, t);
2147
- lookDev.set(0, 1.7, MON_Z);
2148
- camera.lookAt(tmpV.copy(lookFar).lerp(lookDev, wLook));
2149
- camera.updateMatrixWorld();
2150
-
2151
- // derived: focus on the point the camera is looking at
2152
- faceW.set(0, 1.4, MON_Z + 0.3);
2153
- mistUniforms.uTime.value = t;
2154
- mistUniforms.uColor.value.copy(fogCol).multiplyScalar(1.1);
2155
- mistUniforms.uCam.value.copy(camera.position);
2156
- mistUniforms.uCandlePos.value.set(0, 1.2, MON_Z + 0.6);
2157
- mistUniforms.uCandleCol.value.setRGB(0.42, 0.62, 1.0);
2158
- mistUniforms.uCandleI.value = 1.2 * glowLevel;
2159
- sky.position.copy(camera.position);
2160
- const focus = -faceW.clone().applyMatrix4(camera.matrixWorldInverse).z;
2161
- bokeh.uniforms.focus.value = focus;
2162
- bokeh.uniforms.aperture.value = lerp(K.ap0, K.ap1, smoother(5, 15.5, t));
2163
-
2164
- filmPass.uniforms.uTime.value = t;
2165
- filmPass.uniforms.uFade.value = on;
2166
- titlePass.uniforms.uT.value = t - TITLE_T0;
2167
- titlePass.uniforms.uOn.value = on;
2168
- statusPass.uniforms.uT.value = t;
2169
- statusPass.uniforms.uOn.value = on;
2170
- }
2171
-
2172
- /* ---------- drive ---------- */
2173
- function resize() {
2174
- if (RENDER) return;
2175
- W = innerWidth;
2176
- H = innerHeight;
2177
- camera.aspect = W / H;
2178
- camera.updateProjectionMatrix();
2179
- renderer.setPixelRatio(pixelRatio);
2180
- renderer.setSize(W, H);
2181
- composer.setPixelRatio(pixelRatio);
2182
- composer.setSize(W, H);
2183
- bokeh.uniforms.aspect.value = camera.aspect;
2184
- filmPass.uniforms.uRes.value.set(W * pixelRatio, H * pixelRatio);
2185
- titlePass.uniforms.uAspect.value = W / H;
2186
- titlePass.uniforms.uRect.value.w = (TITLE_W * (W / H)) / 4;
2187
- statusPass.uniforms.uRect.value.z = (STATUS_H * status.aspect) / (W / H);
2188
- }
2189
- filmPass.uniforms.uRes.value.set(W * pixelRatio, H * pixelRatio);
2190
- addEventListener('resize', resize);
2191
-
2192
- update(0);
2193
- renderer.compile(scene, camera);
2194
-
2195
- if (RENDER) {
2196
- window.__renderAt = (t) => {
2197
- update(t);
2198
- composer.render();
2199
- return true;
2200
- };
2201
- window.__duration = T.end;
2202
- window.__renderAudio = renderSoundtrack; // base64 WAV, 48 kHz stereo, same timeline
2203
- document.getElementById('hint').remove();
2204
- window.__ready = true;
2205
- } else {
2206
- // Live: the timeline runs on the audio clock so picture and sound stay locked.
2207
- // Browsers only allow sound after a gesture, so it waits on black for a click or key.
2208
- const hint = document.getElementById('hint');
2209
- let actx = null,
2210
- outGain = null,
2211
- cycleOut = null,
2212
- cycleStart = 0,
2213
- nextQueued = false,
2214
- started = false,
2215
- clockStart = 0,
2216
- muted = false;
2217
- let frames = 0,
2218
- acc = 0,
2219
- tuned = false,
2220
- last = 0,
2221
- introCompleteSent = false;
2222
- const schedule = (at) => {
2223
- const g = actx.createGain();
2224
- g.connect(outGain);
2225
- buildSound(actx, at, g);
2226
- return g;
2227
- };
2228
- function begin() {
2229
- hint.classList.add('gone');
2230
- try {
2231
- if (!actx) {
2232
- actx = new (window.AudioContext || window.webkitAudioContext)();
2233
- outGain = actx.createGain();
2234
- outGain.connect(actx.destination);
2235
- }
2236
- actx.resume();
2237
- } catch (e) {
2238
- actx = null;
2239
- }
2240
- if (actx) {
2241
- if (cycleOut) {
2242
- const old = cycleOut;
2243
- old.gain.setTargetAtTime(0, actx.currentTime, 0.04);
2244
- setTimeout(() => old.disconnect(), 800);
2245
- }
2246
- cycleStart = actx.currentTime + 0.15;
2247
- cycleOut = schedule(cycleStart);
2248
- nextQueued = false;
2249
- } else clockStart = performance.now();
2250
- started = true;
2251
- }
2252
- function clock() {
2253
- if (!actx) return (performance.now() - clockStart) / 1000;
2254
- return actx.currentTime - cycleStart - (actx.outputLatency || actx.baseLatency || 0);
2255
- }
2256
- function frame(now) {
2257
- let t = 0;
2258
- if (started) {
2259
- t = clock();
2260
- if (actx && !nextQueued && t > T.loop - 0.6) {
2261
- cycleOut = schedule(cycleStart + T.loop);
2262
- nextQueued = true;
2263
- }
2264
- if (t >= T.loop) {
2265
- if (actx) {
2266
- cycleStart += T.loop;
2267
- nextQueued = false;
2268
- } else clockStart += T.loop * 1000;
2269
- t -= T.loop;
2270
- }
2271
- t = Math.max(0, t);
2272
- }
2273
- update(t);
2274
- composer.render();
2275
- if (EMBED && started && !introCompleteSent && t >= T.end) {
2276
- introCompleteSent = true;
2277
- try {
2278
- window.parent.postMessage(
2279
- { source: 'impact-ui-halloween-intro', type: 'complete' },
2280
- '*'
2281
- );
2282
- } catch (_) {}
2283
- }
2284
- if (started && !tuned && t > 2) {
2285
- acc += now - last;
2286
- frames++;
2287
- if (frames === 90) {
2288
- tuned = true;
2289
- if (acc / frames > 19 && pixelRatio > 1) {
2290
- pixelRatio = 1;
2291
- resize();
2292
- }
2293
- }
2294
- }
2295
- last = now;
2296
- requestAnimationFrame(frame);
2297
- }
2298
- requestAnimationFrame(frame);
2299
- if (EMBED) {
2300
- setTimeout(begin, 150);
2301
- } else {
2302
- addEventListener('pointerdown', begin);
2303
- addEventListener('keydown', (e) => {
2304
- if (e.key === 'm' || e.key === 'M') {
2305
- muted = !muted;
2306
- if (outGain) outGain.gain.setTargetAtTime(muted ? 0 : 1, actx.currentTime, 0.05);
2307
- } else if (e.key === ' ' || e.key === 'Enter') {
2308
- e.preventDefault();
2309
- begin();
2310
- }
2311
- });
2312
- }
2313
- }
2314
- <\/script>
2315
- </body>
2316
- </html>
2317
- `;
2318
- export {
2319
- halloweenCandleLightHtml as default
2320
- };