Sensitivity of the sketch to mouse coordinates.
sensitivity = 0.02
Base of the iterations of the attractor per frame.
iterations = 8000
Each time the attractor hits a pixel, the density increases by…
density = 2
Start x,y coordinate for the attractor.
start = 0
The number of frames to render before being finished.
limit = 200
Size of the canvas in dimensional and then real pixels.
size = Math.round(document.body.clientWidth - (document.body.clientWidth * 0.3))
N = size * (window.devicePixelRatio or 1)
Create the canvas
element and grab its drawing context
.
canvas = document.createElement 'canvas'
canvas.width = canvas.height = N
canvas.style.width = canvas.style.height = "#{size}px"
canvas.style.marginTop = canvas.style.marginLeft = "-#{ size / 2 }px"
document.body.appendChild canvas
context = canvas.getContext '2d'
Pull some handy math functions off the Math
object, for convenient access.
{round, log, max, sin, cos} = Math
Initialize the mouse coordinates to our start
.
mouseX = mouseY = start
Our Sketch class handles drawing to the <canvas>
element.
class Sketch
constructor: ->
@steps = 0
@stopped = no
@button = document.getElementById 'permalink'
@button.addEventListener 'mousedown', @permalink, no
document.addEventListener 'mousedown', @pause, no
document.addEventListener 'mousemove', @record, no
document.addEventListener 'mouseup', @resume, no
@initialSeed()
@attractor = new DeJongAttractor
@loop()
loop: ->
@interval = setInterval @tick, 0
stopLoop: ->
@interval = clearInterval @interval
Draw a single frame of the sketch.
tick: =>
return @attractor.reseed() if @stopped
@steps += 1
@attractor.plot 5
@stopLoop() if @steps > limit
Determine the initial seed.
initialSeed: ->
if hash = window.location.hash.replace('#', '')
[mouseX, mouseY] = (parseInt(num, 10) for num in hash.split(','))
Pause the drawing, when we’ve rendered enough steps.
pause: (e) =>
e.preventDefault()
@stopped = yes
@loop() unless @interval
Record the current mouse position.
record: (e) =>
if @stopped
mouseX = e.pageX - canvas.offsetLeft
mouseY = e.pageY - canvas.offsetTop
Resume drawing the sketch.
resume: =>
@stopped = no
@steps = 0
Save the permalink of the currently-drawn attractor to the URL.
permalink: (e) =>
e.stopPropagation()
window.location.hash = mouseX + ',' + mouseY
The DeJongAttractor contains the Peter De Jong algorithm.
class DeJongAttractor
constructor: ->
@reseed()
Clear the recorded exposures before seeding at a different location.
clear: ->
@image = context.createImageData N, N
@density = (0 for i in [0...N] for j in [0...N])
@maxDensity = 0
Seed the sketch at the current position of the mouse.
seed: ->
@xSeed = (mouseX * 2 / N - 1) * sensitivity
@ySeed = (mouseY * 2 / N - 1) * sensitivity
[@x, @y] = [N / 2, N / 2]
De Jong’s attractor. Iterates a large number of times through random
coordinates in the attractor space, exposing the @density
array.
populate: (samples) ->
for i in [0...samples * iterations]
x = ((sin(@xSeed * @y) - cos(@ySeed * @x)) * N * 0.2) + N / 2
y = ((sin(-@xSeed * @x) - cos(-@ySeed * @y)) * N * 0.2) + N / 2
@density[round x][round y] += density
[@x, @y] = [x, y]
@maxDensity = log max.apply(Math, (max.apply(Math, row) for row in @density))
reseed: ->
@clear()
@seed()
@plot 3
Soft light color blend between two brighness values.
softLight: (a, b) ->
((a * b) >> 7) + ((a * a) >> 8) - ((a * a * b) >> 15)
Plots each pixel on the canvas as ImageData
, using the @maxDensity
to
adjust for over- or under-exposure.
plot: (samples) ->
@populate samples
data = @image.data
for i in [0...N]
for j in [0...N]
dens = @density[i][j]
idx = (i * N + j) * 4
data[idx + 3] = 255
continue if dens <= 0
light = log(dens) / @maxDensity * 255
current = data[idx]
color = @softLight light, current
data[idx] = data[idx + 1] = data[idx + 2] = color
context.putImageData @image, 0, 0
Kick it off by creating the sketch.
new Sketch()