|
<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 http://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 http://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> |