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