training/dashboard(theme): per-theme settings · vaporwave overhaul
Per-theme settings panel - New section in the theme panel that auto-shows the active theme's knobs (and only the active theme). Built dynamically from a THEMES spec; new params drop in by adding one entry. - drift: blob count (3-12), size, blur, opacity - lava: bubble count (4-16), mean size, σ spread, goo merge strength, goo blur σ. Bubbles are now JS-generated with sizes drawn from N(mean, σ) so the spread slider produces real variance, not just one-size-fits-all. - vaporwave: grid cell, horizon angle, sun size, horizon position, blind width. - laser: beam count, thickness, blur, opacity. Beams JS-generated. Vaporwave overhaul (was: a single perspective grid + radial sun) - Layered scene: gradient sky → palette-blended sun with venetian- blind stripes on the lower half → glowing horizon line → palette perspective floor → CRT-style scanline overlay - Sun box-shadow gives a halo; horizon has multi-stop glow Live SVG goo filter - The lava merge strength / blur sliders now mutate the inline <feGaussianBlur stdDeviation> and <feColorMatrix values> attributes via JS (CSS vars don't work inside SVG filter primitives). Settings persist in localStorage; new params are merged in over older snapshots so prior reloads don't break. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fe0ba239ed
commit
a027be72e7
3 changed files with 359 additions and 107 deletions
|
|
@ -75,56 +75,26 @@ body[data-theme="vaporwave"] .bg-vaporwave { display: block; }
|
|||
body[data-theme="laser"] .bg-laser { display: block; }
|
||||
|
||||
/* drift — soft, blurred radial blobs floating up and back down.
|
||||
Renamed from the previous "lava" implementation. */
|
||||
The blobs are JS-generated; styling here uses theme variables
|
||||
(--drift-blur, --drift-opacity) so live slider changes don't
|
||||
need a DOM rebuild. */
|
||||
.drift-blob {
|
||||
position: absolute; width: 36vw; height: 36vw; max-width: 520px; max-height: 520px;
|
||||
border-radius: 50%; filter: blur(70px); opacity: 0.55;
|
||||
position: absolute; border-radius: 50%;
|
||||
filter: blur(var(--drift-blur, 70px));
|
||||
opacity: var(--drift-opacity, 0.55);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
.drift-blob.db1 { left: 6%; bottom: -20%; background: var(--c1);
|
||||
animation: drift-rise calc(16s / var(--anim-speed, 1)) ease-in-out infinite; }
|
||||
.drift-blob.db2 { left: 28%; bottom: -30%; background: var(--c2);
|
||||
animation: drift-rise calc(22s / var(--anim-speed, 1)) ease-in-out -4s infinite; }
|
||||
.drift-blob.db3 { left: 52%; bottom: -25%; background: var(--c3);
|
||||
animation: drift-rise calc(19s / var(--anim-speed, 1)) ease-in-out -8s infinite; }
|
||||
.drift-blob.db4 { left: 70%; bottom: -35%; background: var(--c4);
|
||||
animation: drift-rise calc(25s / var(--anim-speed, 1)) ease-in-out -2s infinite; }
|
||||
.drift-blob.db5 { left: 86%; bottom: -22%; background: var(--c5);
|
||||
animation: drift-rise calc(18s / var(--anim-speed, 1)) ease-in-out -10s infinite; }
|
||||
.drift-blob.db6 { left: 40%; bottom: -40%; background: var(--c1);
|
||||
animation: drift-rise calc(28s / var(--anim-speed, 1)) ease-in-out -14s infinite; }
|
||||
@keyframes drift-rise {
|
||||
0%, 100% { transform: translateY(0) scale(1); }
|
||||
50% { transform: translateY(-130vh) scale(1.4); }
|
||||
}
|
||||
|
||||
/* lava — proper lava-lamp metaballs via SVG goo filter. The filter
|
||||
is a Gaussian blur followed by an alpha threshold that creates a
|
||||
gooey edge; bubbles that overlap merge into a single glowing
|
||||
shape. Defined inline in the HTML <svg> at the top. */
|
||||
.goo-container {
|
||||
position: absolute; inset: 0;
|
||||
filter: url(#goo);
|
||||
}
|
||||
.goo-bubble {
|
||||
position: absolute; border-radius: 50%; opacity: 0.95;
|
||||
}
|
||||
.goo-bubble.gb1 { width: 8vw; height: 8vw; left: 18%; bottom: -10vw; background: var(--c1);
|
||||
animation: goo-rise calc(14s / var(--anim-speed, 1)) ease-in-out -1s infinite; }
|
||||
.goo-bubble.gb2 { width: 11vw; height: 11vw; left: 30%; bottom: -16vw; background: var(--c2);
|
||||
animation: goo-rise calc(20s / var(--anim-speed, 1)) ease-in-out -7s infinite; }
|
||||
.goo-bubble.gb3 { width: 7vw; height: 7vw; left: 44%; bottom: -10vw; background: var(--c3);
|
||||
animation: goo-rise calc(17s / var(--anim-speed, 1)) ease-in-out -3s infinite; }
|
||||
.goo-bubble.gb4 { width: 13vw; height: 13vw; left: 56%; bottom: -20vw; background: var(--c4);
|
||||
animation: goo-rise calc(24s / var(--anim-speed, 1)) ease-in-out -11s infinite; }
|
||||
.goo-bubble.gb5 { width: 9vw; height: 9vw; left: 70%; bottom: -12vw; background: var(--c5);
|
||||
animation: goo-rise calc(19s / var(--anim-speed, 1)) ease-in-out -5s infinite; }
|
||||
.goo-bubble.gb6 { width: 6vw; height: 6vw; left: 80%; bottom: -8vw; background: var(--c1);
|
||||
animation: goo-rise calc(15s / var(--anim-speed, 1)) ease-in-out -9s infinite; }
|
||||
.goo-bubble.gb7 { width: 10vw; height: 10vw; left: 8%; bottom: -14vw; background: var(--c2);
|
||||
animation: goo-rise calc(22s / var(--anim-speed, 1)) ease-in-out -13s infinite; }
|
||||
.goo-bubble.gb8 { width: 7vw; height: 7vw; left: 90%; bottom: -10vw; background: var(--c3);
|
||||
animation: goo-rise calc(18s / var(--anim-speed, 1)) ease-in-out -2s infinite; }
|
||||
/* lava — metaballs via SVG goo filter. The filter values
|
||||
(stdDeviation, alpha-threshold matrix entries) are updated by
|
||||
JS as the goo strength / blur sliders move. Bubbles are
|
||||
JS-generated so their statistical-distribution sliders work. */
|
||||
.goo-container { position: absolute; inset: 0; filter: url(#goo); }
|
||||
.goo-bubble { position: absolute; border-radius: 50%; opacity: 0.95; }
|
||||
@keyframes goo-rise {
|
||||
0% { transform: translateY(0) scale(0.9); }
|
||||
45% { transform: translateY(-65vh) scale(1.05); }
|
||||
|
|
@ -132,51 +102,108 @@ body[data-theme="laser"] .bg-laser { display: block; }
|
|||
100% { transform: translateY(-130vh) scale(0.85); }
|
||||
}
|
||||
|
||||
/* vaporwave — perspective grid crawling toward the viewer + a sun. */
|
||||
.bg-vw-grid {
|
||||
position: absolute; left: -50%; right: -50%; top: 60%; bottom: -10%;
|
||||
background-image:
|
||||
linear-gradient(transparent 96%, var(--c1) 96%),
|
||||
linear-gradient(90deg, transparent 98%, var(--c2) 98%);
|
||||
background-size: 100% 80px, 80px 100%;
|
||||
transform: perspective(700px) rotateX(62deg);
|
||||
transform-origin: top center;
|
||||
animation: vw-grid calc(4s / var(--anim-speed, 1)) linear infinite;
|
||||
opacity: 0.6;
|
||||
/* ─── Vaporwave (overhauled) ───────────────────────────────────────
|
||||
Layered: gradient sky → palette-blended sun with venetian-blind
|
||||
stripes → glowing horizon → perspective grid floor → CRT scanlines.
|
||||
Knobs (CSS vars set by sliders): --vw-horizon, --vw-grid-size,
|
||||
--vw-perspective, --vw-sun-size, --vw-blind. */
|
||||
.bg-vaporwave {
|
||||
position: absolute; inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse 100% 70% at 50% var(--vw-horizon, 55%),
|
||||
transparent 0%, transparent 60%,
|
||||
rgba(0, 0, 0, 0.6) 100%);
|
||||
}
|
||||
@keyframes vw-grid {
|
||||
from { background-position: 0 0, 0 0; }
|
||||
to { background-position: 0 80px, 0 0; }
|
||||
.vw-sky {
|
||||
position: absolute; left: 0; right: 0; top: 0;
|
||||
height: var(--vw-horizon, 55%);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
oklch(15% 0.20 var(--theme-h, 250)) 0%,
|
||||
var(--c1) 60%,
|
||||
var(--c2) 100%);
|
||||
}
|
||||
.bg-vw-sun {
|
||||
position: absolute; left: 50%; top: 35%;
|
||||
width: 50vmin; height: 50vmin; transform: translate(-50%, 0);
|
||||
.vw-sun {
|
||||
position: absolute; left: 50%;
|
||||
top: var(--vw-horizon, 55%);
|
||||
width: var(--vw-sun-size, 50vmin);
|
||||
height: var(--vw-sun-size, 50vmin);
|
||||
transform: translate(-50%, -78%);
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 50% 50%, var(--c3), var(--c4) 60%, transparent 75%);
|
||||
opacity: 0.55; filter: blur(2px);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--c3) 0%,
|
||||
var(--c4) 50%,
|
||||
var(--c5) 100%);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 80px var(--c4), 0 0 160px var(--c3);
|
||||
}
|
||||
/* Venetian-blind stripes on the lower 50% — black-bg occluders, so
|
||||
the sun's gradient peeks through the gaps. */
|
||||
.vw-sun-blinds {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
height: 50%;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0 calc(var(--vw-blind, 11px) - 4px),
|
||||
rgba(0, 0, 0, 0.92) calc(var(--vw-blind, 11px) - 4px) var(--vw-blind, 11px));
|
||||
}
|
||||
.vw-horizon {
|
||||
position: absolute; left: 0; right: 0;
|
||||
top: var(--vw-horizon, 55%);
|
||||
height: 1px; background: var(--c1);
|
||||
box-shadow:
|
||||
0 0 20px var(--c1),
|
||||
0 0 40px var(--c1),
|
||||
0 0 80px var(--c2);
|
||||
}
|
||||
.vw-floor {
|
||||
position: absolute; left: -50%; right: -50%;
|
||||
top: var(--vw-horizon, 55%); bottom: -10%;
|
||||
perspective: 800px; overflow: hidden;
|
||||
}
|
||||
.vw-floor::before {
|
||||
content: ''; position: absolute; inset: 0 0 -50% 0;
|
||||
background-image:
|
||||
linear-gradient(transparent calc(100% - 1px), var(--c1) calc(100% - 1px)),
|
||||
linear-gradient(90deg, transparent calc(100% - 1px), var(--c2) calc(100% - 1px));
|
||||
background-size: 100% var(--vw-grid-size, 80px), var(--vw-grid-size, 80px) 100%;
|
||||
transform: rotateX(var(--vw-perspective, 62deg));
|
||||
transform-origin: top center;
|
||||
animation: vw-floor calc(4s / var(--anim-speed, 1)) linear infinite;
|
||||
}
|
||||
@keyframes vw-floor {
|
||||
from { background-position: 0 0, 0 0; }
|
||||
to { background-position: 0 var(--vw-grid-size, 80px), 0 0; }
|
||||
}
|
||||
.vw-scanlines {
|
||||
position: absolute; inset: 0; pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0 2px,
|
||||
rgba(0, 0, 0, 0.18) 2px 4px);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* laser show — long beams rotating from screen center, palette colors. */
|
||||
/* laser show — long beams rotating from screen center, palette colors.
|
||||
Beams JS-generated so count/thickness/blur sliders work. */
|
||||
.laser-beam {
|
||||
position: absolute; left: 50%; top: 50%;
|
||||
width: 200vmax; height: 3px; transform-origin: left center;
|
||||
filter: blur(2px); opacity: 0.55; mix-blend-mode: screen;
|
||||
height: var(--laser-thickness, 3px);
|
||||
width: 200vmax; transform-origin: left center;
|
||||
filter: blur(var(--laser-blur, 2px));
|
||||
opacity: var(--laser-opacity, 0.55);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
.laser-beam.beam1 { background: linear-gradient(90deg, transparent, var(--c1), transparent);
|
||||
animation: laser-sweep calc(9s / var(--anim-speed, 1)) linear infinite; }
|
||||
.laser-beam.beam2 { background: linear-gradient(90deg, transparent, var(--c2), transparent);
|
||||
animation: laser-sweep calc(13s / var(--anim-speed, 1)) linear infinite reverse; }
|
||||
.laser-beam.beam3 { background: linear-gradient(90deg, transparent, var(--c3), transparent);
|
||||
animation: laser-sweep calc(11s / var(--anim-speed, 1)) linear infinite; }
|
||||
.laser-beam.beam4 { background: linear-gradient(90deg, transparent, var(--c4), transparent);
|
||||
animation: laser-sweep calc(17s / var(--anim-speed, 1)) linear infinite reverse; }
|
||||
.laser-beam.beam5 { background: linear-gradient(90deg, transparent, var(--c5), transparent);
|
||||
animation: laser-sweep calc(15s / var(--anim-speed, 1)) linear infinite; }
|
||||
@keyframes laser-sweep {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ─── Per-theme settings section ───────────────────────────────────── */
|
||||
.theme-bg-section { display: none; }
|
||||
.theme-bg-section.is-active { display: block; }
|
||||
|
||||
/* Continuous-harmony hint line */
|
||||
.theme-harmony-hint {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
|
|
|
|||
|
|
@ -250,17 +250,73 @@
|
|||
// Animations across the bg-canvas read --anim-speed; the bg blur
|
||||
// reads --bg-blur. State persists in localStorage.
|
||||
(function () {
|
||||
// Per-theme settings spec. Each entry becomes a slider in the
|
||||
// panel under the active-theme section. `step` rounds the value
|
||||
// and decides how many decimals to show.
|
||||
const THEMES = {
|
||||
drift: {
|
||||
label: 'drift settings',
|
||||
params: {
|
||||
count: { default: 6, min: 3, max: 12, step: 1, label: 'blobs', rebuild: true },
|
||||
sizeVw: { default: 36, min: 12, max: 60, step: 1, label: 'size (vw)', rebuild: true },
|
||||
blur: { default: 70, min: 20, max: 150, step: 1, label: 'blur (px)' },
|
||||
opacity: { default: 0.55, min: 0.1, max: 1, step: 0.05, label: 'opacity' },
|
||||
},
|
||||
},
|
||||
lava: {
|
||||
label: 'lava settings',
|
||||
params: {
|
||||
count: { default: 8, min: 4, max: 16, step: 1, label: 'bubbles', rebuild: true },
|
||||
sizeMean: { default: 9, min: 3, max: 18, step: 0.5, label: 'mean size (vw)', rebuild: true },
|
||||
sizeSpread: { default: 4, min: 0, max: 8, step: 0.5, label: 'size σ (spread)', rebuild: true },
|
||||
gooStr: { default: 26, min: 8, max: 50, step: 1, label: 'merge strength' },
|
||||
gooBlur: { default: 22, min: 8, max: 40, step: 1, label: 'merge blur (σ)' },
|
||||
},
|
||||
},
|
||||
vaporwave: {
|
||||
label: 'vaporwave settings',
|
||||
params: {
|
||||
gridSize: { default: 80, min: 30, max: 200, step: 5, label: 'grid cell (px)' },
|
||||
perspective: { default: 62, min: 30, max: 80, step: 1, label: 'horizon angle (°)' },
|
||||
sunSize: { default: 50, min: 20, max: 80, step: 1, label: 'sun size (vmin)' },
|
||||
horizonPct: { default: 55, min: 35, max: 70, step: 1, label: 'horizon position (%)' },
|
||||
blindWidth: { default: 11, min: 6, max: 30, step: 1, label: 'blind width (px)' },
|
||||
},
|
||||
},
|
||||
laser: {
|
||||
label: 'laser settings',
|
||||
params: {
|
||||
count: { default: 5, min: 2, max: 12, step: 1, label: 'beams', rebuild: true },
|
||||
thickness: { default: 3, min: 1, max: 12, step: 1, label: 'thickness (px)' },
|
||||
blur: { default: 2, min: 0, max: 10, step: 1, label: 'blur (px)' },
|
||||
opacity: { default: 0.55, min: 0.1, max: 1, step: 0.05, label: 'opacity' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function defaultThemeSettings() {
|
||||
const out = {};
|
||||
for (const t in THEMES) {
|
||||
out[t] = {};
|
||||
for (const p in THEMES[t].params) {
|
||||
out[t][p] = THEMES[t].params[p].default;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
background: 'black',
|
||||
L: 70, C: 0.15, H: 250,
|
||||
count: 3,
|
||||
spread: 60,
|
||||
offsets: null, // populated by regenerate() at boot
|
||||
offsets: null,
|
||||
lVar: 0,
|
||||
cVar: 0,
|
||||
animSpeed: 1.0,
|
||||
bgBlur: 0,
|
||||
tint: 0.10,
|
||||
themes: defaultThemeSettings(),
|
||||
};
|
||||
const state = Object.assign({}, DEFAULTS);
|
||||
|
||||
|
|
@ -281,18 +337,22 @@
|
|||
|
||||
try {
|
||||
const stored = JSON.parse(localStorage.getItem('cis490-theme') || '{}');
|
||||
// Migrate older state shapes (had `harmony` + multiplicative
|
||||
// spread). Drop those fields silently.
|
||||
delete stored.harmony;
|
||||
delete stored.harmony; // discrete-harmony field (gone)
|
||||
Object.assign(state, stored);
|
||||
if (!Array.isArray(state.offsets) || state.offsets.length !== state.count) {
|
||||
state.offsets = evenOffsets(state.count, state.spread);
|
||||
}
|
||||
// Coerce spread back to angular if it looks like a stale multiplier.
|
||||
if (typeof state.spread === 'number' && state.spread <= 5) {
|
||||
state.spread = 60;
|
||||
state.offsets = evenOffsets(state.count, state.spread);
|
||||
}
|
||||
// Merge in theme defaults for any missing per-theme params
|
||||
// (state shape grows over time as new sliders ship).
|
||||
const fresh = defaultThemeSettings();
|
||||
state.themes = state.themes || {};
|
||||
for (const t in fresh) {
|
||||
state.themes[t] = Object.assign({}, fresh[t], state.themes[t] || {});
|
||||
}
|
||||
} catch {}
|
||||
if (!state.offsets) state.offsets = evenOffsets(state.count, state.spread);
|
||||
|
||||
|
|
@ -363,6 +423,39 @@
|
|||
root.setProperty('--bg-blur', `${state.bgBlur}px`);
|
||||
root.setProperty('--tint-strength', state.tint);
|
||||
document.body.dataset.theme = state.background;
|
||||
applyThemeSpecificVars();
|
||||
}
|
||||
|
||||
// Per-theme CSS variables derived from state.themes.<name>. Only
|
||||
// the variables for the currently-active theme need to be set,
|
||||
// but it's cheap to set them all so the dropdown switch is
|
||||
// instant.
|
||||
function applyThemeSpecificVars() {
|
||||
const root = document.documentElement.style;
|
||||
const t = state.themes;
|
||||
// drift
|
||||
root.setProperty('--drift-blur', `${t.drift.blur}px`);
|
||||
root.setProperty('--drift-opacity', t.drift.opacity);
|
||||
// vaporwave
|
||||
root.setProperty('--vw-grid-size', `${t.vaporwave.gridSize}px`);
|
||||
root.setProperty('--vw-perspective', `${t.vaporwave.perspective}deg`);
|
||||
root.setProperty('--vw-sun-size', `${t.vaporwave.sunSize}vmin`);
|
||||
root.setProperty('--vw-horizon', `${t.vaporwave.horizonPct}%`);
|
||||
root.setProperty('--vw-blind', `${t.vaporwave.blindWidth}px`);
|
||||
// laser
|
||||
root.setProperty('--laser-thickness', `${t.laser.thickness}px`);
|
||||
root.setProperty('--laser-blur', `${t.laser.blur}px`);
|
||||
root.setProperty('--laser-opacity', t.laser.opacity);
|
||||
// lava goo filter — has to be set on the SVG element directly
|
||||
const fb = document.querySelector('#goo feGaussianBlur');
|
||||
const fc = document.querySelector('#goo feColorMatrix');
|
||||
if (fb) fb.setAttribute('stdDeviation', String(t.lava.gooBlur));
|
||||
if (fc) {
|
||||
const a = t.lava.gooStr;
|
||||
const b = Math.round(a / 2 + 1);
|
||||
fc.setAttribute('values',
|
||||
`1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${a} -${b}`);
|
||||
}
|
||||
}
|
||||
|
||||
function applyForm() {
|
||||
|
|
@ -428,14 +521,97 @@
|
|||
ms.forEach((m, i) => positionMarker(m, i));
|
||||
}
|
||||
|
||||
function apply({ rebuildMarkers = false } = {}) {
|
||||
function apply({ rebuildMarkers = false, rebuildBg = false } = {}) {
|
||||
applyCSSVars();
|
||||
applyForm();
|
||||
if (rebuildMarkers) renderMarkersFull();
|
||||
else updateMarkers();
|
||||
if (rebuildBg) rebuildBackgroundElements();
|
||||
showActiveBgSection();
|
||||
try { localStorage.setItem('cis490-theme', JSON.stringify(state)); } catch {}
|
||||
}
|
||||
|
||||
// ── Per-theme background element generation ───────────────────
|
||||
// drift / lava / laser each have collections of small DOM nodes
|
||||
// whose count and individual properties depend on per-theme
|
||||
// settings. Rebuild only when count or statistical-spread params
|
||||
// change; styling-only updates (blur, opacity, thickness) flow
|
||||
// through CSS variables and don't need a rebuild.
|
||||
|
||||
// Box-Muller for size variance.
|
||||
function gauss(mean, std) {
|
||||
let u = 0, v = 0;
|
||||
while (u === 0) u = Math.random();
|
||||
while (v === 0) v = Math.random();
|
||||
return mean + std * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
||||
}
|
||||
|
||||
function rebuildDrift() {
|
||||
const root = document.getElementById('bg-drift');
|
||||
if (!root) return;
|
||||
root.innerHTML = '';
|
||||
const t = state.themes.drift;
|
||||
for (let i = 0; i < t.count; i++) {
|
||||
const b = document.createElement('span');
|
||||
b.className = 'drift-blob';
|
||||
b.style.width = b.style.height = `${t.sizeVw}vw`;
|
||||
b.style.left = `${(Math.random() * 90).toFixed(1)}%`;
|
||||
b.style.bottom = `-${(15 + Math.random() * 25).toFixed(0)}%`;
|
||||
b.style.background = `var(--c${(i % 5) + 1})`;
|
||||
const dur = (14 + Math.random() * 16).toFixed(1);
|
||||
const delay = (-Math.random() * parseFloat(dur)).toFixed(1);
|
||||
b.style.animation =
|
||||
`drift-rise calc(${dur}s / var(--anim-speed, 1)) ease-in-out ${delay}s infinite`;
|
||||
root.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildLava() {
|
||||
const root = document.getElementById('bg-lava-bubbles');
|
||||
if (!root) return;
|
||||
root.innerHTML = '';
|
||||
const t = state.themes.lava;
|
||||
for (let i = 0; i < t.count; i++) {
|
||||
const b = document.createElement('span');
|
||||
b.className = 'goo-bubble';
|
||||
const sz = Math.max(2, Math.min(28, gauss(t.sizeMean, t.sizeSpread)));
|
||||
b.style.width = b.style.height = `${sz.toFixed(1)}vw`;
|
||||
b.style.left = `${(Math.random() * 92).toFixed(1)}%`;
|
||||
b.style.bottom = `-${(sz + 4).toFixed(1)}vw`;
|
||||
b.style.background = `var(--c${(i % 5) + 1})`;
|
||||
const dur = (14 + Math.random() * 14).toFixed(1);
|
||||
const delay = (-Math.random() * parseFloat(dur)).toFixed(1);
|
||||
b.style.animation =
|
||||
`goo-rise calc(${dur}s / var(--anim-speed, 1)) ease-in-out ${delay}s infinite`;
|
||||
root.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildLaser() {
|
||||
const root = document.getElementById('bg-laser-beams');
|
||||
if (!root) return;
|
||||
root.innerHTML = '';
|
||||
const t = state.themes.laser;
|
||||
for (let i = 0; i < t.count; i++) {
|
||||
const b = document.createElement('span');
|
||||
b.className = 'laser-beam';
|
||||
b.style.background =
|
||||
`linear-gradient(90deg, transparent, var(--c${(i % 5) + 1}), transparent)`;
|
||||
const dur = (8 + Math.random() * 12).toFixed(1);
|
||||
const delay = (-Math.random() * parseFloat(dur)).toFixed(1);
|
||||
const dir = i % 2 === 0 ? 'normal' : 'reverse';
|
||||
b.style.animation =
|
||||
`laser-sweep calc(${dur}s / var(--anim-speed, 1)) linear ${delay}s infinite ${dir}`;
|
||||
root.appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildBackgroundElements() {
|
||||
rebuildDrift();
|
||||
rebuildLava();
|
||||
rebuildLaser();
|
||||
}
|
||||
|
||||
// ── Drag handling ──────────────────────────────────────────────
|
||||
// Primary marker (idx 0): drag rotates the entire palette
|
||||
// (changes state.H). Other markers: drag moves just that
|
||||
|
|
@ -512,6 +688,61 @@
|
|||
|
||||
els.bg.addEventListener('change', e => { state.background = e.target.value; apply(); });
|
||||
|
||||
// ── Per-theme settings panel ──────────────────────────────────
|
||||
// Build one section per theme (each a <details>) inside
|
||||
// #theme-bg-settings. CSS hides all but the active one (via the
|
||||
// `.is-active` class managed by showActiveBgSection).
|
||||
const $bgSettings = document.getElementById('theme-bg-settings');
|
||||
for (const themeName in THEMES) {
|
||||
const spec = THEMES[themeName];
|
||||
const section = document.createElement('details');
|
||||
section.className = 'theme-advanced theme-bg-section';
|
||||
section.dataset.theme = themeName;
|
||||
section.open = true;
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = spec.label;
|
||||
section.appendChild(summary);
|
||||
|
||||
const sliders = document.createElement('div');
|
||||
sliders.className = 'theme-sliders';
|
||||
for (const key in spec.params) {
|
||||
const p = spec.params[key];
|
||||
const id = `theme-${themeName}-${key}`;
|
||||
const v = state.themes[themeName][key];
|
||||
const fmt = p.step >= 1 ? Math.round(v) : v.toFixed(p.step >= 0.1 ? 2 : 3);
|
||||
const label = document.createElement('label');
|
||||
label.innerHTML =
|
||||
`${p.label} · <span id="${id}-val">${fmt}</span>` +
|
||||
`<input type="range" id="${id}" min="${p.min}" max="${p.max}" ` +
|
||||
`value="${v}" step="${p.step}">`;
|
||||
sliders.appendChild(label);
|
||||
}
|
||||
section.appendChild(sliders);
|
||||
$bgSettings.appendChild(section);
|
||||
|
||||
// Bind inputs after they're in the DOM.
|
||||
for (const key in spec.params) {
|
||||
const p = spec.params[key];
|
||||
const id = `theme-${themeName}-${key}`;
|
||||
const input = document.getElementById(id);
|
||||
const valSpan = document.getElementById(`${id}-val`);
|
||||
if (!input || !valSpan) continue;
|
||||
input.addEventListener('input', e => {
|
||||
const v = parseFloat(e.target.value);
|
||||
state.themes[themeName][key] = v;
|
||||
valSpan.textContent = p.step >= 1 ? Math.round(v) :
|
||||
v.toFixed(p.step >= 0.1 ? 2 : 3);
|
||||
apply({ rebuildBg: !!p.rebuild });
|
||||
});
|
||||
}
|
||||
}
|
||||
function showActiveBgSection() {
|
||||
document.querySelectorAll('.theme-bg-section').forEach(s => {
|
||||
s.classList.toggle('is-active', s.dataset.theme === state.background);
|
||||
});
|
||||
}
|
||||
|
||||
// count + spread together regenerate offsets to an evenly-fanned
|
||||
// distribution. Any prior per-marker drags are overwritten —
|
||||
// the user is asking for a clean palette by touching these.
|
||||
|
|
@ -543,7 +774,7 @@
|
|||
els.panel.addEventListener('click', e => e.stopPropagation());
|
||||
els.panel.addEventListener('pointerdown', e => e.stopPropagation());
|
||||
|
||||
apply({ rebuildMarkers: true });
|
||||
apply({ rebuildMarkers: true, rebuildBg: true });
|
||||
})();
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CIS490 — live</title>
|
||||
<link rel="stylesheet" href="/static/dashboard.css?v=d28553bc">
|
||||
<link rel="stylesheet" href="/static/dashboard.css?v=bb44f27a">
|
||||
</head>
|
||||
<body>
|
||||
<!-- SVG filter defs for the lava-lamp goo effect. Width/height 0
|
||||
|
|
@ -25,37 +25,27 @@
|
|||
</svg>
|
||||
|
||||
<!-- Theme background layers — exactly one is visible at a time,
|
||||
selected by body[data-theme]. Colors come from palette CSS
|
||||
variables (--c1..c5) computed by the theme machinery from the
|
||||
OKLCH base and harmony rule. The bg-tint vignette below
|
||||
applies on every theme so even "black" picks up palette
|
||||
character. -->
|
||||
selected by body[data-theme]. The blobs / bubbles / beams
|
||||
inside drift / lava / laser are generated by JS so the count
|
||||
and statistical-distribution sliders actually take effect. -->
|
||||
<div class="bg-canvas" id="bg-canvas" aria-hidden="true">
|
||||
<div class="bg-tint"></div>
|
||||
|
||||
<!-- Drift: the original soft-blob version of "lava lamp" -->
|
||||
<div class="bg-drift">
|
||||
<span class="drift-blob db1"></span><span class="drift-blob db2"></span>
|
||||
<span class="drift-blob db3"></span><span class="drift-blob db4"></span>
|
||||
<span class="drift-blob db5"></span><span class="drift-blob db6"></span>
|
||||
</div>
|
||||
<div class="bg-drift" id="bg-drift"></div>
|
||||
|
||||
<!-- Lava: SVG-goo metaballs that merge as bubbles approach -->
|
||||
<div class="bg-lava">
|
||||
<div class="goo-container">
|
||||
<span class="goo-bubble gb1"></span><span class="goo-bubble gb2"></span>
|
||||
<span class="goo-bubble gb3"></span><span class="goo-bubble gb4"></span>
|
||||
<span class="goo-bubble gb5"></span><span class="goo-bubble gb6"></span>
|
||||
<span class="goo-bubble gb7"></span><span class="goo-bubble gb8"></span>
|
||||
</div>
|
||||
<div class="goo-container" id="bg-lava-bubbles"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-vaporwave"><div class="bg-vw-grid"></div><div class="bg-vw-sun"></div></div>
|
||||
<div class="bg-laser">
|
||||
<span class="laser-beam beam1"></span><span class="laser-beam beam2"></span>
|
||||
<span class="laser-beam beam3"></span><span class="laser-beam beam4"></span>
|
||||
<span class="laser-beam beam5"></span>
|
||||
<div class="bg-vaporwave">
|
||||
<div class="vw-sky"></div>
|
||||
<div class="vw-sun"><div class="vw-sun-blinds"></div></div>
|
||||
<div class="vw-horizon"></div>
|
||||
<div class="vw-floor"></div>
|
||||
<div class="vw-scanlines"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-laser" id="bg-laser-beams"></div>
|
||||
</div>
|
||||
|
||||
<!-- Floating theme panel, toggled by `t`. -->
|
||||
|
|
@ -116,7 +106,7 @@
|
|||
</div>
|
||||
|
||||
<details class="theme-advanced" open>
|
||||
<summary>animation</summary>
|
||||
<summary>animation · global</summary>
|
||||
<div class="theme-sliders">
|
||||
<label>speed · <span id="theme-speed-val">1.00</span>×
|
||||
<input type="range" id="theme-speed" min="0.1" max="4" value="1" step="0.05"></label>
|
||||
|
|
@ -127,6 +117,10 @@
|
|||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Per-theme settings — dynamically built by JS from the THEMES
|
||||
spec; only the section matching state.background is shown. -->
|
||||
<div id="theme-bg-settings"></div>
|
||||
|
||||
<div class="theme-meta-row">
|
||||
<code id="theme-meta">oklch(70% 0.15 250)</code>
|
||||
<button id="theme-reset" class="ghost">reset</button>
|
||||
|
|
@ -483,6 +477,6 @@
|
|||
</article>
|
||||
</div>
|
||||
|
||||
<script src="/static/dashboard.js?v=a8db3285"></script>
|
||||
<script src="/static/dashboard.js?v=0de4be30"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue