|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script> |
|
|
|
<style> |
|
body { |
|
margin:0; |
|
position:fixed; |
|
top:0; |
|
right:0; |
|
bottom:0; |
|
left:0; |
|
background-color: black; |
|
} |
|
svg { |
|
width:100%; |
|
height: 100%; |
|
} |
|
|
|
path { |
|
fill: none; |
|
stroke: white; |
|
stroke-width: 1.5px; |
|
} |
|
.pattern { |
|
stroke: red; |
|
} |
|
|
|
.wheel .perimeter { |
|
fill: none; |
|
stroke: white; |
|
stroke-width: 1.5px; |
|
} |
|
.wheel .radius { |
|
fill: none; |
|
stroke: white; |
|
stroke-width: 1.5px; |
|
} |
|
.wheel .position { |
|
fill: white; |
|
} |
|
#wheel3 .position { |
|
fill: red; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<script> |
|
var svgWidth=960, |
|
svgHeight=500; |
|
var duration = 3000; |
|
|
|
var pointsPerPath = 1000, |
|
radianIncrement = 2*Math.PI/pointsPerPath; |
|
|
|
var maxR1 = svgHeight/4, |
|
maxR2 = svgHeight/8, |
|
maxR3 = svgHeight/16, |
|
maxSymetryCount = 12, |
|
modulo = -1, //inverse of expected because y-axe goes down in SVG |
|
availableDirections = ["Trigonometric", "Clockwise"], |
|
directionMapping = {"Trigonometric": -1, |
|
"Clockwise": +1}, //inverse of expected because y-axe goes down in SVG |
|
availablePhasings = ["Top", "Left", "Bottom", "Right"], |
|
phasingMapping = {"Top": 0, |
|
"Left": -Math.PI/2, |
|
"Bottom": Math.PI, |
|
"Right": Math.PI/2}, //inverse of expected because y-axe goes down in SVG |
|
requester = {USER: 0, RANDOM: 1}; |
|
|
|
var symetry = {symetryCount: maxSymetryCount}; |
|
var radiuses = {wheel1: maxR1, |
|
wheel2: maxR2, |
|
wheel3: maxR3}; |
|
var directions = {wheel1: "Trigonometric", |
|
wheel2: "Trigonometric", |
|
wheel3: "Trigonometric"}; |
|
var phasings = {wheel1: "Top", |
|
wheel2: "Top", |
|
wheel3: "Top"}; |
|
var randoms = {randomSymetry: true, |
|
randomRadiuses: true, |
|
randomDirections: true, |
|
randomPhasings: false, |
|
only1RandomAtATime: false, |
|
stopRandoms: function() { |
|
randoms.randomSymetry = false; |
|
randoms.randomRadiuses = false; |
|
randoms.randomDirections = false; |
|
randoms.randomPhasings = false; |
|
randoms.automaticRandom = false; |
|
}, |
|
randomNow: function() { |
|
updateRosace(requester.RANDOM); |
|
}, |
|
automaticRandom: true}; |
|
var explanations = {showPattern: false, |
|
showWheels: false}; |
|
|
|
var animatingWheels = false //synchronizer |
|
|
|
var controls = new dat.GUI({width: 340}); |
|
controls.add(symetry, "symetryCount", 2, 12).step(1) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); });; |
|
var radiusesCtrl = controls.addFolder("Radiuses"); |
|
radiusesCtrl.add(radiuses, "wheel1", 0, svgHeight/4).step(1) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
radiusesCtrl.add(radiuses, "wheel2", 0, svgHeight/4).step(1) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
radiusesCtrl.add(radiuses, "wheel3", 0, svgHeight/4).step(1) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
var directionsCtrl = controls.addFolder("Rotation directions"); |
|
directionsCtrl.add(directions, "wheel1", ["Trigonometric"]); |
|
directionsCtrl.add(directions, "wheel2", availableDirections) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
directionsCtrl.add(directions, "wheel3", availableDirections) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
var phasingsCtrl = controls.addFolder("Phasings"); |
|
phasingsCtrl.add(phasings, "wheel1", availablePhasings) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
phasingsCtrl.add(phasings, "wheel2", availablePhasings) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
phasingsCtrl.add(phasings, "wheel3", availablePhasings) |
|
.listen().onChange(function(value) { updateRosace(requester.USER); }); |
|
var randomsCtrl = controls.addFolder("Randomness"); |
|
randomsCtrl.add(randoms, "randomSymetry") |
|
.listen().onChange(function(value) { if (value) { updateRosace(requester.RANDOM); }}); |
|
randomsCtrl.add(randoms, "randomRadiuses") |
|
.listen().onChange(function(value) { if (value) { updateRosace(requester.RANDOM); }}); |
|
randomsCtrl.add(randoms, "randomDirections") |
|
.listen().onChange(function(value) { if (value) { updateRosace(requester.RANDOM); }}); |
|
randomsCtrl.add(randoms, "randomPhasings") |
|
.listen().onChange(function(value) { if (value) { updateRosace(requester.RANDOM); }}); |
|
randomsCtrl.add(randoms, "only1RandomAtATime"); |
|
randomsCtrl.add(randoms, "stopRandoms"); |
|
randomsCtrl.add(randoms, "randomNow"); |
|
randomsCtrl.add(randoms, "automaticRandom") |
|
.listen().onChange(function(value) { if (value) { updateRosace(requester.RANDOM); }}); |
|
randomsCtrl.open(); |
|
var explanationsCtrl = controls.addFolder("Explainations"); |
|
explanationsCtrl.add(explanations, "showPattern") |
|
.onChange(function(value) { pattern.style("opacity", value? 1 : 0); }); |
|
explanationsCtrl.add(explanations, "showWheels"); |
|
|
|
var warn = function (text) { |
|
var textPositioner = d3.select("svg").append("g") |
|
.attr("transform", "translate("+[10,20]+")"); |
|
textPositioner.append("text") |
|
.text(text) |
|
.style("fill", "white") |
|
textPositioner.transition() |
|
.delay(duration/3) |
|
.duration(2*duration/3) |
|
.attr("transform", "translate("+[-300,20]+")") |
|
.style("opacity", 0) |
|
.remove() |
|
} |
|
|
|
var updateRadiuses = function() { |
|
radiuses.wheel1 = maxR1*(0.2+Math.random()*0.8); |
|
radiuses.wheel2 = maxR2*(0.2+Math.random()*0.8); |
|
radiuses.wheel3 = maxR3*(0.2+Math.random()*0.8); |
|
console.log("radiuses updated"); |
|
} |
|
|
|
var updateDirections = function () { |
|
var newDirections = {wheel2: directions.wheel2, |
|
wheel3: directions.wheel3}; |
|
|
|
while (newDirections.wheel2 === directions.wheel2 && |
|
newDirections.wheel3 === directions.wheel3) { |
|
newDirections.wheel2 = availableDirections[Math.round(Math.random())]; |
|
newDirections.wheel3 = availableDirections[Math.round(Math.random())]; |
|
} |
|
directions.wheel2 = newDirections.wheel2; |
|
directions.wheel3 = newDirections.wheel3; |
|
console.log("rotation directions updated") |
|
}; |
|
|
|
var updateSymetryCount = function() { |
|
var newSymetryCount = symetry.symetryCount; |
|
while (newSymetryCount === symetry.symetryCount) { |
|
newSymetryCount = Math.floor((maxSymetryCount-2)*Math.random())+2; |
|
} |
|
symetry.symetryCount = newSymetryCount; |
|
console.log("symetry count updated") |
|
}; |
|
|
|
var updatePhasings = function() { |
|
var newPhasings = {wheel1: phasings.wheel1, |
|
wheel2: phasings.wheel2, |
|
wheel3: phasings.wheel3}; |
|
while (newPhasings.wheel1 === phasings.wheel1 && |
|
newPhasings.wheel2 === phasings.wheel2 && |
|
newPhasings.wheel3 === phasings.wheel3) { |
|
newPhasings.wheel1 = availablePhasings[Math.round(3*Math.random())], |
|
newPhasings.wheel2 = availablePhasings[Math.round(3*Math.random())], |
|
newPhasings.wheel3 = availablePhasings[Math.round(3*Math.random())]; |
|
} |
|
phasings.wheel1 = newPhasings.wheel1; |
|
phasings.wheel2 = newPhasings.wheel2; |
|
phasings.wheel3 = newPhasings.wheel3; |
|
console.log("phasings updated") |
|
}; |
|
|
|
var computePathes = function(currentRequester) { |
|
if (currentRequester === requester.RANDOM) { |
|
if (randoms.randomRadiuses || |
|
randoms.randomDirections || |
|
randoms.randomSymetry || |
|
randoms.randomPhasings) |
|
{ |
|
if (randoms.only1RandomAtATime) { |
|
var randomable = []; |
|
if (randoms.randomRadiuses) { randomable.push(updateRadiuses); } |
|
if (randoms.randomDirections) { randomable.push(updateDirections); } |
|
if (randoms.randomSymetry) { randomable.push(updateSymetryCount); } |
|
if (randoms.randomPhasings) { randomable.push(updatePhasings); } |
|
randomable[Math.floor(Math.random()*randomable.length)](); |
|
} else { |
|
if (randoms.randomRadiuses) { updateRadiuses(); } |
|
if (randoms.randomDirections) { updateDirections(); } |
|
if (randoms.randomSymetry) { updateSymetryCount(); } |
|
if (randoms.randomPhasings) { updatePhasings(); } |
|
} |
|
} else { |
|
warn("Please select something to random !!!"); |
|
} |
|
} |
|
|
|
var newSpeeds = {wheel1: +modulo, |
|
wheel2: directionMapping[directions.wheel2]*(symetry.symetryCount)+modulo, |
|
wheel3: directionMapping[directions.wheel3]*(3*symetry.symetryCount)+modulo}; |
|
|
|
|
|
|
|
rosacePath = "M"; |
|
var radians = {wheel1: phasingMapping[phasings.wheel1], |
|
wheel2: phasingMapping[phasings.wheel2], |
|
wheel3: phasingMapping[phasings.wheel3]}, |
|
x, y; |
|
for(var i=0; i<pointsPerPath/symetry.symetryCount; i++) { |
|
x = radiuses.wheel1*Math.cos(radians.wheel1) + |
|
radiuses.wheel2*Math.cos(radians.wheel2) + |
|
radiuses.wheel3*Math.cos(radians.wheel3) |
|
y = radiuses.wheel1*Math.sin(radians.wheel1) + |
|
radiuses.wheel2*Math.sin(radians.wheel2) + |
|
radiuses.wheel3*Math.sin(radians.wheel3) |
|
rosacePath += [x,y] + " "; |
|
radians.wheel1 += radianIncrement*newSpeeds.wheel1; |
|
radians.wheel2 += radianIncrement*newSpeeds.wheel2; |
|
radians.wheel3 += radianIncrement*newSpeeds.wheel3; |
|
}; |
|
patternPath = rosacePath; |
|
for(true; i<pointsPerPath; i++) { |
|
x = radiuses.wheel1*Math.cos(radians.wheel1) + |
|
radiuses.wheel2*Math.cos(radians.wheel2) + |
|
radiuses.wheel3*Math.cos(radians.wheel3) |
|
y = radiuses.wheel1*Math.sin(radians.wheel1) + |
|
radiuses.wheel2*Math.sin(radians.wheel2) + |
|
radiuses.wheel3*Math.sin(radians.wheel3) |
|
patternPath += "M" + [x,y] + " "; |
|
rosacePath += [x,y] + " "; |
|
radians.wheel1 += radianIncrement*newSpeeds.wheel1; |
|
radians.wheel2 += radianIncrement*newSpeeds.wheel2; |
|
radians.wheel3 += radianIncrement*newSpeeds.wheel3; |
|
}; |
|
rosacePath += "z"; |
|
}; |
|
|
|
var computeWheelCharacteristics = function (name) { |
|
return { |
|
radius: radiuses[name], |
|
direction: directions[name], |
|
phasing: phasings[name], |
|
initialDependenciesPlacement: [ |
|
radiuses[name]*Math.cos(phasingMapping[phasings[name]]), |
|
radiuses[name]*Math.sin(phasingMapping[phasings[name]]) |
|
] |
|
}; |
|
} |
|
|
|
var computeWheelsCharacteristics = function () { |
|
return ["wheel1", "wheel2", "wheel3"].map(function(name) { |
|
return computeWheelCharacteristics(name); |
|
}) |
|
} |
|
|
|
var patternPath; |
|
var rosacePath; |
|
computePathes(requester.RANDOM); |
|
|
|
var drawingArea = d3.select("body").append("svg") |
|
.append("g") |
|
.attr({ |
|
"transform": "translate("+[svgWidth/3, svgHeight/2]+"),rotate(-90)" |
|
}); // rotate(-90) for vertical symetry |
|
|
|
drawingArea.append("g").classed("pathes", true); |
|
var rosace = drawingArea.select(".pathes").append("path", "path") |
|
.classed("rosace show", true) |
|
.attr("d", rosacePath); |
|
var pattern = drawingArea.select(".pathes").append("path", "path") |
|
.classed("pattern", true) |
|
.style("opacity", 0) |
|
.attr("d", patternPath); |
|
|
|
drawingArea.append("g") |
|
.classed("wheels", true); |
|
var wheel1 = drawingArea.select(".wheels").append("g") |
|
.classed("wheel", true) |
|
.attr("id", "wheel1"); |
|
wheel1.append("g") |
|
.classed("dependencies", true); |
|
var wheel2 = wheel1.select(".dependencies").append("g") |
|
.classed("wheel", true) |
|
.attr("id", "wheel2"); |
|
wheel2.append("g") |
|
.classed("dependencies", true); |
|
var wheel3 = wheel2.select(".dependencies").append("g") |
|
.classed("wheel", true) |
|
.attr("id", "wheel3"); |
|
wheel3.append("g") |
|
.classed("dependencies", true); |
|
|
|
var wheels = drawingArea.selectAll(".wheel") |
|
.data(computeWheelsCharacteristics()); |
|
|
|
wheels.insert("circle", ".dependencies") |
|
.classed("perimeter", true) |
|
.attr("r", 0); |
|
wheels.insert("path", ".perimeter") |
|
.classed("radius", true) |
|
.attr("d", "M0,0L0,0"); |
|
drawingArea.selectAll(".dependencies").insert("circle", ".wheel") |
|
.classed("position", true) |
|
.attr("r", 0); |
|
|
|
var continueAfter = function (endedPhase) { |
|
switch (endedPhase) { |
|
case "updateRosace": |
|
if (explanations.showWheels) { |
|
showWheels(); |
|
} else { |
|
if (randoms.automaticRandom) { |
|
updateRosace(requester.RANDOM) |
|
} |
|
} |
|
break; |
|
case "showWheels": |
|
animateWheels(); |
|
break; |
|
case "animateWheels": |
|
hideWheels(); |
|
break; |
|
case "hideWheels": |
|
if (randoms.automaticRandom) { |
|
updateRosace(requester.RANDOM); |
|
} |
|
break; |
|
} |
|
}; |
|
|
|
var updateRosace = function(currentRequester) { |
|
if (animatingWheels) { |
|
warn("update not allowed during wheels' animation") |
|
return true; |
|
} |
|
computePathes(currentRequester); |
|
|
|
pattern.transition() |
|
.duration(duration) |
|
.ease('elastic') |
|
.attr("d", patternPath); |
|
rosace.transition() |
|
.duration(duration) |
|
.ease('elastic') |
|
.attr("d", rosacePath) |
|
.each("end", function() { |
|
continueAfter("updateRosace"); |
|
}) |
|
}; |
|
|
|
var showWheels = function () { |
|
animatingWheels = true; |
|
wheels.data(computeWheelsCharacteristics()); |
|
|
|
wheels.select(".perimeter").attr("r", 0); |
|
wheels.select(".radius").attr({d: "M0,0L0,0", transform: "rotate(0)"}); |
|
wheels.select(".dependencies") |
|
.attr("transform", function(d) { return "translate("+ d.initialDependenciesPlacement +")"; }); |
|
wheels.select(".position").attr("r", 0); |
|
|
|
pattern.transition() |
|
.duration(1000) |
|
.style("opacity", 1); |
|
rosace.transition() |
|
.duration(1000) |
|
.style("opacity", 0); |
|
|
|
wheels.select(".perimeter").transition() |
|
.duration(500) |
|
.delay(function(d,i) { return 1000 + i*500; }) // '1000' for synchro with rosace's fading out |
|
.attr("r", function(d) { return d.radius; }); |
|
wheels.select(".radius").transition() |
|
.duration(0) |
|
.delay(function(d,i) { return 1000 + (i+1)*500; }) // synchro with rosace's fading out and perimeters' enlargement |
|
.each("end", function(d, i) { |
|
d3.select(this).attr("d", function(d){ return "M0,0L" + d.initialDependenciesPlacement}); |
|
if (i===2) { // the last animation |
|
continueAfter("showWheels"); |
|
} |
|
}); |
|
wheels.select(".position").transition() |
|
.duration(0) |
|
.delay(function(d,i) { return 1000 + (i+1)*500; }) // synchro with rosace's fading out and perimeters' enlargement |
|
.each("end", function(d, i) { |
|
d3.select(this).attr("r", 3); |
|
if (i===2) { // the last animation |
|
continueAfter("showWheels"); |
|
} |
|
}); |
|
}; |
|
|
|
var animateWheels = function () { |
|
var finalAngle = 2*Math.PI/symetry.symetryCount; |
|
var finalAngleInDegree = 360/symetry.symetryCount; |
|
function dependenciesPlacementTween(speedFactor) { |
|
return function(d, i) { |
|
return function tween (t) { |
|
var x = d.radius*Math.cos(phasingMapping[d.phasing] +(directionMapping[d.direction]*speedFactor+modulo)*finalAngle*t); |
|
var y = d.radius*Math.sin(phasingMapping[d.phasing] +(directionMapping[d.direction]*speedFactor+modulo)*finalAngle*t); |
|
return "translate("+[x, y]+")"; |
|
}; |
|
}; |
|
}; |
|
function radiusRotationTween(speedFactor) { |
|
return function(d, i) { |
|
return d3.interpolateString( |
|
"rotate(0)", |
|
"rotate("+ (directionMapping[d.direction]*speedFactor+modulo)*finalAngleInDegree +")" |
|
); |
|
}; |
|
}; |
|
|
|
|
|
wheel1.select(".dependencies").transition() |
|
.delay(500) // wait a moment before the animation starts |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", dependenciesPlacementTween(0)); |
|
wheel1.select(".radius").transition() |
|
.delay(500) // wait a moment before the animation starts |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", radiusRotationTween(0)); |
|
|
|
wheel2.select(".dependencies").transition() |
|
.delay(500) // wait a moment before starting animation |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", dependenciesPlacementTween(symetry.symetryCount)); |
|
wheel2.select(".radius").transition() |
|
.delay(500) // wait a moment before the animation starts |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", radiusRotationTween(symetry.symetryCount)); |
|
|
|
wheel3.select(".dependencies").transition() |
|
.delay(500) // wait a moment before starting animation |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", dependenciesPlacementTween(3*symetry.symetryCount)) |
|
.each("end", function() { |
|
d3.transition() |
|
.duration(500) // wait a moment after the animation ends |
|
.each("end", function() { |
|
continueAfter("animateWheels"); |
|
}) |
|
}); |
|
wheel3.select(".radius").transition() |
|
.delay(500) // wait a moment before the animation starts |
|
.duration(5*duration) |
|
.ease("linear") |
|
.attrTween("transform", radiusRotationTween(3*symetry.symetryCount)); |
|
}; |
|
|
|
var hideWheels = function () { |
|
wheels.select(".perimeter").transition() |
|
.duration(500) |
|
.delay(function(d,i) { return (2-i)*500; }) |
|
.attr("r", 0) |
|
.each("end", function(d, i) { |
|
if (i===0) { // the last animation |
|
pattern.transition() |
|
.duration(500) |
|
.style("opacity", explanations.showPattern? 1 : 0); |
|
rosace.transition() |
|
.duration(500) |
|
.style("opacity", 1) |
|
.each("end", function () { |
|
animatingWheels = false; |
|
continueAfter("hideWheels"); |
|
}); |
|
} |
|
}); |
|
wheels.select(".radius").transition() |
|
.duration(0) |
|
.delay(function(d,i) { return (2-i)*500; }) // synchro with perimeters' transitions |
|
.each("end", function(d, i) { |
|
d3.select(this).attr({d: "M0,0L0,0", transform: "rotate(0)"}); |
|
}); |
|
wheels.select(".position").transition() |
|
.duration(0) |
|
.delay(function(d,i) { return (2-i)*500; }) // synchro with perimeters' transitions |
|
.each("end", function(d, i) { |
|
d3.select(this).attr("r", 0); |
|
}); |
|
}; |
|
|
|
updateRosace(requester.RANDOM); |
|
</script> |
|
</body> |