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 +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&animation_style=bubbles) +``` + +### With Cybernetic Theme +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=mad_scientist&animation_style=circuit) +``` + +### Full Customization +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=robot_blue&animation_style=sparks&show_owner=true&all_stats=true) +``` + +### Custom Wave Effect (Fast & Big) +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=mad_scientist&animation_style=bubbles&wave_speed=1&wave_amplitude=6&wave_delay=0.03) +``` + +### Color Morphing Wave +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=electric_laboratory&animation_style=bubbles&color_morph=true) +``` + +### Slow Gentle Wave +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=robot_blue&animation_style=bubbles&wave_speed=3&wave_amplitude=2&wave_delay=0.08) +``` + +### Disable Animations (for static images) +```markdown +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=electric_laboratory&disable_animations=true) +``` + +--- + +## ๐ŸŽจ 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 +[![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&theme=mad_scientist&animation_style=circuit)](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 +![Repo Card](https://your-domain.vercel.app/api/pin?username=hesreallyhim&repo=your-repo&animation_style=bubbles&title_color=ff00ff&icon_color=00ffff&bg_color=000000) +``` + +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 + ? ` + + + ${icons.contribs} + + + ${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;