|
/*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; |
|
}()); |