a study of: Musical Hexagons by Vasco Asturiano
Built with blockbuilder.org
| license: mit |
a study of: Musical Hexagons by Vasco Asturiano
Built with blockbuilder.org
| <!DOCTYPE html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <script src="https://d3js.org/d3.v5.min.js"></script> | |
| <script src="//unpkg.com/[email protected]/src/MIDIUtils.js"></script> | |
| <style> | |
| </style> | |
| <link rel="stylesheet" type="text/css" href="style.css"> | |
| </head> | |
| <body> | |
| <script> | |
| const topMargin = 35, | |
| hexR = 40, | |
| nLevels = 8, // map size | |
| total = 100; | |
| function getTerrain(label,color){ | |
| return { | |
| label, | |
| color, | |
| } | |
| } | |
| const terrain = [getTerrain('path','ivory'), getTerrain('woods','green'), getTerrain('forest','darkgreen'), getTerrain('hill','brown'), getTerrain('mountain','white'), getTerrain('swamp','goldenrod'), getTerrain('beach','yellow'), getTerrain('meadow','lightgreen'), getTerrain('mine','darkgray'), getTerrain('town','grey'), getTerrain('stream','blue'), getTerrain('crops','gold')]; | |
| //const noteScale = d3.scaleOrdinal(d3.schemeCategory20c) | |
| //.domain(terrain) | |
| function getSelectedIntervals() { | |
| return { | |
| hor: 4, | |
| diag: 3 | |
| } | |
| } | |
| var svg = d3.select("body").append("svg") | |
| .attr('id','svg') | |
| .attr("width", window.innerWidth) | |
| .attr("height", window.innerHeight) | |
| const world = svg.append('g') | |
| .attr('id', 'world'); | |
| world.append('g') | |
| .attr('id','sea') | |
| .attr("width", window.innerWidth) | |
| .attr("height", window.innerHeight) | |
| .append('rect') | |
| .attr("width", window.innerWidth) | |
| .attr("height", window.innerHeight) | |
| .style('fill','#c7efff70') | |
| world.append('g') | |
| .append("text") | |
| .text("mine") | |
| .attr("y", 10) | |
| .attr("x", 10) | |
| .attr("font-size", 20) | |
| .attr("font-family", "monospace") | |
| function getPolygonPath(r, nSides, startAngle) { | |
| let d = '' | |
| d3.range(nSides).map(side => { | |
| const angle = startAngle + 2 * Math.PI * side / nSides | |
| return [r * Math.cos(angle), r * Math.sin(angle)] | |
| }).forEach(pt => { | |
| d += (d.length ? 'L' : 'M') + pt.join(',') | |
| }) | |
| return d + 'Z' | |
| } | |
| const transitionTime = 900, | |
| hexPath = getPolygonPath(hexR, 6, Math.PI / 2) | |
| function genHexList(r, centerXy, centralFreq, levels) { | |
| levels += (levels % 2) ? 0 : 1 // Round up to nearest odd number | |
| const {hor, diag} = getSelectedIntervals(), | |
| diagonalUp = diag, | |
| diagonalDown = hor - diag, | |
| leftFreq = total * getIntervalRatio(-hor * (levels - 1) / 2), | |
| leftXy = centerXy | |
| let noteCnt = {} // Keep track of which notes are added to assign a unique ID to each | |
| leftXy[0] -= (levels - 1) * r // Left side of the row | |
| // Central row | |
| let hexs = buildRow(r, leftXy, leftFreq, levels) | |
| d3.range(1, (levels - 1) / 2 + 1).forEach(i => { | |
| const offset = [i * r * 2 * Math.cos(Math.PI / 3), i * r * 2 * Math.sin(Math.PI / 3)] | |
| hexs.push( | |
| // Up-right | |
| ...buildRow(r, [leftXy[0] + offset[0], leftXy[1] - offset[1]], leftFreq * getIntervalRatio(i * diagonalUp), levels - i), | |
| // Down-right | |
| ...buildRow(r, [leftXy[0] + offset[0], leftXy[1] + offset[1]], leftFreq * getIntervalRatio(i * diagonalDown), levels - i) | |
| ) | |
| }) | |
| return hexs | |
| // | |
| function buildRow(r, xy, freq, levels) { | |
| const hexs = [], | |
| horizInterval = getIntervalRatio(hor) | |
| let carryX = xy[0], | |
| carryFreq = freq | |
| while (levels) { | |
| const noteNum = MIDIUtils.frequencyToNoteNumber(carryFreq) | |
| if (noteNum>=12 && noteNum <= 126) { | |
| const index = Math.floor(Math.random() * 10); | |
| //const terrain = terrain[index]; | |
| const name = terrain[index].label | |
| // Assign unique id (noteName + counter) | |
| if (!noteCnt.hasOwnProperty(name)) noteCnt[name] = 0 | |
| noteCnt[name]++ | |
| const id = `${name}-${noteCnt[name]}` | |
| hexs.push({ | |
| x: carryX, | |
| y: xy[1], | |
| freq: carryFreq, | |
| name, | |
| color: terrain[index].color, | |
| id: id | |
| }) | |
| } | |
| carryX += r * 2 | |
| carryFreq *= horizInterval | |
| levels-- | |
| } | |
| return hexs | |
| } | |
| function getIntervalRatio(num) { | |
| // Equal temperament | |
| return Math.pow(2, num / 12) | |
| } | |
| } | |
| function drawMap(){ | |
| let hexs = d3.select('#world') | |
| .selectAll('.hex') | |
| .data(genHexList(hexR, [window.innerWidth / 2, (window.innerHeight - topMargin) / 2], total, nLevels), d => d.id) | |
| // Old hexs | |
| hexs.exit().transition().duration(transitionTime) | |
| // Shrink and fade-out | |
| .attr('transform', d => `translate(${d.x},${d.y}) scale(0)`) | |
| .style('opacity', 0) | |
| .remove() | |
| // New hexs | |
| const newHexs = hexs.enter().append('g') | |
| .attr('class', 'hex') | |
| .attr('transform', d => `translate(${d.x},${d.y}) scale(0)`); | |
| newHexs | |
| .transition() | |
| .duration(5000) | |
| .attr('transform', d => `translate(${d.x},${d.y}) scale(1,1)`); | |
| const getFill = d => { | |
| return d.color; | |
| } | |
| newHexs.append('path') | |
| .attr('d', hexPath) | |
| .style('fill', getFill) | |
| newHexs.append('text') | |
| .attr('text-anchor', 'middle') | |
| .attr('dy', '.35em') | |
| .text(d => d.name) | |
| console.log('drawn!') | |
| } | |
| drawMap() | |
| </script> | |
| </body> |
| body { margin:0; | |
| position:fixed; | |
| top:0; | |
| right:0; | |
| bottom:0; | |
| left:0; | |
| text-align: center; | |
| font-family: Sans-serif; | |
| background-color: 'blue' | |
| } | |
| .hex path { | |
| stroke: darkslategrey; | |
| stroke-width: 0; | |
| opacity: 0.85; | |
| } | |
| .hex.highlight path { | |
| opacity: 1; | |
| stroke-width: 2; | |
| } | |
| .hex text, .legend-hex text { | |
| fill: #333; | |
| pointer-events: none; | |
| } | |
| .hex text { | |
| font-size: 14px; | |
| } | |
| .legend-hex text { | |
| font-size: 11px; | |
| } | |
| .legend-hex path { | |
| fill: #CCE; | |
| } | |
| .legend-hex.central-hex path { | |
| fill: rebeccapurple; | |
| } |