Skip to content

Instantly share code, notes, and snippets.

@keturn
Created May 25, 2011 22:51
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save keturn/992189 to your computer and use it in GitHub Desktop.
playing with canvas and sierpinski

Canvas Experiment: Sierpinski Triangles

This is just a little doodle for me to get a feel for how the HTML5 canvas element can be used. The canvas is a pixel-based sort of thing, and so maybe not well suited to applications that want scalable elements or independent moving parts; there SVG might be a better choice. But for uses where you want to fiddle with a lot of pixels, where it'd be undesirable to make a new object for every point you manipulate, canvas fits the bill.

Procedurally generated textures or other images, for example. I decided to go with Sierpinski triangles by implementing the Chaos Game.

Here come some bullet points:

  • On my development system (64-bit Ubuntu), Chrome 11.0.696.71 is a full order of magnitude faster than Firefox 4.0.1 or 5.0a2 for this use case. I didn't expect that much difference. It means that even in browsers that implement canvas, what seems okay in one might simply take too long to be useful on another, or trigger an "unresponsive script" error.

  • Once I got the basic thing working, I decided to play with layering one canvas over another. The canvas API doesn't have any notion of layers itself or any method to move sprites around, so the fact that canvases can be transparent and layered lets you do a lot more. That's how the "Show Process" option works in this demo, the image is rendered on one canvas and the guides are drawn on another, absolutely positioned over it.

  • I used the canvas transformation operations to make some of my math easier, but there's no way to way to query a canvas for its current transformation matrix. That makes lining up points on layered canvases a little more work than it might be.

  • As Defender of the favicon has shown to great effect, you can get the image from your canvas as a data URL and set that as your page's icon, so I did that: When the image is done rendering, a scaled-down copy is made and set as the icon. Writing the "type='image/png'" attribute to the icon link turned out to be significant here. Without that, Chrome would set the icon once, but not update it when the content changed.

    (Note you won't be able to see the favicon if you're viewing this in a frame on bl.ocks.org.)

All in all, I think canvas is too low-level to be what I want most of the time, but with all we ask from our web applications these days it's nice to finally have the option to throw some pixels around. ("Can't I just draw a circle around this thing here? I could do it in QBasic!")

<!DOCTYPE html>
<html>
<head>
<title>Canvas Experiments</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.6/underscore-min.js"></script>
<script src="sierpinski.js" type="text/javascript"></script>
<script>
$(document).ready(function () {
$("body").click(function (event) {
var clickTarget = event.target.tagName;
// Trigger if you click more or less anywhere, well, anywhere
// that's not one of the input controls, that is.
if (clickTarget !== "BODY" && clickTarget !== "CANVAS") {
return;
}
var sides = Number($("#nsides").val()),
showProcess = $("#showProcess:checked").val();
FunThings.sierpinski("theCanvas", sides, showProcess).go();
});
});
</script>
<style>
body {background: black;}
div.dash {
position: absolute;
right: 0;
top: 0;
bottom: 0;
color: white;
background: rgba(0,0,0,0);
overflow: auto;
}
div.dash > div {
background: rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.5);
}
div.status span:first-child:before {
content: "> ";
}
div.status span:first-child {
color: #E8FFF0;
}
div.status span {
color: rgba(255,255,255,0.8);
}
</style>
</head>
<body>
<div class="dash">
<div class="controls">
<label for="nsides">Sides:</label>
<input id="nsides" value="3" min="3" max="7" type="range"
onchange="$('#nsidesView').text($(this).val())"
/>
<!-- webkit, you have a range input, but it doesn't show the value? -->
<span id="nsidesView" >3</span>
<br />
<label for="showProcess">Show Process:</label>
<input id="showProcess" type="checkbox" />
</div>
<div class="status"><span>Click the canvas to start.</span>
</div>
</div>
<canvas id="theCanvas" width=500 height=500></canvas>
</body>
</html>
/*jshint browser: true */
/*global FunThings: true, _: false, webkitRequestAnimationFrame */
FunThings = (function () {
var exposed = {}, log;
function getCorners(n) {
var angles, corners,
top = Math.PI / 2;
angles = _.map(_.range(n), function (i) {
return Math.PI * 2 * i / n;
});
corners = _.map(angles, function (r) {
return [Math.cos(r - top), Math.sin(r - top)];
});
return corners;
}
function distance(point1, point2) {
return Math.sqrt(
Math.pow(point1[0] - point2[0], 2) +
Math.pow(point1[1] - point2[1], 2));
}
function drawCircle(c, x, y, size, filled) {
c.beginPath();
c.arc(x, y, size, 0, 2 * Math.PI);
if (filled) {
c.fill();
} else {
c.stroke();
}
}
function newLayer(canvas) {
var $canvas = $(canvas),
$newCanvas = $canvas.clone(),
pos = $canvas.position();
$newCanvas.prop('id', canvas.id + '2');
$newCanvas.css({
position: 'absolute',
top: pos.top,
left: pos.left
});
$canvas.after($newCanvas);
return $newCanvas[0];
}
log = function (msg) {
var $status = $(".status");
$status.prepend("<br />");
$("<span></span>", {text: msg}).prependTo($status);
};
exposed.log = log;
// LET US PLOT SOME SERPINSKI TRIANGLES SMALL CHILD
exposed.sierpinski = function (canvasID, sides, showProcess) {
var c, canvas, corners, xs, ys, current = [0, 0], maxPoints, maxDist;
var canvas2, c2;
var me, pt, targetColor;
canvas = document.getElementById(canvasID);
// resize canvas to viewport
canvas.width = Math.min(window.innerWidth, window.innerHeight) - 32;
canvas.height = canvas.width;
c = canvas.getContext('2d');
// Transform so the canvas is basically the unit square centered on
// the origin. (With a little margin on the edge.)
xs = canvas.width / 2 * 0.95;
ys = canvas.height / 2 * 0.95;
function transformCanvas1(context) {
context.scale(xs, ys);
context.translate(1.05, 1.05);
}
transformCanvas1(c);
// now how big is a pixel?
pt = 1 / xs;
canvas2 = $(canvas).data("overlay");
if (canvas2) {
// easier to always clone a new layer than make sure the old one
// has current attributes.
$(canvas2).remove();
}
if (showProcess) {
canvas2 = newLayer(canvas);
$(canvas).data("overlay", canvas2);
c2 = canvas2.getContext('2d');
// shucks, canvas has no way to get the transformation matrix, so
// we can't clone it in newLayer. The transformations have to be
// re-applied to each canvas.
transformCanvas1(c2);
// check scale and translation by drawing the unit circle
// c2.strokeStyle = "#FFFFFF"; c2.lineWidth = pt;
// drawCircle(c2, 0, 0, 1);
c2.globalAlpha = 0.8;
targetColor = '#FFFFFF';
c2.lineWidth = pt;
}
if (!sides) {
sides = 3;
}
corners = getCorners(sides);
// totally made-up heurestic to say how many points we should draw
// before we say it's done.
maxPoints = Math.floor(canvas.width * canvas.height *
sides * sides / 150);
maxDist = distance(corners[0], corners[1]);
me = {
drawCorners: function () {
c.fillStyle = 'white';
_.each(corners, function (point) {
c.fillRect(point[0], point[1], pt, pt);
});
},
colorByDistance: function(current, next) {
// This says the point color should depend on how far apart
// the last point and the next one are.
var dist, fillColor;
dist = distance(current, next);
fillColor = ("hsla(" + dist / maxDist * 360 + ", 95%, 80%, " +
"0.66)");
return fillColor;
},
colorByTarget: function(current, next, target) {
var hue = target / sides * 360, fillColor;
fillColor = ("hsla(" + hue + ", 96%, 70%, 0.66)");
return fillColor;
},
drawNextPoint: function () {
var target, next, factor=0.5;
var targetIndex = Math.floor(Math.random() * corners.length);
target = corners[targetIndex];
next = [(current[0] + target[0]) * factor,
(current[1] + target[1]) * factor];
c.fillStyle = me.pointColor(current, next, targetIndex);
c.fillRect(next[0], next[1], pt, pt);
if (showProcess) {
me.drawNextOverlay(current, target, next, c.fillStyle);
}
//noinspection ReuseOfLocalVariableJS
current = next;
},
drawNextOverlay: function(current, target, next, color) {
var targetSize = 0.04, nextSize = 0.02;
if (me._dirtyOverlay) {
c2.clearRect.apply(c2, me._dirtyOverlay);
}
me.fillColor = color;
drawCircle(c2, current[0], current[1], nextSize, true);
c2.strokeStyle = targetColor;
// drawCircle(c2, target[0], target[1], targetSize);
c2.beginPath();
c2.moveTo(current[0], current[1]);
c2.lineTo(target[0], target[1]);
c2.stroke();
c2.fillStyle = color;
me._lastOverlayColor = color;
drawCircle(c2, next[0], next[1], nextSize, true);
// geez, computing your own damage rects is tedious.
me._dirtyOverlay = [
Math.min(current[0], target[0]) - targetSize - 2 * pt,
Math.min(current[1], target[1]) - targetSize - 2 * pt,
Math.abs(current[0] - target[0]) + 2 * targetSize + 4 * pt,
Math.abs(current[1] - target[1]) + 2 * targetSize + 4 * pt
];
// c2.strokeRect.apply(c2, me._dirtyOverlay);
// debugger;
},
done: function (points, startTime, stopTime) {
log("Done! " + points + " points in " +
(stopTime - startTime) + "ms.");
me.toFavicon();
},
goIteratively: function () {
// This one does setTimeout between every iteration so you
// can watch it being drawn.
var pointsDrawn = 0, looper, startTime, stopTime;
looper = function () {
if (pointsDrawn < maxPoints) {
me.drawNextPoint();
pointsDrawn++;
//noinspection DynamicallyGeneratedCodeJS
setTimeout(looper, 0);
} else {
stopTime = Date.now();
me.done(pointsDrawn, startTime, stopTime);
}
};
startTime = Date.now();
looper();
},
goInOneLoop: function () {
// Draw all the points in one loop. Doesn't yield control
// until it's done, so it would make things unresponsive if
// it weren't so fast.
var remaining, startTime = Date.now(), stopTime;
for (remaining = maxPoints; remaining > 0; remaining--) {
me.drawNextPoint();
}
stopTime = Date.now();
me.done(maxPoints, startTime, stopTime);
},
goByFrame: function () {
// Use requestAnimationFrame to schedule drawing points.
// This is pretty slow! I guess it probably doesn't make
// sense to either the processor or the eye to update more
// often than this, but it means we really want to do a whole
// batch of drawNextPoint every frame, not just one.
var pointsDrawn = 0,
looper, startTime, stopTime;
looper = function () {
if (pointsDrawn < maxPoints) {
me.drawNextPoint();
pointsDrawn++;
webkitRequestAnimationFrame(looper, canvas);
} else {
stopTime = Date.now();
log("Done! " + pointsDrawn + " points in " +
(stopTime - startTime) + "ms.");
}
};
startTime = Date.now();
looper();
},
go: function (startAt) {
log("Plotting " + sides + "-sided figure.");
if (! startAt) {
current[0] = (Math.random() - 0.5);
current[1] = (Math.random() - 0.5);
}
if (showProcess) {
me.goIteratively();
// me.goByFrame();
} else {
me.goInOneLoop();
}
},
toFavicon: function () {
var icosize = 48,
thumbnailCanvas = document.createElement("canvas"),
tc,
$favlink = $('#favicon'), newlink, icondata;
$(thumbnailCanvas).attr({width: icosize, height: icosize});
tc = thumbnailCanvas.getContext('2d');
// $("body").append(thumbnailCanvas);
tc.drawImage(canvas, 0, 0, icosize, icosize);
icondata = thumbnailCanvas.toDataURL();
if (!$favlink.length) {
//noinspection ReuseOfLocalVariableJS
$favlink = $("<link />", {'rel': 'icon',
'id': 'favicon',
'type': 'image/png',
'href': icondata});
$favlink.appendTo('head');
} else {
newlink = $favlink[0].cloneNode(true);
newlink.setAttribute('href', icondata);
$favlink[0].parentNode.replaceChild(newlink, $favlink[0]);
}
}
};
me.pointColor = me.colorByTarget;
// me.pointColor = me.colorByDistance;
me.drawCorners();
return me;
};
return exposed;
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment