Mandelbrot set
Julia set
Newton set
p(z) =
a =
x = [ , ]
y = [ , ]
convergence
iterations
{ "packages": [ "numpy", "sympy" ], "paths": [ "./palettes.py", "./fractals.py" ] } from pyodide.ffi import to_js, create_proxy import numpy as np import sympy from palettes import Magma256 from fractals import mandelbrot, julia, newton from js import ( console, document, devicePixelRatio, ImageData, Uint8ClampedArray, CanvasRenderingContext2D as Context2d, requestAnimationFrame, ) def prepare_canvas(width: int, height: int, canvas: Element) -> Context2d: ctx = canvas.getContext("2d") canvas.style.width = f"{width}px" canvas.style.height = f"{height}px" canvas.width = width canvas.height = height ctx.clearRect(0, 0, width, height) return ctx def color_map(array: np.array, palette: np.array) -> np.array: size, _ = palette.shape index = (array/array.max()*(size - 1)).round().astype("uint8") width, height = array.shape image = np.full((width, height, 4), 0xff, dtype=np.uint8) image[:, :, :3] = palette[index] return image def draw_image(ctx: Context2d, image: np.array) -> None: data = Uint8ClampedArray.new(to_js(image.tobytes())) width, height, _ = image.shape image_data = ImageData.new(data, width, height) ctx.putImageData(image_data, 0, 0) width, height = 600, 600 async def draw_mandelbrot() -> None: spinner = document.querySelector("#mandelbrot .loading") canvas = document.querySelector("#mandelbrot canvas") spinner.style.display = "" canvas.style.display = "none" ctx = prepare_canvas(width, height, canvas) console.log("Computing Mandelbrot set ...") console.time("mandelbrot") iters = mandelbrot(width, height) console.timeEnd("mandelbrot") image = color_map(iters, Magma256) draw_image(ctx, image) spinner.style.display = "none" canvas.style.display = "block" async def draw_julia() -> None: spinner = document.querySelector("#julia .loading") canvas = document.querySelector("#julia canvas") spinner.style.display = "" canvas.style.display = "none" ctx = prepare_canvas(width, height, canvas) console.log("Computing Julia set ...") console.time("julia") iters = julia(width, height) console.timeEnd("julia") image = color_map(iters, Magma256) draw_image(ctx, image) spinner.style.display = "none" canvas.style.display = "block" def ranges(): x0_in = document.querySelector("#x0") x1_in = document.querySelector("#x1") y0_in = document.querySelector("#y0") y1_in = document.querySelector("#y1") xr = (float(x0_in.value), float(x1_in.value)) yr = (float(y0_in.value), float(y1_in.value)) return xr, yr current_image = None async def draw_newton() -> None: spinner = document.querySelector("#newton .loading") canvas = document.querySelector("#newton canvas") spinner.style.display = "" canvas.style.display = "none" ctx = prepare_canvas(width, height, canvas) console.log("Computing Newton set ...") poly_in = document.querySelector("#poly") coef_in = document.querySelector("#coef") conv_in = document.querySelector("#conv") iter_in = document.querySelector("#iter") xr, yr = ranges() # z**3 - 1 # z**8 + 15*z**4 - 16 # z**3 - 2*z + 2 expr = sympy.parse_expr(poly_in.value) coeffs = [ complex(c) for c in reversed(sympy.Poly(expr, sympy.Symbol("z")).all_coeffs()) ] poly = np.polynomial.Polynomial(coeffs) coef = complex(sympy.parse_expr(coef_in.value)) console.time("newton") iters, roots = newton(width, height, p=poly, a=coef, xr=xr, yr=yr) console.timeEnd("newton") if conv_in.checked: n = poly.degree() + 1 k = int(len(Magma256)/n) colors = Magma256[::k, :][:n] colors[0, :] = [255, 0, 0] # red: no convergence image = color_map(roots, colors) else: image = color_map(iters, Magma256) global current_image current_image = image draw_image(ctx, image) spinner.style.display = "none" canvas.style.display = "block" handler = create_proxy(lambda _event: draw_newton()) document.querySelector("#newton fieldset").addEventListener("change", handler) canvas = document.querySelector("#newton canvas") is_selecting = False init_sx, init_sy = None, None sx, sy = None, None async def mousemove(event): global is_selecting global init_sx global init_sy global sx global sy def invert(sx, source_range, target_range): source_start, source_end = source_range target_start, target_end = target_range factor = (target_end - target_start)/(source_end - source_start) offset = -(factor * source_start) + target_start return (sx - offset) / factor bds = canvas.getBoundingClientRect() event_sx, event_sy = event.clientX - bds.x, event.clientY - bds.y ctx = canvas.getContext("2d") pressed = event.buttons == 1 if is_selecting: if not pressed: xr, yr = ranges() x0 = invert(init_sx, xr, (0, width)) x1 = invert(sx, xr, (0, width)) y0 = invert(init_sy, yr, (0, height)) y1 = invert(sy, yr, (0, height)) document.querySelector("#x0").value = x0 document.querySelector("#x1").value = x1 document.querySelector("#y0").value = y0 document.querySelector("#y1").value = y1 is_selecting = False init_sx, init_sy = None, None sx, sy = init_sx, init_sy await draw_newton() else: ctx.save() ctx.clearRect(0, 0, width, height) draw_image(ctx, current_image) sx, sy = event_sx, event_sy ctx.beginPath() ctx.rect(init_sx, init_sy, sx - init_sx, sy - init_sy) ctx.fillStyle = "rgba(255, 255, 255, 0.4)" ctx.strokeStyle = "rgba(255, 255, 255, 1.0)" ctx.fill() ctx.stroke() ctx.restore() else: if pressed: is_selecting = True init_sx, init_sy = event_sx, event_sy sx, sy = init_sx, init_sy canvas.addEventListener("mousemove", create_proxy(mousemove)) import asyncio _ = await asyncio.gather( draw_mandelbrot(), draw_julia(), draw_newton(), )