|
<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> |