CSSでパックマン

19 min read読了の目安(約17800字

はじめに

みなさんパックマンで遊んだことはありますか?わたしはありません。そんな話はさておき、パックマンのビジュアルは可愛らしいしなによりCSSで再現ができそうです。ということで動くパックマンをCSSで作っていきたいと思います。
※執筆中に気づいたのですが、パックマン99というゲームが今日(2021/04/08[木])から配信するというとてもタイムリーなニュースを見て驚きました。せっかくなので、急遽パックマンのロゴ?文字も作成しました。こちらに関しては気合で作れるのとボリュームが増えてしまうので、解説は載せません。
文字のAを作成するためにこちらの記事を参考にしました。

https://zenn.dev/seya/articles/f642acf1c47358
それではやっていきましょう。

完成品

まずは完成品です。

ソースコード
<!DOCTYPE html>
<html>
<head>
    <title>Packman</title>
    <style>
        *,
        *::before,
        *::after {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {background: #333;}

        section {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            width: 100%;
            min-height: 100vh;
        }

        .loading {
            width: 200px;
            height: 100px;
            margin-left: -100px;
            position: relative;
        }

        .pack-man {
            position: absolute;
            width: 50px;
            height: 100px;
            top: 0;
            left: 0;
            border-radius: 50px 0 0 50px;
            background: yellow;
        }

        .pack-man::before,
        .pack-man::after {
            content: "";
            display: block;
            width: 100px;
            height: 50px;
            position: absolute;
            background: yellow;
            left: 0;
        }

        .pack-man::before {
            top: 50px;
            border-radius: 0 0 50px 50px;
            transform-origin: center top;
            transform: rotate(40deg);
            animation: eat1 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
        }

        @keyframes eat1 {
            60%,
            100% {transform: rotate(-5deg);}
        }

        .pack-man::after {
            top: 0;
            border-radius: 50px 50px 0 0;
            transform-origin: center bottom;
            transform: rotate(-40deg);
            animation: eat2 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
        }

        @keyframes eat2 {
            60%,
            100% {transform: rotate(5deg);}
        }

        .fuels {
            position: relative;
            display: flex;
            top: 40px;
            left: 105px;
            width: 210px;
            height: 30px;
        }

        .fuels div {
            width: 30px;
            height: 30px;
            background: yellow;
            border-radius: 50%;
            animation: translate1 ease-out 1s 0s infinite;
        }

        .fuels div + div {margin-left: 60px;}

        @keyframes translate1 {
            100% {transform: translateX(-90px);}
        }

        .fuels div:last-child {
            position: absolute;
            right: 0;
            transform: scale(0);
            animation: translate2 linear 1s 0s infinite;
        }

        @keyframes translate2 {
            0%,
            69% {transform: scale(0);}
            70%,
            90% {transform: scale(.5);}
            80%,
            100% {transform: scale(1);}
        }

        .text {
            display: flex;
            margin-top: 20px;
        }

        .text div {
            background: yellow;
        }

        .p {
            width: 10px;
            height: 40px;
            position: relative;
            margin-right: 14px;
        }

        .p::before {
            content: "";
            display: block;
            background: yellow;
            width: 20px;
            height: 25px;
            position: absolute;
            left: 9px;
            border-radius: 0 12.5px 12.5px 0;
        }

        .p::after {
            content: "";
            display: block;
            background: #333;
            position: absolute;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            top: 8px;
            left: 11px;
        }

        .a {
            width: 40px;
            height: 40px;
            clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
            position: relative;
        }

        .a::before {
            content: "";
            display: block;
            background: #333;
            position: absolute;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            top: 20px;
            left: 16px;
        }

        div.c {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .c::before,
        .c::after {
            content: "";
            display: block;
            background: yellow;
            width: 40px;
            height: 20px;
            position: absolute;
        }

        .c::before {
            border-radius: 20px 20px 0 0;
            transform-origin: 50% 100%;
            transform: rotate(-30deg);
        }

        .c::after {
            border-radius: 0 0 20px 20px;
            transform-origin: 50% 0%;
            transform: rotate(30deg);
            bottom: 0;
        }

        .hyphen {
            width: 20px;
            height: 10px;
            margin: 15px 5px;
        }

        div.m {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .m::before,
        .m::after {
            content: "";
            display: block;
            width: 40px;
            height: 40px;
            background: yellow;
            position: absolute;
        }

        .m::before {
            clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
        }

        .m::after {
            clip-path: polygon(100% 0%, 0% 100%, 100% 100%);
        }

        div.n {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .n::before,
        .n::after {
            content: "";
            display: block;
            height: 40px;
            background: yellow;
            position: absolute;
        }

        .n::before {
            width: 40px;
            clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
        }

        .n::after {
            width: 20px;
            top: -.1px;
            right: 0;
        }

        .text div + div {margin-left: 3px;}
        
    </style>
</head>
<body>
    <section>
        <div class="loading">
            <div class="pack-man">
            </div>
            <div class="fuels">
                <div></div>
                <div></div>
                <div></div>
                <div></div>
            </div>
        </div>
        <div class="text">
            <div class="p"></div>
            <div class="a"></div>
            <div class="c"></div>
            <div class="hyphen"></div>
            <div class="m"></div>
            <div class="a"></div>
            <div class="n"></div>
        </div>
    </section>
</body>
</html>

あとはこれの応用でこんなのもできます。

解説

HTML部分はパックマンだけであれば、これだけです。

<section>
  <div class="loading">
      <div class="pack-man">
      </div>
      <div class="fuels">
          <div></div>
          <div></div>
          <div></div>
          <div></div>
      </div>
  </div>
</section>

黄色い球の数を増やしたい人は

<div class="fuels">
</div>

の中のdivをn+1個追加してください。またそれに合わせて横幅も調整してください。

パックマン作成

顔の上半分と下半分に分けて作成するのでdivを二つ用意してもいいですが、疑似要素を用いて一つのdivで再現してみましょう。この部分を作成している箇所がこちらです。

.pack-man::before,
.pack-man::after {
    content: "";
    display: block;
    width: 100px;
    height: 50px;
    position: absolute;
    background: yellow;
    left: 0;
}

.pack-man::before {
    top: 50px;
    border-radius: 0 0 50px 50px;
    transform-origin: center top;
    transform: rotate(40deg);
    animation: eat1 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
}

.pack-man::after {
    top: 0;
    border-radius: 50px 50px 0 0;
    transform-origin: center bottom;
    transform: rotate(-40deg);
    animation: eat2 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
}

ここでの注意点は一つです。
transform-originの初期値は50% 50%であるためそのまま回転させるとおかしなことになってしまいます。そのため、顔の上半分にはtransform-origin:50% 100%もしくはtransform-origin: center bottomをつけてあげましょう。
下半分も同じく、transform-origin:50% 0%もしくはtransform-origin: center topをつけてあげます。

アニメーション作成

パックマンの@keyframes部分を作成している箇所がこちらです。

@keyframes eat1 {
    60%,
    100% {transform: rotate(-5deg);}
}

@keyframes eat2 {
    60%,
    100% {transform: rotate(5deg);}
}

ここでなぜtransform: rotate0degじゃないのかと思われるかもしれません。
0degにした場合、'Ctr + スクロール'等で縮小すると境目に隙間ができてしまい見栄えが悪くなるからです。そのため、5°ほど余分に回転させることによってこの問題を回避しています。
下記が0degを指定したときの状態です。

しかし、こうすることで新しい問題が出てきます。それは、口の逆側に隙間ができてしまうことです。これに関しては、

.pack-man {
    position: absolute;
    width: 50px;
    height: 100px;
    top: 0;
    left: 0;
    border-radius: 50px 0 0 50px;
    background: yellow;
}

この部分で蓋をすることによって回避しています。
色を変更してみるとこのようになっています。

黄色い球作成

.fuels {
    position: relative;
    display: flex;
    width: 210px;
    height: 30px;
}

.fuels div {
    width: 30px;
    height: 30px;
    background: yellow;
    border-radius: 50%;
    animation: translate1 ease-out 1s 0s infinite;
}

.fuels div + div {margin-left: 60px;}

.fuels div:last-child {
    position: absolute;
    right: 0;
    transform: scale(0);
    animation: translate2 linear 1s 0s infinite;
}

これはシンプルですね。縦横幅を決めて、border-radius: 50%を設定するだけで見た目は完成します。
ここで少し工夫している点がmargin-leftを設定しているこの部分です。

.fuels div + div {margin-left: 60px;}

+セレクタの説明はMDN先生から引用しますが、

https://developer.mozilla.org/ja/docs/Web/CSS/Adjacent_sibling_combinator

隣接兄弟結合子(+) は2つのセレクターを接続し、同じ親要素の子同士であって、1つ目の要素の直後にある2つ目の要素を選択します。

+セレクタを知らない状態ではおそらくこのように実装すると思います。

.fuels div {margin-left: 60px;}
.fuels div:first-child {margin-left: 0;}

これを+セレクタを用いることによって1行で再現しています。このセレクタはCSSアニメーションに限らず色々な場面で活用できるので覚えておくと便利です。
またこのままではパックマンの位置とずれるのでtop: 40px;left: 105px;を追加して位置の調節を行います。

.fuels {
    position: relative;
    display: flex;
+   top: 40px;
+   left: 105px;
    width: 210px;
    height: 30px;
}

アニメーション作成

前3つの球と最後の球では別のアニメーションを付けます。

@keyframes translate1 {
    100% {transform: translateX(-90px);}
}

次に、最後の球のアニメーションはこちらです。

@keyframes translate2 {
    0%,
    69% {transform: scale(0);}
    70%,
    90% {transform: scale(.5);}
    80%,
    100% {transform: scale(1);}
}

ここがお気に入りポイントなのですが、70%90%の時はtransform: scale(.5)なのですが、その間の80%transform: scale(1)を挟むことによって球が出てくる瞬間が無機質ではなく躍動感のあるアニメーションになります。ある程度CSSアニメーションを書ける人であれば、このあたりを意識することによってさらに質の高いアニメーションにすることができます。

完成!!!

<!DOCTYPE html>
<html>
<head>
    <title>Packman</title>
    <style>
        *,
        *::before,
        *::after {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {background: #333;}

        section {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            width: 100%;
            min-height: 100vh;
        }

        .loading {
            width: 200px;
            height: 100px;
            margin-left: -100px;
            position: relative;
        }

        .pack-man {
            position: absolute;
            width: 50px;
            height: 100px;
            top: 0;
            left: 0;
            border-radius: 50px 0 0 50px;
            background: yellow;
        }

        .pack-man::before,
        .pack-man::after {
            content: "";
            display: block;
            width: 100px;
            height: 50px;
            position: absolute;
            background: yellow;
            left: 0;
        }

        .pack-man::before {
            top: 50px;
            border-radius: 0 0 50px 50px;
            transform-origin: center top;
            transform: rotate(40deg);
            animation: eat1 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
        }

        @keyframes eat1 {
            60%,
            100% {transform: rotate(-5deg);}
        }

        .pack-man::after {
            top: 0;
            border-radius: 50px 50px 0 0;
            transform-origin: center bottom;
            transform: rotate(-40deg);
            animation: eat2 cubic-bezier(0.55, 0.06, 0.68, 0.19) .5s 0s infinite alternate;
        }

        @keyframes eat2 {
            60%,
            100% {transform: rotate(5deg);}
        }

        .fuels {
            position: relative;
            display: flex;
            top: 40px;
            left: 105px;
            width: 210px;
            height: 30px;
        }

        .fuels div {
            width: 30px;
            height: 30px;
            background: yellow;
            border-radius: 50%;
            animation: translate1 ease-out 1s 0s infinite;
        }

        .fuels div+div {margin-left: 60px;}

        @keyframes translate1 {
            100% {transform: translateX(-90px);}
        }

        .fuels div:last-child {
            position: absolute;
            right: 0;
            transform: scale(0);
            animation: translate2 linear 1s 0s infinite;
        }

        @keyframes translate2 {
            0%,
            69% {transform: scale(0);}
            70%,
            90% {transform: scale(.5);}
            80%,
            100% {transform: scale(1);}
        }

        .text {
            display: flex;
            margin-top: 20px;
        }

        .text div {
            background: yellow;
        }

        .p {
            width: 10px;
            height: 40px;
            position: relative;
            margin-right: 14px;
        }

        .p::before {
            content: "";
            display: block;
            background: yellow;
            width: 20px;
            height: 25px;
            position: absolute;
            left: 9px;
            border-radius: 0 12.5px 12.5px 0;
        }

        .p::after {
            content: "";
            display: block;
            background: #333;
            position: absolute;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            top: 8px;
            left: 11px;
        }

        .a {
            width: 40px;
            height: 40px;
            clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
            position: relative;
        }

        .a::before {
            content: "";
            display: block;
            background: #333;
            position: absolute;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            top: 20px;
            left: 16px;
        }

        div.c {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .c::before,
        .c::after {
            content: "";
            display: block;
            background: yellow;
            width: 40px;
            height: 20px;
            position: absolute;
        }

        .c::before {
            border-radius: 20px 20px 0 0;
            transform-origin: 50% 100%;
            transform: rotate(-30deg);
        }

        .c::after {
            border-radius: 0 0 20px 20px;
            transform-origin: 50% 0%;
            transform: rotate(30deg);
            bottom: 0;
        }

        .hyphen {
            width: 20px;
            height: 10px;
            margin: 15px 5px;
        }

        div.m {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .m::before,
        .m::after {
            content: "";
            display: block;
            width: 40px;
            height: 40px;
            background: yellow;
            position: absolute;
        }

        .m::before {
            clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
        }

        .m::after {
            clip-path: polygon(100% 0%, 0% 100%, 100% 100%);
        }

        div.n {
            width: 40px;
            height: 40px;
            background: transparent;
            position: relative;
        }

        .n::before,
        .n::after {
            content: "";
            display: block;
            height: 40px;
            background: yellow;
            position: absolute;
        }

        .n::before {
            width: 40px;
            clip-path: polygon(0% 0%, 0% 100%, 100% 100%);
        }

        .n::after {
            width: 20px;
            top: -.1px;
            right: 0;
        }

        .text div + div {margin-left: 3px;}
        
    </style>
</head>
<body>
    <section>
        <div class="loading">
            <div class="pack-man">
            </div>
            <div class="fuels">
                <div></div>
                <div></div>
                <div></div>
                <div></div>
            </div>
        </div>
        <div class="text">
            <div class="p"></div>
            <div class="a"></div>
            <div class="c"></div>
            <div class="hyphen"></div>
            <div class="m"></div>
            <div class="a"></div>
            <div class="n"></div>
        </div>
    </section>
</body>
</html>

おわりに

こんな長々とした記事を読んで下さってありがとうございます。
CSSアニメーションは実際のホームページに役立つ知識を付けられるかというと微妙ですが、動きがあるため初学者でも楽しめるCSSの楽しさを知る第一歩にはなると思っています。実際私自身がCSSアニメーションからCSSを学び始めたものです。しかし、@keyframesやイージングの解説はあっても作り方の解説記事はあまりないと感じたため投稿をはじめました。次からはもっと初心者向けの記事を書いていけたらと思います。