refactor(placeholder): extract tile sprite strip

pull/5954/head
boojack 2 weeks ago
parent 411ba7b34c
commit 439ddaebf5

@ -0,0 +1,67 @@
import { cn } from "@/lib/utils";
import type { TileSprite } from "./tileSprites";
interface TileSpriteStripProps {
sprite: TileSprite;
scale?: number;
className?: string;
testId?: string;
}
const DEFAULT_SCALE = 2;
const getAnimationName = (sprite: TileSprite, scale: number) => `tile-sprite-${sprite.name}-${scale}x`;
const TileSpriteStrip = ({ sprite, scale = DEFAULT_SCALE, className, testId }: TileSpriteStripProps) => {
const stripWidth = sprite.frameWidth * sprite.frames;
const displayStripWidth = stripWidth * scale;
const displayStripHeight = sprite.frameHeight * scale;
const displayFrameWidth = sprite.frameWidth * scale;
const animationName = getAnimationName(sprite, scale);
return (
<>
<style>{`
@keyframes ${animationName} {
from { transform: translateX(0); }
to { transform: translateX(-${displayStripWidth}px); }
}
@media (prefers-reduced-motion: reduce) {
[data-tile-sprite-strip="${animationName}"] {
animation: none !important;
transform: translateX(0) !important;
}
}
`}</style>
<div
aria-hidden="true"
data-testid={testId}
className={cn("relative shrink-0 overflow-hidden", className)}
style={{ width: displayFrameWidth, height: displayStripHeight, overflow: "hidden" }}
>
<img
data-tile-sprite-strip={animationName}
src={sprite.src}
alt=""
width={stripWidth}
height={sprite.frameHeight}
draggable={false}
style={{
display: "block",
width: displayStripWidth,
height: displayStripHeight,
maxWidth: "none",
imageRendering: "pixelated",
animationName,
animationDuration: `${sprite.duration}ms`,
animationTimingFunction: `steps(${sprite.frames})`,
animationIterationCount: "infinite",
}}
/>
</div>
</>
);
};
export default TileSpriteStrip;

@ -1,6 +1,7 @@
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DEFAULT_MESSAGES, type PlaceholderVariant } from "./messages"; import { DEFAULT_MESSAGES, type PlaceholderVariant } from "./messages";
import TileSpriteStrip from "./TileSpriteStrip";
import { pickTileSprite } from "./tileSprites"; import { pickTileSprite } from "./tileSprites";
interface PlaceholderProps { interface PlaceholderProps {
@ -10,17 +11,12 @@ interface PlaceholderProps {
className?: string; className?: string;
} }
const TILE_SIZE = 32;
const DISPLAY_SCALE = 2; const DISPLAY_SCALE = 2;
const DISPLAY_SIZE = TILE_SIZE * DISPLAY_SCALE;
const Placeholder = ({ variant, message, children, className }: PlaceholderProps) => { const Placeholder = ({ variant, message, children, className }: PlaceholderProps) => {
const [sprite] = useState(pickTileSprite); const [sprite] = useState(pickTileSprite);
const resolvedMessage = message ?? DEFAULT_MESSAGES[variant]; const resolvedMessage = message ?? DEFAULT_MESSAGES[variant];
const isLoading = variant === "loading"; const isLoading = variant === "loading";
const stripWidth = sprite.frameWidth * sprite.frames;
const displayStripWidth = stripWidth * DISPLAY_SCALE;
const displayStripHeight = sprite.frameHeight * DISPLAY_SCALE;
return ( return (
<div <div
@ -28,45 +24,7 @@ const Placeholder = ({ variant, message, children, className }: PlaceholderProps
aria-live={isLoading ? "polite" : undefined} aria-live={isLoading ? "polite" : undefined}
className={cn("flex flex-col items-center justify-center max-w-md mx-auto px-4 py-8", className)} className={cn("flex flex-col items-center justify-center max-w-md mx-auto px-4 py-8", className)}
> >
<style>{` <TileSpriteStrip sprite={sprite} scale={DISPLAY_SCALE} className="relative shrink-0" testId="placeholder-sprite" />
@keyframes placeholder-tile-strip {
from { transform: translateX(0); }
to { transform: translateX(-${displayStripWidth}px); }
}
@media (prefers-reduced-motion: reduce) {
[data-placeholder-strip] {
animation: none !important;
transform: translateX(0) !important;
}
}
`}</style>
<div
aria-hidden="true"
data-testid="placeholder-sprite"
className="relative shrink-0"
style={{ width: DISPLAY_SIZE, height: DISPLAY_SIZE, overflow: "hidden" }}
>
<img
data-placeholder-strip=""
src={sprite.src}
alt=""
width={stripWidth}
height={sprite.frameHeight}
draggable={false}
style={{
display: "block",
width: displayStripWidth,
height: displayStripHeight,
maxWidth: "none",
imageRendering: "pixelated",
animationName: "placeholder-tile-strip",
animationDuration: `${sprite.duration}ms`,
animationTimingFunction: `steps(${sprite.frames})`,
animationIterationCount: "infinite",
}}
/>
</div>
<p className="mt-3 font-mono text-sm text-muted-foreground">{resolvedMessage}</p> <p className="mt-3 font-mono text-sm text-muted-foreground">{resolvedMessage}</p>
{children && <div className="mt-4">{children}</div>} {children && <div className="mt-4">{children}</div>}
</div> </div>

@ -1,4 +1,5 @@
import { ExternalLinkIcon } from "lucide-react"; import { ExternalLinkIcon } from "lucide-react";
import TileSpriteStrip from "@/components/Placeholder/TileSpriteStrip";
import { TILE_SPRITES, type TileSprite } from "@/components/Placeholder/tileSprites"; import { TILE_SPRITES, type TileSprite } from "@/components/Placeholder/tileSprites";
import SettingGroup from "@/components/Settings/SettingGroup"; import SettingGroup from "@/components/Settings/SettingGroup";
import SettingSection from "@/components/Settings/SettingSection"; import SettingSection from "@/components/Settings/SettingSection";
@ -14,82 +15,12 @@ const PRODUCT_LINKS = [
const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."]; const PRODUCT_POINTS = ["Open. Write. Done.", "Markdown-native.", "Fully yours."];
const BIRD_META: Record<TileSprite["name"], { label: string; description: string }> = {
OwlBlink: {
label: "Owl",
description: "Night watch idle with a compact blink.",
},
EagleIdle: {
label: "Eagle",
description: "Perched idle with a sharp head and steady chest motion.",
},
ToucanIdle: {
label: "Toucan",
description: "Calm tropical idle built around a large curved beak.",
},
};
const getBirdAnimationName = (sprite: TileSprite) => `about-bird-${sprite.name}`;
const BIRD_KEYFRAMES_CSS = `
${TILE_SPRITES.map(
(sprite) => `
@keyframes ${getBirdAnimationName(sprite)} {
from { transform: translateX(0); }
to { transform: translateX(-${sprite.frameWidth * sprite.frames * SPRITE_SCALE}px); }
}
`,
).join("\n")}
@media (prefers-reduced-motion: reduce) {
.about-bird-strip {
animation: none !important;
transform: translateX(0) !important;
}
}
`;
const BirdSprite = ({ sprite }: { sprite: TileSprite }) => { const BirdSprite = ({ sprite }: { sprite: TileSprite }) => {
const stripWidth = sprite.frameWidth * sprite.frames;
const displayWidth = stripWidth * SPRITE_SCALE;
const displayHeight = sprite.frameHeight * SPRITE_SCALE;
const frameDisplayWidth = displayWidth / sprite.frames;
const meta = BIRD_META[sprite.name];
return ( return (
<figure className="flex min-w-0 flex-1 basis-36 flex-col items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-4 text-center"> <figure className="flex w-auto min-w-28 flex-none flex-col items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-4 text-center">
<div <TileSpriteStrip sprite={sprite} scale={SPRITE_SCALE} className="size-16" testId="about-bird-sprite" />
aria-hidden="true"
data-testid="about-bird-sprite"
className="relative size-16 shrink-0 overflow-hidden"
style={{ width: frameDisplayWidth, height: displayHeight }}
>
<img
className="about-bird-strip"
src={sprite.src}
alt=""
width={stripWidth}
height={sprite.frameHeight}
draggable={false}
style={{
display: "block",
width: displayWidth,
height: displayHeight,
maxWidth: "none",
imageRendering: "pixelated",
animationName: getBirdAnimationName(sprite),
animationDuration: `${sprite.duration}ms`,
animationTimingFunction: `steps(${sprite.frames})`,
animationIterationCount: "infinite",
}}
/>
</div>
<figcaption className="min-w-0"> <figcaption className="min-w-0">
<div className="flex flex-col items-center gap-1"> <h3 className="font-mono text-sm text-foreground">{sprite.name}</h3>
<h3 className="font-mono text-sm font-semibold text-foreground">{sprite.name}</h3>
<span className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">{meta.label}</span>
</div>
<p className="mt-2 text-xs leading-5 text-muted-foreground">{meta.description}</p>
</figcaption> </figcaption>
</figure> </figure>
); );
@ -98,8 +29,6 @@ const BirdSprite = ({ sprite }: { sprite: TileSprite }) => {
const About = () => { const About = () => {
return ( return (
<section className="mx-auto w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8"> <section className="mx-auto w-full max-w-5xl min-h-full flex flex-col justify-start items-start sm:pt-3 md:pt-6 pb-8">
<style>{BIRD_KEYFRAMES_CSS}</style>
<div className="w-full px-4 sm:px-6"> <div className="w-full px-4 sm:px-6">
<div className="w-full rounded-xl border border-border bg-background px-4 py-4 text-muted-foreground"> <div className="w-full rounded-xl border border-border bg-background px-4 py-4 text-muted-foreground">
<SettingSection <SettingSection

Loading…
Cancel
Save