feat: expand appearance with shader effects and new style presets
Some checks failed
Lint & Test / test (push) Failing after 29s
Some checks failed
Lint & Test / test (push) Failing after 29s
Add 5 WebGL shader background effects (Aurora, Plasma, Digital Rain, Starfield, Warp Tunnel) via a new bg-shaders.ts engine that shares a dedicated canvas. Add 5 style presets (Sakura, Ocean, Copper, Vapor, Monolith) with distinctive font pairings. Remove CSS particles effect in favor of shader-based alternatives. Fix dot grid visibility and tune all shader intensities for subtle ambient appearance.
This commit is contained in:
@@ -2,11 +2,16 @@
|
||||
* Appearance — style presets (font + colors) and background effect presets.
|
||||
*
|
||||
* Persists choices to localStorage. Style presets override CSS variables;
|
||||
* background effects toggle CSS classes on <html>.
|
||||
* background effects toggle CSS classes on <html> or start WebGL shaders.
|
||||
*/
|
||||
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { showToast } from '../core/ui.ts';
|
||||
import {
|
||||
startShaderEffect, stopShaderEffect, isShaderEffect,
|
||||
updateShaderAccent, updateShaderTheme,
|
||||
type ShaderEffectId,
|
||||
} from '../core/bg-shaders.ts';
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────
|
||||
|
||||
@@ -42,10 +47,12 @@ interface ThemeVars {
|
||||
interface BgEffectPreset {
|
||||
readonly id: string;
|
||||
readonly nameKey: string;
|
||||
/** CSS class added to <html>. '' = no effect. */
|
||||
/** CSS class added to bg-effect-layer. '' = none (shader or webgl-only). */
|
||||
readonly cssClass: string;
|
||||
/** For the WebGL noise field, we reuse the existing data-bg-anim attr */
|
||||
/** Reuse the existing WebGL noise-field (data-bg-anim). */
|
||||
readonly useBgAnim: boolean;
|
||||
/** Shader effect ID (from bg-shaders.ts). '' = not a shader. */
|
||||
readonly shaderId: string;
|
||||
}
|
||||
|
||||
// ─── Style preset definitions ───────────────────────────────
|
||||
@@ -90,10 +97,10 @@ const STYLE_PRESETS: readonly StylePreset[] = [
|
||||
{
|
||||
id: 'ember',
|
||||
nameKey: 'appearance.preset.ember',
|
||||
fontBody: "'Space Grotesk', 'DM Sans', -apple-system, sans-serif",
|
||||
fontBody: "'Nunito Sans', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Orbitron', sans-serif",
|
||||
accent: '#FF6D00',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#1a1410', bgSecondary: '#241c14', cardBg: '#2c221a',
|
||||
textColor: '#e8ddd0', textSecondary: '#a89880', textMuted: '#7a6e5e',
|
||||
@@ -108,10 +115,10 @@ const STYLE_PRESETS: readonly StylePreset[] = [
|
||||
{
|
||||
id: 'arctic',
|
||||
nameKey: 'appearance.preset.arctic',
|
||||
fontBody: "'Inter', 'DM Sans', -apple-system, sans-serif",
|
||||
fontBody: "'Outfit', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Orbitron', sans-serif",
|
||||
accent: '#0091EA',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#0e1820', bgSecondary: '#142028', cardBg: '#1a2830',
|
||||
textColor: '#d0e4f0', textSecondary: '#88a8c0', textMuted: '#607888',
|
||||
@@ -159,17 +166,111 @@ const STYLE_PRESETS: readonly StylePreset[] = [
|
||||
borderColor: '#e0c8d8', inputBg: '#f5eaf2',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sakura',
|
||||
nameKey: 'appearance.preset.sakura',
|
||||
fontBody: "'Libre Baskerville', 'Georgia', serif",
|
||||
fontHeading: "'Playfair Display', 'Georgia', serif",
|
||||
accent: '#E8607C',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&family=Playfair+Display:wght@700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#1a1216', bgSecondary: '#241820', cardBg: '#2c1e26',
|
||||
textColor: '#f0dce4', textSecondary: '#b89aa8', textMuted: '#8a6e7c',
|
||||
borderColor: '#3e2c36', inputBg: '#1e1418',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#fdf5f7', bgSecondary: '#f8e8ee', cardBg: '#ffffff',
|
||||
textColor: '#3a1e28', textSecondary: '#6e4a58', textMuted: '#a07888',
|
||||
borderColor: '#ecd0da', inputBg: '#faf0f4',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ocean',
|
||||
nameKey: 'appearance.preset.ocean',
|
||||
fontBody: "'Plus Jakarta Sans', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Orbitron', sans-serif",
|
||||
accent: '#0288D1',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#0a1628', bgSecondary: '#0e1e34', cardBg: '#142640',
|
||||
textColor: '#c8ddf0', textSecondary: '#7498b8', textMuted: '#506e88',
|
||||
borderColor: '#1e3858', inputBg: '#0c1a2e',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#eef5fa', bgSecondary: '#deeaf4', cardBg: '#ffffff',
|
||||
textColor: '#0e2840', textSecondary: '#386080', textMuted: '#6890a8',
|
||||
borderColor: '#c0d8ea', inputBg: '#e4eff6',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copper',
|
||||
nameKey: 'appearance.preset.copper',
|
||||
fontBody: "'Barlow', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Barlow Condensed', 'Barlow', sans-serif",
|
||||
accent: '#D4764E',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700&family=Barlow+Condensed:wght@700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#18120e', bgSecondary: '#221a14', cardBg: '#2a201a',
|
||||
textColor: '#e4d4c4', textSecondary: '#a88e78', textMuted: '#7a6454',
|
||||
borderColor: '#3a2e24', inputBg: '#1c1610',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#faf6f2', bgSecondary: '#f0e6dc', cardBg: '#ffffff',
|
||||
textColor: '#362618', textSecondary: '#6e5440', textMuted: '#9a8268',
|
||||
borderColor: '#dcd0c2', inputBg: '#f6efe6',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vapor',
|
||||
nameKey: 'appearance.preset.vapor',
|
||||
fontBody: "'Quicksand', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Righteous', 'Orbitron', sans-serif",
|
||||
accent: '#E040FB',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Quicksand:wght@400;600;700&family=Righteous&display=swap',
|
||||
dark: {
|
||||
bgColor: '#0e0818', bgSecondary: '#160e24', cardBg: '#1c1430',
|
||||
textColor: '#e8d0f8', textSecondary: '#a078c0', textMuted: '#705898',
|
||||
borderColor: '#2c1e48', inputBg: '#120a1e',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#faf2fe', bgSecondary: '#f0e4f8', cardBg: '#ffffff',
|
||||
textColor: '#281438', textSecondary: '#604078', textMuted: '#9870b0',
|
||||
borderColor: '#dcc8ee', inputBg: '#f4eaf8',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'monolith',
|
||||
nameKey: 'appearance.preset.monolith',
|
||||
fontBody: "'Instrument Sans', 'DM Sans', -apple-system, sans-serif",
|
||||
fontHeading: "'Instrument Sans', sans-serif",
|
||||
accent: '#FFFFFF',
|
||||
fontUrl: 'https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;600;700&display=swap',
|
||||
dark: {
|
||||
bgColor: '#000000', bgSecondary: '#0a0a0a', cardBg: '#111111',
|
||||
textColor: '#e8e8e8', textSecondary: '#888888', textMuted: '#555555',
|
||||
borderColor: '#222222', inputBg: '#080808',
|
||||
},
|
||||
light: {
|
||||
bgColor: '#ffffff', bgSecondary: '#f2f2f2', cardBg: '#fafafa',
|
||||
textColor: '#111111', textSecondary: '#555555', textMuted: '#888888',
|
||||
borderColor: '#dddddd', inputBg: '#f5f5f5',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Background effect definitions ──────────────────────────
|
||||
|
||||
const BG_EFFECT_PRESETS: readonly BgEffectPreset[] = [
|
||||
{ id: 'none', nameKey: 'appearance.bg.none', cssClass: '', useBgAnim: false },
|
||||
{ id: 'noise', nameKey: 'appearance.bg.noise', cssClass: '', useBgAnim: true },
|
||||
{ id: 'grid', nameKey: 'appearance.bg.grid', cssClass: 'bg-effect-grid', useBgAnim: false },
|
||||
{ id: 'mesh', nameKey: 'appearance.bg.mesh', cssClass: 'bg-effect-mesh', useBgAnim: false },
|
||||
{ id: 'scanlines', nameKey: 'appearance.bg.scanlines', cssClass: 'bg-effect-scanlines', useBgAnim: false },
|
||||
{ id: 'particles', nameKey: 'appearance.bg.particles', cssClass: 'bg-effect-particles', useBgAnim: false },
|
||||
{ id: 'none', nameKey: 'appearance.bg.none', cssClass: '', useBgAnim: false, shaderId: '' },
|
||||
{ id: 'noise', nameKey: 'appearance.bg.noise', cssClass: '', useBgAnim: true, shaderId: '' },
|
||||
{ id: 'aurora', nameKey: 'appearance.bg.aurora', cssClass: '', useBgAnim: false, shaderId: 'aurora' },
|
||||
{ id: 'plasma', nameKey: 'appearance.bg.plasma', cssClass: '', useBgAnim: false, shaderId: 'plasma' },
|
||||
{ id: 'rain', nameKey: 'appearance.bg.rain', cssClass: '', useBgAnim: false, shaderId: 'rain' },
|
||||
{ id: 'stars', nameKey: 'appearance.bg.stars', cssClass: '', useBgAnim: false, shaderId: 'stars' },
|
||||
{ id: 'warp', nameKey: 'appearance.bg.warp', cssClass: '', useBgAnim: false, shaderId: 'warp' },
|
||||
{ id: 'grid', nameKey: 'appearance.bg.grid', cssClass: 'bg-effect-grid', useBgAnim: false, shaderId: '' },
|
||||
{ id: 'mesh', nameKey: 'appearance.bg.mesh', cssClass: 'bg-effect-mesh', useBgAnim: false, shaderId: '' },
|
||||
{ id: 'scanlines', nameKey: 'appearance.bg.scanlines', cssClass: 'bg-effect-scanlines', useBgAnim: false, shaderId: '' },
|
||||
];
|
||||
|
||||
// ─── Persistence keys ───────────────────────────────────────
|
||||
@@ -218,6 +319,9 @@ export function applyStylePreset(id: string): void {
|
||||
localStorage.setItem('accentColor', preset.accent);
|
||||
}
|
||||
|
||||
// Update shader accent if a shader effect is running
|
||||
updateShaderAccent(preset.accent);
|
||||
|
||||
// Apply theme-specific color overrides
|
||||
_applyThemeVars(preset);
|
||||
|
||||
@@ -237,30 +341,47 @@ export function applyBgEffect(id: string): void {
|
||||
|
||||
const html = document.documentElement;
|
||||
|
||||
// Remove all CSS effect classes from the dedicated layer element
|
||||
// ── 1. Stop all previous effects ──
|
||||
|
||||
// Stop shader effects
|
||||
stopShaderEffect();
|
||||
|
||||
// Remove CSS effect classes from the dedicated layer
|
||||
const layer = document.getElementById('bg-effect-layer');
|
||||
if (layer) {
|
||||
BG_EFFECT_PRESETS.forEach(e => {
|
||||
if (e.cssClass) layer.classList.remove(e.cssClass);
|
||||
});
|
||||
if (effect.cssClass) layer.classList.add(effect.cssClass);
|
||||
}
|
||||
|
||||
// Set data-bg-effect on <html> so CSS can make body transparent
|
||||
if (effect.cssClass) {
|
||||
// Turn off WebGL noise-field
|
||||
html.setAttribute('data-bg-anim', 'off');
|
||||
localStorage.setItem('bgAnim', 'off');
|
||||
|
||||
// Clear data-bg-effect (CSS transparency trigger)
|
||||
html.removeAttribute('data-bg-effect');
|
||||
|
||||
// ── 2. Activate the selected effect ──
|
||||
|
||||
const hasVisualEffect = effect.useBgAnim || !!effect.cssClass || !!effect.shaderId;
|
||||
|
||||
if (effect.useBgAnim) {
|
||||
// Original WebGL noise field
|
||||
html.setAttribute('data-bg-anim', 'on');
|
||||
localStorage.setItem('bgAnim', 'on');
|
||||
} else if (effect.shaderId && isShaderEffect(effect.shaderId)) {
|
||||
// New shader-based effect — needs body transparency via data-bg-effect
|
||||
html.setAttribute('data-bg-effect', effect.id);
|
||||
startShaderEffect(effect.shaderId as ShaderEffectId);
|
||||
} else if (effect.cssClass && layer) {
|
||||
// CSS-based effect
|
||||
layer.classList.add(effect.cssClass);
|
||||
html.setAttribute('data-bg-effect', effect.id);
|
||||
} else {
|
||||
html.removeAttribute('data-bg-effect');
|
||||
}
|
||||
|
||||
// Toggle WebGL bg-anim (only for the "noise" effect)
|
||||
html.setAttribute('data-bg-anim', effect.useBgAnim ? 'on' : 'off');
|
||||
localStorage.setItem('bgAnim', effect.useBgAnim ? 'on' : 'off');
|
||||
|
||||
// Update header toggle button (lit when any effect is active)
|
||||
const hasEffect = effect.useBgAnim || !!effect.cssClass;
|
||||
// Update header toggle button opacity
|
||||
const bgAnimBtn = document.getElementById('bg-anim-btn');
|
||||
if (bgAnimBtn) bgAnimBtn.style.opacity = hasEffect ? '1' : '0.5';
|
||||
if (bgAnimBtn) bgAnimBtn.style.opacity = hasVisualEffect ? '1' : '0.5';
|
||||
|
||||
_updatePresetSelection('bg', id);
|
||||
|
||||
@@ -279,20 +400,35 @@ export function initAppearance(): void {
|
||||
document.documentElement.style.setProperty('--font-body', preset.fontBody);
|
||||
document.documentElement.style.setProperty('--font-heading', preset.fontHeading);
|
||||
_applyThemeVars(preset);
|
||||
|
||||
// Sync accent to shader engine
|
||||
updateShaderAccent(preset.accent);
|
||||
}
|
||||
|
||||
// Apply background effect silently on the dedicated layer element
|
||||
// Sync theme to shader engine
|
||||
const isDark = (document.documentElement.getAttribute('data-theme') || 'dark') === 'dark';
|
||||
updateShaderTheme(isDark);
|
||||
|
||||
// Apply background effect silently
|
||||
const effect = BG_EFFECT_PRESETS.find(e => e.id === _activeBgEffectId);
|
||||
if (effect) {
|
||||
const layer = document.getElementById('bg-effect-layer');
|
||||
if (layer && effect.cssClass) layer.classList.add(effect.cssClass);
|
||||
if (effect.cssClass) {
|
||||
document.documentElement.setAttribute('data-bg-effect', effect.id);
|
||||
}
|
||||
if (effect && effect.id !== 'none') {
|
||||
const html = document.documentElement;
|
||||
|
||||
if (effect.useBgAnim) {
|
||||
document.documentElement.setAttribute('data-bg-anim', 'on');
|
||||
html.setAttribute('data-bg-anim', 'on');
|
||||
localStorage.setItem('bgAnim', 'on');
|
||||
} else if (effect.shaderId && isShaderEffect(effect.shaderId)) {
|
||||
html.setAttribute('data-bg-effect', effect.id);
|
||||
startShaderEffect(effect.shaderId as ShaderEffectId);
|
||||
} else if (effect.cssClass) {
|
||||
const layer = document.getElementById('bg-effect-layer');
|
||||
if (layer) layer.classList.add(effect.cssClass);
|
||||
html.setAttribute('data-bg-effect', effect.id);
|
||||
}
|
||||
|
||||
// Update header button
|
||||
const bgAnimBtn = document.getElementById('bg-anim-btn');
|
||||
if (bgAnimBtn) bgAnimBtn.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +552,9 @@ const _themeObserver = new MutationObserver((mutations) => {
|
||||
if (preset && preset.id !== 'default') {
|
||||
_applyThemeVars(preset);
|
||||
}
|
||||
// Sync shader theme
|
||||
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
updateShaderTheme(isDark);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user