A Pen by Ates Goral on CodePen.
Created
May 17, 2018 00:39
-
-
Save atesgoral/46748a3ba309b45ee148d36e0961021a to your computer and use it in GitHub Desktop.
12-EDO
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<canvas id="circle" width="512" height="512"></canvas> | |
<a id="watermark" href="https://twitter.com/atesgoral">atesgoral</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const SEMITONES = [ 'A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#' ]; | |
const A4 = 440; | |
const TWO_PI = Math.PI * 2; | |
const HALF_PI = Math.PI / 2; | |
const CIRCLE_X = 1 / 2; | |
const CIRCLE_Y = 1 / 2; | |
const CIRCLE_RADIUS = 1 / 3; | |
const CIRCLE_THICKNESS = 1 / 180; | |
const DOT_RADIUS = 1 / 100; | |
const LABEL_DISTANCE = 1 / 40; | |
const LABEL_SIZE = 1 / 25; | |
const FREQ_DISTANCE = 1 / 12; | |
const FREQ_SIZE = 1 / 34; | |
const RATIO_DISTANCE = 1 / 30; | |
const RATIO_SIZE = 1 / 30; | |
const config = { | |
isAxisLogarithmic: false | |
}; | |
function gcd(a,b) { | |
return !b ? a : gcd(b, a % b); | |
} | |
function hsl(h, s, l) { | |
return `hsl(${h}, ${s}%, ${l}%)`; | |
} | |
function setupCircle(canvas) { | |
var ctx = null; | |
function resize() { | |
var scale = window.devicePixelRatio; | |
canvas.width = canvas.clientWidth * scale; | |
canvas.height = canvas.clientHeight * scale; | |
ctx = canvas.getContext('2d'); | |
ctx.scale(canvas.width, canvas.height); | |
} | |
resize(); | |
ctx.fillRect(0, 0, 1, 1); | |
function getAxisPos(semitone) { | |
return config.isAxisLogarithmic | |
? Math.log2(semitone / SEMITONES.length + 1) | |
: semitone / SEMITONES.length; | |
} | |
function drawDot(semitone, radius, color) { | |
ctx.fillStyle = color; | |
var pos = getAxisPos(semitone); | |
var a = TWO_PI * pos - HALF_PI; | |
var cos_a = Math.cos(a); | |
var sin_a = Math.sin(a); | |
ctx.beginPath(); | |
ctx.arc( | |
CIRCLE_X + CIRCLE_RADIUS * cos_a, | |
CIRCLE_Y + CIRCLE_RADIUS * sin_a, | |
radius, | |
0, TWO_PI | |
); | |
ctx.fill(); | |
} | |
function drawText(semitone, distance, size, color, text) { | |
ctx.fillStyle = color; | |
var pos = getAxisPos(semitone); | |
var a = TWO_PI * pos - HALF_PI; | |
var cos_a = Math.cos(a); | |
var sin_a = Math.sin(a); | |
ctx.textAlign = [ | |
'right', | |
'center', | |
'left' | |
][Math.sign(cos_a.toFixed(1) * distance) + 1]; | |
ctx.textBaseline = [ | |
'bottom', | |
'middle', | |
'top' | |
][Math.sign(sin_a.toFixed(1) * distance) + 1]; | |
ctx.font = size + "px sans-serif"; | |
ctx.fillText( | |
text, | |
CIRCLE_X + (CIRCLE_RADIUS + distance) * cos_a, | |
CIRCLE_Y + (CIRCLE_RADIUS + distance) * sin_a, | |
); | |
} | |
function getOvertones(maxOvertones) { | |
var semitones = {}; | |
var order = 1; | |
while (Object.keys(semitones).length < maxOvertones) { | |
var numerator = order; | |
var denominator = 1 << Math.log2(order); | |
var g = gcd(numerator, denominator); | |
numerator /= g; | |
denominator /= g; | |
var ratio = numerator / denominator; | |
var semitone = Math.round(Math.log2(ratio) * SEMITONES.length); | |
if (semitones[semitone]) { | |
console.log( | |
'existing', | |
[ semitones[semitone].numerator, semitones[semitone].denominator ].join(':'), | |
'new', | |
[ numerator, denominator ].join(':') | |
); | |
} | |
semitones[semitone] = semitones[semitone] || { | |
order, | |
numerator, | |
denominator, | |
//just: A4 * ratio | |
just: ratio | |
}; | |
order++; | |
} | |
return Object | |
.keys(semitones) | |
.map((key) => parseInt(key)) | |
.map((semitoneOffset) => Object.assign({ | |
semitoneOffset | |
}, semitones[semitoneOffset])) | |
.sort((a, b) => a.order - b.order); | |
} | |
var overtones = getOvertones(5); | |
//var overtones = getOvertones(SEMITONES.length); | |
overtones.forEach((overtone) => { | |
drawDot( | |
overtone.semitoneOffset, | |
DOT_RADIUS * 2, | |
hsl(overtone.semitoneOffset * 360 / SEMITONES.length, 75, 40) | |
); | |
drawText( | |
overtone.semitoneOffset, | |
-RATIO_DISTANCE, RATIO_SIZE, | |
hsl(0, 0, 75), `${overtone.numerator}:${overtone.denominator}` | |
); | |
// drawText(overtone.semitoneOffset, -FREQ_DISTANCE * 1.5, FREQ_SIZE, hsl(0, 0, 50), overtone.just); | |
// drawText(overtone.semitoneOffset, -FREQ_DISTANCE * 1.5, FREQ_SIZE, hsl(0, 0, 50), ((Math.pow(2, overtone.semitoneOffset / SEMITONES.length) - overtone.just) * 1).toFixed(3)); | |
}); | |
ctx.strokeStyle = 'white'; | |
ctx.lineWidth = CIRCLE_THICKNESS; | |
ctx.beginPath(); | |
ctx.arc( | |
CIRCLE_X, CIRCLE_Y, | |
CIRCLE_RADIUS, | |
-HALF_PI, | |
-HALF_PI + TWO_PI * getAxisPos(SEMITONES.length - 1) | |
); | |
ctx.stroke(); | |
for (var i = 0; i < SEMITONES.length; i++) { | |
var frequency = A4 * Math.pow(2, i / SEMITONES.length); | |
// var frequency = Math.pow(2, i / SEMITONES.length); | |
drawDot(i, DOT_RADIUS, 'white'); | |
drawText(i, LABEL_DISTANCE, LABEL_SIZE, hsl(0, 0, 75), SEMITONES[i]); | |
drawText(i, FREQ_DISTANCE, FREQ_SIZE, hsl(0, 0, 50), frequency.toFixed(2)); | |
} | |
return { | |
resize: resize | |
}; | |
} | |
$(function () { | |
var circle = setupCircle($('#circle').get(0)); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, body { height: 100%; } | |
body { | |
background: black; | |
padding: 0; | |
margin: 0; | |
position: relative; | |
font: 100% sans-serif; | |
} | |
#circle { | |
width: 512px; | |
height: 512px; | |
} | |
#watermark { | |
position: absolute; | |
bottom: 0; | |
right: 0; | |
font-size: 75%; | |
color: #ddd; | |
text-decoration: none; | |
padding: 0 1em; | |
height: 2em; | |
line-height: 2em; | |
border-radius: 1em; | |
margin: 0.5em; | |
background: rgba(255, 255, 255, 0.15); | |
} | |
#watermark:before { | |
content: '@'; | |
} | |
#watermark:hover { | |
color: white; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment