Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active April 27, 2016 16:24
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 enjalot/2c24d1616a8bdb192c9aca65cb43ad5e to your computer and use it in GitHub Desktop.
Save enjalot/2c24d1616a8bdb192c9aca65cb43ad5e to your computer and use it in GitHub Desktop.
Rasterized SVG
license: gpl-3.0

Implementing my own rasterization function for SVGs so I can play with pixels. My ultimate goal is to use this to generate sane meshes for extruding and 3D printing. The tools around converting SVG to 3D are very finicky and overwraught, it would be nice to have simple things be simple to do.

forked from mbostock's block: Revised D3 Logo

<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="sampler.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
#d3-svg {
position: absolute;
top: 0;
left: 0px;
border: 1px solid #333;
width: 475px;
height: 450px;
}
#overlay {
position: absolute;
top: 0;
left: 0px;
width: 475px;
height: 450px;
pointer-events: none;
}
#raster {
position:absolute;
top:0;
right: 0;
width: 475px;
height: 450px;
border: 1px solid #111;
}
#controls {
position: absolute;
top: 460px;
left: 40px;
font-family: Courier, monospace;
font-size: 20px
}
#pixel {
width: 150px;
}
path {
pointer-events: none;
}
</style>
<div id="controls">
pixel size:
<input id="pixel" type="range" min=5 max=45 value=15>
<span id="pixel-size"></span>
</div>
<svg id="d3-svg">
<path transform="translate(121, 156)scale(2)" fill="#62d4ed" d="
M0,0
h7.75
a45.5,45.5 0 1 1 0,91
h-7.75
v-20
h7.75
a25.5,25.5 0 1 0 0,-51
h-7.75
z
m36.2510,0
h32
a27.75,27.75 0 0 1 21.331,45.5
a27.75,27.75 0 0 1 -21.331,45.5
h-32
a53.6895,53.6895 0 0 0 18.7464,-20
h13.2526
a7.75,7.75 0 1 0 0,-15.5
h-7.75
a53.6895,53.6895 0 0 0 0,-20
h7.75
a7.75,7.75 0 1 0 0,-15.5
h-13.2526
a53.6895,53.6895 0 0 0 -18.7464,-20
z"/>
<circle id="pointer" r=27 cx=142 cy=246 fill="orange"></circle>
</svg>
<svg id="overlay"></svg>
<canvas id="raster"></canvas>
<script>
var svg = d3.select("svg")
var raster = d3.select("#raster")
var pixelSize = 13;
var fillX = 1;
var fillY = 1;
var strokeX = 1
var strokeY = 1
var TYPES = [
"circle", "path", "rect", "g", "svg"
]
// TODO: support rasterizing rectangles
var RASTERS = ["circle", "path", "rect"]
// walk the svg tree and create a list of elements that we want to rasterize
var root = svg.node();
// we do a level-order walk down the DOM
function walk(node, flat) {
if(!flat) flat = [];
//console.log("walking", node.nodeName)
if(node && TYPES.indexOf(node.nodeName) >= 0) {
if(RASTERS.indexOf(node.nodeName) >= 0) {
flat.push(node);
}
var children = node.childNodes;
for(var i = 0; i < children.length; i++) {
walk(children[i], flat)
}
}
return flat;
}
var flattened = [];
walk(root, flattened)
// make a grid using the pixel size
var svgBbox = svg.node().getBoundingClientRect();
var svgWidth = svgBbox.width;
var svgHeight = svgBbox.height;
flattened.forEach(function(node) {
var type = node.nodeName;
if(type === "path") {
var pos = getPos(node)
node.sampled = Sampler.getSamples(node, 300)
node.sampled.forEach(function(d){
d.x += pos.x;
d.y += pos.y;
})
}
})
function getPos(node) {
var bbox = node.getBoundingClientRect();
var x = bbox.left - svgBbox.left;
var y = bbox.top - svgBbox.top;
return { x: x, y: y, width: bbox.width, height: bbox.height}
}
function calculateGrid() {
var gridXLength = svgWidth / pixelSize;
var gridYLength = svgHeight / pixelSize;
var grid = [];
d3.range(gridXLength).forEach(function(x, i){
d3.range(gridYLength).forEach(function(y, j) {
var px = x * pixelSize + pixelSize/2;
var py = y * pixelSize + pixelSize/2;
var color = "#fff"
flattened.forEach(function(node) {
var type = node.nodeName;
var pos = getPos(node);
if(type === "circle") {
var cx = pos.x + pos.width/2;
var cy = pos.y + pos.height/2;
var r = pos.width/2;
var dist = Math.sqrt((cx - px)*(cx - px) + (cy - py)*(cy - py))
if(dist <= r) {
color = d3.select(node).style("fill")
}
} else if(type === "path") {
if(inside({x: px, y: py}, node.sampled)) {
color = d3.select(node).style("fill")
}
} else if(type === "rect") {
}
})
grid.push({
x: px,
y: py,
color: color
})
})
})
return grid
}
var canvas = raster.node()
canvas.width = svgWidth;
canvas.height = svgHeight
var ctx = raster.node().getContext('2d')
var tx = 0;
var ty = 0;
var scale = 1;
function renderGrid(grid) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
grid.forEach(function(cell) {
ctx.fillStyle = cell.color;
ctx.strokeStyle = "#d9dddc"
var size = pixelSize * scale
var x = (tx + cell.x - size/2) * scale
var y = (ty + cell.y - size/2) * scale
ctx.fillRect(x,
y,
size * fillX,
size * fillY)
ctx.strokeRect(
x,
y,
size * strokeX,
size * strokeY)
})
}
var overlaySvg = d3.select("#overlay")
function renderDots(grid) {
var dots = overlaySvg
.selectAll("circle.dot").data(grid)
var dotsEnter = dots.enter().append("circle")
.classed("dot", true)
dots.exit().remove();
dots.attr({
cx: function(d,i) {return d.x},
cy: function(d,i) { return d.y},
r: 2,
fill: "none",
stroke: "#888",
})
}
var grid = calculateGrid();
renderGrid(grid);
renderDots(grid);
var zoom = d3.behavior.zoom()
.on("zoom", function() {
console.log(d3.event)
tx = d3.event.translate[0]
ty = d3.event.translate[1]
scale = d3.event.scale;
renderGrid(grid)
})
raster.call(zoom)
var circle = svg.select("circle")
var drag = d3.behavior.drag()
.on("drag", function(d) {
var x = d3.event.x;
var y = d3.event.y;
circle.attr({
cx: x,
cy: y
})
var grid = calculateGrid()
renderGrid(grid);
})
circle.call(drag)
d3.select("#pixel-size").text(pixelSize)
d3.select("#pixel").on("input", function() {
pixelSize = +this.value;
d3.select("#pixel-size").text(pixelSize)
var grid = calculateGrid()
renderGrid(grid);
renderDots(grid);
})
function inside(point, vs) {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
var x = point.x, y = point.y;
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
// bounce the circle at load so people are more likely to click it.
circle.transition()
.duration(4000).ease("bounce")
.attr({
r: 35
})
.each("end", function() {
circle.transition().duration(3000).ease("bounce")
.attr({ r: 20 })
})
</script>
var Sampler = function() {}
Sampler.getSamples = function(path, num) {
//accomodate scale (this only works if no rotation applied)
var absBBox = path.getBoundingClientRect();
var bbox = path.getBBox();
var scale = absBBox.width/bbox.width;
// use point along path to trace our path
var len = path.getTotalLength()
var p, t;
var result = []
for(var i = 0; i < num; i++) {
p = path.getPointAtLength(i * len/num);
t = Sampler.getTangent(path, i/num * 100);
result.push({
x: p.x * scale,
y: p.y * scale,
point: p,
tangent: t,
perp: Sampler.rotate2d(t.v, 90)
});
}
return result
}
Sampler.getTangent = function(path, percent) {
// returns a normalized vector that describes the tangent
// at the point that is found at *percent* of the path's length
var fraction = percent/100;
if(fraction < 0) fraction = 0;
if(fraction > 0.99) fraction = 1;
var len = path.getTotalLength();
var point1 = path.getPointAtLength(fraction * len - 0.1);
var point2 = path.getPointAtLength(fraction * len + 0.1);
var vector = { x: point2.x - point1.x, y: point2.y - point1.y }
var magnitude = Math.sqrt(vector.x*vector.x + vector.y*vector.y);
vector.x /= magnitude;
vector.y /= magnitude;
return {p: point1, v: vector };
}
Sampler.rotate2d = function(vector, angle) {
//rotate a vector
angle *= Math.PI/180; //convert to radians
return {
x: vector.x * Math.cos(angle) - vector.y * Math.sin(angle),
y: vector.x * Math.sin(angle) + vector.y * Math.cos(angle)
}
}
// we average the location of all the array's points to get the center
function centroid(samples) {
var avg = {x:0, y:0};
for(var i = 0; i < samples.length; i++) {
avg.x += samples[i].x;
avg.y += samples[i].y;
}
avg.x /= samples.length;
avg.y /= samples.length;
return avg;
}
// The PolyK library expects a flat array like [x,y,x,y...]
function toPolyK(samples) {
var poly = []
for(var i = 0; i < samples.length; i++) {
poly.push(samples[i].x);
poly.push(samples[i].y);
}
return poly;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment