Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 9, 2016 16:37
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save Kcnarf/91fe3cc845398b34bc3b0913afcb10e0 to your computer and use it in GitHub Desktop.
Interactive Parquet Deformation
license: lgpl-3.0

This block is a recreation (and a continuation of 2 and 1) inspired by parquet deformation, which consists on progressively transform a shape into another one (cf. www.theguardian.com/[...]/crazy-paving-the-twisted-world-of-parquet-deformations or http://www.tess-elation.co.uk/parquet-deformations). The progressive shape transformation is provided by d3-interpolate.

You can modify the starting shape and the ending shape, by choosing among predefined curves, drag&droping control points, or clicking an edge to change its control points in a symetric way.

I came to parquet deformation because the Voronoï layout makes a tessellation of the 2D plane, as the parquet does. Parquet deformation is also closely linked to Escher's researches and amazing drawings (cf. Google or http://en.tessellations-nicolas.com/method.php).

Acknowledgments to:

<html>
<head>
<meta charset="utf-8">
<title>Interactive Parquet Deformation</title>
<meta content="Interactive Parquet Deformation with D3" name="description">
<style>
.shape-container-border {
fill: url("#shape-area-gradient");
}
.vertex {
fill: black;
}
.vertex.draggable {
stroke-width: 5;
stroke: transparent;
cursor: move;
}
.vertex.draggable.horizontal {
fill: green;
}
.vertex.draggable.vertical {
fill: blue;
}
.edge {
fill: none;
stroke: black;
}
.shape-container .edge {
stroke: grey;
stroke-width: 2;
cursor: pointer;
}
.predef-edge-container circle {
fill: transparent;
cursor: pointer;
}
.predef-edge-container .edge {
stroke: lightgrey;
stroke-width: 1.5;
}
.predef-edge-container:hover .edge {
stroke: grey;
}
#tooltip {
font-size: small;
}
</style>
</head>
<body>
<svg>
<defs>
<radialGradient id="shape-area-gradient">
<stop offset="90%" stop-color="white"/>
<stop offset="100%" stop-color="grey"/>
</radialGradient>
</defs>
<g id="drawing-area">
<g id="parquet"></g>
<g id="starting-shape-container" class="shape-container">
<circle class="shape-container-border"></circle>
<g class="predef-edges-container">
<g class="horizontal-predef-edges-container"></g>
<g class="vertical-predef-edges-container"></g>
</g>
<g id="starting-shape" class="shape">
<g class="edges"></g>
<g class="verteces">
<g class="corners"></g>
<g class="draggables"></g>
</g>
</g>
</g>
<g id="ending-shape-container" class="shape-container">
<circle class="shape-container-border"></circle>
<g class="predef-edges-container">
<g class="horizontal-predef-edges-container"></g>
<g class="vertical-predef-edges-container"></g>
</g>
<g id="ending-shape" class="shape">
<g class="edges"></g>
<g class="verteces">
<g class="corners"></g>
<g class="draggables"></g>
</g>
</g>
</g>
<text id="tooltip"></text>
</g>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var _PI = Math.PI,
_2PI = 2*_PI,
_midPI = 0.5*_PI;
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
tileSize = 40, //size of a tile (ie. little shape) in the background
hTileCount = Math.floor(svgWidth/tileSize)-1, //-1 for enought margin
vTileCount = Math.floor(svgHeight/tileSize)-1, //-1 for enought margin
hMargin = (svgWidth-hTileCount*tileSize)/2, ///2 for centering purpose
vMargin = (svgHeight-vTileCount*tileSize)/2, ///2 for centering purpose
margin = {top: vMargin, right: hMargin, bottom: vMargin, left: hMargin},
width = svgWidth - margin.left - margin.right,
height = svgHeight - margin.top - margin.bottom,
shapeSize = 100, //size of the large shapes
outerShapeContainerMargin = 10,
innerShapeContainerMargin = 60,
shapeContainerSize = shapeSize + 2*innerShapeContainerMargin,
shapeTileFactor = shapeSize/tileSize,
predefEdgeSize = 20,
predefEdgeFactor = shapeSize/predefEdgeSize;
//end: layout conf.
//begin: reusable d3-selections
var svg = d3.select("svg"),
drawingArea = d3.select("#drawing-area"),
parquet = d3.select("#parquet"),
startingShapeContainer = d3.select("#starting-shape-container"),
endingShapeContainer = d3.select("#ending-shape-container"),
startingShape = d3.select("#starting-shape"),
endingShape = d3.select("#ending-shape"),
tooltip = d3.select("#tooltip");
//end: reusable d3-selections
//begin: shape-related things
var shapeCurve = d3.curveBasis,
startingShapeEdges = {},
endingShapeEdges = {},
shapeEdgeLiner = d3.line()
.curve(shapeCurve)
.x(function(d){ return d.x; })
.y(function(d){ return d.y; }),
tileEdgeLiner = d3.line()
.curve(shapeCurve)
.x(function(d){ return d.x/shapeTileFactor; })
.y(function(d){ return d.y/shapeTileFactor; }),
d = shapeSize/4,
predefEdgeVerteces = {
horizontal: [
[{x:0, y:0}, {x:d, y:0}, {x:2*d, y:0}, {x:3*d, y:0}, {x:4*d, y:0}],
[{x:0, y:0}, {x:d, y:-d}, {x:2*d, y:0}, {x:3*d, y:d}, {x:4*d, y:0}],
[{x:0, y:0}, {x:3*d, y:-d}, {x:2*d, y:0}, {x:d, y:d}, {x:4*d, y:0}],
[{x:0, y:0}, {x:d, y:-d}, {x:3*d, y:-d}, {x:3*d, y:0}, {x:4*d, y:0}],
[{x:0, y:0}, {x:3*d, y:-d}, {x:3*d, y:d}, {x:3*d, y:0}, {x:4*d, y:0}]
],
vertical: [
[{x:0, y:0}, {x:0, y:d}, {x:0, y:2*d}, {x:0, y:3*d}, {x:0, y:4*d}],
[{x:0, y:0}, {x:d, y:d}, {x:0, y:2*d}, {x:-d, y:3*d}, {x:0, y:4*d}],
[{x:0, y:0}, {x:d, y:3*d}, {x:0, y:2*d}, {x:-d, y:d}, {x:0, y:4*d}],
[{x:0, y:0}, {x:-d, y:d}, {x:-d, y:3*d}, {x:0, y:3*d}, {x:0, y:4*d}],
[{x:0, y:0}, {x:-d, y:3*d}, {x:d, y:3*d}, {x:0, y:3*d}, {x:0, y:4*d}]
]
},
predefEdgeLiner = d3.line()
.curve(shapeCurve)
.x(function(d){ return d.x/predefEdgeFactor; })
.y(function(d){ return d.y/predefEdgeFactor; }),
predefEdgeCount = predefEdgeVerteces.horizontal.length;
//end: shape-related things
//begin: drag behaviors
var dragVertex = d3.drag()
.subject(function(d) { return d; })
.on("start", dragStarted)
.on("drag", vertexDragged)
.on("end", dragEnded);
function dragStarted(d) {
var x = Math.round(d3.event.x),
y = Math.round(d3.event.y);
d3.select(this).classed("dragging", true);
showTooltip(d3.event.sourceEvent.x, d3.event.sourceEvent.y, "["+x+", "+y+"]");
}
function dragEnded(d) {
d3.select(this).classed("dragging", false);
hideTooltip();
}
function vertexDragged(d) {
var x = Math.round(d3.event.x),
y = Math.round(d3.event.y);
showTooltip(d3.event.sourceEvent.x, d3.event.sourceEvent.y, "["+x+", "+y+"]");
d.x = x;
d.y = y;
d.edge.nextSymetry = "horizontal";
redrawShapeEdges(d.edge.shapeEdges);
redrawShapeVerteces(d.edge.shapeEdges);
redrawParquetEdges(d.edge.direction);
}
//end: drag behaviors
initLayout();
initShapeEdges();
redrawShapes();
redrawParquet();
function initLayout() {
svg.attr("width", svgWidth)
.attr("height", svgHeight);
drawingArea.attr("width", width)
.attr("height", height)
.attr("transform", "translate("+[margin.left, margin.top]+")");
var shapeContainerY = (height - shapeContainerSize)/2;
startingShapeContainer.attr("transform", "translate("+[outerShapeContainerMargin,shapeContainerY]+")");
endingShapeContainer.attr("transform", "translate("+[(width-shapeContainerSize-outerShapeContainerMargin),shapeContainerY]+")");
d3.selectAll(".shape-container-border")
.attr("cx", shapeContainerSize/2)
.attr("cy", shapeContainerSize/2)
.attr("r", shapeContainerSize/2);
d3.selectAll(".shape").attr("transform", "translate("+[innerShapeContainerMargin,innerShapeContainerMargin]+")");
//begin: predef-edges
d3.selectAll(".predef-edges-container")
.attr("transform", "translate("+[shapeContainerSize/2,shapeContainerSize/2 ]+")");
[startingShapeEdges, endingShapeEdges].forEach(function(shapeEdges){
["horizontal", "vertical"].forEach(function(direction){
drawPredefEdges(shapeEdges, direction);
})
})
//begin: predef-edges
}
function drawPredefEdges (shapeEdges, direction) {
var distance = shapeContainerSize/2 - predefEdgeSize;
var shapeContainer,
initialAngle, // depending on direction, on top or at left
angle, // depending on direction, to right or to bottom
dx,dy, // center predef edges
entered;
if (shapeEdges===startingShapeEdges) {
shapeContainer = startingShapeContainer;
} else {
shapeContainer = endingShapeContainer;
}
if (direction==="horizontal") {
angle = 3*_midPI/4/predefEdgeCount;
initialAngle = -_midPI-((predefEdgeCount-1)/2*angle);
dx = -predefEdgeSize/2;
dy = 0;
} else {
angle = -3*_midPI/4/predefEdgeCount
initialAngle = -_PI-((predefEdgeCount-1)/2*angle);;
dx = 0;
dy = -predefEdgeSize/2;
}
entered = shapeContainer.select("."+direction+"-predef-edges-container").selectAll(".predef-edge-container")
.data(predefEdgeVerteces[direction].map(function(pev){
return {
shapeEdges: shapeEdges, //starting or ending shape
direction: direction,
predefEdgeVerteces: pev}
}))
.enter()
.append("g")
.classed("predef-edge-container", true)
.attr("transform", function(d,i){
var a = initialAngle+i*angle;
return "translate("+[distance*Math.cos(a), distance*Math.sin(a)]+")";
})
.on("click", predefEdgeClicked);
entered.append("circle")
.attr("r", predefEdgeSize/2);
entered.append("path")
.classed("edge", true)
.attr("transform", "translate("+[dx,dy]+")")
.attr("d", function(d){ return predefEdgeLiner(d.predefEdgeVerteces)});
}
function initShapeEdges() {
[startingShapeEdges, endingShapeEdges].forEach(function(shapeEdges, i) {
["horizontal", "vertical"].forEach(function(direction){
shapeEdges[direction] = {
direction: direction,
nextSymetry : "horizontal",
verteces: [{},{},{},{},{}],
shapeEdges: shapeEdges}
})
})
//begin: set initial verteces positions
udpateVerteces(startingShapeEdges.horizontal.verteces, predefEdgeVerteces.horizontal[0]);
udpateVerteces(startingShapeEdges.vertical.verteces, predefEdgeVerteces.vertical[0]);
udpateVerteces(endingShapeEdges.horizontal.verteces, predefEdgeVerteces.horizontal[1]);
udpateVerteces(endingShapeEdges.vertical.verteces, predefEdgeVerteces.vertical[1]);
//begin: set initial verteces positions
//begin: back references to edge, store initial x and y for auto-update loop
var vertexCount, draggable;
[startingShapeEdges, endingShapeEdges].forEach(function(shapeEdges) {
["horizontal", "vertical"].forEach(function(direction){
vertexCount = shapeEdges[direction].verteces.length;
shapeEdges[direction].verteces.forEach(function(vertex, i){
draggable = (i>0 && i<vertexCount-1);
vertex.draggable = draggable;
vertex.shape = shapeEdges;
vertex.edge = shapeEdges[direction];
})
})
})
//end: back references
}
function udpateVerteces (verteces, newVerteces) {
verteces.forEach(function(v, i){
v.x = newVerteces[i].x;
v.y = newVerteces[i].y;
});
}
function redrawShapes () {
redrawShape(startingShapeEdges);
redrawShape(endingShapeEdges);
}
function redrawShape(shapeEdges) {
redrawShapeEdges(shapeEdges);
redrawShapeVerteces(shapeEdges);
}
function redrawShapeEdges(shapeEdges) {
var drawnShape = (shapeEdges===startingShapeEdges)? startingShape : endingShape;
var edges = [
{x: 0, y: 0, edge: shapeEdges.horizontal}, // top
{x: 0, y: shapeSize, edge: shapeEdges.horizontal}, // bottom
{x: 0, y: 0, edge: shapeEdges.vertical}, // left
{x: shapeSize, y: 0, edge: shapeEdges.vertical} // right
]
var drawnEdges = drawnShape.select(".edges").selectAll(".edge")
.data(edges);
drawnEdges = drawnEdges.enter()
.append("path")
.attr("class", function(d){ return d.edge.direction; })
.classed("edge", true)
.attr("transform", function(d){ return "translate("+[d.x,d.y]+")"; })
.on("click", edgeClicked)
.merge(drawnEdges);
drawnEdges
.attr("d", function(d){ return shapeEdgeLiner(d.edge.verteces); });
drawnEdges.exit().remove();
}
function redrawShapeVerteces(shapeEdges) {
function inHorizontalEdge(v){ return v.edge!=null && v.edge.direction==="horizontal"; };
function inVerticalEdge(v){ return v.edge!=null && v.edge.direction==="vertical"; }
var drawnShape = (shapeEdges===startingShapeEdges)? startingShape : endingShape;
var corners = [
{x: 0, y: 0, draggable: false},
{x: 0, y: shapeSize, draggable: false},
{x: shapeSize, y: 0, draggable: false},
{x: shapeSize, y: shapeSize, draggable: false},
]
var draggables = shapeEdges.horizontal.verteces.slice(1,shapeEdges.horizontal.verteces.length-1).concat(shapeEdges.vertical.verteces.slice(1,shapeEdges.vertical.verteces.length-1));
var drawnVerteces = drawnShape.select(".verteces .corners").selectAll(".vertex")
.data(corners);
drawnVerteces = drawnVerteces.enter()
.append("circle")
.classed("vertex", true)
.merge(drawnVerteces);
drawnVerteces.attr("r", 2)
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
var drawnVerteces = drawnShape.select(".verteces .draggables").selectAll(".vertex")
.data(draggables);
drawnVerteces = drawnVerteces.enter()
.append("circle")
.classed("vertex", true)
.classed("draggable", true)
.classed("horizontal", inHorizontalEdge)
.classed("vertical", inVerticalEdge)
.call(dragVertex)
.merge(drawnVerteces);
drawnVerteces.attr("r", 2)
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
}
function redrawParquet() {
redrawParquetEdges("horizontal");
redrawParquetEdges("vertical")
}
function redrawParquetEdges(edgeDirection) {
var edges = [],
edgeInterpolator = d3.interpolateString(tileEdgeLiner(startingShapeEdges[edgeDirection].verteces), tileEdgeLiner(endingShapeEdges[edgeDirection].verteces)),
hEdgeCount = (edgeDirection==="horizontal")? hTileCount : hTileCount+1, //handle right-most vertical edges
vEdgeCount = (edgeDirection==="vertical")? vTileCount : vTileCount+1, //handle bottom-most horizontal edges
interpolatedPathes = [], // limit computation time, each line draws the same shapes/edges
interpolateFactor;
for (var xi=0; xi<hEdgeCount; xi++) {
x = xi*tileSize;
interpolateFactor = (xi<0)? 0 : (xi>hTileCount)? 1 : xi/hTileCount;
interpolatedPathes.push(edgeInterpolator(interpolateFactor));
for (var yi=0; yi<vEdgeCount; yi++) {
edges.push({
x: x,
y: yi*tileSize,
interpolateFactor: interpolateFactor,
direction: edgeDirection,
xi: xi});
}
}
var drawnEdges = parquet.selectAll(".edge."+edgeDirection).data(edges);
drawnEdges = drawnEdges.enter()
.append("path")
.classed("edge "+edgeDirection, true)
.merge(drawnEdges);
drawnEdges.attr("transform", function(d){ return "translate("+[d.x,d.y]+")"; })
.attr("d", function(d){
return interpolatedPathes[d.xi];
})
}
function predefEdgeClicked (predefEdgeWrapper) {
var shapeEdges = predefEdgeWrapper.shapeEdges,
direction = predefEdgeWrapper.direction,
predefEdgeVerteces = predefEdgeWrapper.predefEdgeVerteces;
shapeEdges[direction].nextSymetry = "horizontal";
udpateVerteces(shapeEdges[direction].verteces, predefEdgeVerteces);
redrawShapeEdges(shapeEdges);
redrawShapeVerteces(shapeEdges);
redrawParquetEdges(direction);
}
function edgeClicked (edgeWrapper) {
var edge = edgeWrapper.edge,
direction = edge.direction,
currentSymetry = edge.nextSymetry,
newVerteces;
if(currentSymetry==="horizontal") {
if(direction==="horizontal") {
newVerteces = edge.verteces.map(function(v){ return {x: shapeSize-v.x, y: v.y}; }).reverse();
} else {
newVerteces = edge.verteces.map(function(v){ return {x: -v.x, y: v.y}; });
}
} else {
if(direction==="horizontal") {
newVerteces = edge.verteces.map(function(v){ return {x: v.x, y: -v.y}; });
} else {
newVerteces = edge.verteces.map(function(v){ return {x: v.x, y: shapeSize-v.y}; }).reverse();
}
}
udpateVerteces(edge.verteces, newVerteces);
edge.nextSymetry = (currentSymetry==="horizontal")? "vertical" : "horizontal";
redrawShapeEdges(edge.shapeEdges);
redrawShapeVerteces(edge.shapeEdges);
redrawParquetEdges(edge.direction);
}
function showTooltip (x, y, text) {
tooltip.attr("x", x)
.attr("y", y)
.text(text)
.classed("hide", false);
}
function hideTooltip () {
tooltip.text("")
.classed("hide", true);
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment