Skip to content

Instantly share code, notes, and snippets.

@rflow
Last active September 21, 2016 22:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rflow/836d213591362ac8e94a13a8494c3371 to your computer and use it in GitHub Desktop.
Save rflow/836d213591362ac8e94a13a8494c3371 to your computer and use it in GitHub Desktop.
viSFest
Display the source blob
Display the rendered blob
Raw
<svg width="290" height="160" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<!--Unicode Character 'EYE' (U+1F441)-->
<g>
<path id="eyeball" d="m285.921906,79.906204q-21.093811,38.10939 -58.781311,58.218796q-36,19.265594 -81,19.265594q-45.140594,0 -81,-19.265594q-37.687494,-20.109406 -59.062494,-58.218796q21.515699,-38.39061 58.9219,-58.5q35.859398,-19.125008 81.140594,-19.125008q45.281311,0 81,19.265709q37.546906,20.109299 58.781311,58.359299zm-26.578094,0q-27.562607,-48.375 -86.906311,-58.218704q17.718704,7.593704 28.265594,23.273407q10.546906,15.679688 10.546906,34.945297q0,18.843796 -10.406204,34.593796q-10.406296,15.75 -27.703201,23.484406q58.921906,-9.984406 86.203217,-58.078201zm-61.593811,0q0,-21.234299 -15.257797,-35.929611q-15.257797,-14.695389 -36.492203,-14.695389q-11.109406,0 -21.375,4.640701l-5.343803,-12.234406q-59.203098,10.125 -86.624996,58.218704q27.2813,47.953201 86.062603,58.078201q-17.296906,-7.734406 -27.632904,-23.414108q-10.335899,-15.679703 -10.335899,-34.664093q0,-11.531204 4.078102,-22.5l13.078094,4.078201q-3.656197,9.140594 -3.656197,18.421799q0,21.23439 15.1875,35.929703q15.1875,14.695297 36.5625,14.695297q21.375,0 36.5625,-14.695297q15.1875,-14.695313 15.1875,-35.929703zm-12.656204,0q0,16.031296 -11.531296,27.210999q-11.531296,11.179703 -27.5625,11.179703q-16.17189,0 -27.632797,-11.109406q-11.461006,-11.109406 -11.461006,-27.281296q0,-7.171799 2.390701,-13.5l39.937508,18.140701l-18.703201,-39.656311q7.593796,-3.375 15.468796,-3.375q16.031204,0 27.5625,11.179703q11.531296,11.179703 11.531296,27.210907z"/>
</g>
</svg>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment