@underpostnet/underpost 2.99.0 → 2.99.4
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/.env.development +1 -0
- package/.env.production +1 -0
- package/.env.test +1 -0
- package/LICENSE +1 -1
- package/README.md +30 -30
- package/bin/deploy.js +49 -3
- package/cli.md +46 -31
- package/jsconfig.json +4 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +52 -0
- package/package.json +1 -1
- package/scripts/rocky-pwa.sh +2 -2
- package/scripts/ssl.sh +12 -6
- package/src/api/user/user.model.js +1 -0
- package/src/cli/baremetal.js +14 -12
- package/src/cli/deploy.js +7 -25
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +4 -4
- package/src/cli/index.js +24 -1
- package/src/cli/monitor.js +17 -0
- package/src/cli/run.js +229 -123
- package/src/cli/ssh.js +49 -0
- package/src/cli/test.js +13 -1
- package/src/client/components/core/Polyhedron.js +896 -7
- package/src/client/components/core/Translate.js +4 -0
- package/src/client/services/default/default.management.js +12 -2
- package/src/index.js +13 -1
- package/src/runtime/express/Express.js +3 -3
- package/src/server/conf.js +6 -4
- package/src/server/logger.js +11 -4
- package/src/server/process.js +27 -2
- package/src/server/proxy.js +4 -6
- package/src/server/tls.js +31 -26
- package/scripts/ssh-cluster-info.sh +0 -15
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { BtnIcon } from './BtnIcon.js';
|
|
4
4
|
import { getId, random } from './CommonJs.js';
|
|
5
5
|
import { dynamicCol } from './Css.js';
|
|
6
|
-
import { htmls, s } from './VanillaJs.js';
|
|
7
|
-
|
|
6
|
+
import { fullScreenIn, htmls, s } from './VanillaJs.js';
|
|
7
|
+
import { Translate } from './Translate.js';
|
|
8
8
|
// https://css-loaders.com/3d/
|
|
9
9
|
|
|
10
10
|
const Polyhedron = {
|
|
@@ -17,11 +17,647 @@ const Polyhedron = {
|
|
|
17
17
|
ct: [0, 0, 0],
|
|
18
18
|
dim: 150,
|
|
19
19
|
interval: null,
|
|
20
|
+
immersive: false,
|
|
21
|
+
immersiveParticles: null,
|
|
22
|
+
immersiveRAF: null,
|
|
23
|
+
immersiveStart: 0,
|
|
24
|
+
immersiveSeed: random(0, 1000000000),
|
|
25
|
+
immersiveEffect: 'waveLight', // 'waveLight' | 'darkElectronic' | 'prismBloom' | 'noirEmbers'
|
|
26
|
+
faceOpacity: 1,
|
|
27
|
+
chosenFace: 'front',
|
|
28
|
+
faces: {
|
|
29
|
+
front: null,
|
|
30
|
+
back: null,
|
|
31
|
+
left: null,
|
|
32
|
+
right: null,
|
|
33
|
+
top: null,
|
|
34
|
+
bottom: null,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getFaceSelector = (faceName) => `.face_${faceName}-${id}`;
|
|
39
|
+
const applyFaceBackground = (faceName) => {
|
|
40
|
+
const el = s(getFaceSelector(faceName));
|
|
41
|
+
if (!el) return;
|
|
42
|
+
const url = this.Tokens[id].faces?.[faceName];
|
|
43
|
+
|
|
44
|
+
if (url) {
|
|
45
|
+
el.style.backgroundImage = `url("${url}")`;
|
|
46
|
+
|
|
47
|
+
// Always cover the full face area.
|
|
48
|
+
// Using `cover` ensures full coverage even during 3D transforms / resizes.
|
|
49
|
+
el.style.backgroundSize = 'cover';
|
|
50
|
+
el.style.backgroundPosition = 'center center';
|
|
51
|
+
el.style.backgroundRepeat = 'no-repeat';
|
|
52
|
+
} else {
|
|
53
|
+
el.style.backgroundImage = '';
|
|
54
|
+
el.style.backgroundSize = '';
|
|
55
|
+
el.style.backgroundPosition = '';
|
|
56
|
+
el.style.backgroundRepeat = '';
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const applyAllFaceBackgrounds = () => {
|
|
60
|
+
['front', 'back', 'left', 'right', 'top', 'bottom'].forEach(applyFaceBackground);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const applyFaceOpacity = () => {
|
|
64
|
+
const opacity = typeof this.Tokens[id].faceOpacity === 'number' ? this.Tokens[id].faceOpacity : 1;
|
|
65
|
+
const faces = document.querySelectorAll(`.face-${id}`);
|
|
66
|
+
faces.forEach((el) => {
|
|
67
|
+
el.style.opacity = `${opacity}`;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const startImmersiveEffects = () => {
|
|
72
|
+
const scene = s(`.scene-${id}`);
|
|
73
|
+
if (!scene) return;
|
|
74
|
+
|
|
75
|
+
const canvas = s(`.polyhedron-immersive-canvas-${id}`);
|
|
76
|
+
if (!canvas) return;
|
|
77
|
+
|
|
78
|
+
if (!s(`.main-body-btn-ui-close`).classList.contains('hide')) s(`.main-body-btn-ui`).click();
|
|
79
|
+
if (!s(`.main-body-btn-ui-menu-close`).classList.contains('hide')) s(`.main-body-btn-menu`).click();
|
|
80
|
+
fullScreenIn();
|
|
81
|
+
// Ensure canvas is visible during immersive mode.
|
|
82
|
+
canvas.style.display = 'block';
|
|
83
|
+
|
|
84
|
+
const ctx = canvas.getContext('2d');
|
|
85
|
+
if (!ctx) return;
|
|
86
|
+
|
|
87
|
+
const resize = () => {
|
|
88
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
89
|
+
const w = scene.clientWidth || window.innerWidth;
|
|
90
|
+
const h = scene.clientHeight || window.innerHeight;
|
|
91
|
+
canvas.width = Math.floor(w * dpr);
|
|
92
|
+
canvas.height = Math.floor(h * dpr);
|
|
93
|
+
canvas.style.width = `${w}px`;
|
|
94
|
+
canvas.style.height = `${h}px`;
|
|
95
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
resize();
|
|
99
|
+
|
|
100
|
+
const mkParticlesWaveLight = () => {
|
|
101
|
+
const count = 110;
|
|
102
|
+
const w = scene.clientWidth || window.innerWidth;
|
|
103
|
+
const h = scene.clientHeight || window.innerHeight;
|
|
104
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
105
|
+
const r = random(1, 4);
|
|
106
|
+
return {
|
|
107
|
+
x: random(0, w),
|
|
108
|
+
y: random(0, h),
|
|
109
|
+
vx: (random(-100, 100) / 100) * 0.25,
|
|
110
|
+
vy: (random(-100, 100) / 100) * 0.25,
|
|
111
|
+
r,
|
|
112
|
+
phase: random(0, 628) / 100,
|
|
113
|
+
i,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const mkParticlesDarkElectronic = () => {
|
|
119
|
+
const count = 170;
|
|
120
|
+
const w = scene.clientWidth || window.innerWidth;
|
|
121
|
+
const h = scene.clientHeight || window.innerHeight;
|
|
122
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
123
|
+
const r = random(1, 3);
|
|
124
|
+
return {
|
|
125
|
+
x: random(0, w),
|
|
126
|
+
y: random(0, h),
|
|
127
|
+
// slower, more "system-like" motion so the pipe network reads clearly
|
|
128
|
+
vx: (random(-100, 100) / 100) * 0.18,
|
|
129
|
+
vy: (random(-100, 100) / 100) * 0.18,
|
|
130
|
+
r,
|
|
131
|
+
phase: random(0, 628) / 100,
|
|
132
|
+
i,
|
|
133
|
+
// stronger spark makes "nodes" pop
|
|
134
|
+
spark: random(0, 1000) / 1000,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
20
137
|
};
|
|
138
|
+
|
|
139
|
+
// New (light): Prism Bloom — bright prismatic bursts with bloom + gently swirling motion
|
|
140
|
+
const mkParticlesPrismBloom = () => {
|
|
141
|
+
const count = 160;
|
|
142
|
+
const w = scene.clientWidth || window.innerWidth;
|
|
143
|
+
const h = scene.clientHeight || window.innerHeight;
|
|
144
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
145
|
+
const r = random(1, 4);
|
|
146
|
+
return {
|
|
147
|
+
x: random(0, w),
|
|
148
|
+
y: random(0, h),
|
|
149
|
+
vx: (random(-100, 100) / 100) * 0.18,
|
|
150
|
+
vy: (random(-100, 100) / 100) * 0.18,
|
|
151
|
+
r,
|
|
152
|
+
phase: random(0, 628) / 100,
|
|
153
|
+
i,
|
|
154
|
+
// prismatic palette offsets + burst timing
|
|
155
|
+
hueOffset: random(0, 360),
|
|
156
|
+
jitter: random(0, 1000) / 1000,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// New (dark): Noir Embers — smoky dark with warm embers + occasional flicker
|
|
162
|
+
const mkParticlesNoirEmbers = () => {
|
|
163
|
+
// Repurposed into a much more eye-catching "full fire" effect:
|
|
164
|
+
// denser, brighter, more turbulent + extra per-particle state for tongues of flame.
|
|
165
|
+
const count = 240;
|
|
166
|
+
const w = scene.clientWidth || window.innerWidth;
|
|
167
|
+
const h = scene.clientHeight || window.innerHeight;
|
|
168
|
+
return Array.from({ length: count }).map((_, i) => {
|
|
169
|
+
const r = random(1, 4);
|
|
170
|
+
return {
|
|
171
|
+
x: random(0, w),
|
|
172
|
+
y: random(h * 0.25, h + 60),
|
|
173
|
+
vx: (random(-100, 100) / 100) * 0.14,
|
|
174
|
+
vy: (random(-100, 100) / 100) * 0.35,
|
|
175
|
+
r,
|
|
176
|
+
phase: random(0, 628) / 100,
|
|
177
|
+
i,
|
|
178
|
+
heat: random(0, 1000) / 1000,
|
|
179
|
+
flicker: random(0, 1000) / 1000,
|
|
180
|
+
// flame "tongue" shape & motion
|
|
181
|
+
curl: random(0, 1000) / 1000,
|
|
182
|
+
life: random(0, 1000) / 1000,
|
|
183
|
+
// small bias so some particles stretch more than others
|
|
184
|
+
stretch: 0.6 + (random(0, 1000) / 1000) * 1.6,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const resetParticlesForEffect = () => {
|
|
190
|
+
const eff = this.Tokens[id].immersiveEffect || 'waveLight';
|
|
191
|
+
console.error(eff);
|
|
192
|
+
if (eff === 'darkElectronic') this.Tokens[id].immersiveParticles = mkParticlesDarkElectronic();
|
|
193
|
+
else if (eff === 'prismBloom') this.Tokens[id].immersiveParticles = mkParticlesPrismBloom();
|
|
194
|
+
else if (eff === 'noirEmbers') this.Tokens[id].immersiveParticles = mkParticlesNoirEmbers();
|
|
195
|
+
else this.Tokens[id].immersiveParticles = mkParticlesWaveLight();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
resetParticlesForEffect();
|
|
199
|
+
this.Tokens[id].immersiveStart = performance.now();
|
|
200
|
+
|
|
201
|
+
const tick = (t) => {
|
|
202
|
+
if (!this.Tokens[id].immersive) return;
|
|
203
|
+
|
|
204
|
+
// Keep canvas sized to the scene.
|
|
205
|
+
if (canvas.clientWidth !== scene.clientWidth || canvas.clientHeight !== scene.clientHeight) resize();
|
|
206
|
+
|
|
207
|
+
const w2 = canvas.clientWidth || window.innerWidth;
|
|
208
|
+
const h2 = canvas.clientHeight || window.innerHeight;
|
|
209
|
+
|
|
210
|
+
const tt = (t - this.Tokens[id].immersiveStart) / 1000;
|
|
211
|
+
const eff = this.Tokens[id].immersiveEffect || 'waveLight';
|
|
212
|
+
|
|
213
|
+
if (eff === 'darkElectronic') {
|
|
214
|
+
// Dark electronic: GREEN PIPES — grid/pipe network with bright green flow + pulsing nodes
|
|
215
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
216
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
217
|
+
|
|
218
|
+
// Pipe layout (more orthogonal so it reads like piping)
|
|
219
|
+
const base = 120; // green hue
|
|
220
|
+
const step = 52;
|
|
221
|
+
const driftX = Math.sin(tt * 0.35) * 10;
|
|
222
|
+
const driftY = Math.cos(tt * 0.28) * 10;
|
|
223
|
+
|
|
224
|
+
// faint background glow to lift the pipes off black
|
|
225
|
+
ctx.globalAlpha = 0.18;
|
|
226
|
+
const bgG = ctx.createRadialGradient(w2 * 0.5, h2 * 0.5, 0, w2 * 0.5, h2 * 0.5, Math.min(w2, h2) * 0.95);
|
|
227
|
+
bgG.addColorStop(0, `hsla(${base}, 90%, 18%, 0.9)`);
|
|
228
|
+
bgG.addColorStop(1, 'rgba(0,0,0,0)');
|
|
229
|
+
ctx.fillStyle = bgG;
|
|
230
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
231
|
+
ctx.globalAlpha = 1;
|
|
232
|
+
|
|
233
|
+
// scanlines, very subtle
|
|
234
|
+
ctx.globalAlpha = 0.04;
|
|
235
|
+
ctx.fillStyle = 'rgba(255,255,255,1)';
|
|
236
|
+
for (let y = 0; y < h2; y += 3) ctx.fillRect(0, y, w2, 1);
|
|
237
|
+
ctx.globalAlpha = 1;
|
|
238
|
+
|
|
239
|
+
// Pipes: horizontal lines
|
|
240
|
+
ctx.lineWidth = 2;
|
|
241
|
+
for (let y = 0; y <= h2 + step; y += step) {
|
|
242
|
+
const yy = y + (Math.sin(tt * 0.6 + y * 0.02) * 2 + driftY);
|
|
243
|
+
// glow pass
|
|
244
|
+
ctx.globalAlpha = 0.15;
|
|
245
|
+
ctx.strokeStyle = `hsla(${base}, 100%, 70%, 1)`;
|
|
246
|
+
ctx.beginPath();
|
|
247
|
+
ctx.moveTo(0, yy);
|
|
248
|
+
ctx.lineTo(w2, yy);
|
|
249
|
+
ctx.stroke();
|
|
250
|
+
|
|
251
|
+
// core pass
|
|
252
|
+
ctx.globalAlpha = 0.35;
|
|
253
|
+
ctx.strokeStyle = `hsla(${base}, 100%, 55%, 1)`;
|
|
254
|
+
ctx.beginPath();
|
|
255
|
+
ctx.moveTo(0, yy);
|
|
256
|
+
ctx.lineTo(w2, yy);
|
|
257
|
+
ctx.stroke();
|
|
258
|
+
}
|
|
259
|
+
ctx.globalAlpha = 1;
|
|
260
|
+
|
|
261
|
+
// Pipes: vertical lines
|
|
262
|
+
for (let x = 0; x <= w2 + step; x += step) {
|
|
263
|
+
const xx = x + (Math.cos(tt * 0.55 + x * 0.02) * 2 + driftX);
|
|
264
|
+
// glow pass
|
|
265
|
+
ctx.globalAlpha = 0.15;
|
|
266
|
+
ctx.strokeStyle = `hsla(${base}, 100%, 70%, 1)`;
|
|
267
|
+
ctx.beginPath();
|
|
268
|
+
ctx.moveTo(xx, 0);
|
|
269
|
+
ctx.lineTo(xx, h2);
|
|
270
|
+
ctx.stroke();
|
|
271
|
+
|
|
272
|
+
// core pass
|
|
273
|
+
ctx.globalAlpha = 0.35;
|
|
274
|
+
ctx.strokeStyle = `hsla(${base}, 100%, 55%, 1)`;
|
|
275
|
+
ctx.beginPath();
|
|
276
|
+
ctx.moveTo(xx, 0);
|
|
277
|
+
ctx.lineTo(xx, h2);
|
|
278
|
+
ctx.stroke();
|
|
279
|
+
}
|
|
280
|
+
ctx.globalAlpha = 1;
|
|
281
|
+
|
|
282
|
+
// Particles as flowing "packets" and junction nodes
|
|
283
|
+
const ps = this.Tokens[id].immersiveParticles || [];
|
|
284
|
+
for (const p of ps) {
|
|
285
|
+
// quantize positions toward pipe lanes so motion reads as "through pipes"
|
|
286
|
+
const laneX = Math.round(p.x / step) * step;
|
|
287
|
+
const laneY = Math.round(p.y / step) * step;
|
|
288
|
+
|
|
289
|
+
// drift toward nearest lane
|
|
290
|
+
p.x += (laneX - p.x) * 0.04;
|
|
291
|
+
p.y += (laneY - p.y) * 0.04;
|
|
292
|
+
|
|
293
|
+
// move slowly along lanes
|
|
294
|
+
p.x += p.vx * 60;
|
|
295
|
+
p.y += p.vy * 60;
|
|
296
|
+
|
|
297
|
+
if (p.x < -20) p.x = w2 + 20;
|
|
298
|
+
if (p.x > w2 + 20) p.x = -20;
|
|
299
|
+
if (p.y < -20) p.y = h2 + 20;
|
|
300
|
+
if (p.y > h2 + 20) p.y = -20;
|
|
301
|
+
|
|
302
|
+
const sparkle = (0.25 + 0.75 * Math.max(0, Math.sin(tt * 5.2 + p.phase))) * (0.55 + p.spark);
|
|
303
|
+
|
|
304
|
+
// packet glow
|
|
305
|
+
ctx.beginPath();
|
|
306
|
+
ctx.fillStyle = `hsla(${base}, 100%, 72%, ${0.08 + sparkle * 0.16})`;
|
|
307
|
+
ctx.arc(p.x, p.y, p.r * 2.6, 0, Math.PI * 2);
|
|
308
|
+
ctx.fill();
|
|
309
|
+
|
|
310
|
+
// packet core
|
|
311
|
+
ctx.beginPath();
|
|
312
|
+
ctx.fillStyle = `hsla(${base + 10}, 100%, 62%, ${0.12 + sparkle * 0.22})`;
|
|
313
|
+
ctx.arc(p.x, p.y, Math.max(1, p.r * 1.05), 0, Math.PI * 2);
|
|
314
|
+
ctx.fill();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Vignette
|
|
318
|
+
const vg = ctx.createRadialGradient(
|
|
319
|
+
w2 / 2,
|
|
320
|
+
h2 / 2,
|
|
321
|
+
Math.min(w2, h2) * 0.12,
|
|
322
|
+
w2 / 2,
|
|
323
|
+
h2 / 2,
|
|
324
|
+
Math.min(w2, h2) * 0.82,
|
|
325
|
+
);
|
|
326
|
+
vg.addColorStop(0, 'rgba(0,0,0,0)');
|
|
327
|
+
vg.addColorStop(1, 'rgba(0,0,0,0.75)');
|
|
328
|
+
ctx.fillStyle = vg;
|
|
329
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
330
|
+
|
|
331
|
+
// Polyhedron motion (calmer so the pipe aesthetic stays readable)
|
|
332
|
+
const drift = 9;
|
|
333
|
+
this.Tokens[id].cr[1] += 0.44;
|
|
334
|
+
this.Tokens[id].cr[0] += 0.14;
|
|
335
|
+
this.Tokens[id].cr[2] += 0.08;
|
|
336
|
+
this.Tokens[id].ct[0] = Math.sin(tt * 0.65) * drift;
|
|
337
|
+
this.Tokens[id].ct[1] = Math.cos(tt * 0.45) * drift;
|
|
338
|
+
this.Tokens[id].ct[2] = Math.sin(tt * 0.35) * (drift * 0.65);
|
|
339
|
+
} else if (eff === 'prismBloom') {
|
|
340
|
+
// Prism Bloom (light): COOL-CORES — soft light/white wave-lite style, slower particles
|
|
341
|
+
const tt2 = tt;
|
|
342
|
+
|
|
343
|
+
// cool white background with a gentle wave tint (very subtle blues)
|
|
344
|
+
const bg = ctx.createLinearGradient(0, 0, w2, h2);
|
|
345
|
+
bg.addColorStop(0, `hsla(205, 45%, 96%, 1)`);
|
|
346
|
+
bg.addColorStop(0.5, `hsla(215, 35%, 98%, 1)`);
|
|
347
|
+
bg.addColorStop(1, `hsla(195, 40%, 95%, 1)`);
|
|
348
|
+
ctx.fillStyle = bg;
|
|
349
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
350
|
+
|
|
351
|
+
// soft wave bands
|
|
352
|
+
ctx.globalAlpha = 0.12;
|
|
353
|
+
ctx.lineWidth = 2;
|
|
354
|
+
for (let y = 0; y < h2; y += 42) {
|
|
355
|
+
const hueW = 200 + Math.sin(tt2 * 0.35 + y * 0.02) * 10;
|
|
356
|
+
ctx.strokeStyle = `hsla(${hueW}, 55%, 72%, 1)`;
|
|
357
|
+
ctx.beginPath();
|
|
358
|
+
for (let x = 0; x <= w2; x += 28) {
|
|
359
|
+
const yy = y + Math.sin(tt2 * 0.9 + x * 0.02 + y * 0.03) * 10;
|
|
360
|
+
if (x === 0) ctx.moveTo(x, yy);
|
|
361
|
+
else ctx.lineTo(x, yy);
|
|
362
|
+
}
|
|
363
|
+
ctx.stroke();
|
|
364
|
+
}
|
|
365
|
+
ctx.globalAlpha = 1;
|
|
366
|
+
|
|
367
|
+
const ps = this.Tokens[id].immersiveParticles || [];
|
|
368
|
+
for (const p of ps) {
|
|
369
|
+
// slow drifting, wave-like
|
|
370
|
+
p.x += (p.vx * 0.55 + Math.sin(tt2 * 0.55 + p.phase) * 0.06) * 60;
|
|
371
|
+
p.y += (p.vy * 0.55 + Math.cos(tt2 * 0.45 + p.phase) * 0.06) * 60;
|
|
372
|
+
|
|
373
|
+
if (p.x < -30) p.x = w2 + 30;
|
|
374
|
+
if (p.x > w2 + 30) p.x = -30;
|
|
375
|
+
if (p.y < -30) p.y = h2 + 30;
|
|
376
|
+
if (p.y > h2 + 30) p.y = -30;
|
|
377
|
+
|
|
378
|
+
const pulse = 0.35 + 0.65 * Math.max(0, Math.sin(tt2 * 1.5 + p.phase + p.jitter * 2));
|
|
379
|
+
|
|
380
|
+
// cool core colors (icy blue -> lavender)
|
|
381
|
+
const hueP = (205 + (p.hueOffset % 40) + Math.sin(tt2 * 0.25 + p.phase) * 8) % 360;
|
|
382
|
+
|
|
383
|
+
// outer soft glow
|
|
384
|
+
ctx.beginPath();
|
|
385
|
+
ctx.fillStyle = `hsla(${hueP}, 70%, 75%, ${0.06 + pulse * 0.08})`;
|
|
386
|
+
ctx.arc(p.x, p.y, p.r * 3.0, 0, Math.PI * 2);
|
|
387
|
+
ctx.fill();
|
|
388
|
+
|
|
389
|
+
// bright cool core
|
|
390
|
+
ctx.beginPath();
|
|
391
|
+
ctx.fillStyle = `hsla(${hueP}, 65%, 86%, ${0.11 + pulse * 0.14})`;
|
|
392
|
+
ctx.arc(p.x, p.y, Math.max(1.0, p.r * 1.1), 0, Math.PI * 2);
|
|
393
|
+
ctx.fill();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// very light vignette so white background still has depth
|
|
397
|
+
const vg = ctx.createRadialGradient(
|
|
398
|
+
w2 / 2,
|
|
399
|
+
h2 / 2,
|
|
400
|
+
Math.min(w2, h2) * 0.25,
|
|
401
|
+
w2 / 2,
|
|
402
|
+
h2 / 2,
|
|
403
|
+
Math.min(w2, h2) * 0.95,
|
|
404
|
+
);
|
|
405
|
+
vg.addColorStop(0, 'rgba(255,255,255,0)');
|
|
406
|
+
vg.addColorStop(1, 'rgba(30,40,60,0.16)');
|
|
407
|
+
ctx.fillStyle = vg;
|
|
408
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
409
|
+
|
|
410
|
+
const drift = 8;
|
|
411
|
+
this.Tokens[id].cr[1] += 0.34;
|
|
412
|
+
this.Tokens[id].cr[0] += 0.11;
|
|
413
|
+
this.Tokens[id].ct[0] = Math.sin(tt2 * 0.6) * drift;
|
|
414
|
+
this.Tokens[id].ct[1] = Math.cos(tt2 * 0.45) * drift;
|
|
415
|
+
this.Tokens[id].ct[2] = Math.sin(tt2 * 0.38) * (drift * 0.6);
|
|
416
|
+
} else if (eff === 'noirEmbers') {
|
|
417
|
+
// FULL FIRE (replaces Noir Embers): bright, high-intensity flame field + hot cores + tongues of fire.
|
|
418
|
+
// Goal: unmistakably "on fire" and eye-catching, not a subtle ember drift.
|
|
419
|
+
ctx.fillStyle = 'rgba(0,0,0,1)';
|
|
420
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
421
|
+
|
|
422
|
+
// Base flame bed (glowing furnace at the bottom)
|
|
423
|
+
const bed = ctx.createRadialGradient(w2 * 0.5, h2 * 1.05, 0, w2 * 0.5, h2 * 1.05, Math.min(w2, h2) * 1.05);
|
|
424
|
+
bed.addColorStop(0, 'hsla(38, 100%, 55%, 0.85)');
|
|
425
|
+
bed.addColorStop(0.35, 'hsla(18, 100%, 42%, 0.65)');
|
|
426
|
+
bed.addColorStop(0.8, 'hsla(8, 100%, 18%, 0.25)');
|
|
427
|
+
bed.addColorStop(1, 'rgba(0,0,0,0)');
|
|
428
|
+
ctx.globalAlpha = 0.9;
|
|
429
|
+
ctx.fillStyle = bed;
|
|
430
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
431
|
+
ctx.globalAlpha = 1;
|
|
432
|
+
|
|
433
|
+
// Heat haze / smoke-lace near the top to add depth (still subtle, fire stays dominant)
|
|
434
|
+
const haze = ctx.createLinearGradient(0, 0, 0, h2);
|
|
435
|
+
haze.addColorStop(0, 'rgba(0,0,0,0.55)');
|
|
436
|
+
haze.addColorStop(0.35, 'rgba(0,0,0,0.18)');
|
|
437
|
+
haze.addColorStop(1, 'rgba(0,0,0,0)');
|
|
438
|
+
ctx.globalAlpha = 0.55;
|
|
439
|
+
ctx.fillStyle = haze;
|
|
440
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
441
|
+
ctx.globalAlpha = 1;
|
|
442
|
+
|
|
443
|
+
// Flame tongues + sparks
|
|
444
|
+
const ps = this.Tokens[id].immersiveParticles || [];
|
|
445
|
+
for (const p of ps) {
|
|
446
|
+
// life cycles: respawn near bottom with new heat/curl to keep it lively
|
|
447
|
+
p.life += 0.012 + p.flicker * 0.01;
|
|
448
|
+
if (p.life >= 1 || p.y < -80) {
|
|
449
|
+
p.x = random(-20, w2 + 20);
|
|
450
|
+
p.y = random(h2 * 0.45, h2 + 80);
|
|
451
|
+
p.vx = (random(-100, 100) / 100) * 0.16;
|
|
452
|
+
p.vy = (random(-100, 100) / 100) * 0.42;
|
|
453
|
+
p.heat = random(0, 1000) / 1000;
|
|
454
|
+
p.flicker = random(0, 1000) / 1000;
|
|
455
|
+
p.curl = random(0, 1000) / 1000;
|
|
456
|
+
p.life = 0;
|
|
457
|
+
p.stretch = 0.6 + (random(0, 1000) / 1000) * 1.6;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Rising motion + turbulence (tongues curl side to side)
|
|
461
|
+
const curl = Math.sin(tt * (1.8 + p.curl * 1.6) + p.phase) * (0.12 + p.curl * 0.18);
|
|
462
|
+
const wag = Math.sin(tt * 6.5 + p.phase + p.flicker * 10) * 0.06;
|
|
463
|
+
p.x += (p.vx + curl + wag) * 60;
|
|
464
|
+
p.y -= (0.25 + Math.abs(p.vy)) * 58;
|
|
465
|
+
|
|
466
|
+
if (p.x < -60) p.x = w2 + 60;
|
|
467
|
+
if (p.x > w2 + 60) p.x = -60;
|
|
468
|
+
|
|
469
|
+
// Intensity ramps up then tapers (flame tongue shape)
|
|
470
|
+
const ramp = Math.sin(Math.min(1, p.life) * Math.PI); // 0..1..0
|
|
471
|
+
const flick = 0.45 + 0.55 * Math.max(0, Math.sin(tt * (8 + p.flicker * 7) + p.phase));
|
|
472
|
+
const heat = 0.25 + 0.75 * p.heat;
|
|
473
|
+
|
|
474
|
+
// Color: deep red -> orange -> yellow-white hot core
|
|
475
|
+
const hueP = 10 + heat * 35; // 10..45
|
|
476
|
+
const lumCore = 70 + heat * 20; // 70..90
|
|
477
|
+
const lumGlow = 45 + heat * 20; // 45..65
|
|
478
|
+
|
|
479
|
+
// Tongue body (stretched vertical glow)
|
|
480
|
+
const tongueH = (14 + heat * 34) * p.stretch * (0.5 + ramp);
|
|
481
|
+
const tongueW = (3 + heat * 4) * (0.7 + ramp);
|
|
482
|
+
|
|
483
|
+
ctx.globalAlpha = 0.06 + ramp * 0.16;
|
|
484
|
+
ctx.fillStyle = `hsla(${hueP}, 100%, ${lumGlow}%, 1)`;
|
|
485
|
+
ctx.beginPath();
|
|
486
|
+
ctx.ellipse(p.x, p.y, tongueW * 2.2, tongueH * 1.05, 0, 0, Math.PI * 2);
|
|
487
|
+
ctx.fill();
|
|
488
|
+
ctx.globalAlpha = 1;
|
|
489
|
+
|
|
490
|
+
// Hot core
|
|
491
|
+
ctx.beginPath();
|
|
492
|
+
ctx.fillStyle = `hsla(${hueP + 8}, 100%, ${lumCore}%, ${0.1 + ramp * 0.22 * flick})`;
|
|
493
|
+
ctx.arc(p.x, p.y, Math.max(1.2, p.r * (1.2 + heat * 0.9)), 0, Math.PI * 2);
|
|
494
|
+
ctx.fill();
|
|
495
|
+
|
|
496
|
+
// White-hot sparkles (a few particles become bright sparks)
|
|
497
|
+
const sparkChance = p.i % 9 === 0 ? 1 : 0;
|
|
498
|
+
if (sparkChance) {
|
|
499
|
+
ctx.beginPath();
|
|
500
|
+
ctx.fillStyle = `hsla(55, 100%, 92%, ${0.08 + ramp * 0.22 * flick})`;
|
|
501
|
+
ctx.arc(p.x + curl * 80, p.y - ramp * 18, Math.max(0.8, p.r * 0.75), 0, Math.PI * 2);
|
|
502
|
+
ctx.fill();
|
|
503
|
+
|
|
504
|
+
// tiny upward streak
|
|
505
|
+
ctx.globalAlpha = 0.06 + ramp * 0.12;
|
|
506
|
+
ctx.strokeStyle = `hsla(48, 100%, 85%, 1)`;
|
|
507
|
+
ctx.lineWidth = 1;
|
|
508
|
+
ctx.beginPath();
|
|
509
|
+
ctx.moveTo(p.x, p.y);
|
|
510
|
+
ctx.lineTo(p.x + curl * 50, p.y - 22 - heat * 18);
|
|
511
|
+
ctx.stroke();
|
|
512
|
+
ctx.globalAlpha = 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Stronger vignette to keep fire contrast & "cinematic" intensity
|
|
517
|
+
const vg = ctx.createRadialGradient(
|
|
518
|
+
w2 / 2,
|
|
519
|
+
h2 / 2,
|
|
520
|
+
Math.min(w2, h2) * 0.12,
|
|
521
|
+
w2 / 2,
|
|
522
|
+
h2 / 2,
|
|
523
|
+
Math.min(w2, h2) * 0.92,
|
|
524
|
+
);
|
|
525
|
+
vg.addColorStop(0, 'rgba(0,0,0,0)');
|
|
526
|
+
vg.addColorStop(1, 'rgba(0,0,0,0.78)');
|
|
527
|
+
ctx.fillStyle = vg;
|
|
528
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
529
|
+
|
|
530
|
+
// Slightly more energetic polyhedron motion to match the intensity
|
|
531
|
+
const drift = 9;
|
|
532
|
+
this.Tokens[id].cr[1] += 0.46;
|
|
533
|
+
this.Tokens[id].cr[0] += 0.15;
|
|
534
|
+
this.Tokens[id].cr[2] += 0.08;
|
|
535
|
+
this.Tokens[id].ct[0] = Math.sin(tt * 0.7) * drift;
|
|
536
|
+
this.Tokens[id].ct[1] = Math.cos(tt * 0.5) * drift;
|
|
537
|
+
this.Tokens[id].ct[2] = Math.sin(tt * 0.42) * (drift * 0.65);
|
|
538
|
+
} else {
|
|
539
|
+
// Wave light: animated gradient + soft floaty particles (brighter particles)
|
|
540
|
+
const hueA = (tt * 18 + this.Tokens[id].immersiveSeed) % 360;
|
|
541
|
+
const hueB = (hueA + 90) % 360;
|
|
542
|
+
|
|
543
|
+
const g = ctx.createLinearGradient(0, 0, w2, h2);
|
|
544
|
+
g.addColorStop(0, `hsla(${hueA}, 78%, 12%, 1)`);
|
|
545
|
+
g.addColorStop(1, `hsla(${hueB}, 78%, 12%, 1)`);
|
|
546
|
+
ctx.fillStyle = g;
|
|
547
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
548
|
+
|
|
549
|
+
const ps = this.Tokens[id].immersiveParticles || [];
|
|
550
|
+
for (const p of ps) {
|
|
551
|
+
p.x += p.vx * 60;
|
|
552
|
+
p.y += p.vy * 60;
|
|
553
|
+
|
|
554
|
+
if (p.x < -10) p.x = w2 + 10;
|
|
555
|
+
if (p.x > w2 + 10) p.x = -10;
|
|
556
|
+
if (p.y < -10) p.y = h2 + 10;
|
|
557
|
+
if (p.y > h2 + 10) p.y = -10;
|
|
558
|
+
|
|
559
|
+
const alpha = 0.22 + 0.16 * Math.sin(tt * 1.7 + p.phase);
|
|
560
|
+
const hue = (hueA + p.i * 2) % 360;
|
|
561
|
+
|
|
562
|
+
// glow
|
|
563
|
+
ctx.beginPath();
|
|
564
|
+
ctx.fillStyle = `hsla(${hue}, 95%, 78%, ${alpha * 0.45})`;
|
|
565
|
+
ctx.arc(p.x, p.y, p.r * 2.4, 0, Math.PI * 2);
|
|
566
|
+
ctx.fill();
|
|
567
|
+
|
|
568
|
+
// core
|
|
569
|
+
ctx.beginPath();
|
|
570
|
+
ctx.fillStyle = `hsla(${hue}, 90%, 72%, ${alpha})`;
|
|
571
|
+
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
572
|
+
ctx.fill();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const vg = ctx.createRadialGradient(
|
|
576
|
+
w2 / 2,
|
|
577
|
+
h2 / 2,
|
|
578
|
+
Math.min(w2, h2) * 0.15,
|
|
579
|
+
w2 / 2,
|
|
580
|
+
h2 / 2,
|
|
581
|
+
Math.min(w2, h2) * 0.75,
|
|
582
|
+
);
|
|
583
|
+
vg.addColorStop(0, 'rgba(0,0,0,0)');
|
|
584
|
+
vg.addColorStop(1, 'rgba(0,0,0,0.55)');
|
|
585
|
+
ctx.fillStyle = vg;
|
|
586
|
+
ctx.fillRect(0, 0, w2, h2);
|
|
587
|
+
|
|
588
|
+
const drift = 8;
|
|
589
|
+
this.Tokens[id].cr[1] += 0.35;
|
|
590
|
+
this.Tokens[id].cr[0] += 0.12;
|
|
591
|
+
this.Tokens[id].ct[0] = Math.sin(tt * 0.6) * drift;
|
|
592
|
+
this.Tokens[id].ct[1] = Math.cos(tt * 0.45) * drift;
|
|
593
|
+
this.Tokens[id].ct[2] = Math.sin(tt * 0.35) * (drift * 0.6);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Keep face backgrounds synced to current face pixel sizing
|
|
597
|
+
// (important when immersive resize happens).
|
|
598
|
+
applyAllFaceBackgrounds();
|
|
599
|
+
|
|
600
|
+
// Apply transformation now (outside the 200ms interval) for smooth motion.
|
|
601
|
+
if (s(`.polyhedron-${id}`)) renderTransform();
|
|
602
|
+
|
|
603
|
+
this.Tokens[id].immersiveRAF = requestAnimationFrame(tick);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
if (this.Tokens[id].immersiveRAF) cancelAnimationFrame(this.Tokens[id].immersiveRAF);
|
|
607
|
+
this.Tokens[id].immersiveRAF = requestAnimationFrame(tick);
|
|
608
|
+
|
|
609
|
+
// Resize handler while immersive
|
|
610
|
+
const onResize = () => {
|
|
611
|
+
if (!this.Tokens[id].immersive) return;
|
|
612
|
+
resize();
|
|
613
|
+
};
|
|
614
|
+
window.removeEventListener('resize', onResize);
|
|
615
|
+
window.addEventListener('resize', onResize);
|
|
616
|
+
this.Tokens[id].immersiveOnResize = onResize;
|
|
617
|
+
|
|
618
|
+
// Expose reset so the effect toggle can rebuild particles quickly.
|
|
619
|
+
this.Tokens[id]._resetImmersiveParticles = resetParticlesForEffect;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const stopImmersiveEffects = () => {
|
|
623
|
+
if (this.Tokens[id].immersiveRAF) cancelAnimationFrame(this.Tokens[id].immersiveRAF);
|
|
624
|
+
this.Tokens[id].immersiveRAF = null;
|
|
625
|
+
this.Tokens[id].immersiveParticles = null;
|
|
626
|
+
|
|
627
|
+
if (this.Tokens[id].immersiveOnResize) {
|
|
628
|
+
window.removeEventListener('resize', this.Tokens[id].immersiveOnResize);
|
|
629
|
+
this.Tokens[id].immersiveOnResize = null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Clear + hide canvas so the last rendered frame is not left visible.
|
|
633
|
+
const canvas = s(`.polyhedron-immersive-canvas-${id}`);
|
|
634
|
+
if (canvas) {
|
|
635
|
+
const ctx = canvas.getContext('2d');
|
|
636
|
+
if (ctx) ctx.clearRect(0, 0, canvas.width || 0, canvas.height || 0);
|
|
637
|
+
canvas.style.display = 'none';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Reset drift so manual controls feel normal again
|
|
641
|
+
this.Tokens[id].ct = [0, 0, 0];
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const setImmersive = (isImmersive) => {
|
|
645
|
+
this.Tokens[id].immersive = !!isImmersive;
|
|
646
|
+
const scene = s(`.scene-${id}`);
|
|
647
|
+
if (!scene) return;
|
|
648
|
+
|
|
649
|
+
if (this.Tokens[id].immersive) {
|
|
650
|
+
scene.classList.add(`scene-immersive-${id}`);
|
|
651
|
+
startImmersiveEffects();
|
|
652
|
+
} else {
|
|
653
|
+
scene.classList.remove(`scene-immersive-${id}`);
|
|
654
|
+
stopImmersiveEffects();
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
21
658
|
const renderTransform = () => {
|
|
22
|
-
s(
|
|
23
|
-
|
|
24
|
-
).style.transform = `rotateX(${this.Tokens[id].cr[0]}deg) rotateY(${this.Tokens[id].cr[1]}deg) rotateZ(${this.Tokens[id].cr[2]}deg)
|
|
659
|
+
s(`.polyhedron-${id}`).style.transform =
|
|
660
|
+
`rotateX(${this.Tokens[id].cr[0]}deg) rotateY(${this.Tokens[id].cr[1]}deg) rotateZ(${this.Tokens[id].cr[2]}deg)
|
|
25
661
|
translateX(${this.Tokens[id].ct[0]}px) translateY(${this.Tokens[id].ct[1]}px) translateZ(${this.Tokens[id].ct[2]}px)`;
|
|
26
662
|
s(`.polyhedron-${id}`).style.left = `${s(`.scene-${id}`).offsetWidth / 2 - this.Tokens[id].dim / 2}px`;
|
|
27
663
|
s(`.polyhedron-${id}`).style.top = `${s(`.scene-${id}`).offsetHeight / 2 - this.Tokens[id].dim / 2}px`;
|
|
@@ -45,6 +681,9 @@ const Polyhedron = {
|
|
|
45
681
|
setTimeout(() => {
|
|
46
682
|
renderTransform();
|
|
47
683
|
s(`.polyhedron-${id}`).style.transition = `.4s`;
|
|
684
|
+
applyAllFaceBackgrounds();
|
|
685
|
+
applyFaceOpacity();
|
|
686
|
+
setImmersive(this.Tokens[id].immersive);
|
|
48
687
|
|
|
49
688
|
s(`.btn-polyhedron-rotate-down-${id}`).onclick = () => {
|
|
50
689
|
this.Tokens[id].cr[0] += 45;
|
|
@@ -65,14 +704,108 @@ const Polyhedron = {
|
|
|
65
704
|
s(`.btn-polyhedron-remove-zoom-${id}`).onclick = () => {
|
|
66
705
|
this.Tokens[id].dim -= 25;
|
|
67
706
|
};
|
|
707
|
+
|
|
708
|
+
const facePicker = s(`.polyhedron-face-picker-${id}`);
|
|
709
|
+
if (facePicker) {
|
|
710
|
+
// Keep UI and state in sync on re-render
|
|
711
|
+
facePicker.value = this.Tokens[id].chosenFace || 'front';
|
|
712
|
+
|
|
713
|
+
facePicker.onchange = (e) => {
|
|
714
|
+
this.Tokens[id].chosenFace = e?.target?.value || 'front';
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const opacitySlider = s(`.polyhedron-face-opacity-${id}`);
|
|
719
|
+
if (opacitySlider) {
|
|
720
|
+
// Keep UI and state in sync on re-render
|
|
721
|
+
opacitySlider.value = `${this.Tokens[id].faceOpacity}`;
|
|
722
|
+
|
|
723
|
+
opacitySlider.oninput = (e) => {
|
|
724
|
+
const v = parseFloat(e?.target?.value);
|
|
725
|
+
this.Tokens[id].faceOpacity = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
726
|
+
applyFaceOpacity();
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const fileInput = s(`.polyhedron-face-image-file-${id}`);
|
|
731
|
+
if (fileInput) {
|
|
732
|
+
fileInput.onchange = (e) => {
|
|
733
|
+
const file = e?.target?.files?.[0];
|
|
734
|
+
if (!file) return;
|
|
735
|
+
|
|
736
|
+
const url = URL.createObjectURL(file);
|
|
737
|
+
const face = this.Tokens[id].chosenFace || 'front';
|
|
738
|
+
|
|
739
|
+
if (face === 'all') {
|
|
740
|
+
['front', 'back', 'left', 'right', 'top', 'bottom'].forEach((f) => {
|
|
741
|
+
this.Tokens[id].faces[f] = url;
|
|
742
|
+
});
|
|
743
|
+
applyAllFaceBackgrounds();
|
|
744
|
+
} else {
|
|
745
|
+
this.Tokens[id].faces[face] = url;
|
|
746
|
+
applyFaceBackground(face);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
fileInput.value = '';
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const clearFaceBtn = s(`.btn-polyhedron-clear-face-image-${id}`);
|
|
754
|
+
if (clearFaceBtn) {
|
|
755
|
+
clearFaceBtn.onclick = () => {
|
|
756
|
+
const face = this.Tokens[id].chosenFace || 'front';
|
|
757
|
+
|
|
758
|
+
if (face === 'all') {
|
|
759
|
+
['front', 'back', 'left', 'right', 'top', 'bottom'].forEach((f) => {
|
|
760
|
+
this.Tokens[id].faces[f] = null;
|
|
761
|
+
});
|
|
762
|
+
applyAllFaceBackgrounds();
|
|
763
|
+
} else {
|
|
764
|
+
this.Tokens[id].faces[face] = null;
|
|
765
|
+
applyFaceBackground(face);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const immersiveBtn = s(`.btn-polyhedron-immersive-${id}`);
|
|
771
|
+
if (immersiveBtn) {
|
|
772
|
+
immersiveBtn.onclick = () => setImmersive(!this.Tokens[id].immersive);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const immersiveExitBtn = s(`.btn-polyhedron-immersive-exit-${id}`);
|
|
776
|
+
if (immersiveExitBtn) {
|
|
777
|
+
immersiveExitBtn.onclick = () => setImmersive(false);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const immersiveEffectBtn = s(`.btn-polyhedron-immersive-effect-${id}`);
|
|
781
|
+
if (immersiveEffectBtn) {
|
|
782
|
+
immersiveEffectBtn.onclick = () => {
|
|
783
|
+
const order = ['waveLight', 'prismBloom', 'darkElectronic', 'noirEmbers'];
|
|
784
|
+
const curr = this.Tokens[id].immersiveEffect || 'waveLight';
|
|
785
|
+
const idx = order.indexOf(curr);
|
|
786
|
+
this.Tokens[id].immersiveEffect = order[(idx + 1) % order.length] || 'waveLight';
|
|
787
|
+
|
|
788
|
+
if (this.Tokens[id].immersive && this.Tokens[id]._resetImmersiveParticles)
|
|
789
|
+
this.Tokens[id]._resetImmersiveParticles();
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const escHandler = (ev) => {
|
|
794
|
+
if (ev?.key === 'Escape' && this.Tokens[id].immersive) setImmersive(false);
|
|
795
|
+
};
|
|
796
|
+
document.removeEventListener('keydown', escHandler);
|
|
797
|
+
document.addEventListener('keydown', escHandler);
|
|
68
798
|
});
|
|
799
|
+
|
|
69
800
|
return html`
|
|
70
801
|
<style>
|
|
71
802
|
.scene-${id} {
|
|
72
|
-
height: 500px;
|
|
73
803
|
background: #c7c7c7;
|
|
74
804
|
overflow: hidden;
|
|
805
|
+
padding-bottom: 400px;
|
|
806
|
+
box-sizing: border-box;
|
|
75
807
|
/* perspective: 10000px; */
|
|
808
|
+
position: relative;
|
|
76
809
|
}
|
|
77
810
|
.polyhedron-${id} {
|
|
78
811
|
transform-style: preserve-3d;
|
|
@@ -81,6 +814,118 @@ const Polyhedron = {
|
|
|
81
814
|
.face-${id} {
|
|
82
815
|
width: 100%;
|
|
83
816
|
height: 100%;
|
|
817
|
+
background-position: center center;
|
|
818
|
+
background-repeat: no-repeat;
|
|
819
|
+
/* default; JS may override with exact px sizing to match face dimensions */
|
|
820
|
+
background-size: cover;
|
|
821
|
+
/* avoid “quarter visible” artifacts in some browsers when faces rotate in 3D */
|
|
822
|
+
background-origin: border-box;
|
|
823
|
+
background-clip: border-box;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
@keyframes polyhedronImmersiveIn-${id} {
|
|
827
|
+
0% {
|
|
828
|
+
opacity: 0;
|
|
829
|
+
transform: scale(0.98);
|
|
830
|
+
filter: blur(6px) saturate(1.1);
|
|
831
|
+
}
|
|
832
|
+
100% {
|
|
833
|
+
opacity: 1;
|
|
834
|
+
transform: scale(1);
|
|
835
|
+
filter: blur(0px) saturate(1);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.scene-immersive-${id} {
|
|
840
|
+
position: fixed !important;
|
|
841
|
+
inset: 0 !important;
|
|
842
|
+
width: 100vw !important;
|
|
843
|
+
height: 100vh !important;
|
|
844
|
+
z-index: 100 !important;
|
|
845
|
+
background: #000 !important;
|
|
846
|
+
animation: polyhedronImmersiveIn-${id} 500ms ease both;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* Hide face labels while immersive */
|
|
850
|
+
.scene-immersive-${id} .center {
|
|
851
|
+
display: none !important;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.polyhedron-immersive-canvas-${id} {
|
|
855
|
+
position: absolute;
|
|
856
|
+
inset: 0;
|
|
857
|
+
width: 100%;
|
|
858
|
+
height: 100%;
|
|
859
|
+
z-index: 0;
|
|
860
|
+
pointer-events: none;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.polyhedron-${id} {
|
|
864
|
+
position: relative;
|
|
865
|
+
z-index: 2;
|
|
866
|
+
will-change: transform;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* Exit + effect buttons are only visible in immersive mode */
|
|
870
|
+
.scene-immersive-exit-${id} {
|
|
871
|
+
display: none;
|
|
872
|
+
position: absolute;
|
|
873
|
+
right: 12px;
|
|
874
|
+
bottom: 12px;
|
|
875
|
+
z-index: 5;
|
|
876
|
+
padding: 6px 10px;
|
|
877
|
+
border-radius: 999px;
|
|
878
|
+
background: rgba(0, 0, 0, 0.35);
|
|
879
|
+
color: rgba(255, 255, 255, 0.85);
|
|
880
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
881
|
+
backdrop-filter: blur(6px);
|
|
882
|
+
font-size: 12px;
|
|
883
|
+
line-height: 1;
|
|
884
|
+
cursor: pointer;
|
|
885
|
+
transition: 180ms ease;
|
|
886
|
+
user-select: none;
|
|
887
|
+
}
|
|
888
|
+
.scene-immersive-${id} .scene-immersive-exit-${id} {
|
|
889
|
+
display: inline-flex;
|
|
890
|
+
align-items: center;
|
|
891
|
+
justify-content: center;
|
|
892
|
+
}
|
|
893
|
+
.scene-immersive-exit-${id}:hover {
|
|
894
|
+
background: rgba(0, 0, 0, 0.55);
|
|
895
|
+
border-color: rgba(255, 255, 255, 0.28);
|
|
896
|
+
color: rgba(255, 255, 255, 0.95);
|
|
897
|
+
transform: translateY(-1px);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.scene-immersive-effect-${id} {
|
|
901
|
+
display: none;
|
|
902
|
+
position: absolute;
|
|
903
|
+
right: 74px;
|
|
904
|
+
bottom: 12px;
|
|
905
|
+
z-index: 5;
|
|
906
|
+
width: 34px;
|
|
907
|
+
height: 26px;
|
|
908
|
+
border-radius: 999px;
|
|
909
|
+
background: rgba(0, 0, 0, 0.30);
|
|
910
|
+
color: rgba(255, 255, 255, 0.82);
|
|
911
|
+
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
912
|
+
backdrop-filter: blur(6px);
|
|
913
|
+
font-size: 12px;
|
|
914
|
+
line-height: 1;
|
|
915
|
+
cursor: pointer;
|
|
916
|
+
transition: 180ms ease;
|
|
917
|
+
user-select: none;
|
|
918
|
+
}
|
|
919
|
+
.scene-immersive-${id} .scene-immersive-effect-${id} {
|
|
920
|
+
display: inline-flex;
|
|
921
|
+
align-items: center;
|
|
922
|
+
justify-content: center;
|
|
923
|
+
}
|
|
924
|
+
.scene-immersive-effect-${id}:hover {
|
|
925
|
+
background: rgba(0, 0, 0, 0.52);
|
|
926
|
+
border-color: rgba(255, 255, 255, 0.26);
|
|
927
|
+
color: rgba(255, 255, 255, 0.95);
|
|
928
|
+
transform: translateY(-1px);
|
|
84
929
|
}
|
|
85
930
|
|
|
86
931
|
${options?.style?.face
|
|
@@ -92,7 +937,7 @@ const Polyhedron = {
|
|
|
92
937
|
}
|
|
93
938
|
`
|
|
94
939
|
: css``}
|
|
95
|
-
|
|
940
|
+
${options?.style?.scene
|
|
96
941
|
? css`
|
|
97
942
|
.scene-${id} {
|
|
98
943
|
${Object.keys(options.style.scene)
|
|
@@ -137,12 +982,56 @@ const Polyhedron = {
|
|
|
137
982
|
class: `inl section-mp btn-custom btn-polyhedron-remove-zoom-${id}`,
|
|
138
983
|
label: html`<i class="fa-solid fa-minus"></i>`,
|
|
139
984
|
})}
|
|
985
|
+
|
|
986
|
+
<div class="in sub-title-modal"><i class="fa-solid fa-droplet"></i> Face opacity</div>
|
|
987
|
+
<input
|
|
988
|
+
class="in polyhedron-face-opacity-${id}"
|
|
989
|
+
type="range"
|
|
990
|
+
min="0"
|
|
991
|
+
max="1"
|
|
992
|
+
step="0.01"
|
|
993
|
+
value="${this.Tokens[id].faceOpacity}"
|
|
994
|
+
title="Face opacity"
|
|
995
|
+
/>
|
|
996
|
+
|
|
997
|
+
<div class="in sub-title-modal"><i class="fa-solid fa-image"></i> Face image</div>
|
|
998
|
+
|
|
999
|
+
<select class="in polyhedron-face-picker-${id}">
|
|
1000
|
+
<option value="all">all</option>
|
|
1001
|
+
<option value="front">front</option>
|
|
1002
|
+
<option value="back">back</option>
|
|
1003
|
+
<option value="left">left</option>
|
|
1004
|
+
<option value="right">right</option>
|
|
1005
|
+
<option value="top">top</option>
|
|
1006
|
+
<option value="bottom">bottom</option>
|
|
1007
|
+
</select>
|
|
1008
|
+
<input class="in polyhedron-face-image-file-${id}" type="file" accept="image/*" />
|
|
1009
|
+
${await BtnIcon.Render({
|
|
1010
|
+
class: `inl section-mp btn-custom btn-polyhedron-clear-face-image-${id}`,
|
|
1011
|
+
label: html`<i class="fa-solid fa-eraser"></i> ${Translate.Render('clear')}`,
|
|
1012
|
+
})}
|
|
1013
|
+
${await BtnIcon.Render({
|
|
1014
|
+
class: `inl section-mp btn-custom btn-polyhedron-immersive-${id}`,
|
|
1015
|
+
label: html`<i class="fa-solid fa-expand"></i> Immersive`,
|
|
1016
|
+
})}
|
|
140
1017
|
</div>
|
|
141
1018
|
</div>
|
|
142
1019
|
<div class="in fll polyhedron-${id}-col-b">
|
|
143
1020
|
<div class="in section-mp">
|
|
144
1021
|
<div class="in sub-title-modal"><i class="fa-solid fa-vector-square"></i> Render</div>
|
|
145
1022
|
<div class="in scene-${id}">
|
|
1023
|
+
<canvas class="polyhedron-immersive-canvas-${id}"></canvas>
|
|
1024
|
+
|
|
1025
|
+
<div
|
|
1026
|
+
class="scene-immersive-effect-${id} btn-polyhedron-immersive-effect-${id}"
|
|
1027
|
+
title="Change background effect"
|
|
1028
|
+
>
|
|
1029
|
+
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="scene-immersive-exit-${id} btn-polyhedron-immersive-exit-${id}" title="Exit immersive (Esc)">
|
|
1032
|
+
Exit
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
146
1035
|
<div class="abs polyhedron-${id}">
|
|
147
1036
|
<div class="abs face-${id} face_front-${id} ${id}-0"><div class="abs center">1</div></div>
|
|
148
1037
|
<div class="abs face-${id} face_bottom-${id} ${id}-1"><div class="abs center">2</div></div>
|