LoginSignup
5
6

More than 1 year has passed since last update.

【React+Nextjs】無数の抽選アプリを駆逐する!? 超アニメーションくじ引きアプリを公開した!

Posted at

くじ引きアプリを公開しました!

いきなりですが、抽選している様子のデモ動画です!
demo.gif

上記は「抽選して大当たり!」のデモ動画です。
抽選感を演出して、当たりなら花火も上がります!
タイトルは大げさかもしれないですが、盛り上がる抽選くじ引きアプリができたと思います!
ちなみにReact+Nextjsで制作しています。(技術については後述)

ここから、技術解説やアプリの特徴などについておはなしします。

目次

1. 作ったアプリ
2. アプリを作った経緯
3. 構成図
4. アニメーションの特徴と実装
5. 今後の展望

作ったアプリ

改めてですが、アニメーションたっぷりのくじ引きアプリを作りました。
ログイン不要、どなたでもお使いいただけます!
名称は、くじ引きの次世代の意味を込めて「くじ引きアプリ2.0」にしました。
使ってみてね。

demo.gif

アプリを作った経緯

賞金をくじ引きで分配したい

会社の表彰で、チームで賞金をもらいました。
貢献度で賞金を分けよう!となるのですが。。

🤔「うーんなんか、そのまま金額で分けるの生々しいなー」という問題にあたりました。
なら、貢献度をくじ枚数に反映させて、分配する賞金をくじ引きで決めよう、ということになりました。

😆「くじ引き方式なら、運の要素が絡むからお金でギスギスせずにゆるく分配できるぞ!」
そして、私はウェブ上のくじ引きアプリを探し始めました。

コレ!というアプリがない

以下のふたつの要望を満たしたかったです。

  1. 画面共有しながら抽選できること
    ブラウザ、PCで使用できるウェブ上のアプリであること (スマホアプリはNG)
  2. 抽選中、ドキドキ・ワクワクすること
    せっかくだし、大いに盛り上がりたい!

しかし、検索しても両方を満たすアプリがなかなか見当たらない。
もっと盛り上がる演出を提供できる! と思ったのでウェブアプリを作ることにしました。

構成図

いきなりですが、技術記事なので構成図を載せます。
データベースなどのバックエンドは用意せず、フロントだけで完結させています。

diagram.drawio.png

JavaScriptのライブラリ紹介

React

  • コンポーネントベースで記述する人気のUIライブラリ
  • 個人的に、これがないとやってられない、最も好きなライブラリ

Next.js

  • SSRなどを簡単に構築、パフォーマンスやSEOで有利にできるライブラリ
  • Vercelが作っているので親和性◎
  • これのおかげか、パフォーマンス評価めちゃ高い

image.png

MUI

  • マテリアルデザインに則ったコンポネントを多数提供しているライブラリ
  • デザイン工数が大幅に削減、これがないとやってられん

react-spring

  • spring(=バネ)の動きをアニメーションに活かしたライブラリ
  • くじが「ビョーン」と出てくる演出などで使用
  • 動きに無駄がない、最高!
  • 実装は慣れるまで苦労した

react-canvas-confetti

  • reactで紙吹雪を降らせるライブラリ
  • canvas-confettiを基にReactで利用し易いコンポネントに変換している
  • あるとないとでは、盛り上がりの演出が全然変わってくるので非常にありがたい

その他の使用サービスの紹介

Vercel

ホスティングサービス。
管理画面も使いやすく、無料でもかなり使えて重宝しています。

Google Analitics

天下のGoogleアクセス解析ツール。
せっかくなので入れてみた。

Formspree

問い合わせフォームを受けるAPIを提供してくれる。
問い合わせが送信されると、私のメルアドに通知がくる。
バリデーションなども提供されていて、使いやすいです。

アニメーションの特徴と実装

デモ

最初に出しましたが、改めてデモ動画です。

demo.gif

抽選中のアニメーションの解説

shuffle.gif

抽選中はクジが高速にシャッフルされます。
これは、Reactの強みをフルに活かしています。
くじリストの順番を高速に入れ替え、それをReactが検知して描画してくれます。

Home.tsx
import { useState, useCallback, useEffect } from 'react';

const SHAFFLE_MILLISECONDS = 120;

const Home: NextPage = () => {
  const [flatLotteries, setFlatLotteries] = useState<FlatLotteriesType[]>([]);

  // シャッフルを行う関数
  const shuffle = useCallback(([...array]: FlatLotteriesType[]) => {
    for (let i = array.length - 1; i >= 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [array[i], array[j]] = [array[j], array[i]];
    }

    return array;
  }, []);

  useEffect(() => {
    if (lotteryStatus === 'shuffling') {
      // ここでシャッフル!flatLotteriesが変化
      const id = setInterval(() => {
        const newList = shuffle(flatLotteries);
        setFlatLotteries(newList);
      }, SHAFFLE_MILLISECONDS);
      setLotteryShuffulInterval(id);
    }
  }, [lotteryStatus])

  // flatLotteriesが変化することで、reactが検知、反映!
  return (
    <>
      {
        // slice(0, 1)で初回のものを取り出す。これが現在引きそうなくじ。
        flatLotteries.slice(0, 1).map((lottery) => (
          <LotteryItem
            key={lottery.id}
            size='large'
            id={lottery.id}
            value={lottery.value}
            rating={lottery.rating}
            color={lottery.color}
            isFlash={lotteryStatus === 'show-result'}
          />
        ))
      }
      {
        // slice(1)で初回以外のものを取り出す。これが後ろで入れ替わっているくじ。
        flatLotteries
          .slice(1)
          .map((lottery) => (
            <Box key={lottery.id}>
              <Box
                component={Grid}
                item
                css={{
                  opacity:
                    lotteryStatus === "shuffling"
                      ? "0.6"
                      : "1.0",
                }}
              >
                <LotteryItem
                  id={lottery.id}
                  value={lottery.value}
                  rating={lottery.rating}
                  color={lottery.color}
                />
              </Box>
            </Box>
          ));
      }
    </>
  )
}

当たったときのアニメーションの解説

前面に「ドン!」と出てくるアニメーションを実装しました。

1.透明の「大当たり!」がだんだん大きくなって...

mojikyo45_640-2.gif

mojikyo45_640-2.gif

2.「ドン!」と出てくる

mojikyo45_640-2.gif

react-springで実装しています。
「ドン!」と出てくる部分だけだと、下記のようなイメージで実装しています。

Home.tsx
import {
  useTransition,
  config,
  SpringValue,
  Controller,
  AnimationResult,
} from "react-spring";
import type { NextPage } from "next";

const Home: NextPage = () => {
  // ... 変数定義などは省略

  // react spring useTransitionの定義
  const centerLotteryPopOut = useTransition(
    // 第一引数: 表示するタイミングを状態制御
    // false -> trueで from -> enter。 true -> falseで enter -> leaveが適用される
    (lotteryStatus === "before-show-result" || lotteryStatus === "show-result") &&
    selectedLotteryStatus === "large-animating",
    // 第二引数: アニメーションとスタイルを定義
    {
      from: { opacity: "0.0", transform: "translate(-50%, -50%) scale(0.0)" },
      enter: { opacity: "1.0", transform: "translate(-50%, -50%) scale(1.0)" },
      leave: { opacity: "0.0", transform: "translate(-50%, -60%) scale(1.0)" },
      trail: 200,  // アニメーションを開始するまでの遅延時間
      config: config.wobbly,  // バネの強さはwobbly(=ガタガタ)
      onRest: (
        result: AnimationResult,
        spring: Controller | SpringValue,
        item: boolean
      ) => {
        if (!item) {
          // onRestにアニメーション終了後の処理を記述
          setSelectedLotteryStatus("top-animating");
        }
      },
    }
  );

  return (
    {centerLotteryPopOut(
      ({ ...style }, item) =>
        item && (
          // 「ドン!」と出てくる子コンポネントにスタイルをセットする(コンポネントの記述は省略)
          <CenterLargeLottery style={style} />
        )
    )}
  )
}


花火の演出

confetti.gif

react-canvas-confettiで実装しています。

Fireworks.tsxでコンポネント化して、呼び出します。
(setTimeout部分の実装ちょっと雑ですが動いているのでヨシ!)

{呼び出し側}.tsx
const [fireworksFire, setFireworksFire] = useState(false);
const [fireworksInterval, setFireworksInterval] = useState<null | NodeJS.Timer>(
  null
);

const Home: NextPage = () => {
  // 花火の打ち上げ
  const startFireworks = useCallback(
    (intervalTime: number) => {
      const interval = setInterval(() => {
        setTimeout(() => {
          // false -> trueになると発火する。次の実行のために、falseに戻す
          setFireworksFire(true);
          setTimeout(() => {
            setFireworksFire(false);
          }, 100);
        }, Math.random() * 300); // 定形すぎないランダムな打ち上げを演出
      }, intervalTime);
      setFireworksInterval(interval);
    },
    [lotteryStatus]
  );

  // clearIntervalで花火を停止
  const stopFireworks = useCallback(() => {
    fireworksInterval && clearInterval(fireworksInterval);
  }, [fireworksInterval]);

  return <Fireworks fire={fireworksFire} />;
};

Fireworks.tsx
import { css } from '@emotion/react';
import { memo, VFC } from 'react';
import ReactCanvasConfetti from 'react-canvas-confetti';

function randomInRange(min: number, max: number): number {
  return Math.random() * (max - min) + min;
}

const canvasStyle = css({
  backgroundColor: '#00000000',
  height: '100%',
  left: '0px',
  pointerEvents: 'none',
  position: 'fixed',
  top: '0px',
  width: '100%',
  zIndex: '1',
});

type Props = {
  fire: boolean;
};

const Fireworks: VFC<Props> = memo(function Fireworks(props: Props) {
  const { fire } = props;

  const animationSettings = {
    startVelocity: 30,
    spread: 360,
    ticks: 60,
    particleCount: 150,
    origin: {
      x: randomInRange(0.2, 0.8),
      y: randomInRange(0.2, 0.5),
    },
  };

  return (
    <>
      <ReactCanvasConfetti
        fire={fire}
        {...animationSettings}
        css={canvasStyle}
      />
    </>
  );
});

export default Fireworks;

感想と今後の展望

まず形にできたので、良かったです。
楽しげなアプリになりました。

実際にアプリを賞金の分配でも実際に使いました。
超大盛り上がりでした!
余談ですが、たまたま開発者である私がど頭に高額当選する事件が起きました。
「あれ、実はなんかコードに仕込んでいない?😂」って笑いが起きました。
(もちろんですが抽選は完全ランダムなのでご安心ください。ある特定の人を優遇する接待モードとかもないです。)

今後の展望として、直近ではパフォーマンス向上してより軽快に動かせるように改善したいです。
また、使ってもらいたいので、検索順位を上げる施策を取れたらなと思います。(SEOの勉強とか)
ぜひ使ってみてください!

さいごに

twitterを始めました。
フロント技術の情報など呟いているので、ぜひフォローお願いします!

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6