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));