Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 3, 2018 08:33
Show Gist options
  • Save Kcnarf/cf64372672d297d326e0 to your computer and use it in GitHub Desktop.
Save Kcnarf/cf64372672d297d326e0 to your computer and use it in GitHub Desktop.
wheels on wheels on wheels
license: mit
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment