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