Skip to content

Commit a3be167

Browse files
authored
🎨 style: 絵文字のアニメーションを使った背景の実装 #185
* 🎨 style: 背景グラデーションの実装 #185 * 🎨 style: 絵文字アニメーションの実装 * 🎨 style: ヘッダーの重ね合わせを修正 * 🎨 style: 絵文字の色を半透明に変更 * ♻️ refactor: 細かい表記の修正
1 parent 53dfd23 commit a3be167

File tree

9 files changed

+341
-59
lines changed

9 files changed

+341
-59
lines changed

src/app/page.tsx

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@ export const BASE_PATH = basePath ? basePath : "";
44
import GitHubIcon from "@mui/icons-material/GitHub";
55
import { SITE_NAME } from "../../lib/constants";
66
import KeyboardDoubleArrowDownSharpIcon from "@mui/icons-material/KeyboardDoubleArrowDownSharp";
7+
import GradientBackground from "@/components/gradient-background";
8+
import ScreenEmojis from "@/components/screen-emojis";
9+
710
import contributors from "../../Contributors.json";
11+
import {
12+
groupContributorsBySection,
13+
latestContributorsColor,
14+
} from "@/utils/contributors-grouping";
815

916
export default function Home() {
1017
const contributorsNumber = contributors.length;
18+
const contributorsGroups = groupContributorsBySection(contributors, 3);
1119

1220
return (
1321
<>
@@ -63,65 +71,85 @@ export default function Home() {
6371
</p>
6472
</section>
6573

66-
<section className="bg-red-600 p-6 pb-0 pt-8 md:px-10 md:pt-10">
67-
<div className="rounded-md bg-white px-5 pb-16 pt-12 md:p-20 md:pt-8">
68-
<h2 className="text-center text-xl font-bold tracking-tighter text-red-600 md:pb-4 md:pt-12 md:text-3xl">
69-
簡単 8 STEP でコントリビューション!
70-
</h2>
71-
<ul className="mx-auto mt-8 w-fit list-inside rounded-md px-2 text-lg font-bold md:text-xl md:leading-7 lg:mb-8 lg:mt-10 lg:flex lg:gap-16 lg:bg-stone-100 lg:px-12 lg:pb-4 lg:pt-8">
72-
<div>
73-
<li className="pb-4 md:pb-6">
74-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 1</p>
75-
プロジェクトをフォーク
76-
</li>
77-
<li className="pb-4 md:pb-6">
78-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 2</p>
79-
ローカルマシンへクローン
80-
</li>
81-
<li className="pb-4 md:pb-6">
82-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 3</p>
83-
作業用ブランチを作成
84-
</li>
85-
<li className="pb-4 md:pb-6">
86-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 4</p>
87-
変更を加える
88-
</li>
74+
<GradientBackground mainColor={latestContributorsColor}>
75+
<ScreenEmojis contributors={contributorsGroups[0]} isTopSection />
76+
<section className="mx-auto h-screen max-w-screen-xl">
77+
セクション1
78+
</section>
79+
80+
<ScreenEmojis contributors={contributorsGroups[1]} />
81+
<section className="mx-auto h-screen max-w-screen-xl">
82+
セクション2
83+
</section>
84+
85+
<ScreenEmojis contributors={contributorsGroups[2]} />
86+
<section className="bg-red-600 p-6 pb-0 pt-8 md:px-10 md:pt-10">
87+
<div className="rounded-md bg-white px-5 pb-16 pt-12 md:p-20 md:pt-8">
88+
<h2 className="text-center text-xl font-bold tracking-tighter text-red-600 md:pb-4 md:pt-12 md:text-3xl">
89+
簡単 8 STEP でコントリビューション!
90+
</h2>
91+
<ul className="mx-auto mt-8 w-fit list-inside rounded-md px-2 text-lg font-bold md:text-xl md:leading-7 lg:mb-8 lg:mt-10 lg:flex lg:gap-16 lg:bg-stone-100 lg:px-12 lg:pb-4 lg:pt-8">
92+
<div>
93+
<li className="pb-4 md:pb-6">
94+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 1</p>
95+
プロジェクトをフォーク
96+
</li>
97+
<li className="pb-4 md:pb-6">
98+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 2</p>
99+
ローカルマシンへクローン
100+
</li>
101+
<li className="pb-4 md:pb-6">
102+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 3</p>
103+
作業用ブランチを作成
104+
</li>
105+
<li className="pb-4 md:pb-6">
106+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 4</p>
107+
変更を加える
108+
</li>
109+
</div>
110+
<div>
111+
<li className="pb-4 md:pb-6">
112+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 5</p>
113+
変更をコミット
114+
</li>
115+
<li className="pb-4 md:pb-6">
116+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 6</p>
117+
変更をプッシュ
118+
</li>
119+
<li className="pb-4 md:pb-6">
120+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 7</p>
121+
プルリクエストを作成
122+
</li>
123+
<li className="pb-4 md:pb-6">
124+
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 8</p>
125+
レビューとフィードバックに対応
126+
</li>
127+
</div>
128+
</ul>
129+
<div className="mb-8 text-center font-bold leading-7 md:text-xl md:leading-8">
130+
<KeyboardDoubleArrowDownSharpIcon className="mb-4 text-6xl text-red-600" />
131+
<p>プルリクエストが承認されると</p>
132+
<p>あなたの変更がメインプロジェクトに反映されます 🎉</p>
89133
</div>
90-
<div>
91-
<li className="pb-4 md:pb-6">
92-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 5</p>
93-
変更をコミット
94-
</li>
95-
<li className="pb-4 md:pb-6">
96-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 6</p>
97-
変更をプッシュ
98-
</li>
99-
<li className="pb-4 md:pb-6">
100-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 7</p>
101-
プルリクエストを作成
102-
</li>
103-
<li className="pb-4 md:pb-6">
104-
<p className="mr-6 text-sm text-red-600 md:text-lg">STEP 8</p>
105-
レビューとフィードバックに対応
106-
</li>
134+
<div className="text-center">
135+
<a
136+
href="https://github.com/first-contributions-ja/first-contributions-ja.github.io"
137+
className="rounded-md bg-red-600 px-3 py-3 text-sm text-white transition hover:opacity-70 md:px-5 md:py-4 md:text-lg"
138+
>
139+
<GitHubIcon className="mr-3 -translate-y-0.5 text-3xl md:text-4xl" />
140+
詳しい手順はこちら
141+
</a>
107142
</div>
108-
</ul>
109-
<div className="mb-8 text-center font-bold leading-7 md:text-xl md:leading-8">
110-
<KeyboardDoubleArrowDownSharpIcon className="mb-4 text-6xl text-red-600" />
111-
<p>プルリクエストが承認されると</p>
112-
<p>あなたの変更がメインプロジェクトに反映されます 🎉</p>
113143
</div>
114-
<div className="text-center">
115-
<a
116-
href="https://github.com/first-contributions-ja/first-contributions-ja.github.io"
117-
className="rounded-md bg-red-600 px-3 py-3 text-sm text-white transition hover:opacity-70 md:px-5 md:py-4 md:text-lg"
118-
>
119-
<GitHubIcon className="mr-3 -translate-y-0.5 text-3xl md:text-4xl" />
120-
詳しい手順はこちら
121-
</a>
122-
</div>
123-
</div>
124-
</section>
144+
</section>
145+
</GradientBackground>
146+
{/* tailwind のclassをCSSに含める */}
147+
<div className="hidden animate-[horizontal_3s_ease-in-out_infinite_alternate_both]">
148+
horizontal
149+
</div>
150+
<div className="hidden animate-[vertical_3s_ease-in-out_infinite_alternate_both]">
151+
vertical
152+
</div>
125153
</>
126154
);
127155
}

src/components/animated-emoji.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
emojiToUnicodeHex,
3+
getAnimationStyles,
4+
getEmojiSize,
5+
} from "@/utils/animated-emoji";
6+
import contributors from "../../Contributors.json";
7+
import { latestContributorsColor } from "@/utils/contributors-grouping";
8+
import { hexToRgb } from "@/utils/background-color";
9+
10+
type AnimatedEmojiProps = {
11+
index: number;
12+
contributor: (typeof contributors)[number];
13+
size?: "small" | "medium" | "large";
14+
isTopSection?: boolean;
15+
};
16+
17+
const AnimatedEmoji: React.FC<AnimatedEmojiProps> = ({
18+
index,
19+
contributor,
20+
isTopSection = false,
21+
}) => {
22+
const emojiSize = getEmojiSize(
23+
isTopSection && index === 0 ? "large" : "medium",
24+
);
25+
const speed = Math.floor(Math.random() * 65 + 35) / 10;
26+
const styles = getAnimationStyles(index, speed);
27+
const notoEmoji = emojiToUnicodeHex(contributor.favoriteEmoji);
28+
const emojiColor = hexToRgb(latestContributorsColor, 0.5);
29+
30+
return (
31+
<>
32+
<div
33+
className="group absolute flex h-20 w-20 cursor-pointer items-center justify-center rounded-full bg-transparent [perspective:1000px]"
34+
style={styles as React.CSSProperties}
35+
>
36+
<div className="relative h-full w-full rounded-full duration-1000 [transform-style:preserve-3d] group-hover:[transform:rotateY(180deg)]">
37+
<div className="absolute h-full w-full rounded-full [backface-visibility:hidden]">
38+
<div
39+
className={`flex h-full w-full flex-col items-center justify-center gap-0.5 ${emojiSize}`}
40+
style={{ color: emojiColor }}
41+
dangerouslySetInnerHTML={{ __html: notoEmoji }}
42+
></div>
43+
</div>
44+
<div
45+
style={{ borderColor: contributor.favoriteColor }}
46+
className="absolute h-full w-full rounded-full border-4 shadow-xl [backface-visibility:hidden] [transform:rotateY(180deg)]"
47+
>
48+
<img
49+
src={`${contributor.github}.png`}
50+
loading="lazy"
51+
alt="contributor's icon"
52+
className="h-full w-full rounded-full object-cover object-top"
53+
/>
54+
</div>
55+
</div>
56+
</div>
57+
</>
58+
);
59+
};
60+
61+
export default AnimatedEmoji;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getGradientStyles } from "@/utils/background-color";
2+
import { ReactNode } from "react";
3+
4+
type GradientBackgroundProps = {
5+
mainColor: string;
6+
children: ReactNode;
7+
};
8+
9+
const GradientBackground: React.FC<GradientBackgroundProps> = ({
10+
mainColor,
11+
children,
12+
}) => {
13+
const gradientStyles = getGradientStyles(mainColor);
14+
15+
return (
16+
<main
17+
className="bg-gradient-to-b from-[var(--background-start-rgb)] via-[var(--background-middle-rgb)] to-[var(--background-end-rgb)]"
18+
style={gradientStyles as React.CSSProperties}
19+
>
20+
<div className=" inset-0 top-0">
21+
<svg
22+
preserveAspectRatio="none"
23+
viewBox="0 0 1200 120"
24+
xmlns="http://www.w3.org/2000/svg"
25+
style={{
26+
fill: "#ffffff",
27+
width: "100%",
28+
height: 137,
29+
transform: "scaleX(-1)",
30+
}}
31+
>
32+
<path
33+
d="M0 0v46.29c47.79 22.2 103.59 32.17 158 28 70.36-5.37 136.33-33.31 206.8-37.5 73.84-4.36 147.54 16.88 218.2 35.26 69.27 18 138.3 24.88 209.4 13.08 36.15-6 69.85-17.84 104.45-29.34C989.49 25 1113-14.29 1200 52.47V0z"
34+
opacity=".25"
35+
/>
36+
<path
37+
d="M0 0v15.81c13 21.11 27.64 41.05 47.69 56.24C99.41 111.27 165 111 224.58 91.58c31.15-10.15 60.09-26.07 89.67-39.8 40.92-19 84.73-46 130.83-49.67 36.26-2.85 70.9 9.42 98.6 31.56 31.77 25.39 62.32 62 103.63 73 40.44 10.79 81.35-6.69 119.13-24.28s75.16-39 116.92-43.05c59.73-5.85 113.28 22.88 168.9 38.84 30.2 8.66 59 6.17 87.09-7.5 22.43-10.89 48-26.93 60.65-49.24V0z"
38+
opacity=".5"
39+
/>
40+
<path d="M0 0v5.63C149.93 59 314.09 71.32 475.83 42.57c43-7.64 84.23-20.12 127.61-26.46 59-8.63 112.48 12.24 165.56 35.4C827.93 77.22 886 95.24 951.2 90c86.53-7 172.46-45.71 248.8-84.81V0z" />
41+
</svg>
42+
</div>
43+
{children}
44+
</main>
45+
);
46+
};
47+
48+
export default GradientBackground;

src/components/header.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { BASE_PATH } from "../app/page";
55

66
export default function Header() {
77
return (
8-
<header className="fixed left-0 top-0 w-full py-3 backdrop-blur-sm lg:py-4">
8+
<header className="fixed left-0 top-0 z-10 w-full py-3 backdrop-blur-sm lg:py-4">
99
<Link href="/">
10-
<h1 className="ml-3 inline text-lg font-bold text-red-600 transition hover:opacity-70 lg:ml-5">
10+
<span className="ml-3 inline text-lg font-bold text-red-600 transition hover:opacity-70 lg:ml-5">
1111
<Image
1212
src={`${BASE_PATH}/logo.svg`}
1313
width={640}
@@ -16,7 +16,7 @@ export default function Header() {
1616
className="mr-2 inline-block w-[40px] lg:mr-4"
1717
/>
1818
{SITE_NAME}
19-
</h1>
19+
</span>
2020
</Link>
2121
</header>
2222
);

src/components/screen-emojis.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import AnimatedEmoji from "./animated-emoji";
2+
import contributors from "../../Contributors.json";
3+
import { Noto_Emoji } from "next/font/google";
4+
5+
const emoji = Noto_Emoji({
6+
subsets: ["emoji"],
7+
weight: "300",
8+
display: "swap",
9+
});
10+
11+
type ScreenEmojisProps = {
12+
contributors: typeof contributors;
13+
isTopSection?: boolean;
14+
};
15+
16+
const ScreenEmojis: React.FC<ScreenEmojisProps> = ({
17+
contributors,
18+
isTopSection,
19+
}) => {
20+
return (
21+
<>
22+
<div className={`${emoji.className} relative`}>
23+
{contributors.map((contributor, index) => (
24+
<AnimatedEmoji
25+
key={index}
26+
index={index}
27+
contributor={contributor}
28+
isTopSection={isTopSection}
29+
/>
30+
))}
31+
</div>
32+
</>
33+
);
34+
};
35+
36+
export default ScreenEmojis;

src/utils/animated-emoji.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const getEmojiSize = (type: "small" | "medium" | "large") => {
2+
switch (type) {
3+
case "small":
4+
return "text-xl";
5+
case "medium":
6+
return "text-7xl";
7+
case "large":
8+
return "text-9xl";
9+
}
10+
};
11+
12+
const getAnimationDirection = (index: number) =>
13+
(index + 1) % 2 === 0 ? "vertical" : "horizontal";
14+
const getAnimationAlternate = (index: number) =>
15+
index < 5 ? "alternate-reverse" : "alternate";
16+
17+
const getAnimationStyles = (index: number, speed: number) => {
18+
const animationDirection = getAnimationDirection(index);
19+
const isHorizontal = animationDirection === "horizontal";
20+
const alternate = getAnimationAlternate(index);
21+
const left = isHorizontal ? 0 : `calc(100vw / 10 * ${index})`;
22+
const top = isHorizontal ? `calc(80vh / 10 * ${index})` : 0;
23+
24+
return {
25+
animation: `${animationDirection} ${speed}s ease-in-out infinite ${alternate} both`,
26+
left,
27+
top,
28+
};
29+
};
30+
31+
const emojiToUnicodeHex = (emoji: string) => {
32+
const codePoint = emoji.codePointAt(0);
33+
if (codePoint === undefined) {
34+
throw new Error("Invalid emoji input");
35+
}
36+
return `&#x${codePoint.toString(16).toUpperCase()};&#xfe0f;`;
37+
};
38+
39+
export { getEmojiSize, getAnimationStyles, emojiToUnicodeHex };

src/utils/background-color.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const hexToRgb = (color: string, transparency = 1) => {
2+
const rgbValues = Object.fromEntries(
3+
(
4+
(color.match(/^#?[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/) ? color : "000")
5+
.replace(/^#?(.*)$/, (_, hex) =>
6+
hex.length == 3 ? hex.replace(/./g, "$&$&") : hex,
7+
)
8+
.match(/../g) ?? []
9+
).map((c, i) => ["rgb".charAt(i), parseInt("0x" + c)]),
10+
);
11+
12+
return `rgb(${rgbValues.r} ${rgbValues.g} ${rgbValues.b} / ${transparency})`;
13+
};
14+
15+
export const getGradientStyles = (color: string) => {
16+
return {
17+
"--background-start-rgb": hexToRgb(color, 0.05),
18+
"--background-middle-rgb": hexToRgb(color, 0.4),
19+
"--background-end-rgb": hexToRgb(color),
20+
};
21+
};

0 commit comments

Comments
 (0)