🐯

【謹賀新年🎍】寅の絵を描いてCSS初めしよう!

2022/01/02に公開

あけましておめでとうございます🎍🐯🌅🙏✨

今年は寅年ということで寅のイラストでCSS初めしてみたので、描いたイラストとどうやって描いたのかを簡単にご紹介したいと思います。

CSSで描いた寅のイラスト

早速こちらが今回描いた寅のイラストになります。こちらのイラストを参考にして可愛い感じにしてみました。全てHTMLとCSSだけで描いています。

友人に見せたところベネッセのトラみたいだと言われた

環境

MacBookPro + Chromeで動作確認しています。(a11yやレスポンシブには配慮していません。)

ソースコード

ちょっとイラストを描くだけだったので、Saasは使わず、生のCSSで書いています。

CodePen

HTML

全文
index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="stylesheet.css" />
    <link
      rel="icon"
      href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 style=%22dominant-baseline:central;text-anchor:middle;font-size:90px;%22>🐯</text></svg>"
    />
    <title>HAPPY NEW YEAR !</title>
  </head>
  <body>
    <div class="container">
      <div class="ear"></div>
      <div class="ear" style="--right: 0"></div>
      <div class="face">
        <div class="eye" style="--left: 20%"></div>
        <div class="eye" style="--right: 20%"></div>
        <div class="around-mouth"></div>
        <div class="nose">
          <div class="mouth"></div>
        </div>
        <div class="beard" style="--left: 15%"></div>
        <div class="beard" style="--right: 15%; --rotate: -1"></div>
        <div class="top-pattern"></div>
        <div class="side-pattern"></div>
        <div class="side-pattern" style="--right: 0; --rotate: -1"></div>
      </div>
    </div>
  </body>
</html>

余談ですが、🐯をファビコンにするのはcatnoseさんの記事を真似してやりました。

CSS

全文
stylesheet.css
/* カスタムプロパティ */
:root {
  --tiger-color: #fcdb57;
  --tiger-border: #594639;
  --right: auto;
  --left: auto;
  --rotate: 1;
}

/* 共通のスタイル */
.face *,
.face *::before,
.face *::after {
  position: absolute;
  content: "";
}

/* コンテナ */
.container {
  position: relative;
  width: 260px;
  height: 220px;
}

/* 輪郭 */
.face {
  position: relative;
  width: 250px;
  height: 210px;
  overflow: hidden;
  background-color: var(--tiger-color);
  border: solid 5px var(--tiger-border);
  border-radius: 50% / 60% 60% 40% 40%;
}

/* 耳 */
.ear {
  position: absolute;
  right: var(--right);
  z-index: -1;
  width: 60px;
  height: 60px;
  background-color: var(--tiger-color);
  border: solid 5px var(--tiger-border);
  border-radius: 50%;
}

.ear::before {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  content: "";
  background-color: white;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

/* 目 */
.eye {
  top: 45%;
  right: var(--right);
  left: var(--left);
  width: 25px;
  height: 25px;
  background-color: var(--tiger-border);
  border-radius: 50%;
}

/* 鼻 */
.nose {
  top: 57%;
  left: 50%;
  width: 35px;
  height: 25px;
  background-color: var(--tiger-border);
  border-radius: 50% / 40% 40% 60% 60%;
  transform: translateX(-50%);
}

/* 口 */
.mouth {
  top: 23px;
  left: 50%;
  width: 70px;
  height: 15px;
  transform: translateX(-50%);
}

.mouth::before,
.mouth::after {
  box-sizing: border-box;
  width: 35px;
  height: 15px;
  content: "";
  border: solid 5px var(--tiger-border);
  border-top: 0;
  border-radius: 0 0 75px 75px;
}

.mouth::before {
  left: 0;
}

.mouth::after {
  left: 50%;
}

/* ひげ */
.beard {
  top: 67%;
  right: var(--right);
  left: var(--left);
  width: 25px;
  height: 3px;
  background-color: var(--tiger-border);
}

.beard::before,
.beard::after {
  right: calc(var(--rotate) * -5px);
  width: 25px;
  height: 3px;
  background-color: var(--tiger-border);
}

.beard::before {
  top: -13px;
  transform: rotate(calc(var(--rotate) * 20deg));
}

.beard::after {
  top: 13px;
  transform: rotate(calc(var(--rotate) * -20deg));
}

/* 口周りの白い部分 */
.around-mouth {
  bottom: 0;
  left: 50%;
  width: 150px;
  height: 100px;
  background-color: white;
  border-radius: 50% / 80% 80% 20% 20%;
  transform: translateX(-50%);
}

/* 模様(上) */
.top-pattern {
  left: 50%;
  width: 20px;
  height: 70px;
  background-color: var(--tiger-border);
  border-radius: 50% / 0% 0% 100% 100%;
  transform: translateX(-50%);
}

.top-pattern::before,
.top-pattern::after {
  left: 50%;
  height: 15px;
  background-color: var(--tiger-border);
  border-radius: 50% / 70% 70% 30% 30%;
  transform: translateX(-50%);
}

.top-pattern::before {
  top: 20%;
  width: 70px;
}

.top-pattern::after {
  top: 55%;
  width: 60px;
}

/* 模様(左右) */
.side-pattern {
  top: 50%;
  right: var(--right);
  width: 70px;
  height: 15px;
  background-color: var(--tiger-border);
  border-radius: 50%;
  transform: translateX(calc(var(--rotate) * -50%))
    rotate(calc(var(--rotate) * -15deg));
}

.side-pattern::before,
.side-pattern::after {
  position: absolute;
  width: 70px;
  height: 15px;
  background-color: var(--tiger-border);
  border-radius: 50%;
}

.side-pattern::before {
  top: -25px;
  left: calc(var(--rotate) * 10px);
}

.side-pattern::after {
  bottom: -25px;
  left: calc(var(--rotate) * -10px);
}

実装のポイント

1から解説するととてつもなく長くなってしまうのでいくつかのポイントに絞って紹介したいと思います。

HTMLの構造

まずは寅のイラストをいくつかのパーツに分けます。
今回はコンテナという透明なボックスを起点に耳パーツと顔パーツに分け、顔パーツの中でさらに目や口など細かいパーツに分けました。
口は鼻にくっついているので鼻を起点にして位置を考えた方が実装しやすいだろうと考え、鼻の子要素にしています。
また、当初は顔の子要素に耳があったのですが、口周りの白い部分や模様が顔からはみ出ないようにする際に都合が悪かったため、コンテナを作るようにしました。(詳細は後述)

index.html
<div class="container">
  <div class="ear"></div>
  <div class="ear" style="--right: 0"></div>
  <div class="face">
    <div class="eye" style="--left: 20%"></div>
    <div class="eye" style="--right: 20%"></div>
    <div class="around-mouth"></div>
    <div class="nose">
      <div class="mouth"></div>
    </div>
    <div class="beard" style="--left: 15%"></div>
    <div class="beard" style="--right: 15%; --rotate: -1"></div>
    <div class="top-pattern"></div>
    <div class="side-pattern"></div>
    <div class="side-pattern" style="--right: 0; --rotate: -1"></div>
  </div>
</div>

HTMLの構造を簡単に図にしてみました。

構成図

position: absoluteで親要素を基準に位置決め

containerface以外の全ての要素のスタイルにposition: absoluteを適用しています。
absoluteとは、親要素からどれくらいの距離か?で位置を指定するものです。

例えば「目」の縦位置は「顔」の真ん中あたり、横位置は「顔」の端から少し内側のところにありますよね。このようにCSSでイラストを描く際は「親要素からどれくらいの位置にあるか?」で位置を考えることが多いので、ほぼ全ての子要素にposition: absoluteを使っています。

具体的な位置はtop bottom right leftプロパティを用いて、親要素から上/下/右/左へどれくらい進むかで指定します。(例:left: 50% top: 23px;


顔(親要素)からの位置で目の位置を指定する

positionプロパティについてはこちらの記事がとてもわかりやすいです。
https://saruwakakun.com/html-css/basic/relative-absolute-fixed

擬似要素 before afterを活用しよう

擬似要素である::before::afterは本来、contentプロパティを使用して、要素の前後に装飾的な内容を追加するためのものです。例えば、以下[1] のように使います。

/* リンクの前にハートを追加 */
a::before {
  content: "♥";
}

一方、CSSでイラストを描く場合、::before::afterわざわざHTMLのタグを増やすほどでもない細かいパーツの実装同じパーツであるものの実装上は2つ〜3つの要素に分けないといけないパーツの実装に役立ちます。
もちろん擬似要素を使わず普通に子要素にしてしまっても仕上がりは変わりませんが、::before::afterを使うことでHTMLをスッキリさせることができるのです。

具体的には以下のパーツで::before::afterを使っています。例えば顔の模様(上)なら縦線がtop-pattern、横線のうち上にある方がtop-pattern::beforeで下にある方がtop-pattern::afterです。

寅のイラストでbeforeやafterを使っている箇所

こちらはear::beforeの実装例です。contentを空にし、後は普通の要素と同様にしてスタイルを書いています。

.ear::before {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  content: "";
  background-color: white;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

共通のスタイルをまとめる

セレクタを活用

CSSでイラストを描こうとするとえげつない記述量になりがちです。
イラストである以上ある程度は仕方がないのですが、できる限り記述量を減らすために、あらゆる箇所で出てくるスタイルはまとめて定義しておきましょう。
.test { position: absolute; }.testの部分はセレクタと呼ばれる部分で、単にクラス名や要素名が指定されることが多いですが、実はもっと詳しく条件を指定することができます。

今回の場合はfaceの全ての子要素内のposition: absolute::before ::afterでのcontent: ""は毎回出てくるのでまとめて定義しておきます。

/* 共通のスタイル */
.face *,
.face *::before,
.face *::after {
  position: absolute;
  content: "";
}

「*」は全ての要素 という意味になります。.faceの後にスペースを開けて*を書くことで 「faceクラス内の全ての子要素(直接の子要素でなくてもいい)」 という意味になります。
そこからさらに::beforeをつけると「faceクラス内の全ての子要素の::before要素」という意味になります。

そして、「,」で区切ることで「.face *あるいは.face *::beforeあるいは.face *::afterで以下のスタイルを適用します」と言う意味になり、同じスタイルを複数のセレクタにまとめて適用することができます。

カスタムプロパティで定数を定義

寅の黄色や焦げ茶色などのカラーコードは定数として定義することで、可読性を上げることができますし、「やっぱりこの寅ピンクにしたいなあ」なんてときも簡単に変更できるようになります。
CSSにはカスタムプロパティという機能があり、変数のような役割を果たします。

/* カスタムプロパティ */
:root {
  --tiger-color: #fcdb57;
  --tiger-border: #594639;
}

--変数名: 値でカスタムプロパティを定義することができます。:rootというところに書くと全ての箇所でこのカスタムプロパティが適用されるようになります。

カスタムプロパティを使いたいときは

background-color: var(--tiger-color);

のようにvar(プロパティ名)と書きます。

関数の引数のようにカスタムプロパティを使いたい

前項でカスタムプロパティを定数として使いましたが、少し工夫すれば関数の引数みたいに使うことができます。

顔には左右対称のパーツがいくつか存在します。例えば目です。
素朴に実装しようとすると、以下のように「右目クラス」と「左目クラス」を作ると思います。

<div class="left-eye"></div>
<div class="right-eye"></div>
.left-eye, .right-eye {
  top: 45%;
  width: 25px;
  height: 25px;
  background-color: var(--tiger-border);
  border-radius: 50%;
}

.left-eye {
  left: 20%;
}

.right-eye {
  right: 20%;
}

left-eyeright-eyeで違うのは左右の位置のみです。しかし、その違いがあるゆえにコード量が増えてしまっています。

カステムプロパティを使えば、「eyeという1つのクラスを定義し、渡される引数の値で適用されるスタイルを変える」という処理を実現することができます。

実際のコードを見てみましょう。
まず、:root--right --leftプロパティを定義し、初期値をautoに設定します。autoというのはleftrightプロパティの初期値で、これらのプロパティを無効にするために使います。
次にeyeクラスの中でrightleftプロパティの値として、先ほど定義したカスタムプロパティを指定します。

:root {
  /* 前略 */
  --right: auto;
  --left: auto;
}

.eye {
  top: 45%;
  right: var(--right);
  left: var(--left);
  width: 25px;
  height: 25px;
  background-color: var(--tiger-border);
  border-radius: 50%;
}

このままではleft rightともにautoになってしまいます。左目のときはleft: 20%;、右目のときはright: 20%;にしたいのでHTML側からカスタムプロパティの値を渡してあげましょう。

<div class="eye" style="--left: 20%"></div>
<div class="eye" style="--right: 20%"></div>

このようにすることでカステムプロパティの値を上書きすることができます。

ちなみにcalc()と併用することでより柔軟に値を設定することもできます。

.beard::before {
  top: -13px;
  transform: rotate(calc(var(--rotate) * 20deg));
}

border-radiusは最大8つの値を指定できる

border-radiusborder-radius: 50%;などのように使うことが多いですが、実は最大8つの値をとることができます。 それによりより複雑な円を書くことができ、今回のイラストでは寅の輪郭などに使っています。

理論を説明するよりも実際に触ってみた方が早いと思うので気になる方は以下のサイトで色々触ってみてください。
https://9elements.github.io/fancy-border-radius/full-control.html

理論的なことについては以下の記事がわかりやすいと思います。
https://coliss.com/articles/build-websites/operation/css/css-border-radius-can-do-that.html

overflow:hiddenで顔からはみ出ないようにする

左右の模様と口周りの白い部分は親要素であるfaceoverflow:hiddenを設定することで、はみ出た部分を非表示にしています。

overflow:hiddenを指定しなかった場合

earfaceの子要素である場合、耳は顔の外にあるものなのでoverflow:hiddenを設定すると非表示になってしまいます。そこでcontainerという顔と同じサイズの透明な要素を設定し、そこを基準にして耳を生やすようにしました。

translateでパーツを真ん中に持ってくる

鼻を顔の真ん中の位置に持ってこようとしてleft: 50%をしても真ん中にはならず右にずれてしまいます。これはleft: 50%要素の左端を基準としているからです。

親要素の左端から要素の左端までで50%

この状態から要素の半分の長さだけ左に戻してあげることで真ん中にすることができます。それにはtranslateXを使います。

/* 鼻 */
.nose {
  /* 前略 */
  left: 50%;
  transform: translateX(-50%);
}

ここでいう50%というのは要素のX軸方向の長さの50% なので、つまり要素の半分の長さの分だけ移動するという意味になります。
同様に縦方向の真ん中にパーツを置きたい場合はtranslateYを使いましょう。

終わりに

この記事ではCSSで寅を描く方法について紹介しました。やっぱりSassを使わないのはしんどいですが、セレクタやカスタムプロパティの勉強になってよかったです。

最後に超即席ですが今回描いた寅を年賀状っぽくしたので貼っておきます。

今年もよろしくお願いします!🎍🐯🌅🙏✨

脚注
  1. https://developer.mozilla.org/ja/docs/Web/CSS/::before ↩︎

Discussion