Skip to content

Instantly share code, notes, and snippets.

@KusaReMKN
Created March 17, 2023 15:33
Show Gist options
  • Save KusaReMKN/201764078be8e185bbd46f29b672aff5 to your computer and use it in GitHub Desktop.
Save KusaReMKN/201764078be8e185bbd46f29b672aff5 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>mandelbrot</title>
<style>
body {
width: 80%;
margin: 1rem auto;
text-align: center;
}
canvas, img {
width: min(100%, calc(100vmin - 20%));
}
</style>
</head>
<body>
<h1>マンデルブロ集合で遊ぼう!</h1>
<p>
画像をクリックすると、その地点を中心として 2 倍拡大します。<br>
右クリックすると中心を変更せずに 2 倍縮小します。<br>
r の値が e-14 くらいになると計算精度の限界がやってきます。<br>
いい方法を考えているところです。<br>
</p>
<div id="canvas"></div>
<div>
<progress id="prog" style="width: 100%;"></progress><br>
<label>Iteration: <input type="number" value="100" min="0" max="1e5" step="1" id="iter"></label>
<label>Size:
<select id="size">
<optgroup label="軽量画質">
<option value="512">512</option>
</optgroup>
<optgroup label="標準画質">
<option value="1024">1024</option>
<option value="2048">2048</option>
</optgroup>
<optgroup label="演算負荷に注意">
<option value="4096">4096</option>
<option value="8192">8192</option>
</optgroup>
</select>
</label>
<button id="draw">Draw</button>
<button id="save">Save</button><br>
<output id="time"></output><br>
<output id="info"></output>
</div>
<script>
'use strict';
const canvas = document.createElement('canvas');
const img = document.createElement('img');
const disp = img;
const ctx = canvas.getContext('2d');
let width = 512, height = 512;
let checkmax = 100;
let xrange = 4;
let yrange = 4;
let xcenter = 0, ycenter = 0;
const xdisp = () => disp.width;
const ydisp = () => disp.height;
const xmin = () => -xrange / 2 + xcenter;
const ymin = () => -yrange / 2 + ycenter;
const iter = document.getElementById('iter');
const size = document.getElementById('size');
const draw = document.getElementById('draw');
const save = document.getElementById('save');
const time = document.getElementById('time');
const prog = document.getElementById('prog');
const info = document.getElementById('info');
[canvas.width, canvas.height] = [width, height];
document.getElementById('canvas').appendChild(disp);
draw.addEventListener('click', () => Mandelbrot());
save.addEventListener('click', () => {
const a = document.createElement('a');
a.href = img.src;
a.target = '_blank';
a.download = `${Date.now()}.png`;
a.click();
});
iter.addEventListener('change', () => checkmax = +iter.value);
size.addEventListener('change', () => width = height = +size.value);
disp.addEventListener('click', e => {
if (draw.disabled === true) {
ControlFree();
return;
}
xcenter = (e.offsetX / xdisp() - .5) * xrange + xcenter;
ycenter = (e.offsetY / ydisp() - .5) * yrange + ycenter;
xrange /= 2; yrange /= 2;
Mandelbrot();
});
disp.addEventListener('contextmenu', e => {
e.preventDefault();
if (draw.disabled === true) return;
xrange *= 2; yrange *= 2;
Mandelbrot();
});
function updateMinMax()
{
[canvas.width, canvas.height] = [width, height];
}
function Power2(r, i)
{
return [(r + i) * (r - i), r * i * 2]; // (r^2 - i^2, ri2)
}
function Absolute(r, i)
{
return Math.hypot(r, i);
}
function CheckDivergence(r, i)
{
const cr = r, ci = i;
for (let n = 0; n < checkmax; n++) {
if (Absolute(r, i) > 2) return n;
[r, i] = Power2(r, i);
r += cr; i += ci;
}
return -1;
}
async function Mandelbrot2(k)
{
const xk = 2 ** k, yk = 2 ** k;
for (let i = 0; i < height; i += yk) {
for (let j = 0; j < width; j += xk) {
const x = j / width * xrange + xmin();
const y = i / height * yrange + ymin();
let n;
if (draw.disabled === false) return; // 描画キャンセル
if ((n = CheckDivergence(x, y)) !== -1) {
const deg = 360 * Math.log1p(n) / Math.log1p(checkmax);
ctx.fillStyle = `hsl(${deg}, 100%, 50%)`;
} else {
ctx.fillStyle = 'black';
}
ctx.fillRect(j, i, xk, yk);
if (k === 0) prog.value = (i * height + j) / (height * width);
}
if (i % 7 === 0) await new Promise(r => setTimeout(() => r()));
}
img.src = canvas.toDataURL();
}
function ControlLock()
{
draw.disabled = iter.disabled = size.disabled = save.disabled = true;
}
function ControlFree()
{
draw.disabled = iter.disabled = size.disabled = save.disabled = false;
}
async function Mandelbrot()
{
const start = window.performance.now();
time.textContent = 'Drawing ... ';
updateMinMax();
ControlLock();
// for (let i = 0; i < height; i++) {
// for (let j = 0; j < width; j++) {
// let x = j / width * xrange + xmin;
// let y = i / height * yrange + ymin;
// let n;
// if ((n = CheckDivergence(x, y)) !== -1)
// ctx.fillStyle =
// `hsl(${360 * (n / checkmax) ** 1}, 100%, 50%)`;
// else
// ctx.fillStyle = 'black';
// ctx.fillRect(j, i, 1, 1);
// prog.value = (i * height + j) / (height * width);
// }
// prog.value = i / height;
// await new Promise(r => setTimeout(() => r()));
// }
const promise = [];
info.textContent = `x: ${xcenter}\ny: ${ycenter}\nr: ${xrange}`;
for (let i = Math.floor(Math.log2(width)); i >= 0 && draw.disabled === true; i--) {
promise.push(Mandelbrot2(i));
await new Promise(r => setTimeout(() => r()));
}
await Promise.all(promise);
time.textContent += `Done. (${Math.floor(window.performance.now() - start)} [ms])`;
ControlFree();
}
Mandelbrot();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment