This block is a recreation based on times tables modulo (cf. https://www.youtube.com/watch?v=qhbuKbxJsk8).
- D3.js (v.4)
- blockbuilder.org
license: gpl-3.0 | |
border: no |
This block is a recreation based on times tables modulo (cf. https://www.youtube.com/watch?v=qhbuKbxJsk8).
<html lang="en"> | |
<head> | |
<meta charset="utf-8" /> | |
<title>Cardioïdes</title> | |
<meta name="description" content="Some fun at making visualization of times tables modulo, with D3js"> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<style> | |
#layouter { | |
text-align: center; | |
position: relative; | |
} | |
#wip { | |
display: none; | |
position: absolute; | |
top: 220px; | |
left: 110px; | |
font-size: 40px; | |
text-align: center; | |
} | |
svg { | |
margin: 1px; | |
border-radius: 1000px; | |
box-shadow: 2px 2px 6px grey; | |
} | |
.pin-path { | |
fill: none; | |
stroke: lightgrey; | |
} | |
.pin { | |
fill: lightgrey; | |
} | |
.chord { | |
fill: none; | |
stroke: lightgreen; | |
stroke-width: 1px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="layouter"> | |
<svg></svg> | |
<div id="wip"> | |
Work in progress ... | |
</div> | |
</div> | |
<script> | |
var _PI = Math.PI, | |
_2PI = 2*Math.PI; | |
//begin: layout conf. | |
var totalWidth = 500, | |
totalHeight = 500, | |
controlsHeight = 0, | |
svgbw = 1, // canvas border width | |
svgbs = 8, // canvas box-shadow | |
radius = (totalHeight-controlsHeight-svgbs-2*svgbw)/2, | |
width = 2*radius, | |
height = 2*radius, | |
halfRadius = radius/2 | |
halfWidth = halfRadius, | |
halfHeight = halfRadius, | |
quarterRadius = radius/4; | |
quarterWidth = quarterRadius, | |
quarterHeight = quarterRadius; | |
//end: layout conf. | |
//begin: drawing conf. | |
var pinAmount = 2*3*4*5, // 120, divisible by 2,3,4,5,6,8,10 | |
multiplier = 2, // an integer, preferably in [2,3,4,5,6] | |
startRadius = radius-20, | |
endRadius = startRadius, | |
startWHRatio = 1, // width/height ratio of starting pins shape | |
endWHRatio = 1, // width/height ratio of ending pins shape | |
// width/height ratio of pins shape : | |
// >1 => v-elipsis | |
// 1 => circle | |
// >0 && <1 => h-elipsis | |
// 0 => line | |
// <0 => curbersome !!! | |
drawPinPathes = true, | |
drawPins = true; | |
//end: drawing conf. | |
//begin: reusable d3 selection | |
var drawingArea, pathContainer, startPinContainer, endPinContainer, chordContainer; | |
//end: reusable d3 selection | |
// data | |
var startPins, endPins, chords, shuffleType = 'radius', shuffleCount = 0; | |
//begin: init layout | |
initLayout(); | |
//end: init layout | |
computeData(); | |
redraw(); | |
d3.interval(function(elapsed) { | |
shuffleConf() | |
computeData(); | |
redraw(); | |
}, 3500); | |
function shuffleConf() { | |
var nextShuffleType, newRatio; | |
shuffleCount++; | |
if (shuffleCount%7 === 0) { | |
endRadius = startRadius; | |
startWHRatio = 1; | |
endWHRatio = 1; | |
} else { | |
if (shuffleType==='radius') { | |
endRadius = startRadius*(0.5+Math.random()/2); | |
} else if (shuffleType==='startWHRatio') { | |
newRatio = 5*Math.random(); | |
if (startWHRatio>1) { | |
startWHRatio = 1/newRatio; | |
} else { | |
startWHRatio = newRatio; | |
} | |
} else { | |
newRatio = 5*Math.random(); | |
if (endWHRatio>1) { | |
endWHRatio = 1/newRatio; | |
} else { | |
endWHRatio = newRatio; | |
} | |
} | |
} | |
nextShuffleType = shuffleType; | |
while(nextShuffleType === shuffleType) { | |
rand = Math.random(); | |
if (rand<0.2) { | |
nextShuffleType = 'radius'; | |
} else if (rand<0.6) { | |
nextShuffleType = 'startWHRatio'; | |
} else { | |
nextShuffleType = 'endWHRatio'; | |
} | |
} | |
shuffleType = nextShuffleType; | |
} | |
function computeData() { | |
var pinAngle = _2PI/pinAmount, | |
startWidth = (startWHRatio>1)? startRadius : startRadius*startWHRatio, | |
startHeight = (startWHRatio>1)? startRadius/startWHRatio : startRadius, | |
endWidth = (endWHRatio>1)? endRadius : endRadius*endWHRatio, | |
endHeight = (endWHRatio>1)? endRadius/endWHRatio : endRadius, | |
newStartPins = [], | |
newEndPins = [], | |
newChords = []; | |
var i, startAngle, endAngle; | |
for (i=0; i<pinAmount; i++) { | |
startAngle = pinAngle*i; | |
endAngle = startAngle; | |
newStartPins.push({ | |
x: Math.cos(startAngle)*startWidth, | |
y: Math.sin(startAngle)*startHeight | |
}); | |
newEndPins.push({ | |
x: Math.cos(endAngle)*endWidth, | |
y: Math.sin(endAngle)*endHeight | |
}); | |
newChords.push({ | |
startIndex: i, | |
endIndex: (i*multiplier)%pinAmount | |
}) | |
} | |
startPins = newStartPins; | |
endPins = newEndPins; | |
chords = newChords; | |
} | |
function redraw() { | |
if (drawPinPathes) { | |
redrawPinPathes(); | |
} | |
if (drawPins) { | |
redrawPins(); | |
} | |
redrawLines(); | |
} | |
function redrawPinPathes() { | |
var pathLiner = d3.line().x(function(d){ return d.x; }).y(function(d){ return d.y; }); | |
var pathes = pathContainer.selectAll("path") | |
.data([startPins, endPins]); | |
pathes.enter() | |
.append("path") | |
.classed("pin-path", true) | |
.attr("d", function(d){ return pathLiner(d)+"z"; }) | |
.merge(pathes) | |
.transition() | |
.duration(3000) | |
.attr("d", function(d){ return pathLiner(d)+"z"; }); | |
} | |
function redrawPins() { | |
var drawnStartPins = startPinContainer.selectAll(".pin").data(startPins); | |
var drawnEndPins = endPinContainer.selectAll(".pin").data(endPins); | |
drawnStartPins.enter() | |
.append("circle") | |
.classed("pin", true) | |
.attr("r", 1.5) | |
.attr("cx", function(d){ return d.x; }) | |
.attr("cy", function(d){ return d.y; }) | |
.merge(drawnStartPins) | |
.transition() | |
.duration(3000) | |
.attr("cx", function(d){ return d.x; }) | |
.attr("cy", function(d){ return d.y; }); | |
drawnEndPins.enter() | |
.append("circle") | |
.classed("pin", true) | |
.attr("r", 1.5) | |
.attr("cx", function(d){ return d.x; }) | |
.attr("cy", function(d){ return d.y; }) | |
.merge(drawnEndPins) | |
.transition() | |
.duration(3000) | |
.attr("cx", function(d){ return d.x; }) | |
.attr("cy", function(d){ return d.y; }); | |
} | |
function redrawLines() { | |
var drawnChords = chordContainer.selectAll(".chord").data(chords); | |
drawnChords.enter() | |
.append("line") | |
.classed("chord", true) | |
.attr("x1", function (d){ return startPins[d.startIndex].x; }) | |
.attr("y1", function (d){ return startPins[d.startIndex].y; }) | |
.attr("x2", function (d){ return endPins[d.endIndex].x; }) | |
.attr("y2", function (d){ return endPins[d.endIndex].y; }) | |
.merge(drawnChords) | |
.transition() | |
.duration(3000) | |
.attr("x1", function (d){ return startPins[d.startIndex].x; }) | |
.attr("y1", function (d){ return startPins[d.startIndex].y; }) | |
.attr("x2", function (d){ return endPins[d.endIndex].x; }) | |
.attr("y2", function (d){ return endPins[d.endIndex].y; }); | |
} | |
function initLayout() { | |
d3.select("#layouter").style("width", totalWidth+"px").style("height", totalHeight+"px"); | |
drawingArea = d3.select("svg").attr("width", width).attr("height", height) | |
.append("g") | |
.classed("drawing-area", true) | |
. attr("transform", "translate("+[radius, radius]+")rotate(-90)"); | |
pathContainer = drawingArea.append("g").attr("id", "path-container"); | |
startPinContainer = drawingArea.append("g").attr("id", "pin-container"); | |
endPinContainer = drawingArea.append("g").attr("id", "pin-container"); | |
chordContainer = drawingArea.append("g").attr("id", "chord-container"); | |
} | |
</script> | |
</body> | |
</html> |