Skip to content

Instantly share code, notes, and snippets.

@atesgoral
Created May 17, 2018 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save atesgoral/46748a3ba309b45ee148d36e0961021a to your computer and use it in GitHub Desktop.
Save atesgoral/46748a3ba309b45ee148d36e0961021a to your computer and use it in GitHub Desktop.
12-EDO
<canvas id="circle" width="512" height="512"></canvas>
<a id="watermark" href="https://twitter.com/atesgoral">atesgoral</div>
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));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
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