mirror of
https://github.com/shadoll/dev.shadoll.git
synced 2026-08-28 11:33:15 +00:00
feat: implement evolution and gradient systems with settings modal
- Add EvolutionController to manage spawning and physics of icons. - Introduce GradientController for animated gradient backgrounds. - Create iconLoader for fetching and caching SVG icons. - Implement modal functionality for settings adjustments. - Connect UI controls to controllers via SettingsController. - Add CSS styles for gradient animations, icons, and modal. - Create utility functions for color manipulation in gradients.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
gradient.css — animated background gradient + rotation
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Register animatable CSS custom property (Houdini) ───── */
|
||||
/* Allows smooth CSS animation of the gradient angle. */
|
||||
/* Fallback: browsers without @property support still show */
|
||||
/* the gradient, just without animated rotation. */
|
||||
@property --gradient-angle {
|
||||
syntax: '<angle>';
|
||||
inherits: false;
|
||||
initial-value: 135deg;
|
||||
}
|
||||
|
||||
/* ── Keyframe: position-based flow (always active) ───────── */
|
||||
/* Creates a fluid "breathing" motion by shifting the large */
|
||||
/* (300%) background across the viewport. */
|
||||
@keyframes gradientFlow {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* ── Keyframe: clockwise angle rotation (toggle-gated) ───── */
|
||||
/* Adds 360° to the starting angle for one full CW rotation. */
|
||||
@keyframes gradientRotate {
|
||||
from { --gradient-angle: 135deg; }
|
||||
to { --gradient-angle: 495deg; }
|
||||
}
|
||||
|
||||
/* ── Background application ──────────────────────────────── */
|
||||
body {
|
||||
background: linear-gradient(
|
||||
var(--gradient-angle),
|
||||
var(--color-1) 0%,
|
||||
var(--color-2) 50%,
|
||||
var(--color-1) 100%
|
||||
);
|
||||
background-size: 300% 300%;
|
||||
|
||||
/*
|
||||
* Both animations are always declared here.
|
||||
* gradientRotate starts PAUSED so it costs nothing until
|
||||
* the user enables rotation — then JS adds .gradient-rotating
|
||||
* which flips its play-state to running.
|
||||
* This avoids restarting gradientFlow when the class toggles.
|
||||
*/
|
||||
animation:
|
||||
gradientFlow var(--anim-duration) ease-in-out infinite,
|
||||
gradientRotate var(--rotation-duration) linear infinite;
|
||||
|
||||
animation-play-state: running, paused;
|
||||
}
|
||||
|
||||
/* ── Rotation enabled ────────────────────────────────────── */
|
||||
body.gradient-rotating {
|
||||
animation-play-state: running, running;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
icons.css — evolution container and floating icon entities
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Evolution stage ─────────────────────────────────────── */
|
||||
/*
|
||||
* Fixed full-viewport layer that holds all icon entities.
|
||||
* pointer-events: none so it never blocks page interaction.
|
||||
*/
|
||||
.evolution-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
overflow: hidden; /* clip any entity that overshoots during bounce */
|
||||
}
|
||||
|
||||
/* ── Icon entity — outer (physics position) ──────────────── */
|
||||
/*
|
||||
* JS writes: el.style.transform = 'translate(Xpx, Ypx)' each frame.
|
||||
* Size is fixed; position is driven entirely by the JS physics loop.
|
||||
* will-change: transform hints the browser to promote this to its own
|
||||
* compositor layer for smooth, layout-free animation.
|
||||
*/
|
||||
.icon-entity {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
will-change: transform;
|
||||
/* colour is set inline by Entity.mount() and inherited by the SVG */
|
||||
}
|
||||
|
||||
/* ── Icon entity — inner (appear scale animation) ────────── */
|
||||
/*
|
||||
* CSS `scale` is a standalone transform property — independent from the
|
||||
* parent's `transform: translate()`. This means the appear transition
|
||||
* runs without interfering with the per-frame position updates above.
|
||||
*
|
||||
* Easing: cubic-bezier(0.34, 1.56, 0.64, 1) is a spring-style curve
|
||||
* that overshoots slightly, giving the icon a satisfying "pop" entry.
|
||||
*/
|
||||
.icon-entity__body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
/* Start as an invisible dot */
|
||||
scale: 0;
|
||||
opacity: 0;
|
||||
|
||||
transition:
|
||||
scale 0.6s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
opacity 0.25s ease;
|
||||
|
||||
/* Subtle shadow so icons are readable over any gradient colour */
|
||||
filter: drop-shadow(0 1px 6px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
/* Alive state — triggered by JS setting data-state="alive" */
|
||||
.icon-entity__body[data-state="alive"] {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── SVG inside the body ─────────────────────────────────── */
|
||||
.icon-entity__body svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
/* SVGs use currentColor — colour is inherited from .icon-entity */
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
main.css — reset, design tokens, global layout, settings btn
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Reset ───────────────────────────────────────────────── */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ── Design Tokens ───────────────────────────────────────── */
|
||||
:root {
|
||||
/* Gradient colors — overridden immediately by JS on load */
|
||||
--color-1: #1a7040;
|
||||
--color-2: #4db8c4;
|
||||
|
||||
/* Animation timing — overridden by JS (default = speed 5) */
|
||||
--anim-duration: 19s;
|
||||
--rotation-duration: 22s;
|
||||
|
||||
/* Modal surface */
|
||||
--modal-surface: rgba(10, 10, 14, 0.82);
|
||||
--modal-border: rgba(255, 255, 255, 0.08);
|
||||
--modal-divider: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* Text */
|
||||
--text-hi: rgba(255, 255, 255, 0.92);
|
||||
--text-lo: rgba(255, 255, 255, 0.42);
|
||||
|
||||
/* Misc surface tints */
|
||||
--surface-tint: rgba(255, 255, 255, 0.10);
|
||||
|
||||
/* Font */
|
||||
--font-mono: 'DM Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* ── Body ────────────────────────────────────────────────── */
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
font-family: var(--font-mono);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Page content area (grows with future features) ──────── */
|
||||
.page-main {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ── Settings Button ─────────────────────────────────────── */
|
||||
.settings-btn {
|
||||
position: fixed;
|
||||
top: 22px;
|
||||
right: 26px;
|
||||
z-index: 100;
|
||||
|
||||
/* Strip all button decoration */
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
/* Shape & layout */
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
/* Typography */
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
transition:
|
||||
color 0.18s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
.settings-btn:active {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
/* Focus-visible ring for keyboard nav */
|
||||
.settings-btn:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
modal.css — overlay, panel, and all settings controls
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Overlay ─────────────────────────────────────────────── */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
|
||||
/* Hidden by default */
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.26s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.modal-visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* ── Panel ───────────────────────────────────────────────── */
|
||||
.modal {
|
||||
width: 310px;
|
||||
|
||||
background: var(--modal-surface);
|
||||
border: 1px solid var(--modal-border);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
|
||||
/* Glass blur effect */
|
||||
backdrop-filter: blur(48px) saturate(1.5);
|
||||
-webkit-backdrop-filter: blur(48px) saturate(1.5);
|
||||
|
||||
box-shadow:
|
||||
0 32px 80px rgba(0, 0, 0, 0.45),
|
||||
0 2px 12px rgba(0, 0, 0, 0.30);
|
||||
|
||||
/* Entry animation: rises and scales into place */
|
||||
transform: translateY(18px) scale(0.96);
|
||||
transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.modal-overlay.modal-visible .modal {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* ── Header ──────────────────────────────────────────────── */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 20px 15px;
|
||||
border-bottom: 1px solid var(--modal-divider);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: lowercase;
|
||||
color: var(--text-lo);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
font-size: 1.15rem;
|
||||
line-height: 1;
|
||||
color: var(--text-lo);
|
||||
|
||||
transition:
|
||||
color 0.18s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-hi);
|
||||
background: var(--surface-tint);
|
||||
}
|
||||
|
||||
.modal-close:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ── Body ────────────────────────────────────────────────── */
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
/* ── Setting group (label + control) ─────────────────────── */
|
||||
.setting-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.60rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: lowercase;
|
||||
color: var(--text-lo);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
Color Picker
|
||||
══════════════════════════════════════════════════════════ */
|
||||
.color-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
width: 52px;
|
||||
height: 30px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--modal-border);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.color-picker:hover {
|
||||
border-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
/* Remove inner chrome swatch border on WebKit */
|
||||
.color-picker::-webkit-color-swatch-wrapper { padding: 0; }
|
||||
.color-picker::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.color-picker::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.color-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-hi);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
Speed Slider
|
||||
══════════════════════════════════════════════════════════ */
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--text-lo);
|
||||
user-select: none;
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
.slider-label:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
.slider {
|
||||
flex: 1;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
height: 18px; /* tap target height */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Track — WebKit */
|
||||
.slider::-webkit-slider-runnable-track {
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Thumb — WebKit */
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-top: -6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.35);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Track — Firefox */
|
||||
.slider::-moz-range-track {
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Thumb — Firefox */
|
||||
.slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
Toggle Switch
|
||||
══════════════════════════════════════════════════════════ */
|
||||
.toggle-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.56rem;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--text-lo);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* The <label> wrapping the whole toggle */
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Hide native checkbox, keep it accessible */
|
||||
.toggle input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Track pill */
|
||||
.toggle-track {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition:
|
||||
background 0.28s ease,
|
||||
border-color 0.28s ease;
|
||||
}
|
||||
|
||||
/* Checked: brighten track */
|
||||
.toggle input:checked + .toggle-track {
|
||||
background: rgba(255, 255, 255, 0.34);
|
||||
border-color: rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
|
||||
/* Thumb dot */
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
transition:
|
||||
transform 0.28s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
background 0.28s ease;
|
||||
}
|
||||
|
||||
/* Checked: slide thumb to the right */
|
||||
.toggle input:checked + .toggle-track .toggle-thumb {
|
||||
transform: translateX(18px);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Focus ring on the label for keyboard nav */
|
||||
.toggle input:focus-visible + .toggle-track {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
Reference in New Issue
Block a user