Roll over the text.
Owes a huge debt to George Michael Brower’s FizzyText.
<!doctype html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
}
.text {
padding: 1em;
}
.text p {
margin: 0;
text-indent: 1em;
}
.text p span {
text-indent: 0;
}
canvas {
position: absolute;
top: 0;
left: 0;
z-index: -1;
display: none;
}
</style>
<title>Letter flow</title>
<script src='d3.min.js'></script>
<script src='d3-jetpack.js'></script>
<script src='improvedNoise.js'></script>
</head>
<body>
<div class="text">
<p>A few days later, Banaka turned up in the cafe. Staggering drunk, he fell off a barstool twice before managing to stay on it, order a calvados, and put his head down on the counter. Tamina noticed he was crying.</p>
<p>“What’s the matter, Mr. Banaka?” she asked him. Banaka looked up at her tearfully and pointed to his chest: “I’m nothing, do you understand? I’m nothing! I don’t exist!” Then he went to the toilet and from the toilet straight out into the street, without paying.</p>
<p>When Tamina told Hugo what had happened, he showed her, by way of explanation, a newspaper page with book reviews, among them a sarcastic four-line note on Banaka’s entire output.</p>
<p>The episode of Banaka’s pointing to his chest and crying because he did not exist reminds me of a line from Goethe’s West-East Divan: “Is one alive when other men are living?” Hidden within Goethe’s question is the mystery of the writer’s condition: By writing books, a man turns into a universe (don’t we speak of the universe of Balzac, the universe of Chekhov, the universe of Kafka?), and it is precisely the nature of a universe to be unique. The existence of another universe threatens it in its very essence. Provided their shops are not on the same street, two cobblers can live in perfect harmony. But if they start writing books on the cobbler’s lot, they are soon going to get in each other’s way and ask: “Is a cobbler alive when other cobblers are living?”</p>
<p>Tamina has the impression that a single outsider’s glance can destroy the entire worth of her intimate notebooks, and Goethe is convinced that a single glance of a single human being which fails to fall on lines written by Goethe calls into question Goethe’s very existence. The difference between Tamina and Goethe is the difference between human being and writer.</p>
<!--<p>Someone who writes books is either everything (a unique universe in himself and to all others) or nothing. And because it will never be given to anyone to be everything, all of us who write books are nothing. We are unrecognized, jealous, embittered, and we wish the others dead. In that we are all equals: Banaka, Bibi, I, and Goethe.</p>
<p>The irresistible proliferation of graphomania among politicians, taxi drivers, childbearers, lovers, murderers, thieves, prostitutes, officials, doctors, and patients shows me that everyone without exception bears a potential writer within him, so that the entire human species has good reason to go down into the streets and shout: "We are all writers!"</p>
<p>For everyone is pained by the thought of disappearing, unheard and unseen, into an indifferent universe, and because of that everyone wants, while there is still time, to turn himself into a universe of words.</p>
<p>One morning (and it will be soon), when everyone wakes up as a writer, the age of universal deafness and incomprehension will have arrived.</p> -->
</div>
<script src='fizzy.js'></script>
<script src='main.js'></script>
</body>
</html>
(function() {
function jetpack(d3) {
d3.selection.prototype.translate = function(xy) {
return this.attr('transform', function(d,i) {
return 'translate('+[typeof xy == 'function' ? xy(d,i) : xy]+')';
});
};
d3.transition.prototype.translate = function(xy) {
return this.attr('transform', function(d,i) {
return 'translate('+[typeof xy == 'function' ? xy(d,i) : xy]+')';
});
};
d3.selection.prototype.tspans = function(lines, lh) {
return this.selectAll('tspan')
.data(lines)
.enter()
.append('tspan')
.text(function(d) { return d; })
.attr('x', 0)
.attr('dy', function(d,i) { return i ? lh || 15 : 0; });
};
d3.selection.prototype.append =
d3.selection.enter.prototype.append = function(name) {
var n = d3_parse_attributes(name), s;
//console.log(name, n);
name = n.attr ? n.tag : name;
name = d3_selection_creator(name);
s = this.select(function() {
return this.appendChild(name.apply(this, arguments));
});
return n.attr ? s.attr(n.attr) : s;
};
d3.selection.prototype.insert =
d3.selection.enter.prototype.insert = function(name, before) {
var n = d3_parse_attributes(name), s;
name = n.attr ? n.tag : name;
name = d3_selection_creator(name);
before = d3_selection_selector(before);
s = this.select(function() {
return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
});
return n.attr ? s.attr(n.attr) : s;
};
var d3_parse_attributes_regex = /([\.#])/g;
function d3_parse_attributes(name) {
if (typeof name === "string") {
var attr = {},
parts = name.split(d3_parse_attributes_regex), p;
name = parts.shift();
while ((p = parts.shift())) {
if (p == '.') attr['class'] = attr['class'] ? attr['class'] + ' ' + parts.shift() : parts.shift();
else if (p == '#') attr.id = parts.shift();
}
return attr.id || attr['class'] ? { tag: name, attr: attr } : name;
}
return name;
}
function d3_selection_creator(name) {
return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() {
return this.ownerDocument.createElementNS(name.space, name.local);
} : function() {
return this.ownerDocument.createElementNS(this.namespaceURI, name);
};
}
function d3_selection_selector(selector) {
return typeof selector === "function" ? selector : function() {
return this.querySelector(selector);
};
}
d3.wordwrap = function(line, maxCharactersPerLine) {
var w = line.split(' '),
lines = [],
words = [],
maxChars = maxCharactersPerLine || 40,
l = 0;
w.forEach(function(d) {
if (l+d.length > maxChars) {
lines.push(words.join(' '));
words.length = 0;
l = 0;
}
l += d.length;
words.push(d);
});
if (words.length) {
lines.push(words.join(' '));
}
return lines;
};
d3.ascendingKey = function(key) {
return typeof key == 'function' ? function (a, b) {
return key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : key(a) >= key(b) ? 0 : NaN;
} : function (a, b) {
return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : a[key] >= b[key] ? 0 : NaN;
};
};
d3.descendingKey = function(key) {
return typeof key == 'function' ? function (a, b) {
return key(b) < key(a) ? -1 : key(b) > key(a) ? 1 : key(b) >= key(a) ? 0 : NaN;
} : function (a, b) {
return b[key] < a[key] ? -1 : b[key] > a[key] ? 1 : b[key] >= a[key] ? 0 : NaN;
};
};
d3.f = function(){
var functions = arguments;
//convert all string arguments into field accessors
var i = 0, l = functions.length;
while (i < l) {
if (typeof(functions[i]) === 'string' || typeof(functions[i]) === 'number'){
functions[i] = (function(str){ return function(d){ return d[str] }; })(functions[i])
}
i++;
}
//return composition of functions
return function(d) {
var i=0, l = functions.length;
while (i++ < l) d = functions[i-1].call(this, d);
return d;
};
};
// store d3.f as convenient unicode character function (alt-f on macs)
if (!window.hasOwnProperty('ƒ')) window.ƒ = d3.f;
// this tweak allows setting a listener for multiple events, jquery style
var d3_selection_on = d3.selection.prototype.on;
d3.selection.prototype.on = function(type, listener, capture) {
if (typeof type == 'string' && type.indexOf(' ') > -1) {
type = type.split(' ');
for (var i = 0; i<type.length; i++) {
d3_selection_on.apply(this, [type[i], listener, capture]);
}
} else {
d3_selection_on.apply(this, [type, listener, capture]);
}
return this;
};
// for heaven's sake, let's add prop as alias for property
d3.selection.prototype.prop = d3.selection.prototype.property;
}
if (typeof d3 === 'object' && d3.version) jetpack(d3);
else if (typeof define === 'function' && define.amd) {
define(['d3'], jetpack);
}
})();
function CanvasText(container, message) {
var width = container.offsetWidth;
var height = container.offsetHeight;
// 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');
r.setAttribute('width', width);
r.setAttribute('height', height);
container.appendChild(r);
// Stores bitmap image
var pixels = [];
s.font = "800 180px helvetica, arial, serif";
s.textBaseline = 'middle';
s.textAlign = "center";
// 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, width/2, height/2);
// Pull reference
var imageData = s.getImageData(0, 0, width, height);
pixels = imageData.data;
};
// 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.
this.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(message);
}
// 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;
}
function split(selection) {
selection.each(function(d,i) {
var text = this.innerText.split("");
this.innerHTML = '';
text.forEach(function(letter,j) {
this.innerHTML += "<span>" + letter + "</span>";
}, this);
d3.select(this).selectAll("span")
.each(function(dd,ii) {
var data = {"bb": this.getBoundingClientRect()};
data.x = data.bb.left;
data.y = data.bb.top;
d3.select(this).datum(data);
});
});
}
function positionAbsolutely(selection) {
selection.each(function(d,i) {
d3.select(this)
.style('left', function(d) { return d.x + "px"; })
.style('top', function(d) { return d.y + "px"; })
.style('position', 'absolute');
})
}
var container = d3.select("body");
var text = container.select(".text");
var graf = text.selectAll("p")
.call(split)
.style('height', function(d,i) { return this.getBoundingClientRect().height + 'px'; });
var letters = text.selectAll("span").call(positionAbsolutely);
var canvasText = new CanvasText(container.node(), 'KUNDERA');
text.on("mousemove", function() {
console.log(canvasText.getColor(d3.mouse(this)[0], d3.mouse(this)[1]));
})
var maxX = container.node().offsetWidth;
var maxY = container.node().offsetHeight;
text.on("mouseenter", function() {
var targets = d3.range(letters.size()).map(function(d) {
r = {};
do {
r.x = Math.floor(Math.random() * maxX);
r.y = Math.floor(Math.random() * maxY);
r.c = canvasText.getColor(r.x, r.y);
} while (r.c == "rgb(255,255,255)");
return r;
});
// semi-random sort by distance from top left corner
targets = targets.sort(function(a,b) {
return Math.random()*(Math.pow(a.x,2) + Math.pow(a.y,2)) - Math.random()*(Math.pow(b.x,2) + Math.pow(b.y,2));
})
letters
.transition()
.duration(2000)
.delay(function(d,i) { return 2*i; })
.style('left', function(d,i) { return targets[i].x + "px"; })
.style('top', function(d,i) { return targets[i].y + "px"; });
});
text.on("mouseleave", function() {
letters
.transition()
.duration(1000)
.delay(function(d,i) { return i; })
.style('left', function(d) { return d.x + "px"; })
.style('top', function(d) { return d.y + "px"; });
});