Skip to content

Instantly share code, notes, and snippets.

@mootari
Last active November 23, 2016 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mootari/dc87bce787fc891577acbb4ebf63f83b to your computer and use it in GitHub Desktop.
Save mootari/dc87bce787fc891577acbb4ebf63f83b to your computer and use it in GitHub Desktop.
Squaring the circle, part 2
license: mit

Experiment in creating regular grids from circular paths, step 2: Deriving and plotting a function.

If you own a high resolution display be sure to increase the Quality setting.

<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
<script>(function() {
function polySine(sides, sidesOffset, angle) {
// Properly handles negative values.
function mod(v, n) {
return ((v % n) + n) % n;
}
var f = Math.PI * 2 / sides;
var n = angle / f;
var prev = sidesOffset + f * Math.floor(n);
var next = sidesOffset + f * Math.floor(n + 1);
var ratio = mod(n, 1);
var sPrev = ratio === 1 ? 0 : Math.sin(prev) * (1 - ratio);
var sNext = ratio === 0 ? 0 : Math.sin(next) * ratio;
return sPrev + sNext;
}
var config = {
sides: 3,
// Angle step increment.
interval: .005,
radius: 1,
canvasSize: 500,
// Canvas resolution (think Retina).
quality: 1,
// Lines per run interval.
updateInterval: 10,
// Run interval, not real fps.
fps: 30,
// Rotation speed.
rotate: 0,
drawDebug: false,
style: {
// Line width.
width: 1,
// Line opacity.
opacity: 1
},
noise: {
// Mask size in relation to target canvas.
size: 2,
// Amount of noise applied at each step.
ratio: .02,
// Noise opacity. Low values will lead to rounding errors.
opacity: .3
}
};
var w, h, wh, hh;
var ctx = {
live: document.createElement('canvas').getContext('2d'),
buffer: document.createElement('canvas').getContext('2d'),
debug: document.createElement('canvas').getContext('2d')
};
document.getElementById('app').appendChild(ctx.live.canvas);
var stats = {
timer: 0,
rotation: 0,
playing: true,
autoClear: false
};
var noiseMask;
updateCanvasSize();
gui();
play();
function updateCanvasSize() {
w = config.canvasSize * config.quality;
h = config.canvasSize * config.quality;
wh = w / 2;
hh = h / 2;
ctx.live.canvas.width = ctx.buffer.canvas.width = ctx.debug.canvas.width = w;
ctx.live.canvas.height = ctx.buffer.canvas.height = ctx.debug.canvas.height = h;
ctx.live.canvas.style.width = (w / config.quality) + 'px';
ctx.live.canvas.style.height = (h / config.quality) + 'px';
ctx.buffer.fillStyle = 'white';
ctx.buffer.fillRect(0, 0, w, h);
updateRenderStyle();
updateNoiseMask();
}
function updateNoiseMask() {
noiseMask = config.noise.ratio
? createNoiseMask(ctx.buffer, config.noise.ratio, config.noise.size, config.noise.opacity)
: null;
}
function createNoiseMask(ctx, ratio, scale, alpha) {
function arrayToHex(arr) {
function isLittleEndian() {
// Function source:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
var buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
}
if(isLittleEndian()) {
arr = arr.slice().reverse();
}
return arr.reduce(function(sum, val, i, arr) {
// Right shift converts signed to unsigned.
return sum | (val << (arr.length - 1 - i) * 8) >>> 0;
}, 0);
}
var canvas = document.createElement('canvas');
canvas.width = ctx.canvas.width * scale;
canvas.height = ctx.canvas.height * scale;
var localCtx = canvas.getContext('2d');
var imgData = localCtx.createImageData(canvas.width, canvas.height);
var buf32 = new Uint32Array(imgData.data.buffer);
var color = arrayToHex([255, 255, 255, ~~(255 * alpha)]);
for(var i = 0; i < buf32.length; i++) {
if(Math.random() < ratio) {
buf32[i] = color;
}
}
localCtx.putImageData(imgData, 0, 0);
return function(ctx) {
var w = ctx.canvas.width;
var h = ctx.canvas.height;
var x = ~~(Math.random() * (canvas.width - w));
var y = ~~(Math.random() * (canvas.height - h));
ctx.drawImage(canvas, x, y, w, h, 0, 0, w, h);
}
}
function updateRenderStyle() {
ctx.buffer.lineWidth = config.style.width * config.quality;
ctx.buffer.strokeStyle = 'rgba(0,0,0,' + config.style.opacity + ')';
ctx.debug.lineWidth = 3 * config.quality;
ctx.debug.fillStyle = 'rgb(255,0,0)';
ctx.debug.strokeStyle = 'rgb(255,0,0)';
}
function gui() {
function autoClear() {
if(stats.autoClear) {
updateCanvasSize();
}
}
var ui = new dat.GUI();
ui.add(config, 'sides').min(3).max(10).step(1).onChange(autoClear);
ui.add(config, 'interval').min(.001).max(.5).step(.001);
ui.add(config, 'rotate').min(-.05).max(.05).step(.001);
ui.add(stats, 'playing');
var fDraw = ui.addFolder('Drawing');
fDraw.open();
fDraw.add(config, 'radius').min(.1).max(1).step(.01);
fDraw.add(config.style, 'width').min(.1).max(5).step(.1).onChange(updateRenderStyle);
fDraw.add(config.style, 'opacity').min(.01).max(1).step(.01).onChange(updateRenderStyle);
fDraw.add(config.noise, 'ratio').name('fade').min(0).max(.15).step(.001).onFinishChange(updateNoiseMask);
fDraw.add(config, 'drawDebug');
// fDraw.add(stats, 'autoClear');
fDraw.add({clear: updateCanvasSize}, 'clear');
var fPerf = ui.addFolder('Performance');
fPerf.open();
fPerf.add(config, 'canvasSize').min(100).max(2000).step(100).onFinishChange(updateCanvasSize);
fPerf.add(config, 'updateInterval').min(1).max(100).step(1);
fPerf.add(config, 'fps').min(1).max(60).step(1);
fPerf.add(config, 'quality').min(.25).max(3).step(.25).onFinishChange(updateCanvasSize);
}
function render(ctx) {
var offset = Math.PI / 2 + Math.PI / config.sides + stats.rotation;
var radius = config.radius * Math.min(wh, hh);
x = radius * polySine(config.sides, offset + Math.PI / 2, stats.timer);
y = radius * polySine(config.sides, offset, stats.timer);
ctx.beginPath();
ctx.moveTo(wh, hh);
ctx.lineTo(wh + x, hh + y);
ctx.stroke();
}
function renderDebug(ctx, clear) {
var offset = Math.PI / 2 + Math.PI / config.sides + stats.rotation;
var radius = config.radius * Math.min(wh, hh);
clear && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
var x = radius * Math.cos(stats.timer + offset);
var y = radius * Math.sin(stats.timer + offset);
ctx.beginPath();
ctx.moveTo(wh, hh);
ctx.lineTo(wh + x, hh + y);
ctx.stroke();
var step = Math.PI * 2 / config.sides;
var dotRadius = 8 / 2 * config.quality;
var piHalf = Math.PI / 2, piDouble = Math.PI * 2;
for(var i = 0; i < config.sides; i++) {
x = radius * polySine(config.sides, offset + piHalf, i * step);
y = radius * polySine(config.sides, offset, i * step);
ctx.beginPath();
ctx.arc(wh + x, hh + y, dotRadius, 0, piDouble);
ctx.fill();
}
}
function play() {
if(stats.playing) {
for(var i = 0; i < config.updateInterval; i++) {
stats.timer += config.interval - config.rotate;
stats.rotation += config.rotate;
noiseMask && noiseMask(ctx.buffer);
render(ctx.buffer, config.drawRefLine);
}
ctx.live.drawImage(ctx.buffer.canvas, 0, 0);
if(config.drawDebug) {
renderDebug(ctx.debug, true);
ctx.live.drawImage(ctx.debug.canvas, 0, 0);
}
}
setTimeout(play, 1000 / config.fps);
}
}());</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment