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.
license: mit |
<!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> |