import * as layout from './layout.js'; import * as pattern from './pattern.js'; import * as synth from './synth.js'; const sqrt3 = Math.sqrt(3); const sqrt32 = sqrt3 / 2; const size = 50; const hs = size / 2; const hh = sqrt32 * size; const hex2px = ([q, r]) => { const x = size * (3/2 * r); const y = -size * (sqrt32 * r + sqrt3 * q); return [x, y]; }; const px2hex = ([x, y]) => { const t1 = x / size; const t2 = -y / hh / 2; const q = Math.floor( (Math.floor(-y / hh) + Math.floor(t2 - t1) + 2 ) / 3); const r = Math.floor( (Math.floor(t1 - t2) + Math.floor(t1 + t2) + 2 ) / 3); return [q, r]; } const rotate = ([x, y], a) => { return [ Math.cos(a) * x + Math.sin(a) * y, -Math.sin(a) * x + Math.cos(a) * y, ]; }; const hexagon = (ctx) => { ctx.beginPath(); ctx.moveTo(-hs, hh); ctx.lineTo(hs, hh); ctx.lineTo(size, 0); ctx.lineTo(hs, -hh); ctx.lineTo(-hs, -hh); ctx.lineTo(-size, 0); ctx.closePath(); }; const main = document.querySelector('main'); const bg = document.getElementById('canvas-bg').getContext('2d'); const fg = document.getElementById('canvas-fg').getContext('2d'); document.body.onkeydown = (e) => { switch (e.key) { case 'q': layout.turn(-1); break; case 'e': layout.turn(1); break; } }; main.onpointerdown = main.onpointermove = (e) => { if (e.type.endsWith('move')) { if (!e.target.hasPointerCapture(e.pointerId)) return; } else { e.target.setPointerCapture(e.pointerId); } e.preventDefault(); const [qq, rr] = layout.getSteps(); const pos = [e.clientX - main.clientWidth/2, e.clientY - main.clientHeight/2]; const [q, r] = px2hex(rotate(pos, layout.getRot())); const note = q*qq + r*rr; synth.on(`ptr-${e.pointerId}`, note); }; main.onpointerup = (e) => { e.preventDefault(); synth.off(`ptr-${e.pointerId}`); }; let lastCanvasSize = 0; const updateForeground = (canvasSize) => { fg.strokeStyle = '#b9bdc1'; fg.strokeWidth = 1.5; fg.canvas.width = fg.canvas.width; fg.translate(canvasSize/2, canvasSize/2); const rad = Math.ceil(canvasSize / size / 3); for (let q = -rad; q <= rad; q++) { const rMin = Math.max(-rad, -q-rad); const rMax = Math.min(rad, rad-q); for (let r = rMin; r <= rMax; r++) { fg.save(); fg.translate(...hex2px([q, r])); hexagon(fg); fg.stroke(); fg.restore(); } } }; const draw = () => { const width = window.innerWidth; const height = window.innerHeight; const canvasSize = Math.sqrt(width*width + height*height); if (canvasSize !== lastCanvasSize) { bg.canvas.width = bg.canvas.height = canvasSize; fg.canvas.width = fg.canvas.height = canvasSize; lastCanvasSize = canvasSize; updateForeground(canvasSize); } bg.canvas.width = bg.canvas.width; layout.update(); bg.translate(canvasSize/2, canvasSize/2); bg.font = `${size*0.5}px sans-serif`; bg.textAlign = 'center'; bg.textBaseline = 'middle'; const rot = layout.getRot(); const [qq, rr] = layout.getSteps(); const steps = pattern.getSteps(); const length = pattern.getLength(); const rad = Math.ceil(canvasSize / size / 3); for (let q = -rad; q <= rad; q++) { const rMin = Math.max(-rad, -q-rad); const rMax = Math.min(rad, rad-q); for (let r = rMin; r <= rMax; r++) { bg.save(); bg.translate(...hex2px([q, r])); const note = q*qq + r*rr; let step = note % length; step = (step + length) % length; const oct = Math.floor(note / length); const stepIndex = steps.indexOf(note); hexagon(bg); if (synth.isOn(note)) { bg.fillStyle = '#e3baf5'; bg.fill(); bg.beginPath(); bg.arc(0, 0, size*0.65, 0, Math.PI*2); } else if (stepIndex > -1) { bg.fillStyle = '#bae3f5'; bg.fill(); bg.beginPath(); bg.arc(0, 0, size*0.65, 0, Math.PI*2); } const c = (1 - Math.abs(oct)/10) * 255; bg.fillStyle = `rgb(${c},${c},${c})`; bg.fill(); bg.rotate(-rot); bg.fillStyle = '#303336'; if (stepIndex > -1) { bg.fillText(step, 0, -size*0.3); bg.fillStyle = '#505557'; bg.fillText(`(${stepIndex})`, 0, size*0.3); } else { bg.fillText(step, 0, 0); } bg.restore(); } } document.body.style.setProperty('--global-rot', `${rot}rad`); requestAnimationFrame(draw); }; draw();