M+ ... Global Users" from the homepage stats section.
+let usersM = 10; // fallback matching the current homepage
+if (homepage) {
+ const m = homepage.match(/(\d+(?:\.\d+)?)M\+<\/p>[^<]*]*>Global Users/);
+ if (m) usersM = Number(m[1]);
+}
+
let tokensT = 100; // conservative fallback
if (rankings?.data?.length) {
// The response contains multiple date buckets; only the latest one is
@@ -43,9 +72,10 @@ if (rankings?.data?.length) {
await mkdir('out', {recursive: true});
const props = {
- models: {value: modelCount, suffix: '+', label: 'Models'},
- providers: {value: providerCount, suffix: '+', label: 'Providers'},
+ models: {value: nice(modelCount), suffix: '+', label: 'Models'},
+ providers: {value: nice(providerCount), suffix: '+', label: 'Providers'},
tokens: {value: tokensT, suffix: 'T', label: 'Tokens / month'},
+ users: {value: usersM, suffix: 'M+', label: 'Global users'},
};
for (const [id, p] of Object.entries(props)) {
diff --git a/stats-cards/scripts/render-all.mjs b/stats-cards/scripts/render-all.mjs
index de5aa32..c3bb4fa 100644
--- a/stats-cards/scripts/render-all.mjs
+++ b/stats-cards/scripts/render-all.mjs
@@ -1,7 +1,21 @@
// Renders all stat cards (dark + light) as transparent, play-once GIFs.
import {execSync} from 'node:child_process';
-const stats = ['models', 'providers', 'tokens'];
+const hasGifsicle = (() => {
+ try {
+ execSync('gifsicle --version', {stdio: 'ignore'});
+ return true;
+ } catch {
+ return false;
+ }
+})();
+if (!hasGifsicle) {
+ throw new Error(
+ 'gifsicle is required (2x renders exceed the ~10MB GitHub camo limit without it). brew/apt install gifsicle.'
+ );
+}
+
+const stats = ['models', 'providers', 'tokens', 'users'];
const themes = ['dark', 'light'];
for (const stat of stats) {
@@ -13,11 +27,26 @@ for (const stat of stats) {
id,
out,
'--codec=gif',
+ // Supersample 2x (1760x712): GIF alpha is 1-bit, so the anti-aliased
+ // corner pixels that pass the 50% threshold form a light 1px ring.
+ // At the README's 202px display width, doubling the source resolution
+ // halves that ring's contribution to each screen pixel.
+ '--scale=2',
'--number-of-gif-loops=0',
'--image-format=png',
`--props=out/props-${stat}.json`,
].join(' ');
console.log(`\n▶ ${id}`);
execSync(cmd, {stdio: 'inherit'});
+ // High-quality Lanczos downscale to exactly 2x the README's 208px
+ // display width. Browsers downscale with cheap bilinear filtering
+ // (~4 samples per output pixel), so shipping the raw 1760px render
+ // aliased the text edges at 8.5x; a clean 2:1 step for the browser
+ // keeps glyph edges smooth. Also recompresses ~15MB -> well under
+ // GitHub's ~10MB camo proxy limit.
+ execSync(
+ `gifsicle -O3 --lossy=60 --resize-width 416 --resize-method lanczos3 --resize-colors 255 ${out} -o ${out}`,
+ {stdio: 'inherit'}
+ );
}
}
diff --git a/stats-cards/src/Root.tsx b/stats-cards/src/Root.tsx
index 0307443..0ed3f76 100644
--- a/stats-cards/src/Root.tsx
+++ b/stats-cards/src/Root.tsx
@@ -32,12 +32,19 @@ const STATS = [
label: 'Tokens / month',
sub: 'routed across the network',
},
+ {
+ id: 'users',
+ value: 10,
+ suffix: 'M+',
+ label: 'Global users',
+ sub: 'developers building on OpenRouter',
+ },
] as const;
export const Root: React.FC = () => {
return (
<>
- {STATS.flatMap((stat) =>
+ {STATS.flatMap((stat, index) =>
(['dark', 'light'] as const).map((theme) => (
{
accent: ACCENT,
accent2: ACCENT2,
theme,
+ variant: index,
}}
/>
))
diff --git a/stats-cards/src/StatCard.tsx b/stats-cards/src/StatCard.tsx
index 074954a..7c618ed 100644
--- a/stats-cards/src/StatCard.tsx
+++ b/stats-cards/src/StatCard.tsx
@@ -19,13 +19,20 @@ export const statCardSchema = z.object({
accent: z.string(),
accent2: z.string(),
theme: z.enum(['dark', 'light']),
+ // Position of this card in the row (0..n-1). Used to vary the background
+ // gradient/glow and phase-shift the loop so the four cards flow into each
+ // other side-by-side (and stacked) instead of looking identical.
+ variant: z.number().default(0),
});
type Props = z.infer;
const THEMES = {
dark: {
- surface: 'linear-gradient(145deg, #101114 0%, #0A0B0D 55%, #0E0F13 100%)',
+ // Flat base: the animated gradient wave exits offscreen before the
+ // play-once GIF holds its final frame, ending on this solid color.
+ base: '#0C0D10',
+ wave: 'rgba(99, 102, 241, 0.10)',
border: '#3B3D42',
text: '#DDE0E2',
sub: '#8B8D98',
@@ -33,8 +40,12 @@ const THEMES = {
shineBase: '#DDE0E2',
},
light: {
- surface: 'linear-gradient(145deg, #FFFFFF 0%, #FAFAFB 55%, #F4F4F6 100%)',
- border: '#E4E4E7',
+ base: '#FAFAFB',
+ wave: 'rgba(99, 102, 241, 0.08)',
+ // Deliberately darker than the site's zinc-200: the GIF corner
+ // stair-step pixels take this color, and near-white reads as white
+ // fringing on dark page backgrounds.
+ border: '#B8B8C0',
text: '#09090B',
sub: '#52525B',
dot: '#09090B14',
@@ -64,11 +75,21 @@ export const StatCard: React.FC = ({
accent,
accent2,
theme,
+ variant = 0,
}) => {
const frame = useCurrentFrame();
const {fps, durationInFrames, width, height} = useVideoConfig();
const t = THEMES[theme];
+ // 0..1 across the row of four cards; drives the background variation.
+ const flow = variant / 3;
+ // Gradient tilts progressively left-to-right so adjacent edges continue
+ // into each other; also reads as a diagonal cascade when cards wrap 2x2.
+ const surfaceAngle = 115 + flow * 60; // 115 / 135 / 155 / 175
+ // Phase-shift the looping effects so the row animates as a wave.
+ const phase = (variant * durationInFrames) / 4;
+ const loopFrame = (frame + phase) % durationInFrames;
+
// --- Entrance ---
const enter = spring({frame, fps, config: {damping: 16, stiffness: 120}});
const rise = interpolate(enter, [0, 1], [24, 0]);
@@ -83,16 +104,41 @@ export const StatCard: React.FC = ({
const displayed = formatValue(countSpring * value, value);
// Theme loop easing: cubic-bezier(0.4, 0, 0.6, 1)
- const loop = pingPong(frame, durationInFrames);
+ const loop = pingPong(loopFrame, durationInFrames);
const glow = interpolate(loop, [0, 1], [0.35, 0.75], {
easing: Easing.bezier(0.4, 0, 0.6, 1),
});
// Conic border sweep (full rotation per loop = seamless)
- const sweep = (frame / durationInFrames) * 360;
+ const sweep = (loopFrame / durationInFrames) * 360;
- // Beam of light traveling across the top edge
- const beamX = interpolate(frame % durationInFrames, [0, durationInFrames], [-30, 130]);
+ // Beam of light traveling across the top edge. Driven by `frame` (not the
+ // phase-shifted loop) so it has fully exited when the GIF holds its last
+ // frame; the per-variant delay keeps the row animating as a wave.
+ const beamX = interpolate(
+ frame,
+ [variant * 2, durationInFrames * 0.75 + variant * 2],
+ [-30, 130],
+ {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}
+ );
+
+ // Gradient wave traveling across the surface: starts fully offscreen left,
+ // exits fully offscreen right, so the held final frame is the flat base.
+ // Staggered per variant so it flows through the row of cards.
+ const waveX = interpolate(
+ frame,
+ [variant * 3, durationInFrames * 0.78 + variant * 3],
+ [-140, 110],
+ {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}
+ );
+
+ // Ambient glows fade out completely before the final held frame.
+ const exit = interpolate(
+ frame,
+ [durationInFrames * 0.65, durationInFrames * 0.9],
+ [0, 1],
+ {extrapolateLeft: 'clamp', extrapolateRight: 'clamp'}
+ );
// Shine sweeping across the number. Completes early and clamps at a
// position where the accent stripe (and its repeat tile) sit fully
@@ -108,31 +154,46 @@ export const StatCard: React.FC = ({
// Transparent canvas: GIF alpha is 1-bit, so no exterior glows/shadows.
{/* Card with animated gradient border */}
+ {/* Large radius: GIF alpha is 1-bit, so anti-aliased corner pixels get
+ thresholded to opaque. With a big radius the stair-step follows the
+ curve; with a small one it filled the corners in as solid wedges. */}
+ {/* Gradient wave: sweeps through once, then fully offscreen so the
+ final frame ends on the flat base color */}
+
{/* Ambient glow, clipped inside the card */}
@@ -141,10 +202,10 @@ export const StatCard: React.FC
= ({
style={{
width: width * 0.8,
height: height * 1.2,
- right: -width * 0.2,
+ right: -width * 0.35 + (1 - flow) * width * 0.7,
bottom: -height * 0.45,
background: `radial-gradient(circle, ${accent2}${theme === 'dark' ? '14' : '0c'} 0%, transparent 70%)`,
- opacity: 1 - glow * 0.6,
+ opacity: (1 - glow * 0.6) * (1 - exit),
filter: 'blur(20px)',
}}
/>
@@ -155,8 +216,7 @@ export const StatCard: React.FC = ({
style={{
backgroundImage: `radial-gradient(circle, ${t.dot} 1px, transparent 1px)`,
backgroundSize: '22px 22px',
- maskImage:
- 'radial-gradient(ellipse at 30% 40%, black 20%, transparent 75%)',
+ maskImage: `radial-gradient(ellipse at ${20 + flow * 40}% 40%, black 20%, transparent 75%)`,
}}
/>