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