TOTALLY SUPER DUPER NOT MY WORK! Extracted from the dat.GUI docs, where it's buried on the gh-pages branch. The only change I made was removing the dat.GUI dependency by adding the constrain function from dat.GUI. Appears to have been written by George Michael Brower.
/README.md Secret
Last active
December 23, 2023 23:49
FizzyText
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); | |
}; | |
} | |
// these are lifted from Processing.js | |
// processing defaults | |
var noiseProfile = { generator: undefined, octaves: 4, fallout: 0.5, 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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>FizzyText</title> | |
<script src='improvedNoise.js'></script> | |
<script src='main.js'></script> | |
<script> | |
window.onload = function() { | |
var fizzyText = new FizzyText('Fizzy Text!'); | |
}; | |
</script> | |
</head> | |
<body> | |
<div id='fizzytext'></div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function FizzyText(message) { | |
var that = this; | |
// These are the variables that we manipulate with gui-dat. | |
// Notice they're all defined with "this". That makes them public. | |
// Otherwise, gui-dat can't see them. | |
this.growthSpeed = 0.2; // how fast do particles change size? | |
this.maxSize = 5.59; // how big can they get? | |
this.noiseStrength = 10; // how turbulent is the flow? | |
this.speed = 0.4; // how fast do particles move? | |
this.displayOutline = false; // should we draw the message as a stroke? | |
this.framesRendered = 0; | |
// __defineGetter__ and __defineSetter__ makes JavaScript believe that | |
// we've defined a variable 'this.message'. This way, whenever we | |
// change the message variable, we can call some more functions. | |
this.__defineGetter__("message", function () { | |
return message; | |
}); | |
this.__defineSetter__("message", function (m) { | |
message = m; | |
createBitmap(message); | |
}); | |
// We can even add functions to the DAT.GUI! As long as they have | |
// 0 arguments, we can call them from the dat-gui panel. | |
this.explode = function() { | |
var mag = Math.random() * 30 + 30; | |
for (var i in particles) { | |
var angle = Math.random() * Math.PI * 2; | |
particles[i].vx = Math.cos(angle) * mag; | |
particles[i].vy = Math.sin(angle) * mag; | |
} | |
}; | |
//////////////////////////////////////////////////////////////// | |
var _this = this; | |
var width = 550; | |
var height = 200; | |
var textAscent = 101; | |
var textOffsetLeft = 80; | |
var noiseScale = 300; | |
var frameTime = 30; | |
var colors = ["#00aeff", "#0fa954", "#54396e", "#e61d5f"]; | |
// 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'); | |
r.setAttribute('width', width); | |
c.setAttribute('width', width); | |
r.setAttribute('height', height); | |
c.setAttribute('height', height); | |
// Add our demo to the HTML | |
document.getElementById('fizzytext').appendChild(c); | |
// Stores bitmap image | |
var pixels = []; | |
// Stores a list of particles | |
var particles = []; | |
// Set g.font to the same font as the bitmap canvas, incase we | |
// want to draw some outlines. | |
s.font = g.font = "800 82px helvetica, arial, sans-serif"; | |
// Instantiate some particles | |
for (var i = 0; i < 1000; 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) { | |
s.fillStyle = "#fff"; | |
s.fillRect(0, 0, width, height); | |
s.fillStyle = "#222"; | |
s.fillText(msg, textOffsetLeft, textAscent); | |
// Pull reference | |
var imageData = s.getImageData(0, 0, width, height); | |
pixels = imageData.data; | |
}; | |
// Called once per frame, updates the animation. | |
var render = function () { | |
that.framesRendered ++; | |
g.clearRect(0, 0, width, height); | |
if (_this.displayOutline) { | |
g.globalCompositeOperation = "source-over"; | |
g.strokeStyle = "#000"; | |
g.lineWidth = .5; | |
g.strokeText(message, textOffsetLeft, textAscent); | |
} | |
g.globalCompositeOperation = "darker"; | |
for (var i = 0; i < particles.length; i++) { | |
g.fillStyle = colors[i % colors.length]; | |
particles[i].render(); | |
} | |
}; | |
// 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 + ")"; | |
}; | |
// This calls the setter we've defined above, so it also calls | |
// the createBitmap function. | |
this.message = message; | |
var loop = function() { | |
requestAnimationFrame(loop); | |
render(); | |
} | |
// This calls the render function every 30 milliseconds. | |
loop(); | |
// 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 velocity is used by the explode function. | |
this.vx = 0; | |
this.vy = 0; | |
this.constrain = function (v, o1, o2) { | |
if (v < o1) v = o1; | |
else if (v > o2) v = o2; | |
return v; | |
}; | |
// Called every frame | |
this.render = function () { | |
// What color is the pixel we're sitting on top of? | |
var c = getColor(this.x, this.y); | |
// Where should we move? | |
var angle = noise(this.x / noiseScale, this.y / noiseScale) * _this.noiseStrength; | |
// Are we within the boundaries of the image? | |
var onScreen = this.x > 0 && this.x < width && | |
this.y > 0 && this.y < height; | |
var isBlack = c != "rgb(255,255,255)" && onScreen; | |
// If we're on top of a black pixel, grow. | |
// If not, shrink. | |
if (isBlack) { | |
this.r += _this.growthSpeed; | |
} else { | |
this.r -= _this.growthSpeed; | |
} | |
// This velocity is used by the explode function. | |
this.vx *= 0.5; | |
this.vy *= 0.5; | |
// Change our position based on the flow field and our | |
// explode velocity. | |
this.x += Math.cos(angle) * _this.speed + this.vx; | |
this.y += -Math.sin(angle) * _this.speed + this.vy; | |
// this.r = 3; | |
// debugger | |
// console.log(DAT.GUI.constrain(this.r, 0, _this.maxSize)); | |
this.r = this.constrain(this.r, 0, _this.maxSize); | |
// If we're tiny, keep moving around until we find a black | |
// pixel. | |
if (this.r <= 0) { | |
this.x = Math.random() * width; | |
this.y = Math.random() * height; | |
return; // Don't draw! | |
} | |
// Draw the circle. | |
g.beginPath(); | |
g.arc(this.x, this.y, this.r, 0, Math.PI * 2, false); | |
g.fill(); | |
} | |
} | |
} |
Thanks for this.
For anyone interested, I've created a working editable version of this code here
https://editor.sbcode.net/1e64b476676f6f945af94f278a392aba642d135c
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://gist.github.com/tophtucker/3ebb9f8ec9ac77d0273f#file-main-js-L118 is actually not "one of each color" but "one chunk of a color" on top of each other (as seen by current website)