こんにちは。fujimuraです。
唐突ですが、皆さんはスピログラフというものをご存知でしょうか?
スピログラフという名前でピンときた方は少ないと思いますが、
この絵を見ればわかるのではないでしょうか?

わかりましたか?
ちなみにsawadaとtsukunoはこれを見てもわかりませんでした。
なんというジェネレーションギャップ!!
年齢は tsukuno >> fujimura >>>> sawada のはずなのですが。
仕方がないので、知ってる人の方が少ないということで話を進めます。
この定規、僕が小学生の頃はわりと配布されてたような気がします。
がしかし、この頃使い方を知らなかったので無駄にでかく使いづらい定規という認識しかありませんでした。筆入れに入るわけもなく、無用の長物となっていました。
そんなギャップも重なり、使い方を初めて知った時には衝撃が走りました。
なんとこれ「曲線の幾何学模様を描く為の装置」なのです。
詳しくはこちらをご覧ください。
しかし!しかしですよ。これ以外と難しいのです。
ずーっと綺麗に歯車を回し続けるのは意外と難しいのです。
あくまでも子供の頃の記憶では、ですが。
そこでうまく描けない方や、定規持ってない方にも簡単にスピログラフ定規を
体験して貰おうと、プログラムで作っちゃいました。
| 定円半径: | 動円半径: | ||
| 描画位置: | 色: | ||
|
スピログラフの動き |
描画結果 |
まずは描画開始ボタンを押してみてください。その後は数値や色を変えて、何度も描画してみてください。きっとキレイな幾何学模様が浮かび上がるはずです。
ちなみに、僕が作ったのはこんな感じです。

解説

定規外側の円の半径をrc
歯車の半径を rm
描画点の半径を rd
回転角を θ
とすると、歯車の中心座標は
x1 = (rc – rm) * cosθ
y1 = (rc – rm) * sinθ
で求められ、描画座標は
x2 = x1 + rd * cos((rc – rm) / rm * θ)
y2 = y1 – rd * sin((rc – rm) / rm * θ)
で求められます。
この数式さえ理解してしまえば(理解しなくともプログラムで式が記述できれば)、
あとは回転角を増やし続けその度に線を引いていけば幾何学模様が描画できてしまいます。
詳しくはこちらをご覧ください。>>トロコイド – Wikipedia
※スピログラフは、この内トロコイドの rc > rm > rd の条件下で動作します。
サンプルソース
今回はcanvasを3つ使っています。
①定規部分描画用と②線描画用、見やすくするためもう一つ③線描画用を用意します。
①と②を一つのcanvasにまとめない理由は
①は毎描画毎に一度すべてクリアした後描画するのに対し、
②は毎描画毎にクリアせず、前回の描画結果に線を追加していく為です。
③には②をそのまま描画しています。
※スピログラフに関しては数式さえわかれば後は描画するだけですので、
ソースコードに対する解説は省略します。
var $j = jQuery.noConflict();
$j(function() {
var spirograph = new Spirograph({});
});
function Spirograph(params) {
params = params || {};
var scope = this;
this.run = false;
this.cvRulerId = params.rulerId || 'ruler';
this.cvPaint1Id = params.paint1Id || 'paint1';
this.cvPaint2Id = params.paint2Id || 'paint2';
this.btnPlayId = params.btnPlayId || 'btnPlay';
this.btnChangeId = params.btnChangeId || 'btnChange';
this.btnClearId = params.btnClearId || 'btnClear';
this.inRcId = params.inRcId || 'rc';
this.inRmId = params.inRcId || 'rm';
this.inRdId = params.inRcId || 'rd';
this.inColorId = params.inRcId || 'color';
$j('#'+this.btnPlayId).text('描画開始');
$j('#'+this.btnPlayId).click(function(e) {
if (scope.run) {
scope.stop();
} else {
scope.start();
}
});
$j('#'+this.btnClearId).click(function(e) {
scope.clearSpiroGraph();
});
$j('#'+this.btnChangeId).click(function(e) {
scope.reset();
});
// 角度
this.startDeg = params.deg || 0;
this.deg = this.startDeg;
this.addDeg = params.addDeg || 3;
this.bx = undefined;
this.by = undefined;
this.cvRuler = $j('#'+this.cvRulerId).get(0);
this.cvPaint1 = $j('#'+this.cvPaint1Id).get(0);
this.cvPaint2 = $j('#'+this.cvPaint2Id).get(0);
this.ctxRuler = this.cvRuler.getContext('2d');
this.ctxPaint1 = this.cvPaint1.getContext('2d');
this.ctxPaint2 = this.cvPaint2.getContext('2d');
this.paintData = this.ctxPaint1.getImageData(0, 0, this.cvPaint1.offsetWidth, this.cvPaint1.offsetHeight);
this.rc = 0;
this.rm = 0;
this.rd = 0;
this.color = 0;
this.reset();
}
Spirograph.prototype.start = function() {
if (!this.run) {
this.run = true;
this.draw();
$j('#'+this.btnPlayId).text('描画停止');
}
}
Spirograph.prototype.stop = function() {
this.run = false;
$j('#'+this.btnPlayId).text('描画開始');
}
Spirograph.prototype.degree2radian = function(degree) {
return (Math.PI/180) * degree;
}
Spirograph.prototype.draw = function(){
if (!this.run) {
return;
}
this.draws(this.addDeg);
var scope = this;
requestAnimationFrame(function() { scope.draw(); });
}
Spirograph.prototype.calcRM = function(rc, rm, rd, rad) {
var d = rc - rm;
var x = d * Math.cos(rad) + rd * Math.cos((d / rm) * rad);
var y = d * Math.sin(rad) - rd * Math.sin((d / rm) * rad);
return {x:x, y:y};
}
Spirograph.prototype.draws = function(addDeg) {
var rad = this.degree2radian(this.deg+addDeg);
var crm = this.calcRM(this.rc, this.rm, this.rd, rad);
this.draw1(rad, crm.x, crm.y);
this.draw2(rad, crm.x, crm.y);
this.deg += addDeg;
}
Spirograph.prototype.draw1 = function(rad, x, y) {
// 定規描画
this.ctxRuler.clearRect(0, 0, this.cvRuler.offsetWidth, this.cvRuler.offsetHeight);
var ox = this.cvRuler.offsetWidth * 0.5;
var oy = this.cvRuler.offsetHeight * 0.5;
// 動円
var rmx = (this.rc - this.rm) * Math.cos(rad);
var rmy = (this.rc - this.rm) * Math.sin(rad);
this.ctxRuler.fillStyle = "rgba(0, 0, 0, 0.5)";
this.ctxRuler.strokeStyle = "rgba(0, 0, 0, 0.5)";
this.ctxRuler.beginPath();
this.ctxRuler.arc(rmx+ox, rmy+oy, this.rm, 0, Math.PI*2, false);
this.ctxRuler.fill();
// 線を引く
this.ctxRuler.fillStyle = "rgba(255, 255, 255, 1)";
this.ctxRuler.strokeStyle = "rgba(255, 255, 255, 1)";
this.line(this.ctxRuler, rmx+ox, rmy+oy, x+ox, y+oy);
// 動円中点
this.ctxRuler.fillStyle = "rgba(255, 255, 255, 1)";
this.ctxRuler.strokeStyle = "rgba(255, 255, 255, 1)";
this.ctxRuler.beginPath();
this.ctxRuler.arc(rmx+ox, rmy+oy, 3, 0, Math.PI*2, false);
this.ctxRuler.fill();
// 描画点
this.ctxRuler.fillStyle = this.color;
this.ctxRuler.strokeStyle = this.color;
this.ctxRuler.beginPath();
this.ctxRuler.arc(x+ox, y+oy, 3, 0, Math.PI*2, false);
this.ctxRuler.fill();
// 定円
this.ctxRuler.fillStyle = "rgba(0, 0, 0, 1)";
this.ctxRuler.strokeStyle = "rgba(0, 0, 0, 1)";
this.ctxRuler.beginPath();
this.ctxRuler.arc(ox, oy, this.rc, 0, Math.PI*2, false);
this.ctxRuler.stroke();
}
Spirograph.prototype.draw2 = function(rad, x, y) {
var ox = this.cvPaint1.offsetWidth * 0.5;
var oy = this.cvPaint1.offsetHeight * 0.5;
var d = this.distance(this.bx, this.by, x, y);
this.ctxPaint1.fillStyle = this.color;
this.ctxPaint1.strokeStyle = this.color;
// 線描画
if (d > 1) {
var addDeg = (1 / d);
for (var i = 0, imax = d; i < imax; i++) {
rad = this.degree2radian(this.deg+(addDeg * (i+1)));
var crm = this.calcRM(this.rc, this.rm, this.rd, rad);
this.line(this.ctxPaint1, this.bx+ox, this.by+oy, crm.x+ox, crm.y+oy);
this.bx = crm.x;
this.by = crm.y;
}
}
this.line(this.ctxPaint1, this.bx+ox, this.by+oy, x+ox, y+oy);
this.bx = x;
this.by = y;
// コピー
this.ctxPaint2.putImageData(this.ctxPaint1.getImageData(0, 0, this.cvPaint1.offsetWidth, this.cvPaint1.offsetHeight), 0, 0);
}
Spirograph.prototype.reset = function() {
this.bx = undefined;
this.by = undefined;
this.ctxRuler.clearRect(0, 0, this.cvRuler.offsetWidth, this.cvRuler.offsetHeight);
// 定円の半径
this.rc = this.calcValue(parseFloat($j('#'+this.inRcId).val()), 50, (this.cvRuler.width*0.5));
// 動円の半径
this.rm = this.calcValue(parseFloat($j('#'+this.inRmId).val()), 10, this.rc-10);
// 描画点の半径
this.rd = this.calcValue(parseFloat($j('#'+this.inRdId).val()), 0, this.rm-1);
$j('#'+this.inRcId).val(this.rc);
$j('#'+this.inRmId).val(this.rm);
$j('#'+this.inRdId).val(this.rd);
this.color = $j('#'+this.inColorId).val();
this.draws(0);
}
Spirograph.prototype.clearSpiroGraph = function() {
this.stop();
this.bx = undefined;
this.by = undefined;
this.deg = this.startDeg;
this.ctxRuler.clearRect(0, 0, this.cvRuler.offsetWidth, this.cvRuler.offsetHeight);
this.ctxPaint1.clearRect(0, 0, this.cvPaint1.offsetWidth, this.cvPaint1.offsetHeight);
this.ctxPaint2.clearRect(0, 0, this.cvPaint2.offsetWidth, this.cvPaint2.offsetHeight);
this.draws(0);
}
Spirograph.prototype.calcValue = function(val, min, max) {
if (val < min) {
return min;
}
if (val > max) {
return max;
}
return val;
}
// 描画
Spirograph.prototype.line = function(ctx, x1, y1, x2, y2) {
if (x1 == undefined) { x1 = x2; }
if (y1 == undefined) { y1 = y2; }
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.stroke();
}
Spirograph.prototype.distance = function(x1, y1, x2, y2) {
var a = x1 - x2;
var b = y1 - y2;
return Math.sqrt(Math.pow(a,2) + Math.pow(b,2));
};
この記事を見て、少しでもプログラムに興味を持ってもらえると幸いです。
それではまた。
fujimura