開発部の柳井です。withコロナで大変な状況が続いていますが、弊社ではリモートワークなどのコロナ対策をしながら、サービスを提供し続けています。出社してる時は34インチの曲面ディスプレイを使っていますが、自宅には置く場所が無いため、代わりに13.3インチのモバイルモニタを使っています(macは13インチ)(経費で補助が出ました!)。
さて本題ですが、今回はcanvasで時計を作ります。デジタル時計も後の方でやりますが、時間を取得して数字を表示するだけなので、メインはアナログ時計です。コーディングには前回はCodePenを使いましたが、モブプログラミングの時に知ったRepl.itを使います。ヘッダーなどが無いのですっきりしてます。
canvasとは
canvasはHTML5で追加された要素で、図形を描画することができます。似たような技術にSVGがあります。こちらは<svg>要素の中に<circle>などを書いていきます。実際に自分が携わっている案件でも、データの可視化にD3.jsを使っていますが、今回はcanvasで「円を描く」とか「線を引く」というメソッドを書いていく方法を使います。
今回は、高校の数学Ⅱ(?)でやった、三角関数を使います。忘れた人はこちらのようなサイトで復習してください。
第一段階。真ん中にエディタ、右に実行結果、左にファイル一覧が表示されますが、単体で開くこともできます。
枠
まずは時計の枠として円を描きます。
1 |
<canvas id="canvas" width="400" height="400" style="border: solid;"></canvas> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function draw() { const board = document.querySelector("#canvas"); const canvasWidth = canvasHeight = 400; const context = board.getContext("2d"); // 色 const colorRed = "#FF0000"; const colorBlack = "#000000"; // 枠 const flameR = canvasWidth / 2.5; const flameLineWidth = 3; // 略 // 描画開始 context.clearRect(0, 0, canvasWidth, canvasHeight); // 枠 context.beginPath(); context.strokeStyle = colorBlack; context.lineWidth = flameLineWidth; context.arc(canvasWidth / 2, canvasHeight / 2, flameR, 0, Math.PI * 2, true); context.stroke(); |
canvasでの描画は、描画用のエリアを用意してから、beginPath()→円など図形の点・パス(経路)を指定する→実際にstroke()またはfill()で描くという流れです。draw()では最初にclearRect()でcanvasをクリアします。第一・第二段階ではしていませんが、draw()はタイマーで1秒に1回描画し直すので最初にクリアする必要があります。
円弧を描くarc()では中心のx座標、y座標、半径、何度から何度まで、時計回りor反時計回りを指定します。360度(=2π)なので深く考えずに描けば良いです。詳しくはドキュメントを見てください。座標を指定して、どのくらいの太さ・色で描画するかを指定する方法は、canvas(JavaScript)だけでなくDXライブラリなどグラフィックス描画ライブラリではよくあるパターンなので、慣れると簡単です。
- 矩形(四角形)、円など:canvas に図形を描く - 開発者ガイド | MDN
- 線や塗り潰しの色、太さなど:スタイルと色を適用する - 開発者ガイド | MDN
目盛り
目盛りというのは、1〜12の目盛りと、間にある4本の目盛りです。先ほどは2πを指定しましたが、今度は2π/60ごとに1本の線を引きます。以下の図のように、円の中心から円の縁までの弦の90%程度の点を(A)とし、そこから円の縁(B)まで線を引きます。
線は、ペンをmoveTo()で始点(A)へ移動させ、lineTo()で終点(B)まで線を引くイメージです。(A)や(B)の座標を決めるには、2π/60 * n(n=0〜59の整数)をを使えばOKです。単位円r=1を任意の長さにし、x座標にはcosθ、y座標にはsinθを使いましょう。iはひとまず10までにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function calcRadian(theta) { return (Math.PI * 2) * theta; } function draw() { // 略 // 目盛り for (let i = 0; i < 10; i++) { context.beginPath(); context.strokeStyle = colorBlack; if (i % 5 === 0) { // 5分刻み:長い目盛り context.lineWidth = scaleLongWidth; context.moveTo(canvasWidth / 2 + (Math.cos(calcRadian(i / 60)) * flameR) * scaleLongLength, canvasHeight / 2 + (Math.sin(calcRadian(i / 60)) * flameR) * scaleLongLength); } else { // それ以外:短い目盛り context.lineWidth = scaleShortWidth; context.moveTo(canvasWidth / 2 + (Math.cos(calcRadian(i / 60)) * flameR) * scaleShortLength, canvasHeight / 2 + (Math.sin(calcRadian(i / 60)) * flameR) * scaleShortLength); } context.lineTo(canvasWidth / 2 + (Math.cos(calcRadian(i / 60)) * flameR), canvasHeight / 2 + (Math.sin(calcRadian(i / 60)) * flameR)); context.stroke(); } |
12時から始まると思ったら、3時から始まりましたね?canvasのxy座標系は左上が0で、xは右、yは下へ+になります。また、数学の三角関数は反時計回りなので、それらを考慮すると、calcRadian()を以下のようにすると感覚的に扱えるようになります。
1 |
return (Math.PI * 2) * theta - Math.PI / 2; |
目盛りも、枠と同様に2π一周描くので、どこから開始してどっち周りでも描けますが、ここでラジアンの問題を解決しておくと後が楽です。
時針
針は時針、分針、秒針がありますが、まず時針から描きます。12時間なので、1時間で2π/12動きます。目盛りの描き方を真似すれば余裕ですね!例えば10時16分33分にしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 現在時刻(仮) const hour = 10; const minute = 16; const second = 33; // 略 // 時針(間違い) context.beginPath(); context.strokeStyle = colorBlack; context.lineWidth = handHourWidth; context.moveTo(canvasWidth / 2, canvasHeight / 2); context.lineTo(canvasWidth / 2 + (Math.cos(calcRadian(hour / 12)) * flameR) * handHourLength, canvasHeight / 2 + (Math.sin(calcRadian(hour / 12)) * flameR) * handHourLength); context.stroke(); |
ここから第二段階です。
分針
次は分針。60分なので、1分に2π/60動きます。hour / 12の部分をminute / 60にすると以下のようになります。
なんか違和感ありませんか?10時59分にしてみてください。
11時になると、時針がいきなり2π/12動いてしまいます。時針は正しくは、1時間で2π * (hour + (minute / 60)/12)動くようにしないといけないのでした。もっとちゃんとしたい方はsecondも入れてもらえばいいですが、画面でそんなに見えるものではないのでここでは省いています。
秒針
秒針は60秒なので、1秒に2π/60動きます。これより小さい数字は考えなくていいので、second / 60だけです。色や太さを変えてこんな感じになりました。
1 2 3 4 5 6 7 |
// 秒針 context.beginPath(); context.strokeStyle = colorRed; context.lineWidth = handSecondWidth; context.moveTo(canvasWidth / 2, canvasHeight / 2); context.lineTo(canvasWidth / 2 + (Math.cos(calcRadian(second / 60)) * flameR) * handSecondLength, canvasHeight / 2 + (Math.sin(calcRadian(second / 60)) * flameR) * handSecondLength); context.stroke(); |
ここから第三段階(これで終わり)です。
仕上げ
現在時刻を取得し、タイマーで1秒に1回draw()を実行するようにします。針が中心より反対側にも伸びている(-rとしてhandSecondLength2)などを良い感じに実装して完成です。詳しくはコードを読んでください。
おまけ
デジタル時計も付けてみました。取得した時・分・秒が1桁の場合は"0"の文字列と連結して0埋めをしましょう。デジタルっぽいフォントは、7セグ・14セグフォント 「DSEG」を使用しました。
まとめ
「canvas 時計」で検索するとたくさん記事が公開されています。CSSに凝ってたりするのが面白いです。皆さんも作ってみてください。
キー・ポイントでは、いろいろなことに挑戦する意欲のある方を募集しています。