自分用のプログラムをいろいろ書いてるのなかで、気がついたことを記しています。
SVG(Scalable Vector Graphics)の勉強のため、習作としてアナログ時計を作成したのでメモ。 HTMLファイル全体はこちら。
時計の針は正時を指した状態とし、Javascrpitで回転させることでアナログ時計を実現している。
1. スタイル定義
時計の針などの色の定義。
<style>
body {
font-family:Arial, Helvetica, sans-serif;
padding:1em;
background-image: url(./img/tile.svg);
background-size: 32px 32px;
}
#clock_frame {
fill:white; stroke:gray; stroke-width: 2px;
}
.clock_idx {
fill:silver; stroke:slategray; stroke-width: 0.3px;
}
#clock_hr, #clock_min, #clock_sec {
stroke-width:0.1px;
}
#clock_hr {
fill:silver; stroke:black;
}
#clock_min {
fill:silver; stroke:black;
}
#clock_sec {
fill:red; stroke:none;
}
</style>
2. 時計の枠の定義
時計のサイズは48,48とし、フレームとインデックスをSVGで記述。 インデックスは、transformで、描画領域の真ん中(24,24)を中心に回転させて配置。
<svg xmlns="http://www.w3.org/2000/svg" highit='40vh' width='40vh' viewbox='0 0 48 48'>
<g id='clock'>
<!-- Frame & Index -->
<circle id='clock_frame'cx='24' cy='24' r='20'/>
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate( 0,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate( 30,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate( 60,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate( 90,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(120,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(150,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(180,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(210,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(240,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(270,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(300,24,24)' />
<path class='clock_idx' d='M23.5,6h1v4h-1z' transform='rotate(330,24,24)' />
3. 時計の針の定義
短針・長針・秒針の順に正時を指すように配置。
<!-- Short hand -->
<g id='clock_hr'>
<path d='M23,28h2l-0.3,-15h-1.4z' />
</g>
<!-- Long hand-->
<g id='clock_min'>
<path d='M23.2,28h1.6l-0.2,-20h-1.2z' />
</g>
<!-- Second hand -->
<g id='clock_sec'>
<path d='M23.8,28h0.4l-0.1,-20h-0.2z' />
<path d='M23.5,9 a0.6,0.6 0 1 0 1,0 q-0.5,-1.2 -0.5,-2 q0,0.8 -0.5,2'/>
<circle cx='24' cy='24' r='1'/>
</g>
</g>
</svg>
4. 時計のスクリプト
針を回転させるだけでなく、日時表示などもしている。
<script type="text/javascript">
function clock() {
var cx = 24; var cy = 24;
// 曜日変換のための配列。
var sDoW = ['San','Mon','Tue','Wed','Thu','Fri','Sat'];
// 現在の時刻などを取得
var now = new Date();
var msec = now.getMilliseconds();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
var yr = now.getFullYear();
var mth = now.getMonth()+1;
var dy = now.getDate();
// 時計の針のobjectを取得
var g_hr = document.getElementById('clock_hr');
var g_min = document.getElementById('clock_min');
var g_sec = document.getElementById('clock_sec');
// 必ずしも00秒でコールされる訳ではないので、針の描画は、0.1秒毎で実施。
if (msec > 100) return;
// 滑らかに秒針を描画するのであれは、下記の行を生かす。
// sec += msec/1000;
// transformで針を回転させる。
g_sec.setAttribute('transform','rotate('+(sec*6)+','+cx+','+cy+')');
g_min.setAttribute('transform','rotate('+(min*6+sec/10)+','+cx+','+cy+')');
g_hr.setAttribute('transform','rotate('+(hr*30+min/2+sec/120)+','+cx+','+cy+')');
// デジタル表示部
document.getElementById('txt_yr').innerHTML = yr;
document.getElementById('txt_mth').innerHTML = mth < 10 ? '0' + mth : mth;
document.getElementById('txt_dy').innerHTML = dy < 10 ? '0' + dy : dy;
document.getElementById('txt_dw').innerHTML = sDoW[now.getDay()];
document.getElementById('txt_h').innerHTML = hr < 10 ? '0' + hr : hr;
document.getElementById('txt_m').innerHTML = min < 10 ? '0' + min : min;
document.getElementById('txt_s').innerHTML = sec < 10 ? '0' + sec : sec;
}
// 上で定義した関数を50ms毎に呼び出す。
window.setInterval(clock,50);
</script>
数式ですべてが描けるSVGは自分の性にあっている気がする。
Internet Explorer 9がWindows Updateが入ったので、試してみたら問題なく動作した。 回転はのIE9方が早い。
まずは、javascriptを別ファイルに分離。高速化の基本だね。 全て分離するつもりだったけど、GETでの引数引き渡しは、分離したjavascriptに埋め込む事がちょっと面倒だったので、メインページに埋め込み。 AJAX化するつもりだったが、あまりレイアウト的に使いやすくならななそうなので、現状のままかもしれない。
構成としては、dir-lister.phpで特定のディレクトリの画像一覧を取得して、その際にリンクとしてphoto-viewer.phpを呼び出し、画面表示を行う。 まずは、画像ビューアの方から。
<!--photo-viewer.php -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="ja" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<link rel="STYLESHEET" type="text/css" href="/css/photo-index.css" title="photo frame" />
<title>Photo Image Viewer</title>
<!--[if IE]><script type="text/javascript" src="/common/js/excanvas.js"></script><![endif]-->
<script type="text/javascript" src="/common/js/photo-viewer.js"></script>
<script type="text/javascript">
var photo = document.createElement('img');
photo.src = <?php echo '"'.$_GET["imgName"].'"' ?> +"?" + new Date().getTime(); // AJAX化したときは、EXIF情報で埋め込み
photoInfo.width = <?php echo $_GET["imgWidth"] ?>; // ditto.
photoInfo.height = <?php echo $_GET["imgHeight"] ?>; // ditto.
</script>
<style>
</style>
</head>
<body>
<div class="main_frame">
<canvas id="DrawField" width="600" height="600" class="pframe"></canvas>
</div>
<div class="sidebar">
<p>
<form id='imgCtrl'>
<p>
<input class="imgCtrlButton" id= "Anticlockwise" type=button value="左回転" onClick="Rotate(90,-1)" />
<input class="imgCtrlButton" id= "Clockwise" type=button value="右回転" onClick="Rotate(90,1)" /> <br />
<input class="imgCtrlButton" id= "Shrink" type=button value="縮小" onClick="ResizeView(-1)" />
<input class="imgCtrlButton" id= "Expand" type=button value="拡大" onClick="ResizeView(1)" /> <br />
<input class="imgCtrlButton" id= "Reset" type=button value="リセット" onClick="ResetView()" />
</p><p align='center'>キャンバスサイズ :
<select id= "CanvasSize" type=select onChange="changeCanvasSize(this.options[this.selectedIndex].value)" >
<option value="480">480</option>
<option value="600">600</option>
<option value="840">840</option>
<option value="1024">1024</option>
<option value="1200">1200</option>
<option value="1440">1440</option>
</select>
</form>
</p>
<p align="center">
<img id='thumbnail' src="<?php echo preg_replace("|^/photo/|","/photo/thumbnail/",$_GET["imgName"]); ?>" />
</p>
<p id="debug_info1"></p>
</div>
</body>
</html>
上記が、メインのページ。cgi的に動くため、引数として、imgName,imsWidth,imgHeightをとる。imgNameは、URLになります。
javascriptが下のほう。キャンバスのサイズ変更とか、その情報をcookieで保管するとか、マウスのドラッグで画面を移動するとかいろいろ修正しています。
// photo-vewer.js
var myCanvas;
var ConMode;
var debugInfo;
var zoomLevel = new Array(1, 1.4, 2, 2.8, 4, 5.6, 8, 11.2, 16);
var zoomCurrent = 0;
var angle = 0;
var canvasSize = 600;
var photoInfo = Object();
var viewInfo = Object();
var cookies = new Array();
var mbStatus = false;
var prvX, prvY;
//ページがロードされてから必要な初期処理を行う
onload = function() {
//キャンバスオブジェクトへの参照をセット
myCanvas = document.getElementById("DrawField");
myCanvas.onmousedown = mouseDownListener; // mouseDown event listener関数を設定
myCanvas.onmouseup = mouseUpListener; // mouseUp event listener関数を設定
myCanvas.onmouseout = mouseOutListener; // mouseOut event listener関数を設定
myCanvas.onmousemove = mouseMoveListener; // mouseMove event listener関数を設定
debugInfo = document.getElementById("debug_info1"); // debug用のメッセージを表示するため
var selectCanvasSize = document.getElementById("CanvasSize"); // 選択しているサイズと同期するため。
//2次元描画用コンテキストを設定
ConMode = myCanvas.getContext('2d');
getCookie();
if ("canvasSize" in cookies) {
canvasSize = cookies["canvasSize"]; // cookieにcanvasのサイズが指定され低場合はその値を利用。
}
changeCanvasSize(cookies["canvasSize"]);
// HTML側のselectboxを変更。
for ( var i=0, items=selectCanvasSize.options.length; i < items; i++ ) {
if ( selectCanvasSize.options[i].value == canvasSize ) {
selectCanvasSize.options[i].selected = true;
break;
}
}
};
// Cookieの設定関数
function setCookie() {
var expire = 1000; // expireするまでの日数
var exDate = new Date();
var cookie_value = "";
exDate.setHours(exDate.getHours() + expire);
exDate = exDate.toGMTString(); // GMT化。
for ( idx in cookies ) { // cookiesの連想配列に入れたものを全て書き出す。
cookie_value = idx + "=" + cookies[idx] +";";
}
cookie_value = cookie_value + "expires=" + exDate;
// debugInfo.innerText = "Cookie : " + cookie_value; // debug時にcooikeの内容を表示したい場合は、戻す。
document.cookie = cookie_value;
}
// Cookieの読出関数
function getCookie() {
var cookie_value = new Array();
cookie_value = document.cookie.split(";"); // セパレータで切り出す。
for ( var i=0, end = cookie_value.length ; i < end ; i++ ) {
var idx = cookie_value[i].indexOf("=");
var key = cookie_value[i].substring(0,idx);
var val = cookie_value[i].substring(idx+1);
key = key.replace(/^\s+|\s+$/g, ""); // keyの前後のスペースを削除(trim関数相当)
val = val.replace(/^\s+|\s+$/g, ""); // valの・・
cookies[key]=val;
}
}
// マウスドラッグで画像移動
function mouseMoveListener(e) {
//ここにあまり重い処理を書くべきではないとは思うが・・・
if ( mbStatus ) { // マウスボタンが押されていたら実行。
// 移動量を計算
var moveX =prvX - e.clientX; // 相対座標なので、色々な変換不要。
var moveY =prvY - e.clientY; // 同じくY座標も。
// 保管
prvX = e.clientX;
prvY = e.clientY;
// 画像が回転している場合は、方向を修正。
var offsetX = ( moveX*Math.cos(Math.PI*angle/180) + moveY*Math.sin(Math.PI*angle/180)) / viewInfo.width * photoInfo.viewWidth;
var offsetY = ( moveY*Math.cos(Math.PI*angle/180) - moveX*Math.sin(Math.PI*angle/180)) / viewInfo.height * photoInfo.viewHeight;
// 表示位置の変更のため、offsetを計算
photoInfo.offsetX = photoInfo.offsetX + offsetX;
photoInfo.offsetY = photoInfo.offsetY + offsetY;
if ( photoInfo.offsetX < 0 ) photoInfo.offsetX = 0; if ( photoInfo.offsetY < 0 ) photoInfo.offsetY = 0;
if ( photoInfo.offsetX + photoInfo.viewWidth > photoInfo.width ) photoInfo.offsetX = photoInfo.width - photoInfo.viewWidth;
if ( photoInfo.offsetY + photoInfo.viewHeight > photoInfo.height) photoInfo.offsetY = photoInfo.height - photoInfo.viewHeight;
//表示された画像を消す。多少範囲を大きめに消さないと、ごみが残る。
ConMode.clearRect(-myCanvas.width/2-5, -myCanvas.height/2-5 ,myCanvas.width+5, myCanvas.height+5);
// 表示
ConMode.drawImage(photo,photoInfo.offsetX,photoInfo.offsetY,photoInfo.viewWidth,photoInfo.viewHeight,
viewInfo.offsetX,viewInfo.offsetY,viewInfo.width,viewInfo.height);
}
}
// マウスドラッグで画像を移動できるように、マウスボタンのon,offを管理。
function mouseDownListener(e) {
mbStatus = true;
// マウスボタンが押されたときの座標を保管
prvX =e.clientX;
prvY =e.clientY;
myCanvas.style.cursor = "hand";
}
// マウスボタンがはなされた時のイベントハンドラ。
function mouseUpListener(e) {
mbStatus = false;
}
// マウスフォーカスが外れた時のイベントハンドラ
function mouseOutListener(e) {
mbStatus = false; // フォーカス外では、mouseUpイベントがとれないため。
}
// 画像回転。回転させた角度を保管する処理。その後はRoteteView()をそのまま呼びだす。
function Rotate(rot,direction) {
angle = (angle + rot * direction) % 360;
RotateView(rot,direction);
}
// 回転を行う処理。回転中を表現するため、タイマーで再帰的に繰り返す。
function RotateView(rot,direction){
if (rot == 0) {
return;
} else {
//表示された画像を消す。多少範囲を大きめに消さないと、ごみが残る。
ConMode.clearRect(-myCanvas.width/2-5, -myCanvas.height/2-5 ,myCanvas.width+5, myCanvas.height+5);
//座標を回転させる
ConMode.rotate(5 * direction * Math.PI / 180);
//画像の中心がキャンバス座標の中心点にくるように画像を表示する
ConMode.drawImage(photo,photoInfo.offsetX,photoInfo.offsetY,photoInfo.viewWidth,photoInfo.viewHeight,
viewInfo.offsetX,viewInfo.offsetY,viewInfo.width,viewInfo.height);
//10ミリ秒ごとに、5度づつ座標の回転と画像の描画を繰り返す
if ( rot > 0 ) {
rot = rot - 5
} else {
rot + 5;
}
setTimeout(function(){RotateView(rot,direction)},10);
}
}
// 画像を拡大・縮小表示する処理
function ResizeView(zoom){
// 現在の状態を退避。
var viewWidth = photoInfo.viewWidth;
var viewHeight = photoInfo.viewHeight;
//表示された画像を消す。多少範囲を大きめに消さないと、ごみが残る。
ConMode.clearRect(-myCanvas.width/2-5, -myCanvas.height/2-5 ,myCanvas.width+5, myCanvas.height+5);
zoomCurrent += zoom;
if ( zoomCurrent < 0 || zoomCurrent >= zoomLevel.length ) {
zoomCurrent -= zoom;
}
//画像の大きさを変更する。
photoInfo.viewWidth = photoInfo.width / zoomLevel[zoomCurrent];
photoInfo.viewHeight = photoInfo.height / zoomLevel[zoomCurrent];
//表示画像の中心から拡大するように、オフセットを計算。
photoInfo.offsetX = photoInfo.offsetX + ( viewWidth - photoInfo.viewWidth )/2;
photoInfo.offsetY = photoInfo.offsetY + ( viewHeight - photoInfo.viewHeight)/2;
if ( photoInfo.offsetX < 0 ) photoInfo.offsetX = 0; if ( photoInfo.offsetY < 0 ) photoInfo.offsetY = 0;
if ( photoInfo.offsetX + photoInfo.viewWidth > photoInfo.width ) photoInfo.offsetX = photoInfo.width - photoInfo.viewWidth;
if ( photoInfo.offsetY + photoInfo.viewHeight > photoInfo.height) photoInfo.offsetY = photoInfo.height - photoInfo.viewHeight;
ConMode.drawImage(photo,photoInfo.offsetX,photoInfo.offsetY,photoInfo.viewWidth,photoInfo.viewHeight,
viewInfo.offsetX,viewInfo.offsetY,viewInfo.width,viewInfo.height);
}
// 画像をリセットする
function ResetView(){
//色々なパラメータをもとに戻す
photoInfo.offsetX = 0; photoInfo.offsetY = 0;
photoInfo.viewWidth = photoInfo.width;
photoInfo.viewHeight = photoInfo.height;
if ( photoInfo.width > photoInfo.height ) {
viewInfo.width = canvasSize; // canvas上に表示するイメージの幅
viewInfo.height = canvasSize/photoInfo.width*photoInfo.height; // canvas上に表示するイメージの高さ
} else {
viewInfo.height = canvasSize; // canvas上に表示するイメージの高さ
viewInfo.width = canvasSize*photoInfo.width/photoInfo.height; // canvas上に表示するイメージの幅
}
zoomCurrent = 0;
angle = 0;
// canvasの中心で回転させたいので座標系を移動。
viewInfo.offsetX = -viewInfo.width/2; viewInfo.offsetY = -viewInfo.height/2;
//表示された画像を消す。多少範囲を大きめに消さないと、ごみが残る。
ConMode.clearRect(-myCanvas.width/2-5, -myCanvas.height/2-5 ,myCanvas.width+5, myCanvas.height+5);
//変換マトリックスをリセットする。
ConMode.setTransform(1,0,0,1,myCanvas.width/2 ,myCanvas.height/2);
//画像の中心がキャンバス座標の中心点にくるように画像を表示する
ConMode.drawImage(photo,photoInfo.offsetX,photoInfo.offsetY,photoInfo.viewWidth,photoInfo.viewHeight,
viewInfo.offsetX,viewInfo.offsetY,viewInfo.width,viewInfo.height);
}
// キャンバスサイズが変更された場合の処理。initもこれを呼び出すように集約。引数は、HTML側から呼び出す時に必要。
function changeCanvasSize(newCanvasSize) {
var myCanvasRect;
canvasSize = newCanvasSize;
myCanvas.setAttribute('width', canvasSize);
myCanvas.setAttribute('height', canvasSize);
myCanvasRect = myCanvas.getBoundingClientRect(); // Canvas objectのサイズ
cookies["canvasSize"] = canvasSize;
setCookie();
ResetView();
}