CSSでパックマン
はじめに
みなさんパックマンで遊んだことはありますか?わたしはありません。そんな話はさておき、パックマンのビジュアルは可愛らしいしなによりCSSで再現ができそうです。ということで動くパックマンをCSSで作っていきたいと思います。
※執筆中に気づいたのですが、パックマン99というゲームが今日(2021/04/08[木])から配信するというとてもタイムリーなニュースを見て驚きました。せっかくなので、急遽パックマンのロゴ?文字も作成しました。こちらに関しては気合で作れるのとボリュームが増えてしまうので、解説は載せません。
文字のAを作成するためにこちらの記事を参考にしました。
それではやっていきましょう。
完成品
まずは完成品です。
ソースコード
<!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: rotate
が0deg
じゃないのかと思われるかもしれません。
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先生から引用しますが、
隣接兄弟結合子(+) は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
やイージングの解説はあっても作り方の解説記事はあまりないと感じたため投稿をはじめました。次からはもっと初心者向けの記事を書いていけたらと思います。
Discussion