fractal.js

4.4 kB · javascript · 138 lines

1import { pick, rgb } from './frame.js';2import { build, drop } from './gl.js';34const CYCLE = 65536;5const FADE = 2048;6const MAX_ZOOM = 128;7const ROT = 1 / 2048;8const COLOR = 1 / 256;9const START_SCALE = 1 / 2;10const SAMPLES = 200;11const PROBE = 150;1213const MANDELBROT = { xMin: -2, xMax: 1, yMin: -1.5, yMax: 1.5 };14const JULIA = { xMin: -1.5, xMax: 1.5, yMin: -1.5, yMax: 1.5 };15const PRESETS = [[-0.4, 0.6], [-0.8, 0.156], [0.285, 0.01], [-0.7269, 0.1889], [-0.1, 0.651], [0.355, 0.355]];1617/* MATH */1819function fit(v, w, h) {20  const vw = v.xMax - v.xMin;21  const vh = v.yMax - v.yMin;22  const ca = w / h;23  const cx = (v.xMin + v.xMax) / 2;24  const cy = (v.yMin + v.yMax) / 2;25  if (ca > vw / vh) {26    const nw = vh * ca;27    return { xMin: cx - nw / 2, xMax: cx + nw / 2, yMin: v.yMin, yMax: v.yMax };28  }29  const nh = vw / ca;30  return { xMin: v.xMin, xMax: v.xMax, yMin: cy - nh / 2, yMax: cy + nh / 2 };31}3233function autoMaxIter(zoom) {34  return 100 + Math.floor(50 * Math.log2(Math.max(zoom, 1)));35}3637function escaper(seed) {38  return (px, py, max) => {39    let zr = seed ? px : 0;40    let zi = seed ? py : 0;41    const cr = seed ? seed[0] : px;42    const ci = seed ? seed[1] : py;43    let n = 0;44    while (zr * zr + zi * zi <= 4 && n < max) {45      const t = zr * zr - zi * zi + cr;46      zi = 2 * zr * zi + ci;47      zr = t;48      n++;49    }50    return n;51  };52}5354export function wayfind(escape, v, rand) {55  let best = -1;56  let x = (v.xMin + v.xMax) / 2;57  let y = (v.yMin + v.yMax) / 2;58  for (let i = 0; i < SAMPLES; i++) {59    const px = v.xMin + rand() * (v.xMax - v.xMin);60    const py = v.yMin + rand() * (v.yMax - v.yMin);61    const n = escape(px, py, PROBE);62    const score = n < PROBE ? n : 0;63    if (score > best) {64      best = score;65      x = px;66      y = py;67    }68  }69  return { x, y };70}7172/* SAVER */7374function fractal(canvas, view, home, seed) {75  const gl = canvas.getContext('webgl2', { powerPreference: 'low-power' });76  if (!gl) return { draw: () => {} };77  const kit = build(gl);78  if (!kit) return { draw: () => {}, stop: () => gl.getExtension('WEBGL_lose_context')?.loseContext() };79  const loc = kit.loc;80  const rand = view.rand;81  const escape = escaper(seed);82  const wide = () => {83    const cx = (home.xMin + home.xMax) / 2;84    const cy = (home.yMin + home.yMax) / 2;85    const hw = (home.xMax - home.xMin) / START_SCALE / 2;86    const hh = (home.yMax - home.yMin) / START_SCALE / 2;87    return fit({ xMin: cx - hw, xMax: cx + hw, yMin: cy - hh, yMax: cy + hh }, view.w, view.h);88  };89  let start = wide();90  let target = wayfind(escape, start, rand);91  let accent = rgb(view.look().accent);92  let dir = rand() < 0.5 ? 1 : -1;93  let born = performance.now();94  let rotation = rand() * Math.PI * 2;95  let clock = 0;96  const again = () => {97    start = wide();98    target = wayfind(escape, start, rand);99    accent = rgb(view.look().accent);100    dir = rand() < 0.5 ? 1 : -1;101    rotation = rand() * Math.PI * 2;102    born = performance.now();103  };104  const draw = () => {105    if (performance.now() - born >= CYCLE) again();106    const age = performance.now() - born;107    const opacity = view.still ? 1 : Math.max(0, Math.min(age / FADE, (CYCLE - age) / FADE, 1));108    canvas.style.opacity = String(opacity);109    if (opacity < 0.01) return;110    const zoom = Math.pow(MAX_ZOOM, age / CYCLE);111    const vw = (start.xMax - start.xMin) / zoom;112    const vh = (start.yMax - start.yMin) / zoom;113    rotation += ROT * dir;114    clock += COLOR;115    const primary = rgb(view.look().paper);116    gl.viewport(0, 0, view.w, view.h);117    gl.uniform2f(loc.resolution, view.w, view.h);118    gl.uniform4f(loc.viewport, target.x - vw / 2, target.x + vw / 2, target.y - vh / 2, target.y + vh / 2);119    gl.uniform1i(loc.julia, seed ? 1 : 0);120    gl.uniform2f(loc.c, seed ? seed[0] : 0, seed ? seed[1] : 0);121    gl.uniform1i(loc.maxIter, autoMaxIter(zoom));122    gl.uniform3f(loc.primary, primary[0] / 255, primary[1] / 255, primary[2] / 255);123    gl.uniform3f(loc.accent, accent[0] / 255, accent[1] / 255, accent[2] / 255);124    gl.uniform1f(loc.rotation, rotation);125    gl.uniform1f(loc.time, clock);126    gl.drawArrays(gl.TRIANGLES, 0, 6);127  };128  const stop = () => {129    canvas.style.opacity = '';130    drop(gl, kit);131  };132  const theme = () => (accent = rgb(view.look().accent));133  return { draw, size: () => (start = wide()), theme, stop };134}135136export const mandelbrot = (canvas, view) => fractal(canvas, view, MANDELBROT, null);137138export const julia = (canvas, view) => fractal(canvas, view, JULIA, pick(view.rand, PRESETS));