Built with blockbuilder.org
forked from aktraiser's block: loolo
license: mit |
<html> | |
<body> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.0.0/pixi.min.js"></script> | |
<script> | |
// measure doc | |
const width = document.body.clientWidth; | |
const height = document.body.clientHeight; | |
// determine proportions | |
const radius = (width > 1024) ? 3 : 2; | |
const spacing = radius * 3; | |
const fontSize = Math.floor(0.2 * width) + "px"; | |
console.log("window is", width, "x", height, "px"); | |
console.log("dot radius is", radius + "px"); | |
console.log("font size is", fontSize); | |
// tweak these! | |
const options = { | |
width: width, | |
height: height, | |
imgWidth: width * 0.439790, // daft magic to ensure eye outline has appropriate point count | |
imgHeight: width * 0.439790 * 0.595000, // as above -- the golden ratio :) | |
x: width / 2, | |
y: height / 3, | |
radius: radius, | |
spacing: spacing, | |
fontSize: fontSize, | |
fill: 0x2e88fd, //"rgba(46, 136, 253, 1)", // #2e88fd | |
collisionStrength: 0.1, | |
velocityDecay: 0.2 | |
}; | |
const titles = ["viSFest"];//, "d3.unconf", "oct 16-17"]; | |
// create pixi renderer and stage objects | |
//var renderer = new PIXI.CanvasRenderer(800, 600); | |
const renderer = new PIXI.autoDetectRenderer(width, height, { backgroundColor : 0xffffff }); | |
const stage = new PIXI.Container(); | |
// snapshot a circle to a texture for optimal rendering perf | |
const gfx = new PIXI.Graphics(); | |
const tileSize = options.spacing; | |
const texture = PIXI.RenderTexture.create(tileSize, tileSize); | |
gfx.beginFill(options.fill); | |
gfx.drawCircle(tileSize/2, tileSize/2, options.radius); | |
gfx.endFill(); | |
renderer.render(gfx, texture); | |
// add fx filters | |
// stage.filters = createEffectFilters(); | |
// rasterize title text to build point maps | |
const titleCoords = titles.map(function(title){ | |
return rasterizeText(title, options); | |
}); | |
// determine coords for svg outline and start animation | |
getOutlineForSVG("eye.svg", options, function (eyeCoords) { | |
// each state contains a list of x/y coords to target | |
const states = [eyeCoords].concat(titleCoords); | |
// determine how many nodes needed for longest list of coords | |
const nodeCount = d3.max(states, function(state){ | |
return state.length; | |
}) | |
// create required nodes | |
const nodes = d3.range(nodeCount).map(function (index) { | |
// create a new Sprite using the texture | |
const sprite = new PIXI.Sprite(texture); | |
// center the sprite's anchor point | |
sprite.anchor.x = 0.5 * tileSize; | |
sprite.anchor.y = 0.5 * tileSize; | |
return { | |
_id: index, | |
sprite: sprite, | |
rTarget: options.radius, | |
active: false | |
}; | |
}); | |
console.log("created", nodeCount, "nodes"); | |
// create force simulation that will animate the node positions | |
const simulation = d3.forceSimulation(nodes); | |
const strength = options.collisionStrength; | |
const decay = options.velocityDecay; | |
// define forces that will act on the above | |
const xForce = d3.forceX(function(d) { return d.xTarget; }).strength(strength); | |
const yForce = d3.forceY(function(d) { return d.yTarget; }).strength(strength); | |
const collisionForce = d3.forceCollide().radius(function(d) { return d.rTarget; }); | |
const updateNodeLocations = function () { | |
nodes.forEach(function(node) { | |
node.sprite.position.x = node.x; | |
node.sprite.position.y = node.y; | |
}) | |
} | |
const updateNodeTargets = function (nodes, coords, options) { | |
const coordCount = coords.length; | |
for (var i = 0, node; i < nodeCount; i++) { | |
node = nodes[i]; | |
if (i < coordCount) { | |
// bring in previously inactive notes at random locations | |
if (!node.active) { | |
node.x = Math.random() * options.width; | |
node.y = Math.random() * options.height; | |
} | |
// set targets of force simulation according to next coords | |
node.xTarget = coords[i][0]; | |
node.yTarget = coords[i][1]; | |
node.active = true; | |
stage.addChild(node.sprite); | |
} else { | |
node.active = false; | |
stage.removeChild(node.sprite); | |
} | |
}; | |
} | |
const restartSimulation = function (simulation, nodes, xForce, yForce) { | |
simulation | |
.nodes(nodes.filter(function(n){ | |
return n.active; | |
})) | |
.force("x", xForce) | |
.force("y", yForce) | |
.alpha(1) | |
.restart(); | |
} | |
var state = 0; | |
var targetCoords = states[state]; | |
var gotoNextState = function () { | |
state = (state + 1) % states.length; | |
targetCoords = states[state]; | |
console.log("advancing to state", state); | |
updateNodeTargets(nodes, targetCoords, options); | |
restartSimulation(simulation, nodes, xForce, yForce); | |
} | |
var animateSprites = function () { | |
requestAnimationFrame(animateSprites); | |
renderer.render(stage); | |
} | |
updateNodeTargets(nodes, targetCoords, options); | |
simulation | |
.velocityDecay(decay) | |
.force("x", xForce) | |
.force("y", yForce) | |
.force("collide", collisionForce) | |
.on("tick", updateNodeLocations) | |
.on("end", gotoNextState); | |
document.body.appendChild(renderer.view); | |
animateSprites(); | |
}) | |
//////////// welcome to the library /////////////////////////////////////////// | |
// Convert text into grid of points that lay on top of the text | |
// Inspired by FizzyText. cf 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); | |
return findPoints(imageData, bBox, spacing); | |
} | |
// scan image data for filled pixels, | |
// return list of x,y coords spaced as required | |
function findPoints (imageData, rect, spacing) { | |
var points = []; | |
for (var x = rect[0][0]; x < rect[1][0]; x += spacing) { | |
for (var y = rect[0][1]; y < rect[1][1]; y += spacing) { | |
var pixel = getPixel(imageData, x, y); | |
if (pixel[3] != 0) points.push([x, y]); | |
} | |
} | |
return points; | |
} | |
// read pixel from imageData at required coords | |
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] ]; | |
} | |
// Blur effect filter | |
function createEffectFilters () { | |
const colorMatrix = new PIXI.filters.ColorMatrixFilter(); | |
const blurFilter = new PIXI.filters.BlurFilter(); | |
colorMatrix.saturate(2); | |
blurFilter.blur = 0.5; | |
return [colorMatrix, blurFilter]; | |
} | |
// legacy version using svg | |
function createSVGCircles (svg, nodes, options) { | |
// create group to hold circle elements | |
const layer = svg.append("g").attr("class", "circles"); | |
// bind svg circle elements to all nodes | |
const circles = layer.selectAll("circle") | |
.data(nodes) | |
.enter() | |
.append("circle"); | |
// apply glow filter | |
layer.style("filter", "url(#glow)"); | |
// init class, position, radius of circle elements | |
circles | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }) | |
.attr("r", function(d) { return d.rTarget; }) | |
.style("fill", options.fill); | |
return circles; | |
} | |
// get outline for contents of img element | |
// using same approach as text rasterizer | |
// c.f. http://jsfiddle.net/AbdiasSoftware/Y3K57/ | |
const getOutlineForImage = function (img, width, height, spacing) { | |
const canvas = document.createElement("canvas"); | |
const context = canvas.getContext("2d"); | |
const bounds = [[0,0],[width,height]]; | |
canvas.width = width; | |
canvas.height = height; | |
context.drawImage(img, 0, 0, width, height); | |
return findPoints( | |
context.getImageData(0, 0, width, height), | |
bounds, | |
spacing | |
); | |
} | |
// load svg from path into img element, feed into routine above | |
// c.f. http://jsfiddle.net/AbdiasSoftware/Y3K57/ | |
function getOutlineForSVG (path, options, onComplete) { | |
const img = new Image; | |
const spacing = options.spacing; | |
img.onload = function () { | |
const width = img.width; | |
const height = img.height; | |
// read outline coords and translate to viewport | |
const coords = getOutlineForImage(img, width, height, spacing); | |
const offset = { | |
x: options.x - width / 2, | |
y: options.y - height / 2 | |
} | |
for (var i = 0; i < coords.length; i++) { | |
coords[i][0] += offset.x; | |
coords[i][1] += offset.y; | |
}; | |
// return outline coords to callback | |
onComplete(coords); | |
}; | |
img.crossOrigin = 'anonymous'; | |
img.width = options.imgWidth; | |
img.height = options.imgHeight; | |
img.src = path; | |
} | |
</script> | |
</body> | |
</html> |