print("Starting up...") from array import array import asyncio import math import time from pyscript import document, window, PyWorker from libthree import THREE, clear, SoundPlayer from libthree import get_renderer, get_ortho_camera from libthree import get_loading_manager, get_stats_gl from libthree import line2, linemat, lsgeo from libfft import BeatSync from multipyjs import MICROPYTHON, new, call, to_js, create_proxy from js import Float32Array scene = new(THREE.Scene) view_size = 1 renderer = get_renderer() camera = get_ortho_camera(view_size) loading_mgr, loaded_event = get_loading_manager() t_loader = new(THREE.TextureLoader, loading_mgr) t_loader.setPath('assets/') light = new(THREE.AmbientLight, 0xffffff, 1.0) scene.add(light) fft_res = 2048 audio_listener = new(THREE.AudioListener) camera.add(audio_listener) sound = new(THREE.Audio, audio_listener) audio_loader = new(THREE.AudioLoader, loading_mgr) analyser = new(THREE.AudioAnalyser, sound, fft_res) @create_proxy def on_audio_load(buffer): sound.setBuffer(buffer) sound.setVolume(0.9) sound.setLoop(False) audio_loader.load("assets/genuary25-18.m4a", on_audio_load) spheres = new(THREE.Group) scene.add(spheres) line_basic_mat = new( THREE.LineBasicMaterial, color=0xffffff, ) zero_mat = new( linemat.LineMaterial, color=0x662503, linewidth=3, ) other_mat = new( linemat.LineMaterial, color=0x662503, linewidth=1.5, ) grid_mat = new( linemat.LineMaterial, color=0x662503, linewidth=1, dashed=True, dashScale=1, dashSize=0.5, gapSize=1, dashOffset=0, ) lines = [new(THREE.Group), new(THREE.Group)] scene.add(lines[0]) scene.add(lines[1]) def draw_lines(line_coords, mat_name, spy=False): if spy: line_coords_f32a = new(Float32Array, line_coords.length) _it = line_coords.items for i in range(line_coords.length): line_coords_f32a[i] = _it[i] else: line_coords_f32a = new(Float32Array, line_coords) if mat_name == 'zero': mat = zero_mat elif mat_name == 'grid': mat = grid_mat else: mat = other_mat geo = new(THREE.BufferGeometry) geo.setAttribute('position', new(THREE.BufferAttribute, line_coords_f32a, 3)) seg = new(THREE.LineSegments, geo, line_basic_mat) lsg = new(lsgeo.LineSegmentsGeometry) lsg.fromLineSegments(seg) l1 = new(line2.Line2, lsg, mat) l1.computeLineDistances() l2 = new(line2.Line2, lsg, mat) l2.computeLineDistances() lines[0].add(l1) lines[1].add(l2) seg.geometry.dispose() del geo del seg def drawing_done(): maybe_with_spy = "with SPy" if USE_SPY else "with pure Python" print(f"Time elapsed computing {maybe_with_spy}:", time.time() - start_ts) drawing_event.set() grid_width = 0 grid_height = 0 scroll_offset = 0 def scale_lines(grid_ws=None, grid_hs=None, offset=None): global grid_width, grid_height, scroll_offset if grid_ws: grid_width = grid_ws else: grid_ws = grid_width if grid_hs: grid_height = grid_hs else: grid_hs = grid_height if offset: scroll_offset = offset else: offset = scroll_offset scale = 2.04/grid_hs lines[0].scale.set(scale, scale, scale) lines[1].scale.set(scale, scale, scale) lines[0].position.set((offset - grid_ws/2) * scale, -grid_hs/2 * scale, 0) lines[1].position.set((offset + grid_ws/2) * scale, -grid_hs/2 * scale, 0) def append_p(lines, p1, p2): lines.append(p1[0]) lines.append(p1[1]) lines.append(0) lines.append(p2[0]) lines.append(p2[1]) lines.append(0) def initial_calc(): grid_w = int(1920 * 4) grid_h = 1080 * 2 grid_scale = 10 noise_factor = 500 grid_hs = int(grid_h/grid_scale) grid_ws = int(grid_w/grid_scale) crossfade_range = int(grid_ws/12.5) def grid_lines(): lines = array("d") grid_goal = 24 grid_size_i = int(round((grid_ws - crossfade_range) / grid_goal)) grid_actual = (grid_ws - crossfade_range) / grid_size_i for i in range(0, grid_size_i): x = i * grid_actual append_p(lines, (x, 0), (x, grid_hs)) for y in range(0, grid_hs, grid_goal): append_p(lines, (0, y), (grid_ws-crossfade_range, y)) return lines import perlin spy_perlin = perlin.lib spy_perlin.init() spy_perlin.seed(44) scale_lines(grid_ws - crossfade_range, grid_hs) print("Computing the height map") spy_perlin.make_height_map(grid_ws, grid_hs) spy_perlin.update_height_map(grid_ws, grid_hs, grid_scale / noise_factor, 0) print("Cross-fading the height map") spy_perlin.crossfade_height_map(grid_ws, grid_hs, crossfade_range) print("Drawing grid") draw_lines(grid_lines(), 'grid') print("Marching squares") draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0), 'zero', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.3), 'positive', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.3), 'negative', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.45), 'positive', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.45), 'negative', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.6), 'positive', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.6), 'negative', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.8), 'negative', spy=True) draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.8), 'positive', spy=True) drawing_done() drawing_event = asyncio.Event() start_ts = time.time() USE_SPY = True if USE_SPY: initial_calc() else: worker = PyWorker("./worker.py", type="pyodide", configURL="./pyscript.toml") worker.sync.draw_lines = draw_lines worker.sync.drawing_done = drawing_done worker.sync.scale_lines = scale_lines worker.sync.print = print @create_proxy def on_tap(event): clear() player.toggle() document.addEventListener("click", on_tap) @create_proxy def on_key_down(event): element = document.activeElement _class = element.getAttribute("class") in_xterm = element.tagName != "BODY" and _class and "xterm" in _class if event.code == "Backquote": # Screenshot mode. clear() elif not in_xterm: # Don't react to those bindings when typing code. if event.code == "Space": player.toggle() document.addEventListener("keydown", on_key_down) @create_proxy def on_window_resize(event): aspect_ratio = window.innerWidth / window.innerHeight if camera.type == "OrthographicCamera": camera.left = -view_size * aspect_ratio camera.right = view_size * aspect_ratio camera.top = view_size camera.bottom = -view_size camera.updateProjectionMatrix() elif camera.type == "PerspectiveCamera": camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() else: raise ValueError("Unknown camera type") renderer.setSize(window.innerWidth, window.innerHeight) scale_lines() window.addEventListener("resize", on_window_resize) @create_proxy def animate(now=0.0): data = analyser.getFrequencyData()#.to_py() in Pyodide audio_now = player.running_time bs.update(data, audio_now) if grid_width: offset = -((20 * audio_now) % grid_width) scale_lines(offset=offset) renderer.render(scene, camera) stats_gl.update() def reset(): global scroll_offset bs.reset() scale_lines() def on_stop(): global scroll_offset bs.reset() scale_lines() await loaded_event.wait() stats_gl = get_stats_gl(renderer) player = SoundPlayer(sound=sound, on_start=reset, on_stop=on_stop) bs = BeatSync(fft_res=fft_res) renderer.setAnimationLoop(animate) print("Waiting for the contours...") await drawing_event.wait() print("Tap the map to start...")