紙吹雪をふらせる React のコンポーネントを作成する
紙吹雪をふらせる React のコンポーネントを作成してみます。作成物は以下になります。
準備
上記をテンプレートとして使用します。今回 tailwindcss は使用せず、また Next.js ですが styled-jsx を使用せず CSS Modules のみで作成していきます。
紙吹雪の紙のコンポーネントを作成する
紙吹雪を回転させる
Piece.tsx
という紙吹雪の紙部分のコンポーネントを作っていきます。
コンポーネントには紙の長さと高さ、色を指定できるようにしておきます。
// components/Piece/Piece.tsx import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from "react"; import style from "./index.module.css"; type Props = Omit< DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "className" | "color" | "width" | "height" > & { color: Required<CSSProperties>["backgroundColor"]; width: Required<CSSProperties>["width"]; height: Required<CSSProperties>["height"]; }; export const Piece = forwardRef<HTMLDivElement, Props>((props, ref) => { const { color: frontColor, width, height, style: pieceStyle, ...rest } = props; return ( <div ref={ref} className={style.piece} style={{ backgroundColor, width, height, ...pieceStyle }} {...rest} /> ); }); Piece.displayName = "Piece";
この Piece を CSS の rotate3d で回転させてみましょう。
/* components/Piece/index.module.css */ .piece { animation: piece linear infinite; animation-duration: 2s; } @keyframes piece { 0% { transform: rotate3d(1, 1, 1, 0deg); } 100% { transform: rotate3d(1, 1, 1, 359deg); } }
上記で一見回転してくれそうなのですが、上記の CSS ではうまく動かないブラウザが存在します。
Mac Chrome | Mac Safari |
---|---|
この現象は rotate3d ではなく rotate では発生せず、意外と気づけないので注意が必要です。
解決策としては Mac Safari のようなブラウザでは、最終形のアニメーションの結果に対して最短のアニメーションをとるようなので、その中間の状態を追加することです。
@keyframes piece { 0% { transform: rotate3d(1, 1, 1, 0deg); } 25% { transform: rotate3d(1, 1, 1, 90deg); } 50% { transform: rotate3d(1, 1, 1, 180deg); } 75% { transform: rotate3d(1, 1, 1, 270deg); } 100% { transform: rotate3d(1, 1, 1, 359deg); } }
1 回転させようとすると 50%
だけではなく 25%
と 75%
時点の状態も指定して上げる必要があります。これで Mac Safari でも 1 回転させることができました。
紙吹雪に裏面をつくる
紙吹雪の裏面を表面よりすこし薄い色にしてみます。
// components/Piece/Piece.tsx export const Piece = forwardRef<HTMLDivElement, Props>((props, ref) => { const { color: frontColor, width, height, style: pieceStyle, ...rest } = props; return ( <div ref={ref} className={style.root} style={{ ...pieceStyle, width, height }} {...rest} > <div className={style.front} style={{ backgroundColor: frontColor }} /> <div className={style.back} /> </div> ); });
実際に 3D の空間を回転するアニメーションを行うのは、style.root
が指定された <div>
になります。紙の表裏になるのは 2 つの子要素で、それらを同一の 3D 空間に所属させるには CSS の transform-style
プロパティを使います。
.root { transform-style: preserve-3d; animation: piece linear infinite; animation-duration: 2s; } .front { position: absolute; width: 100%; height: 100%; } .back { position: absolute; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.8); backface-visibility: hidden; }
style.front
と style.back
はそれぞれ紙吹雪の全面と背面になります。ここで気をつける必要があるのは style.back
に指定されている backface-visibility: hidden;
です。この指定によって、表面が表示されているとき背面を表示しないようにしています。
紙吹雪を 1 枚落下させる
先ほど作成した Piece を 1枚だけ、落下させるコンポーネントを作成します。
まずは 1 個だけ落下させるコンポーネント Confetti.tsx
を作成してみます。
// components/Confetti/Confetti.tsx import { DetailedHTMLProps, forwardRef, HTMLAttributes } from "react"; import { Piece } from "../Piece"; import style from "./index.module.css"; type Props = Omit< DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "className" >; export const Confetti = forwardRef<HTMLDivElement, Props>((props, ref) => { return ( <div ref={ref} className={style.root} {...props}> <div className={style.piece}> <Piece color="green" width={10} height={10} /> </div> </div> ); }); Confetti.displayName = "Confetti";
/* components/Confetti/index.module.css */ .root { position: relative; width: 100%; height: 100%; overflow: hidden; } .piece { position: absolute; height: 100%; animation: fall linear infinite; animation-duration: 2s; margin-left: 50%; } @keyframes fall { 0% { transform: translate(0%, 0); } 25% { transform: translate(-200%, 25%); } 50% { transform: translate(0%, 50%); } 75% { transform: translate(200%, 75%); } 100% { transform: translate(0%, 100%); } }
アニメーションには bottom や margin を使用したくないので、Piece をラップする <div>
の高さを 100%
に指定しています。これで translate で %
を利用したときに親要素の上から下まで移動できるようになります。
紙吹雪を指定枚数落下させる
先ほど作成した Confetti を指定枚数分、落下させるコンポーネントに修正していきます。
// components/Confetti/Confetti.tsx const getRandomArbitrary = (min: number, max: number) => { return Math.random() * (max - min) + min; }; export const Confetti = (props: Props) => { const { count, duration, delay } = props; const pieces = useMemo(() => { return ( <> {[...Array(count)].map((_, i) => { const pieceStyle = { marginLeft: `${Math.random() * 100}%`, animationDelay: `-${getRandomArbitrary(...delay)}s`, animationDuration: `${getRandomArbitrary(...duration)}s`, }; return ( <div key={i} className={style.piece} style={pieceStyle}> <Piece color="green" width={10} height={10} /> </div> ); })} </> ); }, [count, delay, duration]); return <div className={style.root}>{pieces}</div>; };
紙吹雪の降り始める位置と開始位置、落下速度は pieceStyles で指定しています。
降り始める位置は margin-left
で 0%
から 100%
の間でランダムに配置しています。
開始位置は animation-delay
で指定しています。getRandomArbitrary()
は第 1 引数を最小値、第 2 引数を最大値とするランダムの値を戻す関数で、MDN からそのままもってきたコードです。負の値で指定することで、直ちに開始させる指定時間後のアニメーションを実行できます。
落下速度は animation-duration
で指定しています。こちらも getRandomArbitrary()
でランダムになるように調整しています。
紙吹雪の落下パターンを増やす
このままだと多少はランダムに散っているように見えますが、もうすこしランダムに降ってくるとそれっぽい感じになりそうなので、落下のアニメーションパターンを増やしてみます。
const classNameList = [style.fallA, style.fallB, style.fallC]; export const Confetti = (props: Props) => { const { count, duration, delay } = props; const pieces = useMemo(() => { return ( <> {[...Array(count)].map((_, i) => { const pieceStyle = { marginLeft: `${Math.random() * 100}%`, animationDelay: `-${getRandomArbitrary(...delay)}s`, animationDuration: `${getRandomArbitrary(...duration)}s`, }; const className = classNameList[Math.floor(Math.random() * classNameList.length)]; return ( <div key={i} className={`${style.piece} ${className}`} style={pieceStyle} > <Piece color="green" width={10} height={10} /> </div> ); })} </> ); }, [count, delay, duration]); return <div className={style.root}>{pieces}</div>; };
CSS に fallA, fallB, fallC のアニメーションを追加すると、それらのアニメーションがランダムで選択されるようになります。
紙吹雪を回転をランダムにする
紙吹雪の回転タイミングが同じになっているため、表を向いたときにはすべての紙吹雪が表を、裏のときには裏を向いてしまっています。そのため紙吹雪の回転の速度を変更していきます。
type Props = { count: number; pieceContainer: { duration: [number, number]; delay: [number, number]; }; piece: { duration: [number, number]; }; }; export const Confetti = (props: Props) => { const { count, pieceContainer, piece } = props; const pieces = useMemo(() => { return ( <> {[...Array(count)].map((_, i) => { const pieceContainerStyle = { marginLeft: `${Math.random() * 100}%`, animationDelay: `-${getRandomArbitrary(...pieceContainer.delay)}s`, animationDuration: `${getRandomArbitrary( ...pieceContainer.duration )}s`, }; const className = classNameList[Math.floor(Math.random() * classNameList.length)]; const pieceStyle = { animationDuration: `${getRandomArbitrary(...piece.duration)}s`, }; return ( <div key={i} className={`${style.piece} ${className}`} style={pieceContainerStyle} > <Piece color="green" width={10} height={10} style={pieceStyle} /> </div> ); })} </> ); }, [count, pieceContainer.delay, pieceContainer.duration, piece.duration]); return <div className={style.root}>{pieces}</div>; };
まとめ
CSS だけで紙吹雪を作ってみました。ランダムで色を変える機能を作ってみたりしてみても良さそうです。
黒い背景に黄色の紙吹雪などの配色にするとブラックフライデー感がでたり、紙吹雪の四角形を三角形にしてエフェクトにしてみたりとそこそこ応用がききやすいアニメーションだと思います。