ツクログ

tsukulognet

tsukulognet

道産子。Reactでなまら面白いものを作りたい。

猫のアナログ時計を作る方法

eye catch

HTML,CSS,JavaScriptによる猫のアナログ時計の作り方です。

Demo

以下が今回作るものです。

秒針の移動と同時に猫が瞬きしている様子が確認できます。

HTMLで全体の構造を定義

始めに以下のHTMLを書いて全体の骨組みを作ります。

<div class="clock"> <div class="clock__inner"> <ul class="clock__nums"> <li class="clock__num clock__num--1"><span class="clock__num__text clock__num__text--1">1</span></li> <li class="clock__num clock__num--2"><span class="clock__num__text clock__num__text--2">2</span></li> <li class="clock__num clock__num--3"><span class="clock__num__text clock__num__text--3">3</span></li> <li class="clock__num clock__num--4"><span class="clock__num__text clock__num__text--4">4</span></li> <li class="clock__num clock__num--5"><span class="clock__num__text clock__num__text--5">5</span></li> <li class="clock__num clock__num--6"><span class="clock__num__text clock__num__text--6">6</span></li> <li class="clock__num clock__num--7"><span class="clock__num__text clock__num__text--7">7</span></li> <li class="clock__num clock__num--8"><span class="clock__num__text clock__num__text--8">8</span></li> <li class="clock__num clock__num--9"><span class="clock__num__text clock__num__text--9">9</span></li> <li class="clock__num clock__num--10"><span class="clock__num__text clock__num__text--10">10</span></li> <li class="clock__num clock__num--11"><span class="clock__num__text clock__num__text--11">11</span></li> <li class="clock__num clock__num--12"><span class="clock__num__text clock__num__text--12">12</span></li> </ul> <div class="clock__needles"> <div class="clock__needle clock__needle--hour" id="hour"></div> <div class="clock__needle clock__needle--min" id="min"></div> <div class="clock__needle clock__needle--sec" id="sec"></div> </div> </div> <div class="cat"> <div class="cat__ears"> <div class="cat__ears__ear cat__ears__ear--left"> </div> <div class="cat__ears__ear cat__ears__ear--right"> </div> </div> <div class="cat__whiskers"> <div class="cat__whiskers__whisker cat__whiskers__whisker--left"> <span></span> </div> <div class="cat__whiskers__whisker cat__whiskers__whisker--right"> <span></span> </div> </div> <div class="cat__face"> <div class="cat__face__pattern cat__face__pattern--top"> </div> <div class="cat__face__pattern cat__face__pattern--left"> </div> <div class="cat__face__pattern cat__face__pattern--right"> </div> <div class="cat__eyes"> <div class="cat__eyes__eye cat__eyes__eye--left"> <div class="cat__eyes__eyelid cat__eyes__eyelid--left"></div> <span></span> </div> <div class="cat__eyes__eye cat__eyes__eye--right"> <div class="cat__eyes__eyelid cat__eyes__eyelid--right"></div> <span></span> </div> </div> <div class="cat__nose"> </div> <div class="cat__mouth"> </div> </div> <div class="cat__hands"> <div class="cat__hands__hand cat__hands__hand--left"> </div> <div class="cat__hands__hand cat__hands__hand--right"> </div> </div> <div class="cat__legs"> <div class="cat__legs__leg cat__legs__leg--left"> </div> <div class="cat__legs__leg cat__legs__leg--right"> </div> </div> <div class="cat__tail"> <div class="cat__tail__inner"> <span class="cat__tail__pattern cat__tail__pattern--1"></span> <span class="cat__tail__pattern cat__tail__pattern--2"></span> <span class="cat__tail__pattern cat__tail__pattern--3"></span> <span class="cat__tail__pattern cat__tail__pattern--4"></span> <span class="cat__tail__pattern cat__tail__pattern--5"></span> <span class="cat__tail__pattern cat__tail__pattern--6"></span> <span class="cat__tail__pattern cat__tail__pattern--7"></span> <span class="cat__tail__pattern cat__tail__pattern--8"></span> <span class="cat__tail__pattern cat__tail__pattern--9"></span> </div> </div> </div> </div>

.clock以下はアナログ時計の骨組みであり、文字盤span、中心軸ul.point、各針.needleで構成されています。

.cat以下は猫の構成を定義しています。

CSS(SCSS)でアナログ時計の見た目を作る

次に以下のCSSを書いてアナログ時計の見た目を作ります。

* { box-sizing: border-box; } html { font-size: 10vw; @media screen and (min-width: 500px) { font-size: 200%; } } body { background: #79D1B0; font-family: 'Vollkorn', serif; margin: 0; padding: 0; } ul { list-style: none; margin: 0; padding: 0; } .clock { -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); position: absolute; top: 50%; left: 50%; height: 6em; width: 6em; &__inner { background: #fff; border: 0.2em solid #F27398; border-radius: 50%; height: 100%; position: absolute; top: 0; left: 0%; width: 100%; z-index: 6; } &__nums { background: #F27398; border-radius: 100%; display: block; height: 0.5em; list-style: none; margin: 0; padding: 0; position: absolute; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); width: 0.5em; z-index: 10; } &__num { height: 2.8em; position: absolute; left: 50%; bottom: 0.25em; margin-left: -0.25em; text-align: center; -webkit-transform-origin: center bottom; transform-origin: center bottom; width: 0.5em; z-index: 3; &--1 { -webkit-transform: rotate(30deg); transform: rotate(30deg); } &--2 { -webkit-transform: rotate(60deg); transform: rotate(60deg); } &--3 { -webkit-transform: rotate(90deg); transform: rotate(90deg); } &--4 { -webkit-transform: rotate(120deg); transform: rotate(120deg); } &--5 { -webkit-transform: rotate(150deg); transform: rotate(150deg); } &--6 { -webkit-transform: rotate(180deg); transform: rotate(180deg); } &--7 { -webkit-transform: rotate(210deg); transform: rotate(210deg); } &--8 { -webkit-transform: rotate(240deg); transform: rotate(240deg); } &--9 { -webkit-transform: rotate(270deg); transform: rotate(270deg); } &--10 { -webkit-transform: rotate(300deg); transform: rotate(300deg); } &--11 { -webkit-transform: rotate(330deg); transform: rotate(330deg); } &--12 { -webkit-transform: rotate(360deg); transform: rotate(360deg); } &__text { color: #95a5a6; display: inline-block; font-size: 0.8em; &--1 { -webkit-transform: rotate(-30deg); transform: rotate(-30deg); } &--2 { -webkit-transform: rotate(-60deg); transform: rotate(-60deg); } &--3 { -webkit-transform: rotate(-90deg); transform: rotate(-90deg); } &--4 { -webkit-transform: rotate(-120deg); transform: rotate(-120deg); } &--5 { -webkit-transform: rotate(-150deg); transform: rotate(-150deg); } &--6 { -webkit-transform: rotate(-180deg); transform: rotate(-180deg); } &--7 { -webkit-transform: rotate(-210deg); transform: rotate(-210deg); } &--8 { -webkit-transform: rotate(-240deg); transform: rotate(-240deg); } &--9 { -webkit-transform: rotate(-270deg); transform: rotate(-270deg); } &--10 { -webkit-transform: rotate(-300deg); transform: rotate(-300deg); } &--11 { -webkit-transform: rotate(-330deg); transform: rotate(-330deg); } &--12 { -webkit-transform: rotate(-360deg); transform: rotate(-360deg); } } } &__needles { height: 0.5em; position: absolute; top: 50%; left: 50%; -webkit-transform: translate(-50%, -50%); transform: translate(-50%, -50%); width: 0.5em; z-index: 9; } &__needle { border-radius: 0.08em 0.08em 0 0; height: 2em; margin-left: -0.08em; position: absolute; bottom: 0.25em; left: 50%; -webkit-transform-origin: center bottom; transform-origin: center bottom; width: 0.16em; &--hour { background: #95a5a6; height: 1.4em; } &--min { background: #F27398; } &--sec { background: #aaa; margin-left: -0.04em; width: 0.08em; &:before { background: #aaa; border-radius: 0 0 0.08em 0.08em; content: ''; height: 0.8em; margin-left: -0.04em; position: absolute; top: 100%; left: 50%; width: 0.08em; } } } } .cat { height: 6em; position: absolute; top: 0; left: 0; width: 100%; &__face { background: #bda46b; border-radius: 50%; height: 4em; overflow: hidden; position: absolute; top: -3.5em; left: 0%; width: 6em; z-index: 5; &__pattern { background: #695b3b; position: absolute; &:before, &:after { background: #695b3b; content: ''; position: absolute; } &--top { border-radius: 0 0 50% 50%; height: 1em; margin-left: -0.1em; top: 0; left: 50%; width: 0.2em; &:before { border-radius: 0 0 50% 50%; height: 0.6em; margin-left: -0.6em; top: 0; left: 50%; width: 0.2em; } &:after { border-radius: 0 0 50% 50%; height: 0.6em; margin-right: -0.6em; top: 0; right: 50%; width: 0.2em; } } &--left { border-radius: 0 50% 50% 0; height: 0.2em; margin-top: -0.1em; top: 50%; left: 0; width: 1em; &:before { border-radius: 0 50% 50% 0; height: 0.2em; margin-top: -0.6em; top: 50%; left: 0; width: 0.6em; } &:after { border-radius: 0 50% 50% 0; height: 0.2em; margin-bottom: -0.6em; bottom: 50%; left: 0; width: 0.6em; } } &--right { border-radius: 50% 0 0 50%; height: 0.2em; margin-top: -0.1em; top: 50%; right: 0; width: 1em; &:before { border-radius: 50% 0 0 50%; height: 0.2em; margin-top: -0.6em; top: 50%; right: 0; width: 0.6em; } &:after { border-radius: 50% 0 0 50%; height: 0.2em; margin-bottom: -0.6em; bottom: 50%; right: 0; width: 0.6em; } } } } &__ears { height: 1em; position: absolute; top: -3.5em; left: 0; width: 100%; z-index: -1; &__ear { background: #bda46b; border-radius: 25%; height: 1.8em; position: absolute; top: -0.3em; width: 1.8em; &:before { content: ''; background: #f4e8da; border-radius: 25%; height: 1.4em; margin-top: -0.7em; margin-left: -0.7em; position: absolute; top: 50%; left: 50%; width: 1.4em; } &--left { left: 0.5em; transform: rotate(20deg); } &--right { right: 0.5em; transform: rotate(-20deg); } } } &__eyes { height: 1em; position: absolute; top: 1em; width: 100%; &__eye { background: #333; border: 0.06em solid #333; border-top: 0.12em solid #333; height: 1em; overflow: hidden; position: absolute; bottom: 0; width: 1.3em; &:before { background: #69CC68; content: ''; height: 1em; position: absolute; top: 0; width: 1.2em; } &:after { background: #333; border-radius: 50%; content: ''; height: 1em; margin-top: -0.25em; position: absolute; top: 0em; width: 1em; } span { background: #fff; border-radius: 50%; height: 0.2em; margin-left: -0.1em; margin-top: -0.2em; position: absolute; top: 50%; left: 50%; width: 0.2em; z-index: 1; } &--left { border-radius: 0 70% 0 70%; left: 1.1em; &:before { border-radius: 0 50% 50% 50%; left: 0; } &:after { margin-left: -0.45em; left: 50%; } } &--right { border-radius: 70% 0 70% 0; right: 1.1em; &:before { border-radius: 50% 0 50% 50%; right: 0; } &:after { margin-right: -0.45em; right: 50%; } } } &__eyelid { background: #bda46b; height: 0; position: absolute; left: 0; top: 0; width: 100%; z-index: 10; } } &__nose { border-top: 0.5em solid #F26752; border-left: 0.5em solid transparent; border-right: 0.5em solid transparent; height: 0.5em; margin-left: -0.5em; position: absolute; top: 2.5em; left: 50%; width: 1em; } &__mouth { margin-left: -1em; overflow: hidden; position: absolute; top: 3em; left: 50%; height: 1em; width: 2em; &:before, &:after { border: 0.01em solid #333; border-radius: 50%; content: ''; display: block; height: 0.8em; position: absolute; top: -0.4em; width: 50%; } &:before { margin-left: -0.04em; left: 0; } &:after { margin-right: -0.04em; right: 0; } } &__whiskers { height: 1em; position: absolute; top: -1.5em; left: 0; width: 100%; z-index: 8; &__whisker { height: 1em; position: absolute; top: 0; width: 3em; &:before, &:after, span { border-top: 0.01em solid #eee; height: 0.01em; position: absolute; left: 0; width: 3em; } &--left { margin-left: -1em; left: 0; &:before { content: ''; top: 0; -webkit-transform: rotate(10deg); transform: rotate(10deg); } &:after { content: ''; margin-top: -0.005em; top: 0.5em; } span { bottom: 0; -webkit-transform: rotate(-10deg); transform: rotate(-10deg); } } &--right { margin-right: -1em; right: 0; &:before { content: ''; top: 0; -webkit-transform: rotate(-10deg); transform: rotate(-10deg); } &:after { content: ''; margin-top: -0.005em; top: 0.5em; } span { bottom: 0; -webkit-transform: rotate(10deg); transform: rotate(10deg); } } } } &__hands { height: 1em; position: absolute; top: -0.5em; left: 0; width: 100%; z-index: 9; &__hand { background: #a8925f; border-radius: 50%; height: 1em; overflow: hidden; position: absolute; top: 0.5em; width: 2em; &:before { border-right: 0.08em solid #333; content: ''; position: absolute; bottom: 0; left: 0; height: 0.3em; width: 0.6em; } &:after { border-left: 0.08em solid #333; content: ''; position: absolute; bottom: 0; right: 0; height: 0.3em; width: 0.6em; } &--left { left: 0; -webkit-transform: rotate(-20deg); transform: rotate(-20deg); } &--right { right: 0; -webkit-transform: rotate(20deg); transform: rotate(20deg); } } } &__legs { height: 1em; position: absolute; bottom: 0em; left: 0; width: 100%; z-index: 8; &:after { background: #bda46b; border-radius: 100%; content: ''; height: 0.5em; margin-bottom: -0.25em; position: absolute; bottom: 0em; right: 0; width: 0.5em; z-index: 2; } &__leg { background: #a8925f; border-radius: 50%; height: 1em; overflow: hidden; position: absolute; top: 0; width: 2em; &:before { border-right: 0.08em solid #333; content: ''; position: absolute; top: 0; left: 0; height: 0.3em; width: 0.6em; } &:after { border-left: 0.08em solid #333; content: ''; position: absolute; top: 0; right: 0; height: 0.3em; width: 0.6em; } &--left { left: 0; -webkit-transform: rotate(20deg); transform: rotate(20deg); } &--right { right: 0; -webkit-transform: rotate(-20deg); transform: rotate(-20deg); } } } &__tail { height: 2em; position: absolute; bottom: 0.04em; left: 0; width: 100%; z-index: 0; &__inner { background: #bda46b; border-radius: 0 0 3em 3em; height: 2em; overflow: hidden; position: absolute; top: 2em; left: 3em; width: 3em; &:before { background: #79D1B0; border-radius: 0 0 2em 2em; content: ''; height: 3em; margin-left: -1em; overflow:hidden; position: absolute; top: -1.5em; left: 50%; width: 2em; z-index: 4; } } &__pattern { background: #695b3b; height: 0.2em; position: absolute; width: 0.5em; &:nth-child(odd) { border-radius: 50% 0 0 50%; } &:nth-child(even) { border-radius: 0 50% 50% 0; } &--1 { top: 0.2em; left: 0.1em; } &--2 { top: 0.7em; left: -0.1em; -webkit-transform: rotate(-30deg); transform: rotate(-30deg); } &--3 { top: 1em; left: 0.3em; -webkit-transform: rotate(-50deg); transform: rotate(-50deg); } &--4 { top: 1.5em; left: 0.5em; -webkit-transform: rotate(-60deg); transform: rotate(-60deg); } &--5 { top: 1.5em; left: 1.1em; -webkit-transform: rotate(-80deg); transform: rotate(-80deg); } &--6 { top: 1.7em; left: 1.6em; -webkit-transform: rotate(-100deg); transform: rotate(-100deg); } &--7 { top: 1.2em; left: 2em; -webkit-transform: rotate(-130deg); transform: rotate(-130deg); } &--8 { top: 0.9em; left: 2.6em; -webkit-transform: rotate(-150deg); transform: rotate(-150deg); } &--9 { top: 0.4em; left: 2.4em; -webkit-transform: rotate(-180deg); transform: rotate(-180deg); z-index: 3; } } } } @-webkit-keyframes blink { 0% { height: 0; } 50% { height: 100%; } 100% { height: 0; } } @keyframes blink { 0% { height: 0; } 50% { height: 100%; } 100% { height: 0; } } .blink { -webkit-animation: blink infinite 1s; animation: blink infinite 1s; }

CSSのポイントは以下の通りです。

文字盤の配置方法

文字盤はliを利用して以下の手順を経て配置されます。

1. liの下端を回転軸にする

litransform-origin: center bottom;を指定し、liの下端を回転軸とします。

2. liを中心軸を基準に絶対配置

liを中心軸ulを基準に絶対配置します。

3. liを30度間隔で回転させる

transformrotateliを30度間隔で回転させて、数字spanが円周に沿うようにします。

rotate

4. 数字の傾きを直す

rotateliを回転させると中身のspanも一緒に回転されるため、数字が傾いてしまいます。

なのでspanrotateliとは逆の方向へ回転させて数字の傾きを直します。

猫を描く

猫は画像ではなくCSSでborder-radius等を駆使して描きます。

瞬きのアニメーションを作成

瞬きのアニメーションはCSSで作成します。始めに@keyframesで各キーフレームごと(今回は0%, 50%, 100%)に変化させる要素(今回は瞼.cat__eyes__eyelid)のプロパティ(今回はheight)を指定してキーフレームスを作成します。また、キーフレームスには名前(今回はblink)を付けます。

次に、.blinkクラスにanimationプロパティを記述してその値に先ほどのキーフレームスblinkとアニメーションの回数(今回は永遠に繰り返すinfinite)、アニメーションの時間を指定します。

この.blinkクラスがJavaScriptによって.cat__eyes__eyelidに追加されると猫が瞬きを開始します。

JavaScript(TypeScript)でアナログ時計を機能させる

最後に以下のJavaScriptを書いてアナログ時計を機能させます。

(() => { class Cat { private _eyelids: NodeList; constructor() { //瞼 this._eyelids = document.querySelectorAll('.cat__eyes__eyelid'); } get eyelids(): NodeList { return this._eyelids; } //猫を瞬きさせるメソッド blink() { //.cat__eyes__eyelidに.blinkクラスを追加 this._eyelids.forEach((eyelid: HTMLElement) => eyelid.classList.add('blink')); } } class Needles { private needle; constructor() { //3つの針 this.needle = { sec: document.getElementById('sec'), min: document.getElementById('min'), hour: document.getElementById('hour') } } //3つの針を回転させるメソッド rotate(deg): void { this.needle.sec.style.transform = `rotateZ(${deg.sec}deg)`; this.needle.min.style.transform = `rotateZ(${deg.min}deg)`; this.needle.hour.style.transform = `rotateZ(${deg.hour}deg)`; } } //それぞれの針の振れ幅 const increment = { sec: (360 / 60), min: (360 / 60), hour: (360 / 12) } class Clock { private needles: Needles; private cat: Cat; constructor() { this.needles = new Needles; this.cat = new Cat; //Clockクラスがインスタンス化されるとrunメソッドを実行 this.run(); } //時計を動かすメソッド run(): void { //現在の秒・分・時間を取得 const date: Date = new Date(); const sec: number = date.getSeconds(); const min: number = date.getMinutes(); const hour: number = date.getHours(); //針の角度 const deg = { sec: sec * increment.sec, min: min * increment.min, hour: hour * increment.hour + min * (360 / 12 / 60) } //針を回転させる this.needles.rotate(deg); //runメソッドを繰り返し実行 requestAnimationFrame(this.run.bind(this)); } } //時計を起動 const clock: Clock = new Clock; })();

JavaScriptでは以下のことを行っています。

Catクラスを作成

猫に関するCatクラスを作成します。猫といってもこのクラスは瞼eyelidsと瞬きさせるblinkメソッドのみを持ちます。

Needlesクラスを作成

時計の針に関するNeedlesクラスを作成します。このクラスは秒針sec・分針min・時針hourを持ったオブジェクトneedleと3つの針を回転させるrotateメソッドを持ちます。

針の回転は受け取った角度degrotateZに指定することで行います。

incrementオブジェクトを作成

それぞれの針の振れ幅を持つincrementオブジェクトを作成します。

秒針と分針の振れ幅は1周(360°)を60等分したうちの1つ分なので、360 / 60で6となります。時針の振れ幅は1周(360°)を12等分したうちの1つ分なので、360 / 12で30となります。

Clockクラスを作成

時計全体に関するClockクラスを作成します。このクラスはneedlecatオブジェクトに加え、時計を動かすrunメソッドを持ちます。

runメソッドでは現在の秒sec・分min・時間hourを取得してそれらに振れ幅incrementを掛けることで、1秒・1分間・1時間に進む針の角度degを求めています。

ですがここで一つ注意が必要です。1時間に進む針の角度をそのままhour * increment.hourとしてしまうと、1時間経過直後に針が30°進むため不自然な動きになってしまいます。

これを防ぐために1分経過毎に時針を少しずつ進めて(分刻み)いき、60分が経過すると時針が30°進んでいるようにします。これを実現するには以下の点を押さえて式を作ります。

押さえる点

1. 時針は1時間(60分)で30°(360 / 12)進む。 2. 1分では0.5°(30 / 60)進む。 3. 時針を1分で0.5°進むようにするにはhour * degmin * 0.5を足す。

完成した式

hour * deg + min * (360 / 12) / 60

式に当てはめてみる

//min: 33, hour: 16の場合 16 * 30 = 480deg //1\. 現在の時針の角度 480deg + 33 * (30 / 60) = 496.5deg //2\. 1.から1分後の角度 480deg + 34 * (30 / 60) = 497deg //3\. 2.から20分後の角度 480deg + 54 * (30 / 60) = 507deg(497+0.5 * 20) //4\. 3.から5分後の角度 480deg + 59 * (30 / 60) = 509.5deg(507 + 0.5 * 5) //5\. 4.から1分後(1時間経過)の角度 510deg(17 * 30) + 0(60) * (30 / 60) = 510deg

これで時針が正常に動作します。

次に、前述で取得した角度をneedlesオブジェクトが持つrotateメソッドに渡して針を回転させています。

最後にrequestAnimationFrameメソッドにrunメソッドを渡すことでrunメソッドを繰り返し実行させています。

時計を起動する

最後にClockクラスをインスタンス化すると時計が動き出します。

さいごに

以上で、HTML+CSS+JavaScriptで猫のアナログ時計を作る方法を終わります。

参考文献