Mouse around to stipple with increasing refinement.
Largely derived from FizzyText (seen here, source here, my attempted explanation here).
<!doctype html>
<html>
<head>
<style>
canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
<title>Particle Painter</title>
<script src='d3.min.js'></script>
<script src='improvedNoise.js'></script>
<script src='main.js'></script>
<script>
window.onload = function() {
var particlePainter = new ParticlePainter('seurat.jpg', d3.select("#starry").node());
};
</script>
</head>
<body>
<div id='starry'></div>
</body>
</html>
// http://mrl.nyu.edu/~perlin/noise/
var ImprovedNoise = function () {
var p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,
23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,
174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,
133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,
89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,
202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,
248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,
178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,
14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,
93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
for ( var i = 0; i < 256 ; i++ ) {
p[ 256 + i ] = p[ i ];
}
function fade( t ) {
return t * t * t * ( t * ( t * 6 - 15 ) + 10 );
}
function lerp( t, a, b ) {
return a + t * ( b - a );
}
function grad( hash, x, y, z ) {
var h = hash & 15;
var u = h < 8 ? x : y, v = h < 4 ? y : h == 12 || h == 14 ? x : z;
return ( ( h & 1 ) == 0 ? u : -u ) + ( ( h & 2 ) == 0 ? v : -v );
}
return {
noise: function ( x, y, z ) {
var floorX = Math.floor( x ), floorY = Math.floor( y ), floorZ = Math.floor( z );
var X = floorX & 255, Y = floorY & 255, Z = floorZ & 255;
x -= floorX;
y -= floorY;
z -= floorZ;
var xMinus1 = x -1, yMinus1 = y - 1, zMinus1 = z - 1;
var u = fade( x ), v = fade( y ), w = fade( z );
var A = p[ X ] + Y, AA = p[ A ] + Z, AB = p[ A + 1 ] + Z, B = p[ X + 1 ] + Y, BA = p[ B ] + Z, BB = p[ B + 1 ] + Z;
return lerp( w, lerp( v, lerp( u, grad( p[ AA ], x, y, z ),
grad( p[ BA ], xMinus1, y, z ) ),
lerp( u, grad( p[ AB ], x, yMinus1, z ),
grad( p[ BB ], xMinus1, yMinus1, z ) ) ),
lerp( v, lerp( u, grad( p[ AA + 1 ], x, y, zMinus1 ),
grad( p[ BA + 1 ], xMinus1, y, z - 1 ) ),
lerp( u, grad( p[ AB + 1 ], x, yMinus1, zMinus1 ),
grad( p[ BB + 1 ], xMinus1, yMinus1, zMinus1 ) ) ) );
}
}
}
var currentRandom = Math.random;
// Pseudo-random generator
function Marsaglia(i1, i2) {
// from http://www.math.uni-bielefeld.de/~sillke/ALGORITHMS/random/marsaglia-c
var z=i1 || 362436069, w= i2 || 521288629;
var nextInt = function() {
z=(36969*(z&65535)+(z>>>16)) & 0xFFFFFFFF;
w=(18000*(w&65535)+(w>>>16)) & 0xFFFFFFFF;
return (((z&0xFFFF)<<16) | (w&0xFFFF)) & 0xFFFFFFFF;
};
this.nextDouble = function() {
var i = nextInt() / 4294967296;
return i < 0 ? 1 + i : i;
};
this.nextInt = nextInt;
}
Marsaglia.createRandomized = function() {
var now = new Date();
return new Marsaglia((now / 60000) & 0xFFFFFFFF, now & 0xFFFFFFFF);
};
// Noise functions and helpers
function PerlinNoise(seed) {
var rnd = seed !== undefined ? new Marsaglia(seed) : Marsaglia.createRandomized();
var i, j;
// http://www.noisemachine.com/talk1/17b.html
// http://mrl.nyu.edu/~perlin/noise/
// generate permutation
var p = new Array(512);
for(i=0;i<256;++i) { p[i] = i; }
for(i=0;i<256;++i) { var t = p[j = rnd.nextInt() & 0xFF]; p[j] = p[i]; p[i] = t; }
// copy to avoid taking mod in p[0];
for(i=0;i<256;++i) { p[i + 256] = p[i]; }
function grad3d(i,x,y,z) {
var h = i & 15; // convert into 12 gradient directions
var u = h<8 ? x : y,
v = h<4 ? y : h===12||h===14 ? x : z;
return ((h&1) === 0 ? u : -u) + ((h&2) === 0 ? v : -v);
}
function grad2d(i,x,y) {
var v = (i & 1) === 0 ? x : y;
return (i&2) === 0 ? -v : v;
}
function grad1d(i,x) {
return (i&1) === 0 ? -x : x;
}
function lerp(t,a,b) { return a + t * (b - a); }
this.noise3d = function(x, y, z) {
var X = Math.floor(x)&255, Y = Math.floor(y)&255, Z = Math.floor(z)&255;
x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z);
var fx = (3-2*x)*x*x, fy = (3-2*y)*y*y, fz = (3-2*z)*z*z;
var p0 = p[X]+Y, p00 = p[p0] + Z, p01 = p[p0 + 1] + Z, p1 = p[X + 1] + Y, p10 = p[p1] + Z, p11 = p[p1 + 1] + Z;
return lerp(fz,
lerp(fy, lerp(fx, grad3d(p[p00], x, y, z), grad3d(p[p10], x-1, y, z)),
lerp(fx, grad3d(p[p01], x, y-1, z), grad3d(p[p11], x-1, y-1,z))),
lerp(fy, lerp(fx, grad3d(p[p00 + 1], x, y, z-1), grad3d(p[p10 + 1], x-1, y, z-1)),
lerp(fx, grad3d(p[p01 + 1], x, y-1, z-1), grad3d(p[p11 + 1], x-1, y-1,z-1))));
};
this.noise2d = function(x, y) {
var X = Math.floor(x)&255, Y = Math.floor(y)&255;
x -= Math.floor(x); y -= Math.floor(y);
var fx = (3-2*x)*x*x, fy = (3-2*y)*y*y;
var p0 = p[X]+Y, p1 = p[X + 1] + Y;
return lerp(fy,
lerp(fx, grad2d(p[p0], x, y), grad2d(p[p1], x-1, y)),
lerp(fx, grad2d(p[p0 + 1], x, y-1), grad2d(p[p1 + 1], x-1, y-1)));
};
this.noise1d = function(x) {
var X = Math.floor(x)&255;
x -= Math.floor(x);
var fx = (3-2*x)*x*x;
return lerp(fx, grad1d(p[X], x), grad1d(p[X+1], x-1));
};
}
function noiser(oct, fall) {
// these are lifted from Processing.js
// processing defaults
var noiseProfile = {
generator: undefined,
octaves: oct || 4,
fallout: fall || .8,
seed: undefined
};
function noise(x, y, z) {
if(noiseProfile.generator === undefined) {
// caching
noiseProfile.generator = new PerlinNoise(noiseProfile.seed);
}
var generator = noiseProfile.generator;
var effect = 1, k = 1, sum = 0;
for(var i=0; i<noiseProfile.octaves; ++i) {
effect *= noiseProfile.fallout;
switch (arguments.length) {
case 1:
sum += effect * (1 + generator.noise1d(k*x))/2; break;
case 2:
sum += effect * (1 + generator.noise2d(k*x, k*y))/2; break;
case 3:
sum += effect * (1 + generator.noise3d(k*x, k*y, k*z))/2; break;
}
k *= 2;
}
return sum;
};
return noise;
}
// TOTAL RIPOFF OF FIZZYTEXT
// https://github.com/dataarts/dat.gui/blob/gh-pages/docs/demo.js
// (still has a ton of vestigial elements thereof)
function ParticlePainter(imgSrc, container) {
var width = 960;
var height = 600;
this.initialR = 5;
this.decaySpeed = .8;
var initialRScale = d3.scale.log()
.domain([1,10000])
.range([500,2])
.clamp(true);
this.noiseStrength = 10; // how turbulent is the flow?
var noiseScale = 600;
var noise = noiser(4,.8);
var initialParticles = 10;
var maxParticles = 2000;
var _this = this;
var mouse = [-1000,-1000];
var rngDeviation = 40;
var rngX = d3.random.normal(mouse[0], rngDeviation);
var rngY = d3.random.normal(mouse[1], rngDeviation);
d3.select(container).on('mousemove', function() {
mouse = d3.mouse(this);
rngX = d3.random.normal(mouse[0], rngDeviation);
rngY = d3.random.normal(mouse[1], rngDeviation);
});
// This is the context we use to get a bitmap of text using
// the getImageData function.
var r = document.createElement('canvas');
var s = r.getContext('2d');
// This is the context we actually use to draw.
var c = document.createElement('canvas');
var g = c.getContext('2d');
// Aaaand sticky drawing background
var c2 = document.createElement('canvas');
var g2 = c2.getContext('2d');
r.setAttribute('width', width);
c.setAttribute('width', width);
c2.setAttribute('width', width);
r.setAttribute('height', height);
c.setAttribute('height', height);
c2.setAttribute('height', height);
// Add our demo to the HTML
// document.getElementById('streakytext').appendChild(r);
container.appendChild(c);
container.appendChild(c2);
// Stores bitmap image
var pixels = [];
// Stores a list of particles
var particles = [];
g.globalAlpha = 1;
g2.globalAlpha = .5;
// Instantiate some particles
for (var i = 0; i < initialParticles; i++) {
particles.push(new Particle(Math.random() * width, Math.random() * height));
}
// This function creates a bitmap of pixels based on your message
// It's called every time we change the message property.
var createBitmap = function (msg) {
var img = new Image(); // Create new img element
img.addEventListener("load", function() {
// execute drawImage statements here
s.drawImage(img,0,0);
var imageData = s.getImageData(0, 0, width, height);
pixels = imageData.data;
d3.timer(render);
}, false);
img.src = msg; // Set source path
// Pull reference
var imageData = s.getImageData(0, 0, width, height);
pixels = imageData.data;
};
// Called once per frame, updates the animation.
var render = function (t) {
g.clearRect(0,0,width,height);
g.globalCompositeOperation = "darken";
g2.globalCompositeOperation = "source-over";
if(particles.length < maxParticles) {
particles.push(new Particle(Math.random() * width, Math.random() * height));
}
for (var i = 0; i < particles.length; i++) {
particles[i].render(t);
}
};
// Returns x, y coordinates for a given index in the pixel array.
var getPosition = function (i) {
return {
x: (i - (width * 4) * Math.floor(i / (width * 4))) / 4,
y: Math.floor(i / (width * 4))
};
};
// Returns a color for a given pixel in the pixel array.
var getColor = function (x, y) {
var base = (Math.floor(y) * width + Math.floor(x)) * 4;
var c = {
r: pixels[base + 0],
g: pixels[base + 1],
b: pixels[base + 2],
a: pixels[base + 3]
};
return "rgb(" + c.r + "," + c.g + "," + c.b + ")";
};
createBitmap(imgSrc);
// This class is responsible for drawing and moving those little
// colored dots.
function Particle(x, y, c) {
// Position
this.x = x;
this.y = y;
// Size of particle
this.r = 0; //_this.initialR * Math.random();
this.dr = /*Math.random() **/ _this.decaySpeed;
this.v = 1;
this.c = undefined;
// Called every frame
this.render = function (t) {
this.r -= this.dr;
if(this.r <= 0) {
this.x = rngX();
this.y = rngY();
// this.r = _this.initialR * Math.random();
this.r = initialRScale(t) * Math.random();
this.dr = /*Math.random() **/ _this.decaySpeed;
this.c = undefined;
}
// this.c = this.c === undefined ? getColor(this.x, this.y) : d3.interpolate(this.c, getColor(this.x, this.y))(.01);
if(this.c === undefined) this.c = getColor(this.x, this.y);
// Where should we move?
var angle = noise(this.x / noiseScale, this.y / noiseScale) * _this.noiseStrength;
// Change our position based on the flow field and our
// explode velocity.
this.x += Math.cos(angle) * this.v;
this.y += -Math.sin(angle) * this.v;
// Draw the streak.
g.fillStyle = this.c;
g.beginPath();
g.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
g.fill();
g2.fillStyle = this.c;
g2.beginPath();
g2.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
g2.fill();
}
}
}
function constrain(v, o1, o2) {
if (v < o1) v = o1;
else if (v > o2) v = o2;
return v;
};