feat(shop): animated backgrounds — system-wide cosmetic + picker
A new cosmetic family: a fixed-position overlay painted behind every
page of the app, switchable from the profile shop. 4 free presets + 6
paid (250-1200 coins) so the new economy has another sink. Every
animation respects prefers-reduced-motion and falls back to its static
gradient.
Catalogue (migration 035):
free: none, gradient-soft, dots, dark
paid: gradient-flow, grid, bubbles, stars (mid)
aurora, nebula (premium)
Backend:
• migration 035 adds users.active_background + rebuilds shop_items
CHECK to include 'background' (standard SQLite 'new + copy + swap')
and seeds 10 items
• shopController.getMyActive returns { background: { slug } } and
activateItem handles type='background' (stores bare slug in
active_background) + skips the user_purchases check for price=0
so free presets work for everyone without per-user rows
• routes/shop validate schema lets 'background' through
Frontend:
• api.js applyCosmetics injects <div id='ls-bg-fx'> at body start
and toggles class to bg-<slug>. Cleared backgrounds remove the
element so dark→light transitions don't leave artifacts.
• ls.css gains a self-contained 'ANIMATED BACKGROUNDS' block:
keyframes per animated slug (ls-bg-flow, ls-bg-grid-scan,
ls-bg-bubble-rise, ls-bg-stars-twinkle, ls-bg-aurora-spin,
ls-bg-nebula-pan) wrapped in a prefers-reduced-motion kill-switch.
Same .bg-<slug> classes are reused for the .bg-preview swatches.
• profile.html shop:
- new 'Фоны' filter button between Рамки and Титулы
- _renderItemPreview type='background' draws a real 56-aspect swatch
(same CSS as the page bg — what you see is what you apply)
- _isItemActive matches by slug for background type
- free items (price===0) treated as auto-owned in render so users
can apply them without a fake 'purchase' step
Verified: getMyActive returns { background: { slug: 'nebula' } } after
flipping users.active_background; activate path updates the row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 035: Animated backgrounds — a new cosmetic family
|
||||
--
|
||||
-- Adds an animated background layer that paints behind every page
|
||||
-- of the app. Switchable from the profile page (Phase 6 UI). 4
|
||||
-- backgrounds are free (price=0, available to everyone — the picker
|
||||
-- treats price-0 items as owned without a user_purchases row), 6 are
|
||||
-- paid in the 200-1200 coin range to give the new economy something
|
||||
-- to reach for.
|
||||
--
|
||||
-- Storage:
|
||||
-- • new column users.active_background — slug of the chosen bg
|
||||
-- (NULL = system default; client renders nothing extra)
|
||||
-- • new shop_items rows with type='background' + data.slug
|
||||
-- • the existing CHECK (type IN ('frame','theme','title','effect'))
|
||||
-- on shop_items has to be rebuilt to include 'background' — done
|
||||
-- here via the standard SQLite "create new + copy + swap" dance.
|
||||
--
|
||||
-- Rendering (frontend):
|
||||
-- The applyCosmetics call injects <div id="ls-bg-fx"> at body start
|
||||
-- and sets its class to bg-<slug>. CSS in ls.css (added in the same
|
||||
-- commit) drives the per-slug animation. Every animation respects
|
||||
-- prefers-reduced-motion — falls back to the static gradient.
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE users ADD COLUMN active_background TEXT;
|
||||
|
||||
-- ── Rebuild shop_items with 'background' added to the type check ──
|
||||
CREATE TABLE shop_items_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'frame'
|
||||
CHECK (type IN ('frame','theme','title','effect','background')),
|
||||
category TEXT NOT NULL DEFAULT 'cosmetic',
|
||||
price INTEGER NOT NULL DEFAULT 100,
|
||||
data TEXT NOT NULL DEFAULT '{}',
|
||||
icon TEXT NOT NULL DEFAULT 'star',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
INSERT INTO shop_items_new
|
||||
(id, name, description, type, category, price, data, icon, is_active, created_at)
|
||||
SELECT
|
||||
id, name, description, type, category, price, data, icon, is_active, created_at
|
||||
FROM shop_items;
|
||||
|
||||
DROP TABLE shop_items;
|
||||
ALTER TABLE shop_items_new RENAME TO shop_items;
|
||||
|
||||
-- ── Free backgrounds (price=0; auto-owned) ─────────────────────
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Без фона', 'Системный фон без эффектов', 'background', 'cosmetic', 0,
|
||||
'{"slug":"none"}', 'circle-off', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Без фона' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Мягкий градиент', 'Спокойный переход двух пастельных тонов', 'background', 'cosmetic', 0,
|
||||
'{"slug":"gradient-soft"}', 'palette', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Мягкий градиент' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Точки', 'Тонкая сетка точек на светлом фоне', 'background', 'cosmetic', 0,
|
||||
'{"slug":"dots"}', 'more-horizontal', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Точки' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Тёмный фон', 'Глубокий синий фон без анимаций', 'background', 'cosmetic', 0,
|
||||
'{"slug":"dark"}', 'moon', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Тёмный фон' AND type='background');
|
||||
|
||||
-- ── Paid backgrounds ─────────────────────────────────────────────
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Плавающий градиент', 'Медленно перетекающие фиолетово-бирюзовые волны', 'background', 'cosmetic', 250,
|
||||
'{"slug":"gradient-flow"}', 'waves', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Плавающий градиент' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Сетка', 'Голубая инженерная сетка с подсветкой', 'background', 'cosmetic', 350,
|
||||
'{"slug":"grid"}', 'grid-3x3', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Сетка' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Пузырьки', 'Лёгкие восходящие пузырьки', 'background', 'cosmetic', 450,
|
||||
'{"slug":"bubbles"}', 'droplets', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Пузырьки' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Звёзды', 'Мерцающее звёздное небо', 'background', 'cosmetic', 600,
|
||||
'{"slug":"stars"}', 'sparkles', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Звёзды' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Северное сияние', 'Плавающий conic-gradient в холодных тонах', 'background', 'cosmetic', 900,
|
||||
'{"slug":"aurora"}', 'wand-sparkles', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Северное сияние' AND type='background');
|
||||
|
||||
INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active)
|
||||
SELECT 'Туманность', 'Космическая туманность с глубиной', 'background', 'cosmetic', 1200,
|
||||
'{"slug":"nebula"}', 'galaxy', 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM shop_items WHERE name='Туманность' AND type='background');
|
||||
Reference in New Issue
Block a user