Initial
This commit is contained in:
10
README.md
Normal file
10
README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Tesla-Vortex – Modulo Multiplication
|
||||
|
||||
A graphical tool to visualize modulo multiplication. [Click here to try out](https://relikd.github.io/Vortex-Math/).
|
||||
|
||||
This is a direct response to a coding challange ([YouTube video](https://www.youtube.com/watch?v=6ZrO90AI0c8)).
|
||||
|
||||
|
||||
### License
|
||||
|
||||
This work is made available under the Creative Commons [CC0 1.0 Universal Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/deed.en).
|
||||
370
draw.js
Normal file
370
draw.js
Normal file
@@ -0,0 +1,370 @@
|
||||
let loop_colors = [
|
||||
'#7f0000', '#006400', '#708090', '#808000', '#483d8b', '#3cb371',
|
||||
'#00008b', '#32cd32', '#7f007f', '#b03060', '#ff4500', '#00ced1',
|
||||
'#ffa500', '#00ff00', '#00fa9a', '#8a2be2', '#dc143c', '#00bfff',
|
||||
'#f4a460', '#0000ff', '#f08080', '#adff2f', '#ff00ff', '#1e90ff',
|
||||
'#f0e68c', '#ffff54', '#ff1493', '#7b68ee', '#ee82ee', '#dcdcdc'
|
||||
];
|
||||
let length_colors = ['#3293e2', '#3ad46b', '#facb2a', '#ff8132', '#843a53'];
|
||||
|
||||
window.onload = function(e) {
|
||||
change_color_mode();
|
||||
draw();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
document.getElementById('select').innerHTML = '';
|
||||
document.getElementById('arrows').innerHTML = '';
|
||||
let lw = document.getElementById('linewidth').value;
|
||||
let hover = document.getElementById('hover').checked;
|
||||
let msg = document.getElementById('message');
|
||||
let main = document.getElementById('loops');
|
||||
let arrow = document.getElementById('arrows');
|
||||
let parent = document.getElementById('img');
|
||||
msg.title = '';
|
||||
msg.innerHTML = '';
|
||||
main.innerHTML = '';
|
||||
main.setAttribute('stroke-width', lw + 'px');
|
||||
if (hover) {
|
||||
parent.classList.add('hover');
|
||||
parent.style.cursor = 'crosshair';
|
||||
arrow.setAttribute('onclick', 'loop_hover_click(this)');
|
||||
main.setAttribute('onclick', 'loop_hover_click(this)');
|
||||
} else {
|
||||
parent.classList.remove('hover');
|
||||
parent.removeAttribute('style');
|
||||
arrow.removeAttribute('onclick');
|
||||
main.removeAttribute('onclick');
|
||||
}
|
||||
}
|
||||
|
||||
function draw(changes_graph=false) {
|
||||
let sel = document.getElementById('select')
|
||||
let prev_sel = sel.value;
|
||||
reset();
|
||||
|
||||
let colorMode = document.getElementById('color').value;
|
||||
let mul = document.getElementById('multiply').value;
|
||||
let mod = document.getElementById('modulo').value;
|
||||
// draw background circle and numbers / marks
|
||||
let radius = 440;
|
||||
let center = radius + 60;
|
||||
let circle_points = get_circle_points(center, radius, mod);
|
||||
draw_background(center, radius, circle_points);
|
||||
// calculate loop groups
|
||||
let loops = get_loops(mul, mod);
|
||||
// (optionally) draw arrow heads
|
||||
if (document.getElementById('arrow').checked) {
|
||||
let lw = document.getElementById('linewidth').value;
|
||||
draw_arrows(loops, circle_points, lw);
|
||||
}
|
||||
// draw either individual lines or chained loop
|
||||
if (colorMode == 'segment') {
|
||||
draw_lines_separate(loops, circle_points);
|
||||
} else {
|
||||
draw_lines_group(loops, circle_points);
|
||||
}
|
||||
// apply coloring
|
||||
if (colorMode == 'loop') {
|
||||
set_colors_loop()
|
||||
} else if (colorMode == 'length') {
|
||||
set_colors_loop_length(mod);
|
||||
} else if (colorMode == 'segment') {
|
||||
set_colors_line_length(circle_points);
|
||||
} else if (colorMode == 'select') {
|
||||
set_selection_options()
|
||||
if (prev_sel && !changes_graph) {
|
||||
sel.value = prev_sel;
|
||||
}
|
||||
update_highlight();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// Main loop drawing
|
||||
// -----------------
|
||||
|
||||
function get_circle_points(cp, radius, divisor) {
|
||||
var points = [];
|
||||
let piece = 2 * Math.PI / divisor;
|
||||
for (var i = 0; i < divisor; i++) {
|
||||
let x = radius * Math.cos(i * piece - Math.PI / 2);
|
||||
let y = radius * Math.sin(i * piece - Math.PI / 2);
|
||||
points.push([cp + x, cp + y]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
function get_loops(multiplier, modulo) {
|
||||
var all_loops = [];
|
||||
var visited = [0];
|
||||
for (var i = 1; i < modulo; i++) {
|
||||
if (visited.includes(i)) continue;
|
||||
visited.push(i);
|
||||
var one_loop = [i];
|
||||
var u = i;
|
||||
while (true) {
|
||||
u = (u * multiplier) % modulo;
|
||||
one_loop.push(u);
|
||||
if (visited.includes(u)) break;
|
||||
visited.push(u);
|
||||
}
|
||||
all_loops.push(one_loop);
|
||||
}
|
||||
return all_loops;
|
||||
}
|
||||
|
||||
function draw_background(cp, radius, points) {
|
||||
let fs = document.getElementById('fontsize').value;
|
||||
// show every n-th text
|
||||
let max_chr_count = 1 + Math.floor(Math.log10(points.length - 1));
|
||||
let label_dist = dist(points[0], points[1]);
|
||||
let label_mod = Math.floor(fs / 2 * max_chr_count / label_dist) + 1;
|
||||
// draw circle
|
||||
var src_txt = '';
|
||||
var src_bg = `\n<circle cx="${cp}" cy="${cp}" r="${radius}"/>`;
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
let x = points[i][0] - cp;
|
||||
let y = points[i][1] - cp;
|
||||
if (fs > 0 && i % label_mod == 0) {
|
||||
let chr_count = i < 1 ? 1 : 1 + Math.floor(Math.log10(i));
|
||||
let mi = 1.01; // inner line startpoint multiply
|
||||
let mo = 1.05; // outer line endpoint multiply
|
||||
src_bg += `\n<line x1="${f3(cp + x*mi)}" y1="${f3(cp + y*mi)}" x2="${f3(cp + x*mo)}" y2="${f3(cp + y*mo)}"/>`; // or <path d="Mx1,y1Lx2,y2Z"/>
|
||||
// additional font offset
|
||||
let ls_fix = .15 * 2 * (chr_count-1); // letter spacing fix
|
||||
let fsdx = chr_count * fs / (3.35 + ls_fix) * (1-x/radius*1.2);
|
||||
let fsdy = fs / 3.35 * (1+y/radius*1.5);
|
||||
src_txt += `\n<text x="${f3(cp + x*mo - fsdx)}" y="${f3(cp + y*mo + fsdy)}">${i}</text>`;
|
||||
}
|
||||
}
|
||||
let bgtxt = document.getElementById('bgtxt');
|
||||
bgtxt.setAttribute('font-size', fs + 'px');
|
||||
document.getElementById('bg').innerHTML = src_bg + '\n';
|
||||
bgtxt.innerHTML = src_txt + '\n';
|
||||
}
|
||||
|
||||
function draw_arrows(loops, points, linewidth) {
|
||||
var txt = '';
|
||||
for (var o = 0; o < loops.length; o++) {
|
||||
let loop = loops[o];
|
||||
txt += '\n<path d="';
|
||||
for (var i = 1; i < loop.length; i++) {
|
||||
txt += arrow_path(points[loop[i - 1]], points[loop[i]], linewidth);
|
||||
}
|
||||
txt += '"/>';
|
||||
}
|
||||
document.getElementById('arrows').innerHTML = txt + '\n';
|
||||
}
|
||||
|
||||
function draw_lines_group(loops, points) {
|
||||
var txt = '';
|
||||
for (var o = 0; o < loops.length; o++) {
|
||||
let loop = loops[o];
|
||||
txt += '\n<path d="M' + P(points[loop[0]][0], points[loop[0]][1]);
|
||||
for (var i = 1; i < loop.length; i++) {
|
||||
let pt = points[loop[i]];
|
||||
txt += 'L' + P(pt[0], pt[1]);
|
||||
}
|
||||
txt += `" data-loop="${loop.join(',')}"/>`;
|
||||
}
|
||||
document.getElementById('loops').innerHTML = txt + '\n';
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// Color by loop group
|
||||
// -------------------
|
||||
|
||||
function set_colors_loop() {
|
||||
let children = document.getElementById('loops').children;
|
||||
// colors
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
children[i].setAttribute('stroke', loop_colors[i % loop_colors.length]);
|
||||
}
|
||||
// info message
|
||||
document.getElementById('message').innerHTML = children.length + ' loops';
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Color by loop length
|
||||
// --------------------
|
||||
|
||||
function set_colors_loop_length(total) {
|
||||
let split = 5 * (1 + Math.floor(Math.log10(total)));
|
||||
// colors
|
||||
let children = document.getElementById('loops').children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
let count = children[i].getAttribute('data-loop').split(',').length;
|
||||
let idx = Math.min(Math.floor(count / total * split), length_colors.length - 1);
|
||||
children[i].setAttribute('stroke', length_colors[idx]);
|
||||
}
|
||||
// info message
|
||||
var info = '';
|
||||
for (var i = 0; i < length_colors.length; i++) {
|
||||
let lower = Math.ceil(i * total / split);
|
||||
let upper;
|
||||
if (i + 1 == length_colors.length) { upper = total; }
|
||||
else { upper = Math.floor(((i + 1) * total - 1) / split); }
|
||||
if (lower > upper || upper < 2) continue;
|
||||
info += ' , ' + color_label(i, lower + ' – ' + upper);
|
||||
}
|
||||
document.getElementById('message').innerHTML = info.slice(3);
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// Color by line length
|
||||
// --------------------
|
||||
|
||||
function draw_lines_separate(loops, points) {
|
||||
var txt = '';
|
||||
for (var o = 0; o < loops.length; o++) {
|
||||
let loop = loops[o];
|
||||
txt += '\n<g data-loop="' + loop.join(',') + '">';
|
||||
for (var i = 1; i < loop.length; i++) {
|
||||
let p1 = points[loop[i - 1]];
|
||||
let p2 = points[loop[i]];
|
||||
txt += '\n<path data-len="' + Math.floor(dist(p1, p2)) + '" d="M' + P(p1[0], p1[1]) + 'L' + P(p2[0], p2[1]) + '"/>';
|
||||
}
|
||||
txt += '\n</g>';
|
||||
}
|
||||
document.getElementById('loops').innerHTML = txt + '\n';
|
||||
}
|
||||
|
||||
function set_colors_line_length(points) {
|
||||
let min = dist(points[0], points[1]);
|
||||
let max = Math.max(1, dist(points[0], points[Math.floor(points.length/2)]) - min);
|
||||
var counter = [];
|
||||
var count_dots = 1; // 0 * x is always a point
|
||||
// colors
|
||||
let children = document.querySelectorAll('#loops path');
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
let len = children[i].getAttribute('data-len') ;
|
||||
if (len == 0) {
|
||||
++count_dots;
|
||||
continue;
|
||||
}
|
||||
let percent = (len - min) / max; // OR len / (2 * radius);
|
||||
let idx = Math.round((1 - percent) * (length_colors.length - 1));
|
||||
children[i].setAttribute('stroke', length_colors[idx]);
|
||||
counter[idx] = (counter[idx] || 0) + 1;
|
||||
}
|
||||
// info message
|
||||
var info = '';
|
||||
for (var i = 0; i < length_colors.length; i++) {
|
||||
info += ' , ' + color_label(i, (counter[i] || 0) + ' lines');
|
||||
}
|
||||
info += ' , [' + count_dots + ' points]';
|
||||
document.getElementById('message').innerHTML = info.slice(3);
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// Color by selection (highlight mode)
|
||||
// -----------------------------------
|
||||
|
||||
function set_selection_options() {
|
||||
var txt = '<option value="-1">None</option>';
|
||||
let children = document.getElementById('loops').children;
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
let loop = children[i].getAttribute('data-loop').split(',');
|
||||
let abbrev;
|
||||
if (loop.length > 10) {
|
||||
abbrev = loop.slice(0, 5).join(',') + ` + ${loop.length - 5} more`;
|
||||
} else {
|
||||
abbrev = loop.join(',');
|
||||
}
|
||||
txt += `<option value="${i}">${abbrev}</option>`;
|
||||
}
|
||||
document.getElementById('select').innerHTML = txt;
|
||||
}
|
||||
|
||||
function loop_hover_click(sender) {
|
||||
if (document.getElementById('color').value != 'select') {
|
||||
return;
|
||||
}
|
||||
var node = sender.querySelector(':hover');
|
||||
var indexOfChild = 0;
|
||||
while (node = node.previousSibling) {
|
||||
if (node.nodeType === 1) { ++indexOfChild; }
|
||||
}
|
||||
document.getElementById('select').value = indexOfChild;
|
||||
update_highlight();
|
||||
}
|
||||
|
||||
function update_highlight() {
|
||||
if (document.getElementById('color').value != 'select') {
|
||||
return;
|
||||
}
|
||||
remove_highlight();
|
||||
let msg = document.getElementById('message');
|
||||
let idx = document.getElementById('select').value;
|
||||
let child = document.getElementById('loops').children[idx];
|
||||
let arrow = document.getElementById('arrows').children[idx];
|
||||
if (!child) { msg.innerHTML = ''; return; }
|
||||
child.classList.add('selected');
|
||||
if (arrow) arrow.classList.add('selected');
|
||||
let txt = child.getAttribute('data-loop');
|
||||
msg.innerHTML = txt.length > 52 ? txt.slice(0, 50) + '…' : txt;
|
||||
msg.title = txt;
|
||||
}
|
||||
|
||||
function remove_highlight() {
|
||||
let selected = document.querySelectorAll('.selected');
|
||||
for (var i = selected.length - 1; i >= 0; i--) {
|
||||
selected[i].classList.remove('selected');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Input interaction callbacks
|
||||
// ---------------------------
|
||||
|
||||
function change_color_mode() {
|
||||
let colorMode = document.getElementById('color').value;
|
||||
document.getElementById('select').hidden = colorMode != 'select';
|
||||
}
|
||||
|
||||
function svg_download() {
|
||||
let mul = document.getElementById('multiply').value;
|
||||
let mod = document.getElementById('modulo').value;
|
||||
let svg = document.getElementById('img').innerHTML;
|
||||
let blob = new Blob([svg], {type:'image/svg+xml'});
|
||||
var tmp = document.createElement('a');
|
||||
tmp.href = window.URL.createObjectURL(blob);
|
||||
tmp.download = `graph_x${mul}_mod${mod}.svg`;
|
||||
document.body.appendChild(tmp);
|
||||
tmp.click();
|
||||
document.body.removeChild(tmp);
|
||||
}
|
||||
|
||||
// --------------
|
||||
// Helper methods
|
||||
// --------------
|
||||
|
||||
function f3(num) { return Math.floor(num * 10) / 10; }
|
||||
function P(p1, p2) { return f3(p1) + ',' + f3(p2); }
|
||||
|
||||
function color_label(idx, label) {
|
||||
return '<strong style="color: ' + length_colors[idx] + '">' + label + '</strong>';
|
||||
}
|
||||
|
||||
function dist(p1, p2) {
|
||||
let dx = p1[0] - p2[0];
|
||||
let dy = p1[1] - p2[1];
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
function arrow_path(p1, p2, linewidth) {
|
||||
let arrow_width = linewidth * 2.5;
|
||||
let arrow_length = arrow_width * 3;
|
||||
let arrow_offset = -arrow_length - 20;
|
||||
let diff = dist(p1, p2);
|
||||
let xnorm = (p1[0] - p2[0]) / diff;
|
||||
let ynorm = (p1[1] - p2[1]) / diff;
|
||||
let x1 = p1[0] + arrow_offset * xnorm;
|
||||
let y1 = p1[1] + arrow_offset * ynorm;
|
||||
let x2 = xnorm * arrow_length - ynorm * arrow_width;
|
||||
let y2 = ynorm * arrow_length + xnorm * arrow_width;
|
||||
let x3 = +ynorm * 2 * arrow_width;
|
||||
let y3 = -xnorm * 2 * arrow_width;
|
||||
return 'M' + P(x1, y1) + 'l' + P(x2, y2) + 'l' + P(x3, y3) + 'Z';
|
||||
}
|
||||
83
index.html
Normal file
83
index.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="initial-scale=1.0,maximum-scale=1.0" />
|
||||
<title>Modulo Multiplication Visualizer</title>
|
||||
<script type="text/javascript" src="draw.js"></script>
|
||||
<style type="text/css">
|
||||
body { margin: 10px 0; font-family: Helvetica, sans-serif; }
|
||||
input[type=number] { width: 4em; }
|
||||
header>div { display: inline-block; white-space: nowrap; margin: .2em .5em; }
|
||||
footer { text-align: center; font-size: small; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div>
|
||||
<label for="multiply">Multiplier:</label>
|
||||
<input id="multiply" type="number" name="multiply" value="2" min="2" onchange="draw(true)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="modulo">Modulo:</label>
|
||||
<input id="modulo" type="number" name="modulo" value="9" min="2" onchange="draw(true)">
|
||||
</div>
|
||||
<div>
|
||||
<label for="fontsize">Font size:</label>
|
||||
<input id="fontsize" type="number" name="fontsize" value="16" min="0" onchange="draw()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="linewidth">Line width:</label>
|
||||
<select id="linewidth" name="linewidth" onchange="draw()">
|
||||
<option value="0.125">0.125 px</option>
|
||||
<option value="0.25">0.25 px</option>
|
||||
<option value="0.5">0.5 px</option>
|
||||
<option value="1">1 px</option>
|
||||
<option value="2">2px</option>
|
||||
<option value="4" selected>4px</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="arrow">Arrows:</label>
|
||||
<input id="arrow" name="arrow" type="checkbox" onchange="draw()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="hover">Hover:</label>
|
||||
<input id="hover" name="hover" type="checkbox" onchange="draw()">
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="svg_download()">Download SVG</button>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="color">Color by:</label>
|
||||
<select id="color" name="color" onchange="change_color_mode(); draw()">
|
||||
<option value="none">None</option>
|
||||
<option value="loop" selected>Loop group</option>
|
||||
<option value="length">Loop length</option>
|
||||
<option value="segment">Line length</option>
|
||||
<option value="select">Select</option>
|
||||
</select>
|
||||
|
||||
<select id="select" name="select" onchange="update_highlight()"></select>
|
||||
</div>
|
||||
<div id="message"></div>
|
||||
</header>
|
||||
|
||||
<main id="img"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
||||
<style type="text/css">
|
||||
#loops>.selected { stroke: red; }
|
||||
#arrows>.selected { fill: red; }
|
||||
.hover #loops>*, .hover #arrows>* { opacity: 0.3; }
|
||||
.hover :hover, .hover .selected { opacity: 1.0 !important; }
|
||||
</style>
|
||||
<g id="bgtxt" font-family="Courier" style="letter-spacing:-.15em"></g>
|
||||
<g id="bg" fill="none" stroke="black" stroke-width="1px"></g>
|
||||
<g id="loops" fill="none" stroke="black"></g>
|
||||
<g id="arrows"></g>
|
||||
</svg>
|
||||
</main>
|
||||
|
||||
<footer>What is this? A coding challange, watch this <a href="https://www.youtube.com/watch?v=6ZrO90AI0c8">YouTube video</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user