Skip to content

Commit 8febaf1

Browse files
Merge pull request #618 from sheleoni/shelly/bugchan_logo
2 parents 842e3d9 + ab7921f commit 8febaf1

File tree

5 files changed

+135
-9
lines changed

5 files changed

+135
-9
lines changed
16.7 KB
Loading
468 KB
Loading
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import { useState, useEffect } from "react";
5+
import staticLogoSrc from "@/../public/logo/buggie_bugchan_logo.png";
6+
import spriteSrc from "@/../public/logo/jsconf_bugchan_v5_minified.png";
7+
8+
type Props = {
9+
width: number;
10+
height: number;
11+
alt: string;
12+
fetchPriority?: "high" | "low" | "auto";
13+
};
14+
15+
const FRAME_COUNT = 29;
16+
const SPRITE_WIDTH = 400;
17+
const SPRITE_HEIGHT = 11600;
18+
const ANIMATION_DURATION = 1500;
19+
20+
export function AnimatedLogo({
21+
width,
22+
height,
23+
alt,
24+
fetchPriority,
25+
}: Props) {
26+
const [isAnimating, setIsAnimating] = useState(false);
27+
const [isSpriteLoaded, setIsSpriteLoaded] = useState(false);
28+
const [clickCount, setClickCount] = useState(0);
29+
30+
// detect when sprite sheet has fully loaded
31+
useEffect(() => {
32+
const img = new window.Image();
33+
img.onload = () => {
34+
setIsSpriteLoaded(true);
35+
};
36+
img.src = spriteSrc.src;
37+
}, []);
38+
39+
const handleClick = () => {
40+
if (isAnimating || !isSpriteLoaded) return;
41+
42+
setClickCount(clickCount + 1);
43+
setIsAnimating(true);
44+
setTimeout(() => {
45+
setIsAnimating(false);
46+
}, ANIMATION_DURATION);
47+
};
48+
49+
const handleKeyDown = (e: React.KeyboardEvent) => {
50+
// when focused, allow to animate the logo by pressing Enter or Space key
51+
if (e.key === "Enter" || e.key === " ") {
52+
e.preventDefault();
53+
handleClick();
54+
}
55+
};
56+
57+
const isReverse = clickCount % 3 === 0;
58+
59+
60+
return (
61+
<div
62+
className="logo-container"
63+
onClick={handleClick}
64+
onKeyDown={handleKeyDown}
65+
role="button"
66+
tabIndex={0}
67+
aria-label={`${alt}. Click to animate.`}
68+
aria-busy={!isSpriteLoaded}
69+
style={{
70+
width: `${width}px`,
71+
height: `${height}px`,
72+
flexShrink: 0,
73+
cursor: isSpriteLoaded ? "pointer" : "default",
74+
}}
75+
>
76+
{/* show static logo only if sprite hasn't loaded yet */}
77+
{!isSpriteLoaded && (
78+
<Image
79+
src={staticLogoSrc}
80+
fetchPriority={fetchPriority}
81+
alt={alt}
82+
width={width}
83+
height={height}
84+
style={{
85+
width: `${width}px`,
86+
height: `${height}px`,
87+
objectFit: "contain",
88+
}}
89+
/>
90+
)}
91+
92+
{/* after sprite is loaded, show sprite at frame 0 (idle) / animating */}
93+
{isSpriteLoaded && (
94+
<div
95+
role="img"
96+
aria-label={alt}
97+
style={{
98+
width: `${width}px`,
99+
height: `${height}px`,
100+
backgroundImage: `url(${spriteSrc.src})`,
101+
backgroundSize: `${width}px ${(SPRITE_HEIGHT / SPRITE_WIDTH) * width}px`,
102+
animation: isAnimating
103+
? `sprite-animate ${ANIMATION_DURATION}ms steps(${FRAME_COUNT - 1}) 1 ${isReverse ? "reverse" : "normal"}`
104+
: "none",
105+
}}
106+
/>
107+
)}
108+
109+
<style jsx>{`
110+
.logo-container {
111+
transition: transform 0.2s ease-out;
112+
}
113+
114+
.logo-container:hover {
115+
transform: scale(1.05);
116+
}
117+
118+
@keyframes sprite-animate {
119+
from {
120+
background-position: 0 0;
121+
}
122+
to {
123+
background-position: 0 -${((FRAME_COUNT - 1) * height)}px;
124+
}
125+
}
126+
`}</style>
127+
</div>
128+
);
129+
}
130+

2025/src/components/Hero.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import Image from "next/image";
21
import { useTranslations } from "next-intl";
3-
import logoSrc from "@/../public/logo.svg";
2+
import { AnimatedLogo } from "./AnimatedLogo";
43

54
export function Hero() {
65
const t = useTranslations("about");
76

87
return (
98
<div className="flex flex-col md:flex-row items-center justify-center gap-4 md:gap-16">
10-
<Image
11-
src={logoSrc}
12-
fetchPriority="high"
13-
alt={t("title")}
9+
<AnimatedLogo
1410
width={240}
1511
height={240}
16-
// この指定がないとレイアウトがガタつく
17-
style={{ width: "240px", height: "240px" }}
12+
fetchPriority="high"
13+
alt={t("title")}
1814
/>
1915
<div>
2016
<h1 className="text-5xl md:text-6xl font-bold">{t("title")}</h1>

2025/src/components/og/Logo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type Props = {
77
size: number;
88
};
99

10-
const LOGO_URL = join(process.cwd(), "public", "logo.svg");
10+
const LOGO_URL = join(process.cwd(), "public", "logo", "buggie_bugchan_logo.png");
1111

1212
export function Logo({ size }: Props) {
1313
return <img alt="" src={makeDataUrl(LOGO_URL)} width={size} height={size} />;

0 commit comments

Comments
 (0)