1
0
mirror of https://github.com/pyscript/pyscript.git synced 2022-05-01 19:47:48 +03:00

Improvements to numpy_canvas_fractals example

This commit is contained in:
Mateusz Paprocki
2022-04-29 23:36:44 +02:00
parent 3be4e2621c
commit d939e5ad15
2 changed files with 260 additions and 30 deletions

View File

@@ -1,4 +1,6 @@
from typing import Tuple
import numpy as np import numpy as np
from numpy.polynomial import Polynomial
def mandelbrot(width: int, height: int, *, def mandelbrot(width: int, height: int, *,
x: float = -0.5, y: float = 0, zoom: int = 1, max_iterations: int = 100) -> np.array: x: float = -0.5, y: float = 0, zoom: int = 1, max_iterations: int = 100) -> np.array:
@@ -60,3 +62,48 @@ def julia(width: int, height: int, *,
div_time[m] = i div_time[m] = i
return div_time return div_time
Range = Tuple[float, float]
def newton(width: int, height: int, *,
p: Polynomial, a: complex, xr: Range = (-2.5, 1), yr: Range = (-1, 1), max_iterations: int = 100) -> (np.array, np.array):
""" """
# To make navigation easier we calculate these values
x_from, x_to = xr
y_from, y_to = yr
# Here the actual algorithm starts
x = np.linspace(x_from, x_to, width).reshape((1, width))
y = np.linspace(y_from, y_to, height).reshape((height, 1))
z = x + 1j*y
# Compute the derivative
dp = p.deriv()
# Compute roots
roots = p.roots()
epsilon = 1e-5
# Set the initial conditions
a = np.full(z.shape, a)
# To keep track in which iteration the point diverged
div_time = np.zeros(z.shape, dtype=int)
# To keep track on which points did not converge so far
m = np.full(a.shape, True, dtype=bool)
# To keep track which root each point converged to
r = np.full(a.shape, 0, dtype=int)
for i in range(max_iterations):
z[m] = z[m] - a[m]*p(z[m])/dp(z[m])
for j, root in enumerate(roots):
converged = (np.abs(z.real - root.real) < epsilon) & (np.abs(z.imag - root.imag) < epsilon)
m[converged] = False
r[converged] = j + 1
div_time[m] = i
return div_time, r

View File

@@ -1,6 +1,6 @@
<html> <html>
<head> <head>
<title>Visualization of Mandelbrot and Julia sets with NumPy and HTML5 canvas</title> <title>Visualization of Mandelbrot, Julia and Newton sets with NumPy and HTML5 canvas</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="../build/pyscript.css" /> <link rel="stylesheet" href="../build/pyscript.css" />
@@ -17,6 +17,10 @@
animation: spin 1s ease-in-out infinite; animation: spin 1s ease-in-out infinite;
} }
canvas {
display: none;
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@@ -28,35 +32,68 @@
<body> <body>
<b> <b>
</b> </b>
<div style="display: flex; flex-direction: row; gap: 1em"> <div style="display: flex; flex-direction: column; gap: 1em; width: 600px">
<div> <div id="mandelbrot">
<div style="text-align: center">Mandelbrot set</div> <div style="text-align: center">Mandelbrot set</div>
<div id="mandelbrot" style="width: 600px; height: 600px">
<div class="loading"></div>
</div>
</div>
<div> <div>
<div style="text-align: center">Julia set</div>
<div id="julia" style="width: 600px; height: 600px">
<div class="loading"></div> <div class="loading"></div>
<canvas></canvas>
</div>
</div>
<div id="julia">
<div style="text-align: center">Julia set</div>
<div>
<div class="loading"></div>
<canvas></canvas>
</div>
</div>
<div id="newton">
<div style="text-align: center">Newton set</div>
<fieldset style="display: flex; flex-direction: row; gap: 1em">
<div><span style="white-space: pre">p(z) = </span><input id="poly" type="text" value="z**3 - 2*z + 2"></div>
<div><span style="white-space: pre">a = </span><input id="coef" type="text" value="1" style="width: 40px"></div>
<div style="display: flex; flex-direction: row">
<span style="white-space: pre">x = [</span>
<input id="x0" type="text" value="-2.5" style="width: 80px; text-align: right">
<span style="white-space: pre">, </span>
<input id="x1" type="text" value="2.5" style="width: 80px; text-align: right">
<span style="white-space: pre">]</span>
</div>
<div style="display: flex; flex-direction: row">
<span style="white-space: pre">y = [</span>
<input id="y0" type="text" value="-5.0" style="width: 80px; text-align: right">
<span style="white-space: pre">, </span>
<input id="y1" type="text" value="5.0" style="width: 80px; text-align: right">
<span style="white-space: pre">]</span>
</div>
<div style="display: flex; flex-direaction: row; gap: 1em">
<div style="white-space: pre"><input type="radio" id="conv" name="type" value="convergence" checked> convergence</div>
<div style="white-space: pre"><input type="radio" id="iter" name="type" value="iterations"> iterations</div>
</div>
</fieldset>
<div>
<div class="loading"></div>
<canvas></canvas>
</div> </div>
</div> </div>
</div> </div>
<py-env> <py-env>
- numpy - numpy
- sympy
- paths: - paths:
- /palettes.py - /palettes.py
- /fractals.py - /fractals.py
</py-env> </py-env>
<py-script> <py-script>
from pyodide import to_js from pyodide import to_js, create_proxy
import numpy as np import numpy as np
import sympy
from palettes import Magma256 from palettes import Magma256
from fractals import mandelbrot, julia from fractals import mandelbrot, julia, newton
from js import ( from js import (
console, console,
@@ -65,28 +102,20 @@ from js import (
ImageData, ImageData,
Uint8ClampedArray, Uint8ClampedArray,
CanvasRenderingContext2D as Context2d, CanvasRenderingContext2D as Context2d,
requestAnimationFrame,
) )
def create_canvas(width: int, height: int, target: str) -> Context2d: def prepare_canvas(width: int, height: int, canvas: Element) -> Context2d:
pixel_ratio = devicePixelRatio
canvas = document.createElement("canvas")
ctx = canvas.getContext("2d") ctx = canvas.getContext("2d")
canvas.style.width = f"{width}px" canvas.style.width = f"{width}px"
canvas.style.height = f"{height}px" canvas.style.height = f"{height}px"
canvas.width = width*pixel_ratio canvas.width = width
canvas.height = height*pixel_ratio canvas.height = height
ctx.scale(pixel_ratio, pixel_ratio)
ctx.translate(0.5, 0.5)
ctx.clearRect(0, 0, width, height) ctx.clearRect(0, 0, width, height)
el = document.querySelector(target)
el.replaceChildren(canvas)
return ctx return ctx
def color_map(array: np.array, palette: np.array) -> np.array: def color_map(array: np.array, palette: np.array) -> np.array:
@@ -105,30 +134,184 @@ def draw_image(ctx: Context2d, image: np.array) -> None:
image_data = ImageData.new(data, width, height) image_data = ImageData.new(data, width, height)
ctx.putImageData(image_data, 0, 0) ctx.putImageData(image_data, 0, 0)
def draw_mandelbrot(width: int, height: int) -> None: width, height = 600, 600
ctx = create_canvas(width, height, "#mandelbrot")
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.log("Computing Mandelbrot set ...")
console.time("mandelbrot") console.time("mandelbrot")
array = mandelbrot(width, height) iters = mandelbrot(width, height)
console.timeEnd("mandelbrot") console.timeEnd("mandelbrot")
image = color_map(array, Magma256) image = color_map(iters, Magma256)
draw_image(ctx, image) draw_image(ctx, image)
def draw_julia(width: int, height: int) -> None: spinner.style.display = "none"
ctx = create_canvas(width, height, "#julia") 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.log("Computing Julia set ...")
console.time("julia") console.time("julia")
array = julia(width, height) iters = julia(width, height)
console.timeEnd("julia") console.timeEnd("julia")
image = color_map(array, Magma256) image = color_map(iters, Magma256)
draw_image(ctx, image) draw_image(ctx, image)
draw_mandelbrot(600, 600) spinner.style.display = "none"
draw_julia(600, 600) 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(),
)
</py-script> </py-script>
</body> </body>