block by armollica 06a202c9f7df191ace8a1f97e33ffb97

Slimy Text

Full Screen

Move your mouse to slosh the text around.

The SVG filter comes from Nadieh Bremer’s great blog post about making gooey effects.

The idea of drawing text on a <canvas> element and using the bitmap to identify points within the text came from Toph Tucker’s block that reverse-engineers FizzyText.

index.html

<html>
<head>
<style>
circle { fill: #99cc00; }
circle.mouse { fill: none; }
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

var text = "Slime";

var width = 960,
		height = 500,
		radius = 5,
		collisionStrength = 0.1;

var options = {
	width: width,
	height: height,
	spacing: 10,
	fontSize: "250px"
};

// Rasterized grid of points that represent the text
var pixels = rasterizeText(text, options)	
	.map(function(d) {
		return {
			x: Math.random() * width,
			y: Math.random() * height,
			xTarget: d[0],
			yTarget: d[1],
			rTarget: radius
		};
	});

// Wrecking ball node (associated with mouse movement)
var mouseNode = {
	x: 0,
	y: height / 2,
	xTarget: width + 100,
	yTarget: height / 2,
	rTarget: 150
};

// Combine mouse node with pixel nodes
var nodes = [mouseNode].concat(pixels);

var svg = d3.select("body").append("svg")
	.attr("width", width)
	.attr("height", height);

// Gooey effect filter
// See //www.visualcinnamon.com/2016/06/fun-data-visualizations-svg-gooey-effect.html
var filter = svg.append("defs")
	.append("filter")
		.attr("id","gooey");

filter.append("feGaussianBlur")
	.attr("in", "SourceGraphic")
	.attr("stdDeviation", "8")
	.attr("color-interpolation-filters", "sRGB") 
	.attr("result", "blur");

filter.append("feColorMatrix")
	.attr("in" ,"blur")
	.attr("mode", "matrix")
	.attr("values", "1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7")
	.attr("result", "gooey");

// Circles that form the text
var circle = svg.append("g")
		.attr("class", "circles")
		.style("filter", "url(#gooey)")
	.selectAll("circle").data(nodes)
		.enter().append("circle")
			.classed("mouse", function(d, i) { return i == 0; })
			.attr("cx", function(d) { return d.x; })
			.attr("cy", function(d) { return d.y; })
			.attr("r", function(d) { return d.rTarget; });

// Nodes want to form the text but won't overlap
var simulation = d3.forceSimulation(nodes)
	.velocityDecay(0.2)
	.force("x", d3.forceX(function(d) { return d.xTarget; }).strength(collisionStrength))
	.force("y", d3.forceY(function(d) { return d.yTarget; }).strength(collisionStrength))
	.force("collide", d3.forceCollide().radius(function(d) { return d.rTarget; }))
	.on("tick", ticked);

function ticked() {
	circle
		.attr("cx", function(d) { return d.x; })
		.attr("cy", function(d) { return d.y; });
}

// Slosh the nodes around with the mouse
d3.select("body")
	.on("mousemove", mousemove);

function mousemove() {
	var p = d3.mouse(this);
	mouseNode.xTarget = p[0];
	mouseNode.yTarget = p[1];

	simulation
		.force("x", d3.forceX(function(d) { return d.xTarget; }).strength(collisionStrength))
		.force("y", d3.forceY(function(d) { return d.yTarget; }).strength(collisionStrength))
		.alpha(1)
		.restart();
}

// Convert text into grid of points that lay on top of the text
// Inspired by FizzyText. See //bl.ocks.org/tophtucker/978513bc74d0b32d3795
function rasterizeText(text, options) {
	var o = options || {};

	var fontSize = o.fontSize || "200px",
			fontWeight = o.fontWeight || "600",
			fontFamily = o.fontFamily || "sans-serif",
			textAlign = o.center || "center",
			textBaseline = o.textBaseline || "middle",
			spacing = o.spacing || 10,
			width = o.width || 960,
			height = o.height || 500,
			x = o.x || (width / 2),
			y = o.y || (height / 2);
	
	var canvas = document.createElement("canvas");
	canvas.width = width;
	canvas.height = height;

	var context = canvas.getContext("2d");

	context.font = [fontWeight, fontSize, fontFamily].join(" ");
	context.textAlign = textAlign;
	context.textBaseline = textBaseline;

	var dx = context.measureText(text).width,
			dy = +fontSize.replace("px", ""),
			bBox = [[x - dx / 2, y - dy / 2], [x + dx / 2, y + dy / 2]];

	context.fillText(text, x, y);

	var imageData = context.getImageData(0, 0, width, height);

	var pixels = [];
	for (var x = bBox[0][0]; x < bBox[1][0]; x += spacing) {
		for (var y = bBox[0][1]; y < bBox[1][1]; y += spacing) {
			var pixel = getPixel(imageData, x, y);
			if (pixel[3] != 0) pixels.push([x, y]);
		}
	}
	
	return pixels;
}

function getPixel(imageData, x, y) {
	var i = 4 * (parseInt(x) + parseInt(y) * imageData.width);
	var d = imageData.data;
	return [ d[i], d[i+1], d[i+2], d[i+3] ];
}
</script>
</body>
</html>