|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
line { |
|
stroke: rgba(0, 0, 0, 0.2); |
|
fill: none; |
|
stroke-width: 3px; |
|
stroke-linecap: round; |
|
} |
|
|
|
circle { |
|
stroke-width: 0.5px; |
|
stroke: rgb(0, 0, 0); |
|
} |
|
|
|
</style> |
|
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script> |
|
<script src="flat-fold.js" charset="utf-8"></script> |
|
<body> |
|
<svg width="960px" height="960px"></svg> |
|
</body> |
|
<script> |
|
|
|
var svg = d3.select("svg"), |
|
margin = { top: 100, left: 100, right: 100, bottom: 100}, |
|
width = 960, |
|
height = 960, |
|
radius = 3, |
|
seedLo = 7, |
|
seedHi = 12; |
|
|
|
svg = svg.append("g") |
|
.attr("transform", translate(margin.left, height / 2)); |
|
|
|
var X = d3.scaleLinear() |
|
.domain([0, 1]) |
|
.range([0, width - margin.left - margin.right]); |
|
|
|
// Create random creases in a unit interval [0, 1]. |
|
var data = d3.range(d3.randomUniform(seedLo, seedHi)() | 0).map((d, i, arr) => { |
|
return d3.randomUniform()(); |
|
}).sort((a, b) => a - b).filter((d, i, arr) => { |
|
// Ensure minimum distance between creases to avoid vertex overlaps. |
|
return i === 0 || (X(d) - X(arr[i - 1]) > 4 * radius); |
|
}); |
|
|
|
// True for mountain fold; false for valley fold. |
|
var creases = data.map((d, i) => d3.randomUniform()() >= 0.2); |
|
|
|
data = [0].concat(data).concat([1]); |
|
|
|
var chain = svg; |
|
|
|
var folding = FlatFold(data, creases); |
|
|
|
var foldedOrder = folding.order(); |
|
|
|
var foldedLine = folding.line(); |
|
|
|
var foldedCreases = folding.creases(); |
|
|
|
var max = d3.max(foldedOrder); |
|
var lastIndex = foldedOrder.indexOf(max); |
|
|
|
// Create nested chain of groups. |
|
for (let i = 0; i < foldedLine.length - 1; i++) { |
|
chain = chain.selectAll("g") |
|
.data([foldedLine[i]]) |
|
.enter().append("g") |
|
.attr("transform", translate((i > 0 |
|
? X(foldedLine[i] - foldedLine[i - 1]) |
|
: X(foldedLine[i])), 0)); |
|
|
|
chain.append("line") |
|
.attr("x1", 0) |
|
.attr("y1", 0) |
|
.attr("y2", 0) |
|
.attr("x2", X(foldedLine[i + 1]) - X(foldedLine[i])); |
|
|
|
// The endpoints are not creases. |
|
if (i > 0 && i < foldedLine.length - 1) { |
|
chain.append("circle") |
|
.attr("cx", 0) |
|
.attr("cy", 0) |
|
.attr("r", radius); |
|
} |
|
} |
|
|
|
var stopped = false; |
|
|
|
// Keep the segments centered in the viewport. |
|
var timer = d3.timer(function(elapsed) { |
|
let bbox = svg.node().getBBox(), |
|
center = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2], |
|
dx = width / 2 - center[0], |
|
dy = height / 2 - center[1]; |
|
|
|
svg.transition() |
|
.ease(d3.easeLinear) |
|
// Provide a little hysteresis. |
|
.duration(500) |
|
.attr("transform", (d, i) => { |
|
return translate(dx, dy); |
|
}); |
|
|
|
if (stopped) timer.stop(); |
|
}); |
|
|
|
svg.select("g").selectAll("g") |
|
.style("fill", (d, i) => { |
|
if (data.indexOf(foldedLine[i]) === -1) return "#00FF00"; |
|
return (foldedCreases[i]) ? "#FF0000" : "#0000FF"; |
|
}) |
|
.transition() |
|
.duration(2500) |
|
.delay((d, i) => 2500 * foldedOrder[i]) |
|
.attr("transform", (d, i) => { |
|
let dx = i > 0 ? X(foldedLine[i + 1] - foldedLine[i]) : X(foldedLine[i + 1]); |
|
return translate(dx, 0) + rotate(180 * ((foldedCreases[i]) ? 1 : -1)); |
|
}) |
|
.style("fill", "#000000") |
|
.on("end", function(d, i) { |
|
// The last crease has finished folding. |
|
if (i === lastIndex) { |
|
window.setTimeout(() => { stopped = true; }, 1000); |
|
} |
|
}); |
|
|
|
svg.select("g").selectAll("circle") |
|
.transition() |
|
.duration(2500) |
|
.delay((d, i) => 2500 * foldedOrder[i]) |
|
.attr("r", radius / 2); |
|
|
|
function translate(x, y) { |
|
return "translate(" + x + "," + y + ")"; |
|
} |
|
|
|
function rotate(degrees) { |
|
return "rotate(" + degrees + ")"; |
|
} |
|
|
|
</script> |
TODO