diff --git a/ANIMATION_EXAMPLES.md b/ANIMATION_EXAMPLES.md
new file mode 100644
index 0000000000000..4b53154978d1c
--- /dev/null
+++ b/ANIMATION_EXAMPLES.md
@@ -0,0 +1,316 @@
+# ๐จ Cybernetic Mad Scientist Animation Styles
+
+This document showcases the new animation styles and cybernetic themes for repo cards!
+
+## ๐ค Cybernetic Themes
+
+Five new themes inspired by a cybernetic mad scientist laboratory aesthetic:
+
+### 1. **mad_scientist** - Electric Blue Laboratory
+Bright cyan and blue tones with a dark space background.
+```
+theme=mad_scientist
+```
+- Title: `#00d9ff` (bright cyan)
+- Text: `#7dd3fc` (sky blue)
+- Icons: `#38bdf8` (blue)
+- Border: `#0ea5e9` (deep blue)
+- Background: `#0c1021` (dark navy)
+
+### 2. **mad_scientist_dark** - Deep Laboratory
+Darker, more intense cyan with near-black background.
+```
+theme=mad_scientist_dark
+```
+- Title: `#22d3ee` (cyan)
+- Text: `#67e8f9` (light cyan)
+- Icons: `#06b6d4` (darker cyan)
+- Border: `#0891b2` (teal)
+- Background: `#020617` (almost black)
+
+### 3. **cybernetic_lab** - Classic Blue Tech
+Traditional tech blue with laboratory feel.
+```
+theme=cybernetic_lab
+```
+- Title: `#3b82f6` (blue)
+- Text: `#60a5fa` (light blue)
+- Icons: `#2563eb` (royal blue)
+- Border: `#1d4ed8` (deep blue)
+- Background: `#0a0e1a` (dark blue-black)
+
+### 4. **robot_blue** - Robot Head Inspired
+Inspired by the blue robot avatar aesthetic.
+```
+theme=robot_blue
+```
+- Title: `#0ea5e9` (sky blue)
+- Text: `#7dd3fc` (light sky)
+- Icons: `#38bdf8` (bright blue)
+- Border: `#0284c7` (ocean blue)
+- Background: `#082f49` (dark teal)
+
+### 5. **electric_laboratory** - Electric Cyan
+High-contrast electric cyan with modern lab feel.
+```
+theme=electric_laboratory
+```
+- Title: `#00ffff` (pure cyan)
+- Text: `#5eead4` (teal)
+- Icons: `#2dd4bf` (turquoise)
+- Border: `#14b8a6` (dark teal)
+- Background: `#0f172a` (slate black)
+
+---
+
+## โก Animation Styles
+
+Five unique animation effects for your repo cards:
+
+### 1. **bubbles** - Fishtank Effect ๐
+A complete aquarium experience with bubbles, glowing jellyfish, drifting starfish, and a mesmerizing wave text effect.
+```
+animation_style=bubbles
+```
+- 8 bubbles floating upward with varying sizes and speeds
+- 2 glowing jellyfish with wavy tentacles drifting left to right
+- 2 starfish slowly rotating and drifting right to left
+- **๐ Horizontal wave text effect**: Letters ripple like a wave traveling across the text
+ - Each character animates individually with a staggered delay
+ - Creates a smooth left-to-right wave motion across the title
+ - Fully customizable wave parameters (see below)
+ - Optional color-morphing gradient effect
+- Jellyfish appear every ~12 seconds with gentle pulsing glow
+- Starfish drift across every ~15 seconds with slow rotation
+- All creatures layered behind text for depth
+- Perfect for: Calm, steady progress projects, marine/ocean themes, underwater aesthetics
+
+**Wave Customization Parameters:**
+- `wave_speed` - Wave cycle duration in seconds (default: `2`)
+ - Lower = faster wave, Higher = slower wave
+ - Example: `wave_speed=1.5` for faster waves
+- `wave_amplitude` - Vertical movement in pixels (default: `3`)
+ - How high each letter bounces
+ - Example: `wave_amplitude=5` for bigger waves
+- `wave_delay` - Delay between each character in seconds (default: `0.05`)
+ - Controls how quickly wave travels horizontally
+ - Example: `wave_delay=0.08` for slower wave travel
+- `color_morph` - Enable color morphing gradient (default: `false`)
+ - Letters cycle through theme colors
+ - Example: `color_morph=true`
+
+### 2. **embers** - Burning Particles ๐ฅ
+Glowing particles pulse and float like hot embers.
+```
+animation_style=embers
+```
+- 12 glowing particles
+- Pulsing glow effect with blur
+- Gentle floating motion
+- 2-4 second animation cycles
+- Perfect for: Active, hot projects
+
+### 3. **radiant** - Pulsing Sun โ๏ธ
+Radiant rays emanate from the center with a pulsing core.
+```
+animation_style=radiant
+```
+- 16 rays radiating from center
+- Pulsing central core
+- Sequential wave animation
+- 2 second pulse cycle
+- Perfect for: Central, important projects
+
+### 4. **circuit** - Edge Traveler ๐
+Dots travel around the card edges like signals in a circuit.
+```
+animation_style=circuit
+```
+- 6 glowing dots traveling the perimeter
+- Glowing edge trail effects
+- Continuous loop motion
+- 4 second travel cycle
+- Perfect for: Tech, systematic projects
+
+### 5. **sparks** - Electric Sparks โก
+Electric sparks flash randomly across the card.
+```
+animation_style=sparks
+```
+- 10 electric spark bursts
+- Random positions
+- Flash and fade effect
+- 5 second cycle with stagger
+- Perfect for: Energetic, innovative projects
+
+---
+
+## ๐ฏ Usage Examples
+
+### Basic Animation
+```markdown
+
+```
+
+### With Cybernetic Theme
+```markdown
+
+```
+
+### Full Customization
+```markdown
+
+```
+
+### Custom Wave Effect (Fast & Big)
+```markdown
+
+```
+
+### Color Morphing Wave
+```markdown
+
+```
+
+### Slow Gentle Wave
+```markdown
+
+```
+
+### Disable Animations (for static images)
+```markdown
+
+```
+
+---
+
+## ๐จ Recommended Combinations
+
+Here are some great theme + animation pairings:
+
+### The Scientist's Lab
+```
+theme=mad_scientist&animation_style=bubbles
+```
+Blue laboratory with gentle bubbles rising - perfect for research projects.
+
+### The Robot Workshop
+```
+theme=robot_blue&animation_style=circuit
+```
+Robot-inspired blues with circuit paths - ideal for robotics/automation.
+
+### Electric Experiment
+```
+theme=electric_laboratory&animation_style=sparks
+```
+High-voltage cyan with electric sparks - great for exciting new projects.
+
+### Burning Innovation
+```
+theme=cybernetic_lab&animation_style=embers
+```
+Tech blue with glowing embers - perfect for hot, active development.
+
+### Radiant Core
+```
+theme=mad_scientist_dark&animation_style=radiant
+```
+Dark background with pulsing radiant center - excellent for core libraries.
+
+---
+
+## ๐ Parameters Reference
+
+### Animation Parameters
+- `animation_style` - Animation effect to use
+ - Options: `none`, `bubbles`, `embers`, `radiant`, `circuit`, `sparks`
+ - Default: `none`
+
+- `disable_animations` - Disable all animations
+ - Options: `true`, `false`
+ - Default: `false`
+
+### Wave Customization Parameters (bubbles only)
+- `wave_speed` - Duration of one wave cycle in seconds
+ - Range: `0.5` to `5` (recommended)
+ - Default: `2`
+ - Example: `wave_speed=1.5` (faster)
+
+- `wave_amplitude` - Vertical movement height in pixels
+ - Range: `1` to `10` (recommended)
+ - Default: `3`
+ - Example: `wave_amplitude=5` (bigger waves)
+
+- `wave_delay` - Delay between each character in seconds
+ - Range: `0.01` to `0.2` (recommended)
+ - Default: `0.05`
+ - Example: `wave_delay=0.08` (slower horizontal travel)
+
+- `color_morph` - Enable color morphing gradient effect
+ - Options: `true`, `false`
+ - Default: `false`
+ - Cycles through title, icon, and text colors
+ - Example: `color_morph=true`
+
+### All Compatible Parameters
+You can combine animations with all existing repo card parameters:
+- `theme` - Choose from 65+ themes (including 5 new cybernetic ones)
+- `title_color`, `icon_color`, `text_color`, `bg_color`, `border_color` - Custom colors
+- `hide_border`, `hide_title`, `hide_text` - Hide elements
+- `show_owner` - Show full username/repo
+- `show_issues`, `show_prs`, `show_age` - Show extra stats
+- `all_stats` - Show all available stats
+- `border_radius` - Customize corner rounding
+- `locale` - Set language
+
+---
+
+## ๐ฌ Animation Performance
+
+All animations are:
+- โ
Pure CSS/SVG (no JavaScript required)
+- โ
Lightweight (minimal impact on file size)
+- โ
Smooth (GPU-accelerated where possible)
+- โ
Accessible (can be disabled with `disable_animations=true`)
+- โ
Compatible with all modern browsers
+
+---
+
+## ๐ Quick Start
+
+1. Choose a theme from the cybernetic collection
+2. Pick an animation style that matches your project vibe
+3. Add to your README:
+
+```markdown
+[](https://github.com/hesreallyhim/your-repo)
+```
+
+---
+
+## ๐ก Tips
+
+1. **For READMEs viewed on GitHub**: All animations work perfectly in SVG!
+2. **For static documentation**: Use `disable_animations=true`
+3. **Performance**: Animations use minimal resources and won't slow page load
+4. **Accessibility**: Users with `prefers-reduced-motion` should disable animations
+5. **Caching**: Animation style is included in cache key, so changes update immediately
+
+---
+
+## ๐จ Color Customization
+
+You can override theme colors while keeping animations:
+
+```markdown
+
+```
+
+Animations will automatically use your custom colors!
+
+---
+
+## ๐งช Experiment!
+
+Don't be afraid to mix and match! Try different combinations to find the perfect look for your project. The cybernetic mad scientist aesthetic is all about creative experimentation! ๐ฌโก๐ค
diff --git a/api/index.js b/api/index.js
index 6ea4ffe0c20e7..b7a4d53d237ed 100644
--- a/api/index.js
+++ b/api/index.js
@@ -48,6 +48,7 @@ export default async (req, res) => {
border_color,
rank_icon,
show,
+ rank_animation,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
@@ -131,6 +132,7 @@ export default async (req, res) => {
disable_animations: parseBoolean(disable_animations),
rank_icon,
show: showStats,
+ rank_animation: rank_animation || "default",
}),
);
} catch (err) {
diff --git a/api/pin.js b/api/pin.js
index 6690ca9206aec..d988e37d1b4e0 100644
--- a/api/pin.js
+++ b/api/pin.js
@@ -42,6 +42,12 @@ export default async (req, res) => {
show_prs,
show_age,
age_metric,
+ animation_style,
+ disable_animations,
+ wave_speed,
+ wave_amplitude,
+ wave_delay,
+ color_morph,
} = req.query;
res.setHeader("Content-Type", "image/svg+xml");
@@ -116,6 +122,12 @@ export default async (req, res) => {
show_prs: finalShowPrs,
show_age: finalShowAge,
age_metric: age_metric || "first",
+ animation_style: animation_style || "none",
+ disable_animations: parseBoolean(disable_animations),
+ wave_speed: wave_speed ? parseFloat(wave_speed) : 2,
+ wave_amplitude: wave_amplitude ? parseFloat(wave_amplitude) : 3,
+ wave_delay: wave_delay ? parseFloat(wave_delay) : 0.05,
+ color_morph: parseBoolean(color_morph),
}),
);
} catch (err) {
diff --git a/src/cards/repo.js b/src/cards/repo.js
index 12b450a6f5bec..dedca620d00dc 100644
--- a/src/cards/repo.js
+++ b/src/cards/repo.js
@@ -19,6 +19,444 @@ const ICON_SIZE = 16;
const DESCRIPTION_LINE_WIDTH = 59;
const DESCRIPTION_MAX_LINES = 3;
+/**
+ * Wraps each character of text in a tspan with staggered animation delay for wave effect.
+ *
+ * @param {string} text The text to wrap.
+ * @param {number} baseDelay Base delay in seconds before wave starts.
+ * @param {number} delayPerChar Delay in seconds between each character.
+ * @param {boolean} colorMorph Whether to enable color morphing effect.
+ * @returns {string} SVG tspan elements with wave animation.
+ */
+const wrapTextInWave = (
+ text,
+ baseDelay = 0,
+ delayPerChar = 0.05,
+ colorMorph = false,
+) => {
+ return Array.from(text)
+ .map((char, i) => {
+ const delay = baseDelay + i * delayPerChar;
+ // Preserve spaces
+ const displayChar = char === " " ? "\u00A0" : char;
+ const morphClass = colorMorph ? " wave-char-morph" : "";
+ return `${displayChar}`;
+ })
+ .join("");
+};
+
+/**
+ * Generates animation styles and SVG elements for different effects.
+ *
+ * @param {string} style Animation style: bubbles, embers, radiant, circuit, sparks.
+ * @param {object} colors Card colors for theming animations.
+ * @param {number} width Card width.
+ * @param {number} height Card height.
+ * @param {object} waveParams Wave animation parameters.
+ * @returns {{css: string, svg: string}} Animation CSS and SVG elements.
+ */
+const getAnimationStyle = (style, colors, width, height, waveParams = {}) => {
+ if (!style || style === "none") {
+ return { css: "", svg: "" };
+ }
+
+ const iconColor = colors.iconColor || "38bdf8";
+ const titleColor = colors.titleColor || "00d9ff";
+ const textColor = colors.textColor || "434d58";
+
+ // Wave parameters with defaults
+ const waveSpeed = waveParams.speed || 2; // seconds
+ const waveAmplitude = waveParams.amplitude || 3; // pixels
+
+ switch (style) {
+ case "bubbles": {
+ // Fishtank-style floating bubbles
+ const bubbles = Array.from({ length: 8 }, (_, i) => {
+ const x = (width * (i + 1)) / 9;
+ const size = 3 + (i % 3) * 2;
+ const delay = i * 0.4;
+ const duration = 3 + (i % 3);
+ return `
+ `;
+ }).join("");
+
+ // Glowing jellyfish that floats across
+ const jellyfishCount = 2;
+ const jellyfish = Array.from({ length: jellyfishCount }, (_, i) => {
+ const startY = height * 0.3 + i * height * 0.25;
+ const delay = i * 12 + 2; // Appear every 12 seconds, staggered
+ const bellSize = 12 + i * 3;
+
+ return `
+
+
+
+
+
+ ${Array.from({ length: 6 }, (_, t) => {
+ const tentacleX = -bellSize * 0.6 + t * bellSize * 0.24;
+ return `
+ `;
+ }).join("")}
+
+
+ `;
+ }).join("");
+
+ // Starfish that drifts across
+ const starfishCount = 2;
+ const starfish = Array.from({ length: starfishCount }, (_, i) => {
+ const startY = height * 0.5 + i * height * 0.2;
+ const delay = i * 15 + 7; // Offset from jellyfish timing
+ const size = 8 + i * 2;
+
+ // Create 5-pointed star path
+ const points =
+ Array.from({ length: 5 }, (_, p) => {
+ const angle = ((p * 72 - 90) * Math.PI) / 180;
+ const outerX = Math.cos(angle) * size;
+ const outerY = Math.sin(angle) * size;
+ const innerAngle = ((p * 72 + 36 - 90) * Math.PI) / 180;
+ const innerX = Math.cos(innerAngle) * size * 0.4;
+ const innerY = Math.sin(innerAngle) * size * 0.4;
+ return `${p === 0 ? "M" : "L"} ${outerX},${outerY} L ${innerX},${innerY}`;
+ }).join(" ") + " Z";
+
+ return `
+
+
+
+
+
+
+ `;
+ }).join("");
+
+ const css = `
+ @keyframes bubbleFloat {
+ 0% { transform: translateY(0) scale(1); opacity: 0.3; }
+ 50% { opacity: 0.5; }
+ 100% { transform: translateY(-${height + 20}px) scale(0.5); opacity: 0; }
+ }
+ @keyframes jellyfishPulse {
+ 0%, 100% { opacity: 0; }
+ 10%, 90% { opacity: 1; }
+ 50% { opacity: 0.8; }
+ }
+ @keyframes tentacleWave {
+ 0%, 100% { transform: translateX(0); }
+ 50% { transform: translateX(2px); }
+ }
+ @keyframes starfishDrift {
+ 0%, 100% { opacity: 0; }
+ 10%, 90% { opacity: 1; }
+ }
+ @keyframes letterWave {
+ 0%, 100% { transform: translateY(0px); }
+ 50% { transform: translateY(-${waveAmplitude}px); }
+ }
+ @keyframes colorMorph {
+ 0% { fill: #${titleColor}; }
+ 25% { fill: #${iconColor}; }
+ 50% { fill: #${textColor}; }
+ 75% { fill: #${iconColor}; }
+ 100% { fill: #${titleColor}; }
+ }
+ .bubble {
+ animation: bubbleFloat 3s infinite ease-in-out;
+ }
+ .jellyfish {
+ animation: jellyfishPulse 20s infinite ease-in-out;
+ filter: drop-shadow(0 0 4px ${titleColor}40);
+ }
+ .tentacle {
+ animation: tentacleWave 2s infinite ease-in-out;
+ }
+ .starfish {
+ animation: starfishDrift 25s infinite ease-in-out;
+ }
+ /* Character-by-character wave effect */
+ .wave-char {
+ animation: letterWave ${waveSpeed}s ease-in-out infinite;
+ }
+ /* Color morphing effect */
+ .wave-char-morph {
+ animation: letterWave ${waveSpeed}s ease-in-out infinite, colorMorph ${waveSpeed * 3}s ease-in-out infinite;
+ }`;
+
+ // SVG filter for jellyfish glow
+ const filters = `
+
+
+
+
+
+
+
+
+ `;
+
+ return {
+ css,
+ svg: `${filters}${jellyfish}${starfish}${bubbles}`,
+ };
+ }
+
+ case "embers": {
+ // Glowing particles like burning embers
+ const embers = Array.from({ length: 12 }, (_, i) => {
+ const x = 10 + Math.random() * (width - 20);
+ const y = height * 0.2 + Math.random() * (height * 0.6);
+ const size = 1.5 + Math.random() * 2;
+ const delay = i * 0.3;
+ return `
+ `;
+ }).join("");
+
+ const css = `
+ @keyframes emberGlow {
+ 0%, 100% { opacity: 0.2; filter: blur(0px); }
+ 25% { opacity: 0.8; filter: blur(1px); }
+ 50% { opacity: 0.4; filter: blur(0.5px); }
+ 75% { opacity: 0.9; filter: blur(1.5px); }
+ }
+ @keyframes emberFloat {
+ 0%, 100% { transform: translate(0, 0); }
+ 33% { transform: translate(3px, -5px); }
+ 66% { transform: translate(-3px, 5px); }
+ }
+ .ember {
+ animation: emberGlow 2s infinite ease-in-out, emberFloat 4s infinite ease-in-out;
+ }`;
+
+ return { css, svg: `${embers}` };
+ }
+
+ case "radiant": {
+ // Radiant sun with pulsing rays
+ const rays = Array.from({ length: 16 }, (_, i) => {
+ const angle = (i * 360) / 16;
+ const length = 80;
+ const x1 = width / 2;
+ const y1 = height / 2;
+ const x2 = x1 + Math.cos((angle * Math.PI) / 180) * length;
+ const y2 = y1 + Math.sin((angle * Math.PI) / 180) * length;
+ const delay = i * 0.05;
+ return `
+ `;
+ }).join("");
+
+ const core = `
+ `;
+
+ const css = `
+ @keyframes rayPulse {
+ 0%, 100% { opacity: 0.1; stroke-width: 1; }
+ 50% { opacity: 0.4; stroke-width: 2; }
+ }
+ @keyframes corePulse {
+ 0%, 100% { opacity: 0.2; transform: scale(1); }
+ 50% { opacity: 0.5; transform: scale(1.2); }
+ }
+ .ray {
+ animation: rayPulse 2s infinite ease-in-out;
+ transform-origin: ${width / 2}px ${height / 2}px;
+ }
+ .radiant-core {
+ animation: corePulse 2s infinite ease-in-out;
+ transform-origin: ${width / 2}px ${height / 2}px;
+ }`;
+
+ return { css, svg: `${rays}${core}` };
+ }
+
+ case "circuit": {
+ // Elements traveling around the edges like circuit paths
+ const dotCount = 6;
+ const dots = Array.from({ length: dotCount }, (_, i) => {
+ const delay = i * 0.8;
+ return `
+
+
+ `;
+ }).join("");
+
+ // Glowing trail effect
+ const trail = `
+
+
+
+ `;
+
+ const css = `
+ @keyframes circuitGlow {
+ 0%, 100% { opacity: 0.1; }
+ 50% { opacity: 0.4; }
+ }
+ .circuit-dot {
+ filter: drop-shadow(0 0 2px ${titleColor});
+ }
+ [class^="circuit-glow-"] {
+ animation: circuitGlow 2s infinite ease-in-out;
+ }`;
+
+ return { css, svg: `${trail}${dots}` };
+ }
+
+ case "sparks": {
+ // Electric sparks appearing randomly
+ const sparks = Array.from({ length: 10 }, (_, i) => {
+ const x = 20 + Math.random() * (width - 40);
+ const y = 20 + Math.random() * (height - 40);
+ const delay = i * 0.5;
+ const rotation = Math.random() * 360;
+ return `
+
+
+
+
+
+ `;
+ }).join("");
+
+ const css = `
+ @keyframes sparkFlash {
+ 0%, 90%, 100% { opacity: 0; transform: scale(0); }
+ 5% { opacity: 1; transform: scale(1.2); }
+ 10% { opacity: 0.8; transform: scale(0.9); }
+ 15% { opacity: 0; transform: scale(0.6); }
+ }
+ .spark {
+ animation: sparkFlash 5s infinite ease-in-out;
+ transform-origin: center;
+ }`;
+
+ return { css, svg: `${sparks}` };
+ }
+
+ default:
+ return { css: "", svg: "" };
+ }
+};
+
/**
* Retrieves the repository description and wraps it to fit the card width.
*
@@ -83,6 +521,12 @@ const renderRepoCard = (repo, options = {}) => {
show_prs = false,
show_age = false,
age_metric = "first",
+ animation_style = "none",
+ disable_animations = false,
+ wave_speed = 2,
+ wave_amplitude = 3,
+ wave_delay = 0.05,
+ color_morph = false,
} = options;
const lineHeight = 10;
@@ -269,9 +713,29 @@ const renderRepoCard = (repo, options = {}) => {
colors,
});
- card.disableAnimations();
+ // Get animation styles if enabled
+ const hasAnimation = !disable_animations && animation_style !== "none";
+ const waveParams = {
+ speed: wave_speed,
+ amplitude: wave_amplitude,
+ delay: wave_delay,
+ colorMorph: color_morph,
+ };
+ const animationData = hasAnimation
+ ? getAnimationStyle(animation_style, colors, 400, cardHeight, waveParams)
+ : { css: "", svg: "" };
+
+ // Check if we should add wave effect to text
+ const useBubblesWave = animation_style === "bubbles" && !disable_animations;
+
+ if (disable_animations) {
+ card.disableAnimations();
+ }
card.setHideBorder(hide_border);
+
+ // Only hide title if explicitly requested (not for wave effect)
card.setHideTitle(shouldHideTitle);
+
if (compactStatsOnlyLayout) {
card.paddingX = 25;
}
@@ -281,9 +745,41 @@ const renderRepoCard = (repo, options = {}) => {
.icon { fill: ${colors.iconColor} }
.badge { font: 600 11px 'Segoe UI', Ubuntu, Sans-Serif; }
.badge rect { opacity: 0.2 }
+ .wave-title { font: 600 18px 'Segoe UI', Ubuntu, Sans-Serif; fill: ${colors.titleColor}; }
+ @supports(-moz-appearance: auto) {
+ .wave-title { font-size: 15.5px; }
+ }
+ ${useBubblesWave ? `g[data-testid="card-title"]:not(:has(.wave-title)) { display: none; }` : ""}
+ ${animationData.css}
`);
+ // Create custom wave title if needed
+ const customWaveTitle =
+ useBubblesWave && !shouldHideTitle
+ ? `
+
+
+
+ ${wrapTextInWave(header.length > 35 ? `${header.slice(0, 35)}...` : header, 0, wave_delay, color_morph)}
+
+
+ `
+ : "";
+
return card.render(`
+ ${animationData.svg}
+ ${customWaveTitle}
+
${
isTemplate
? getBadgeSVG(i18n.t("repocard.template"), colors.textColor)
diff --git a/src/cards/stats.js b/src/cards/stats.js
index 6b428d48c34ae..0461dd6c36757 100644
--- a/src/cards/stats.js
+++ b/src/cards/stats.js
@@ -51,6 +51,152 @@ const LONG_LOCALES = [
"zh-tw",
];
+/**
+ * Generates creative rank circle animations.
+ *
+ * @param {string} style Animation style: eye, fire, default.
+ * @param {object} colors Card colors for theming.
+ * @param {string} rankLevel The rank level (A, B, C, etc.).
+ * @returns {{svg: string, css: string}} Animation SVG and CSS.
+ */
+const getRankAnimation = (style, colors, rankLevel) => {
+ const ringColor = colors.ringColor || "4c71f2";
+ const titleColor = colors.titleColor || "2f80ed";
+
+ if (style === "eye") {
+ // Blinking eyeball animation - eyelids that slide together like doors!
+ return {
+ svg: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ css: `
+ @keyframes blinkTop {
+ 0%, 45%, 55%, 100% {
+ transform: translateY(-38px);
+ }
+ 50% {
+ transform: translateY(0);
+ }
+ }
+ @keyframes blinkBottom {
+ 0%, 45%, 55%, 100% {
+ transform: translateY(38px);
+ }
+ 50% {
+ transform: translateY(0);
+ }
+ }
+ @keyframes pupilDilate {
+ 0%, 100% { r: 8px; }
+ 50% { r: 10px; }
+ }
+ .eyelid-top {
+ animation: blinkTop 4s infinite ease-in-out;
+ }
+ .eyelid-bottom {
+ animation: blinkBottom 4s infinite ease-in-out;
+ }
+ .pupil {
+ animation: pupilDilate 3s infinite ease-in-out;
+ }
+ `,
+ };
+ } else if (style === "fire") {
+ // Ring of fire animation!
+ const flames = Array.from({ length: 12 }, (_, i) => {
+ const angle = (i * 360) / 12;
+ const x = -10 + Math.cos((angle * Math.PI) / 180) * 45;
+ const y = 8 + Math.sin((angle * Math.PI) / 180) * 45;
+ const delay = i * 0.1;
+ return `
+
+
+ `;
+ }).join("");
+
+ return {
+ svg: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${flames}
+
+
+
+ ${rankLevel}
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ css: `
+ @keyframes flicker {
+ 0%, 100% {transform: scale(1) translateY(0); opacity: 0.9;}
+ 25% { transform: scale(1.1) translateY(-2px); opacity: 1; }
+ 50% { transform: scale(0.95) translateY(1px); opacity: 0.8; }
+ 75% { transform: scale(1.05) translateY(-1px); opacity: 0.95; }
+ }
+ @keyframes fireGlow {
+ 0%, 100% { opacity: 0.6; }
+ 50% { opacity: 0.9; }
+ }
+ .flame {
+ animation: flicker 1.5s infinite ease-in-out;
+ }
+ .fire-rank-text {
+ animation: fireGlow 2s infinite ease-in-out;
+ }
+ `,
+ };
+ }
+
+ // Default: return empty (will use standard rank circle)
+ return { svg: "", css: "" };
+};
+
/**
* Create a stats card text item.
*
@@ -175,6 +321,7 @@ const getStyles = ({
ringColor,
show_icons,
progress,
+ rankAnimationCss = "",
}) => {
return `
.stat {
@@ -198,7 +345,7 @@ const getStyles = ({
.rank-percentile-text {
font-size: 16px;
}
-
+
.not_bold { font-weight: 400 }
.bold { font-weight: 700 }
.icon {
@@ -224,6 +371,7 @@ const getStyles = ({
animation: rankAnimation 1s forwards ease-in-out;
}
${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })}
+ ${rankAnimationCss}
`;
};
@@ -294,6 +442,7 @@ const renderStatsCard = (stats, options = {}) => {
locale,
disable_animations = false,
rank_icon = "default",
+ rank_animation = "default",
show = [],
} = options;
@@ -451,6 +600,21 @@ const renderStatsCard = (stats, options = {}) => {
// the lower the user's percentile the better
const progress = 100 - rank.percentile;
+
+ // Get rank animation if specified
+ const rankAnimationData =
+ rank_animation === "default"
+ ? { svg: "", css: "" }
+ : getRankAnimation(
+ rank_animation,
+ {
+ titleColor,
+ ringColor,
+ textColor,
+ },
+ rank.level,
+ );
+
const cssStyles = getStyles({
titleColor,
ringColor,
@@ -458,6 +622,7 @@ const renderStatsCard = (stats, options = {}) => {
iconColor,
show_icons,
progress,
+ rankAnimationCss: rankAnimationData.css,
});
const calculateTextWidth = () => {
@@ -553,16 +718,23 @@ const renderStatsCard = (stats, options = {}) => {
// Conditionally rendered elements
const rankCircle = hide_rank
? ""
- : `
-
-
-
- ${rankIcon(rank_icon, rank?.level, rank?.percentile)}
-
- `;
+ : rank_animation === "default"
+ ? `
+
+
+
+ ${rankIcon(rank_icon, rank?.level, rank?.percentile)}
+
+ `
+ : `
+ ${rankAnimationData.svg}
+ `;
// Accessibility Labels
const labels = Object.keys(STATS)
diff --git a/themes/index.js b/themes/index.js
index f5d8d9160fd1b..b3f6f7c02074f 100644
--- a/themes/index.js
+++ b/themes/index.js
@@ -462,6 +462,41 @@ export const themes = {
icon_color: "ffffff",
bg_color: "35,4158d0,c850c0,ffcc70",
},
+ mad_scientist: {
+ title_color: "00d9ff",
+ text_color: "7dd3fc",
+ icon_color: "38bdf8",
+ border_color: "0ea5e9",
+ bg_color: "0c1021",
+ },
+ mad_scientist_dark: {
+ title_color: "22d3ee",
+ text_color: "67e8f9",
+ icon_color: "06b6d4",
+ border_color: "0891b2",
+ bg_color: "020617",
+ },
+ cybernetic_lab: {
+ title_color: "3b82f6",
+ text_color: "60a5fa",
+ icon_color: "2563eb",
+ border_color: "1d4ed8",
+ bg_color: "0a0e1a",
+ },
+ robot_blue: {
+ title_color: "0ea5e9",
+ text_color: "7dd3fc",
+ icon_color: "38bdf8",
+ border_color: "0284c7",
+ bg_color: "082f49",
+ },
+ electric_laboratory: {
+ title_color: "00ffff",
+ text_color: "5eead4",
+ icon_color: "2dd4bf",
+ border_color: "14b8a6",
+ bg_color: "0f172a",
+ },
};
export default themes;