デジタルな針式時計

針のある時計はアナログ式なのか

針のある時計はアナログ式の時計として説明に使われます。でもこのページの時計のようにカクカクと時を刻むタイプの時計はデジタル時計だと思います。

昔、駅のホームには大きな丸い針式の時計がありました。両面に文字盤のある時計です。秒針はなく、1分ごとに長針がピョンと動くのをよく見ていたものです。この駅の時計全部を正確に合わせる方法を聞いたことがあります。それは一台の時計から1分ごとに電気信号を送り、時間を進めるというものでした。一般家庭の時計と言えば振り子式の柱時計でネジを巻いたり時報に合わせたりしていた時代でしたから、その発想にいたく関心したものです。

この記憶があって、針式の時計はアナログ時計であるという説明は「そうとは限らない」とすぐに思いました。

授業で扱うときは、1分待ってられませんから、秒針をつけて1秒ずつ動かすことにしました。1秒ごとに目盛をつけて秒針,分針がピタリと目盛に合っているのが欲しくて今回加えました。制作当時は時間がなくて、秒針がカクカクすることでよしとしていました。

改めて考えると、駅の時計でなくても脱進機が往復運動をして一定の速度で針が回転するようにしているわけで、動かす方式まで考えるとなかなか難しい問題になります。

あくまで「表示」の方式の違いと捉えましょう。といっても針が表示されていたらアナログではなく、針がなめらかに動いて途中が読めればアナログ、カクカクと動いて1秒や1分という単位でしか読めなければデジタルです。

すると、次はなめらかに動く時計を示さなければなりません。本当にアナログな針式時計

時計のプログラム

Javascriptです。

まず、bodyにcanvasを置きます。

<div class="clock">
<canvas id="carea" width="640" height="640"></canvas>
</div>

divに入れているのはセンタリングするためですが、描画自体はcanvasの座標なので、ここはご自由に。

div.clock{
  text-align:center;
}

canvasはhtml5で追加されたタグです。width属性とheight属性で大きさをピクセルで指定しています。指定しなければ初期値は300×150となります。cssなどで拡大縮小してもキャンバスとしての大きさは指定した(または初期値)を拡大縮小するだけという話ですが、後で確認します。

授業でスクリーンに投影する状態に合わせて640✕640にしていますが、ここで指定した値をプログラムで拾うようにしていますので、好みの値にできます。width=height であることを前提にしていますが、違ってもそれなりに動きます。

ブラウザのウィンドウを小さくすると合わせて小さくなります。canvasは640✕640の解像度のままですが、max-width:100%;のcss指定で画像のように縮小されます。特にモバイルでの表示は、320✕320に縮小されます。プログラムには640という値を取得してそれに合わせて描画するほかは、なにも仕掛けをしていません。

idの値はプログラム中の変数値と合わせる必要があります。

javascript本体

htmlファイル内に書いてしまっています。まずは構造だけ

<script type="text/javascript">
function hariclock() {
   //時間を取得してdraw1hari()で針を書き、
   //次の0.1秒後に自分自身を呼ぶようセットします
   setTimeout('hariclock()',100);
}
function draw1hari(sec,l,w,b){
   //1本の針を描きます
}
/* canvとgをグローバル変数にします */
var canv;
var g;
function init(){
  //canvとgに値を入れて時計をスタートします
   hariclock();
}
/* htmlファイルを読み込んだらinit()を実行します */
window.onload = function() {
  init();
}
</script>

init()ではidを頼りにcanvasを認識しますが、htmlが全部読み込まれていないと失敗しますから、onloadで呼び出します。javascriptはvarを付けない変数はグローバルになるので、init()の外のvar宣言はなくても動きますが、グローバルとして意識するために書いています。

時間で繰り返すには、functionの最後にsetTimeout()を書いて自分自身を呼び出すという手法が定番です。100は100ミリ秒=0.1秒です。この繰り返しは理論的には1秒ごとでいいので、1000としてもほとんどOKですが、たまに少しずれて1秒飛ばしたりすることがあったので、0.1にしておきました。秒の値は整数で得られるので、0.1秒ごとに書き換えても針の表示は1秒ごとに変化します。

function hariclock()

0.1秒ごとに繰り返す部分。秒針と文字が重なっているので、文字も毎回書きなおします。

g,canvはグローバル変数。draw1hari()は針を一本描く関数です。

function hariclock() {
  //時間の取得と分解
  var now = new Date();
  var hour = now.getHours();
  var mint = now.getMinutes();
  var sec = now.getSeconds();
  //文字盤 古い針を消して数字を書きなおす
  g.fillStyle   = '#fffff0';
  var x0 = canv.width/2;
  var y0 = canv.height/2;
  g.beginPath();
  var harir = x0*0.96;
  g.arc(x0, y0, harir, 0, Math.PI*2, true);
  //anticlockwise=trueは反時計回りだが一回りするのでどちらでも
  g.closePath();
  g.fill();
  g.fillStyle   = '#999999';
  var fhight = Math.floor(y0/5);
  g.font = fhight + "px 'monospace'";
  g.fillText("3",x0+x0-fhight,y0+fhight/2.5);
  g.fillText("9",fhight/2,y0+fhight/2.5);
  g.fillText("6", x0-fhight/4-fhight/40, y0+y0-fhight/2.5);
  g.fillText("12",x0-fhight/2-fhight/10, fhight+fhight/5);
  //針の大きさ 時針、分針、秒針の順
  var haril = 0.5;
  var hariw = 0.04;
  var harib = 0.1;
  g.fillStyle   = '#000099';
  draw1hari(hour*5+mint/12,haril,hariw,harib);
  haril = 0.8;
  hariw = 0.02;
  harib = 0.1;
  g.fillStyle   = '#0033ff';
  draw1hari(mint,haril,hariw,harib);
  haril = 0.95;
  hariw = 0.01;
  harib = 0.1;
  g.fillStyle   = '#ff0000';
  draw1hari(sec,haril,hariw,harib);
  setTimeout('hariclock()',100);
}

draw1hari()の第一引数は0から60未満の値。秒、分はそのまま使います。時は12時が60になるように5倍した上、分に見合った量だけ加えます。時だけアナログ的に進めることになりますが、これは11時59分まで時針が11を指しているのはさすがに違和感があるからです。

針一本

引数secは0から59未満までの値できます。Math.cos()とMath.sin()は弧度法(ラジアン)での値を使いますから変換をします。60が、2*Math.PIになればいいのですが、角度の基準はx軸からy軸のプラスの方向に測った角度です。0から59未満の0はy軸上になります。加えてcanvasの座標は左上が(0,0)でyが下に増加します。結局0はy軸のマイナス側でになります。時計回りが角度の増加に対応します。

従ってsecが0から59未満まで動くのは、th(thetaの意味)が-π/2から増加して3π/2までということになります。

canvasは正方形なのでx0=y0になるのですが、正方形でない時にも対応するように、それぞれの辺の半分の長さとし、これを基準にしています。第二引数のlはその基準に対してl倍の長さの針にするという意味です。時針は半分、分針は0.8倍、秒針は0.95倍としています。これを元にx1,y1を決めます。

針は回転軸の反対側にも少し長さがありますが、第四引数のbで、lで求めた長さのさらに何割にするかで決めます。ここではどれも共通で0.1倍としています。これでx3,y3を計算します。

第三引数のwは針の回転軸での太さを決める数字です。x1,y1と同様に辺の半分の長さのw倍で計算します。時針は0.04倍、分針は0.02倍、秒針は0.01倍としています。x2,y2,x4,y4を決めます。

針は、(x1,y1),(x2,y2),(x3,y3),(x4,y4)をつないいで、(x1,y1)方向に引き伸ばされた菱形を描いて中を塗りつぶして表現します。

function draw1hari(sec,l,w,b){
   g.beginPath();
   var x0 = canv.width/2;
   var y0 = canv.height/2;
   var th = (sec/30-0.5)*Math.PI;
       // = (sec/60)*2*Math.PI-Math.PI/2
   var x1 = Math.cos(th)*x0*l;
   var y1 = Math.sin(th)*y0*l;
   var x2 = Math.cos(th+Math.PI/2)*x0*w;
   var y2 = Math.sin(th+Math.PI/2)*y0*w;
   var x3 = -x1*b;
   var y3 = -y1*b;
   var x4 = -x2;
   var y4 = -y2;
   g.moveTo(x1+x0,y1+y0);
   g.lineTo(x2+x0,y2+y0);
   g.lineTo(x3+x0,y3+y0);
   g.lineTo(x4+x0,y4+y0);
   g.closePath();
   g.fill();
}

初期化と開始

全体の構造で示したグローバル変数とid="carea"で得るcanvasのオブジェクトと、そのコンテキストgでを代入しておきます。

今回、文字盤の外側の目盛の部分を初期化の中で行うように加えました。いまいち役割分担がすっきりしませんが、我慢します。

var canv;
var g;
function init(){
  canv = document.getElementById('carea');
  g = canv.getContext('2d');
  //文字盤の円と中の塗りつぶし
  g.clearRect( 0, 0, canv.width, canv.height );
  var x0 = canv.width/2;
  var y0 = canv.height/2;
  g.beginPath();
  g.arc(x0, y0, x0-1, 0, Math.PI*2,true);
  //-1 for not clip
  g.closePath();
  g.fillStyle   = '#fffff0';
  g.fill();
  g.strokeStyle = '#000000';
  g.stroke();
  //秒・分の目盛
  var r  = x0*0.98;// -4;
  var dt = Math.PI/30;
  var drlg = x0*0.016;//3;
  var drsm = x0*0.008;
  var dr = drlg;
  g.fillStyle   = '#666666';
  for(var i=0;60>i;i++){
    g.beginPath();
    (i%5==0)?dr=drlg:dr=drsm;
    g.arc(Math.cos(dt*i)*r+x0,
          Math.sin(dt*i)*r+y0,
          dr,0,
          Math.PI*2,
          true);
    g.closePath();
    g.fill();
  }
  hariclock();
}
window.onload = function() {
  init();
}

⇨本当にアナログな針式時計