Experimentation based on http://scholarcommons.scu.edu/cgi/viewcontent.cgi?article=1004&context=math_compsci
If you see a surprising rosace, click 'stopRandoms' (in the controller) to maintain current parameters. Then unfold and play with the available controls to see the rosace updating itself.
The 'Explanations' folder of the controller can help to understand what's going on.
xxxxxxxxxx
<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>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js