diff --git a/README.md b/README.md
index d2c75bf..f186298 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,10 @@ To Be Played is split into three repositories:
years of full-time work, and its oldest unplayed game; most played and most loyal games; top genres and moods;
trips to the moon, the longest gaming day, games started but under one hour; finished games per year; games per platform.
- **Pile Points** (`/points`): points for playing games off your pile (start, finish by time to beat, 100 %, month and
- streak bonuses; Core checks proof of play), spent in a small shop on an avatar border, a badge, a premium accent and a
- banner frame, shown on the dashboard and the public profile. The balance is in the header.
+ streak bonuses; Core checks proof of play), spent in a small shop on an avatar border, a badge, a premium accent, a
+ banner frame, an effect for 100 % games and an animated page or banner background (CSS and SVG, in `effects.css`),
+ shown on the dashboard and the public profile. The balance is in the header. Every effect stands still with the
+ system setting "reduce motion" or the "Reduce animations" switch in the settings.
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
finished games per year of all users.
- **Library**: status filters, "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
@@ -71,7 +73,7 @@ To Be Played is split into three repositories:
every achievement is unlocked.
- **Confetti**: opening a finished game plays a small confetti burst, a finished game with every achievement a big
one; setting the status to finished plays it at once. Logo and accent colors, a canvas of our own (no library),
- nothing with "reduce motion".
+ nothing with "reduce motion" (system or the settings switch).
- **Finished?**: Core marks a game as finished by itself when the ending achievement is unlocked and the status was
never set by hand. Otherwise the game page asks: "You unlocked the ending, finished?" or "You played longer than
most players need, finished?", with "Mark as finished" and "Not yet".
@@ -85,7 +87,7 @@ To Be Played is split into three repositories:
The owner arranges it with the same edit mode on the profile page itself (the tiles, incl. the stat groups of
"Your stats"), and sets its look in the settings: accent, corners, banner and background from own game art
("Profile art" on a game's page). Visitors see the owner's look on that page only; light or dark mode stays theirs.
-- **Appearance**: accent color, corner style, wide layout, page background and banner from your own game art.
+- **Appearance**: accent color, corner style, wide layout, reduce animations, page background and banner from your own game art.
Saved in Auth, so it follows you to every device.
- **Language**: kept in the browser and saved in Auth; after the login Auth's language wins when one is saved. Dark
mode stays per device. The registration sends the browser language (`de` for a German browser, else `en`): Auth
diff --git a/public/i18n/de.json b/public/i18n/de.json
index 37c5e40..0bdaf3b 100644
--- a/public/i18n/de.json
+++ b/public/i18n/de.json
@@ -860,7 +860,10 @@
"AVATAR_BORDER": "Avatarrahmen",
"BADGE": "Abzeichen",
"ACCENT": "Akzent",
- "BANNER_FRAME": "Bannerrahmen"
+ "BANNER_FRAME": "Bannerrahmen",
+ "COMPLETED_EFFECT": "Effekt für 100 %",
+ "PAGE_BACKGROUND": "Seitenhintergrund",
+ "BANNER_BACKGROUND": "Bannerhintergrund"
},
"ITEMS": {
"BORDER_GOLD": "Goldener Avatarrahmen",
@@ -870,7 +873,27 @@
"ACCENT_GOLD": "Akzent Gold",
"ACCENT_TEAL": "Akzent Türkis",
"FRAME_GOLD": "Goldener Bannerrahmen",
- "FRAME_NEON": "Neon-Bannerrahmen"
+ "FRAME_NEON": "Neon-Bannerrahmen",
+ "FX_GOLD_SHINE": "Goldglanz",
+ "FX_NEON_PULSE": "Neon-Puls",
+ "FX_HOLO": "Holo-Folie",
+ "FX_EMBER": "Glut",
+ "BG_TRIANGLES": "Animierter Hintergrund: Dreiecke",
+ "BN_TRIANGLES": "Animiertes Banner: Dreiecke",
+ "BG_STARFIELD": "Animierter Hintergrund: Sternenhimmel",
+ "BN_STARFIELD": "Animiertes Banner: Sternenhimmel",
+ "BG_PIXELS": "Animierter Hintergrund: Pixel",
+ "BN_PIXELS": "Animiertes Banner: Pixel",
+ "BG_AURORA": "Animierter Hintergrund: Polarlicht",
+ "BN_AURORA": "Animiertes Banner: Polarlicht",
+ "BG_MESH": "Animierter Hintergrund: Farbverlauf",
+ "BN_MESH": "Animiertes Banner: Farbverlauf",
+ "BG_GRID": "Animierter Hintergrund: Synthwave-Gitter",
+ "BN_GRID": "Animiertes Banner: Synthwave-Gitter",
+ "BG_WAVES": "Animierter Hintergrund: Wellen",
+ "BN_WAVES": "Animiertes Banner: Wellen",
+ "BG_BOKEH": "Animierter Hintergrund: Bokeh-Lichter",
+ "BN_BOKEH": "Animiertes Banner: Bokeh-Lichter"
},
"KINDS": {
"STARTED": "Gestartet",
diff --git a/public/i18n/en.json b/public/i18n/en.json
index cfc7af6..95ba825 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -860,7 +860,10 @@
"AVATAR_BORDER": "Avatar border",
"BADGE": "Badge",
"ACCENT": "Accent",
- "BANNER_FRAME": "Banner frame"
+ "BANNER_FRAME": "Banner frame",
+ "COMPLETED_EFFECT": "Completed effect",
+ "PAGE_BACKGROUND": "Page background",
+ "BANNER_BACKGROUND": "Banner background"
},
"ITEMS": {
"BORDER_GOLD": "Golden avatar border",
@@ -870,7 +873,27 @@
"ACCENT_GOLD": "Gold accent",
"ACCENT_TEAL": "Teal accent",
"FRAME_GOLD": "Golden banner frame",
- "FRAME_NEON": "Neon banner frame"
+ "FRAME_NEON": "Neon banner frame",
+ "FX_GOLD_SHINE": "Gold shine",
+ "FX_NEON_PULSE": "Neon pulse",
+ "FX_HOLO": "Holographic foil",
+ "FX_EMBER": "Embers",
+ "BG_TRIANGLES": "Animated background: triangles",
+ "BN_TRIANGLES": "Animated banner: triangles",
+ "BG_STARFIELD": "Animated background: starfield",
+ "BN_STARFIELD": "Animated banner: starfield",
+ "BG_PIXELS": "Animated background: pixels",
+ "BN_PIXELS": "Animated banner: pixels",
+ "BG_AURORA": "Animated background: aurora",
+ "BN_AURORA": "Animated banner: aurora",
+ "BG_MESH": "Animated background: mesh gradient",
+ "BN_MESH": "Animated banner: mesh gradient",
+ "BG_GRID": "Animated background: synthwave grid",
+ "BN_GRID": "Animated banner: synthwave grid",
+ "BG_WAVES": "Animated background: waves",
+ "BN_WAVES": "Animated banner: waves",
+ "BG_BOKEH": "Animated background: bokeh",
+ "BN_BOKEH": "Animated banner: bokeh"
},
"KINDS": {
"STARTED": "Started",
diff --git a/src/components/EffectBackground.jsx b/src/components/EffectBackground.jsx
new file mode 100644
index 0000000..e623706
--- /dev/null
+++ b/src/components/EffectBackground.jsx
@@ -0,0 +1,84 @@
+import {cn} from '@/lib/utils';
+
+export const EFFECTS = ['triangles', 'starfield', 'pixels', 'aurora', 'mesh', 'grid', 'waves', 'bokeh'];
+
+const range = (count) => Array.from({length: count}, (_, i) => i);
+
+// Three groups of eight triangles on a 1600x900 canvas; a fixed pattern, so the picture is the same on every visit.
+const TRIANGLES = range(3).map((group) => range(8).map((i) => {
+ const x = (i * 211 + group * 97) % 1600;
+ const y = (i * 137 + group * 263) % 900;
+ const size = 80 + ((i * 53 + group * 29) % 90);
+ return {
+ points: `${x},${y - size} ${x - size * 0.87},${y + size / 2} ${x + size * 0.87},${y + size / 2}`,
+ rotate: `rotate(${i * 40} ${x} ${y})`,
+ opacity: 0.06 + ((i + group) % 4) * 0.04,
+ };
+}));
+
+// One period is 1600 units; two periods make the band 2 widths wide, so a move of one width loops without a jump.
+const WAVE = 'M0 100 C200 30 600 30 800 100 C1000 170 1400 170 1600 100 C1800 30 2200 30 2400 100 '
+ + 'C2600 170 3000 170 3200 100 L3200 200 L0 200 Z';
+
+/**
+ * The markup of an animated page or banner background (CSS in effects.css). Fills its parent: pass `absolute inset-0`
+ * or `fixed inset-0`. Nothing here is read by a screen reader.
+ *
+ * @param effect one of EFFECTS; anything else renders nothing
+ */
+export default function EffectBackground({effect, className}) {
+ if (!EFFECTS.includes(effect)) return null;
+
+ return (
+