summaryrefslogtreecommitdiff
path: root/html/toys/julia
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-07-05 12:42:44 -0700
committerElizabeth Alexander Hunt <me@liz.coffee>2026-07-05 12:42:44 -0700
commit94b912b649150b9863a62096ba4740b2cc8ad21a (patch)
tree1882236fc980fa5946d3089a2f2e4670f3a70580 /html/toys/julia
parentab0ddcaeb1fc37351d14b89100e07d3a343ba9e1 (diff)
downloadlizdotcoffee-94b912b649150b9863a62096ba4740b2cc8ad21a.tar.gz
lizdotcoffee-94b912b649150b9863a62096ba4740b2cc8ad21a.zip
Factor
Diffstat (limited to 'html/toys/julia')
-rw-r--r--html/toys/julia/index.html51
-rw-r--r--html/toys/julia/julia.js143
2 files changed, 194 insertions, 0 deletions
diff --git a/html/toys/julia/index.html b/html/toys/julia/index.html
new file mode 100644
index 0000000..9d9de51
--- /dev/null
+++ b/html/toys/julia/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <style>
+ body {
+ height: 100vh;
+ width: 100vw;
+ margin: 0;
+ }
+ .bottomRight {
+ padding: 12px;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ background-color: rgba(255,255,255,0.5);
+ border: 1px solid white;
+ border-radius: 8px;
+ margin-right: 6px;
+ margin-bottom: 6px;
+ }
+ .verticalSlider
+ {
+ writing-mode: bt-lr; /* IE */
+ -webkit-appearance: slider-vertical; /* Chromium */
+ width: 8px;
+ padding: 0 5px;
+ }
+ </style>
+ </head>
+ <body>
+ <div id="canvasHolder">
+
+ </div>
+ <div class="bottomRight">
+ <input orient="vertical" type="range" min="-1000" max="1000" value="0" id="imaginarySlider" class="verticalSlider">
+ <input type="range" min="-1000" max="1000" value="0" id="realSlider">
+ <br>
+ imaginary: <input id="imag-val" size="10" type="number" step="0.001"></input>
+ <button id="animate-imag" onclick="startAnim('animate-imag', 'ci')">Animate</button>
+ <br>
+ real: <input id="real-val" size="10" type="number" step="0.001"></input>
+ <button id="animate-real" onclick="startAnim('animate-real', 'cr')">Animate</button>
+ <br>
+ powered by <a href="https://github.com/gpujs/gpu.js">gpu.js</a>
+ </div>
+
+ <script src="https://cdn.jsdelivr.net/npm/gpu.js@latest/dist/gpu-browser.min.js"></script>
+ <script src="julia.js"></script>
+ </body>
+</html>
diff --git a/html/toys/julia/julia.js b/html/toys/julia/julia.js
new file mode 100644
index 0000000..0744e6f
--- /dev/null
+++ b/html/toys/julia/julia.js
@@ -0,0 +1,143 @@
+const MIN_ZOOM = 0.0001;
+const MAX_ZOOM = 4;
+const C_THRESHOLD = Math.sqrt(2)/2;
+const SLIDER_DIV = 2000*C_THRESHOLD;
+
+function initGPU() {
+ try {
+ return new window.GPU.GPU();
+ } catch (e) {
+ return new GPU();
+ }
+}
+const gpu = initGPU();
+const buildRender = (width, height) => gpu.createKernel(function (maxIterations, cr, ci, centerX, centerY, zoom, colorMultipliers) {
+ let zx = (this.output.x / this.output.y) * (centerX + (4 * this.thread.x / this.output.x - 2) * (zoom / 4));
+ let zy = centerY + (4 * this.thread.y / this.output.y - 2) * (zoom / 4);
+ let iterations = 0;
+ for (let i = 0; i < maxIterations; i++) {
+ const xtemp = zx * zx - zy * zy + cr;
+ zy = 2 * zx * zy + ci;
+ zx = xtemp;
+ if (zx * zx + zy * zy > 4) {
+ iterations = i;
+ break;
+ }
+ }
+ if (iterations == 0 || iterations == maxIterations) {
+ this.color(0, 0, 0);
+ } else {
+ this.color(colorMultipliers[0] * iterations, colorMultipliers[1] * iterations, colorMultipliers[2] * iterations);
+ }
+}, { output: [width, height], graphical: true });
+
+const canvasHolder = document.getElementById('canvasHolder');
+let render; // The GPU kernel built from buildRender
+let state = {
+ colorMultipliers: [0.01 * Math.random() + 0.015, 0.03 * Math.random() + 0.007, 0.02 * Math.random() + 0.010],
+ changes: {
+ centerX: 0,
+ centerY: 0,
+ zoom: 3,
+ cr: parseFloat(document.getElementById('realSlider').value) / SLIDER_DIV,
+ ci: parseFloat(document.getElementById('imaginarySlider').value) / SLIDER_DIV,
+ maxIterations: 1000,
+ width: document.body.clientWidth,
+ height: document.body.clientHeight,
+ },
+};
+
+const doRender = (renderF, state) => {
+ // gpu.js doesn't support JS objects as kernel parameters - https://github.com/gpujs/gpu.js/issues/245
+ renderF(state.maxIterations, state.cr, state.ci, state.centerX, state.centerY, state.zoom, state.colorMultipliers);
+};
+
+const loop = () => {
+ const stateChanges = Object.keys(state.changes);
+ if (stateChanges.length > 0) {
+ state = {...state, ...state.changes};
+ if (state.changes.width || state.changes.height) {
+ render = buildRender(state.width, state.height);
+ canvasHolder.appendChild(render.canvas);
+ }
+ if (typeof state.changes.ci !== 'undefined') {
+ document.getElementById('imag-val').value = state.ci.toFixed(4);
+ document.getElementById('imaginarySlider').value = state.changes.ci * SLIDER_DIV;
+ }
+ if (typeof state.changes.cr !== 'undefined') {
+ document.getElementById('real-val').value = state.cr.toFixed(4);
+ document.getElementById('realSlider').value = state.changes.cr * SLIDER_DIV;
+ }
+ state.changes = {};
+ doRender(render, state);
+ }
+
+ requestAnimationFrame(loop);
+};
+
+loop();
+
+// UI Code
+
+const startAnim = (sliderId, complexComponentName='ci') => {
+ const restart = (interval) => {
+ clearInterval(interval);
+ document.getElementById(sliderId).innerHTML = 'Animate';
+ document.getElementById(sliderId).onclick = ()=>startAnim(sliderId, complexComponentName);
+ return;
+ };
+ const start = setInterval(() => {
+ if (state[complexComponentName] >= C_THRESHOLD) {
+ restart(start);
+ }
+
+ state.changes[complexComponentName] = state[complexComponentName] + 0.001;
+ }, 1000/60);
+ document.getElementById(sliderId).innerHTML = 'Stop';
+ document.getElementById(sliderId).onclick = ()=>restart(start);
+};
+
+document.getElementById('imaginarySlider').oninput = function() {
+ state.changes.ci = parseFloat(this.value) / SLIDER_DIV;
+};
+document.getElementById('realSlider').oninput = function() {
+ state.changes.cr = parseFloat(this.value) / SLIDER_DIV;
+};
+
+document.getElementById('imag-val').addEventListener('change', function (e) {
+ state.changes.ci = parseFloat(this.value);
+});
+document.getElementById('real-val').addEventListener('change', function (e) {
+ state.changes.cr = parseFloat(this.value);
+});
+
+canvasHolder.addEventListener('wheel', (e) => {
+ e.preventDefault();
+ state.changes.zoom = Math.min(Math.max(state.zoom + e.deltaY * 0.001 * state.zoom, MIN_ZOOM), MAX_ZOOM);
+});
+
+let isDown = false;
+canvasHolder.addEventListener('mousedown', (e) => {
+ e.preventDefault();
+ isDown = true;
+}, true);
+
+canvasHolder.addEventListener('mouseup', (e) => {
+ e.preventDefault();
+ isDown = false;
+}, true);
+
+canvasHolder.addEventListener('mousemove', (e) => {
+ e.preventDefault();
+ if (isDown) {
+ let deltaX = -e.movementX * state.zoom / document.body.clientWidth;
+ let deltaY = e.movementY * state.zoom / document.body.clientHeight;
+ state.changes.centerX = state.centerX + deltaX;
+ state.changes.centerY = state.centerY + deltaY;
+ }
+}, true);
+
+window.addEventListener('resize', () => {
+ state.changes.width = document.body.clientWidth;
+ state.changes.height = document.body.clientHeight;
+});