mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (Video): add the paper-cutout intro and hype reel sources
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# renders, audio and downloaded fonts are generated, not sources
|
||||||
|
out/
|
||||||
|
out_*/
|
||||||
|
fonts/
|
||||||
|
__pycache__/
|
||||||
|
*.mp4
|
||||||
|
*.wav
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.srt
|
||||||
|
*.json
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# MobileGL videos
|
||||||
|
|
||||||
|
Sources for two promotional videos. Rendered output, audio and downloaded fonts are not committed (see
|
||||||
|
`.gitignore`); each folder has a `fetch_fonts.py` and a README with the exact render steps.
|
||||||
|
|
||||||
|
| folder | video |
|
||||||
|
|---|---|
|
||||||
|
| [`paper/`](paper/README.md) | **MobileGL intro**: a ~4:50 handmade paper-cutout explainer (English + Chinese, captions, cover image). CPU-rendered with Pillow. |
|
||||||
|
| [`hype/`](hype/README.md) | **MobileGL hype reel**: a 3:33 beat-synced 1080p60 showreel with a synthesised soundtrack, rendered on the GPU through MobileGL itself. Also contains a short animated sting of the MobileGlues mark. |
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# MobileGL hype reel
|
||||||
|
|
||||||
|
A 3:33 code-rendered, beat-synced showreel at 1080p60. The track is a 128 BPM electro-pop instrumental,
|
||||||
|
synthesised from the same score the picture is cut to. The palette is sampled from the MobileGlues icon.
|
||||||
|
|
||||||
|
The final cut was rendered **through MobileGL itself**. The renderer is an ordinary OpenGL 4.3 core program, and
|
||||||
|
pointing it at MobileGL's `opengl32.dll` (Magma → Vulkan) sends every draw, framebuffer and readback through
|
||||||
|
MobileGL. The outro credits the GL implementation that actually drew the frames.
|
||||||
|
|
||||||
|
## How it is put together
|
||||||
|
|
||||||
|
- **One score, two consumers.** `score.py` holds the tempo grid (128 BPM, 112 bars), the sections, harmony and
|
||||||
|
melodies, and every drum/fx event. `music.py` synthesises the track from it. Scenes read the same events
|
||||||
|
(kick, clap, roll, crash, impact…) as decaying pulses, so cuts, zooms, shakes and squashes land exactly on the
|
||||||
|
music.
|
||||||
|
- **A frame is a pure function of time.** Each frame averages 8 sub-frames across a 180° shutter (motion blur),
|
||||||
|
then goes through HDR post: bloom, chromatic aberration, vignette, a fixed print grain.
|
||||||
|
- **Pacing.** Each drop speeds up toward a climax: 4-beat ideas, then 2, then 1, then 8ths, with growing zoom,
|
||||||
|
shake and speed lines. Full-screen colour never changes faster than once a beat.
|
||||||
|
|
||||||
|
| file | what |
|
||||||
|
|---|---|
|
||||||
|
| `score.py` | tempo grid, sections, chords, hook melody, all events, `pulse()` |
|
||||||
|
| `dsp.py` | numba DSP kit: PolyBLEP oscillators, TPT state-variable filter, ping-pong delay, compressor, look-ahead limiter |
|
||||||
|
| `music.py` | the instrumental: kick/clap/hats/rolls/risers/impacts, supersaw chords with sidechain, plucks, FM keys, a mono lead with glide, convolution reverb, stutters, mastering to −10.5 LUFS |
|
||||||
|
| `analyze.py` | per-section loudness/band report and a spectrogram of a rendered mix |
|
||||||
|
| `engine.py` | the GL engine (moderngl): instanced SDF shapes, HarfBuzz + FreeType variable-font text with a texture cache, offscreen layers, sub-frame accumulation, bloom and final post |
|
||||||
|
| `shaders/block.glsl` | the MobileGlues mark, raymarched: a crafting-table / command-block block in translucent, slime-like glue that can be dropped in (splash, ripples, tongues erupting) |
|
||||||
|
| `shaders/goo.glsl` | 2D glue metaballs (smooth-union circles and tapered capsules, dome-shaded) |
|
||||||
|
| `anim.py` | palette, easing, `Frame` (local beats and pulses) |
|
||||||
|
| `kit.py` | backgrounds, HUD, chips, slams, stamps, checks, arrows, speed lines, isometric boxes, glue, the mark |
|
||||||
|
| `devices.py` | isometric phone / monitor / laptop / tower / board and the screens painted into them |
|
||||||
|
| `sc_open.py` … `sc_final.py`, `scenes.py` | one module per section; `scenes.py` is the timeline and the stutter glitch |
|
||||||
|
| `render.py` | `still`, `sheet`, `video`, `final`; pipes raw frames to ffmpeg and muxes the audio |
|
||||||
|
| `sting.py`, `sting_audio.py` | a 4.5 s standalone sting of the mark (block drops into glue), no text, with its own sound |
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python fetch_fonts.py # Archivo (+ Italic) and JetBrains Mono, SIL OFL
|
||||||
|
```
|
||||||
|
|
||||||
|
`ffmpeg` must be on `PATH`. Any OpenGL 4.3 core driver works for previews.
|
||||||
|
|
||||||
|
## Render
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python music.py out/music.wav # the soundtrack (~40 s)
|
||||||
|
python render.py sheet 30 60 24 --scale 0.5 # contact sheet of a time range
|
||||||
|
python render.py still 31.5 86 --scale 0.5 # single frames
|
||||||
|
python render.py video 30 60 --scale 0.5 --fps 30 # quick preview clip with audio
|
||||||
|
python render.py final --samples 8 --crf 18 # out/mobilegl_hype.mp4, 1080p60
|
||||||
|
```
|
||||||
|
|
||||||
|
Through MobileGL (Windows, `opengl32.dll` drop-in built from this repo):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MOBILEGL_BACKEND_TYPE=DirectVulkan MGL_LIBGL=C:\path\to\opengl32.dll \
|
||||||
|
python render.py final --samples 8 --crf 18
|
||||||
|
```
|
||||||
|
|
||||||
|
On the RTX 3080 this runs at about 8 fps (≈28 min for the full reel). The final credit line names the backend and
|
||||||
|
GPU from `GL_RENDERER`. To check MobileGL against the native driver, render the same frames both ways and diff
|
||||||
|
them. They match to well under 1/255 on average (the film grain uses an integer hash so it is identical on every
|
||||||
|
implementation).
|
||||||
|
|
||||||
|
The sting:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python sting_audio.py out/sting.wav
|
||||||
|
python render.py video 0 4.5 --module sting --audio out/sting.wav --samples 8 --out out/glue_drop.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Grain options: `GRAIN_K` scales the grain (default 0.6), and `GRAIN_RATE=n` re-rolls it every n frames
|
||||||
|
(default 0 = a fixed print texture; animated grain makes the encode several times larger).
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import sys, numpy as np, soundfile as sf
|
||||||
|
from scipy.signal import stft
|
||||||
|
import score as S
|
||||||
|
x, sr = sf.read(sys.argv[1]); m = x.mean(1)
|
||||||
|
def band(seg, lo, hi):
|
||||||
|
f = np.fft.rfft(seg * np.hanning(len(seg))); fr = np.fft.rfftfreq(len(seg), 1/sr)
|
||||||
|
return 10*np.log10(np.sum(np.abs(f[(fr>=lo)&(fr<hi)])**2)+1e-9)
|
||||||
|
for name,a,b in S.SECTIONS:
|
||||||
|
seg = m[int(S.tb(a)*sr):int(S.tb(b)*sr)]
|
||||||
|
rms = 20*np.log10(np.sqrt(np.mean(seg**2)))
|
||||||
|
print(f'{name:7s} rms {rms:6.1f} sub {band(seg,20,80):5.1f} low {band(seg,80,300):5.1f} mid {band(seg,300,2000):5.1f} pres {band(seg,2000,6000):5.1f} air {band(seg,6000,16000):5.1f} width {np.std(x[int(S.tb(a)*sr):int(S.tb(b)*sr),0]-x[int(S.tb(a)*sr):int(S.tb(b)*sr),1])/np.std(seg)+1e-9:.2f}')
|
||||||
|
# spectrogram image
|
||||||
|
f,t,Z = stft(m[::2], sr/2, nperseg=2048, noverlap=1024)
|
||||||
|
P = 20*np.log10(np.abs(Z)+1e-7); P = np.clip((P+100)/80,0,1)
|
||||||
|
from PIL import Image
|
||||||
|
img = (P[::-1][-700:]*255).astype(np.uint8)
|
||||||
|
im = Image.fromarray(img).resize((1800, 500)); im.save('out/spec.png')
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Easing, palette and per-frame timing helpers shared by the scenes."""
|
||||||
|
import math
|
||||||
|
from engine import hexc, mix, with_alpha, scale_rgb
|
||||||
|
import score as S
|
||||||
|
|
||||||
|
# --- palette (sampled from the MobileGlues icon, pushed for contrast) --------------------------------
|
||||||
|
INK = hexc('#06110a') # near-black green: dark plates
|
||||||
|
INK2 = hexc('#0d2213') # panels on dark
|
||||||
|
MOSS = hexc('#1d4a25') # deep forest
|
||||||
|
LEAF = hexc('#4e9a3e') # icon outline green
|
||||||
|
GLUE = hexc('#6edb55') # icon highlight lime: the signal colour
|
||||||
|
NEON = hexc('#8cff62', 2.6) # emissive lime (blooms)
|
||||||
|
MINT = hexc('#a0e28c') # icon background: light plates
|
||||||
|
PALE = hexc('#d4f0c9') # pale top face
|
||||||
|
BONE = hexc('#f1f7ec') # type on dark
|
||||||
|
CORAL = hexc('#ff6a55') # rare accent: errors / device lost
|
||||||
|
CORAL_HOT = hexc('#ff6a55', 2.2)
|
||||||
|
AMBER = hexc('#e7a94f') # speckle tan, tiny accents
|
||||||
|
CLEAR = (0.0, 0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def a(c, al):
|
||||||
|
return with_alpha(c, al)
|
||||||
|
|
||||||
|
|
||||||
|
# --- easing ----------------------------------------------------------------------------------------
|
||||||
|
def clamp01(x):
|
||||||
|
return 0.0 if x < 0 else 1.0 if x > 1 else x
|
||||||
|
|
||||||
|
|
||||||
|
def lin(t, t0, t1):
|
||||||
|
return clamp01((t - t0) / (t1 - t0)) if t1 != t0 else float(t >= t0)
|
||||||
|
|
||||||
|
|
||||||
|
def out_expo(u):
|
||||||
|
u = clamp01(u)
|
||||||
|
return 1.0 if u >= 1 else 1 - 2 ** (-10 * u)
|
||||||
|
|
||||||
|
|
||||||
|
def in_expo(u):
|
||||||
|
u = clamp01(u)
|
||||||
|
return 0.0 if u <= 0 else 2 ** (10 * u - 10)
|
||||||
|
|
||||||
|
|
||||||
|
def out_cubic(u):
|
||||||
|
u = clamp01(u)
|
||||||
|
return 1 - (1 - u) ** 3
|
||||||
|
|
||||||
|
|
||||||
|
def in_cubic(u):
|
||||||
|
u = clamp01(u)
|
||||||
|
return u ** 3
|
||||||
|
|
||||||
|
|
||||||
|
def in_out_cubic(u):
|
||||||
|
u = clamp01(u)
|
||||||
|
return 4 * u ** 3 if u < 0.5 else 1 - (-2 * u + 2) ** 3 / 2
|
||||||
|
|
||||||
|
|
||||||
|
def out_back(u, k=1.7):
|
||||||
|
u = clamp01(u)
|
||||||
|
return 1 + (k + 1) * (u - 1) ** 3 + k * (u - 1) ** 2
|
||||||
|
|
||||||
|
|
||||||
|
def spring(u, freq=3.0, damp=6.0):
|
||||||
|
"""0 -> 1 with overshoot; u in seconds-ish units (unclamped above)."""
|
||||||
|
if u <= 0:
|
||||||
|
return 0.0
|
||||||
|
return 1 - math.exp(-damp * u) * math.cos(2 * math.pi * freq * u)
|
||||||
|
|
||||||
|
|
||||||
|
def bump(u):
|
||||||
|
"""0 -> 1 -> 0 over u in [0, 1]."""
|
||||||
|
u = clamp01(u)
|
||||||
|
return math.sin(math.pi * u)
|
||||||
|
|
||||||
|
|
||||||
|
def lerp(a_, b, u):
|
||||||
|
return a_ + (b - a_) * u
|
||||||
|
|
||||||
|
|
||||||
|
def rnd(*k):
|
||||||
|
"""Deterministic hash -> [0, 1)."""
|
||||||
|
h = 2166136261
|
||||||
|
for v in k:
|
||||||
|
for ch in repr(v):
|
||||||
|
h = ((h ^ ord(ch)) * 16777619) & 0xffffffff
|
||||||
|
h ^= h >> 13
|
||||||
|
h = (h * 1274126177) & 0xffffffff
|
||||||
|
return (h & 0xffffff) / float(0x1000000)
|
||||||
|
|
||||||
|
|
||||||
|
# --- frame timing ----------------------------------------------------------------------------------
|
||||||
|
class Frame:
|
||||||
|
def __init__(self, t, entry_start_bar=0):
|
||||||
|
self.t = t
|
||||||
|
self.bar = t / S.BAR
|
||||||
|
self.beat = t / S.BEAT
|
||||||
|
self.b0 = entry_start_bar
|
||||||
|
self.lt = t - entry_start_bar * S.BAR # local seconds in entry
|
||||||
|
self.lbeat = self.lt / S.BEAT # local beats
|
||||||
|
self.lbar = self.lt / S.BAR
|
||||||
|
self.kick = S.pulse(t, 'kick', 0.11)
|
||||||
|
self.clap = S.pulse(t, 'clap', 0.10)
|
||||||
|
self.hat = S.pulse(t, 'hat', 0.05)
|
||||||
|
self.crash = S.pulse(t, 'crash', 0.6)
|
||||||
|
self.impact = S.pulse(t, 'impact', 0.35)
|
||||||
|
self.roll = S.pulse(t, 'roll', 0.08)
|
||||||
|
|
||||||
|
def at(self, beat):
|
||||||
|
"""Seconds since local beat `beat` (negative before)."""
|
||||||
|
return self.lt - beat * S.BEAT
|
||||||
|
|
||||||
|
def u(self, b0, b1):
|
||||||
|
"""Progress through local beats [b0, b1]."""
|
||||||
|
return clamp01((self.lbeat - b0) / (b1 - b0))
|
||||||
|
|
||||||
|
def step(self, div=1.0):
|
||||||
|
"""Index of the current local beat subdivision (div=2 -> 8ths)."""
|
||||||
|
return int(math.floor(self.lbeat * div + 1e-6))
|
||||||
|
|
||||||
|
|
||||||
|
def frame_idx(t, fps=60):
|
||||||
|
return int(math.floor(t * fps + 1e-6))
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
"""Isometric pop illustrations (icon style: flat faces, chunky light outlines) and their screen contents."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import Iso, COS30, SIN30, code_line, cubes, goo
|
||||||
|
from engine import LW, LH
|
||||||
|
|
||||||
|
from engine import hexc as _hex
|
||||||
|
|
||||||
|
FACE_DARK = (_hex('#2e6e2f'), _hex('#1d4a25'), _hex('#163a1d')) # top, left, right on dark plates
|
||||||
|
FACE_LIGHT = (_hex('#d4f0c9'), _hex('#82cb70'), _hex('#55b441')) # icon faces on light plates
|
||||||
|
OUTL = _hex('#62ba4b')
|
||||||
|
|
||||||
|
|
||||||
|
def screen_layer(E, name, w, h, painter, f):
|
||||||
|
"""Paint 2D content into an offscreen layer (logical w x h) and return the texture."""
|
||||||
|
tex = E.push_target(name, w, h)
|
||||||
|
painter(E, f, w, h)
|
||||||
|
E.pop_target()
|
||||||
|
return tex
|
||||||
|
|
||||||
|
|
||||||
|
def put_on_face(E, iso, tex, face, x, y, z, w, h, alpha=1.0):
|
||||||
|
"""Map a w x h (world units) texture onto an iso face whose content centre is (x, y, z)."""
|
||||||
|
cx, cy = iso.p(x, y, z)
|
||||||
|
s = iso.s
|
||||||
|
if face == 'left':
|
||||||
|
m = iso.mat_left(1.0)
|
||||||
|
E.image(tex, cx, cy, w * s, h * s, mat=m, color=(alpha,) * 4)
|
||||||
|
elif face == 'right':
|
||||||
|
E.image(tex, cx, cy, w * s, h * s, mat=iso.mat_right(1.0), color=(alpha,) * 4)
|
||||||
|
else:
|
||||||
|
E.image(tex, cx, cy, w * s, h * s, mat=iso.mat_top(1.0), color=(alpha,) * 4)
|
||||||
|
|
||||||
|
|
||||||
|
# --- devices ---------------------------------------------------------------------------------------
|
||||||
|
def phone(E, iso, f, x=0.0, y=0.0, faces=FACE_DARK, screen=None, line=OUTL, sw=5.0):
|
||||||
|
"""Upright phone facing left-front; screen texture on the left face."""
|
||||||
|
W_, T, H_ = 3.0, 0.35, 6.2
|
||||||
|
iso.box(E, x - W_ / 2, y, 0, W_, T, H_, faces[0], faces[1], faces[2], line, sw)
|
||||||
|
if screen is not None:
|
||||||
|
put_on_face(E, iso, screen, 'left', x, y + T, H_ / 2, W_ - 0.35, H_ - 0.7)
|
||||||
|
|
||||||
|
|
||||||
|
def monitor(E, iso, f, x=0.0, y=0.0, faces=FACE_DARK, screen=None, line=OUTL, sw=5.0):
|
||||||
|
W_, T, H_ = 8.0, 0.4, 4.8
|
||||||
|
iso.box(E, x - 1.0, y - 1.0, 0, 2.0, 2.0, 0.25, faces[0], faces[1], faces[2], line, sw) # foot
|
||||||
|
iso.box(E, x - 0.25, y - 0.2, 0.25, 0.5, 0.35, 1.3, faces[0], faces[1], faces[2], line, sw) # neck
|
||||||
|
iso.box(E, x - W_ / 2, y, 1.5, W_, T, H_, faces[0], faces[1], faces[2], line, sw)
|
||||||
|
if screen is not None:
|
||||||
|
put_on_face(E, iso, screen, 'left', x, y + T, 1.5 + H_ / 2, W_ - 0.4, H_ - 0.4)
|
||||||
|
|
||||||
|
|
||||||
|
def laptop(E, iso, f, x=0.0, y=0.0, faces=FACE_DARK, screen=None, line=OUTL, sw=5.0, open_=1.0):
|
||||||
|
W_, D, T = 6.4, 4.2, 0.3
|
||||||
|
iso.box(E, x - W_ / 2, y - D, 0, W_, D, T, faces[0], faces[1], faces[2], line, sw) # base
|
||||||
|
# keyboard hint
|
||||||
|
for r in range(4):
|
||||||
|
for c in range(10):
|
||||||
|
iso.quad_top(E, x - W_ / 2 + 0.4 + c * 0.56, y - D + 0.5 + r * 0.55, T + 0.001, 0.42, 0.4,
|
||||||
|
a(faces[2], 0.9))
|
||||||
|
# lid (upright, back edge)
|
||||||
|
H_ = 4.0 * open_
|
||||||
|
iso.box(E, x - W_ / 2, y - D - T, T, W_, T, H_, faces[0], faces[1], faces[2], line, sw)
|
||||||
|
if screen is not None and open_ > 0.6:
|
||||||
|
put_on_face(E, iso, screen, 'left', x, y - D, T + H_ / 2, W_ - 0.4, H_ - 0.4)
|
||||||
|
|
||||||
|
|
||||||
|
def tower(E, iso, f, x=0.0, y=0.0, faces=FACE_DARK, line=OUTL, sw=5.0, leds=True):
|
||||||
|
W_, D, H_ = 2.6, 4.0, 6.0
|
||||||
|
iso.box(E, x - W_ / 2, y - D / 2, 0, W_, D, H_, faces[0], faces[1], faces[2], line, sw)
|
||||||
|
if leds:
|
||||||
|
for k in range(5):
|
||||||
|
on = (int(f.t * 8) + k * 3) % 5 < 3
|
||||||
|
px, py = iso.p(x - W_ / 2 + 0.4 + k * 0.4, y + D / 2, H_ - 0.6)
|
||||||
|
E.circle(px, py, 5, NEON if on else a(GLUE, 0.3))
|
||||||
|
for k in range(4):
|
||||||
|
px0, py0 = iso.p(x - W_ / 2 + 0.3, y + D / 2, H_ - 1.4 - k * 0.9)
|
||||||
|
px1, py1 = iso.p(x + W_ / 2 - 0.3, y + D / 2, H_ - 1.4 - k * 0.9)
|
||||||
|
E.line(px0, py0, px1, py1, 5, a(line, 0.6))
|
||||||
|
|
||||||
|
|
||||||
|
def board(E, iso, f, x=0.0, y=0.0, faces=FACE_DARK, line=OUTL, sw=5.0):
|
||||||
|
"""Single-board computer (ARM Linux)."""
|
||||||
|
iso.box(E, x - 3.0, y - 2.0, 0, 6.0, 4.0, 0.25, faces[1], faces[1], faces[2], line, sw)
|
||||||
|
iso.box(E, x - 1.0, y - 1.0, 0.25, 2.0, 2.0, 0.35, _hex('#202a22'), _hex('#111611'), _hex('#0b0f0b'), line, 3)
|
||||||
|
for k in range(10):
|
||||||
|
iso.box(E, x - 2.7 + k * 0.5, y - 1.9, 0.25, 0.3, 0.3, 0.45, a(AMBER, 1), a(AMBER, 1), a(AMBER, 1))
|
||||||
|
iso.box(E, x + 1.5, y + 0.2, 0.25, 1.3, 1.6, 0.9, PALE, faces[1], faces[2], line, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def file_card(E, x, y, w, h, title, sub='', col=PALE, ink=INK, rot=0.0, s=1.0, fold=36):
|
||||||
|
"""Flat document icon with a folded corner."""
|
||||||
|
c, sn = math.cos(rot), math.sin(rot)
|
||||||
|
|
||||||
|
def P(px, py):
|
||||||
|
px, py = px * s, py * s
|
||||||
|
return (x + c * px - sn * py, y + sn * px + c * py)
|
||||||
|
pts = [P(-w / 2, -h / 2), P(w / 2 - fold, -h / 2), P(w / 2, -h / 2 + fold), P(w / 2, h / 2), P(-w / 2, h / 2)]
|
||||||
|
E.poly(pts, col, stroke=OUTL, sw=5 * s)
|
||||||
|
E.poly([P(w / 2 - fold, -h / 2), P(w / 2 - fold, -h / 2 + fold), P(w / 2, -h / 2 + fold)], MINT, stroke=OUTL,
|
||||||
|
sw=4 * s)
|
||||||
|
tx, ty = P(0, h * 0.12)
|
||||||
|
E.text(title, tx, ty, 34 * s, ink, font='mono', wght=800, rot=rot)
|
||||||
|
if sub:
|
||||||
|
sx, sy = P(0, h * 0.3)
|
||||||
|
E.text(sub, sx, sy, 20 * s, a(ink, 0.6), font='mono', wght=500, rot=rot)
|
||||||
|
|
||||||
|
|
||||||
|
# --- screen painters -------------------------------------------------------------------------------
|
||||||
|
def paint_voxels(E, f, w, h):
|
||||||
|
"""A tiny blocky landscape (a sandbox game, generically)."""
|
||||||
|
E.rect(w / 2, h / 2, w, h, _hex('#7fc8f0'))
|
||||||
|
E.rect(w / 2, h * 0.86, w, h * 0.5, _hex('#5aa0d0'))
|
||||||
|
iso = Iso(w / 2, h * 0.36, w / 15)
|
||||||
|
hgt = {}
|
||||||
|
for gx in range(-4, 5):
|
||||||
|
for gy in range(-4, 5):
|
||||||
|
hgt[(gx, gy)] = 1 + int(2.2 * (0.5 + 0.5 * math.sin(gx * 0.9 + 1.3) * math.cos(gy * 0.8 - 0.4)))
|
||||||
|
order = sorted(hgt, key=lambda k: (k[0] + k[1]))
|
||||||
|
for gx, gy in order:
|
||||||
|
z = hgt[(gx, gy)]
|
||||||
|
iso.box(E, gx, gy, 0, 1, 1, z, _hex('#6ccf4f'), _hex('#8a5a32'), _hex('#6b4426'), _hex('#3b6b2b'), 1.2)
|
||||||
|
# a tree
|
||||||
|
iso.box(E, 1, -2, hgt[(1, -2)], 1, 1, 2, _hex('#8a5a32'), _hex('#6b4426'), _hex('#5a3820'))
|
||||||
|
iso.box(E, 0, -3, hgt[(1, -2)] + 2, 3, 3, 1.6, _hex('#3f9a3a'), _hex('#2e7a2a'), _hex('#24621f'))
|
||||||
|
sun = 0.5 + 0.5 * math.sin(f.t * 0.7)
|
||||||
|
E.circle(w * 0.82, h * 0.12, w * 0.07, _hex('#fff7c2', 1.4))
|
||||||
|
E.text('60 FPS', w * 0.07, h * 0.07, w * 0.06, BONE, font='mono', wght=700, align='left')
|
||||||
|
|
||||||
|
|
||||||
|
def paint_desktop(E, f, w, h):
|
||||||
|
E.rect(w / 2, h / 2, w, h, _hex('#1b3a5c'))
|
||||||
|
for k in range(5): # wallpaper bands
|
||||||
|
E.circle(w * (0.2 + 0.18 * k), h * (0.7 - 0.05 * k), w * 0.25, a(_hex('#2c5d8a'), 0.35))
|
||||||
|
E.rect(w / 2, h - h * 0.045, w, h * 0.09, a(_hex('#0e1a26'), 0.92)) # panel
|
||||||
|
for k in range(6):
|
||||||
|
E.rect(w * 0.06 + k * w * 0.05, h - h * 0.045, w * 0.032, h * 0.05, a(_hex('#5fa8e8'), 0.85), r=3)
|
||||||
|
E.text('12:34', w * 0.95, h - h * 0.045, h * 0.035, BONE, font='mono', wght=600, align='right')
|
||||||
|
wins = [(0.32, 0.36, 0.42, 0.42, 'Terminal'), (0.62, 0.42, 0.46, 0.48, 'Browser'), (0.47, 0.6, 0.34, 0.3, 'glmark2')]
|
||||||
|
for i, (cx, cy, ww, hh, title) in enumerate(wins):
|
||||||
|
jig = 0.01 * math.sin(f.t * 1.3 + i)
|
||||||
|
X, Y, Wn, Hn = w * (cx + jig), h * cy, w * ww, h * hh
|
||||||
|
E.rect(X + 6, Y + 8, Wn, Hn, a(INK, 0.35), r=8)
|
||||||
|
E.rect(X, Y, Wn, Hn, _hex('#eef3f7'), r=8)
|
||||||
|
E.rect(X, Y - Hn / 2 + h * 0.025, Wn, h * 0.05, _hex('#d6dee6'), r=8)
|
||||||
|
E.text(title, X, Y - Hn / 2 + h * 0.025, h * 0.028, _hex('#30404e'), wght=700)
|
||||||
|
if title == 'Terminal':
|
||||||
|
E.rect(X, Y + h * 0.025, Wn - 10, Hn - h * 0.06, _hex('#0f1b14'))
|
||||||
|
for L, s in enumerate(('$ glxinfo -B', 'OpenGL core profile', 'version: 4.6', '$ _')):
|
||||||
|
E.text(s, X - Wn / 2 + 14, Y - Hn / 2 + h * 0.09 + L * h * 0.045, h * 0.03, GLUE, font='mono',
|
||||||
|
wght=500, align='left')
|
||||||
|
elif title == 'glmark2':
|
||||||
|
E.rect(X, Y + h * 0.025, Wn - 10, Hn - h * 0.06, _hex('#202020'))
|
||||||
|
E.circle(X + math.sin(f.t * 2) * Wn * 0.2, Y + h * 0.03, h * 0.06, _hex('#e05050'))
|
||||||
|
else:
|
||||||
|
for L in range(5):
|
||||||
|
E.rect(X - Wn * 0.1, Y - Hn / 2 + h * 0.1 + L * h * 0.06, Wn * 0.7 * (0.6 + 0.4 * ((L * 7) % 3) / 2),
|
||||||
|
h * 0.022, _hex('#b8c4cf'), r=4)
|
||||||
|
|
||||||
|
|
||||||
|
def paint_mac(E, f, w, h):
|
||||||
|
E.rect(w / 2, h / 2, w, h, _hex('#2a2440'))
|
||||||
|
for k in range(3):
|
||||||
|
E.circle(w * (0.3 + 0.25 * k), h * 0.6, w * 0.3, a(_hex('#5a3f86'), 0.3))
|
||||||
|
E.rect(w / 2, h * 0.03, w, h * 0.06, a(_hex('#ffffff'), 0.25))
|
||||||
|
E.rect(w / 2, h * 0.9, w * 0.5, h * 0.1, a(_hex('#ffffff'), 0.25), r=12)
|
||||||
|
X, Y, Wn, Hn = w * 0.5, h * 0.45, w * 0.6, h * 0.58
|
||||||
|
E.rect(X, Y, Wn, Hn, _hex('#101010'), r=10)
|
||||||
|
E.rect(X, Y - Hn / 2 + h * 0.03, Wn, h * 0.06, _hex('#2a2a2a'), r=10)
|
||||||
|
for k, cc in enumerate(('#ff5f57', '#febc2e', '#28c840')):
|
||||||
|
E.circle(X - Wn / 2 + 18 + k * 18, Y - Hn / 2 + h * 0.03, 5, _hex(cc))
|
||||||
|
# a little lit scene: hills + sun (a game, generically)
|
||||||
|
E.rect(X, Y + Hn * 0.2, Wn - 8, Hn * 0.5, _hex('#3b7d3a'))
|
||||||
|
E.circle(X + Wn * 0.25, Y - Hn * 0.1, h * 0.06, _hex('#ffd36b', 1.3))
|
||||||
|
|
||||||
|
|
||||||
|
def paint_term(E, f, w, h, lines, typed=None):
|
||||||
|
E.rect(w / 2, h / 2, w, h, _hex('#0b160e'))
|
||||||
|
for i, s in enumerate(lines):
|
||||||
|
code_line(E, s, 20, 40 + i * 44, 26, chars=None if typed is None else typed - sum(len(x) for x in lines[:i]))
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
"""Small numba DSP kit: band-limited oscillators, a TPT state-variable filter, a ping-pong delay, a
|
||||||
|
compressor and a look-ahead limiter. Everything runs on float64 mono arrays."""
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
|
from numba import njit
|
||||||
|
|
||||||
|
SR = 48000
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def _blep(t, dt):
|
||||||
|
if t < dt:
|
||||||
|
t /= dt
|
||||||
|
return t + t - t * t - 1.0
|
||||||
|
if t > 1.0 - dt:
|
||||||
|
t = (t - 1.0) / dt
|
||||||
|
return t * t + t + t + 1.0
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def saw(freq, phase0):
|
||||||
|
n = freq.shape[0]
|
||||||
|
out = np.empty(n)
|
||||||
|
p = phase0
|
||||||
|
for i in range(n):
|
||||||
|
dt = freq[i] / SR
|
||||||
|
out[i] = 2.0 * p - 1.0 - _blep(p, dt)
|
||||||
|
p += dt
|
||||||
|
if p >= 1.0:
|
||||||
|
p -= 1.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def pulse(freq, width, phase0):
|
||||||
|
n = freq.shape[0]
|
||||||
|
out = np.empty(n)
|
||||||
|
p = phase0
|
||||||
|
for i in range(n):
|
||||||
|
dt = freq[i] / SR
|
||||||
|
v = 1.0 if p < width else -1.0
|
||||||
|
v += _blep(p, dt)
|
||||||
|
q = p - width
|
||||||
|
if q < 0.0:
|
||||||
|
q += 1.0
|
||||||
|
v -= _blep(q, dt)
|
||||||
|
out[i] = v
|
||||||
|
p += dt
|
||||||
|
if p >= 1.0:
|
||||||
|
p -= 1.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def sine(freq, phase0):
|
||||||
|
n = freq.shape[0]
|
||||||
|
out = np.empty(n)
|
||||||
|
p = phase0
|
||||||
|
for i in range(n):
|
||||||
|
out[i] = math.sin(2.0 * math.pi * p)
|
||||||
|
p += freq[i] / SR
|
||||||
|
if p >= 1.0:
|
||||||
|
p -= 1.0
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def svf(x, cutoff, res, mode):
|
||||||
|
"""Zavalishin TPT SVF. cutoff: per-sample Hz array; res 0..1; mode 0=lp 1=bp 2=hp."""
|
||||||
|
n = x.shape[0]
|
||||||
|
out = np.empty(n)
|
||||||
|
ic1 = 0.0
|
||||||
|
ic2 = 0.0
|
||||||
|
k = 2.0 - 2.0 * min(res, 0.98)
|
||||||
|
for i in range(n):
|
||||||
|
fc = min(max(cutoff[i], 10.0), SR * 0.45)
|
||||||
|
g = math.tan(math.pi * fc / SR)
|
||||||
|
a1 = 1.0 / (1.0 + g * (g + k))
|
||||||
|
a2 = g * a1
|
||||||
|
a3 = g * a2
|
||||||
|
v3 = x[i] - ic2
|
||||||
|
v1 = a1 * ic1 + a2 * v3
|
||||||
|
v2 = ic2 + a2 * ic1 + a3 * v3
|
||||||
|
ic1 = 2.0 * v1 - ic1
|
||||||
|
ic2 = 2.0 * v2 - ic2
|
||||||
|
if mode == 0:
|
||||||
|
out[i] = v2
|
||||||
|
elif mode == 1:
|
||||||
|
out[i] = v1
|
||||||
|
else:
|
||||||
|
out[i] = x[i] - k * v1 - v2
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def onepole_hp(x, fc):
|
||||||
|
n = x.shape[0]
|
||||||
|
out = np.empty(n)
|
||||||
|
a = math.exp(-2.0 * math.pi * fc / SR)
|
||||||
|
yp = 0.0
|
||||||
|
xp = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
yp = a * (yp + x[i] - xp)
|
||||||
|
xp = x[i]
|
||||||
|
out[i] = yp
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def pingpong(l, r, delay_s, fb, damp_hz):
|
||||||
|
n = l.shape[0]
|
||||||
|
d = int(delay_s * SR)
|
||||||
|
bl = np.zeros(d)
|
||||||
|
br = np.zeros(d)
|
||||||
|
ol = np.empty(n)
|
||||||
|
orr = np.empty(n)
|
||||||
|
a = math.exp(-2.0 * math.pi * damp_hz / SR)
|
||||||
|
zl = 0.0
|
||||||
|
zr = 0.0
|
||||||
|
j = 0
|
||||||
|
for i in range(n):
|
||||||
|
yl = bl[j]
|
||||||
|
yr = br[j]
|
||||||
|
ol[i] = yl
|
||||||
|
orr[i] = yr
|
||||||
|
zl = (1 - a) * yl + a * zl
|
||||||
|
zr = (1 - a) * yr + a * zr
|
||||||
|
bl[j] = 0.5 * (l[i] + r[i]) + fb * zr # mono in on the left tap, cross-feed
|
||||||
|
br[j] = fb * zl
|
||||||
|
j += 1
|
||||||
|
if j >= d:
|
||||||
|
j = 0
|
||||||
|
return ol, orr
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def compress(l, r, thresh_db, ratio, attack, release, makeup_db):
|
||||||
|
n = l.shape[0]
|
||||||
|
ol = np.empty(n)
|
||||||
|
orr = np.empty(n)
|
||||||
|
aa = math.exp(-1.0 / (attack * SR))
|
||||||
|
ar = math.exp(-1.0 / (release * SR))
|
||||||
|
env = 0.0
|
||||||
|
mk = 10 ** (makeup_db / 20)
|
||||||
|
for i in range(n):
|
||||||
|
x = max(abs(l[i]), abs(r[i]))
|
||||||
|
if x > env:
|
||||||
|
env = aa * env + (1 - aa) * x
|
||||||
|
else:
|
||||||
|
env = ar * env + (1 - ar) * x
|
||||||
|
db = 20 * math.log10(env + 1e-9)
|
||||||
|
over = db - thresh_db
|
||||||
|
gr = 0.0
|
||||||
|
if over > 0:
|
||||||
|
gr = over * (1 - 1 / ratio)
|
||||||
|
g = 10 ** (-gr / 20) * mk
|
||||||
|
ol[i] = l[i] * g
|
||||||
|
orr[i] = r[i] * g
|
||||||
|
return ol, orr
|
||||||
|
|
||||||
|
|
||||||
|
@njit(cache=True)
|
||||||
|
def limit(l, r, ceiling, lookahead_s, release_s):
|
||||||
|
"""Look-ahead peak limiter: gain = min over the window ahead, smoothed on release."""
|
||||||
|
n = l.shape[0]
|
||||||
|
la = int(lookahead_s * SR)
|
||||||
|
need = np.ones(n)
|
||||||
|
for i in range(n):
|
||||||
|
p = max(abs(l[i]), abs(r[i]))
|
||||||
|
if p > ceiling:
|
||||||
|
need[i] = ceiling / p
|
||||||
|
# running minimum over the next `la` samples (simple deque-free two-pass block min)
|
||||||
|
gmin = np.ones(n)
|
||||||
|
for i in range(n - 1, -1, -1):
|
||||||
|
m = need[i]
|
||||||
|
if i + 1 < n:
|
||||||
|
# decay toward the earlier future minimum with a linear attack ramp over `la`
|
||||||
|
nxt = gmin[i + 1]
|
||||||
|
ramp = nxt + (1.0 - nxt) / la
|
||||||
|
if ramp < m:
|
||||||
|
m = ramp
|
||||||
|
gmin[i] = m
|
||||||
|
ar = math.exp(-1.0 / (release_s * SR))
|
||||||
|
g = 1.0
|
||||||
|
ol = np.empty(n)
|
||||||
|
orr = np.empty(n)
|
||||||
|
for i in range(n):
|
||||||
|
t = gmin[i]
|
||||||
|
if t < g:
|
||||||
|
g = t
|
||||||
|
else:
|
||||||
|
g = ar * g + (1 - ar) * t
|
||||||
|
ol[i] = l[i] * g
|
||||||
|
orr[i] = r[i] * g
|
||||||
|
return ol, orr
|
||||||
|
|
||||||
|
|
||||||
|
def mtof(m):
|
||||||
|
return 440.0 * 2.0 ** ((np.asarray(m, dtype=np.float64) - 69.0) / 12.0)
|
||||||
|
|
||||||
|
|
||||||
|
def adsr(n, a, d, s, r_start, r, sr=SR):
|
||||||
|
"""Envelope of n samples; release begins at sample r_start and lasts r seconds."""
|
||||||
|
t = np.arange(n) / sr
|
||||||
|
e = np.where(t < a, t / max(a, 1e-6), s + (1 - s) * np.exp(-(t - a) / max(d, 1e-6)))
|
||||||
|
rs = r_start / sr
|
||||||
|
if r_start < n:
|
||||||
|
lvl = e[min(r_start, n - 1)]
|
||||||
|
tail = t >= rs
|
||||||
|
e[tail] = lvl * np.exp(-(t[tail] - rs) / max(r / 4.0, 1e-6))
|
||||||
|
return e
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
"""GPU motion-graphics engine for the MobileGL showreel (moderngl, OpenGL 4.3+ core).
|
||||||
|
|
||||||
|
Everything is laid out in logical 1920x1080 pixels, y down. A frame is the average of `samples`
|
||||||
|
sub-frames across a shutter (motion blur), then bloom / aberration / grain / vignette in post.
|
||||||
|
Set MGL_LIBGL to an opengl32.dll to render through another GL implementation (MobileGL)."""
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import functools
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import moderngl
|
||||||
|
import freetype
|
||||||
|
import uharfbuzz as hb
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
LW, LH = 1920, 1080
|
||||||
|
|
||||||
|
|
||||||
|
# --- colour ----------------------------------------------------------------------------------------
|
||||||
|
def srgb2lin(c):
|
||||||
|
c = c / 255.0
|
||||||
|
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
||||||
|
|
||||||
|
|
||||||
|
def hexc(h, k=1.0, a=1.0):
|
||||||
|
"""'#rrggbb' -> linear (r, g, b, a), rgb scaled by k (k > 1 for emissive glow)."""
|
||||||
|
h = h.lstrip('#')
|
||||||
|
return (srgb2lin(int(h[0:2], 16)) * k, srgb2lin(int(h[2:4], 16)) * k, srgb2lin(int(h[4:6], 16)) * k, a)
|
||||||
|
|
||||||
|
|
||||||
|
def mix(a, b, u):
|
||||||
|
return tuple(x + (y - x) * u for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def with_alpha(c, a):
|
||||||
|
return (c[0], c[1], c[2], c[3] * a)
|
||||||
|
|
||||||
|
|
||||||
|
def scale_rgb(c, k):
|
||||||
|
return (c[0] * k, c[1] * k, c[2] * k, c[3])
|
||||||
|
|
||||||
|
|
||||||
|
# --- fonts -----------------------------------------------------------------------------------------
|
||||||
|
class Face:
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.hbface = hb.Face(hb.Blob.from_file_path(path))
|
||||||
|
self.hbfont = hb.Font(self.hbface)
|
||||||
|
self.upem = self.hbface.upem
|
||||||
|
self.ft = freetype.Face(path)
|
||||||
|
self.axes = [a.tag for a in self.ft.get_variation_info().axes] if self.ft.has_multiple_masters else []
|
||||||
|
self.ft.set_char_size(1000 * 64)
|
||||||
|
self.ft.load_char('H', freetype.FT_LOAD_NO_HINTING)
|
||||||
|
self.cap = self.ft.glyph.metrics.horiBearingY / 64 / 1000 # cap height / em
|
||||||
|
self.ft.load_char('x', freetype.FT_LOAD_NO_HINTING)
|
||||||
|
self.xh = self.ft.glyph.metrics.horiBearingY / 64 / 1000
|
||||||
|
|
||||||
|
def _vary(self, var):
|
||||||
|
coords = tuple(var.get(a, None) for a in self.axes)
|
||||||
|
if self.axes:
|
||||||
|
info = {a.tag: a for a in self.ft.get_variation_info().axes}
|
||||||
|
full = tuple(v if v is not None else info[a].default for a, v in zip(self.axes, coords))
|
||||||
|
self.ft.set_var_design_coords(full)
|
||||||
|
self.hbfont.set_variations({a: v for a, v in zip(self.axes, full)})
|
||||||
|
|
||||||
|
def shape(self, s, size, var, tracking=0.0):
|
||||||
|
"""-> list of (gid, x, y, cluster) pen positions in px, and total advance."""
|
||||||
|
self._vary(var)
|
||||||
|
buf = hb.Buffer()
|
||||||
|
buf.add_str(s)
|
||||||
|
buf.guess_segment_properties()
|
||||||
|
hb.shape(self.hbfont, buf, {'kern': True, 'liga': True})
|
||||||
|
k = size / self.upem
|
||||||
|
out = []
|
||||||
|
x = 0.0
|
||||||
|
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
||||||
|
out.append((info.codepoint, x + pos.x_offset * k, -pos.y_offset * k, info.cluster))
|
||||||
|
x += pos.x_advance * k + tracking
|
||||||
|
return out, x - (tracking if out else 0)
|
||||||
|
|
||||||
|
def raster(self, gid, size, var):
|
||||||
|
self._vary(var)
|
||||||
|
self.ft.set_char_size(int(round(size * 64)))
|
||||||
|
self.ft.load_glyph(gid, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_NO_HINTING)
|
||||||
|
g = self.ft.glyph
|
||||||
|
b = g.bitmap
|
||||||
|
arr = np.array(b.buffer, dtype=np.uint8).reshape(b.rows, b.width) if b.rows else np.zeros((0, 0), np.uint8)
|
||||||
|
return arr, g.bitmap_left, g.bitmap_top
|
||||||
|
|
||||||
|
|
||||||
|
FONT_FILES = {
|
||||||
|
'archivo': 'Archivo[wdth,wght].ttf',
|
||||||
|
'archivo-i': 'Archivo-Italic[wdth,wght].ttf',
|
||||||
|
'mono': 'JetBrainsMono[wght].ttf',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def face(name):
|
||||||
|
return Face(os.path.join(HERE, 'fonts', FONT_FILES[name]))
|
||||||
|
|
||||||
|
|
||||||
|
SIZE_LADDER = 2 ** (1 / 10)
|
||||||
|
|
||||||
|
|
||||||
|
def quant_size(px):
|
||||||
|
"""Raster size ladder (~7% steps) so animated sizes reuse textures."""
|
||||||
|
k = math.ceil(math.log(max(px, 2)) / math.log(SIZE_LADDER) - 1e-6)
|
||||||
|
return SIZE_LADDER ** k
|
||||||
|
|
||||||
|
|
||||||
|
# --- shaders ---------------------------------------------------------------------------------------
|
||||||
|
def _read(name):
|
||||||
|
with open(os.path.join(HERE, 'shaders', name), encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess(src):
|
||||||
|
out = []
|
||||||
|
for line in src.splitlines():
|
||||||
|
if line.strip().startswith('#include'):
|
||||||
|
name = line.split('"')[1]
|
||||||
|
out.append(preprocess(COMMON if name == 'common.glsl' else _read(name)))
|
||||||
|
else:
|
||||||
|
out.append(line)
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
|
||||||
|
VS_FULL = '''#version 430
|
||||||
|
out vec2 v_uv;
|
||||||
|
void main(){ vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); v_uv = p; gl_Position = vec4(p * 2.0 - 1.0, 0, 1); }
|
||||||
|
'''
|
||||||
|
|
||||||
|
SHAPE_VS = '''#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
in vec2 a_corner;
|
||||||
|
in vec4 i_rect; in vec4 i_xf; in vec4 i_kind; in vec4 i_p0; in vec4 i_p1; in vec4 i_p2; in vec4 i_p3;
|
||||||
|
in vec4 i_fill; in vec4 i_stroke; in vec4 i_sw;
|
||||||
|
out vec2 v_lp;
|
||||||
|
flat out vec4 v_kind; flat out vec4 v_p0; flat out vec4 v_p1; flat out vec4 v_p2; flat out vec4 v_p3;
|
||||||
|
flat out vec4 v_fill; flat out vec4 v_stroke; flat out vec4 v_sw;
|
||||||
|
void main(){
|
||||||
|
float pad = i_sw.x + 4.0 * i_sw.y + 2.0;
|
||||||
|
vec2 lp = mix(i_rect.xy - pad, i_rect.zw + pad, a_corner);
|
||||||
|
float c = cos(i_xf.z), s = sin(i_xf.z);
|
||||||
|
vec2 wp = i_xf.xy + i_xf.w * vec2(c * lp.x - s * lp.y, s * lp.x + c * lp.y);
|
||||||
|
gl_Position = vec4(to_ndc(cam(wp)), 0, 1);
|
||||||
|
v_lp = lp; v_kind = i_kind; v_p0 = i_p0; v_p1 = i_p1; v_p2 = i_p2; v_p3 = i_p3;
|
||||||
|
v_fill = i_fill; v_stroke = i_stroke; v_sw = i_sw;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
SHAPE_FS = '''#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
in vec2 v_lp;
|
||||||
|
flat in vec4 v_kind; flat in vec4 v_p0; flat in vec4 v_p1; flat in vec4 v_p2; flat in vec4 v_p3;
|
||||||
|
flat in vec4 v_fill; flat in vec4 v_stroke; flat in vec4 v_sw;
|
||||||
|
out vec4 o;
|
||||||
|
vec2 vtx(int i){
|
||||||
|
vec4 q = i < 2 ? v_p0 : i < 4 ? v_p1 : i < 6 ? v_p2 : v_p3;
|
||||||
|
return (i % 2 == 0) ? q.xy : q.zw;
|
||||||
|
}
|
||||||
|
float sd_poly(vec2 p, int n){
|
||||||
|
vec2 v0 = vtx(0);
|
||||||
|
float d = dot(p - v0, p - v0), sg = 1.0;
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i, i++){
|
||||||
|
vec2 vi = vtx(i), vj = vtx(j);
|
||||||
|
vec2 e = vj - vi, w = p - vi;
|
||||||
|
vec2 b = w - e * clamp(dot(w, e) / dot(e, e), 0.0, 1.0);
|
||||||
|
d = min(d, dot(b, b));
|
||||||
|
bvec3 cnd = bvec3(p.y >= vi.y, p.y < vj.y, e.x * w.y > e.y * w.x);
|
||||||
|
if (all(cnd) || all(not(cnd))) sg *= -1.0;
|
||||||
|
}
|
||||||
|
return sg * sqrt(d);
|
||||||
|
}
|
||||||
|
float sd_arc(vec2 p, float r, float th, float a0, float a1){
|
||||||
|
float a = atan(p.y, p.x);
|
||||||
|
float mid = 0.5 * (a0 + a1), half_ = 0.5 * (a1 - a0);
|
||||||
|
float da = mod(a - mid + PI, 2.0 * PI) - PI;
|
||||||
|
if (abs(da) <= half_) return abs(length(p) - r) - th * 0.5;
|
||||||
|
vec2 e0 = r * vec2(cos(a0), sin(a0)), e1 = r * vec2(cos(a1), sin(a1));
|
||||||
|
return min(length(p - e0), length(p - e1)) - th * 0.5;
|
||||||
|
}
|
||||||
|
void main(){
|
||||||
|
int kind = int(v_kind.x + 0.5);
|
||||||
|
vec2 p = v_lp;
|
||||||
|
float d;
|
||||||
|
if (kind == 0) d = length(p) - v_p0.x;
|
||||||
|
else if (kind == 1) { vec2 q = abs(p) - v_p0.xy + v_p0.z; d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - v_p0.z; }
|
||||||
|
else if (kind == 2) { vec2 a = v_p0.xy, b = v_p0.zw; vec2 pa = p - a, ba = b - a;
|
||||||
|
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
|
||||||
|
d = length(pa - ba * h) - mix(v_p1.x, v_p1.y, h); }
|
||||||
|
else if (kind == 5) d = sd_arc(p, v_p0.x, v_p0.y, v_p0.z, v_p0.w);
|
||||||
|
else d = sd_poly(p, int(v_kind.y + 0.5)) - v_kind.z;
|
||||||
|
float aa = max(length(fwidth(p)) * 0.75, 1e-4);
|
||||||
|
vec4 col = vec4(0);
|
||||||
|
float af = clamp(0.5 - d / aa, 0.0, 1.0) * v_fill.a;
|
||||||
|
col = vec4(v_fill.rgb * af, af);
|
||||||
|
if (v_sw.x > 0.0) {
|
||||||
|
float as_ = clamp(0.5 - (abs(d) - v_sw.x * 0.5) / aa, 0.0, 1.0) * v_stroke.a;
|
||||||
|
col = vec4(v_stroke.rgb * as_, as_) + col * (1.0 - as_);
|
||||||
|
}
|
||||||
|
if (v_sw.y > 0.0) { // emissive halo, additive (alpha untouched)
|
||||||
|
float q = max(d, 0.0) / v_sw.y;
|
||||||
|
float g = exp(-0.5 * q * q) * v_sw.z * (d > 0.0 ? 1.0 : 0.0);
|
||||||
|
col.rgb += v_fill.rgb * g;
|
||||||
|
}
|
||||||
|
o = col * u_alpha;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
TEX_VS = '''#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
in vec2 a_corner;
|
||||||
|
uniform vec4 u_rect; // local quad x0,y0,x1,y1
|
||||||
|
uniform vec2 u_t; // translation
|
||||||
|
uniform vec4 u_m; // 2x2 matrix (m00, m10, m01, m11)
|
||||||
|
out vec2 v_uv;
|
||||||
|
void main(){
|
||||||
|
vec2 lp = mix(u_rect.xy, u_rect.zw, a_corner);
|
||||||
|
vec2 wp = u_t + vec2(u_m.x * lp.x + u_m.z * lp.y, u_m.y * lp.x + u_m.w * lp.y);
|
||||||
|
gl_Position = vec4(to_ndc(cam(wp)), 0, 1);
|
||||||
|
v_uv = a_corner;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
TEX_FS = '''#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
in vec2 v_uv;
|
||||||
|
uniform sampler2D u_tex;
|
||||||
|
uniform vec4 u_color;
|
||||||
|
uniform vec4 u_reveal; // u0, u1, softness, mode (0 = horizontal, 1 = vertical)
|
||||||
|
uniform int u_mode; // 0 = mask (R channel) coloured, 1 = premultiplied rgba image
|
||||||
|
out vec4 o;
|
||||||
|
void main(){
|
||||||
|
vec4 c;
|
||||||
|
if (u_mode == 0) { float m = texture(u_tex, v_uv).r; c = vec4(u_color.rgb, 1.0) * m * u_color.a; }
|
||||||
|
else c = texture(u_tex, v_uv) * u_color;
|
||||||
|
float x = u_reveal.w > 0.5 ? v_uv.y : v_uv.x;
|
||||||
|
float sft = max(u_reveal.z, 1e-4);
|
||||||
|
float r = smoothstep(u_reveal.x - sft, u_reveal.x, x) * (1.0 - smoothstep(u_reveal.y, u_reveal.y + sft, x));
|
||||||
|
o = c * r * u_alpha;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
COMMON = '''
|
||||||
|
uniform vec4 u_cam; // centre x, y, zoom, rotation
|
||||||
|
uniform vec2 u_shake;
|
||||||
|
uniform vec2 u_logical; // 1920, 1080
|
||||||
|
uniform float u_alpha;
|
||||||
|
const float PI = 3.14159265358979;
|
||||||
|
vec2 cam(vec2 p){
|
||||||
|
vec2 c = u_logical * 0.5;
|
||||||
|
vec2 q = p - u_cam.xy;
|
||||||
|
float cs = cos(u_cam.w), sn = sin(u_cam.w);
|
||||||
|
q = vec2(cs * q.x - sn * q.y, sn * q.x + cs * q.y) * u_cam.z;
|
||||||
|
return q + c + u_shake;
|
||||||
|
}
|
||||||
|
vec2 uncam(vec2 s){
|
||||||
|
vec2 c = u_logical * 0.5;
|
||||||
|
vec2 q = (s - c - u_shake) / u_cam.z;
|
||||||
|
float cs = cos(-u_cam.w), sn = sin(-u_cam.w);
|
||||||
|
return vec2(cs * q.x - sn * q.y, sn * q.x + cs * q.y) + u_cam.xy;
|
||||||
|
}
|
||||||
|
vec2 to_ndc(vec2 p){ return vec2(p.x / u_logical.x * 2.0 - 1.0, 1.0 - p.y / u_logical.y * 2.0); }
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def _mat(xf, skew):
|
||||||
|
"""xf = (tx, ty, rot, scale) or (tx, ty, (m00, m01, m10, m11)); skew = (x shear, y scale)."""
|
||||||
|
if len(xf) == 3:
|
||||||
|
m00, m01, m10, m11 = xf[2]
|
||||||
|
else:
|
||||||
|
c, s_ = math.cos(xf[2]) * xf[3], math.sin(xf[2]) * xf[3]
|
||||||
|
m00, m01, m10, m11 = c, -s_, s_, c
|
||||||
|
sh, sy = skew
|
||||||
|
# M * [[1, sh], [0, sy]]
|
||||||
|
a, b, c2, d = m00, m00 * sh + m01 * sy, m10, m10 * sh + m11 * sy
|
||||||
|
return (a, c2, b, d)
|
||||||
|
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
def __init__(self, scale=1.0, samples=1, shutter=0.5, fps=60, libgl=None):
|
||||||
|
libgl = libgl or os.environ.get('MGL_LIBGL') or 'opengl32.dll'
|
||||||
|
self.ctx = moderngl.create_context(standalone=True, require=430, libgl=libgl)
|
||||||
|
self.renderer = self.ctx.info['GL_RENDERER']
|
||||||
|
self.scale = scale
|
||||||
|
self.W, self.H = int(LW * scale), int(LH * scale)
|
||||||
|
self.samples, self.shutter, self.fps = samples, shutter, fps
|
||||||
|
ctx = self.ctx
|
||||||
|
self.progs = {}
|
||||||
|
self.p_shape = self.program(SHAPE_VS, SHAPE_FS)
|
||||||
|
self.p_tex = self.program(TEX_VS, TEX_FS)
|
||||||
|
self.quad = ctx.buffer(np.array([0, 0, 1, 0, 0, 1, 1, 1], dtype='f4').tobytes())
|
||||||
|
self.inst = ctx.buffer(reserve=4096 * 40 * 4, dynamic=True)
|
||||||
|
self.vao_shape = ctx.vertex_array(self.p_shape, [
|
||||||
|
(self.quad, '2f', 'a_corner'),
|
||||||
|
(self.inst, '4f 4f 4f 4f 4f 4f 4f 4f 4f 4f/i',
|
||||||
|
'i_rect', 'i_xf', 'i_kind', 'i_p0', 'i_p1', 'i_p2', 'i_p3', 'i_fill', 'i_stroke', 'i_sw')])
|
||||||
|
self.vao_tex = ctx.vertex_array(self.p_tex, [(self.quad, '2f', 'a_corner')])
|
||||||
|
self.empty_vao = {}
|
||||||
|
# targets
|
||||||
|
self.t_scene = ctx.texture((self.W, self.H), 4, dtype='f2')
|
||||||
|
self.f_scene = ctx.framebuffer([self.t_scene])
|
||||||
|
self.t_acc = ctx.texture((self.W, self.H), 4, dtype='f4')
|
||||||
|
self.f_acc = ctx.framebuffer([self.t_acc])
|
||||||
|
self.t_out = ctx.texture((self.W, self.H), 4, dtype='f1')
|
||||||
|
self.f_out = ctx.framebuffer([self.t_out])
|
||||||
|
self.bloom = []
|
||||||
|
w, h = self.W, self.H
|
||||||
|
for i in range(7):
|
||||||
|
w, h = max(w // 2, 1), max(h // 2, 1)
|
||||||
|
t = ctx.texture((w, h), 4, dtype='f2')
|
||||||
|
t.repeat_x = t.repeat_y = False
|
||||||
|
self.bloom.append((t, ctx.framebuffer([t])))
|
||||||
|
for t in (self.t_scene, self.t_acc):
|
||||||
|
t.repeat_x = t.repeat_y = False
|
||||||
|
self.p_acc = self.program(VS_FULL, ACC_FS)
|
||||||
|
self.p_bright = self.program(VS_FULL, BRIGHT_FS)
|
||||||
|
self.p_down = self.program(VS_FULL, DOWN_FS)
|
||||||
|
self.p_up = self.program(VS_FULL, UP_FS)
|
||||||
|
self.p_final = self.program(VS_FULL, FINAL_FS)
|
||||||
|
self.text_cache = OrderedDict()
|
||||||
|
self.img_cache = {}
|
||||||
|
self.rt_cache = {}
|
||||||
|
self.batch = []
|
||||||
|
self.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
||||||
|
self.shake = (0.0, 0.0)
|
||||||
|
self.alpha = 1.0
|
||||||
|
self.logical = (LW, LH)
|
||||||
|
self.target_stack = []
|
||||||
|
|
||||||
|
# ---- programs
|
||||||
|
def program(self, vs, fs):
|
||||||
|
key = (vs, fs)
|
||||||
|
if key not in self.progs:
|
||||||
|
self.progs[key] = self.ctx.program(vertex_shader=preprocess(vs), fragment_shader=preprocess(fs))
|
||||||
|
return self.progs[key]
|
||||||
|
|
||||||
|
def fs_program(self, src):
|
||||||
|
return self.program(VS_FULL, src)
|
||||||
|
|
||||||
|
def _set(self, prog, **u):
|
||||||
|
for k, v in u.items():
|
||||||
|
if k in prog:
|
||||||
|
prog[k].value = v
|
||||||
|
|
||||||
|
def _common(self, prog):
|
||||||
|
self._set(prog, u_cam=self.cam, u_shake=self.shake, u_logical=self.logical, u_alpha=self.alpha)
|
||||||
|
|
||||||
|
# ---- render targets (offscreen layers in logical space)
|
||||||
|
def layer(self, name, w=LW, h=LH):
|
||||||
|
key = (name, w, h)
|
||||||
|
if key not in self.rt_cache:
|
||||||
|
t = self.ctx.texture((int(w * self.scale), int(h * self.scale)), 4, dtype='f2')
|
||||||
|
t.build_mipmaps()
|
||||||
|
t.filter = (moderngl.LINEAR_MIPMAP_LINEAR, moderngl.LINEAR)
|
||||||
|
self.rt_cache[key] = (t, self.ctx.framebuffer([t]))
|
||||||
|
return self.rt_cache[key]
|
||||||
|
|
||||||
|
def push_target(self, name, w=LW, h=LH, clear=(0, 0, 0, 0)):
|
||||||
|
"""Draw into an offscreen layer of logical size w x h until pop_target(); returns its texture."""
|
||||||
|
self.flush()
|
||||||
|
t, fb = self.layer(name, w, h)
|
||||||
|
self.target_stack.append((fb, t, self.cam, self.shake, self.logical, self.alpha))
|
||||||
|
fb.use()
|
||||||
|
fb.clear(*clear)
|
||||||
|
self.logical = (w, h)
|
||||||
|
self.cam = (w / 2, h / 2, 1.0, 0.0)
|
||||||
|
self.shake = (0.0, 0.0)
|
||||||
|
self.alpha = 1.0
|
||||||
|
return t
|
||||||
|
|
||||||
|
def pop_target(self):
|
||||||
|
self.flush()
|
||||||
|
fb, t, self.cam, self.shake, self.logical, self.alpha = self.target_stack.pop()
|
||||||
|
t.build_mipmaps()
|
||||||
|
prev = self.target_stack[-1][0] if self.target_stack else self.f_scene
|
||||||
|
prev.use()
|
||||||
|
return t
|
||||||
|
|
||||||
|
# ---- shapes
|
||||||
|
def _shape(self, kind, rect, xf, p, fill, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0, extra=(0, 0)):
|
||||||
|
pp = list(p) + [0.0] * (16 - len(p))
|
||||||
|
self.batch.append(('s', (rect, xf, (kind, extra[0], extra[1], 0), pp, fill, stroke, (sw, glow, glow_k, 0))))
|
||||||
|
|
||||||
|
def circle(self, x, y, r, fill, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0):
|
||||||
|
self._shape(0, (-r, -r, r, r), (x, y, 0, 1), (r,), fill, stroke, sw, glow, glow_k)
|
||||||
|
|
||||||
|
def rect(self, x, y, w, h, fill, r=0.0, rot=0.0, stroke=(0, 0, 0, 0), sw=0.0, glow=0.0, glow_k=0.0, s=1.0):
|
||||||
|
"""Centred rectangle (x, y = centre) with corner radius r."""
|
||||||
|
hw, hh = abs(w) / 2, abs(h) / 2
|
||||||
|
r = min(r, hw, hh)
|
||||||
|
self._shape(1, (-hw, -hh, hw, hh), (x, y, rot, s), (hw, hh, r), fill, stroke, sw, glow, glow_k)
|
||||||
|
|
||||||
|
def line(self, x0, y0, x1, y1, width, fill, w1=None, glow=0.0, glow_k=0.0):
|
||||||
|
r0 = width / 2
|
||||||
|
r1 = (w1 if w1 is not None else width) / 2
|
||||||
|
rr = max(r0, r1)
|
||||||
|
self._shape(2, (min(x0, x1) - rr, min(y0, y1) - rr, max(x0, x1) + rr, max(y0, y1) + rr), (0, 0, 0, 1),
|
||||||
|
(x0, y0, x1, y1, r0, r1), fill, glow=glow, glow_k=glow_k)
|
||||||
|
|
||||||
|
def poly(self, pts, fill, x=0.0, y=0.0, rot=0.0, s=1.0, stroke=(0, 0, 0, 0), sw=0.0, round_=0.0, glow=0.0,
|
||||||
|
glow_k=0.0):
|
||||||
|
assert 3 <= len(pts) <= 8
|
||||||
|
xs = [p[0] for p in pts]
|
||||||
|
ys = [p[1] for p in pts]
|
||||||
|
flat = [c for p in pts for c in p]
|
||||||
|
self._shape(4, (min(xs) - round_, min(ys) - round_, max(xs) + round_, max(ys) + round_), (x, y, rot, s),
|
||||||
|
flat, fill, stroke, sw, glow, glow_k, extra=(len(pts), round_))
|
||||||
|
|
||||||
|
def arc(self, x, y, r, th, a0, a1, fill, glow=0.0, glow_k=0.0):
|
||||||
|
"""Ring segment from angle a0 to a1 (radians, y down -> clockwise)."""
|
||||||
|
if a1 - a0 >= 2 * math.pi - 1e-4:
|
||||||
|
a0, a1 = -math.pi - 0.01, math.pi + 0.01
|
||||||
|
R = r + th
|
||||||
|
self._shape(5, (-R, -R, R, R), (x, y, 0, 1), (r, th, a0, a1), fill, glow=glow, glow_k=glow_k)
|
||||||
|
|
||||||
|
def ring(self, x, y, r, th, fill, glow=0.0, glow_k=0.0):
|
||||||
|
self.arc(x, y, r, th, -math.pi, math.pi, fill, glow, glow_k)
|
||||||
|
|
||||||
|
def polyline(self, pts, width, fill, glow=0.0, glow_k=0.0):
|
||||||
|
for a, b in zip(pts, pts[1:]):
|
||||||
|
self.line(a[0], a[1], b[0], b[1], width, fill, glow=glow, glow_k=glow_k)
|
||||||
|
|
||||||
|
# ---- text
|
||||||
|
def _text_tex(self, s, font, size, var, tracking):
|
||||||
|
fsize = quant_size(size * self.scale)
|
||||||
|
k = fsize / (size * self.scale)
|
||||||
|
var = {a: round(v / 2) * 2 for a, v in var.items()}
|
||||||
|
key = (s, font, round(fsize, 3), tuple(sorted(var.items())), round(tracking * self.scale * k, 2))
|
||||||
|
e = self.text_cache.get(key)
|
||||||
|
if e is not None:
|
||||||
|
self.text_cache.move_to_end(key)
|
||||||
|
return e, fsize
|
||||||
|
F = face(font)
|
||||||
|
glyphs, adv = F.shape(s, fsize, var, tracking * self.scale * k)
|
||||||
|
pad = int(fsize * 0.05) + 4
|
||||||
|
asc = int(math.ceil(fsize * 1.05))
|
||||||
|
desc = int(math.ceil(fsize * 0.35))
|
||||||
|
wpx = int(math.ceil(adv)) + 2 * pad + int(fsize * 0.3)
|
||||||
|
hpx = asc + desc + 2 * pad
|
||||||
|
canvas = np.zeros((hpx, wpx), np.uint8)
|
||||||
|
ox, oy = pad + int(fsize * 0.15), pad + asc
|
||||||
|
for gid, gx, gy, _ in glyphs:
|
||||||
|
bm, left, top = F.raster(gid, fsize, var)
|
||||||
|
if bm.size == 0:
|
||||||
|
continue
|
||||||
|
x0 = int(round(ox + gx)) + left
|
||||||
|
y0 = int(round(oy + gy)) - top
|
||||||
|
h, w = bm.shape
|
||||||
|
xa, ya = max(x0, 0), max(y0, 0)
|
||||||
|
xb, yb = min(x0 + w, wpx), min(y0 + h, hpx)
|
||||||
|
if xb <= xa or yb <= ya:
|
||||||
|
continue
|
||||||
|
sub = canvas[ya:yb, xa:xb]
|
||||||
|
np.maximum(sub, bm[ya - y0:yb - y0, xa - x0:xb - x0], out=sub)
|
||||||
|
tex = self.ctx.texture((wpx, hpx), 1, canvas.tobytes())
|
||||||
|
tex.build_mipmaps()
|
||||||
|
tex.filter = (moderngl.LINEAR_MIPMAP_LINEAR, moderngl.LINEAR)
|
||||||
|
tex.repeat_x = tex.repeat_y = False
|
||||||
|
e = (tex, wpx, hpx, ox, oy, adv, F.cap * fsize)
|
||||||
|
self.text_cache[key] = e
|
||||||
|
if len(self.text_cache) > 900:
|
||||||
|
_, old = self.text_cache.popitem(last=False)
|
||||||
|
old[0].release()
|
||||||
|
return e, fsize
|
||||||
|
|
||||||
|
def measure(self, s, font='archivo', size=100, wght=700, wdth=100, tracking=0.0):
|
||||||
|
F = face(font)
|
||||||
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
||||||
|
return F.shape(s, size, var, tracking)[1]
|
||||||
|
|
||||||
|
def text(self, s, x, y, size=100, color=(1, 1, 1, 1), font='archivo', wght=700, wdth=100, align='center',
|
||||||
|
valign='cap', rot=0.0, s_=1.0, tracking=0.0, reveal=(0.0, 1.0, 0.0, 0.0), skew=0.0, sy=1.0, mat=None):
|
||||||
|
"""Draw a line of text. (x, y) is the anchor: align = left/center/right on the advance box; valign =
|
||||||
|
'cap' (centre of cap height), 'base' (baseline), 'top' (cap top). Returns the advance width."""
|
||||||
|
if not s:
|
||||||
|
return 0.0
|
||||||
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
||||||
|
(tex, wpx, hpx, ox, oy, adv, cap), fsize = self._text_tex(s, font, size, var, tracking)
|
||||||
|
k = size / (fsize / self.scale) / self.scale # texture px -> logical px
|
||||||
|
adv_l = adv * k
|
||||||
|
ax = {'left': 0.0, 'center': adv_l / 2, 'right': adv_l}[align]
|
||||||
|
ay = {'base': 0.0, 'cap': -cap * k / 2, 'top': -cap * k}[valign]
|
||||||
|
x0 = -ox * k - ax
|
||||||
|
y0 = -oy * k - ay
|
||||||
|
xf = (x, y, mat) if mat is not None else (x, y, rot, s_)
|
||||||
|
self.batch.append(('t', (tex, (x0, y0, x0 + wpx * k, y0 + hpx * k), xf, color, reveal, (skew, sy), 0)))
|
||||||
|
return adv_l
|
||||||
|
|
||||||
|
def image(self, tex, x, y, w, h, rot=0.0, color=(1, 1, 1, 1), s=1.0, reveal=(0.0, 1.0, 0.0, 0.0), mat=None):
|
||||||
|
"""Premultiplied RGBA texture (e.g. a pushed layer, which is stored bottom-up) centred at x, y."""
|
||||||
|
xf = (x, y, mat) if mat is not None else (x, y, rot, s)
|
||||||
|
self.batch.append(('t', (tex, (-w / 2, h / 2, w / 2, -h / 2), xf, color, reveal, (0, 1), 1)))
|
||||||
|
|
||||||
|
def glyph_runs(self, s, font='archivo', size=100, wght=700, wdth=100, tracking=0.0):
|
||||||
|
"""Per-character x offsets (logical px, pen start = 0) and total advance, for per-letter animation."""
|
||||||
|
F = face(font)
|
||||||
|
var = {'wght': wght, 'wdth': wdth} if font != 'mono' else {'wght': wght}
|
||||||
|
glyphs, adv = F.shape(s, size, var, tracking)
|
||||||
|
xs = [0.0] * len(s)
|
||||||
|
for gid, gx, gy, cl in glyphs:
|
||||||
|
xs[cl] = gx
|
||||||
|
ends = []
|
||||||
|
for i in range(len(s)):
|
||||||
|
ends.append(xs[i + 1] if i + 1 < len(s) else adv)
|
||||||
|
return xs, ends, adv
|
||||||
|
|
||||||
|
# ---- fullscreen shader layer
|
||||||
|
def fullscreen(self, prog, **uniforms):
|
||||||
|
self.flush()
|
||||||
|
self._common(prog)
|
||||||
|
self._set(prog, u_res=(self.W, self.H), **uniforms)
|
||||||
|
vao = self.empty_vao.get(id(prog))
|
||||||
|
if vao is None:
|
||||||
|
vao = self.empty_vao[id(prog)] = self.ctx.vertex_array(prog, [])
|
||||||
|
vao.render(moderngl.TRIANGLES, vertices=3)
|
||||||
|
|
||||||
|
def bind_textures(self, prog, **tex):
|
||||||
|
for i, (name, t) in enumerate(tex.items()):
|
||||||
|
t.use(location=i + 1)
|
||||||
|
if name in prog:
|
||||||
|
prog[name].value = i + 1
|
||||||
|
|
||||||
|
# ---- batching
|
||||||
|
def flush(self):
|
||||||
|
if not self.batch:
|
||||||
|
return
|
||||||
|
ctx = self.ctx
|
||||||
|
ctx.enable(moderngl.BLEND)
|
||||||
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA)
|
||||||
|
i = 0
|
||||||
|
while i < len(self.batch):
|
||||||
|
kind = self.batch[i][0]
|
||||||
|
j = i
|
||||||
|
while j < len(self.batch) and self.batch[j][0] == kind and (kind == 's' or j == i):
|
||||||
|
j += 1
|
||||||
|
if kind == 's':
|
||||||
|
rows = []
|
||||||
|
for _, (rect, xf, kd, pp, fill, stroke, sw) in self.batch[i:j]:
|
||||||
|
rows.append(list(rect) + list(xf) + list(kd) + pp + list(fill) + list(stroke) + list(sw))
|
||||||
|
data = np.array(rows, dtype='f4')
|
||||||
|
if data.nbytes > self.inst.size:
|
||||||
|
self.inst.orphan(data.nbytes * 2)
|
||||||
|
self.inst.write(data.tobytes())
|
||||||
|
self._common(self.p_shape)
|
||||||
|
self.vao_shape.render(moderngl.TRIANGLE_STRIP, vertices=4, instances=len(rows))
|
||||||
|
else:
|
||||||
|
tex, rect, xf, color, reveal, skew, mode = self.batch[i][1]
|
||||||
|
p = self.p_tex
|
||||||
|
self._common(p)
|
||||||
|
tex.use(location=0)
|
||||||
|
p['u_tex'].value = 0
|
||||||
|
p['u_rect'].value = rect
|
||||||
|
p['u_t'].value = xf[:2]
|
||||||
|
p['u_m'].value = _mat(xf, skew)
|
||||||
|
p['u_color'].value = color
|
||||||
|
p['u_reveal'].value = reveal
|
||||||
|
p['u_mode'].value = mode
|
||||||
|
self.vao_tex.render(moderngl.TRIANGLE_STRIP, vertices=4)
|
||||||
|
i = j
|
||||||
|
self.batch = []
|
||||||
|
|
||||||
|
# ---- frame
|
||||||
|
def begin_sub(self, clear):
|
||||||
|
self.f_scene.use()
|
||||||
|
self.f_scene.clear(*clear)
|
||||||
|
self.logical = (LW, LH)
|
||||||
|
self.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
||||||
|
self.shake = (0.0, 0.0)
|
||||||
|
self.alpha = 1.0
|
||||||
|
|
||||||
|
def accumulate(self, weight, first):
|
||||||
|
self.flush()
|
||||||
|
ctx = self.ctx
|
||||||
|
self.f_acc.use()
|
||||||
|
if first:
|
||||||
|
self.f_acc.clear(0, 0, 0, 0)
|
||||||
|
ctx.enable(moderngl.BLEND)
|
||||||
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE)
|
||||||
|
self.t_scene.use(location=0)
|
||||||
|
self.p_acc['u_src'].value = 0
|
||||||
|
self.p_acc['u_w'].value = weight
|
||||||
|
self._fs(self.p_acc)
|
||||||
|
|
||||||
|
def _fs(self, prog):
|
||||||
|
vao = self.empty_vao.get(id(prog))
|
||||||
|
if vao is None:
|
||||||
|
vao = self.empty_vao[id(prog)] = self.ctx.vertex_array(prog, [])
|
||||||
|
vao.render(moderngl.TRIANGLES, vertices=3)
|
||||||
|
|
||||||
|
def post(self, P, frame_idx):
|
||||||
|
ctx = self.ctx
|
||||||
|
ctx.disable(moderngl.BLEND)
|
||||||
|
# bloom
|
||||||
|
t0, f0 = self.bloom[0]
|
||||||
|
f0.use()
|
||||||
|
self.t_acc.use(location=0)
|
||||||
|
self.p_bright['u_src'].value = 0
|
||||||
|
self.p_bright['u_thresh'].value = P.get('bloom_thresh', 0.9)
|
||||||
|
self.p_bright['u_texel'].value = (1 / self.W, 1 / self.H)
|
||||||
|
self._fs(self.p_bright)
|
||||||
|
for i in range(1, len(self.bloom)):
|
||||||
|
src, _ = self.bloom[i - 1]
|
||||||
|
dst, fb = self.bloom[i]
|
||||||
|
fb.use()
|
||||||
|
src.use(location=0)
|
||||||
|
self.p_down['u_src'].value = 0
|
||||||
|
self.p_down['u_texel'].value = (1 / src.width, 1 / src.height)
|
||||||
|
self._fs(self.p_down)
|
||||||
|
ctx.enable(moderngl.BLEND)
|
||||||
|
ctx.blend_func = (moderngl.ONE, moderngl.ONE)
|
||||||
|
for i in range(len(self.bloom) - 1, 0, -1):
|
||||||
|
src, _ = self.bloom[i]
|
||||||
|
dst, fb = self.bloom[i - 1]
|
||||||
|
fb.use()
|
||||||
|
src.use(location=0)
|
||||||
|
self.p_up['u_src'].value = 0
|
||||||
|
self.p_up['u_texel'].value = (1 / src.width, 1 / src.height)
|
||||||
|
self._fs(self.p_up)
|
||||||
|
ctx.disable(moderngl.BLEND)
|
||||||
|
self.f_out.use()
|
||||||
|
p = self.p_final
|
||||||
|
self.t_acc.use(location=0)
|
||||||
|
self.bloom[0][0].use(location=1)
|
||||||
|
p['u_src'].value = 0
|
||||||
|
p['u_bloom'].value = 1
|
||||||
|
self._set(p, u_res=(self.W, self.H), u_scale=self.scale,
|
||||||
|
u_bloom_k=P.get('bloom', 0.6), u_ca=P.get('ca', 0.0), u_vig=P.get('vignette', 0.35),
|
||||||
|
u_grain=P.get('grain', 0.045) * float(os.environ.get('GRAIN_K', '0.6')), u_flash=P.get('flash', (0, 0, 0, 0)), u_invert=P.get('invert', 0.0),
|
||||||
|
u_frame=float(frame_idx), u_exposure=P.get('exposure', 1.0), u_fade=P.get('fade', 0.0),
|
||||||
|
u_scan=P.get('scan', 0.0), u_grain_rate=float(os.environ.get('GRAIN_RATE', '0')))
|
||||||
|
self._fs(p)
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return self.t_out.read()
|
||||||
|
|
||||||
|
|
||||||
|
ACC_FS = '''#version 430
|
||||||
|
in vec2 v_uv; uniform sampler2D u_src; uniform float u_w; out vec4 o;
|
||||||
|
void main(){ o = texture(u_src, v_uv) * u_w; }
|
||||||
|
'''
|
||||||
|
|
||||||
|
BRIGHT_FS = '''#version 430
|
||||||
|
in vec2 v_uv; uniform sampler2D u_src; uniform float u_thresh; uniform vec2 u_texel; out vec4 o;
|
||||||
|
void main(){
|
||||||
|
vec3 c = vec3(0);
|
||||||
|
for (int i = 0; i < 4; i++){ vec2 d = vec2((i & 1) == 0 ? -0.5 : 0.5, (i & 2) == 0 ? -0.5 : 0.5) * u_texel;
|
||||||
|
c += texture(u_src, v_uv + d).rgb; }
|
||||||
|
c *= 0.25;
|
||||||
|
float l = max(c.r, max(c.g, c.b));
|
||||||
|
float knee = 0.25;
|
||||||
|
float s = clamp(l - u_thresh + knee, 0.0, 2.0 * knee);
|
||||||
|
s = s * s / (4.0 * knee + 1e-4);
|
||||||
|
float w = max(s, l - u_thresh) / max(l, 1e-4);
|
||||||
|
o = vec4(min(c * w, vec3(40.0)), 1);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
DOWN_FS = '''#version 430
|
||||||
|
in vec2 v_uv; uniform sampler2D u_src; uniform vec2 u_texel; out vec4 o;
|
||||||
|
void main(){
|
||||||
|
vec2 t = u_texel;
|
||||||
|
vec3 a = texture(u_src, v_uv + t * vec2(-2, -2)).rgb, b = texture(u_src, v_uv + t * vec2(0, -2)).rgb,
|
||||||
|
c = texture(u_src, v_uv + t * vec2(2, -2)).rgb, d = texture(u_src, v_uv + t * vec2(-2, 0)).rgb,
|
||||||
|
e = texture(u_src, v_uv).rgb, f = texture(u_src, v_uv + t * vec2(2, 0)).rgb,
|
||||||
|
g = texture(u_src, v_uv + t * vec2(-2, 2)).rgb, h = texture(u_src, v_uv + t * vec2(0, 2)).rgb,
|
||||||
|
i = texture(u_src, v_uv + t * vec2(2, 2)).rgb, j = texture(u_src, v_uv + t * vec2(-1, -1)).rgb,
|
||||||
|
k = texture(u_src, v_uv + t * vec2(1, -1)).rgb, l = texture(u_src, v_uv + t * vec2(-1, 1)).rgb,
|
||||||
|
m = texture(u_src, v_uv + t * vec2(1, 1)).rgb;
|
||||||
|
vec3 r = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
|
||||||
|
o = vec4(r, 1);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
UP_FS = '''#version 430
|
||||||
|
in vec2 v_uv; uniform sampler2D u_src; uniform vec2 u_texel; out vec4 o;
|
||||||
|
void main(){
|
||||||
|
vec2 t = u_texel;
|
||||||
|
vec3 r = texture(u_src, v_uv).rgb * 4.0;
|
||||||
|
r += (texture(u_src, v_uv + vec2(-t.x, 0)).rgb + texture(u_src, v_uv + vec2(t.x, 0)).rgb +
|
||||||
|
texture(u_src, v_uv + vec2(0, -t.y)).rgb + texture(u_src, v_uv + vec2(0, t.y)).rgb) * 2.0;
|
||||||
|
r += texture(u_src, v_uv + vec2(-t.x, -t.y)).rgb + texture(u_src, v_uv + vec2(t.x, -t.y)).rgb +
|
||||||
|
texture(u_src, v_uv + vec2(-t.x, t.y)).rgb + texture(u_src, v_uv + vec2(t.x, t.y)).rgb;
|
||||||
|
o = vec4(r / 16.0 * 0.85, 1);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
FINAL_FS = '''#version 430
|
||||||
|
in vec2 v_uv; uniform sampler2D u_src; uniform sampler2D u_bloom; uniform vec2 u_res; uniform float u_scale;
|
||||||
|
uniform float u_grain_rate, u_bloom_k, u_ca, u_vig, u_grain, u_invert, u_frame, u_exposure, u_fade, u_scan; uniform vec4 u_flash;
|
||||||
|
out vec4 o;
|
||||||
|
uint pcg(uint v){ uint s = v * 747796405u + 2891336453u; uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;
|
||||||
|
return (w >> 22u) ^ w; }
|
||||||
|
float hash(vec3 p){ // integer hash: bit-identical on every GL implementation
|
||||||
|
uvec3 q = uvec3(ivec3(p) + 65536);
|
||||||
|
return float(pcg(q.x + pcg(q.y + pcg(q.z))) & 0xffffffu) / 16777216.0; }
|
||||||
|
vec3 tonemap(vec3 c){
|
||||||
|
// linear below 0.8, smooth shoulder to 1
|
||||||
|
vec3 k = max(c - 0.8, 0.0);
|
||||||
|
return min(c, 0.8) + 0.2 * (1.0 - exp(-k / 0.2));
|
||||||
|
}
|
||||||
|
void main(){
|
||||||
|
vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y); // flip so the readback is top row first
|
||||||
|
vec2 d = uv - 0.5;
|
||||||
|
vec3 c;
|
||||||
|
if (u_ca > 0.0) {
|
||||||
|
vec2 off = d * u_ca * 0.012;
|
||||||
|
c = vec3(texture(u_src, uv + off).r, texture(u_src, uv).g, texture(u_src, uv - off).b);
|
||||||
|
} else c = texture(u_src, uv).rgb;
|
||||||
|
c += texture(u_bloom, uv).rgb * u_bloom_k;
|
||||||
|
c *= u_exposure;
|
||||||
|
c = tonemap(c);
|
||||||
|
c = mix(c, vec3(1.0) - c, u_invert);
|
||||||
|
c = mix(c, u_flash.rgb, u_flash.a);
|
||||||
|
float v = 1.0 - u_vig * pow(length(d * vec2(1.0, 0.75)) * 1.25, 2.4);
|
||||||
|
c *= clamp(v, 0.0, 1.0);
|
||||||
|
if (u_scan > 0.0) c *= 1.0 - u_scan * (0.5 + 0.5 * sin(uv.y * u_res.y * 3.14159 / max(u_scale * 2.0, 1.0)));
|
||||||
|
c *= 1.0 - u_fade;
|
||||||
|
vec2 px = floor(vec2(gl_FragCoord.x, u_res.y - gl_FragCoord.y) / max(u_scale, 0.5));
|
||||||
|
float gf = u_grain_rate > 0.0 ? floor(u_frame / u_grain_rate) : 0.0; // 0 = a fixed print texture
|
||||||
|
float g = hash(vec3(px, mod(gf, 97.0))) + hash(vec3(px + 17.0, mod(gf, 89.0))) - 1.0;
|
||||||
|
float lum = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
c += g * u_grain * (0.35 + 0.65 * (1.0 - lum)) * (0.6 + 0.4 * lum);
|
||||||
|
c = clamp(c, 0.0, 1.0);
|
||||||
|
vec3 s = mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
||||||
|
s += (hash(vec3(px, mod(gf, 61.0) + 3.0)) - 0.5) / 255.0;
|
||||||
|
o = vec4(s, 1);
|
||||||
|
}
|
||||||
|
'''
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"""Download the fonts the hype reel uses into ./fonts (SIL OFL 1.1, Google Fonts). Run once before rendering."""
|
||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
GF = 'https://github.com/google/fonts/raw/main/ofl/'
|
||||||
|
FONTS = {
|
||||||
|
'Archivo[wdth,wght].ttf': GF + 'archivo/Archivo%5Bwdth,wght%5D.ttf',
|
||||||
|
'Archivo-Italic[wdth,wght].ttf': GF + 'archivo/Archivo-Italic%5Bwdth,wght%5D.ttf',
|
||||||
|
'JetBrainsMono[wght].ttf': GF + 'jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf',
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
out = os.path.join(HERE, 'fonts')
|
||||||
|
os.makedirs(out, exist_ok=True)
|
||||||
|
for name, url in FONTS.items():
|
||||||
|
path = os.path.join(out, name)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print('fetching', name)
|
||||||
|
with urllib.request.urlopen(url) as r, open(path, 'wb') as f:
|
||||||
|
f.write(r.read())
|
||||||
|
print('fonts ready in', out)
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
"""Reusable visual pieces: backgrounds, HUD, glue blobs, jelly cubes, isometric boxes, code panels, slams."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from engine import LW, LH, _read, hexc
|
||||||
|
|
||||||
|
COS30, SIN30 = math.cos(math.pi / 6), 0.5
|
||||||
|
|
||||||
|
|
||||||
|
# --- background ------------------------------------------------------------------------------------
|
||||||
|
def bg(E, c):
|
||||||
|
E.rect(LW / 2, LH / 2, LW * 4, LH * 4, c)
|
||||||
|
|
||||||
|
|
||||||
|
def dot_grid(E, f, c, step=60, r=2.2, drift=(0.0, 0.0), pulse=0.0):
|
||||||
|
ox = (drift[0] * f.lt) % step
|
||||||
|
oy = (drift[1] * f.lt) % step
|
||||||
|
rr = r * (1 + pulse * f.kick)
|
||||||
|
y = -step + oy
|
||||||
|
while y < LH + step:
|
||||||
|
x = -step + ox
|
||||||
|
while x < LW + step:
|
||||||
|
E.circle(x, y, rr, c)
|
||||||
|
x += step
|
||||||
|
y += step
|
||||||
|
|
||||||
|
|
||||||
|
def line_grid(E, f, c, step=120, w=1.5, drift=(0.0, 0.0)):
|
||||||
|
ox = (drift[0] * f.lt) % step
|
||||||
|
oy = (drift[1] * f.lt) % step
|
||||||
|
x = -step + ox
|
||||||
|
while x < LW + step:
|
||||||
|
E.line(x, -10, x, LH + 10, w, c)
|
||||||
|
x += step
|
||||||
|
y = -step + oy
|
||||||
|
while y < LH + step:
|
||||||
|
E.line(-10, y, LW + 10, y, w, c)
|
||||||
|
y += step
|
||||||
|
|
||||||
|
|
||||||
|
# --- HUD -------------------------------------------------------------------------------------------
|
||||||
|
def hud(E, f, left='', right='', col=None, alpha=1.0):
|
||||||
|
"""Crop marks + tiny mono annotations; drawn in screen space."""
|
||||||
|
col = col or a(BONE, 0.55)
|
||||||
|
col = a(col, alpha)
|
||||||
|
cam, sh = E.cam, E.shake
|
||||||
|
E.cam, E.shake = (LW / 2, LH / 2, 1.0, 0.0), (0.0, 0.0)
|
||||||
|
m, L = 44, 34
|
||||||
|
for sx, sy in ((m, m), (LW - m, m), (m, LH - m), (LW - m, LH - m)):
|
||||||
|
dx = 1 if sx < LW / 2 else -1
|
||||||
|
dy = 1 if sy < LH / 2 else -1
|
||||||
|
E.line(sx, sy, sx + dx * L, sy, 2.5, col)
|
||||||
|
E.line(sx, sy, sx, sy + dy * L, 2.5, col)
|
||||||
|
if left:
|
||||||
|
E.text(left, m + 52, m + 6, 17, col, font='mono', wght=500, align='left', valign='cap')
|
||||||
|
if right:
|
||||||
|
E.text(right, LW - m - 52, m + 6, 17, col, font='mono', wght=500, align='right', valign='cap')
|
||||||
|
E.text(f'{int(f.bar):03d}.{int(f.beat) % 4 + 1} ', LW - m - 52, LH - m - 6, 17, col, font='mono', wght=500,
|
||||||
|
align='right')
|
||||||
|
E.text('MOBILEGL', m + 52, LH - m - 6, 17, col, font='mono', wght=700, align='left')
|
||||||
|
E.flush()
|
||||||
|
E.cam, E.shake = cam, sh
|
||||||
|
|
||||||
|
|
||||||
|
# --- glue ------------------------------------------------------------------------------------------
|
||||||
|
_goo = {}
|
||||||
|
|
||||||
|
|
||||||
|
def goo(E, balls, caps=(), k=40.0, col=GLUE, rim=hexc('#2f8a2b'), hi=(1, 1, 1, 1), outline=5.0, bevel=0.0, glow=0.0):
|
||||||
|
"""balls: [(x, y, r)], caps: [(ax, ay, bx, by, r0, r1)] in logical px (camera applies)."""
|
||||||
|
prog = _goo.get('p')
|
||||||
|
if prog is None:
|
||||||
|
prog = _goo['p'] = E.fs_program(_read('goo.glsl'))
|
||||||
|
bl = list(balls)[:64]
|
||||||
|
if bevel <= 0: # dome over the whole blob: bevel ~ the biggest radius
|
||||||
|
bevel = max([r for _, _, r in bl] + [max(c[4], c[5]) * 1.5 for c in caps] + [10.0])
|
||||||
|
cp = list(caps)[:24]
|
||||||
|
bv = [(x, y, r, 0.0) for x, y, r in bl] + [(0, 0, 0, 0)] * (64 - len(bl))
|
||||||
|
cv = [(c[0], c[1], c[2], c[3]) for c in cp] + [(0, 0, 0, 0)] * (24 - len(cp))
|
||||||
|
cr = [(c[4], c[5], 0, 0) for c in cp] + [(0, 0, 0, 0)] * (24 - len(cp))
|
||||||
|
prog['u_balls'].value = bv
|
||||||
|
if 'u_caps' in prog:
|
||||||
|
prog['u_caps'].value = cv
|
||||||
|
prog['u_capr'].value = cr
|
||||||
|
E.fullscreen(prog, u_nb=len(bl), u_nc=len(cp), u_k=k, u_col=col, u_rim=rim, u_hi=hi, u_outline=outline,
|
||||||
|
u_bevel=bevel, u_glow=glow)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_shape(x, y, r, stretch=0.0):
|
||||||
|
"""A falling droplet: ball + tapered tail upward (stretch in px)."""
|
||||||
|
if stretch <= 1:
|
||||||
|
return [(x, y, r)], []
|
||||||
|
return [(x, y, r)], [(x, y - stretch, x, y, r * 0.18, r * 0.85)]
|
||||||
|
|
||||||
|
|
||||||
|
# --- jelly cubes -----------------------------------------------------------------------------------
|
||||||
|
_cube = {}
|
||||||
|
|
||||||
|
|
||||||
|
GLUE_BODY = hexc('#5cb846')
|
||||||
|
GLUE_DEEP = hexc('#1f5e22')
|
||||||
|
|
||||||
|
|
||||||
|
def cubes(E, f, items, body=GLUE_BODY, alt=hexc('#2c6e30'), deep=GLUE_DEEP, speckle=1.0, pitch=0.6155):
|
||||||
|
"""The MobileGlues mark: a block floating in glue.
|
||||||
|
items: [(x, y, half_size_px, yaw, squash, wobble, tint, alpha[, seconds_since_impact])]; a negative last value
|
||||||
|
means the block is still falling towards the glue, 0 is the hit, large (default) is settled."""
|
||||||
|
prog = _cube.get('p')
|
||||||
|
if prog is None:
|
||||||
|
prog = _cube['p'] = E.fs_program(_read('block.glsl'))
|
||||||
|
it = list(items)[:16]
|
||||||
|
c1 = [(i[0], i[1], i[2], i[3]) for i in it] + [(0, 0, 1, 0)] * (16 - len(it))
|
||||||
|
c2 = [(i[4], i[5], i[6], i[7]) for i in it] + [(0, 0, 0, 0)] * (16 - len(it))
|
||||||
|
c3 = [((i[8] if len(i) > 8 else 100.0), 0, 0, 0) for i in it] + [(100.0, 0, 0, 0)] * (16 - len(it))
|
||||||
|
prog['u_cubes'].value = c1
|
||||||
|
prog['u_cubes2'].value = c2
|
||||||
|
prog['u_cubes3'].value = c3
|
||||||
|
E.fullscreen(prog, u_n=len(it), u_time=f.t, u_body=body[:3], u_alt=alt[:3], u_deep=deep[:3],
|
||||||
|
u_speckle=speckle, u_pitch=pitch)
|
||||||
|
|
||||||
|
|
||||||
|
# --- isometric -------------------------------------------------------------------------------------
|
||||||
|
class Iso:
|
||||||
|
def __init__(self, cx, cy, s):
|
||||||
|
self.cx, self.cy, self.s = cx, cy, s
|
||||||
|
|
||||||
|
def p(self, x, y, z):
|
||||||
|
return (self.cx + (x - y) * COS30 * self.s, self.cy + (x + y) * SIN30 * self.s - z * self.s)
|
||||||
|
|
||||||
|
def box(self, E, x0, y0, z0, dx, dy, dz, top, left, right, line=None, sw=0.0):
|
||||||
|
x1, y1, z1 = x0 + dx, y0 + dy, z0 + dz
|
||||||
|
P = self.p
|
||||||
|
st = line or (0, 0, 0, 0)
|
||||||
|
E.poly([P(x0, y1, z0), P(x1, y1, z0), P(x1, y1, z1), P(x0, y1, z1)], left, stroke=st, sw=sw)
|
||||||
|
E.poly([P(x1, y0, z0), P(x1, y1, z0), P(x1, y1, z1), P(x1, y0, z1)], right, stroke=st, sw=sw)
|
||||||
|
E.poly([P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1)], top, stroke=st, sw=sw)
|
||||||
|
|
||||||
|
def quad_top(self, E, x0, y0, z, dx, dy, c, line=None, sw=0.0):
|
||||||
|
P = self.p
|
||||||
|
E.poly([P(x0, y0, z), P(x0 + dx, y0, z), P(x0 + dx, y0 + dy, z), P(x0, y0 + dy, z)], c,
|
||||||
|
stroke=line or (0, 0, 0, 0), sw=sw)
|
||||||
|
|
||||||
|
def mat_top(self, s=1.0):
|
||||||
|
return (COS30 * s, -COS30 * s, SIN30 * s, SIN30 * s)
|
||||||
|
|
||||||
|
def mat_left(self, s=1.0): # face y = const, u along +x, v down
|
||||||
|
return (COS30 * s, 0.0, SIN30 * s, s)
|
||||||
|
|
||||||
|
def mat_right(self, s=1.0): # face x = const, u along -y, v down
|
||||||
|
return (COS30 * s, 0.0, -SIN30 * s, s)
|
||||||
|
|
||||||
|
|
||||||
|
# --- type pieces -----------------------------------------------------------------------------------
|
||||||
|
def chip(E, x, y, s, size=26, fg=INK, bg_=GLUE, font='mono', wght=700, align='center', pad=(18, 10), rot=0.0,
|
||||||
|
alpha=1.0, sc=1.0):
|
||||||
|
w = E.measure(s, font, size, wght)
|
||||||
|
ax = {'left': w / 2 + pad[0], 'center': 0, 'right': -w / 2 - pad[0]}[align]
|
||||||
|
c, sn = math.cos(rot), math.sin(rot)
|
||||||
|
cx, cy = x + ax * c * sc, y + ax * sn * sc
|
||||||
|
E.rect(cx, cy, (w + 2 * pad[0]) * sc, (size * 0.75 + 2 * pad[1]) * sc, a(bg_, alpha), r=10 * sc, rot=rot)
|
||||||
|
E.text(s, cx, cy, size * sc, a(fg, alpha), font=font, wght=wght, rot=rot)
|
||||||
|
return w + 2 * pad[0]
|
||||||
|
|
||||||
|
|
||||||
|
SYNTAX = {'kw': hexc('#8cff62'), 'fn': hexc('#f1f7ec'), 'num': hexc('#e7a94f'), 'str': hexc('#a0e28c'),
|
||||||
|
'cm': hexc('#5f8f63'), 'p': hexc('#a9c4a6'), 'const': hexc('#d4f0c9')}
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize(line):
|
||||||
|
"""Very small GL/GLSL-ish highlighter -> [(text, kind)]."""
|
||||||
|
import re
|
||||||
|
out = []
|
||||||
|
for m in re.finditer(r'(//.*$)|("[^"]*")|(\b\d[\w.]*\b)|(\bGL_\w+|\bMOBILEGL_\w+)|(\bgl[A-Z]\w*|\bwgl\w*|\begl\w*)|'
|
||||||
|
r'(\b(?:void|in|out|uniform|vec[234]|float|int|return|layout|#version|core|export)\b)|(\w+)|(\s+)|(.)',
|
||||||
|
line):
|
||||||
|
g = m.lastindex
|
||||||
|
kind = {1: 'cm', 2: 'str', 3: 'num', 4: 'const', 5: 'kw', 6: 'kw', 7: 'fn', 8: 'p', 9: 'p'}[g]
|
||||||
|
out.append((m.group(0), kind))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def code_line(E, s, x, y, size=30, alpha=1.0, chars=None, cursor=False, f=None):
|
||||||
|
"""Monospace syntax-coloured line, left-aligned at x (baseline-ish centre y). chars = typed so far."""
|
||||||
|
cw = size * 0.6
|
||||||
|
n = len(s) if chars is None else max(0, min(len(s), int(chars)))
|
||||||
|
col = 0
|
||||||
|
for tok, kind in tokenize(s):
|
||||||
|
if col >= n:
|
||||||
|
break
|
||||||
|
part = tok[:n - col]
|
||||||
|
if part.strip():
|
||||||
|
E.text(part, x + col * cw, y, size, a(SYNTAX[kind], alpha), font='mono', wght=500, align='left')
|
||||||
|
col += len(tok)
|
||||||
|
if cursor and (f is None or (f.t * 2.2) % 1 < 0.6):
|
||||||
|
E.rect(x + n * cw + cw * 0.5, y, cw * 0.9, size * 1.05, a(GLUE, alpha))
|
||||||
|
return n * cw
|
||||||
|
|
||||||
|
|
||||||
|
def slam(E, f, word, x, y, t_hit, size=260, col=BONE, wght=900, wdth=100, rot=0.0, punch=0.18, align='center',
|
||||||
|
font='archivo', tracking=0.0, hold=None):
|
||||||
|
"""A word that lands at t_hit: scales down from (1 + punch) with a quick fade-in, then holds."""
|
||||||
|
u = f.t - t_hit
|
||||||
|
if u < -0.02:
|
||||||
|
return 0.0
|
||||||
|
k = 1 + punch * math.exp(-max(u, 0) / 0.07)
|
||||||
|
al = clamp01((u + 0.02) / 0.03)
|
||||||
|
if hold is not None and u > hold:
|
||||||
|
al *= 1 - clamp01((u - hold) / 0.06)
|
||||||
|
return E.text(word, x, y, size, a(col, al * col[3]), font=font, wght=wght, wdth=wdth, rot=rot, s_=k, align=align,
|
||||||
|
tracking=tracking)
|
||||||
|
|
||||||
|
|
||||||
|
def stamp(E, f, text, x, y, t_hit, size=70, col=CORAL, rot=-0.12, w_pad=34):
|
||||||
|
u = f.t - t_hit
|
||||||
|
if u < 0:
|
||||||
|
return
|
||||||
|
k = 1 + 0.6 * math.exp(-u / 0.05)
|
||||||
|
w = E.measure(text, 'archivo', size, 900, 80)
|
||||||
|
E.rect(x, y, (w + 2 * w_pad) * k, size * 1.2 * k, (0, 0, 0, 0), r=10, rot=rot, stroke=col, sw=8 * k)
|
||||||
|
E.text(text, x, y, size * k, col, wght=900, wdth=80, rot=rot)
|
||||||
|
|
||||||
|
|
||||||
|
def cross(E, x, y, r, col=CORAL, w=18, u=1.0):
|
||||||
|
u = clamp01(u)
|
||||||
|
if u <= 0:
|
||||||
|
return
|
||||||
|
d = r * 0.7071
|
||||||
|
u1 = clamp01(u * 2)
|
||||||
|
u2 = clamp01(u * 2 - 1)
|
||||||
|
E.line(x - d, y - d, x - d + 2 * d * u1, y - d + 2 * d * u1, w, col)
|
||||||
|
if u2 > 0:
|
||||||
|
E.line(x + d, y - d, x + d - 2 * d * u2, y - d + 2 * d * u2, w, col)
|
||||||
|
|
||||||
|
|
||||||
|
def check(E, x, y, r, col=GLUE, w=12, u=1.0):
|
||||||
|
u = clamp01(u)
|
||||||
|
pts = [(x - r * 0.55, y), (x - r * 0.15, y + r * 0.42), (x + r * 0.6, y - r * 0.45)]
|
||||||
|
l1 = math.dist(pts[0], pts[1])
|
||||||
|
l2 = math.dist(pts[1], pts[2])
|
||||||
|
L = (l1 + l2) * u
|
||||||
|
if L <= 0:
|
||||||
|
return
|
||||||
|
if L <= l1:
|
||||||
|
q = L / l1
|
||||||
|
E.line(pts[0][0], pts[0][1], lerp(pts[0][0], pts[1][0], q), lerp(pts[0][1], pts[1][1], q), w, col)
|
||||||
|
else:
|
||||||
|
E.line(pts[0][0], pts[0][1], pts[1][0], pts[1][1], w, col)
|
||||||
|
q = (L - l1) / l2
|
||||||
|
E.line(pts[1][0], pts[1][1], lerp(pts[1][0], pts[2][0], q), lerp(pts[1][1], pts[2][1], q), w, col)
|
||||||
|
|
||||||
|
|
||||||
|
def arrow(E, x0, y0, x1, y1, w, col, head=None, u=1.0):
|
||||||
|
u = clamp01(u)
|
||||||
|
if u <= 0:
|
||||||
|
return
|
||||||
|
xe, ye = lerp(x0, x1, u), lerp(y0, y1, u)
|
||||||
|
ang = math.atan2(ye - y0, xe - x0)
|
||||||
|
head = head or w * 3.2
|
||||||
|
bx, by = xe - math.cos(ang) * head * 0.8, ye - math.sin(ang) * head * 0.8
|
||||||
|
E.line(x0, y0, bx, by, w, col)
|
||||||
|
E.poly([(xe, ye), (xe - math.cos(ang - 0.5) * head, ye - math.sin(ang - 0.5) * head),
|
||||||
|
(xe - math.cos(ang + 0.5) * head, ye - math.sin(ang + 0.5) * head)], col)
|
||||||
|
|
||||||
|
|
||||||
|
def screen_cam(E, f, zoom=1.0, rot=0.0, cx=LW / 2, cy=LH / 2, shake=0.0, seed=0):
|
||||||
|
E.cam = (cx, cy, zoom, rot)
|
||||||
|
if shake > 0:
|
||||||
|
fi = int(f.t * 60)
|
||||||
|
E.shake = ((rnd(seed, fi, 1) - 0.5) * 2 * shake, (rnd(seed, fi, 2) - 0.5) * 2 * shake)
|
||||||
|
else:
|
||||||
|
E.shake = (0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# --- plates / words --------------------------------------------------------------------------------
|
||||||
|
PLATES = {
|
||||||
|
'ink': (INK, BONE, a(MOSS, 0.55)),
|
||||||
|
'mint': (MINT, INK, a(LEAF, 0.35)),
|
||||||
|
'glue': (GLUE, INK, a(LEAF, 0.45)),
|
||||||
|
'pale': (PALE, INK, a(LEAF, 0.3)),
|
||||||
|
'black': (hexc('#000000'), BONE, None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def plate(E, f, kind='ink', grid=True, drift=(0.0, 0.0)):
|
||||||
|
"""Background colour + dot grid; returns (bg, fg)."""
|
||||||
|
b, fg, g = PLATES[kind]
|
||||||
|
bg(E, b)
|
||||||
|
if grid and g is not None:
|
||||||
|
dot_grid(E, f, g, 60, 2.2, drift)
|
||||||
|
return b, fg
|
||||||
|
|
||||||
|
|
||||||
|
def fit_size(E, word, width, wght=900, wdth=100, font='archivo', cap=640):
|
||||||
|
w = E.measure(word, font, 100, wght, wdth)
|
||||||
|
return min(cap, 100 * width / max(w, 1))
|
||||||
|
|
||||||
|
|
||||||
|
TRI_FS = '''#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
in vec2 v_uv; out vec4 o;
|
||||||
|
uniform vec2 u_res; uniform vec2 u_p[3]; uniform vec4 u_c[3];
|
||||||
|
float cross2(vec2 a, vec2 b){ return a.x * b.y - a.y * b.x; }
|
||||||
|
void main(){
|
||||||
|
vec2 p = uncam(vec2(v_uv.x, 1.0 - v_uv.y) * u_logical);
|
||||||
|
vec2 a = u_p[0], b = u_p[1], c = u_p[2];
|
||||||
|
float A = cross2(b - a, c - a);
|
||||||
|
float w0 = cross2(b - p, c - p) / A, w1 = cross2(c - p, a - p) / A, w2 = 1.0 - w0 - w1;
|
||||||
|
// signed distance to the triangle (negative inside)
|
||||||
|
float d = 1e5;
|
||||||
|
vec2 e[3] = vec2[3](b - a, c - b, a - c), v[3] = vec2[3](a, b, c);
|
||||||
|
for (int i = 0; i < 3; i++){ vec2 pv = p - v[i]; vec2 q = pv - e[i] * clamp(dot(pv, e[i]) / dot(e[i], e[i]), 0.0, 1.0);
|
||||||
|
d = min(d, length(q)); }
|
||||||
|
bool inside = w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0;
|
||||||
|
float sd = inside ? -d : d;
|
||||||
|
float px = u_logical.x / u_res.x / u_cam.z;
|
||||||
|
float al = clamp(0.5 - sd / px, 0.0, 1.0);
|
||||||
|
vec4 col = u_c[0] * clamp(w0, 0.0, 1.0) + u_c[1] * clamp(w1, 0.0, 1.0) + u_c[2] * clamp(w2, 0.0, 1.0);
|
||||||
|
o = vec4(col.rgb, 1.0) * al * col.a * u_alpha;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
_tri = {}
|
||||||
|
|
||||||
|
|
||||||
|
def tri_rgb(E, pts, cols):
|
||||||
|
prog = _tri.get('p')
|
||||||
|
if prog is None:
|
||||||
|
prog = _tri['p'] = E.fs_program(TRI_FS)
|
||||||
|
prog['u_p'].value = [tuple(p) for p in pts]
|
||||||
|
prog['u_c'].value = [tuple(c) for c in cols]
|
||||||
|
E.fullscreen(prog)
|
||||||
|
|
||||||
|
|
||||||
|
def typed(f, s, b0, b1):
|
||||||
|
"""Substring of s typed between local beats b0 and b1."""
|
||||||
|
n = int(len(s) * clamp01((f.lbeat - b0) / (b1 - b0)) + 1e-6)
|
||||||
|
return s[:n]
|
||||||
|
|
||||||
|
|
||||||
|
def word_seq(E, f, words, b0, step, x, y, size, col, **kw):
|
||||||
|
"""Words that replace each other, one per `step` beats from b0; returns the active index."""
|
||||||
|
k = int(math.floor((f.lbeat - b0) / step + 1e-6))
|
||||||
|
if 0 <= k < len(words):
|
||||||
|
slam(E, f, words[k], x, y, f.t - (f.lbeat - (b0 + k * step)) * S.BEAT, size, col, **kw)
|
||||||
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
def speedlines(E, f, k, col=None, n=70, cx=LW / 2, cy=LH / 2, seed=0):
|
||||||
|
"""Radial streaks rushing outward; k = intensity 0..1 (speed, length and count all scale)."""
|
||||||
|
if k <= 0.01:
|
||||||
|
return
|
||||||
|
col = col or BONE
|
||||||
|
for i in range(int(n * min(1.0, 0.3 + k))):
|
||||||
|
ang = rnd('sl', seed, i) * 2 * math.pi
|
||||||
|
sp = 0.4 + 0.6 * rnd('sp', seed, i)
|
||||||
|
ph = (rnd('ph', seed, i) + f.t * sp * (0.6 + 2.4 * k)) % 1.0
|
||||||
|
r0 = 120 + ph * ph * 1400
|
||||||
|
ln = (40 + 360 * k) * (0.3 + ph)
|
||||||
|
x0, y0 = cx + math.cos(ang) * r0, cy + math.sin(ang) * r0
|
||||||
|
x1, y1 = cx + math.cos(ang) * (r0 + ln), cy + math.sin(ang) * (r0 + ln)
|
||||||
|
E.line(x0, y0, x1, y1, 2 + 5 * k * ph, a(col, (0.15 + 0.5 * k) * min(1, ph * 3)))
|
||||||
|
|
||||||
|
|
||||||
|
def beat_plate(f, kinds, b0=0.0):
|
||||||
|
"""Plate kind for the current beat (never changes faster than once a beat)."""
|
||||||
|
k = int(math.floor(f.lbeat - b0 + 1e-6))
|
||||||
|
return kinds[k % len(kinds)], k
|
||||||
|
|
||||||
|
|
||||||
|
def climax(f, b0, b1):
|
||||||
|
"""0 -> 1 ramp (accelerating) over local beats [b0, b1]."""
|
||||||
|
return in_cubic(clamp01((f.lbeat - b0) / (b1 - b0)))
|
||||||
|
|
||||||
|
|
||||||
|
def pre_drop(E, f, cx, cy, size, t_hit, scale=1.0):
|
||||||
|
"""The silent beat before a drop: a spotlit blob of glue waits, the block falls into it on the downbeat."""
|
||||||
|
bg(E, hexc('#000000'))
|
||||||
|
E.circle(cx, cy + 40 * scale, 520 * scale, a(INK2, 0.9))
|
||||||
|
cubes(E, f, [(cx, cy, size, 0.0, 0.0, 0.0, 0.0, 1.0, f.t - t_hit)])
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
"""Hype electro-pop instrumental for the MobileGL showreel, synthesised from score.py.
|
||||||
|
usage: python music.py out.wav"""
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
from scipy.signal import fftconvolve, butter, sosfiltfilt
|
||||||
|
from dsp import SR, saw, pulse, sine, svf, onepole_hp, pingpong, compress, limit, mtof, adsr
|
||||||
|
import score as S
|
||||||
|
|
||||||
|
N = int(S.END * SR)
|
||||||
|
rng = np.random.default_rng(1234)
|
||||||
|
|
||||||
|
|
||||||
|
def idx(t):
|
||||||
|
return int(round(t * SR))
|
||||||
|
|
||||||
|
|
||||||
|
class Bus:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.l = np.zeros(N)
|
||||||
|
self.r = np.zeros(N)
|
||||||
|
|
||||||
|
def add(self, t, sig, pan=0.0, gain=1.0):
|
||||||
|
"""sig: mono array or (l, r) tuple; pan -1..1 (equal-power)."""
|
||||||
|
i0 = idx(t)
|
||||||
|
if isinstance(sig, tuple):
|
||||||
|
l, r = sig
|
||||||
|
else:
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
l, r = sig * np.cos(a) * 1.4142, sig * np.sin(a) * 1.4142
|
||||||
|
if i0 < 0:
|
||||||
|
l, r, i0 = l[-i0:], r[-i0:], 0
|
||||||
|
n = min(len(l), N - i0)
|
||||||
|
if n <= 0:
|
||||||
|
return
|
||||||
|
self.l[i0:i0 + n] += gain * l[:n]
|
||||||
|
self.r[i0:i0 + n] += gain * r[:n]
|
||||||
|
|
||||||
|
def apply(self, g):
|
||||||
|
self.l *= g
|
||||||
|
self.r *= g
|
||||||
|
|
||||||
|
|
||||||
|
def noise(n):
|
||||||
|
return rng.standard_normal(n)
|
||||||
|
|
||||||
|
|
||||||
|
def tarr(n):
|
||||||
|
return np.arange(n) / SR
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
# automation helpers
|
||||||
|
def curve(points):
|
||||||
|
"""Piecewise-exponential-ish automation over the whole song from [(time, value)] (log interp)."""
|
||||||
|
ts = np.array([p[0] for p in points])
|
||||||
|
vs = np.log(np.array([p[1] for p in points]))
|
||||||
|
return np.exp(np.interp(np.arange(N) / SR, ts, vs))
|
||||||
|
|
||||||
|
|
||||||
|
def lin_curve(points):
|
||||||
|
ts = np.array([p[0] for p in points])
|
||||||
|
vs = np.array([p[1] for p in points])
|
||||||
|
return np.interp(np.arange(N) / SR, ts, vs)
|
||||||
|
|
||||||
|
|
||||||
|
def sidechain(depth, rel=0.16):
|
||||||
|
"""Gain curve ducking on every kick."""
|
||||||
|
g = np.ones(N)
|
||||||
|
kn = int(0.6 * SR)
|
||||||
|
t = tarr(kn)
|
||||||
|
shape = 1 - depth * np.where(t < 0.004, t / 0.004, np.exp(-(np.maximum(t - 0.004, 0) / rel) ** 1.6))
|
||||||
|
for tk, v in S.events('kick'):
|
||||||
|
i0 = idx(tk)
|
||||||
|
n = min(kn, N - i0)
|
||||||
|
g[i0:i0 + n] = np.minimum(g[i0:i0 + n], 1 - (1 - shape[:n]) * min(v, 1.0))
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
def gaps():
|
||||||
|
"""Mute window (one beat of silence) before each drop."""
|
||||||
|
g = np.ones(N)
|
||||||
|
for b in (16, 56, 88):
|
||||||
|
a0, a1 = idx(S.tb(b - 1, 3)), idx(S.tb(b))
|
||||||
|
f = int(0.006 * SR)
|
||||||
|
g[a0:a1] = 0
|
||||||
|
g[a0 - f:a0] = np.linspace(1, 0, f)
|
||||||
|
g[a1 - f // 2:a1] = 0
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
# drums
|
||||||
|
def kick(v):
|
||||||
|
n = int(0.5 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
f = 46 + 130 * np.exp(-t / 0.032) + 40 * np.exp(-t / 0.006)
|
||||||
|
body = sine(f, 0.0) * (np.exp(-t / 0.32) * np.minimum(t / 0.002, 1))
|
||||||
|
click = svf(noise(n), np.full(n, 3500.0), 0.2, 1) * np.exp(-t / 0.004) * 0.6
|
||||||
|
x = np.tanh(1.6 * (body + click)) * 0.95
|
||||||
|
return x * v
|
||||||
|
|
||||||
|
|
||||||
|
def clap(v):
|
||||||
|
n = int(0.35 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
nz = svf(noise(n), np.full(n, 1300.0), 0.35, 1) * 2.2
|
||||||
|
env = np.zeros(n)
|
||||||
|
for k, d in enumerate((0.0, 0.009, 0.018, 0.027)):
|
||||||
|
m = t >= d
|
||||||
|
env[m] += np.exp(-(t[m] - d) / (0.006 if k < 3 else 0.11)) * (0.8 if k < 3 else 1.0)
|
||||||
|
body = sine(np.full(n, 190.0) + 40 * np.exp(-t / 0.01), 0.25) * np.exp(-t / 0.05) * 0.35
|
||||||
|
return (nz * env + body) * v * 0.8
|
||||||
|
|
||||||
|
|
||||||
|
def snare(v, pitch=1.0):
|
||||||
|
n = int(0.25 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
nz = svf(noise(n), np.full(n, 4200.0 * pitch), 0.1, 1) * 1.8 * np.exp(-t / 0.07)
|
||||||
|
body = sine(np.full(n, 200.0 * pitch) + 60 * np.exp(-t / 0.008), 0.0) * np.exp(-t / 0.06)
|
||||||
|
return (nz * 0.7 + body * 0.6) * v
|
||||||
|
|
||||||
|
|
||||||
|
METAL = np.array([205.3, 304.4, 369.6, 522.7, 540.0, 800.0]) * 1.7
|
||||||
|
|
||||||
|
|
||||||
|
def metal(n):
|
||||||
|
x = np.zeros(n)
|
||||||
|
for f in METAL:
|
||||||
|
x += pulse(np.full(n, f), 0.5, rng.random())
|
||||||
|
return x / len(METAL)
|
||||||
|
|
||||||
|
|
||||||
|
def hat(v, decay=0.035, n_s=0.12):
|
||||||
|
n = int(n_s * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
x = 0.55 * metal(n) + 0.6 * noise(n)
|
||||||
|
x = svf(x, np.full(n, 8500.0), 0.2, 2)
|
||||||
|
return x * np.exp(-t / decay) * v * 0.55
|
||||||
|
|
||||||
|
|
||||||
|
def shaker(v):
|
||||||
|
n = int(0.09 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
x = svf(noise(n), np.full(n, 6500.0), 0.3, 1)
|
||||||
|
env = np.minimum(t / 0.012, 1) * np.exp(-t / 0.03)
|
||||||
|
return x * env * v * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def crash(v):
|
||||||
|
n = int(2.6 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
x = 0.5 * metal(n) + noise(n)
|
||||||
|
x = svf(x, np.full(n, 5200.0), 0.1, 2)
|
||||||
|
xl = x * np.exp(-t / 0.75)
|
||||||
|
xr = np.roll(x, 37) * np.exp(-t / 0.7)
|
||||||
|
return (xl * v * 0.3, xr * v * 0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def impact(v):
|
||||||
|
n = int(3.0 * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
boom = sine(30 + 45 * np.exp(-t / 0.35), 0.0) * np.exp(-t / 0.9) * np.minimum(t / 0.003, 1)
|
||||||
|
crack = svf(noise(n), 6000 * np.exp(-t / 0.25) + 120, 0.2, 0) * np.exp(-t / 0.5)
|
||||||
|
x = np.tanh(1.3 * boom) * 0.9 + crack * 0.35
|
||||||
|
return x * v
|
||||||
|
|
||||||
|
|
||||||
|
def riser(dur, v):
|
||||||
|
n = int(dur * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
u = t / dur
|
||||||
|
nz = svf(noise(n), 300 * (9000 / 300) ** (u ** 1.4), 0.55, 1)
|
||||||
|
tone = saw(110 * 2 ** (u ** 1.3 * 2), 0.0) * 0.15
|
||||||
|
tone = svf(tone, 400 + 5000 * u ** 2, 0.3, 0)
|
||||||
|
env = u ** 2.2
|
||||||
|
x = (nz * 0.9 + tone) * env * v * 0.55
|
||||||
|
return (x, np.roll(x, 211))
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_swell(v, dur):
|
||||||
|
"""Reversed crash-ish swell that ends exactly on the drop."""
|
||||||
|
l, r = crash(1.0)
|
||||||
|
n = int(dur * SR)
|
||||||
|
return (l[:n][::-1] * v * 1.4, r[:n][::-1] * v * 1.4)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
# tonal voices
|
||||||
|
DETUNE = np.array([-0.24, -0.15, -0.06, 0.0, 0.06, 0.15, 0.24]) # semitones
|
||||||
|
PANS = np.array([-0.9, -0.6, -0.3, 0.0, 0.3, 0.6, 0.9])
|
||||||
|
|
||||||
|
|
||||||
|
def supersaw(midi, dur, v, att=0.006, rel=0.25, spread=1.0):
|
||||||
|
n = int((dur + rel) * SR)
|
||||||
|
l = np.zeros(n)
|
||||||
|
r = np.zeros(n)
|
||||||
|
for d, p in zip(DETUNE * spread, PANS):
|
||||||
|
x = saw(np.full(n, float(mtof(midi + d))), rng.random())
|
||||||
|
a = (p + 1) * np.pi / 4
|
||||||
|
l += x * np.cos(a)
|
||||||
|
r += x * np.sin(a)
|
||||||
|
env = adsr(n, att, 0.3, 0.75, int(dur * SR), rel)
|
||||||
|
return (l * env * v / 3.2, r * env * v / 3.2)
|
||||||
|
|
||||||
|
|
||||||
|
def pluck(midi, dur, v, bright=1.0):
|
||||||
|
n = int((dur + 0.35) * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
f = np.full(n, float(mtof(midi)))
|
||||||
|
x = 0.6 * saw(f, rng.random()) + 0.4 * pulse(f * 1.003, 0.35, rng.random())
|
||||||
|
cut = 250 + bright * 4200 * np.exp(-t / 0.09)
|
||||||
|
x = svf(x, cut, 0.25, 0)
|
||||||
|
return x * np.exp(-t / 0.22) * np.minimum(t / 0.002, 1) * v
|
||||||
|
|
||||||
|
|
||||||
|
def keys(midi, dur, v):
|
||||||
|
"""FM electric piano stab."""
|
||||||
|
n = int((dur + 0.4) * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
f = float(mtof(midi))
|
||||||
|
idx_env = 2.2 * np.exp(-t / 0.15) + 0.4
|
||||||
|
mod = sine(np.full(n, f), 0.0) * idx_env
|
||||||
|
car = np.sin(2 * np.pi * f * t + mod)
|
||||||
|
tine = sine(np.full(n, f * 14.0), 0.0) * np.exp(-t / 0.03) * 0.15
|
||||||
|
env = np.exp(-t / 0.6) * np.minimum(t / 0.002, 1)
|
||||||
|
rs = int(dur * SR)
|
||||||
|
if rs < n:
|
||||||
|
env[rs:] *= np.exp(-(t[rs:] - t[rs]) / 0.06)
|
||||||
|
return (car + tine) * env * v
|
||||||
|
|
||||||
|
|
||||||
|
def bell(midi, dur, v):
|
||||||
|
n = int((dur + 1.6) * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
f = float(mtof(midi))
|
||||||
|
mod = sine(np.full(n, f * 3.5), 0.0) * (1.4 * np.exp(-t / 0.4) + 0.1)
|
||||||
|
car = np.sin(2 * np.pi * f * t + mod)
|
||||||
|
sub = np.sin(2 * np.pi * f * t) * 0.5
|
||||||
|
env = np.exp(-t / 1.1) * np.minimum(t / 0.004, 1)
|
||||||
|
return (car * 0.6 + sub) * env * v
|
||||||
|
|
||||||
|
|
||||||
|
def mono_line(notes, kind, glide=0.035, vib=0.12):
|
||||||
|
"""Monophonic lead over a whole note list [(t, dur, midi, vel)] -> (l, r) full-length arrays (sparse
|
||||||
|
regions zero). Glides between notes that touch; vibrato fades in on held notes."""
|
||||||
|
if not notes:
|
||||||
|
return np.zeros(N), np.zeros(N)
|
||||||
|
notes = sorted(notes)
|
||||||
|
t0 = notes[0][0] - 0.05
|
||||||
|
t1 = notes[-1][0] + notes[-1][1] + 0.6
|
||||||
|
i0, i1 = idx(t0), min(idx(t1), N)
|
||||||
|
n = i1 - i0
|
||||||
|
tt = (np.arange(n) + i0) / SR
|
||||||
|
pitch = np.full(n, float(notes[0][2]))
|
||||||
|
gate = np.zeros(n)
|
||||||
|
vel = np.zeros(n)
|
||||||
|
since = np.full(n, 10.0)
|
||||||
|
for k, (ts, d, m, v) in enumerate(notes):
|
||||||
|
a, b = idx(ts) - i0, min(idx(ts + d) - i0, n)
|
||||||
|
nxt = idx(notes[k + 1][0]) - i0 if k + 1 < len(notes) else n
|
||||||
|
seg = slice(a, min(nxt, n))
|
||||||
|
prev = pitch[a - 1] if a > 0 else m
|
||||||
|
u = (tt[seg] - ts)
|
||||||
|
pitch[seg] = m + (prev - m) * np.exp(-u / glide) if k > 0 and notes[k - 1][0] + notes[k - 1][1] >= ts - 0.02 else m
|
||||||
|
gate[a:b] = 1
|
||||||
|
vel[a:min(nxt, n)] = v
|
||||||
|
since[seg] = u
|
||||||
|
vibr = vib * np.sin(2 * np.pi * 5.6 * tt) * np.clip((since - 0.18) / 0.25, 0, 1)
|
||||||
|
f = mtof(pitch + vibr)
|
||||||
|
# amp: attack 4ms, release 70ms (one-pole on gate)
|
||||||
|
up, dn = np.exp(-1 / (0.004 * SR)), np.exp(-1 / (0.07 * SR))
|
||||||
|
from scipy.signal import lfilter
|
||||||
|
att = lfilter([1 - up], [1, -up], gate)
|
||||||
|
rel = lfilter([1 - dn], [1, -dn], gate)
|
||||||
|
env = np.where(gate > 0, att, rel) * vel
|
||||||
|
if kind == 'lead':
|
||||||
|
x = 0.45 * saw(f * 2 ** (0.08 / 12), 0.1) + 0.45 * saw(f * 2 ** (-0.08 / 12), 0.6) + 0.35 * pulse(f, 0.3, 0.3)
|
||||||
|
cut = 900 + 5200 * np.clip(env, 0, 1) * (0.55 + 0.45 * np.exp(-np.minimum(since, 5) / 0.15))
|
||||||
|
x = svf(x, cut, 0.3, 0)
|
||||||
|
sub = sine(f / 2, 0.0) * 0.25
|
||||||
|
x = (x + sub) * env
|
||||||
|
l = x + 0.0
|
||||||
|
r = 0.45 * saw(f * 2 ** (0.12 / 12), 0.9)
|
||||||
|
r = svf(r, cut, 0.3, 0) * env * 0.6 + x * 0.7
|
||||||
|
l = l * 1.0
|
||||||
|
out = (l, r)
|
||||||
|
else: # 'chop': vowel-ish formant voice
|
||||||
|
src = saw(f, 0.2) * 0.7 + pulse(f, 0.5, 0.0) * 0.3
|
||||||
|
vowel = 0.5 + 0.5 * np.sin(2 * np.pi * tt * S.BPM / 60 / 2)
|
||||||
|
f1 = 700 - 300 * vowel
|
||||||
|
f2 = 1220 - 400 * vowel
|
||||||
|
x = svf(src, f1, 0.85, 1) + 0.7 * svf(src, f2, 0.85, 1) + 0.25 * svf(src, np.full(n, 2600.0), 0.8, 1)
|
||||||
|
x = x * env * 1.6
|
||||||
|
out = (x, x)
|
||||||
|
L, R = np.zeros(N), np.zeros(N)
|
||||||
|
L[i0:i1] = out[0][:n]
|
||||||
|
R[i0:i1] = out[1][:n]
|
||||||
|
return L, R
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
def build():
|
||||||
|
B = {k: Bus(k) for k in ('kick', 'drums', 'bass', 'chords', 'pad', 'arp', 'lead', 'keys', 'fx')}
|
||||||
|
|
||||||
|
# --- drums / fx from the shared event list
|
||||||
|
for t, k, v in S.EVENTS:
|
||||||
|
if k == 'kick':
|
||||||
|
B['kick'].add(t, kick(v))
|
||||||
|
elif k == 'clap':
|
||||||
|
B['drums'].add(t, clap(v), 0.0, 0.7)
|
||||||
|
elif k == 'snare':
|
||||||
|
B['drums'].add(t, snare(v), 0.1, 0.6)
|
||||||
|
elif k == 'roll':
|
||||||
|
B['drums'].add(t, snare(v, 1.0 + 0.5 * v), 0.0, 0.38)
|
||||||
|
elif k == 'hat':
|
||||||
|
B['drums'].add(t, hat(v), 0.25, 0.75)
|
||||||
|
elif k == 'ohat':
|
||||||
|
B['drums'].add(t, hat(v, 0.12, 0.35), -0.2, 0.6)
|
||||||
|
elif k == 'shaker':
|
||||||
|
B['drums'].add(t, shaker(v), -0.35, 0.8)
|
||||||
|
elif k == 'crash':
|
||||||
|
B['fx'].add(t, crash(v), gain=0.9)
|
||||||
|
elif k == 'impact':
|
||||||
|
B['fx'].add(t, impact(v), gain=0.8)
|
||||||
|
elif k == 'heart':
|
||||||
|
B['kick'].add(t, kick(v) * 0.6)
|
||||||
|
B['kick'].add(t + 0.16, kick(v) * 0.35)
|
||||||
|
elif k == 'riser':
|
||||||
|
B['fx'].add(t, riser(v, 1.0), gain=0.42)
|
||||||
|
elif k == 'drop':
|
||||||
|
B['fx'].add(t - S.BAR, reverse_swell(0.5, S.BAR), gain=0.6)
|
||||||
|
|
||||||
|
# --- harmony
|
||||||
|
for bar in range(S.NBARS):
|
||||||
|
name, a, b = S.section(bar)
|
||||||
|
i = bar - a
|
||||||
|
voicing, root = S.chord(bar)
|
||||||
|
t = S.tb(bar)
|
||||||
|
drop = name.startswith('drop')
|
||||||
|
# pads
|
||||||
|
if name in ('intro', 'build1', 'verse', 'break', 'build2', 'build3', 'outro') or drop:
|
||||||
|
pv = {'intro': 1.2, 'build1': 0.45, 'verse': 0.3, 'break': 0.6, 'build2': 0.4, 'build3': 0.4,
|
||||||
|
'outro': 0.5}.get(name, 0.3)
|
||||||
|
for m in voicing:
|
||||||
|
B['pad'].add(t, supersaw(m - 12 if m > 60 else m, S.BAR, pv * 0.35, att=0.35, rel=0.8,
|
||||||
|
spread=0.6))
|
||||||
|
# big chords
|
||||||
|
if drop or name in ('build2', 'build3') or (name == 'build1' and i >= 4) or (name == 'outro'):
|
||||||
|
vv = 1.0 if drop else 0.45
|
||||||
|
if name == 'outro' and i >= 4:
|
||||||
|
if i == 4:
|
||||||
|
for m in voicing + (voicing[0] + 12, voicing[1] + 12):
|
||||||
|
B['chords'].add(t, supersaw(m, 4 * S.BAR - 0.2, 0.9, rel=1.5))
|
||||||
|
else:
|
||||||
|
if drop:
|
||||||
|
# rhythmic stabs: the classic pumping offbeat-driven pattern
|
||||||
|
for s, l in ((0, 3), (3, 3), (6, 4), (10, 2), (12, 4)):
|
||||||
|
for m in voicing + (voicing[2] + 12,):
|
||||||
|
B['chords'].add(S.tb(bar, 0, s), supersaw(m, l * S.S16 - 0.01, vv, rel=0.12))
|
||||||
|
else:
|
||||||
|
for m in voicing + (voicing[2] + 12,):
|
||||||
|
B['chords'].add(t, supersaw(m, S.BAR - 0.02, vv, rel=0.2))
|
||||||
|
# bass
|
||||||
|
if name not in ('intro', 'break') and not (name == 'outro' and i >= 4):
|
||||||
|
if drop or name == 'outro':
|
||||||
|
for s in range(0, 16, 2):
|
||||||
|
m = root + (12 if s in (6, 14) and drop else 0)
|
||||||
|
B['bass'].add(S.tb(bar, 0, s + 1), bassnote(m, S.S16 * 1.6, 0.9), 0.0)
|
||||||
|
B['bass'].add(t, subnote(root, S.BAR - 0.03, 0.9))
|
||||||
|
elif name == 'verse':
|
||||||
|
for s, l, o in ((0, 3, 0), (3, 2, 12), (6, 2, 0), (8, 3, 0), (11, 1, 12), (12, 2, 7), (14, 2, 12)):
|
||||||
|
B['bass'].add(S.tb(bar, 0, s), bassnote(root + o, l * S.S16, 0.6))
|
||||||
|
B['bass'].add(S.tb(bar, 0, s), subnote(root + o if o != 12 else root, l * S.S16, 0.5))
|
||||||
|
else: # builds: plain quarter-note roots, saving the 8th-note pump for the drops
|
||||||
|
bv = 0.7 if name == 'build1' else 0.55 + 0.25 * (i / 8)
|
||||||
|
for q in range(4):
|
||||||
|
B['bass'].add(S.tb(bar, q), bassnote(root, S.BEAT * 0.8, bv))
|
||||||
|
B['bass'].add(S.tb(bar, q), subnote(root, S.BEAT * 0.8, bv * 0.85))
|
||||||
|
elif name == 'outro' and i == 4:
|
||||||
|
B['bass'].add(t, subnote(root, 3 * S.BAR, 0.9))
|
||||||
|
# arp
|
||||||
|
if name in ('intro', 'build1', 'verse', 'build2', 'build3', 'break') or drop:
|
||||||
|
if name == 'intro' and i < 2:
|
||||||
|
continue
|
||||||
|
tones = [voicing[0] + 12, voicing[1] + 12, voicing[2] + 12, voicing[0] + 24]
|
||||||
|
order = [0, 1, 2, 3, 2, 1, 2, 3, 0, 1, 2, 3, 2, 3, 1, 2]
|
||||||
|
step = 2 if name == 'break' else 1
|
||||||
|
av = {'intro': 0.5, 'build1': 0.4, 'verse': 0.42, 'break': 0.32}.get(name, 0.3)
|
||||||
|
for s in range(0, 16, step):
|
||||||
|
if name in ('intro', 'build1', 'build2', 'build3'):
|
||||||
|
bright = 0.25 + 0.75 * (i + s / 16) / (b - a)
|
||||||
|
else:
|
||||||
|
bright = 0.7
|
||||||
|
B['arp'].add(S.tb(bar, 0, s), pluck(tones[order[s]], S.S16, av * (1.0 if s % 4 == 0 else 0.75),
|
||||||
|
bright), pan=0.35 * np.sin(s * 0.9))
|
||||||
|
# keys (verse stabs, break bells)
|
||||||
|
if name == 'verse':
|
||||||
|
for s in (2, 6, 10, 14) if i % 2 else (2, 6, 7, 10, 14):
|
||||||
|
for m in voicing:
|
||||||
|
B['keys'].add(S.tb(bar, 0, s), keys(m + 12, S.S16 * 1.5, 0.22), pan=-0.2)
|
||||||
|
if name == 'break':
|
||||||
|
BREAK = [[(0, 4, 72), (4, 4, 69), (8, 8, 72)], [(0, 6, 77), (6, 2, 76), (8, 8, 72)],
|
||||||
|
[(0, 4, 74), (4, 4, 71), (8, 8, 74)], [(0, 6, 79), (6, 2, 77), (8, 8, 74)],
|
||||||
|
[(0, 4, 76), (4, 4, 72), (8, 8, 76)], [(0, 6, 81), (6, 2, 79), (8, 8, 76)],
|
||||||
|
[(0, 8, 77), (8, 8, 76)], [(0, 8, 74), (8, 4, 71), (12, 4, 74)]]
|
||||||
|
for ts, d, m in S.notes_in(bar, BREAK[i]):
|
||||||
|
B['keys'].add(ts, bell(m, d, 0.3), pan=0.1)
|
||||||
|
|
||||||
|
# --- lead lines
|
||||||
|
lead = []
|
||||||
|
chop = []
|
||||||
|
lead_hi = []
|
||||||
|
for bar in range(S.NBARS):
|
||||||
|
name, a, b = S.section(bar)
|
||||||
|
i = bar - a
|
||||||
|
if name.startswith('drop'):
|
||||||
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i)):
|
||||||
|
lead.append((ts, d * 0.92, m, 1.0))
|
||||||
|
if name != 'drop1' or i >= 8:
|
||||||
|
chop.append((ts, d * 0.92, m + 12, (0.6 if name == 'drop1' else 0.7 if name == 'drop2' else 0.85)
|
||||||
|
* (1.0 if i >= 8 else 0.7)))
|
||||||
|
if i >= 8:
|
||||||
|
lead_hi.append((ts, d * 0.92, m + 12, 0.5 + 0.03 * (i - 8)))
|
||||||
|
elif name == 'outro' and i < 4:
|
||||||
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i if i < 3 else 7)):
|
||||||
|
lead.append((ts, d * 0.92, m, 1.0 - 0.12 * i))
|
||||||
|
elif name == 'build3' and i >= 4:
|
||||||
|
for ts, d, m in S.notes_in(bar, S.hook_bar(i - 4)):
|
||||||
|
lead.append((ts, d * 0.9, m, 0.45 + 0.1 * (i - 4)))
|
||||||
|
lead.append((S.tb(108), S.BAR * 3, 76 + 2, 0.9))
|
||||||
|
ll, lr = mono_line(lead, 'lead')
|
||||||
|
B['lead'].add(0, (ll, lr), gain=0.42)
|
||||||
|
hl, hr = mono_line(lead_hi, 'lead', vib=0.08)
|
||||||
|
B['lead'].add(0, (hr, hl), gain=0.16)
|
||||||
|
cl, cr = mono_line(chop, 'chop', glide=0.02, vib=0.05)
|
||||||
|
B['lead'].add(0, (cl, cr), gain=0.2)
|
||||||
|
return B
|
||||||
|
|
||||||
|
|
||||||
|
def bassnote(m, dur, v):
|
||||||
|
n = int((dur + 0.05) * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
f = np.full(n, float(mtof(m)))
|
||||||
|
x = saw(f, 0.0) * 0.7 + saw(f * 1.004, 0.5) * 0.5
|
||||||
|
x = svf(x, 180 + 900 * np.exp(-t / 0.07), 0.35, 0)
|
||||||
|
env = np.minimum(t / 0.003, 1) * np.where(t < dur, 1.0, np.exp(-(t - dur) / 0.012))
|
||||||
|
return np.tanh(x * 1.5) * env * v * 0.55
|
||||||
|
|
||||||
|
|
||||||
|
def subnote(m, dur, v):
|
||||||
|
n = int((dur + 0.05) * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
x = sine(np.full(n, float(mtof(m))), 0.0)
|
||||||
|
env = np.minimum(t / 0.004, 1) * np.where(t < dur, 1.0, np.exp(-(t - dur) / 0.015))
|
||||||
|
return x * env * v * 0.6
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------------------------------
|
||||||
|
def reverb_ir(seconds, damp, seed):
|
||||||
|
r = np.random.default_rng(seed)
|
||||||
|
n = int(seconds * SR)
|
||||||
|
t = tarr(n)
|
||||||
|
out = []
|
||||||
|
for ch in range(2):
|
||||||
|
x = r.standard_normal(n) * np.exp(-t / (seconds / 6.9))
|
||||||
|
x = svf(x, damp * np.exp(-t / seconds * 1.5) + 400, 0.0, 0)
|
||||||
|
x[: int(0.012 * SR)] *= np.linspace(0, 1, int(0.012 * SR))
|
||||||
|
out.append(x / np.sqrt(np.sum(x ** 2)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def conv(l, r, ir):
|
||||||
|
return fftconvolve(l, ir[0])[:N], fftconvolve(r, ir[1])[:N]
|
||||||
|
|
||||||
|
|
||||||
|
def mix(B):
|
||||||
|
sc_hard = sidechain(0.85)
|
||||||
|
sc_mid = sidechain(0.55)
|
||||||
|
sc_soft = sidechain(0.3)
|
||||||
|
# section automation of the chord / pad / arp low-pass
|
||||||
|
T = S.tb
|
||||||
|
chord_cut = curve([(0, 300), (T(12), 500), (T(16) - 0.01, 9000), (T(16), 11000), (T(32), 11000),
|
||||||
|
(T(48), 600), (T(55, 3), 8000), (T(56), 12000), (T(72), 12000), (T(80), 500),
|
||||||
|
(T(87, 3), 8000), (T(88), 13000), (T(104), 13000), (T(108), 6000), (T(112) + 3, 300)])
|
||||||
|
pad_cut = curve([(0, 450), (T(8), 2400), (T(16), 2600), (T(72), 2600), (T(73), 1600), (T(80), 2600),
|
||||||
|
(T(108), 3000), (T(112) + 3, 300)])
|
||||||
|
arp_cut = curve([(0, 900), (T(8), 3000), (T(16), 7000), (T(112), 7000)])
|
||||||
|
for k in ('chords',):
|
||||||
|
B[k].l = svf(B[k].l, chord_cut, 0.15, 0)
|
||||||
|
B[k].r = svf(B[k].r, chord_cut, 0.15, 0)
|
||||||
|
B['pad'].l = svf(B['pad'].l, pad_cut, 0.1, 0)
|
||||||
|
B['pad'].r = svf(B['pad'].r, pad_cut, 0.1, 0)
|
||||||
|
B['arp'].l = svf(B['arp'].l, arp_cut, 0.1, 0)
|
||||||
|
B['arp'].r = svf(B['arp'].r, arp_cut, 0.1, 0)
|
||||||
|
|
||||||
|
B['chords'].apply(sc_hard)
|
||||||
|
B['pad'].apply(sc_hard)
|
||||||
|
B['bass'].apply(sc_hard)
|
||||||
|
B['arp'].apply(sc_mid)
|
||||||
|
B['keys'].apply(sc_soft)
|
||||||
|
B['lead'].apply(sc_soft)
|
||||||
|
g = gaps()
|
||||||
|
for k in ('chords', 'pad', 'bass', 'arp', 'keys', 'lead'):
|
||||||
|
B[k].apply(g)
|
||||||
|
|
||||||
|
gains = {'kick': 0.95, 'drums': 1.0, 'bass': 0.68, 'chords': 0.3, 'pad': 0.4, 'arp': 0.28, 'lead': 1.4,
|
||||||
|
'keys': 0.42, 'fx': 0.45}
|
||||||
|
L = np.zeros(N)
|
||||||
|
R = np.zeros(N)
|
||||||
|
for k, b in B.items():
|
||||||
|
L += b.l * gains[k]
|
||||||
|
R += b.r * gains[k]
|
||||||
|
|
||||||
|
# sends
|
||||||
|
hall = reverb_ir(2.8, 5000, 7)
|
||||||
|
room = reverb_ir(0.7, 7000, 9)
|
||||||
|
send_hall_l = B['lead'].l * 0.35 + B['keys'].l * 0.35 + B['pad'].l * 0.3 + B['arp'].l * 0.25 + B['fx'].l * 0.3
|
||||||
|
send_hall_r = B['lead'].r * 0.35 + B['keys'].r * 0.35 + B['pad'].r * 0.3 + B['arp'].r * 0.25 + B['fx'].r * 0.3
|
||||||
|
send_room_l = B['drums'].l * 0.4
|
||||||
|
send_room_r = B['drums'].r * 0.4
|
||||||
|
hl, hr = conv(send_hall_l, send_hall_r, hall)
|
||||||
|
rl, rr = conv(send_room_l, send_room_r, room)
|
||||||
|
dl, dr = pingpong(B['lead'].l * 0.5 + B['arp'].l * 0.3 + B['keys'].l * 0.3,
|
||||||
|
B['lead'].r * 0.5 + B['arp'].r * 0.3 + B['keys'].r * 0.3, S.S16 * 3, 0.38, 3500.0)
|
||||||
|
L += hl * 0.4 + rl * 0.3 + dl * 0.2
|
||||||
|
R += hr * 0.4 + rr * 0.3 + dr * 0.2
|
||||||
|
|
||||||
|
# stutters (glitch repeats of the first 16th of the beat)
|
||||||
|
for t0, _ in S.events('stutter'):
|
||||||
|
a = idx(t0)
|
||||||
|
pattern = [2, 2, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25] # in 16ths
|
||||||
|
src_l, src_r = L[a:a + idx(S.S16 * 2)].copy(), R[a:a + idx(S.S16 * 2)].copy()
|
||||||
|
p = a
|
||||||
|
for k, s in enumerate(pattern):
|
||||||
|
n = idx(S.S16 * s)
|
||||||
|
f = np.ones(n)
|
||||||
|
fd = min(64, n // 4)
|
||||||
|
f[:fd] = np.linspace(0, 1, fd)
|
||||||
|
f[-fd:] = np.linspace(1, 0, fd)
|
||||||
|
gain = 1.0 - 0.02 * k
|
||||||
|
L[p:p + n] = src_l[:n] * f * gain
|
||||||
|
R[p:p + n] = src_r[:n] * f * gain
|
||||||
|
p += n
|
||||||
|
|
||||||
|
# master
|
||||||
|
L = onepole_hp(L, 22.0)
|
||||||
|
R = onepole_hp(R, 22.0)
|
||||||
|
L, R = compress(L, R, -9.0, 2.0, 0.01, 0.15, 1.5)
|
||||||
|
L, R = np.tanh(L * 0.9) / 0.9, np.tanh(R * 0.9) / 0.9
|
||||||
|
return L, R
|
||||||
|
|
||||||
|
|
||||||
|
def master(L, R, target_lufs=-10.5):
|
||||||
|
import pyloudnorm as pyln
|
||||||
|
meter = pyln.Meter(SR)
|
||||||
|
loud = meter.integrated_loudness(np.stack([L, R], 1))
|
||||||
|
g = 10 ** ((target_lufs - loud) / 20)
|
||||||
|
L, R = L * g, R * g
|
||||||
|
L, R = limit(L, R, 0.89, 0.005, 0.08)
|
||||||
|
fade = int(1.5 * SR)
|
||||||
|
L[-fade:] *= np.linspace(1, 0, fade) ** 2
|
||||||
|
R[-fade:] *= np.linspace(1, 0, fade) ** 2
|
||||||
|
return L, R, loud
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import soundfile as sf
|
||||||
|
B = build()
|
||||||
|
L, R = mix(B)
|
||||||
|
L, R, pre = master(L, R)
|
||||||
|
import pyloudnorm as pyln
|
||||||
|
after = pyln.Meter(SR).integrated_loudness(np.stack([L, R], 1))
|
||||||
|
print(f'pre {pre:.1f} LUFS -> {after:.1f} LUFS, peak {max(abs(L).max(), abs(R).max()):.3f}, '
|
||||||
|
f'dc {L.mean():.4f}')
|
||||||
|
sf.write(sys.argv[1], np.stack([L, R], 1).astype(np.float32), SR, subtype='PCM_24')
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""Render driver.
|
||||||
|
python render.py still T [T ...] [--scale S] [--samples N] [--only name] -> out/still_T.png
|
||||||
|
python render.py sheet T0 T1 N [--scale S] -> out/sheet_T0_T1.jpg
|
||||||
|
python render.py video T0 T1 [--scale S] [--samples N] [--out file] -> mp4 (with music)
|
||||||
|
python render.py final [--samples N] -> out/mobilegl_hype.mp4
|
||||||
|
MGL_LIBGL=<path to opengl32.dll> renders through that GL implementation."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import score as S
|
||||||
|
from engine import Engine
|
||||||
|
from anim import Frame, frame_idx
|
||||||
|
import importlib
|
||||||
|
scenes = importlib.import_module(sys.argv[sys.argv.index('--module') + 1] if '--module' in sys.argv else 'scenes')
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
OUT = os.path.join(HERE, 'out')
|
||||||
|
FPS = int(sys.argv[sys.argv.index('--fps') + 1]) if '--fps' in sys.argv else 60
|
||||||
|
|
||||||
|
|
||||||
|
def opt(name, default, cast=float):
|
||||||
|
if name in sys.argv:
|
||||||
|
return cast(sys.argv[sys.argv.index(name) + 1])
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def draw_frame(E, t, fi):
|
||||||
|
n = E.samples
|
||||||
|
P = None
|
||||||
|
for s in range(n):
|
||||||
|
ts = t + ((s + 0.5) / n - 0.5) * E.shutter / FPS if n > 1 else t
|
||||||
|
p = scenes.draw(E, ts)
|
||||||
|
if s == n // 2 or P is None:
|
||||||
|
P = p
|
||||||
|
E.accumulate(1.0 / n, s == 0)
|
||||||
|
E.post(P or {}, fi)
|
||||||
|
|
||||||
|
|
||||||
|
def grab(E):
|
||||||
|
a = np.frombuffer(E.read(), np.uint8).reshape(E.H, E.W, 4)
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def make_engine():
|
||||||
|
E = Engine(scale=opt('--scale', 1.0), samples=opt('--samples', 1, int), shutter=opt('--shutter', 0.5))
|
||||||
|
if 'MobileGL' in E.renderer:
|
||||||
|
import sc_final
|
||||||
|
gpu = E.renderer.rsplit('(', 1)[1].split(',')[0] if '(' in E.renderer else 'GPU'
|
||||||
|
backend = 'Magma → Vulkan' if 'Vulkan' in E.renderer else 'Espryt → OpenGL ES'
|
||||||
|
sc_final.RENDERED_WITH['line'] = f'{backend} · {gpu} · Windows / WGL'
|
||||||
|
return E
|
||||||
|
|
||||||
|
|
||||||
|
def still(times):
|
||||||
|
E = make_engine()
|
||||||
|
print('renderer:', E.renderer)
|
||||||
|
paths = []
|
||||||
|
for t in times:
|
||||||
|
draw_frame(E, t, frame_idx(t))
|
||||||
|
p = os.path.join(OUT, f'still_{t:07.2f}.png')
|
||||||
|
Image.fromarray(grab(E)[:, :, :3]).save(p)
|
||||||
|
paths.append(p)
|
||||||
|
print('\n'.join(paths))
|
||||||
|
|
||||||
|
|
||||||
|
def sheet(t0, t1, n, cols=4):
|
||||||
|
E = make_engine()
|
||||||
|
imgs = []
|
||||||
|
for k in range(n):
|
||||||
|
t = t0 + (t1 - t0) * k / max(n - 1, 1)
|
||||||
|
draw_frame(E, t, frame_idx(t))
|
||||||
|
im = Image.fromarray(grab(E)[:, :, :3]).resize((480, 270), Image.LANCZOS)
|
||||||
|
imgs.append((t, im))
|
||||||
|
rows = (n + cols - 1) // cols
|
||||||
|
sh = Image.new('RGB', (cols * 484, rows * 290), (30, 30, 30))
|
||||||
|
from PIL import ImageDraw
|
||||||
|
d = ImageDraw.Draw(sh)
|
||||||
|
for k, (t, im) in enumerate(imgs):
|
||||||
|
x, y = (k % cols) * 484 + 2, (k // cols) * 290 + 2
|
||||||
|
sh.paste(im, (x, y))
|
||||||
|
d.text((x + 4, y + 272), f'{t:.2f}s bar {t / S.BAR:.2f}', fill=(220, 220, 220))
|
||||||
|
p = os.path.join(OUT, f'sheet_{t0:.1f}_{t1:.1f}.jpg')
|
||||||
|
sh.save(p, quality=88)
|
||||||
|
print(p)
|
||||||
|
|
||||||
|
|
||||||
|
def video(t0, t1, out, audio=True, crf=16, preset='slow'):
|
||||||
|
E = make_engine()
|
||||||
|
print('renderer:', E.renderer)
|
||||||
|
f0, f1 = int(round(t0 * FPS)), int(round(t1 * FPS))
|
||||||
|
args = ['ffmpeg', '-y', '-loglevel', 'error', '-f', 'rawvideo', '-pix_fmt', 'rgba', '-s', f'{E.W}x{E.H}',
|
||||||
|
'-r', str(FPS), '-i', '-']
|
||||||
|
wav = opt('--audio', os.path.join(OUT, 'music.wav'), str)
|
||||||
|
if audio and os.path.exists(wav):
|
||||||
|
args += ['-ss', f'{t0:.4f}', '-t', f'{t1 - t0:.4f}', '-i', wav, '-c:a', 'aac', '-b:a', '256k']
|
||||||
|
args += ['-c:v', 'libx264', '-preset', preset, '-crf', str(crf), '-pix_fmt', 'yuv420p',
|
||||||
|
'-movflags', '+faststart', '-shortest' if audio else '-an', out]
|
||||||
|
args = [a for a in args if a]
|
||||||
|
proc = subprocess.Popen(args, stdin=subprocess.PIPE)
|
||||||
|
start = time.time()
|
||||||
|
for fi in range(f0, f1):
|
||||||
|
t = fi / FPS
|
||||||
|
draw_frame(E, t, fi)
|
||||||
|
proc.stdin.write(E.read())
|
||||||
|
if (fi - f0) % 300 == 0:
|
||||||
|
el = time.time() - start
|
||||||
|
done = fi - f0 + 1
|
||||||
|
print(f'frame {fi} ({t:.1f}s) {done / max(el, 1e-6):.1f} fps', flush=True)
|
||||||
|
proc.stdin.close()
|
||||||
|
proc.wait()
|
||||||
|
print(out, f'{time.time() - start:.0f}s')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
mode = sys.argv[1]
|
||||||
|
nums = []
|
||||||
|
for a in sys.argv[2:]:
|
||||||
|
if a.startswith('--'):
|
||||||
|
break
|
||||||
|
nums.append(a)
|
||||||
|
if mode == 'still':
|
||||||
|
still([float(x) for x in nums])
|
||||||
|
elif mode == 'sheet':
|
||||||
|
sheet(float(nums[0]), float(nums[1]), int(nums[2]))
|
||||||
|
elif mode == 'video':
|
||||||
|
video(float(nums[0]), float(nums[1]), opt('--out', os.path.join(OUT, 'clip.mp4'), str),
|
||||||
|
crf=opt('--crf', 18, int), preset=opt('--preset', 'veryfast', str))
|
||||||
|
elif mode == 'final':
|
||||||
|
video(0.0, S.END, opt('--out', os.path.join(OUT, 'mobilegl_hype.mp4'), str), crf=opt('--crf', 16, int),
|
||||||
|
preset=opt('--preset', 'slow', str))
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
numba
|
||||||
|
pillow
|
||||||
|
moderngl
|
||||||
|
glcontext
|
||||||
|
uharfbuzz
|
||||||
|
freetype-py
|
||||||
|
soundfile
|
||||||
|
pyloudnorm
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""Bars 72-80: the breakdown. Device lost (not a crash), memory stays yours, zero-copy."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
|
||||||
|
def soft_words(E, f, words, b0, step, x, y, size, col, align='center', font='archivo-i', wght=300):
|
||||||
|
"""Words fade/slide in one per `step` beats, laid out on one line."""
|
||||||
|
total = sum(E.measure(w + ' ', font, size, wght) for w in words)
|
||||||
|
cx = x - total / 2 if align == 'center' else x
|
||||||
|
for i, w in enumerate(words):
|
||||||
|
bi = b0 + i * step
|
||||||
|
u = out_cubic((f.lbeat - bi) / 0.8)
|
||||||
|
adv = E.measure(w + ' ', font, size, wght)
|
||||||
|
if u > 0:
|
||||||
|
E.text(w, cx, y + 24 * (1 - u), size, a(col, u), font=font, wght=wght, align='left')
|
||||||
|
cx += adv
|
||||||
|
|
||||||
|
|
||||||
|
def breakdown(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
heart = S.pulse(f.t, 'heart', 0.15)
|
||||||
|
P = {'bloom': 0.65, 'grain': 0.032, 'vignette': 0.5}
|
||||||
|
plate(E, f, 'ink', drift=(4, 2))
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.02 * math.sin(f.lt * 0.4) + 0.015 * heart)
|
||||||
|
if lb < 16:
|
||||||
|
# server + clients orbiting slowly; client 2 fails
|
||||||
|
apps = ['browser', 'terminal', 'game', 'compositor', 'video', 'editor']
|
||||||
|
balls = [(960, 600, 120)]
|
||||||
|
caps = []
|
||||||
|
bad = None
|
||||||
|
for k in range(6):
|
||||||
|
ang = k * 2 * math.pi / 6 + f.t * 0.25
|
||||||
|
x, y = 960 + math.cos(ang) * 470, 600 + math.sin(ang) * 260
|
||||||
|
if k == 2 and lb >= 8:
|
||||||
|
bad = (x, y)
|
||||||
|
fall = clamp01((lb - 10) / 4)
|
||||||
|
x += 60 * fall
|
||||||
|
y += 500 * in_cubic(fall)
|
||||||
|
continue
|
||||||
|
balls.append((x, y, 56))
|
||||||
|
caps.append((960, 600, x, y, 10, 10))
|
||||||
|
goo(E, balls, caps, k=36, glow=0.15)
|
||||||
|
if bad is not None:
|
||||||
|
fall = clamp01((lb - 10) / 4)
|
||||||
|
jit = 6 * math.sin(f.t * 60) * (lb < 10)
|
||||||
|
bx, by = bad[0] + 60 * fall + jit, bad[1] + 500 * in_cubic(fall)
|
||||||
|
if lb < 10: # strand still attached, straining
|
||||||
|
goo(E, [(bx, by, 56)], [(960, 600, bx, by, 6, 6)], k=20, col=CORAL, rim=hexc('#9c2f22'), glow=0.2)
|
||||||
|
else:
|
||||||
|
goo(E, [(bx, by, 56)], k=20, col=a(CORAL, 1 - 0.6 * fall), rim=hexc('#9c2f22'))
|
||||||
|
if lb >= 11:
|
||||||
|
stamp(E, f, 'DEVICE LOST', bx + 40, by - 110, f.t - (lb - 11) * S.BEAT, 46, CORAL, rot=0.1)
|
||||||
|
for k in range(6):
|
||||||
|
if k == 2 and lb >= 8:
|
||||||
|
continue
|
||||||
|
ang = k * 2 * math.pi / 6 + f.t * 0.25
|
||||||
|
chip(E, 960 + math.cos(ang) * 470, 600 + math.sin(ang) * 260, apps[k], 20, INK, BONE)
|
||||||
|
chip(E, 960, 600, 'SERVER', 26, INK, PALE)
|
||||||
|
if lb < 8:
|
||||||
|
soft_words(E, f, ['and', 'when', 'things', 'go', 'wrong?'], 0.5, 1.0, 960, 170, 84, BONE)
|
||||||
|
else:
|
||||||
|
if lb >= 12:
|
||||||
|
soft_words(E, f, ['a', 'clean', 'device-lost.', 'not', 'a', 'crash.'], 12, 0.5, 960, 150, 72, BONE)
|
||||||
|
if lb >= 14:
|
||||||
|
E.text('everyone else keeps rendering', 960, 1010, 30, a(GLUE, clamp01((lb - 14) / 0.6)), font='mono',
|
||||||
|
wght=600)
|
||||||
|
elif lb < 24:
|
||||||
|
soft_words(E, f, ['your', 'memory', 'stays', 'yours.'], 16, 0.75, 960, 160, 90, BONE)
|
||||||
|
# client memory grid
|
||||||
|
E.rect(480, 620, 560, 520, INK2, r=26, stroke=a(GLUE, 0.6), sw=3)
|
||||||
|
E.text('CLIENT MEMORY', 480, 400, 26, GLUE, font='mono', wght=800)
|
||||||
|
for r in range(7):
|
||||||
|
for c in range(8):
|
||||||
|
on = rnd('mem', r, c, int(f.t * 4)) > 0.6
|
||||||
|
E.rect(270 + c * 60, 470 + r * 56, 46, 42, a(GLUE, 0.85 if on else 0.18), r=6)
|
||||||
|
E.rect(1440, 620, 460, 360, INK2, r=26, stroke=a(MINT, 0.6), sw=3)
|
||||||
|
E.text('SERVER', 1440, 560, 54, MINT, wght=900)
|
||||||
|
E.text('backend', 1440, 620, 24, a(MINT, 0.7), font='mono')
|
||||||
|
# only records travel; reaching in is walled off
|
||||||
|
for k in range(5):
|
||||||
|
ph = ((lb - 16) / 2 + k / 5) % 1
|
||||||
|
E.rect(lerp(800, 1180, ph), 560, 54, 26, GLUE, r=8)
|
||||||
|
if lb >= 18:
|
||||||
|
u = clamp01((lb - 18) / 0.6)
|
||||||
|
arrow(E, 1200, 720, lerp(1200, 860, u), 720, 8, a(CORAL, 0.9), u=1.0)
|
||||||
|
E.line(830, 640, 830, 800, 12, a(BONE, u))
|
||||||
|
E.text('the backend never reads frontend memory', 960, 1000, 30, a(BONE, 0.8 * u), font='mono', wght=600)
|
||||||
|
else:
|
||||||
|
soft_words(E, f, ['zero-copy', 'where', 'it', 'counts.'], 24, 0.75, 960, 150, 90, BONE)
|
||||||
|
# one window buffer in GPU memory, shared by the app (writes) and the compositor (reads)
|
||||||
|
k = max(0, int(lb - 25) + 1) if lb >= 25 else 0 # frames rendered so far
|
||||||
|
bx, by, bw, bh = 960, 560, 420, 280
|
||||||
|
|
||||||
|
def frame_art(cx, cy, w, h, n, alpha=1.0):
|
||||||
|
E.rect(cx, cy, w, h, a(PALE, alpha), r=12)
|
||||||
|
E.rect(cx, cy - h / 2 + 18 * h / 280, w, 36 * h / 280, a(MINT, alpha), r=10)
|
||||||
|
ang = n * 0.9
|
||||||
|
E.circle(cx + math.cos(ang) * w * 0.25, cy + 20 * h / 280 + math.sin(ang) * h * 0.18, 34 * h / 280,
|
||||||
|
a(GLUE, alpha))
|
||||||
|
E.text(f'frame {n}', cx + w * 0.3, cy + h * 0.36, 20 * h / 280, a(INK, 0.7 * alpha), font='mono', wght=700)
|
||||||
|
# the shared buffer
|
||||||
|
E.rect(bx, by, bw + 40, bh + 90, INK2, r=20, stroke=a(GLUE, 0.8), sw=4)
|
||||||
|
E.text('one window buffer · GPU memory', bx, by - bh / 2 - 22, 22, GLUE, font='mono', wght=700)
|
||||||
|
frame_art(bx, by + 10, bw, bh, max(k, 1) if lb >= 25 else 0)
|
||||||
|
# app (left) and compositor (right)
|
||||||
|
E.rect(300, 560, 320, 200, INK2, r=20, stroke=a(BONE, 0.5), sw=3)
|
||||||
|
E.text('APP', 300, 520, 50, BONE, wght=900)
|
||||||
|
E.text('draws into it', 300, 585, 22, a(BONE, 0.7), font='mono')
|
||||||
|
E.rect(1620, 560, 320, 260, INK2, r=20, stroke=a(BONE, 0.5), sw=3)
|
||||||
|
E.text('COMPOSITOR', 1620, 470, 38, BONE, wght=900)
|
||||||
|
if lb >= 25.5:
|
||||||
|
frame_art(1620, 590, 240, 160, max(k, 1), 0.95) # what it shows IS the shared buffer
|
||||||
|
# both point at the same memory
|
||||||
|
E.line(460, 560, bx - bw / 2 - 30, 560, 5, a(GLUE, 0.7))
|
||||||
|
E.line(bx + bw / 2 + 30, 560, 1460, 560, 5, a(GLUE, 0.7))
|
||||||
|
# each beat: only a small handle travels across, the pixels stay where they are
|
||||||
|
if lb >= 25:
|
||||||
|
u = clamp01(((lb - 25) % 1) / 0.6)
|
||||||
|
hx = lerp(460, 1460, in_out_cubic(u))
|
||||||
|
hy = 470 - 90 * bump(u)
|
||||||
|
if u < 1:
|
||||||
|
chip(E, hx, hy, 'handle', 22, INK, GLUE)
|
||||||
|
pulse = math.exp(-((lb - 25) % 1) * 5)
|
||||||
|
E.rect(bx, by, bw + 40 + 30 * pulse, bh + 90 + 30 * pulse, (0, 0, 0, 0), r=24, stroke=a(NEON, 0.6 * pulse),
|
||||||
|
sw=4)
|
||||||
|
# counters
|
||||||
|
E.text(f'frames shown: {k}', 760, 900, 34, BONE, font='mono', wght=800)
|
||||||
|
E.text('copies made: 0', 1180, 900, 34, NEON, font='mono', wght=800)
|
||||||
|
if lb >= 27:
|
||||||
|
E.text('the app and the compositor share one buffer: handed over, never copied', 960, 1000, 28,
|
||||||
|
a(GLUE, clamp01((lb - 27) / 0.6)), font='mono', wght=600)
|
||||||
|
return P
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Bars 80-88: rapid-fire checklist over the last build."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
ITEMS = ['OpenGL 4.6 Core', 'GLSL → SPIR-V', 'Vulkan backend', 'OpenGL ES backend',
|
||||||
|
'Android', 'Linux', 'Windows', 'macOS',
|
||||||
|
'EGL · GLX', 'WGL', 'CGL · NSOpenGL', 'glvnd vendor',
|
||||||
|
'same process', 'another process', 'another machine', 'open source']
|
||||||
|
|
||||||
|
|
||||||
|
def checklist(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.55, 'grain': 0.035, 'ca': 0.3 * f.roll}
|
||||||
|
if lb >= 31: # the silent beat: the block falls into the glue, the last drop lands on the downbeat
|
||||||
|
pre_drop(E, f, 960, 247, 181.5, f.t - (lb - 32) * S.BEAT)
|
||||||
|
return P
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
c = climax(f, 24, 31)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.35 * c + 0.01 * f.roll, rot=0.04 * c, shake=2 + 14 * c)
|
||||||
|
n = min(16, max(0, int(lb) + 1))
|
||||||
|
sweep = int(lb * 2) % 16 if lb >= 16 else -1
|
||||||
|
if lb >= 24:
|
||||||
|
speedlines(E, f, 0.3 + 0.7 * c, a(GLUE, 0.7), n=100, seed=31)
|
||||||
|
for k in range(16):
|
||||||
|
r, cc = k // 4, k % 4
|
||||||
|
x = 270 + cc * 460
|
||||||
|
y = 260 + r * 190
|
||||||
|
if k >= n:
|
||||||
|
E.rect(x, y, 420, 150, a(INK2, 0.6), r=20)
|
||||||
|
continue
|
||||||
|
# from beat 24 the cards fly out at the camera, one per 8th, accelerating
|
||||||
|
fly = 0.0
|
||||||
|
tf = 24 + 0.44 * k * (1 - 0.35 * k / 16)
|
||||||
|
if lb >= tf:
|
||||||
|
fly = in_cubic(clamp01((lb - tf) / 0.6))
|
||||||
|
if fly >= 1:
|
||||||
|
continue
|
||||||
|
sc_ = 1 + 3.5 * fly
|
||||||
|
x = 960 + (x - 960) * (1 + 2.8 * fly)
|
||||||
|
y = 540 + (y - 540) * (1 + 2.8 * fly)
|
||||||
|
u = out_back((lb - k) / 0.35)
|
||||||
|
hot = k == sweep or (lb < 16 and int(lb) == k) or fly > 0
|
||||||
|
col = scale_rgb(GLUE, 1.5) if hot else INK2
|
||||||
|
E.alpha = 1 - fly
|
||||||
|
E.rect(x, y, 420 * u * sc_, 150 * u * sc_, col, r=20 * sc_, stroke=a(GLUE, 0.6), sw=3 * sc_)
|
||||||
|
E.text(ITEMS[k], x - 180 * u * sc_, y, 30 * u * sc_, INK if hot else BONE, wght=800, align='left', wdth=90)
|
||||||
|
check(E, x + 165 * u * sc_, y, 30 * u * sc_, INK if hot else GLUE, 9 * sc_, (lb - k - 0.1) / 0.3)
|
||||||
|
E.flush()
|
||||||
|
E.alpha = 1.0
|
||||||
|
E.cam, E.shake = (LW / 2, LH / 2, 1.0, 0.0), (0.0, 0.0)
|
||||||
|
if lb < 24:
|
||||||
|
E.rect(960, 1000, 380, 80, INK, r=40, stroke=a(GLUE, 0.6), sw=3)
|
||||||
|
E.text(f'{n:2d} / 16', 960, 1000, 44, NEON, font='mono', wght=800)
|
||||||
|
P['ca'] = 0.3 * f.roll + 1.5 * c
|
||||||
|
P['bloom'] = 0.55 + 0.4 * c
|
||||||
|
return P
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""Bars 16-32: the first drop. Logo hit, the hook, four platforms, the hook again."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
|
||||||
|
def logo(E, f, lb0, cx=960, cy=560, splat=True, scale=1.0, cube_y=None):
|
||||||
|
"""The MobileGlues mark (block dropping into glue, hitting at local beat lb0) + MobileGL wordmark."""
|
||||||
|
u = f.lbeat - lb0
|
||||||
|
t_hit = f.t - u * S.BEAT
|
||||||
|
s = scale
|
||||||
|
cy_cube = cube_y if cube_y is not None else cy - 230 * s
|
||||||
|
if splat and u >= 0: # shockwave rings off the impact
|
||||||
|
ts = u * S.BEAT
|
||||||
|
for k in range(3):
|
||||||
|
w = out_expo(clamp01((ts - 0.06 * k) / 0.9))
|
||||||
|
if 0 < w < 1:
|
||||||
|
E.ring(cx, cy_cube + 60 * s, (120 + 1100 * w) * s, 14 * (1 - w) + 1, a(GLUE, 0.7 * (1 - w)))
|
||||||
|
cubes(E, f, [(cx, cy_cube, 165 * s, 0.0, 0.12 * f.kick, 0.4 * f.kick, 0.0, 1.0, u * S.BEAT)])
|
||||||
|
k = 1 + 0.03 * f.kick
|
||||||
|
w1 = E.measure('Mobile', 'archivo', 220 * s, 900, 100)
|
||||||
|
w2 = E.measure('GL', 'archivo', 220 * s, 900, 100)
|
||||||
|
x0 = cx - (w1 + w2) / 2
|
||||||
|
E.text('Mobile', x0, cy + 150 * s, 220 * s * k, BONE, wght=900, align='left')
|
||||||
|
E.text('GL', x0 + w1, cy + 150 * s, 220 * s * k, scale_rgb(GLUE, 1.6), wght=900, align='left')
|
||||||
|
return t_hit
|
||||||
|
|
||||||
|
|
||||||
|
def hook_word(E, f, word, kind, t_hit, color=None, wdth=100):
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
size = fit_size(E, word, 1720, 900, wdth, cap=560)
|
||||||
|
slam(E, f, word, 960, 560, t_hit, size, color or fg, wdth=wdth, punch=0.12)
|
||||||
|
|
||||||
|
|
||||||
|
PLATFORMS = [
|
||||||
|
('ANDROID', 'ink', ['Minecraft: Java Edition', 'via Fold Craft Launcher', 'Vulkan or OpenGL ES underneath']),
|
||||||
|
('LINUX', 'mint', ['EGL · GLX · Wayland', 'a glvnd vendor', 'a whole KDE Plasma desktop']),
|
||||||
|
('WINDOWS', 'ink', ['WGL', 'drop-in opengl32.dll', 'Vulkan, or GLES via ANGLE']),
|
||||||
|
('MACOS', 'glue', ['CGL · NSOpenGL', 'Vulkan via MoltenVK', 'OpenGL 4.6 on a Mac']),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def draw_device(E, f, name, kind, u, cx, cy, sc, bounce=0.0):
|
||||||
|
"""The platform's hero device anchored at (cx, cy) with iso scale sc; u = beats since its cut."""
|
||||||
|
faces = FACE_LIGHT if kind != 'ink' else FACE_DARK
|
||||||
|
line = OUTL if kind == 'ink' else hexc('#2f7d2a')
|
||||||
|
k = sc / 82
|
||||||
|
if name == 'ANDROID':
|
||||||
|
scr = screen_layer(E, 'scr_phone', 265, 550, paint_voxels, f)
|
||||||
|
iso = Iso(cx, cy - bounce, sc)
|
||||||
|
phone(E, iso, f, faces=faces, screen=scr, line=line)
|
||||||
|
drip = 120 * out_cubic((u - 1.5) / 2)
|
||||||
|
if drip > 2:
|
||||||
|
px, py = iso.p(0, 0.2, 6.2)
|
||||||
|
goo(E, [(px + 60 * k, py + 10, 26 * k), (px + 60 * k, py + 10 + drip * k, 18 * k)],
|
||||||
|
[(px + 60 * k, py + 10, px + 60 * k, py + 10 + drip * k, 16 * k, 12 * k)], k=20, outline=4, bevel=16)
|
||||||
|
elif name == 'LINUX':
|
||||||
|
scr = screen_layer(E, 'scr_desk', 760, 440, paint_desktop, f)
|
||||||
|
monitor(E, Iso(cx, cy - bounce, sc), f, faces=faces, screen=scr, line=line)
|
||||||
|
elif name == 'WINDOWS':
|
||||||
|
iso = Iso(cx, cy, sc)
|
||||||
|
iso.box(E, -3.5, -2.5, 0, 7, 5, 2.4, faces[0], faces[1], faces[2], line, 5) # folder body
|
||||||
|
iso.box(E, -3.5, -2.5, 2.4, 3.0, 5, 0.4, faces[0], faces[1], faces[2], line, 5) # tab
|
||||||
|
x, y0 = iso.p(0, 0, 2.4)
|
||||||
|
E.text('C:\\Games\\YourApp\\', x, y0 + 210 * k, 30 * k, a(BONE if kind == 'ink' else INK, 0.85),
|
||||||
|
font='mono', wght=600)
|
||||||
|
fall = clamp01((u - 0.4) / 0.6)
|
||||||
|
land = u >= 1.0
|
||||||
|
if not land:
|
||||||
|
y = lerp(-200, y0 - 120 * k, in_cubic(fall))
|
||||||
|
else:
|
||||||
|
y = y0 - 120 * k + 26 * k * math.exp(-(u - 1.0) * 6) * math.sin((u - 1.0) * 20)
|
||||||
|
if u >= 0.4:
|
||||||
|
file_card(E, x, y - bounce * 0.5, 230, 280, 'opengl32', '.dll', rot=-0.08 if not land else 0.0, s=k)
|
||||||
|
if land:
|
||||||
|
ring = out_expo((u - 1.0) / 0.8)
|
||||||
|
E.ring(x, y0 - 20, 60 + 300 * ring * k, 6 * (1 - ring) + 0.5, a(GLUE, 1 - ring))
|
||||||
|
else: # MACOS
|
||||||
|
scr = screen_layer(E, 'scr_mac', 600, 380, paint_mac, f)
|
||||||
|
laptop(E, Iso(cx, cy - bounce, sc), f, y=2.0, faces=FACE_DARK, screen=scr, line=hexc('#0d2213'),
|
||||||
|
open_=out_back(clamp01(u / 0.5)))
|
||||||
|
|
||||||
|
|
||||||
|
DEV_POS = {'ANDROID': (1380, 1010, 92), 'LINUX': (1330, 1000, 78), 'WINDOWS': (1360, 880, 82), 'MACOS': (1330, 930, 86)}
|
||||||
|
DEV_CLOSE = {'ANDROID': (1450, 1180, 120), 'LINUX': (1400, 1200, 104), 'WINDOWS': (1450, 1060, 110),
|
||||||
|
'MACOS': (1400, 1120, 116)}
|
||||||
|
|
||||||
|
|
||||||
|
def platform(E, f, i, lb0):
|
||||||
|
"""4-beat platform card: the name slams on the cut, its three facts pop on the next 8ths."""
|
||||||
|
name, kind, chips_ = PLATFORMS[i]
|
||||||
|
u = f.lbeat - lb0
|
||||||
|
t0 = f.t - u * S.BEAT
|
||||||
|
b, fg = plate(E, f, kind, drift=(-20, 0))
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.02 * u + 0.012 * f.kick)
|
||||||
|
E.text(f'0{i + 1} / 04', 130, 170, 26, a(fg, 0.7), font='mono', wght=700, align='left')
|
||||||
|
slam(E, f, name, 120, 290, t0, 190, fg, align='left', wdth=110 if i != 2 else 95)
|
||||||
|
for k, c in enumerate(chips_):
|
||||||
|
bk = 0.5 + 0.5 * k
|
||||||
|
if u >= bk:
|
||||||
|
uu = out_back((u - bk) / 0.3)
|
||||||
|
chip(E, 130, 430 + 82 * k, c, 30, b if kind != 'ink' else INK, fg if kind != 'ink' else GLUE,
|
||||||
|
align='left', sc=uu, font='mono', wght=700)
|
||||||
|
draw_device(E, f, name, kind, u, *DEV_POS[name], bounce=18 * f.kick)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def platform_close(E, f, i, lb0):
|
||||||
|
"""2-beat recap: a huge name, the device pushed in close."""
|
||||||
|
name, kind, _ = PLATFORMS[i]
|
||||||
|
u = f.lbeat - lb0
|
||||||
|
b, fg = plate(E, f, kind, grid=True, drift=(-80, 0))
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.12 * out_cubic(u / 2), rot=(0.02 if i % 2 else -0.02) * u,
|
||||||
|
shake=10 * math.exp(-u * 5))
|
||||||
|
draw_device(E, f, name, kind, 2.0 + u, *DEV_CLOSE[name], bounce=24 * f.kick)
|
||||||
|
slam(E, f, name, 960, 300, f.t - u * S.BEAT, fit_size(E, name, 1650, cap=330), fg, punch=0.15)
|
||||||
|
|
||||||
|
|
||||||
|
API_WORDS = [('EGL', 'linux · android'), ('GLX', 'x11 apps'), ('WAYLAND', 'linux desktops'),
|
||||||
|
('GLVND', 'a system-wide vendor'), ('WGL', 'windows'), ('CGL', 'macos'), ('NSOPENGL', 'macos'),
|
||||||
|
('FCL', 'minecraft on android')]
|
||||||
|
TILES = ['ANDROID', 'LINUX', 'WINDOWS', 'MACOS', 'EGL', 'GLX', 'WAYLAND', 'GLVND', 'WGL', 'CGL', 'NSOPENGL', 'VULKAN',
|
||||||
|
'OPENGL ES', '4.6 CORE', 'SPIR-V', 'LGPL-3.0']
|
||||||
|
|
||||||
|
|
||||||
|
def drop1(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.55, 'grain': 0.032, 'ca': 0.5 * f.kick}
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT
|
||||||
|
if lb < 4: # LOGO HIT (4 beats)
|
||||||
|
plate(E, f, 'ink', grid=False)
|
||||||
|
rot = f.lt * 0.25
|
||||||
|
for k in range(18): # rays
|
||||||
|
ang = rot + k * math.pi / 9
|
||||||
|
E.poly([(960, 400), (960 + 2200 * math.cos(ang - 0.06), 400 + 2200 * math.sin(ang - 0.06)),
|
||||||
|
(960 + 2200 * math.cos(ang + 0.06), 400 + 2200 * math.sin(ang + 0.06))], a(MOSS, 0.35))
|
||||||
|
dot_grid(E, f, a(MOSS, 0.5), 60, 2.2, pulse=0.6)
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.012 * f.kick + 0.03 * lb / 4, shake=26 * math.exp(-lb * 2.2))
|
||||||
|
logo(E, f, 0, 960, 560)
|
||||||
|
E.text('desktop OpenGL 4.6 Core · on Vulkan & OpenGL ES · anywhere', 960, 880, 30, a(BONE, 0.75),
|
||||||
|
font='mono', wght=500)
|
||||||
|
P['flash'] = (*GLUE[:3], 0.9 * math.exp(-lb * S.BEAT / 0.12))
|
||||||
|
P['bloom'] = 0.7
|
||||||
|
elif lb < 8: # hook, one word per beat
|
||||||
|
words = [('DESKTOP', 'glue', None), ('OPENGL', 'ink', None), ('ON', 'mint', None), ('ANYTHING', 'ink', NEON)]
|
||||||
|
k = int(lb - 4)
|
||||||
|
wd, kind, col = words[k]
|
||||||
|
hook_word(E, f, wd, kind, tb_(4 + k), col, wdth=125 if k == 2 else 100)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.03 * f.kick + 0.02 * (lb % 1))
|
||||||
|
elif lb < 24: # platforms, 4 beats each
|
||||||
|
i = int((lb - 8) // 4)
|
||||||
|
platform(E, f, i, 8 + 4 * i)
|
||||||
|
E.cam, E.shake = (LW / 2, LH / 2, 1.0, 0.0), (0.0, 0.0)
|
||||||
|
hud(E, f, 'PLATFORMS', PLATFORMS[i][0], col=a(BONE, 0.5) if i in (0, 2) else a(INK, 0.5))
|
||||||
|
elif lb < 32: # recap, 2 beats each, pushed in
|
||||||
|
i = int((lb - 24) // 2)
|
||||||
|
platform_close(E, f, i, 24 + 2 * i)
|
||||||
|
elif lb < 40: # window-system APIs, one per beat
|
||||||
|
kind, k = beat_plate(f, ['ink', 'glue', 'ink', 'mint'], 32)
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
word, cap = API_WORDS[k % 8]
|
||||||
|
screen_cam(E, f, zoom=1 + 0.04 * (lb % 1) + 0.02 * f.kick, rot=(0.015 if k % 2 else -0.015))
|
||||||
|
speedlines(E, f, 0.25, a(fg, 0.5), n=40, seed=k)
|
||||||
|
slam(E, f, word, 960, 500, tb_(32 + k), fit_size(E, word, 1500, cap=420), fg if kind != 'ink' else NEON,
|
||||||
|
punch=0.2)
|
||||||
|
E.text(cap, 960, 800, 40, a(fg, 0.8), font='mono', wght=700)
|
||||||
|
elif lb < 48: # hook in 8ths: the plate changes once a beat, the word twice
|
||||||
|
kind, k = beat_plate(f, ['glue', 'ink', 'mint', 'ink'], 40)
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
j = int((lb - 40) * 2)
|
||||||
|
plat = ['ANDROID', 'LINUX', 'WINDOWS', 'MACOS'][(j // 4) % 4]
|
||||||
|
word = ['DESKTOP', 'OPENGL', 'ON', plat][j % 4]
|
||||||
|
screen_cam(E, f, zoom=1 + 0.05 * ((lb * 2) % 1) + 0.03 * f.kick, shake=4 + 4 * climax(f, 40, 48))
|
||||||
|
speedlines(E, f, 0.35 + 0.3 * climax(f, 40, 48), a(fg, 0.45), seed=j)
|
||||||
|
slam(E, f, word, 960, 540, tb_(40 + j * 0.5), fit_size(E, word, 1700, cap=520),
|
||||||
|
fg if kind != 'ink' else (NEON if word == plat else BONE), punch=0.25)
|
||||||
|
P['ca'] = 0.6 + 0.6 * f.kick
|
||||||
|
else: # 4x4 grid fills on 8ths, then the whole grid flashes and shakes up to the climax
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
n = min(16, int((lb - 48) * 2) + 1)
|
||||||
|
c = climax(f, 56, 63)
|
||||||
|
z = 1.25 - 0.25 * out_cubic((lb - 48) / 4) + 0.08 * in_cubic(clamp01((lb - 52) / 4)) + 0.25 * c
|
||||||
|
screen_cam(E, f, zoom=z + (0.02 + 0.05 * c) * f.kick, rot=0.03 * c * math.sin(f.lt * 9), shake=3 + 16 * c)
|
||||||
|
if lb >= 56:
|
||||||
|
speedlines(E, f, 0.3 + 0.7 * c, a(GLUE, 0.6), n=90, seed=5)
|
||||||
|
# a highlight that races round the grid: one tile per 8th, then per 16th
|
||||||
|
if lb >= 56:
|
||||||
|
rate = 2 if lb < 60 else 4
|
||||||
|
hot = int((lb - 56) * rate) % 16
|
||||||
|
else:
|
||||||
|
hot = -1
|
||||||
|
order = [0, 1, 2, 3, 7, 11, 15, 14, 13, 12, 8, 4, 5, 6, 10, 9]
|
||||||
|
all_on = lb >= 62.0
|
||||||
|
for k in range(16):
|
||||||
|
x = 270 + (k % 4) * 460
|
||||||
|
y = 200 + (k // 4) * 225
|
||||||
|
if k >= n:
|
||||||
|
E.rect(x, y, 420, 190, a(INK2, 0.7), r=22)
|
||||||
|
continue
|
||||||
|
uu = out_back((lb - 48 - k * 0.5) / 0.3)
|
||||||
|
jit = (8 + 20 * c) * f.kick
|
||||||
|
x += (rnd('jx', k, int(f.t * 30)) - 0.5) * jit
|
||||||
|
y += (rnd('jy', k, int(f.t * 30)) - 0.5) * jit
|
||||||
|
lit = all_on or (hot >= 0 and order[hot] == k)
|
||||||
|
col = scale_rgb(GLUE, 1.9) if lit else [GLUE, MINT, PALE, GLUE][(k + k // 4) % 4]
|
||||||
|
sc_ = 1.0 + (0.12 if lit else 0.0) + 0.04 * f.kick
|
||||||
|
E.rect(x, y, 420 * uu * sc_, 190 * uu * sc_, col, r=22)
|
||||||
|
E.text(TILES[k], x - 10, y, fit_size(E, TILES[k], 330, cap=70) * uu * sc_, INK, wght=900)
|
||||||
|
check(E, x + 170 * uu, y - 60 * uu, 22 * uu, INK, 7, (lb - 48 - k * 0.5 - 0.1) / 0.25)
|
||||||
|
P['ca'] = 0.4 + 1.8 * c
|
||||||
|
P['bloom'] = 0.55 + 0.35 * c
|
||||||
|
return P
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""Bars 88-112: the last drop (key lift) and the outro."""
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
from sc_drop1 import logo, hook_word
|
||||||
|
|
||||||
|
# what the frames were drawn with; render.py fills this in from the GL renderer string
|
||||||
|
RENDERED_WITH = {'line': None}
|
||||||
|
|
||||||
|
|
||||||
|
def cube_rain(E, f, lb0, n=10, floor=1010, step=0.5):
|
||||||
|
"""A row of little glue blobs; a block drops into each in turn."""
|
||||||
|
items = []
|
||||||
|
for k in range(n):
|
||||||
|
land = lb0 + k * step
|
||||||
|
x = 140 + (k * 173) % 1640
|
||||||
|
sz = 50 + 20 * rnd('cs', k)
|
||||||
|
u = (f.lbeat - land) * S.BEAT
|
||||||
|
items.append((x, floor - sz, sz, 0.0, 0.1 * f.kick, 0.4, rnd('tint', k) * 0.5, 1.0, u))
|
||||||
|
cubes(E, f, items)
|
||||||
|
|
||||||
|
|
||||||
|
MONTAGE = [ # local beat, word, plate, painter
|
||||||
|
(8, 'ANDROID', 'ink'), (12, 'LINUX', 'mint'), (16, 'WINDOWS', 'ink'), (20, 'MACOS', 'glue'),
|
||||||
|
(24, 'VULKAN', 'ink'), (28, 'OPENGL ES', 'mint'), (32, 'ANY MACHINE', 'ink'), (36, 'OPEN SOURCE', 'glue'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def montage(E, f, i, lb0):
|
||||||
|
word, kind = MONTAGE[i][1], MONTAGE[i][2]
|
||||||
|
u = f.lbeat - lb0
|
||||||
|
b, fg = plate(E, f, kind, grid=True, drift=(40, 0))
|
||||||
|
size = fit_size(E, word, 1700, cap=330)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.02 * f.kick + 0.01 * u, shake=12 * math.exp(-u * 4))
|
||||||
|
faces = FACE_DARK if kind == 'ink' else FACE_LIGHT
|
||||||
|
line = OUTL if kind == 'ink' else hexc('#2f7d2a')
|
||||||
|
if word == 'ANDROID':
|
||||||
|
iso = Iso(1450, 1000, 62)
|
||||||
|
phone(E, iso, f, faces=faces, screen=screen_layer(E, 'f_phone', 265, 550, paint_voxels, f), line=line)
|
||||||
|
elif word == 'LINUX':
|
||||||
|
iso = Iso(1380, 1010, 56)
|
||||||
|
monitor(E, iso, f, faces=faces, screen=screen_layer(E, 'f_desk', 760, 440, paint_desktop, f), line=line)
|
||||||
|
elif word == 'WINDOWS':
|
||||||
|
y = lerp(-200, 820, out_back(clamp01(u / 0.6), 1.2))
|
||||||
|
file_card(E, 1520, y, 230, 280, 'opengl32', '.dll', rot=0.05)
|
||||||
|
elif word == 'MACOS':
|
||||||
|
iso = Iso(1420, 960, 58)
|
||||||
|
laptop(E, iso, f, y=2.0, faces=FACE_DARK, screen=screen_layer(E, 'f_mac', 600, 380, paint_mac, f),
|
||||||
|
line=hexc('#0d2213'))
|
||||||
|
elif word == 'VULKAN':
|
||||||
|
cubes(E, f, [(1500, 830, 150, f.lt * 1.2, 0.2 * f.kick, 0.4 * f.kick, 0.0, 1.0)],
|
||||||
|
deep=hexc('#0a2a10'))
|
||||||
|
E.text('Magma', 1500, 1030, 34, GLUE, font='mono', wght=800)
|
||||||
|
elif word == 'OPENGL ES':
|
||||||
|
cubes(E, f, [(1500, 830, 150, -f.lt * 1.2, 0.2 * f.kick, 0.4 * f.kick, 0.0, 1.0)],
|
||||||
|
deep=hexc('#2e6e2f'))
|
||||||
|
E.text('Espryt', 1500, 1030, 34, MOSS, font='mono', wght=800)
|
||||||
|
elif word == 'ANY MACHINE':
|
||||||
|
pts = [(260 + k * 120, 820 + 40 * math.sin(k * 0.8 + f.lt * 3)) for k in range(13)]
|
||||||
|
caps = [(p[0], p[1], q[0], q[1], 14, 14) for p, q in zip(pts, pts[1:])]
|
||||||
|
goo(E, [(pts[0][0], pts[0][1], 50), (pts[-1][0], pts[-1][1], 50)], caps, k=30, bevel=18)
|
||||||
|
for k in range(6):
|
||||||
|
ph = (u * 2 + k / 6) % 1
|
||||||
|
x = lerp(pts[0][0], pts[-1][0], ph)
|
||||||
|
E.rect(x, 820 + 40 * math.sin((x - 260) / 120 * 0.8 + f.lt * 3), 30, 22, scale_rgb(PALE, 1.5), r=5)
|
||||||
|
else: # OPEN SOURCE
|
||||||
|
chip(E, 960, 820, 'LGPL-3.0', 54, GLUE, INK, font='archivo', wght=900, sc=out_back(u / 0.4))
|
||||||
|
chip(E, 960, 940, 'github.com/MobileGL-Dev/MobileGL', 32, INK, BONE, sc=out_back((u - 0.5) / 0.4))
|
||||||
|
slam(E, f, word, 960, 330, f.t - u * S.BEAT, size, fg if kind != 'ink' else BONE, punch=0.2)
|
||||||
|
|
||||||
|
|
||||||
|
QUICK = [('ANDROID', 'ink'), ('LINUX', 'glue'), ('WINDOWS', 'ink'), ('MACOS', 'mint'), ('VULKAN', 'ink'),
|
||||||
|
('OPENGL ES', 'glue'), ('LOCAL', 'ink'), ('REMOTE', 'mint')]
|
||||||
|
FEATS = ['4.6 CORE', 'SPIR-V', 'VULKAN', 'GLES', 'ANDROID', 'LINUX', 'WINDOWS', 'MACOS', 'EGL', 'GLX', 'WGL', 'CGL',
|
||||||
|
'INPROC', 'SPAWN', 'TCP', 'OPEN SOURCE']
|
||||||
|
|
||||||
|
|
||||||
|
def final_drop(E, f):
|
||||||
|
"""Last drop, accelerating: logo, 2-beat montage, 1-beat words, 8th-note features, multiplying hook, collapse."""
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.6, 'grain': 0.032, 'ca': 0.5 * f.kick}
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT
|
||||||
|
if lb < 4: # biggest logo hit + jelly rain
|
||||||
|
plate(E, f, 'ink', grid=False)
|
||||||
|
rot = -f.lt * 0.3
|
||||||
|
for k in range(18):
|
||||||
|
ang = rot + k * math.pi / 9
|
||||||
|
E.poly([(960, 330), (960 + 2200 * math.cos(ang - 0.07), 330 + 2200 * math.sin(ang - 0.07)),
|
||||||
|
(960 + 2200 * math.cos(ang + 0.07), 330 + 2200 * math.sin(ang + 0.07))], a(MOSS, 0.45))
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.015 * f.kick + 0.04 * lb / 4, shake=30 * math.exp(-lb * 2.0))
|
||||||
|
cube_rain(E, f, 0.25, 10, step=0.3)
|
||||||
|
logo(E, f, 0, 960, 500, scale=1.1)
|
||||||
|
P['flash'] = (*GLUE[:3], 0.95 * math.exp(-lb * S.BEAT / 0.12))
|
||||||
|
P['bloom'] = 0.75
|
||||||
|
elif lb < 20: # montage, 2 beats each
|
||||||
|
i = int((lb - 4) // 2)
|
||||||
|
montage(E, f, i, 4 + 2 * i)
|
||||||
|
elif lb < 28: # one word a beat
|
||||||
|
k = int(lb - 20)
|
||||||
|
word, kind = QUICK[k]
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.06 * (lb % 1) + 0.02 * f.kick, rot=(0.02 if k % 2 else -0.02))
|
||||||
|
speedlines(E, f, 0.35, a(fg, 0.45), seed=k + 80)
|
||||||
|
slam(E, f, word, 960, 540, tb_(20 + k), fit_size(E, word, 1650, cap=460), fg if kind != 'ink' else NEON,
|
||||||
|
punch=0.25)
|
||||||
|
elif lb < 36: # sixteen features on 8ths; the plate still changes only once a beat
|
||||||
|
kind, k = beat_plate(f, ['glue', 'ink', 'mint', 'ink'], 28)
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
j = int((lb - 28) * 2)
|
||||||
|
w = FEATS[j % 16]
|
||||||
|
c = climax(f, 28, 36)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.06 * ((lb * 2) % 1) + 0.03 * f.kick, shake=3 + 6 * c)
|
||||||
|
speedlines(E, f, 0.4 + 0.3 * c, a(fg, 0.45), seed=j + 90)
|
||||||
|
slam(E, f, w, 960, 500, tb_(28 + j * 0.5), fit_size(E, w, 1550, cap=440), fg if kind != 'ink' else NEON,
|
||||||
|
punch=0.25)
|
||||||
|
check(E, 960, 820, 60, fg if kind != 'ink' else GLUE, 16, ((lb * 2) % 1) / 0.4)
|
||||||
|
E.text(f'{j + 1:2d} / 16', 1750, 1010, 30, a(fg, 0.8), font='mono', wght=800, align='right')
|
||||||
|
elif lb < 52: # hub: MobileGL in the middle, glue strands shoot out to everything it runs on
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
SAT = ['ANDROID', 'LINUX', 'WINDOWS', 'MACOS', 'VULKAN', 'OPENGL ES', 'EGL · GLX · WGL · CGL', 'TCP']
|
||||||
|
c = climax(f, 44, 52)
|
||||||
|
spin = 0.15 * (lb - 36) + 2.2 * c * c
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.03 * f.kick + 0.12 * c, rot=0.03 * c * math.sin(f.lt * 7), shake=2 + 10 * c)
|
||||||
|
if lb >= 44:
|
||||||
|
speedlines(E, f, 0.3 + 0.6 * c, a(GLUE, 0.5), n=80, seed=17)
|
||||||
|
balls = [(960, 540, 130 + 14 * f.kick)]
|
||||||
|
caps, pos = [], []
|
||||||
|
for k, name in enumerate(SAT):
|
||||||
|
bk = 36 + k
|
||||||
|
if lb < bk:
|
||||||
|
continue
|
||||||
|
ang = k * 2 * math.pi / 8 + spin - math.pi / 2
|
||||||
|
shoot = out_expo(clamp01((lb - bk) / 0.3))
|
||||||
|
pull = in_cubic(clamp01((lb - 50) / 2)) # reeled back in at the end
|
||||||
|
R = (1 - pull) * shoot
|
||||||
|
x, y = 960 + math.cos(ang) * 640 * R, 540 + math.sin(ang) * 360 * R
|
||||||
|
caps.append((960, 540, x, y, 14, 10))
|
||||||
|
balls.append((x, y, 26))
|
||||||
|
pos.append((x, y, name, shoot * (1 - pull), k))
|
||||||
|
goo(E, balls, caps, k=40, glow=0.2 + 0.3 * c)
|
||||||
|
# packets racing out along the strands: 8ths, then 16ths
|
||||||
|
rate = 2 if lb < 48 else 4
|
||||||
|
for x, y, name, sh, k in pos:
|
||||||
|
if sh < 0.95:
|
||||||
|
continue
|
||||||
|
ph = (lb * rate + k * 0.37) % 1
|
||||||
|
E.rect(lerp(960, x, ph), lerp(540, y, ph), 22, 16, scale_rgb(PALE, 1.6), r=4)
|
||||||
|
for x, y, name, sh, k in pos:
|
||||||
|
if sh <= 0.05:
|
||||||
|
continue
|
||||||
|
fresh = math.exp(-max(0.0, lb - (36 + k)) * 4)
|
||||||
|
chip(E, x, y, name, 40, INK, scale_rgb(GLUE, 1.0 + 0.8 * fresh), font='archivo', wght=900, sc=sh * (1 + 0.3 * fresh))
|
||||||
|
E.text('MobileGL', 960, 540, 46 * (1 + 0.05 * f.kick), INK, wght=900)
|
||||||
|
P['ca'] = 0.4 * f.kick + 1.2 * c
|
||||||
|
P['bloom'] = 0.6 + 0.3 * c
|
||||||
|
else: # collapse into one glue drop, faster and faster
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
u = in_cubic(clamp01((lb - 52) / 11.0))
|
||||||
|
c = climax(f, 52, 63)
|
||||||
|
speedlines(E, f, 0.3 + 0.7 * c, a(GLUE, 0.8), n=110, seed=13)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.5 * c + 0.03 * f.kick, rot=0.05 * c, shake=3 + 14 * c)
|
||||||
|
balls = []
|
||||||
|
for k in range(32):
|
||||||
|
ang = k * 2 * math.pi / 32 + f.lt * (1.5 + 6 * c) + 3.0 * (lb - 52) / 11
|
||||||
|
R = 900 * (1 - u) * (0.6 + 0.4 * rnd('cv', k))
|
||||||
|
balls.append((960 + math.cos(ang) * R, 540 + math.sin(ang) * R * 0.6, 40 + 30 * u))
|
||||||
|
balls.append((960, 540, 60 + 180 * u))
|
||||||
|
goo(E, balls, k=60 + 60 * u, glow=0.2 + 0.6 * u)
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0 + 0.04 * f.kick, 0.0)
|
||||||
|
E.text('DESKTOP OPENGL.', 960, 160, 110, a(BONE, 1 - 0.3 * u), wght=900, wdth=100 + 25 * c)
|
||||||
|
E.text('ANYWHERE.', 960, 930, 130, a(NEON, 1 - 0.3 * u), wght=900, wdth=100 + 25 * c)
|
||||||
|
P['bloom'] = 0.6 + 0.5 * u
|
||||||
|
P['ca'] = 0.5 + 1.8 * c
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def outro(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.55, 'grain': 0.035}
|
||||||
|
plate(E, f, 'ink', drift=(3, 0))
|
||||||
|
fade_out = clamp01((lb - 26) / 5.0)
|
||||||
|
if lb < 16:
|
||||||
|
if lb < 0.3:
|
||||||
|
P['flash'] = (*GLUE[:3], 0.7 * (1 - lb / 0.3))
|
||||||
|
logo(E, f, 0, 960, 430, splat=True, scale=0.9)
|
||||||
|
chips_ = [('OpenGL 4.6 Core', GLUE), ('Vulkan', MINT), ('OpenGL ES', MINT), ('LGPL-3.0', PALE)]
|
||||||
|
total = sum(E.measure(s_, 'mono', 30, 700) + 36 for s_, _ in chips_) + 24 * (len(chips_) - 1)
|
||||||
|
x = 960 - total / 2
|
||||||
|
for k, (s, c) in enumerate(chips_):
|
||||||
|
if lb >= 2 + k * 0.5:
|
||||||
|
w = chip(E, x, 760, s, 30, INK, c, align='left', sc=out_back((lb - 2 - k * 0.5) / 0.35))
|
||||||
|
x += w + 24
|
||||||
|
if lb >= 5:
|
||||||
|
u = clamp01((lb - 5) / 0.5)
|
||||||
|
E.text('github.com/MobileGL-Dev/MobileGL', 960, 880, 44, a(BONE, u), font='mono', wght=700)
|
||||||
|
else:
|
||||||
|
u = clamp01((lb - 16) / 0.4)
|
||||||
|
if lb < 16.3:
|
||||||
|
P['flash'] = (*BONE[:3], 0.5 * (1 - (lb - 16) / 0.3))
|
||||||
|
cubes(E, f, [(960, 360, 120, f.lt * 0.4, 0.0, 0.0, 0.0, 1.0 - fade_out)])
|
||||||
|
line = RENDERED_WITH['line']
|
||||||
|
if line:
|
||||||
|
E.text('every frame you just saw', 960, 600, 70, a(BONE, u * (1 - fade_out)), wght=800)
|
||||||
|
E.text('was rendered through MobileGL.', 960, 690, 70, a(NEON, u * (1 - fade_out)), wght=900)
|
||||||
|
E.text(line, 960, 800, 28, a(BONE, 0.65 * u * (1 - fade_out)), font='mono', wght=600)
|
||||||
|
else:
|
||||||
|
E.text('desktop OpenGL. anywhere.', 960, 640, 80, a(BONE, u * (1 - fade_out)), wght=900)
|
||||||
|
E.text('github.com/MobileGL-Dev/MobileGL', 960, 920, 34, a(GLUE, u * (1 - fade_out)), font='mono', wght=700)
|
||||||
|
if lb >= 28:
|
||||||
|
code_line(E, '', 900, 540, 34, chars=0, cursor=True, f=f, alpha=clamp01((lb - 28) / 0.5))
|
||||||
|
P['fade'] = clamp01((lb - 32) / 1.2)
|
||||||
|
return P
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
"""Bars 32-48: how it works. Pipeline, frontend, shaders, two backends."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
STAGES = [('YOUR APP', 'OpenGL 4.6 calls'), ('FRONTEND', 'state + validation'), ('MGPIPE', 'records'),
|
||||||
|
('BACKEND', 'Vulkan · OpenGL ES'), ('GPU', 'pixels')]
|
||||||
|
SX = [220, 590, 960, 1330, 1700]
|
||||||
|
TOKENS = ['glDrawElements(...)', 'valid · state Δ', 'record #4217', 'vkCmdDrawIndexed', 'pixels!']
|
||||||
|
|
||||||
|
|
||||||
|
def pipeline(E, f, lb):
|
||||||
|
plate(E, f, 'ink', drift=(30, 0))
|
||||||
|
E.text('THE PIPELINE', 130, 150, 34, a(GLUE, 0.9), font='mono', wght=800, align='left')
|
||||||
|
y = 520
|
||||||
|
for i, (name, sub) in enumerate(STAGES):
|
||||||
|
bi = 4 + i
|
||||||
|
if lb < bi:
|
||||||
|
continue
|
||||||
|
u = out_back((lb - bi) / 0.35)
|
||||||
|
hot = 0.0
|
||||||
|
hop = (lb - 8) / 2.0
|
||||||
|
if 0 <= hop and int(hop) == i and lb >= 8:
|
||||||
|
hot = 1 - (hop % 1)
|
||||||
|
iso = Iso(SX[i], y + 40, 54 * u)
|
||||||
|
top = mix(FACE_DARK[0], scale_rgb(GLUE, 1.3), hot * 0.8)
|
||||||
|
iso.box(E, -1.6, -1.6, 0, 3.2, 3.2, 1.4, top, FACE_DARK[1], FACE_DARK[2], OUTL, 5)
|
||||||
|
E.text(name, SX[i], y + 210, 40, a(BONE, u), wght=800, wdth=105)
|
||||||
|
E.text(sub, SX[i], y + 258, 22, a(BONE, 0.6 * u), font='mono')
|
||||||
|
if i < 4 and lb >= bi + 0.3:
|
||||||
|
arrow(E, SX[i] + 120, y + 40, SX[i + 1] - 120, y + 40, 6, a(GLUE, 0.8), u=(lb - bi - 0.3) / 0.4)
|
||||||
|
if lb >= 8: # the call rides a glue drop through the stages
|
||||||
|
hop = min((lb - 8) / 2.0, 4.0)
|
||||||
|
i = int(hop)
|
||||||
|
fr = hop - i
|
||||||
|
j = min(i + 1, 4)
|
||||||
|
x = lerp(SX[i], SX[j], in_out_cubic(fr / 0.6) if fr < 0.6 else 1.0)
|
||||||
|
yy = y - 120 - 140 * bump(min(fr / 0.6, 1.0)) - 6 * f.kick
|
||||||
|
stage = j if fr >= 0.6 else i
|
||||||
|
goo(E, [(x, yy, 34 + 6 * f.kick)], k=20, outline=5, bevel=20, glow=0.3)
|
||||||
|
chip(E, x, yy - 80, TOKENS[stage], 26, INK, GLUE if stage < 4 else PALE)
|
||||||
|
if lb >= 16 - 0.3:
|
||||||
|
for k in range(14):
|
||||||
|
ang = k * 2 * math.pi / 14 + f.lt
|
||||||
|
rr = 90 + 260 * out_expo((lb - 15.7) / 0.6)
|
||||||
|
E.rect(SX[4] + math.cos(ang) * rr, y + 40 + math.sin(ang) * rr * 0.6, 26, 26,
|
||||||
|
[GLUE, MINT, AMBER, CORAL, PALE][k % 5], r=4, rot=ang)
|
||||||
|
|
||||||
|
|
||||||
|
STATE = ['DEPTH_TEST on', 'BLEND off', 'CULL_FACE back', 'VAO 3', 'PROGRAM 12', 'TEX0 2D:7', 'VIEWPORT 1920×1080',
|
||||||
|
'STENCIL off', 'FBO 2', 'SCISSOR off', 'UBO0 5', 'SSBO1 9', 'DEPTH_FUNC less', 'COLOR_MASK rgba',
|
||||||
|
'BLEND_EQ add', 'FRONT ccw', 'SAMPLER0 4', 'TEX1 cube:2', 'DRAW_BUF 0', 'PRIM_RESTART off', 'POLYGON fill',
|
||||||
|
'PATCH_VERTS 3', 'SRGB on', 'LINE 1.0']
|
||||||
|
ALT = ['DEPTH_TEST off', 'BLEND on', 'CULL_FACE front', 'VAO 8', 'PROGRAM 4', 'TEX0 2D:11', 'VIEWPORT 512×512',
|
||||||
|
'STENCIL on', 'FBO 0', 'SCISSOR on', 'UBO0 2', 'SSBO1 3', 'DEPTH_FUNC lequal', 'COLOR_MASK rgb_',
|
||||||
|
'BLEND_EQ max', 'FRONT cw', 'SAMPLER0 9', 'TEX1 cube:6', 'DRAW_BUF 1', 'PRIM_RESTART on', 'POLYGON line',
|
||||||
|
'PATCH_VERTS 4', 'SRGB off', 'LINE 2.0']
|
||||||
|
|
||||||
|
|
||||||
|
def frontend(E, f, lb):
|
||||||
|
plate(E, f, 'mint', drift=(0, 20))
|
||||||
|
E.text('FRONTEND', 120, 170, 150, INK, wght=900, align='left', wdth=105)
|
||||||
|
E.text('MG_State · MG_Impl', 130, 270, 28, a(INK, 0.7), font='mono', wght=700, align='left')
|
||||||
|
if lb < 24:
|
||||||
|
slam(E, f, 'TRACKS EVERY BIT OF STATE', 1800, 290, f.t - (lb - 16.5) * S.BEAT, 54, MOSS, align='right')
|
||||||
|
step = int(lb * 4)
|
||||||
|
for k in range(24):
|
||||||
|
r, c = k // 4, k % 4
|
||||||
|
x = 300 + c * 440
|
||||||
|
yy = 420 + r * 95
|
||||||
|
flips = sum(1 for s in range(int(16 * 4), step + 1) if rnd('flip', s) * 24 // 1 == k)
|
||||||
|
alt = flips % 2 == 1
|
||||||
|
recent = any(rnd('flip', s) * 24 // 1 == k for s in range(step - 1, step + 1))
|
||||||
|
col = NEON if recent else (MOSS if alt else INK)
|
||||||
|
E.rect(x, yy, 410, 78, a(PALE, 0.95) if not recent else a(INK, 0.95), r=12)
|
||||||
|
E.text((ALT if alt else STATE)[k], x, yy, 26, col if recent else INK, font='mono', wght=600)
|
||||||
|
else:
|
||||||
|
slam(E, f, 'VALIDATES EVERY CALL', 1800, 290, f.t - (lb - 24) * S.BEAT, 54, MOSS, align='right')
|
||||||
|
calls = ['glBindBuffer(GL_ARRAY_BUFFER, 4);', 'glBufferSubData(GL_ARRAY_BUFFER, 0, 256, data);',
|
||||||
|
'glUseProgram(12);', 'glUniformMatrix4fv(0, 1, GL_FALSE, mvp);', 'glBindVertexArray(3);',
|
||||||
|
'glBindVertexArray(999);', 'glBindTexture(GL_TEXTURE_2D, 7);',
|
||||||
|
'glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);']
|
||||||
|
BAD = 5
|
||||||
|
E.rect(960, 680, 1640, 600, INK, r=24)
|
||||||
|
# the checker steps down the list, one call per 8th; it stops dead on the bad one
|
||||||
|
step = (lb - 24.5) * 2
|
||||||
|
cur = BAD if step >= BAD else int(max(step, -1))
|
||||||
|
after = lb - (24.5 + BAD * 0.5) # beats since the bad call was hit
|
||||||
|
if after >= 2.0: # then carries on past it
|
||||||
|
cur = min(len(calls) - 1, BAD + 1 + int((after - 2.0) * 2))
|
||||||
|
for i, c in enumerate(calls):
|
||||||
|
yy = 420 + i * 66
|
||||||
|
checked = i < cur or (i == cur and (step - int(step)) > 0.5) or (i < BAD + 1 and after >= 0 and i != BAD and i < cur + 1)
|
||||||
|
if i == BAD and after >= 0:
|
||||||
|
shake = 10 * math.exp(-after * 6) * math.sin(after * 60)
|
||||||
|
E.rect(960 + shake, yy, 1612, 56, a(CORAL, 0.35 + 0.35 * math.exp(-after * 3)), r=8)
|
||||||
|
code_line(E, c, 210 + shake, yy, 30)
|
||||||
|
cross(E, 172 + shake, yy, 15, CORAL, 5)
|
||||||
|
continue
|
||||||
|
if i == cur:
|
||||||
|
E.rect(960, yy, 1612, 56, a(GLUE, 0.22), r=8, stroke=a(GLUE, 0.9), sw=3)
|
||||||
|
code_line(E, c, 210, yy, 30, alpha=1.0 if i <= cur else 0.4)
|
||||||
|
if i < cur or (i > BAD and i <= cur and i != cur):
|
||||||
|
check(E, 172, yy, 16, GLUE, 5)
|
||||||
|
if after >= 0.5:
|
||||||
|
stamp(E, f, 'GL_INVALID_OPERATION', 1250, 420 + BAD * 66 - 8, f.t - (after - 0.5) * S.BEAT, 54, CORAL,
|
||||||
|
rot=-0.05)
|
||||||
|
if after >= 1.0:
|
||||||
|
E.text('caught in the frontend, before the driver ever sees it', 960, 1020, 30,
|
||||||
|
a(INK, clamp01((after - 1.0) / 0.5)), font='mono', wght=600)
|
||||||
|
|
||||||
|
|
||||||
|
GLSL = ['#version 460 core', 'layout(location = 0) in vec3 pos;', 'uniform mat4 mvp;', 'void main() {',
|
||||||
|
' gl_Position = mvp * vec4(pos, 1.0);', '}']
|
||||||
|
ESSL = ['#version 320 es', 'precision highp float;', 'layout(location = 0) in vec3 pos;', 'uniform mat4 mvp;',
|
||||||
|
'void main() { ... }']
|
||||||
|
SPV = ['0x07230203', '0x00010600', '0x0008000b', '0x0000002e', '0x00000000', '0x00020011', '0x00000001',
|
||||||
|
'0x0006000b', '0x00000001', '0x4c534c47', '0x6474732e', '0x3035342e', '0x00000000', '0x0003000e']
|
||||||
|
|
||||||
|
|
||||||
|
def shaders(E, f, lb):
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
E.text('SHADERS', 120, 150, 120, BONE, wght=900, align='left', wdth=105)
|
||||||
|
E.text('one compiler front, two ways out', 130, 245, 28, a(GLUE, 0.85), font='mono', wght=600, align='left')
|
||||||
|
# GLSL panel
|
||||||
|
E.rect(390, 640, 680, 420, INK2, r=20, stroke=a(GLUE, 0.6), sw=3)
|
||||||
|
E.text('GLSL 4.60', 80, 465, 26, GLUE, font='mono', wght=800, align='left')
|
||||||
|
total = sum(len(s_) for s_ in GLSL)
|
||||||
|
n = int(total * clamp01((lb - 32) / 2.0))
|
||||||
|
for i, s_ in enumerate(GLSL):
|
||||||
|
before = sum(len(x) for x in GLSL[:i])
|
||||||
|
code_line(E, s_, 80, 525 + i * 52, 25, chars=n - before)
|
||||||
|
if lb >= 35:
|
||||||
|
arrow(E, 745, 640, 850, 640, 7, GLUE, u=(lb - 35) / 0.3)
|
||||||
|
chip(E, 800, 570, 'glslang', 26, INK, GLUE, sc=out_back((lb - 35) / 0.3))
|
||||||
|
E.rect(1010, 640, 300, 420, INK2, r=20, stroke=a(GLUE, 0.6), sw=3)
|
||||||
|
E.text('SPIR-V', 880, 465, 26, GLUE, font='mono', wght=800, align='left')
|
||||||
|
k = int((lb - 35) * 6)
|
||||||
|
for i in range(9):
|
||||||
|
w = SPV[(i + k) % len(SPV)]
|
||||||
|
E.text(w, 1010, 520 + i * 40 - ((lb * 6) % 1) * 40, 26, a(AMBER, 0.9), font='mono', wght=600)
|
||||||
|
E.rect(1010, 452, 300, 30, INK) # crop the scrolling words at the panel top
|
||||||
|
E.text('SPIR-V', 880, 465, 26, GLUE, font='mono', wght=800, align='left')
|
||||||
|
if lb >= 40:
|
||||||
|
u1 = (lb - 40) / 0.4
|
||||||
|
arrow(E, 1170, 560, 1290, 420, 7, GLUE, u=u1)
|
||||||
|
arrow(E, 1170, 720, 1290, 830, 7, GLUE, u=u1)
|
||||||
|
uu = out_back((lb - 40.3) / 0.35)
|
||||||
|
if lb >= 40.3:
|
||||||
|
E.rect(1580, 380, 520 * uu, 210 * uu, GLUE, r=22)
|
||||||
|
E.text('VULKAN', 1580, 345, 70 * uu, INK, wght=900)
|
||||||
|
E.text('SPIR-V, as-is · Magma', 1580, 420, 28 * uu, INK, font='mono', wght=700)
|
||||||
|
E.rect(1580, 830, 520 * uu, 330 * uu, INK2, r=22, stroke=a(MINT, 0.8), sw=3)
|
||||||
|
E.text('OPENGL ES', 1580, 710, 54 * uu, MINT, wght=900)
|
||||||
|
for i, s_ in enumerate(ESSL):
|
||||||
|
code_line(E, s_, 1345, 770 + i * 38, 22, alpha=uu, chars=int(len(s_) * clamp01((lb - 41) / 1.5)))
|
||||||
|
chip(E, 1580, 1030, 'SPIRV-Cross → GLSL ES · Espryt', 24, INK, MINT, sc=uu)
|
||||||
|
|
||||||
|
|
||||||
|
def backends(E, f, lb):
|
||||||
|
"""Split screen: Magma/Vulkan vs Espryt/GLES, same cube, the switch flips each beat."""
|
||||||
|
side = int(lb) % 2 # 0 = Vulkan active
|
||||||
|
E.rect(480, 540, 960, 1080, INK)
|
||||||
|
E.rect(1440, 540, 960, 1080, MINT)
|
||||||
|
dot_grid(E, f, a(MOSS, 0.5), 60, 2.0)
|
||||||
|
yaw = f.lt * 0.9
|
||||||
|
sq = 0.18 * f.kick
|
||||||
|
cubes(E, f, [(480, 520, 150, yaw, sq, 0.4 * f.kick, 0.0, 1.0)], deep=hexc('#123a18'))
|
||||||
|
cubes(E, f, [(1440, 520, 150, yaw, sq, 0.4 * f.kick, 0.0, 1.0)], deep=hexc('#2e6e2f'))
|
||||||
|
E.text('MAGMA', 480, 160, 120, BONE, wght=900, wdth=110)
|
||||||
|
E.text('→ VULKAN', 480, 250, 40, GLUE, font='mono', wght=800)
|
||||||
|
E.text('ESPRYT', 1440, 160, 120, INK, wght=900, wdth=110)
|
||||||
|
E.text('→ OPENGL ES', 1440, 250, 40, MOSS, font='mono', wght=800)
|
||||||
|
# dim the inactive half
|
||||||
|
if lb < 60:
|
||||||
|
E.rect(480 + 960 * (1 - side), 540, 960, 1080, a(INK, 0.45))
|
||||||
|
# the switch
|
||||||
|
E.rect(960, 870, 980, 110, INK2, r=55, stroke=a(GLUE, 0.7), sw=3)
|
||||||
|
E.text('MOBILEGL_BACKEND_TYPE=', 560, 870, 30, BONE, font='mono', wght=700, align='left')
|
||||||
|
E.text('DirectVulkan' if side == 0 else 'DirectGLES', 1000, 870, 30, NEON, font='mono', wght=800, align='left')
|
||||||
|
E.text('identical frames, either way', 960, 980, 26, a(BONE, 0.7), font='mono')
|
||||||
|
if lb >= 60:
|
||||||
|
words = [('SAME APP.', 60), ('SAME FRAME.', 61), ('YOUR PICK.', 62)]
|
||||||
|
k = min(int(lb - 60), 2)
|
||||||
|
w, b = words[k]
|
||||||
|
E.rect(960, 540, 1920, 330, a(INK, 0.85))
|
||||||
|
slam(E, f, w, 960, 540, f.t - (lb - b) * S.BEAT, fit_size(E, w, 1500, cap=240), NEON if k == 2 else BONE)
|
||||||
|
|
||||||
|
|
||||||
|
def how(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.5, 'grain': 0.032, 'ca': 0.25 * f.kick}
|
||||||
|
if lb < 4:
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
slam(E, f, 'SO…', 960, 300, f.t - lb * S.BEAT, 200, a(BONE, 0.8))
|
||||||
|
if lb >= 1:
|
||||||
|
slam(E, f, 'HOW?', 960, 640, f.t - (lb - 1) * S.BEAT, 440, NEON, punch=0.3)
|
||||||
|
elif lb < 16:
|
||||||
|
pipeline(E, f, lb)
|
||||||
|
elif lb < 32:
|
||||||
|
frontend(E, f, lb)
|
||||||
|
elif lb < 48:
|
||||||
|
shaders(E, f, lb)
|
||||||
|
else:
|
||||||
|
backends(E, f, lb)
|
||||||
|
return P
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
"""Bars 56-72: the second drop. Same process / another process / another machine, run-ahead, many clients."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
|
||||||
|
def label_chip(E, x, y, s, col=INK, bgc=BONE, size=26, sc=1.0):
|
||||||
|
chip(E, x, y, s, size, col, bgc, sc=sc, font='mono', wght=800)
|
||||||
|
|
||||||
|
|
||||||
|
def blob_pair(E, f, sep, strand_r, k_goo=90, glow=0.15, y=560, r=200, packets=False, lb=0.0):
|
||||||
|
"""Two blobs separated by sep px, joined by a strand (r=0 -> no strand)."""
|
||||||
|
cx = 960
|
||||||
|
wob = 10 * f.kick
|
||||||
|
xa, xb = cx - sep / 2, cx + sep / 2
|
||||||
|
balls = [(xa, y + wob, r + 8 * f.kick), (xb, y - wob, r + 8 * f.kick)]
|
||||||
|
for k in range(5): # surface jiggle
|
||||||
|
ang = f.lt * 2 + k * 1.3
|
||||||
|
balls.append((xa + math.cos(ang) * r * 0.5, y + math.sin(ang) * r * 0.5, r * 0.45))
|
||||||
|
balls.append((xb + math.cos(-ang) * r * 0.5, y + math.sin(-ang) * r * 0.5, r * 0.45))
|
||||||
|
caps = [(xa, y, xb, y, strand_r, strand_r)] if strand_r > 0 else []
|
||||||
|
goo(E, balls, caps, k=k_goo, glow=glow)
|
||||||
|
if packets and strand_r > 0:
|
||||||
|
for k in range(16):
|
||||||
|
ph = (f.lbeat * 2 + k / 4) % 4 / 4
|
||||||
|
x = lerp(xa + r * 0.7, xb - r * 0.7, ph)
|
||||||
|
E.circle(x, y, strand_r * 0.45, scale_rgb(PALE, 1.6))
|
||||||
|
return xa, xb
|
||||||
|
|
||||||
|
|
||||||
|
def modes_v1(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.6, 'grain': 0.032, 'ca': 0.45 * f.kick}
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT
|
||||||
|
if lb < 8: # SAME PROCESS
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
screen_cam(E, f, zoom=1 + 0.015 * f.kick, shake=18 * math.exp(-lb * 2.5))
|
||||||
|
if lb < 0.25:
|
||||||
|
P['flash'] = (*GLUE[:3], 0.8 * (1 - lb / 0.25))
|
||||||
|
slam(E, f, 'SAME PROCESS.', 960, 160, tb_(0), fit_size(E, 'SAME PROCESS.', 1500, cap=170), BONE)
|
||||||
|
blob_pair(E, f, 0, 0, r=260, glow=0.25)
|
||||||
|
label_chip(E, 960, 500, 'FRONTEND', sc=out_back(lb / 0.4))
|
||||||
|
label_chip(E, 960, 620, 'BACKEND', sc=out_back((lb - 0.5) / 0.4))
|
||||||
|
E.text('monolith · inproc', 960, 920, 34, GLUE, font='mono', wght=700)
|
||||||
|
elif lb < 16: # ANOTHER PROCESS
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
screen_cam(E, f, zoom=1 + 0.015 * f.kick, shake=14 * math.exp(-(lb - 8) * 2.5))
|
||||||
|
slam(E, f, 'ANOTHER PROCESS.', 960, 160, tb_(8), fit_size(E, 'ANOTHER PROCESS.', 1600, cap=170), BONE)
|
||||||
|
u = in_out_cubic((lb - 8) / 1.8)
|
||||||
|
sep = 900 * u
|
||||||
|
sr = lerp(150, 26, u)
|
||||||
|
xa, xb = blob_pair(E, f, sep, sr, r=lerp(260, 190, u), packets=lb > 10, lb=lb)
|
||||||
|
label_chip(E, xa, 560, 'CLIENT', sc=1.0)
|
||||||
|
label_chip(E, xb, 560, 'SERVER', sc=1.0)
|
||||||
|
if lb >= 10:
|
||||||
|
uu = clamp01((lb - 10) / 0.5)
|
||||||
|
E.text('your app · frontend', xa, 820, 26, a(BONE, 0.8 * uu), font='mono')
|
||||||
|
E.text('owns the GPU · backend', xb, 820, 26, a(BONE, 0.8 * uu), font='mono')
|
||||||
|
E.text('spawn + shared memory', 960, 470, 30, a(GLUE, uu), font='mono', wght=700)
|
||||||
|
elif lb < 24: # ANOTHER MACHINE
|
||||||
|
plate(E, f, 'ink', drift=(0, 0))
|
||||||
|
z = lerp(1.25, 1.0, out_expo((lb - 16) / 1.5))
|
||||||
|
screen_cam(E, f, zoom=z + 0.012 * f.kick, shake=14 * math.exp(-(lb - 16) * 2.5))
|
||||||
|
slam(E, f, 'ANOTHER MACHINE.', 960, 150, tb_(16), fit_size(E, 'ANOTHER MACHINE.', 1600, cap=170), BONE)
|
||||||
|
il = Iso(360, 820, 58)
|
||||||
|
laptop(E, il, f, y=2.0, screen=screen_layer(E, 'm_lap', 600, 380, paint_mac, f))
|
||||||
|
it = Iso(1580, 860, 62)
|
||||||
|
tower(E, it, f)
|
||||||
|
# long strand with packets, sagging like glue
|
||||||
|
pts = []
|
||||||
|
for k in range(13):
|
||||||
|
u = k / 12
|
||||||
|
pts.append((lerp(560, 1420, u), 560 + 120 * math.sin(math.pi * u) + 14 * math.sin(f.lt * 3 + u * 6)))
|
||||||
|
caps = [(a_[0], a_[1], b[0], b[1], 16, 16) for a_, b in zip(pts, pts[1:])]
|
||||||
|
goo(E, [(560, 560, 40), (1420, 560, 40)], caps, k=30, outline=5, bevel=18)
|
||||||
|
for k in range(8):
|
||||||
|
ph = ((lb - 16) * 4 / 8 + k / 8) % 1
|
||||||
|
idx_ = ph * 12
|
||||||
|
i0 = int(idx_)
|
||||||
|
fr = idx_ - i0
|
||||||
|
x = lerp(pts[i0][0], pts[min(i0 + 1, 12)][0], fr)
|
||||||
|
y = lerp(pts[i0][1], pts[min(i0 + 1, 12)][1], fr)
|
||||||
|
E.rect(x, y - 2, 30, 22, scale_rgb(PALE, 1.5), r=5)
|
||||||
|
chip(E, 990, 440, 'TCP', 34, INK, PALE, font='mono', wght=800)
|
||||||
|
E.text('client', 360, 960, 30, BONE, font='mono', wght=700)
|
||||||
|
E.text('server', 1580, 960, 30, BONE, font='mono', wght=700)
|
||||||
|
if lb >= 18:
|
||||||
|
uu = clamp01((lb - 18) / 0.5)
|
||||||
|
E.text('the client is the frontend · the server is the backend', 960, 1030, 26, a(GLUE, uu),
|
||||||
|
font='mono', wght=600)
|
||||||
|
elif lb < 28: # recap list
|
||||||
|
plate(E, f, 'mint', grid=False)
|
||||||
|
rows = ['SAME PROCESS', 'ANOTHER PROCESS', 'ANOTHER MACHINE']
|
||||||
|
for i, s in enumerate(rows):
|
||||||
|
if lb < 24 + i:
|
||||||
|
continue
|
||||||
|
y = 290 + i * 230
|
||||||
|
slam(E, f, s, 260, y, tb_(24 + i), 150, INK, align='left', wdth=95)
|
||||||
|
check(E, 170, y, 60, MOSS, 16, (lb - 24 - i) / 0.3)
|
||||||
|
if lb >= 27:
|
||||||
|
E.text('monolith · inproc · spawn + shm · tcp', 960, 990, 34, MOSS, font='mono', wght=800)
|
||||||
|
elif lb < 32: # YOUR CALL.
|
||||||
|
plate(E, f, 'glue', grid=False)
|
||||||
|
slam(E, f, 'YOUR CALL.', 960, 540, tb_(28), fit_size(E, 'YOUR CALL.', 1600), INK, punch=0.3)
|
||||||
|
screen_cam(E, f, shake=16 * math.exp(-(lb - 28) * 3))
|
||||||
|
elif lb < 40: # RUN-AHEAD lanes
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
slam(E, f, 'RUN-AHEAD', 120, 160, tb_(32), 150, BONE, align='left', wdth=110)
|
||||||
|
E.text('the client keeps going while the server catches up', 130, 260, 28, a(GLUE, 0.9), font='mono',
|
||||||
|
wght=600, align='left')
|
||||||
|
scroll = (lb - 32) * 70
|
||||||
|
for lane, (name, y, wait) in enumerate((('APP · monolith', 470, True), ('GPU', 560, True),
|
||||||
|
('APP · split', 760, False), ('GPU', 850, False))):
|
||||||
|
E.text(name, 130, y, 24, a(BONE, 0.8), font='mono', wght=700, align='left')
|
||||||
|
for k in range(14):
|
||||||
|
if lane in (0, 1):
|
||||||
|
x0 = 420 + k * 260 - scroll
|
||||||
|
if lane == 0:
|
||||||
|
E.rect(x0 + 60, y, 110, 54, GLUE, r=8)
|
||||||
|
E.rect(x0 + 180, y, 110, 54, a(BONE, 0.12), r=8)
|
||||||
|
else:
|
||||||
|
E.rect(x0 + 180, y, 110, 54, MINT, r=8)
|
||||||
|
else:
|
||||||
|
x0 = 420 + k * 150 - scroll * 1.0
|
||||||
|
if lane == 2:
|
||||||
|
E.rect(x0 + 60, y, 130, 54, GLUE, r=8)
|
||||||
|
else:
|
||||||
|
E.rect(x0 + 120, y, 130, 54, MINT, r=8)
|
||||||
|
E.rect(200, 660, 400, 520, INK) # mask the lanes under the labels
|
||||||
|
for lane, (name, y) in enumerate((('APP · monolith', 470), ('GPU', 560), ('APP · split', 760), ('GPU', 850))):
|
||||||
|
E.text(name, 130, y, 24, a(BONE, 0.85), font='mono', wght=700, align='left')
|
||||||
|
E.text('wait', 700 - (scroll % 260), 420, 20, a(BONE, 0.4), font='mono')
|
||||||
|
elif lb < 48: # ~1/3 faster
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
slam(E, f, '≈33% FASTER', 960, 260, tb_(40), 210, NEON, punch=0.3)
|
||||||
|
u = out_expo((lb - 41) / 1.5)
|
||||||
|
for i, (name, val, col) in enumerate((('monolith', 1.0, a(BONE, 0.6)), ('inproc / spawn', 1.33, GLUE))):
|
||||||
|
y = 560 + i * 170
|
||||||
|
E.text(name, 520, y, 34, BONE, font='mono', wght=700, align='right')
|
||||||
|
w = 900 * val / 1.33 * u
|
||||||
|
E.rect(560 + w / 2, y, w, 110, col, r=14)
|
||||||
|
E.text(f'{val:.2f}×', 560 + w + 30, y, 44, a(BONE, clamp01(u * 3)), wght=900, align='left')
|
||||||
|
E.text('under heavy load, with the backend split off', 960, 960, 28, a(BONE, 0.7), font='mono')
|
||||||
|
else: # many clients orbiting one server
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
zoom = 1 + 0.25 * in_expo((lb - 58) / 6)
|
||||||
|
screen_cam(E, f, zoom=zoom + 0.01 * f.kick, rot=0.05 * in_expo((lb - 58) / 6))
|
||||||
|
apps = ['browser', 'terminal', 'game', 'compositor', 'video', 'editor']
|
||||||
|
n = min(6, max(0, int(lb - 48) + 1))
|
||||||
|
balls = [(960, 560, 150 + 10 * f.kick)]
|
||||||
|
caps = []
|
||||||
|
pos = []
|
||||||
|
for k in range(n):
|
||||||
|
ang = k * 2 * math.pi / 6 + f.lt * 0.35
|
||||||
|
u = out_back((lb - 48 - k) / 0.5)
|
||||||
|
R = 400 * u
|
||||||
|
x, y = 960 + math.cos(ang) * R * 1.25, 560 + math.sin(ang) * R * 0.8
|
||||||
|
balls.append((x, y, 70))
|
||||||
|
caps.append((960, 560, x, y, 14, 14))
|
||||||
|
pos.append((x, y, apps[k]))
|
||||||
|
goo(E, balls, caps, k=40, glow=0.2)
|
||||||
|
label_chip(E, 960, 560, 'SERVER', size=30)
|
||||||
|
for x, y, s in pos:
|
||||||
|
label_chip(E, x, y, s, size=22)
|
||||||
|
if lb >= 56:
|
||||||
|
uu = clamp01((lb - 56) / 0.5)
|
||||||
|
E.text('one server · many apps · a whole desktop', 960, 1000, 36, a(GLUE, uu), font='mono', wght=700)
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def run_ahead(E, f, u):
|
||||||
|
"""6 beats. Time advances one slot per 8th, snapping: the monolith app alternates work and waiting, the split
|
||||||
|
app never waits while the GPU follows one slot behind."""
|
||||||
|
P = {'bloom': 0.55, 'grain': 0.032, 'ca': 0.4 * f.kick}
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
tb0 = f.t - u * S.BEAT
|
||||||
|
slam(E, f, 'RUN-AHEAD', 120, 150, tb0, 150, BONE, align='left', wdth=110)
|
||||||
|
E.text('the client keeps going while the server catches up', 130, 250, 28, a(GLUE, 0.9), font='mono', wght=600,
|
||||||
|
align='left')
|
||||||
|
n = min(12, int(u * 2) + 1) # slots shown so far
|
||||||
|
frac = (u * 2) % 1
|
||||||
|
pop = out_back(clamp01(frac / 0.35), 2.0)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.012 * f.kick + 0.006 * (1 - clamp01(frac / 0.3)))
|
||||||
|
X0, SW = 440, 94
|
||||||
|
lanes = [('APP · monolith', 430), ('GPU', 515), ('APP · split', 700), ('GPU', 785)]
|
||||||
|
for name, y in lanes:
|
||||||
|
E.text(name, 130, y, 26, a(BONE, 0.85), font='mono', wght=700, align='left')
|
||||||
|
E.line(X0 - 20, y + 36, X0 + 12 * SW, y + 36, 2, a(MOSS, 0.8))
|
||||||
|
waits = 0
|
||||||
|
for k in range(n):
|
||||||
|
sc_ = pop if k == n - 1 else 1.0
|
||||||
|
x = X0 + k * SW + SW / 2
|
||||||
|
# monolith: the app works, then waits while the GPU draws that frame
|
||||||
|
if k % 2 == 0:
|
||||||
|
E.rect(x, 430, (SW - 10) * sc_, 58 * sc_, GLUE, r=8)
|
||||||
|
else:
|
||||||
|
waits += 1
|
||||||
|
E.rect(x, 430, (SW - 10) * sc_, 58 * sc_, a(BONE, 0.14), r=8)
|
||||||
|
E.text('wait', x, 430, 20 * sc_, a(BONE, 0.6), font='mono', wght=700)
|
||||||
|
E.rect(x, 515, (SW - 10) * sc_, 58 * sc_, MINT, r=8)
|
||||||
|
# split: the app records every slot, the server draws one slot behind
|
||||||
|
E.rect(x, 700, (SW - 10) * sc_, 58 * sc_, GLUE, r=8)
|
||||||
|
if k >= 1:
|
||||||
|
E.rect(x, 785, (SW - 10) * sc_, 58 * sc_, MINT, r=8)
|
||||||
|
# playhead
|
||||||
|
px = X0 + n * SW - (1 - pop) * 0.0
|
||||||
|
E.line(px, 380, px, 830, 4, a(NEON, 0.9))
|
||||||
|
E.text(f'app waited: {waits}', X0 + 12 * SW + 40, 472, 30, a(CORAL, 0.95), font='mono', wght=800, align='left')
|
||||||
|
E.text('app waited: 0', X0 + 12 * SW + 40, 742, 30, GLUE, font='mono', wght=800, align='left')
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def vf(f, vlb):
|
||||||
|
"""A copy of the frame whose local clock reads vlb beats (beat pulses stay on the real clock)."""
|
||||||
|
import copy
|
||||||
|
g = copy.copy(f)
|
||||||
|
d = (vlb - f.lbeat) * S.BEAT
|
||||||
|
g.lbeat, g.lt, g.t = vlb, f.lt + d, f.t + d
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
WORDS2 = [('MONOLITH', 'one process'), ('INPROC', 'same process, through the pipe'), ('SPAWN + SHM', 'a child process'),
|
||||||
|
('TCP', 'another machine'), ('ONE SERVER', 'owns the GPU'), ('MANY APPS', 'one per client'),
|
||||||
|
('A WHOLE DESKTOP', 'compositor included'), ('ANY MACHINE', 'over the network')]
|
||||||
|
|
||||||
|
|
||||||
|
def modes(E, f):
|
||||||
|
"""Second drop, accelerating: 4-6 beat ideas, then 1-beat words, then doubling clients, then the vortex."""
|
||||||
|
lb = f.lbeat
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT
|
||||||
|
if lb < 4:
|
||||||
|
return modes_v1(E, f)
|
||||||
|
if lb < 8:
|
||||||
|
return modes_v1(E, vf(f, 8 + (lb - 4) * 1.5))
|
||||||
|
if lb < 14:
|
||||||
|
return modes_v1(E, vf(f, 16 + (lb - 8)))
|
||||||
|
if lb < 17:
|
||||||
|
return modes_v1(E, vf(f, 24 + (lb - 14)))
|
||||||
|
if lb < 18:
|
||||||
|
return modes_v1(E, vf(f, 28 + (lb - 17)))
|
||||||
|
if lb < 24:
|
||||||
|
return run_ahead(E, f, lb - 18)
|
||||||
|
if lb < 28:
|
||||||
|
return modes_v1(E, vf(f, 40 + (lb - 24) * 2))
|
||||||
|
P = {'bloom': 0.6, 'grain': 0.032, 'ca': 0.45 * f.kick}
|
||||||
|
if lb < 36: # clients join on 8ths, the orbit speeds up
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
c = climax(f, 28, 36)
|
||||||
|
screen_cam(E, f, zoom=1.05 - 0.1 * c + 0.012 * f.kick)
|
||||||
|
apps = ['browser', 'terminal', 'game', 'compositor', 'video', 'editor', 'launcher', 'IDE', 'player', 'chat',
|
||||||
|
'office', 'shell']
|
||||||
|
n = min(12, int((lb - 28) * 2) + 1)
|
||||||
|
balls = [(960, 540, 150 + 12 * f.kick)]
|
||||||
|
caps, pos = [], []
|
||||||
|
spin = f.lt * 0.4 + 2.0 * c * c
|
||||||
|
for k in range(n):
|
||||||
|
ring = 0 if k < 6 else 1
|
||||||
|
ang = (k % 6) * math.pi / 3 + spin * (1 if ring == 0 else -0.7) + ring * 0.5
|
||||||
|
u = out_back((lb - 28 - k * 0.5) / 0.35)
|
||||||
|
R = (330 + 190 * ring) * u
|
||||||
|
x, y = 960 + math.cos(ang) * R * 1.35, 540 + math.sin(ang) * R * 0.8
|
||||||
|
balls.append((x, y, 58 - 8 * ring))
|
||||||
|
caps.append((960, 540, x, y, 12, 12))
|
||||||
|
pos.append((x, y, apps[k]))
|
||||||
|
goo(E, balls, caps, k=40, glow=0.2 + 0.2 * c)
|
||||||
|
chip(E, 960, 540, 'SERVER', 30, INK, PALE)
|
||||||
|
for x, y, s in pos:
|
||||||
|
chip(E, x, y, s, 20, INK, BONE)
|
||||||
|
E.text(f'{n} clients', 960, 1010, 34, GLUE, font='mono', wght=800)
|
||||||
|
elif lb < 44: # one word a beat
|
||||||
|
kind, k = beat_plate(f, ['ink', 'glue', 'ink', 'mint'], 36)
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
w, cap = WORDS2[k % 8]
|
||||||
|
screen_cam(E, f, zoom=1 + 0.05 * (lb % 1) + 0.02 * f.kick, rot=(0.02 if k % 2 else -0.02))
|
||||||
|
speedlines(E, f, 0.3 + 0.2 * climax(f, 36, 44), a(fg, 0.45), seed=k + 40)
|
||||||
|
slam(E, f, w, 960, 500, tb_(36 + k), fit_size(E, w, 1600, cap=380), fg if kind != 'ink' else NEON, punch=0.22)
|
||||||
|
E.text(cap, 960, 780, 40, a(fg, 0.8), font='mono', wght=700)
|
||||||
|
elif lb < 56: # clients doubling every beat
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
k = int(lb - 44)
|
||||||
|
fr = lb - 44 - k
|
||||||
|
n_prev = 2 ** min(k, 6)
|
||||||
|
split = out_back(clamp01(fr / 0.45))
|
||||||
|
cols = 8
|
||||||
|
n_draw = min(64, 2 ** min(k + 1, 6))
|
||||||
|
balls = []
|
||||||
|
for j in range(n_draw):
|
||||||
|
parent = j // 2 if k < 6 else j
|
||||||
|
if k < 6:
|
||||||
|
# parent positions laid out on a grid sized for n_prev, children slide apart
|
||||||
|
pc = min(n_prev, 64)
|
||||||
|
gx = max(1, int(math.ceil(math.sqrt(pc * 16 / 9))))
|
||||||
|
px = 960 + ((parent % gx) - (gx - 1) / 2) * 1500 / gx
|
||||||
|
py = 540 + ((parent // gx) - (math.ceil(pc / gx) - 1) / 2) * 820 / max(1, math.ceil(pc / gx))
|
||||||
|
r = min(140, 620 / gx)
|
||||||
|
off = (j % 2 - 0.5) * r * 1.2 * split
|
||||||
|
balls.append((px + off, py, r * (1 - 0.25 * split)))
|
||||||
|
else:
|
||||||
|
x = 960 + ((j % cols) - 3.5) * 200
|
||||||
|
y = 540 + ((j // cols) - 3.5) * 110
|
||||||
|
wave = math.sin(lb * math.pi * 2 - j * 0.4)
|
||||||
|
balls.append((x, y + 10 * wave, 38 + 8 * f.kick))
|
||||||
|
screen_cam(E, f, zoom=1.0 + 0.02 * f.kick + 0.08 * climax(f, 50, 56), shake=3 * climax(f, 50, 56))
|
||||||
|
goo(E, balls, k=24 if k < 6 else 16, glow=0.25, outline=5, bevel=24)
|
||||||
|
E.cam, E.shake = (LW / 2, LH / 2, 1.0, 0.0), (0.0, 0.0)
|
||||||
|
E.rect(960, 1010, 640, 76, INK, r=38, stroke=a(GLUE, 0.7), sw=3)
|
||||||
|
E.text('more clients · one server', 960, 1010, 36, NEON, font='mono', wght=800)
|
||||||
|
P['bloom'] = 0.7
|
||||||
|
else: # vortex climax
|
||||||
|
c = climax(f, 56, 63)
|
||||||
|
plate(E, f, 'ink', grid=False)
|
||||||
|
speedlines(E, f, 0.4 + 0.6 * c, a(GLUE, 0.8), n=110, seed=11)
|
||||||
|
screen_cam(E, f, zoom=1 + 0.9 * c + 0.03 * f.kick, rot=-0.08 * c, shake=4 + 16 * c)
|
||||||
|
balls = [(960, 540, 120 + 140 * c)]
|
||||||
|
for j in range(48):
|
||||||
|
ang = j * 2.399 + f.lt * (0.8 + 7 * c)
|
||||||
|
R = (220 + 700 * rnd('vx', j)) * (1 - 0.85 * c)
|
||||||
|
balls.append((960 + math.cos(ang) * R * 1.5, 540 + math.sin(ang) * R * 0.85, 26 + 10 * rnd('vr', j)))
|
||||||
|
goo(E, balls, k=50 + 60 * c, glow=0.25 + 0.5 * c)
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0 + 0.04 * f.kick + 0.1 * c, 0.0)
|
||||||
|
E.text('ONE SERVER.', 960, 200, 150, BONE, wght=900, wdth=100 + 25 * c)
|
||||||
|
E.text('EVERY APP.', 960, 900, 150, NEON, wght=900, wdth=100 + 25 * c)
|
||||||
|
P['ca'] = 0.5 + 2.0 * c
|
||||||
|
P['bloom'] = 0.6 + 0.4 * c
|
||||||
|
return P
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
"""Bars 0-16: the cold open (a prompt that the screen obeys) and the first build-up."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
CLEAR_C = hexc('#0b2412')
|
||||||
|
|
||||||
|
|
||||||
|
def cold_open(E, f):
|
||||||
|
"""Bars 0-8. GL calls typed at a prompt; each one is executed by the frame itself."""
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.7, 'grain': 0.032, 'vignette': 0.45}
|
||||||
|
bg(E, INK)
|
||||||
|
# glClear: flood from the prompt
|
||||||
|
if lb >= 8:
|
||||||
|
u = out_expo((lb - 8) / 1.2)
|
||||||
|
E.circle(150, 930, 2400 * u, CLEAR_C)
|
||||||
|
dot_grid(E, f, a(MOSS, 0.55 * u), 60, 2.2)
|
||||||
|
cmds = [ # text, type start, type end, execute beat
|
||||||
|
('glClearColor(0.04, 0.14, 0.07, 1.0);', 1.0, 3.6, 4.0),
|
||||||
|
('glClear(GL_COLOR_BUFFER_BIT);', 4.5, 7.0, 8.0),
|
||||||
|
('glDrawArrays(GL_TRIANGLES, 0, 3);', 9.0, 11.6, 12.0),
|
||||||
|
('glUseProgram(glue);', 16.0, 18.0, 20.0),
|
||||||
|
('// ...and now: run it on everything', 24.0, 27.5, 99.0),
|
||||||
|
]
|
||||||
|
# centre visual: triangle -> glue drop
|
||||||
|
cx, cy = 960, 470
|
||||||
|
if 12 <= lb < 22:
|
||||||
|
u = out_back((lb - 12) / 0.9, 2.2)
|
||||||
|
melt = in_out_cubic((lb - 20) / 2.0)
|
||||||
|
R = 300 * u * (1 - 0.85 * melt)
|
||||||
|
rot = 0.05 * (lb - 12) + 0.25 * melt
|
||||||
|
pts = [(cx + R * math.cos(rot - math.pi / 2 + k * 2 * math.pi / 3),
|
||||||
|
cy + 30 + R * math.sin(rot - math.pi / 2 + k * 2 * math.pi / 3)) for k in range(3)]
|
||||||
|
hp = 1 + 0.25 * f.hat
|
||||||
|
if melt < 1:
|
||||||
|
tri_rgb(E, pts, [scale_rgb(GLUE, 2.2 * hp), scale_rgb(PALE, 1.0), scale_rgb(MOSS, 1.3)])
|
||||||
|
if melt > 0: # vertices turn into glue and run to the centre
|
||||||
|
balls = [(lerp(px, cx, melt), lerp(py, cy + 30, melt), 30 + 120 * melt) for px, py in pts]
|
||||||
|
goo(E, balls, k=90, outline=6)
|
||||||
|
elif 22 <= lb:
|
||||||
|
bob = math.sin(f.lt * 3.0) * 8
|
||||||
|
stretch = 0.0
|
||||||
|
r = 150 + 10 * f.hat
|
||||||
|
if lb >= 24:
|
||||||
|
stretch = 260 * in_cubic((lb - 24) / 7.5)
|
||||||
|
balls = [(cx, cy + 30 + bob, r)]
|
||||||
|
caps = [(cx, cy + 30 + bob - stretch - 40, cx, cy + 30 + bob, r * 0.25, r * 0.95)] if stretch > 1 else []
|
||||||
|
z = 1 + 9 * in_expo((lb - 27) / 4.6)
|
||||||
|
E.cam = (cx, cy + 30 + bob, z, 0.0)
|
||||||
|
goo(E, balls, caps, k=80, outline=7, glow=0.2 * clamp01((lb - 24) / 8))
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
||||||
|
if lb > 30.6:
|
||||||
|
bg(E, a(GLUE, clamp01((lb - 30.6) / 0.6)))
|
||||||
|
# prompt + log
|
||||||
|
py = 930
|
||||||
|
done = [c for c in cmds if lb >= c[2]]
|
||||||
|
cur = [c for c in cmds if c[1] <= lb < c[2] or (lb >= c[2] and lb < c[3])]
|
||||||
|
alpha_ui = 1.0 - clamp01((lb - 29) / 1.5)
|
||||||
|
for i, c in enumerate(reversed([c for c in cmds if lb >= c[3]])):
|
||||||
|
code_line(E, c[0], 150, py - 56 * (i + 1) - 20, 28, alpha=(0.55 - 0.1 * i) * alpha_ui)
|
||||||
|
active = None
|
||||||
|
for c in cmds:
|
||||||
|
if c[1] <= lb < c[3] and lb < 99:
|
||||||
|
active = c
|
||||||
|
E.text('>', 112, py, 30, a(GLUE, alpha_ui), font='mono', wght=700, align='left')
|
||||||
|
if active:
|
||||||
|
n = len(typed(f, active[0], active[1], active[2]))
|
||||||
|
code_line(E, active[0], 150, py, 30, chars=n, cursor=True, f=f, alpha=alpha_ui)
|
||||||
|
else:
|
||||||
|
code_line(E, '', 150, py, 30, chars=0, cursor=True, f=f, alpha=alpha_ui)
|
||||||
|
# execute flashes
|
||||||
|
for c in cmds:
|
||||||
|
u = lb - c[3]
|
||||||
|
if 0 <= u < 0.5:
|
||||||
|
E.rect(LW / 2, LH / 2, LW, LH, a(GLUE, 0.025 * (1 - u / 0.5)))
|
||||||
|
hud(E, f, 'MOBILEGL // REEL', 'OPENGL 4.6 CORE', alpha=clamp01(lb / 4) * alpha_ui)
|
||||||
|
if lb > 30.6:
|
||||||
|
P['exposure'] = 1.0 + 0.6 * clamp01((lb - 30.6) / 1.4)
|
||||||
|
return P
|
||||||
|
|
||||||
|
|
||||||
|
def your_app(E, f):
|
||||||
|
"""Bars 8-16: your app speaks GL 4.6 -> your device doesn't -> so we made the glue -> countdown."""
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.5, 'grain': 0.035}
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT # absolute time of local beat b
|
||||||
|
if lb < 8:
|
||||||
|
plate(E, f, 'mint', drift=(0, -12))
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0 + 0.004 * lb, -0.004 * lb)
|
||||||
|
x0 = 130
|
||||||
|
w = slam(E, f, 'YOUR', x0, 270, tb_(0), 210, INK, align='left')
|
||||||
|
slam(E, f, 'APP', x0 + w + 50, 270, tb_(1), 210, INK, align='left')
|
||||||
|
slam(E, f, 'SPEAKS', x0, 500, tb_(2), 210, INK, align='left')
|
||||||
|
w = slam(E, f, 'OPENGL', x0, 730, tb_(3), 210, INK, align='left')
|
||||||
|
slam(E, f, '4.6', x0 + w + 50, 730, tb_(3) + 0.06, 210, MOSS, align='left', wdth=80)
|
||||||
|
chips_ = ['#version 460 core', 'glBindVertexArray', 'glDrawElementsInstanced', 'GL_ARB_direct_state_access',
|
||||||
|
'glMultiDrawElementsIndirect', 'compute shaders', 'SSBOs', 'tessellation']
|
||||||
|
pos = [(1460, 190), (1500, 330), (1420, 470), (900, 900), (1430, 610), (420, 905), (1650, 905), (1300, 905)]
|
||||||
|
for i, s in enumerate(chips_):
|
||||||
|
bi = 4 + i * 0.5
|
||||||
|
if lb >= bi:
|
||||||
|
u = out_back((lb - bi) / 0.35)
|
||||||
|
chip(E, pos[i][0], pos[i][1], s, 30, BONE, INK, sc=u, rot=0.03 * math.sin(i * 2.3))
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
||||||
|
elif lb < 16:
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
w = slam(E, f, 'YOUR', 130, 190, tb_(8), 150, BONE, align='left')
|
||||||
|
w2 = slam(E, f, 'DEVICE', 130 + w + 40, 190, tb_(8.5), 150, BONE, align='left')
|
||||||
|
if lb >= 15:
|
||||||
|
stamp(E, f, "DOESN'T.", 960, 560, tb_(15), 210, CORAL, rot=-0.09, w_pad=50)
|
||||||
|
if lb >= 15:
|
||||||
|
P['shake'] = 1
|
||||||
|
screen_cam(E, f, shake=10 * math.exp(-(lb - 15) * 3))
|
||||||
|
devs = [('phone', 400, 'Android', 'OpenGL ES + Vulkan only'), ('laptop', 960, 'macOS', 'OpenGL 4.1, deprecated'),
|
||||||
|
('board', 1520, 'ARM Linux', 'GLES-only vendor drivers')]
|
||||||
|
for i, (kind, x, name, sub) in enumerate(devs):
|
||||||
|
bi = 9 + i
|
||||||
|
if lb < bi:
|
||||||
|
continue
|
||||||
|
u = out_back((lb - bi) / 0.4)
|
||||||
|
iso = Iso(x, 760 + 40 * (1 - u), 34 * u)
|
||||||
|
if kind == 'phone':
|
||||||
|
phone(E, iso, f)
|
||||||
|
elif kind == 'laptop':
|
||||||
|
laptop(E, iso, f, y=2.0)
|
||||||
|
else:
|
||||||
|
board(E, iso, f)
|
||||||
|
E.text(name, x, 880, 44, a(BONE, u), wght=800)
|
||||||
|
E.text(sub, x, 935, 24, a(BONE, 0.65 * u), font='mono', wght=500)
|
||||||
|
ci = 12 + i
|
||||||
|
if lb >= ci:
|
||||||
|
cross(E, x, 600, 120, CORAL, 26, (lb - ci) / 0.25)
|
||||||
|
E.cam = (LW / 2, LH / 2, 1.0, 0.0)
|
||||||
|
E.shake = (0.0, 0.0)
|
||||||
|
elif lb < 24:
|
||||||
|
plate(E, f, 'mint', drift=(10, 0))
|
||||||
|
slam(E, f, 'SO WE MADE', 960, 230, tb_(16), 120, INK, wdth=110)
|
||||||
|
size = fit_size(E, 'THE GLUE.', 1640)
|
||||||
|
if lb >= 18:
|
||||||
|
slam(E, f, 'THE GLUE.', 960, 470, tb_(18), size, INK, punch=0.25)
|
||||||
|
top = 470 - size * 0.36 # cap top
|
||||||
|
# a drop falls onto the words at beat 19 and drips over them
|
||||||
|
if lb >= 18.2:
|
||||||
|
fall = clamp01((lb - 18.2) / 0.8)
|
||||||
|
X = 1290
|
||||||
|
y = lerp(-150, top - 30, in_cubic(fall))
|
||||||
|
if fall < 1:
|
||||||
|
b, c = drop_shape(X, y, 56, 90 * fall)
|
||||||
|
goo(E, b, c, k=50)
|
||||||
|
else:
|
||||||
|
spread = out_expo((lb - 19) / 0.6)
|
||||||
|
balls = [(X, top - 18, 46 * (1 - 0.4 * spread))]
|
||||||
|
caps = [(X - 250 * spread, top - 6, X + 230 * spread, top - 6, 14, 14)]
|
||||||
|
for k, (dx, ln) in enumerate(((-230, 70), (-120, 150), (-20, 60), (80, 190), (190, 90))):
|
||||||
|
L = ln * out_cubic((lb - 19 - 0.12 * k) / 3.0)
|
||||||
|
x = X + dx * spread
|
||||||
|
if L > 2:
|
||||||
|
caps.append((x, top - 6, x, top - 6 + L, 10, 13))
|
||||||
|
balls.append((x, top - 6 + L, 17))
|
||||||
|
goo(E, balls, caps, k=34, outline=5, bevel=22)
|
||||||
|
# the crossed devices come back with checks
|
||||||
|
for i, (name, x) in enumerate((('Android', 560), ('macOS', 960), ('ARM Linux', 1360))):
|
||||||
|
bi = 20 + i
|
||||||
|
if lb < bi:
|
||||||
|
continue
|
||||||
|
u = out_back((lb - bi) / 0.3)
|
||||||
|
E.circle(x, 780, 80 * u, INK)
|
||||||
|
check(E, x, 780, 70 * u, GLUE, 14, (lb - bi - 0.1) / 0.3)
|
||||||
|
E.text(name, x, 910, 40, a(INK, u), wght=800)
|
||||||
|
E.line(x, 690, x, 600, 8, a(LEAF, u))
|
||||||
|
else:
|
||||||
|
# bars 14-15: loading bar, then 3-2-1, then one beat of silence
|
||||||
|
if lb < 28:
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
u = clamp01((lb - 24) / 3.9)
|
||||||
|
k = int(u * 32)
|
||||||
|
E.text('LOADING DESKTOP OPENGL', 960, 420, 64, BONE, wght=800, wdth=110)
|
||||||
|
for i in range(32):
|
||||||
|
c = NEON if i < k else a(MOSS, 0.8)
|
||||||
|
E.rect(960 - 31 * 26 / 2 + i * 26, 540, 18, 54, c, r=3)
|
||||||
|
E.text(f'{int(u * 100):3d}%', 960, 640, 40, GLUE, font='mono', wght=700)
|
||||||
|
E.text('frontend · pipe · backend · window system', 960, 720, 24, a(BONE, 0.5), font='mono')
|
||||||
|
elif lb < 31:
|
||||||
|
i = int(lb - 28)
|
||||||
|
kind = ['glue', 'ink', 'mint'][i]
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
slam(E, f, str(3 - i), 960, 560, tb_(28 + i), 760, fg if kind != 'ink' else NEON, punch=0.35, wght=900,
|
||||||
|
wdth=110)
|
||||||
|
P['shake'] = 1
|
||||||
|
screen_cam(E, f, shake=14 * math.exp(-(lb - 28 - i) * 5), seed=i)
|
||||||
|
else: # the silent beat: the block falls into the glue on the downbeat
|
||||||
|
pre_drop(E, f, 960, 330, 165, tb_(32))
|
||||||
|
P['grain'] = 0.05
|
||||||
|
return P
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""Bars 48-56: MGPipe over the snare roll. Only what changed, no round trips, deduplicated."""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import *
|
||||||
|
from devices import *
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
ROLLS = [t for t, _ in S.events('roll') if S.tb(48) <= t < S.tb(56)]
|
||||||
|
LABELS = ['BIND', 'STATE Δ', 'DRAW', 'UNIFORM', 'CLEAR', 'DRAW', 'STATE Δ', 'BLIT', 'DRAW', 'FENCE']
|
||||||
|
|
||||||
|
|
||||||
|
def tube(E, f, y, x0=330, x1=1590, rate_scale=1.0):
|
||||||
|
E.rect((x0 + x1) / 2, y, x1 - x0, 96, INK2, r=48, stroke=a(GLUE, 0.6), sw=4)
|
||||||
|
speed = 900.0
|
||||||
|
for k, ts in enumerate(ROLLS):
|
||||||
|
if ts > f.t:
|
||||||
|
break
|
||||||
|
x = x0 + 40 + speed * (f.t - ts)
|
||||||
|
if x > x1 - 40:
|
||||||
|
continue
|
||||||
|
lab = LABELS[k % len(LABELS)]
|
||||||
|
w = E.measure(lab, 'mono', 20, 800) + 26
|
||||||
|
E.rect(x, y, w, 46, GLUE if 'Δ' not in lab else NEON, r=23)
|
||||||
|
E.text(lab, x, y, 20, INK, font='mono', wght=800)
|
||||||
|
|
||||||
|
|
||||||
|
def card(E, x, y, title, sub, col=GLUE, fg=INK, w=300, h=200):
|
||||||
|
E.rect(x, y, w, h, col, r=26)
|
||||||
|
E.text(title, x, y - 18, 44, fg, wght=900)
|
||||||
|
E.text(sub, x, y + 40, 20, a(fg, 0.7), font='mono', wght=600)
|
||||||
|
|
||||||
|
|
||||||
|
ROWS = [('BLEND', 'off', 'on'), ('DEPTH_TEST', 'on', 'on'), ('VIEWPORT', '1920×1080', '1920×1080'),
|
||||||
|
('PROGRAM', '12', '12'), ('TEX0', '2D:7', '2D:9'), ('CULL_FACE', 'back', 'back'), ('VAO', '3', '3'),
|
||||||
|
('SCISSOR', 'off', 'off'), ('UBO0', '5', '5'), ('STENCIL', 'off', 'off')]
|
||||||
|
|
||||||
|
|
||||||
|
def pipe(E, f):
|
||||||
|
lb = f.lbeat
|
||||||
|
P = {'bloom': 0.55, 'grain': 0.035, 'ca': 0.2 + 0.6 * f.roll * clamp01(lb / 24)}
|
||||||
|
tb_ = lambda b: f.t - (lb - b) * S.BEAT
|
||||||
|
zoom = 1 + 0.06 * clamp01(lb / 31) + 0.01 * f.roll
|
||||||
|
if lb < 8:
|
||||||
|
plate(E, f, 'ink', drift=(-60, 0))
|
||||||
|
screen_cam(E, f, zoom=zoom)
|
||||||
|
slam(E, f, 'MGPIPE', 960, 300, tb_(0), 260, BONE, wdth=120)
|
||||||
|
if lb >= 1:
|
||||||
|
E.text('the stream between frontend and backend', 960, 450, 32, a(GLUE, clamp01(lb - 1)), font='mono',
|
||||||
|
wght=600)
|
||||||
|
card(E, 200, 720, 'FRONT', 'your process', GLUE)
|
||||||
|
card(E, 1720, 720, 'BACK', 'owns the GPU', MINT)
|
||||||
|
tube(E, f, 720)
|
||||||
|
elif lb < 16:
|
||||||
|
plate(E, f, 'mint')
|
||||||
|
screen_cam(E, f, zoom=zoom)
|
||||||
|
slam(E, f, 'ONLY WHAT CHANGED.', 960, 150, tb_(8), 120, INK, wdth=105)
|
||||||
|
beat = int(lb - 8)
|
||||||
|
changed = {0: [0], 1: [4], 2: [0, 6], 3: [9], 4: [2], 5: [4, 7], 6: [1], 7: [3, 0]}
|
||||||
|
live = changed.get(beat, [])
|
||||||
|
for i, (name, v0, v1) in enumerate(ROWS):
|
||||||
|
y = 290 + i * 70
|
||||||
|
hot = i in live
|
||||||
|
E.rect(470, y, 640, 58, INK if hot else a(PALE, 0.9), r=10)
|
||||||
|
E.text(name, 170, y, 26, GLUE if hot else INK, font='mono', wght=700, align='left')
|
||||||
|
val = (v1 if (beat + i) % 2 else v0) if hot else v0
|
||||||
|
E.text(val, 770, y, 26, NEON if hot else a(INK, 0.6), font='mono', wght=700, align='right')
|
||||||
|
if hot:
|
||||||
|
u = (lb % 1) / 0.8
|
||||||
|
x = lerp(800, 1500, in_out_cubic(u))
|
||||||
|
yy = lerp(y, 620, in_out_cubic(u))
|
||||||
|
if u < 1:
|
||||||
|
chip(E, x, yy, f'{name}={val}', 22, INK, GLUE)
|
||||||
|
E.rect(1600, 620, 420, 300, INK, r=26)
|
||||||
|
E.text('SENT', 1600, 520, 30, GLUE, font='mono', wght=800)
|
||||||
|
E.text(f'{len(live)}', 1600, 630, 150, NEON, wght=900)
|
||||||
|
E.text(f'of {len(ROWS)} fields', 1600, 730, 26, a(BONE, 0.7), font='mono')
|
||||||
|
elif lb < 24:
|
||||||
|
plate(E, f, 'ink')
|
||||||
|
screen_cam(E, f, zoom=zoom)
|
||||||
|
slam(E, f, 'NO ROUND TRIPS.', 960, 160, tb_(16), 130, BONE, wdth=105)
|
||||||
|
card(E, 380, 560, 'CLIENT', 'frontend', GLUE, w=420, h=260)
|
||||||
|
card(E, 1540, 560, 'SERVER', 'backend', MINT, w=420, h=260)
|
||||||
|
code_line(E, 'glGenBuffers(1, &buf);', 170, 780, 32, chars=int(clamp01((lb - 16.3) / 1.2) * 22))
|
||||||
|
if lb >= 18:
|
||||||
|
u = out_back((lb - 18) / 0.35)
|
||||||
|
chip(E, 380, 880, 'buf = { slot: 17, gen: 3 }', 30, INK, PALE, sc=u)
|
||||||
|
E.text('minted on the client, instantly', 380, 960, 24, a(BONE, 0.7 * u), font='mono')
|
||||||
|
if lb >= 19:
|
||||||
|
arrow(E, 610, 520, 1310, 520, 10, GLUE, u=(lb - 19) / 0.5)
|
||||||
|
E.text('records →', 960, 470, 26, GLUE, font='mono', wght=700)
|
||||||
|
if lb >= 20:
|
||||||
|
u = clamp01((lb - 20) / 0.5)
|
||||||
|
for k in range(12):
|
||||||
|
x0 = 1310 - k * 60
|
||||||
|
if (1310 - x0) / 700 < u:
|
||||||
|
E.line(x0, 620, x0 - 30, 620, 8, a(BONE, 0.4))
|
||||||
|
E.poly([(600, 620), (640, 600), (640, 640)], a(BONE, 0.4))
|
||||||
|
if lb >= 21:
|
||||||
|
cross(E, 960, 620, 80, CORAL, 18, (lb - 21) / 0.3)
|
||||||
|
E.text('waiting for a reply', 960, 700, 26, a(CORAL, 0.9), font='mono', wght=700)
|
||||||
|
elif lb < 28:
|
||||||
|
plate(E, f, 'glue', grid=False)
|
||||||
|
screen_cam(E, f, zoom=zoom)
|
||||||
|
slam(E, f, 'DEDUPLICATED.', 960, 180, tb_(24), 150, INK, wdth=105)
|
||||||
|
u = in_out_cubic((lb - 25) / 1.6)
|
||||||
|
for k in range(6):
|
||||||
|
ang = k * math.pi / 3 + 0.4
|
||||||
|
x = 960 + math.cos(ang) * 520 * (1 - u)
|
||||||
|
y = 640 + math.sin(ang) * 260 * (1 - u)
|
||||||
|
E.rect(x, y, 360, 220, INK, r=24, rot=0.1 * math.sin(k) * (1 - u))
|
||||||
|
for L in range(3):
|
||||||
|
E.rect(x - 60 + 20 * (L % 2), y - 50 + L * 50, 220 - 40 * (L % 2), 22, a(GLUE, 0.8), r=8,
|
||||||
|
rot=0.1 * math.sin(k) * (1 - u))
|
||||||
|
if lb >= 26.6:
|
||||||
|
chip(E, 1140, 520, '×6 → 1', 40, INK, PALE, sc=out_back((lb - 26.6) / 0.3), font='archivo', wght=900)
|
||||||
|
elif lb < 31:
|
||||||
|
words = ['RECORD', 'DIFF', 'DEDUPE', 'SEND', 'RECORD', 'DIFF']
|
||||||
|
k = min(int((lb - 28) * 2), 5)
|
||||||
|
kind = ['ink', 'ink', 'glue', 'glue', 'mint', 'mint'][k]
|
||||||
|
b, fg = plate(E, f, kind, grid=False)
|
||||||
|
slam(E, f, words[k], 960, 560, tb_(28 + k * 0.5), fit_size(E, words[k], 1600, cap=520), fg if kind != 'ink' else NEON,
|
||||||
|
punch=0.2)
|
||||||
|
P['ca'] = 1.0 + 0.3 * k
|
||||||
|
else:
|
||||||
|
bg(E, hexc('#000000'))
|
||||||
|
return P
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""Timeline: which scene draws which bars, plus the global beat-driven post and the stutter glitch."""
|
||||||
|
import math
|
||||||
|
import importlib
|
||||||
|
from anim import *
|
||||||
|
from engine import LW, LH
|
||||||
|
|
||||||
|
# (first bar, end bar, module, function)
|
||||||
|
TIMELINE = [
|
||||||
|
(0, 8, 'sc_open', 'cold_open'),
|
||||||
|
(8, 16, 'sc_open', 'your_app'),
|
||||||
|
(16, 32, 'sc_drop1', 'drop1'),
|
||||||
|
(32, 48, 'sc_how', 'how'),
|
||||||
|
(48, 56, 'sc_pipe', 'pipe'),
|
||||||
|
(56, 72, 'sc_modes', 'modes'),
|
||||||
|
(72, 80, 'sc_break', 'breakdown'),
|
||||||
|
(80, 88, 'sc_build3', 'checklist'),
|
||||||
|
(88, 104, 'sc_final', 'final_drop'),
|
||||||
|
(104, 200, 'sc_final', 'outro'),
|
||||||
|
]
|
||||||
|
_fns = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _fn(mod, name):
|
||||||
|
key = (mod, name)
|
||||||
|
if key not in _fns:
|
||||||
|
try:
|
||||||
|
_fns[key] = getattr(importlib.import_module(mod), name)
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
_fns[key] = None
|
||||||
|
return _fns[key]
|
||||||
|
|
||||||
|
|
||||||
|
STUTTER = [t for t, _ in S.events('stutter')]
|
||||||
|
PATTERN = [2, 2, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25]
|
||||||
|
|
||||||
|
|
||||||
|
def stutter_remap(t):
|
||||||
|
"""Same slicing as the music's stutter: inside the beat, show the first 8th again and again."""
|
||||||
|
for ts in STUTTER:
|
||||||
|
if ts <= t < ts + S.BEAT:
|
||||||
|
u = t - ts
|
||||||
|
p = 0.0
|
||||||
|
for k, s in enumerate(PATTERN):
|
||||||
|
d = s * S.S16
|
||||||
|
if u < p + d:
|
||||||
|
return ts + (u - p), k
|
||||||
|
p += d
|
||||||
|
return t, -1
|
||||||
|
|
||||||
|
|
||||||
|
def draw(E, t):
|
||||||
|
t_src, sk = stutter_remap(t)
|
||||||
|
bar = t_src / S.BAR
|
||||||
|
entry = None
|
||||||
|
for e in TIMELINE:
|
||||||
|
if e[0] <= bar < e[1]:
|
||||||
|
entry = e
|
||||||
|
break
|
||||||
|
E.begin_sub(INK)
|
||||||
|
P = {}
|
||||||
|
if entry:
|
||||||
|
fn = _fn(entry[2], entry[3])
|
||||||
|
f = Frame(t_src, entry[0])
|
||||||
|
if fn:
|
||||||
|
P = fn(E, f) or {}
|
||||||
|
else:
|
||||||
|
E.rect(LW / 2, LH / 2, LW, LH, INK)
|
||||||
|
E.text(f'{entry[2]}.{entry[3]} bar {bar:.2f}', LW / 2, LH / 2, 60, BONE, font='mono')
|
||||||
|
E.flush()
|
||||||
|
if sk >= 0: # glitch dressing on the repeats
|
||||||
|
P = dict(P)
|
||||||
|
P['ca'] = P.get('ca', 0) + 3.0 + sk * 0.3
|
||||||
|
P['scan'] = 0.25
|
||||||
|
return P
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""The arrangement both the music and the picture are cut to: tempo grid, sections, harmony, melodies
|
||||||
|
and every drum / fx event. The video reads the same events for its beat pulses, so they never drift."""
|
||||||
|
import math
|
||||||
|
|
||||||
|
BPM = 128.0
|
||||||
|
BEAT = 60.0 / BPM # 0.46875 s
|
||||||
|
BAR = 4 * BEAT # 1.875 s
|
||||||
|
S16 = BEAT / 4
|
||||||
|
NBARS = 112
|
||||||
|
END = NBARS * BAR + 3.5 # music + reverb tail (213.5 s)
|
||||||
|
|
||||||
|
SECTIONS = [ # name, first bar, end bar
|
||||||
|
('intro', 0, 8), ('build1', 8, 16), ('drop1', 16, 32), ('verse', 32, 48), ('build2', 48, 56),
|
||||||
|
('drop2', 56, 72), ('break', 72, 80), ('build3', 80, 88), ('drop3', 88, 104), ('outro', 104, 112),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def tb(bar, beat=0.0, s16=0.0):
|
||||||
|
"""Time of bar + beat + sixteenth, in seconds."""
|
||||||
|
return bar * BAR + beat * BEAT + s16 * S16
|
||||||
|
|
||||||
|
|
||||||
|
def section(bar):
|
||||||
|
for name, a, b in SECTIONS:
|
||||||
|
if a <= bar < b:
|
||||||
|
return name, a, b
|
||||||
|
return 'end', NBARS, NBARS + 4
|
||||||
|
|
||||||
|
|
||||||
|
def key_shift(bar):
|
||||||
|
return 2 if bar >= 88 else 0 # truck-driver lift into the last drop
|
||||||
|
|
||||||
|
|
||||||
|
# --- harmony ---------------------------------------------------------------------------------------
|
||||||
|
A, F, C, G, Em = 'Am', 'F', 'C', 'G', 'Em'
|
||||||
|
CHORDS = { # close voicings around middle C (midi), root for the bass
|
||||||
|
'Am': ((57, 60, 64), 45), 'F': ((57, 60, 65), 41), 'C': ((55, 60, 64), 48),
|
||||||
|
'G': ((55, 59, 62), 43), 'Em': ((55, 59, 64), 40), 'Dm': ((57, 62, 65), 38),
|
||||||
|
}
|
||||||
|
MAIN = [A, F, C, G]
|
||||||
|
VERSE = [F, G, Em, A]
|
||||||
|
|
||||||
|
|
||||||
|
def chord_name(bar):
|
||||||
|
name, a, _ = section(bar)
|
||||||
|
i = bar - a
|
||||||
|
if name == 'verse':
|
||||||
|
return VERSE[i % 4]
|
||||||
|
if name == 'break':
|
||||||
|
return [F, F, G, G, A, A, F, G][i]
|
||||||
|
if name == 'outro':
|
||||||
|
return [A, F, C, G, C, C, C, C][i]
|
||||||
|
if name == 'end':
|
||||||
|
return C
|
||||||
|
return MAIN[i % 4]
|
||||||
|
|
||||||
|
|
||||||
|
def chord(bar):
|
||||||
|
"""(voicing, bass root) for the bar, transposed by the key lift."""
|
||||||
|
v, r = CHORDS[chord_name(bar)]
|
||||||
|
k = key_shift(bar)
|
||||||
|
return tuple(n + k for n in v), r + k
|
||||||
|
|
||||||
|
|
||||||
|
# --- melodies (16th-step grid: (step, length, midi)) ------------------------------------------------
|
||||||
|
HOOK = [
|
||||||
|
[(0, 2, 76), (3, 1, 76), (4, 2, 74), (6, 2, 76), (8, 3, 81), (11, 2, 79), (14, 2, 76)], # Am
|
||||||
|
[(0, 2, 77), (3, 1, 76), (4, 2, 74), (6, 4, 72), (10, 2, 74), (12, 4, 76)], # F
|
||||||
|
[(0, 2, 79), (3, 1, 79), (4, 2, 77), (6, 2, 76), (8, 3, 76), (11, 2, 72), (14, 2, 74)], # C
|
||||||
|
[(0, 6, 74), (6, 2, 71), (8, 4, 74), (12, 2, 76), (14, 2, 79)], # G
|
||||||
|
]
|
||||||
|
HOOK_END = [(0, 3, 74), (3, 3, 71), (6, 2, 74), (8, 8, 69)] # G, resolving
|
||||||
|
|
||||||
|
|
||||||
|
def hook_bar(i):
|
||||||
|
"""Bar i (0..7) of the 8-bar lead phrase."""
|
||||||
|
return HOOK_END if i % 8 == 7 else HOOK[i % 4]
|
||||||
|
|
||||||
|
|
||||||
|
def notes_in(bar, pattern, shift=0):
|
||||||
|
k = key_shift(bar)
|
||||||
|
return [(tb(bar, 0, s), l * S16, m + k + shift) for s, l, m in pattern]
|
||||||
|
|
||||||
|
|
||||||
|
# --- events ----------------------------------------------------------------------------------------
|
||||||
|
def _events():
|
||||||
|
ev = [] # (time, kind, vel)
|
||||||
|
|
||||||
|
def add(t, kind, vel=1.0):
|
||||||
|
ev.append((t, kind, vel))
|
||||||
|
|
||||||
|
for bar in range(NBARS):
|
||||||
|
name, a, b = section(bar)
|
||||||
|
i = bar - a
|
||||||
|
last = bar == b - 1
|
||||||
|
if name == 'intro':
|
||||||
|
if i >= 4:
|
||||||
|
for s in range(2, 16, 4):
|
||||||
|
add(tb(bar, 0, s), 'hat', 0.5 + 0.1 * (i - 4))
|
||||||
|
if i == 0:
|
||||||
|
add(tb(bar), 'impact', 0.5)
|
||||||
|
elif name == 'build1':
|
||||||
|
for q in range(4):
|
||||||
|
if not (last and q == 3):
|
||||||
|
add(tb(bar, q), 'kick', 0.7 if i < 6 else 0.78)
|
||||||
|
add(tb(bar, q, 2), 'hat', 0.7)
|
||||||
|
for q in (1, 3):
|
||||||
|
if not (last and q == 3):
|
||||||
|
add(tb(bar, q), 'clap', 0.65)
|
||||||
|
elif name in ('drop1', 'drop2', 'drop3'):
|
||||||
|
hot = {'drop1': 1.0, 'drop2': 1.0, 'drop3': 1.0}[name]
|
||||||
|
fill = last or i == 7
|
||||||
|
for q in range(4):
|
||||||
|
add(tb(bar, q), 'kick', hot)
|
||||||
|
add(tb(bar, q, 2), 'ohat', 0.8)
|
||||||
|
for q in (1, 3):
|
||||||
|
add(tb(bar, q), 'clap', 1.0)
|
||||||
|
for s in range(16):
|
||||||
|
if s % 4 != 2:
|
||||||
|
add(tb(bar, 0, s), 'hat', 0.45 + 0.25 * (s % 2 == 0))
|
||||||
|
if i % 8 == 0:
|
||||||
|
add(tb(bar), 'crash', 1.0)
|
||||||
|
if i == 14: # climb: 8th-note snares, then 16ths in the last bar
|
||||||
|
for s in range(0, 16, 2):
|
||||||
|
add(tb(bar, 0, s), 'snare', 0.35 + 0.02 * s)
|
||||||
|
elif last:
|
||||||
|
for s in range(12):
|
||||||
|
add(tb(bar, 0, s), 'snare', 0.55 + 0.035 * s)
|
||||||
|
elif fill:
|
||||||
|
for s in (12, 13, 14, 15):
|
||||||
|
add(tb(bar, 0, s), 'snare', 0.6 + 0.1 * (s - 12))
|
||||||
|
if last:
|
||||||
|
add(tb(bar, 3), 'stutter', 1.0)
|
||||||
|
elif name == 'verse':
|
||||||
|
for q in range(4):
|
||||||
|
add(tb(bar, q), 'kick', 0.8)
|
||||||
|
for q in (1, 3):
|
||||||
|
add(tb(bar, q), 'clap', 0.75)
|
||||||
|
for s in range(16):
|
||||||
|
add(tb(bar, 0, s), 'shaker', 0.5 + 0.3 * (s % 4 == 2))
|
||||||
|
if i % 4 == 3:
|
||||||
|
add(tb(bar, 3, 2), 'snare', 0.5)
|
||||||
|
add(tb(bar, 3, 3), 'snare', 0.6)
|
||||||
|
if i == 8:
|
||||||
|
add(tb(bar), 'crash', 0.7)
|
||||||
|
elif name in ('build2', 'build3'):
|
||||||
|
n = 8
|
||||||
|
for q in range(4):
|
||||||
|
if i < 6:
|
||||||
|
add(tb(bar, q), 'kick', 0.7)
|
||||||
|
# snare roll: quarters -> 8ths -> 16ths -> 32nds, rising
|
||||||
|
div = 1 if i < 2 else 2 if i < 4 else 4 if i < 6 else 8
|
||||||
|
steps = 4 * div
|
||||||
|
for s in range(steps):
|
||||||
|
if last and s >= steps * 3 // 4:
|
||||||
|
break # one beat of silence before the drop
|
||||||
|
prog = (i + s / steps) / n
|
||||||
|
add(tb(bar, s * 4 / steps), 'roll', 0.35 + 0.65 * prog)
|
||||||
|
if i == 0:
|
||||||
|
add(tb(bar), 'crash', 0.8)
|
||||||
|
elif name == 'break':
|
||||||
|
if i == 0:
|
||||||
|
add(tb(bar), 'impact', 0.8)
|
||||||
|
if i >= 4:
|
||||||
|
for q in (0, 2):
|
||||||
|
add(tb(bar, q), 'heart', 0.5 + 0.1 * (i - 4))
|
||||||
|
elif name == 'outro':
|
||||||
|
if i < 4:
|
||||||
|
for q in range(4):
|
||||||
|
add(tb(bar, q), 'kick', 1.0 - 0.15 * i)
|
||||||
|
add(tb(bar, q, 2), 'ohat', 0.7 - 0.1 * i)
|
||||||
|
for q in (1, 3):
|
||||||
|
add(tb(bar, q), 'clap', 0.9 - 0.15 * i)
|
||||||
|
if i == 0:
|
||||||
|
add(tb(bar), 'crash', 1.0)
|
||||||
|
if i == 4:
|
||||||
|
add(tb(bar), 'impact', 1.0)
|
||||||
|
add(tb(bar), 'crash', 1.0)
|
||||||
|
add(tb(bar), 'kick', 1.0)
|
||||||
|
for b in (16, 56, 88): # drops: impact + reverse swell into them
|
||||||
|
add(tb(b), 'impact', 1.0)
|
||||||
|
add(tb(b), 'drop', 1.0)
|
||||||
|
for a, b in ((4, 8), (12, 16), (48, 56), (80, 88), (30, 32), (70, 72), (102, 104)):
|
||||||
|
add(tb(a), 'riser', tb(b) - tb(a))
|
||||||
|
return sorted(ev)
|
||||||
|
|
||||||
|
|
||||||
|
EVENTS = _events()
|
||||||
|
|
||||||
|
|
||||||
|
def events(kind):
|
||||||
|
return [(t, v) for t, k, v in EVENTS if k == kind]
|
||||||
|
|
||||||
|
|
||||||
|
def pulse(t, kind, decay=0.12, kinds_cache={}):
|
||||||
|
"""Exponentially decaying pulse from the most recent event of kind (0..1 * vel)."""
|
||||||
|
ts = kinds_cache.get(kind)
|
||||||
|
if ts is None:
|
||||||
|
ts = kinds_cache[kind] = events(kind)
|
||||||
|
best = 0.0
|
||||||
|
lo, hi = 0, len(ts)
|
||||||
|
while lo < hi: # last event <= t
|
||||||
|
mid = (lo + hi) // 2
|
||||||
|
if ts[mid][0] <= t:
|
||||||
|
lo = mid + 1
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
if lo:
|
||||||
|
te, v = ts[lo - 1]
|
||||||
|
best = v * math.exp(-(t - te) / decay)
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
print(f'{NBARS} bars, {NBARS * BAR:.2f}s + tail -> {END:.2f}s, {len(EVENTS)} events')
|
||||||
|
for name, a, b in SECTIONS:
|
||||||
|
print(f'{name:8s} bars {a:3d}-{b:3d} {tb(a):7.2f}s - {tb(b):7.2f}s')
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
// The MobileGlues mark, alive: a crafting-table / command-block block floating in a blob of glue
|
||||||
|
// (a glue pool around its lower half, tongues of glue rising behind it, a few loose drops), raymarched
|
||||||
|
// under an orthographic isometric camera.
|
||||||
|
in vec2 v_uv;
|
||||||
|
out vec4 o;
|
||||||
|
uniform vec2 u_res;
|
||||||
|
uniform vec4 u_cubes[16]; // screen x, y, size (half-edge px), yaw
|
||||||
|
uniform vec4 u_cubes2[16]; // squash, wobble, tint mix (0 green .. 1 alt), alpha
|
||||||
|
uniform vec4 u_cubes3[16]; // seconds since the block hit the glue (big = settled)
|
||||||
|
uniform int u_n;
|
||||||
|
uniform float u_time;
|
||||||
|
uniform vec3 u_body; // glue colour (linear)
|
||||||
|
uniform vec3 u_alt; // alternative block tint
|
||||||
|
uniform vec3 u_deep; // deep glue colour
|
||||||
|
uniform float u_speckle; // visibility of the items on the block top
|
||||||
|
uniform float u_pitch;
|
||||||
|
|
||||||
|
vec4 C1, C2, C3;
|
||||||
|
float seed;
|
||||||
|
float sd_rbox(vec3 p, vec3 b, float r){ vec3 q = abs(p) - b + r; return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r; }
|
||||||
|
float sd_cap(vec3 p, vec3 a, vec3 b, float r){ vec3 pa = p - a, ba = b - a; float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); return length(pa - ba * h) - r; }
|
||||||
|
float smin(float a, float b, float k){ float h = max(k - abs(a - b), 0.0) / k; return min(a, b) - h * h * k * 0.25; }
|
||||||
|
float smax(float a, float b, float k){ float h = max(k - abs(a - b), 0.0) / k; return max(a, b) + h * h * k * 0.25; }
|
||||||
|
|
||||||
|
// C3.x = seconds since the block hit the glue (negative: still falling, large: settled)
|
||||||
|
float since(){ return C3.x; }
|
||||||
|
float splash(){ return since() < 0.0 ? 0.0 : exp(-since() * 2.6); }
|
||||||
|
float bob(){
|
||||||
|
float u = since();
|
||||||
|
float rest = 0.06 * sin(u_time * 2.2 + seed * 3.0) - C2.x * 0.5;
|
||||||
|
if (u < 0.0) return 0.25 + 22.0 * u * u; // falling in
|
||||||
|
return rest * (1.0 - exp(-u * 2.0)) - 0.42 * exp(-u * 4.5) * cos(u * 11.0); // plunge, bounce, settle
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 deform(vec3 p){
|
||||||
|
float wob = C2.y;
|
||||||
|
return p + wob * 0.04 * vec3(sin(3.1 * p.y + u_time * 9.0), 0.0, sin(2.9 * p.x + u_time * 8.0));
|
||||||
|
}
|
||||||
|
float d_block(vec3 p){ return sd_rbox(p - vec3(0.0, bob(), 0.0), vec3(0.78), 0.07); }
|
||||||
|
|
||||||
|
// glue level around the block: a wavy line about half-way up its sides
|
||||||
|
float level(vec3 p){
|
||||||
|
float a = atan(p.z, p.x);
|
||||||
|
return -0.12 + 0.13 * sin(a * 5.0 + u_time * 1.4 + seed) + 0.07 * sin(a * 9.0 - u_time * 1.1) + 0.25 * splash();
|
||||||
|
}
|
||||||
|
float d_glue(vec3 p){
|
||||||
|
float sq = C2.x, u = since(), sp = splash();
|
||||||
|
// the pool: spreads and ripples outward from the impact
|
||||||
|
float r = length(p.xz);
|
||||||
|
float ripple = sp * 0.09 * sin(9.0 * r - 16.0 * u) * smoothstep(1.9, 0.6, r);
|
||||||
|
float wide = 1.5 * (1.0 + 0.12 * sp * sin(u * 12.0));
|
||||||
|
float th = atan(p.z, p.x);
|
||||||
|
float R = wide * (1.0 + 0.08 * sin(3.0 * th + seed + u_time * 0.7) + 0.05 * sin(5.0 * th - u_time * 0.9 + seed * 2.0));
|
||||||
|
float hh = 0.2 * (1.0 - 0.5 * sq) * (1.0 - 0.35 * sp);
|
||||||
|
vec2 dq = vec2(r - (R - 0.3), abs(p.y + 0.78 - ripple) - hh + 0.0); // a pillow: round in plan, soft rim
|
||||||
|
float pool = length(max(dq, 0.0)) + min(max(dq.x, dq.y), 0.0) - 0.3;
|
||||||
|
float g = pool;
|
||||||
|
// before the hit: a fat, wobbling blob of glue waiting in the middle; the block squashes it flat
|
||||||
|
float mound = u < 0.0 ? 1.0 : exp(-u * 10.0);
|
||||||
|
if (mound > 0.01) {
|
||||||
|
vec3 m = p - vec3(0.0, -0.55, 0.0);
|
||||||
|
float w = 1.0 + 0.06 * sin(u_time * 5.0);
|
||||||
|
vec3 rad = vec3(1.2 * w, 0.85 * mound / w + 0.01, 1.2 * w);
|
||||||
|
float e = (length(m / rad) - 1.0) * min(min(rad.x, rad.y), rad.z);
|
||||||
|
g = smin(g, e, 0.3);
|
||||||
|
}
|
||||||
|
// a coat that hugs the block up to the wavy level (only once the block is in)
|
||||||
|
if (u >= 0.0) {
|
||||||
|
vec3 pb = p - vec3(0.0, bob(), 0.0);
|
||||||
|
float coat = smax(sd_rbox(pb, vec3(0.86), 0.32), pb.y - level(pb), 0.18);
|
||||||
|
coat += (1.0 - smoothstep(0.0, 0.12, u)) * 0.6;
|
||||||
|
g = smin(g, coat, 0.35);
|
||||||
|
}
|
||||||
|
// tongues erupt behind the block on impact, overshoot, then sway; their drops are flung up and hover
|
||||||
|
float erupt = u < 0.0 ? 0.0 : max(0.0, 1.0 - exp(-u * 5.0) + 0.55 * exp(-u * 3.0) * sin(u * 10.0));
|
||||||
|
if (erupt > 0.0) {
|
||||||
|
for (int i = 0; i < 5; i++){
|
||||||
|
float fi = float(i);
|
||||||
|
float ang = 3.93 - C1.w + (fi - 2.0) * 0.48; // spread around the back (relative to the view)
|
||||||
|
vec2 xz = vec2(cos(ang), sin(ang)) * (1.18 + 0.12 * sin(fi * 2.1));
|
||||||
|
float lag = clamp(u * 6.0 - fi * 0.35, 0.0, 1.0);
|
||||||
|
float top = ((0.55 + 0.45 * fract(sin(fi * 17.3 + seed) * 4375.5)) * (1.0 - 0.6 * sq)
|
||||||
|
+ 0.08 * sin(u_time * 2.0 + fi * 1.7) + 0.7) * erupt * lag - 0.7;
|
||||||
|
float rr = (0.24 + 0.08 * fract(sin(fi * 9.1 + seed) * 1735.3)) * min(1.0, 0.3 + lag);
|
||||||
|
g = smin(g, sd_cap(p, vec3(xz.x, -0.7, xz.y), vec3(xz.x, top, xz.y), rr), 0.25);
|
||||||
|
float fly = 1.6 * exp(-u * 1.7) * max(sin(u * 4.0), -0.2);
|
||||||
|
float dy = top + 0.45 + fly + 0.12 * sin(u_time * 2.6 + fi * 2.3);
|
||||||
|
g = min(g, length(p - vec3(xz.x * 1.05, dy, xz.y * 1.05)) - rr * 0.45 * min(1.0, u * 4.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the splash itself: droplets thrown out radially, falling back
|
||||||
|
if (u >= 0.0 && u < 0.9) {
|
||||||
|
for (int i = 0; i < 10; i++){
|
||||||
|
float fi = float(i);
|
||||||
|
float a = fi * 0.628 + 0.3;
|
||||||
|
float sp2 = 2.6 + 1.2 * fract(sin(fi * 31.7) * 943.1);
|
||||||
|
vec3 c = vec3(cos(a) * (1.0 + sp2 * u), -0.35 + 4.6 * u - 8.5 * u * u, sin(a) * (1.0 + sp2 * u));
|
||||||
|
g = min(g, length(p - c) - 0.13 * (1.0 - u / 0.9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
float map(vec3 p){ p = deform(p); return min(d_block(p), d_glue(p)) * 0.7; }
|
||||||
|
float map_block(vec3 p){ return d_block(deform(p)) * 0.8; }
|
||||||
|
float map_glue(vec3 p){ return d_glue(deform(p)) * 0.7; }
|
||||||
|
vec3 nrm(vec3 p){ vec2 e = vec2(1e-3, 0); return normalize(vec3(map(p + e.xyy) - map(p - e.xyy), map(p + e.yxy) - map(p - e.yxy), map(p + e.yyx) - map(p - e.yyx))); }
|
||||||
|
|
||||||
|
float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
||||||
|
|
||||||
|
vec3 block_albedo(vec3 q, vec3 n){
|
||||||
|
vec3 base = mix(vec3(0.16, 0.5, 0.1), u_alt, C2.z);
|
||||||
|
vec3 an = abs(n);
|
||||||
|
vec2 uv; float top = 0.0;
|
||||||
|
if (an.y > max(an.x, an.z)) { uv = q.xz; top = 1.0; }
|
||||||
|
else if (an.x > an.z) uv = vec2(q.z, q.y); else uv = vec2(q.x, q.y);
|
||||||
|
uv /= 0.78;
|
||||||
|
vec2 tx = floor((uv * 0.5 + 0.5) * 16.0); // 16x16 texels
|
||||||
|
float grain = 0.92 + 0.16 * h2(tx + top * 31.0);
|
||||||
|
vec2 c = (tx + 0.5) / 16.0 * 2.0 - 1.0;
|
||||||
|
vec3 col = base * grain;
|
||||||
|
if (top > 0.5) {
|
||||||
|
// crafting grid: pale panel, dark frame, 3x3 slots, some with items
|
||||||
|
if (max(abs(c.x), abs(c.y)) < 0.6) {
|
||||||
|
col = vec3(0.47, 0.75, 0.42) * (0.95 + 0.1 * h2(tx));
|
||||||
|
vec2 slot = floor((c + 0.6) / 0.4);
|
||||||
|
vec2 sc = (slot + 0.5) * 0.4 - 0.6;
|
||||||
|
if (max(abs(c.x - sc.x), abs(c.y - sc.y)) > 0.15) col *= 0.82;
|
||||||
|
float r = h2(slot + 7.0);
|
||||||
|
vec3 item = r < 0.2 ? vec3(0.62, 0.11, 0.08) : r < 0.4 ? vec3(0.21, 0.09, 0.01) : r < 0.55 ? vec3(0.55, 0.3, 0.08)
|
||||||
|
: r < 0.7 ? vec3(0.13, 0.09, 0.06) : r < 0.85 ? vec3(0.66, 0.42, 0.25) : vec3(0.1, 0.3, 0.08);
|
||||||
|
if (h2(slot + 3.0) < 0.75 && max(abs(c.x - sc.x), abs(c.y - sc.y)) < 0.1) col = mix(col, item, u_speckle);
|
||||||
|
} else if (max(abs(c.x), abs(c.y)) < 0.72) col = base * 0.55;
|
||||||
|
} else {
|
||||||
|
// command-block side: framed square with an up chevron
|
||||||
|
float m = max(abs(c.x), abs(c.y + 0.05));
|
||||||
|
if (m > 0.5 && m < 0.66) col = base * 0.55;
|
||||||
|
else if (m <= 0.5) {
|
||||||
|
col = base * 1.1 * grain;
|
||||||
|
float chev = abs(abs(c.x) - (0.25 - (c.y + 0.05)));
|
||||||
|
if (chev < 0.1 && abs(c.x) < 0.4 && c.y > -0.35 && c.y < 0.3) col = base * 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 shade_cube(vec2 s, int i){
|
||||||
|
C1 = u_cubes[i]; C2 = u_cubes2[i]; C3 = u_cubes3[i];
|
||||||
|
seed = float(i) * 1.37;
|
||||||
|
float sz = C1.z;
|
||||||
|
const float FIT = 1.4; // the glue blob is wider than the old cube
|
||||||
|
vec2 q = (s - C1.xy) / sz * FIT;
|
||||||
|
if (dot(q, q) > (C3.x < 0.0 || C3.x < 1.0 ? 30.0 : 9.0)) return vec4(0);
|
||||||
|
float yaw = C1.w + 0.7853982, pit = u_pitch;
|
||||||
|
vec3 fw = normalize(vec3(-cos(pit) * sin(yaw), -sin(pit), -cos(pit) * cos(yaw)));
|
||||||
|
vec3 rt = normalize(cross(fw, vec3(0, 1, 0)));
|
||||||
|
vec3 up = cross(rt, fw);
|
||||||
|
vec3 ro = -fw * 5.0 + rt * q.x - up * q.y;
|
||||||
|
float t = 0.0, d, dmin = 1e5;
|
||||||
|
bool hit = false;
|
||||||
|
for (int k = 0; k < 180; k++){
|
||||||
|
d = map(ro + fw * t);
|
||||||
|
dmin = min(dmin, d);
|
||||||
|
if (d < 6e-4){ hit = true; break; }
|
||||||
|
t += d;
|
||||||
|
if (t > 10.0) break;
|
||||||
|
}
|
||||||
|
float pxw = FIT / sz * u_logical.x / u_res.x;
|
||||||
|
float cov = hit ? 1.0 : clamp(1.0 - dmin / pxw, 0.0, 1.0) * step(dmin, pxw);
|
||||||
|
if (cov <= 0.0) return vec4(0);
|
||||||
|
vec3 p = ro + fw * t;
|
||||||
|
vec3 n = nrm(p);
|
||||||
|
vec3 qd = deform(p);
|
||||||
|
bool glue = hit && d_glue(qd) < d_block(qd);
|
||||||
|
vec3 L = normalize(vec3(-0.45, 0.85, 0.35));
|
||||||
|
vec3 H = normalize(L - fw);
|
||||||
|
vec3 c;
|
||||||
|
float alpha = 1.0;
|
||||||
|
if (glue) {
|
||||||
|
// slime: a glossy skin over translucent green; march through it, see the block inside
|
||||||
|
float fres = 0.04 + 0.96 * pow(1.0 - clamp(dot(n, -fw), 0.0, 1.0), 4.0);
|
||||||
|
vec3 rd = refract(fw, n, 0.75);
|
||||||
|
if (dot(rd, rd) < 0.5) rd = fw;
|
||||||
|
vec3 q2 = p - n * 0.01;
|
||||||
|
float path = 0.0;
|
||||||
|
bool inner = false;
|
||||||
|
vec3 pin;
|
||||||
|
for (int k = 0; k < 64; k++){
|
||||||
|
float db = map_block(q2);
|
||||||
|
if (db < 1e-3) { inner = true; pin = q2; break; }
|
||||||
|
float dg = map_glue(q2);
|
||||||
|
if (dg > 0.004) { // left the glue: is the block behind this bit of glue?
|
||||||
|
float t2 = 0.0;
|
||||||
|
for (int j = 0; j < 40; j++){ float d2 = map_block(q2 + rd * t2); if (d2 < 1e-3) { inner = true; pin = q2 + rd * t2; break; } t2 += d2; if (t2 > 4.0) break; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
float st = clamp(min(-dg, db), 0.015, 0.2);
|
||||||
|
q2 += rd * st;
|
||||||
|
path += st;
|
||||||
|
}
|
||||||
|
vec3 sigma = vec3(2.3, 0.55, 2.7); // green passes, red/blue are absorbed
|
||||||
|
vec3 T = exp(-sigma * path * 1.2);
|
||||||
|
vec3 scatter = u_body * (0.55 + 0.45 * clamp(dot(n, L), 0.0, 1.0));
|
||||||
|
vec3 behind = vec3(0.0);
|
||||||
|
if (inner) {
|
||||||
|
vec3 nb = normalize(vec3(map_block(pin + vec3(1e-3, 0, 0)) - map_block(pin - vec3(1e-3, 0, 0)),
|
||||||
|
map_block(pin + vec3(0, 1e-3, 0)) - map_block(pin - vec3(0, 1e-3, 0)),
|
||||||
|
map_block(pin + vec3(0, 0, 1e-3)) - map_block(pin - vec3(0, 0, 1e-3))));
|
||||||
|
vec3 qb = deform(pin) - vec3(0.0, bob(), 0.0);
|
||||||
|
behind = block_albedo(qb, nb) * (0.6 + 0.35 * clamp(dot(nb, L), 0.0, 1.0));
|
||||||
|
}
|
||||||
|
float Tavg = dot(T, vec3(0.3, 0.5, 0.2));
|
||||||
|
float body = 1.0 - Tavg; // how much the slime itself is seen
|
||||||
|
c = behind * T + scatter * body;
|
||||||
|
c = mix(c, vec3(0.75, 1.0, 0.65), fres * 0.35);
|
||||||
|
c += vec3(1.0) * pow(clamp(dot(n, H), 0.0, 1.0), 120.0) * 1.4;
|
||||||
|
c += vec3(0.85, 1.0, 0.75) * pow(clamp(dot(n, H), 0.0, 1.0), 16.0) * 0.12;
|
||||||
|
// nothing solid behind: the background shows through (premultiplied, alpha < 1)
|
||||||
|
alpha = inner ? 1.0 : clamp(0.35 + 0.65 * body + 0.5 * fres, 0.0, 1.0);
|
||||||
|
if (!inner) c = c * 1.0;
|
||||||
|
} else {
|
||||||
|
float dif = clamp(dot(n, L), 0.0, 1.0);
|
||||||
|
vec3 qb = qd - vec3(0.0, bob(), 0.0);
|
||||||
|
vec3 an = abs(n);
|
||||||
|
vec3 alb = block_albedo(qb, n);
|
||||||
|
float face = an.y > max(an.x, an.z) ? 1.25 : (n.x * rt.x + n.z * rt.z < 0.0 ? 1.0 : 0.78);
|
||||||
|
c = alb * face * (0.7 + 0.4 * dif);
|
||||||
|
vec3 e = abs(qb) - 0.78;
|
||||||
|
float edge = smoothstep(-0.06, -0.02, max(min(e.x, e.y), max(min(e.y, e.z), min(e.x, e.z))));
|
||||||
|
c = mix(c, vec3(0.3, 0.9, 0.18), edge * 0.7); // the icon's bright lime edges
|
||||||
|
}
|
||||||
|
float a = cov * C2.w * alpha;
|
||||||
|
return vec4(c * cov * C2.w, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(){
|
||||||
|
vec2 s = uncam(vec2(v_uv.x, 1.0 - v_uv.y) * u_logical);
|
||||||
|
vec4 acc = vec4(0);
|
||||||
|
for (int i = 0; i < u_n; i++){
|
||||||
|
vec4 c = shade_cube(s, i);
|
||||||
|
acc = c + acc * (1.0 - c.a);
|
||||||
|
}
|
||||||
|
o = acc * u_alpha;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#version 430
|
||||||
|
#include "common.glsl"
|
||||||
|
// 2D "glue" metaballs: smooth union of circles and tapered capsules, shaded as a glossy bead.
|
||||||
|
in vec2 v_uv;
|
||||||
|
out vec4 o;
|
||||||
|
uniform vec2 u_res;
|
||||||
|
uniform vec4 u_balls[64]; // x, y, r, unused
|
||||||
|
uniform int u_nb;
|
||||||
|
uniform vec4 u_caps[24]; // ax, ay, bx, by
|
||||||
|
uniform vec4 u_capr[24]; // r0, r1
|
||||||
|
uniform int u_nc;
|
||||||
|
uniform float u_k; // smooth-union radius (px)
|
||||||
|
uniform vec4 u_col; // body colour (linear)
|
||||||
|
uniform vec4 u_rim; // outline colour
|
||||||
|
uniform vec4 u_hi; // specular colour
|
||||||
|
uniform float u_outline; // outline width (px)
|
||||||
|
uniform float u_bevel; // bevel width (px)
|
||||||
|
uniform float u_glow; // emissive inner glow amount
|
||||||
|
|
||||||
|
float smin(float a, float b, float k){
|
||||||
|
float h = max(k - abs(a - b), 0.0) / k;
|
||||||
|
return min(a, b) - h * h * k * 0.25;
|
||||||
|
}
|
||||||
|
float field(vec2 p){
|
||||||
|
float d = 1e5;
|
||||||
|
for (int i = 0; i < u_nb; i++) d = smin(d, length(p - u_balls[i].xy) - u_balls[i].z, u_k);
|
||||||
|
for (int i = 0; i < u_nc; i++){
|
||||||
|
vec2 a = u_caps[i].xy, b = u_caps[i].zw;
|
||||||
|
vec2 pa = p - a, ba = b - a;
|
||||||
|
float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-4), 0.0, 1.0);
|
||||||
|
d = smin(d, length(pa - ba * h) - mix(u_capr[i].x, u_capr[i].y, h), u_k);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
float height(float d){ float u = 1.0 - clamp(-d / u_bevel, 0.0, 1.0); return sqrt(max(1.0 - u * u, 0.0)); } // dome
|
||||||
|
void main(){
|
||||||
|
vec2 s = vec2(v_uv.x, 1.0 - v_uv.y) * u_logical;
|
||||||
|
vec2 p = uncam(s);
|
||||||
|
float d = field(p);
|
||||||
|
float px = u_logical.x / u_res.x / u_cam.z;
|
||||||
|
if (d > 2.0 * px) { o = vec4(0); return; }
|
||||||
|
// wide central differences: smooths the creases a distance field has along its medial axes
|
||||||
|
float e = max(1.5, 0.14 * u_bevel);
|
||||||
|
float hx = 0.0, hy = 0.0;
|
||||||
|
for (int k = 0; k < 2; k++){
|
||||||
|
float ee = e * (k == 0 ? 1.0 : 0.5);
|
||||||
|
hx += (height(field(p + vec2(ee, 0))) - height(field(p - vec2(ee, 0)))) / ee;
|
||||||
|
hy += (height(field(p + vec2(0, ee))) - height(field(p - vec2(0, ee)))) / ee;
|
||||||
|
}
|
||||||
|
vec3 n = normalize(vec3(-hx * u_bevel * 0.175, -hy * u_bevel * 0.175, 1.0));
|
||||||
|
vec3 L = normalize(vec3(-0.55, -0.75, 0.75));
|
||||||
|
float dif = clamp(dot(n, L), 0.0, 1.0);
|
||||||
|
vec3 H = normalize(L + vec3(0, 0, 1));
|
||||||
|
float spec = pow(clamp(dot(n, H), 0.0, 1.0), 70.0);
|
||||||
|
vec3 c = u_col.rgb * (0.45 + 0.65 * dif);
|
||||||
|
c *= 0.8 + 0.3 * clamp(0.5 - 0.5 * (n.x + n.y) * 1.6, 0.0, 1.0) + 0.0; // lit from the top-left, thick body
|
||||||
|
c += u_col.rgb * u_glow * height(d);
|
||||||
|
float rim = 1.0 - smoothstep(u_outline - px, u_outline + px, -d);
|
||||||
|
c = mix(c, u_rim.rgb, rim * u_rim.a);
|
||||||
|
c += u_hi.rgb * spec * 1.4;
|
||||||
|
float a = clamp(0.5 - d / px, 0.0, 1.0) * u_col.a;
|
||||||
|
o = vec4(c * a, a) * u_alpha;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""MobileGlues mark sting: the block drops into a blob of glue, which splashes, erupts behind it and settles.
|
||||||
|
render: python render.py video 0 4.5 --module sting --audio out/sting.wav --samples 8 --out out/glue_drop.mp4"""
|
||||||
|
import math
|
||||||
|
from anim import *
|
||||||
|
from kit import cubes, bg
|
||||||
|
from engine import LW, LH, hexc
|
||||||
|
|
||||||
|
END = 4.5
|
||||||
|
T_HIT = 1.1
|
||||||
|
|
||||||
|
|
||||||
|
def draw(E, t):
|
||||||
|
E.begin_sub(MINT)
|
||||||
|
f = Frame(t, 0)
|
||||||
|
bg(E, MINT)
|
||||||
|
u = t - T_HIT
|
||||||
|
zoom = 1.0 + (0.03 * math.exp(-u * 3) * math.sin(u * 20) if u > 0 else 0.0)
|
||||||
|
E.cam = (LW / 2, LH / 2, zoom, 0.0)
|
||||||
|
cubes(E, f, [(960, 560, 330, 0.0, 0.0, 0.0, 0.0, 1.0, u)])
|
||||||
|
fade = clamp01((t - (END - 0.4)) / 0.4)
|
||||||
|
return {'bloom': 0.2, 'grain': 0.02, 'vignette': 0.22, 'flash': (*MINT[:3], fade)}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Sound for the glue-drop sting: a falling whoosh, a wet thump, splash plinks, tongues popping, a soft chime."""
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import soundfile as sf
|
||||||
|
from dsp import SR, sine, svf, saw, compress, limit
|
||||||
|
from music import reverb_ir, conv
|
||||||
|
import music as M
|
||||||
|
import sting
|
||||||
|
|
||||||
|
N = int(sting.END * SR)
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
L = np.zeros(N)
|
||||||
|
R = np.zeros(N)
|
||||||
|
|
||||||
|
|
||||||
|
def put(t, x, pan=0.0, g=1.0):
|
||||||
|
i0 = int(t * SR)
|
||||||
|
n = min(len(x), N - i0)
|
||||||
|
a = (pan + 1) * np.pi / 4
|
||||||
|
L[i0:i0 + n] += x[:n] * np.cos(a) * 1.414 * g
|
||||||
|
R[i0:i0 + n] += x[:n] * np.sin(a) * 1.414 * g
|
||||||
|
|
||||||
|
|
||||||
|
def tt(n):
|
||||||
|
return np.arange(n) / SR
|
||||||
|
|
||||||
|
|
||||||
|
def bloop(f0, f1, dur, v):
|
||||||
|
n = int(dur * SR)
|
||||||
|
t = tt(n)
|
||||||
|
f = f1 + (f0 - f1) * np.exp(-t / (dur * 0.25))
|
||||||
|
return sine(f, 0.0) * np.minimum(t / 0.004, 1) * np.exp(-t / (dur * 0.35)) * v
|
||||||
|
|
||||||
|
|
||||||
|
hit = sting.T_HIT
|
||||||
|
# whoosh while it falls
|
||||||
|
n = int(0.5 * SR)
|
||||||
|
t = tt(n)
|
||||||
|
nz = svf(rng.standard_normal(n), 400 + 3000 * (t / 0.5) ** 2, 0.5, 1)
|
||||||
|
put(hit - 0.5, nz * (t / 0.5) ** 2 * 0.25)
|
||||||
|
# impact: low thump + wet splat
|
||||||
|
n = int(0.9 * SR)
|
||||||
|
t = tt(n)
|
||||||
|
thump = sine(45 + 120 * np.exp(-t / 0.05), 0.0) * np.exp(-t / 0.25)
|
||||||
|
splat = svf(rng.standard_normal(n), 2500 * np.exp(-t / 0.08) + 200, 0.3, 0) * np.exp(-t / 0.12)
|
||||||
|
put(hit, np.tanh(1.5 * thump) * 0.9 + splat * 0.7)
|
||||||
|
put(hit, bloop(520, 140, 0.35, 0.45))
|
||||||
|
# droplets landing around
|
||||||
|
for k in range(9):
|
||||||
|
tk = hit + 0.25 + 0.06 * k + 0.05 * rng.random()
|
||||||
|
put(tk, bloop(1400 + 600 * rng.random(), 700, 0.09, 0.14), pan=rng.uniform(-0.8, 0.8))
|
||||||
|
# tongues popping up, rising
|
||||||
|
notes = [392, 440, 523.25, 587.33, 659.25]
|
||||||
|
for k, fz in enumerate(notes):
|
||||||
|
put(hit + 0.12 + 0.07 * k, bloop(fz * 1.5, fz, 0.2, 0.28), pan=-0.5 + 0.25 * k)
|
||||||
|
# settle chime: soft FM bells, C add9
|
||||||
|
for k, m in enumerate([60, 64, 67, 74, 79]):
|
||||||
|
put(hit + 0.75 + 0.02 * k, M.bell(m, 1.4, 0.16), pan=-0.3 + 0.15 * k)
|
||||||
|
ir = reverb_ir(1.8, 6000, 3)
|
||||||
|
M.N = N
|
||||||
|
rl, rr = conv(L, R, ir)
|
||||||
|
L2, R2 = L + rl * 0.35, R + rr * 0.35
|
||||||
|
L2, R2 = limit(L2, R2, 0.89, 0.004, 0.05)
|
||||||
|
peak = max(abs(L2).max(), abs(R2).max())
|
||||||
|
g = 0.85 / peak
|
||||||
|
sf.write(sys.argv[1], np.stack([L2 * g, R2 * g], 1).astype(np.float32), SR, subtype='PCM_24')
|
||||||
|
print('ok', peak)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# MobileGL intro — paper-cutout video
|
||||||
|
|
||||||
|
A ~4:50 explainer of what MobileGL is and how it works, in a handmade paper-cutout / origami style:
|
||||||
|
hand-cut shapes with paper grain and layered shadows, stop-motion "boil" at 12 fps, torn-paper wipes, a
|
||||||
|
procedural ukulele-and-marimba soundtrack, burned-in captions plus `.srt` files. English and Chinese versions are
|
||||||
|
rendered from the same code.
|
||||||
|
|
||||||
|
Everything is plain Python on the CPU (Pillow + numpy + scipy), deterministic, and rendered in parallel worker
|
||||||
|
processes.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| file | what |
|
||||||
|
|---|---|
|
||||||
|
| `paper.py` | the paper engine: palette, fonts and mixed Latin/CJK text runs, hand-cut polygon roughening, periodic paper grain, sprites with inner/drop shadows, stop-motion jitter, torn-paper wipes, translation lookup (`T()`) |
|
||||||
|
| `video.py` | the twelve scenes (title, the problem, meet MobileGL, why it works, frontend, shaders, records, backends, anywhere, perks, platforms, outro), their captions, and the `stills` / `meta` / `render` entry points |
|
||||||
|
| `music.py` | the soundtrack: Karplus-Strong ukulele, marimba, bass and light percussion (120 BPM, I-V-vi-IV in C), plus paper swooshes on wipes; reads the scene timings written by `video.py meta` |
|
||||||
|
| `zh.py` | the Chinese strings (`MGL_LANG=zh`) |
|
||||||
|
| `cover.py` | a 16:9 thumbnail whose centred 4:3 crop keeps everything important |
|
||||||
|
| `fetch_fonts.py` | downloads the fonts into `fonts/` |
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python fetch_fonts.py # Fredoka, Patrick Hand, ZCOOL KuaiLe, LXGW WenKai, Cascadia Mono (all SIL OFL)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ffmpeg` must be on `PATH`.
|
||||||
|
|
||||||
|
## Render
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python video.py stills 3.0 45.5 # spot-check frames -> out/still_*.png
|
||||||
|
python video.py render ffmpeg # out/video_noaudio.mp4 + out/mobilegl_intro.json/.srt (scene timings)
|
||||||
|
python music.py out/mobilegl_intro.json out/music.wav
|
||||||
|
ffmpeg -i out/video_noaudio.mp4 -i out/music.wav -c:v copy -c:a aac -b:a 192k -shortest out/mobilegl_intro.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Chinese version: set `MGL_LANG=zh` for the `video.py` and `cover.py` steps; output goes to `out_zh/`
|
||||||
|
(`video_noaudio_zh.mp4`, `mobilegl_intro_zh.srt`). The scene timings are identical, so the same `music.wav` is used:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MGL_LANG=zh python video.py render ffmpeg
|
||||||
|
ffmpeg -i out_zh/video_noaudio_zh.mp4 -i out/music.wav -c:v copy -c:a aac -b:a 192k -shortest out_zh/mobilegl_intro_zh.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Cover: `python cover.py` writes `out/cover.png` and a 1280×720 JPEG; `--guides` also writes a preview with the
|
||||||
|
4:3 crop lines and the cropped image.
|
||||||
|
|
||||||
|
The full render uses every core but two; expect a few minutes per language.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Cover / thumbnail for the MobileGL intro: one 16:9 image whose centre 4:3 crop keeps everything
|
||||||
|
important; the side bands only carry decoration. MGL_LANG=zh for the Chinese cover."""
|
||||||
|
from video import *
|
||||||
|
import paper
|
||||||
|
|
||||||
|
paper.TR.update({
|
||||||
|
'Desktop OpenGL, anywhere': '桌面 OpenGL,处处可用',
|
||||||
|
'How it works, in 5 minutes': '5 分钟看懂它如何工作',
|
||||||
|
} if LANG == 'zh' else {})
|
||||||
|
|
||||||
|
SAFE_W = H * 4 // 3 # 1440: width kept by a centred 4:3 crop
|
||||||
|
OX = (W - SAFE_W) // 2 # 240: left edge of the safe area
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def spr_big_tile(ch, color, k=1.22):
|
||||||
|
w, h = int(160 * k), int(180 * k)
|
||||||
|
a = Art(w, h, f'bigtile|{ch}|{color}|{k}')
|
||||||
|
c = 46 * k
|
||||||
|
a.poly([(0, 14 * k), (14 * k, 0), (w - c, 0), (w, c), (w, h - 14 * k), (w - 14 * k, h), (14 * k, h),
|
||||||
|
(0, h - 14 * k)], color, rough=0.9)
|
||||||
|
a.poly([(w - c, 0), (w - c, c), (w, c)], shade(color, 1.3), rough=0.4)
|
||||||
|
a.text(w / 2 - 2, h / 2 + 4, ch, ('fredoka', int(122 * k), 'Bold'), WHITE, shade=True)
|
||||||
|
return a.build()
|
||||||
|
|
||||||
|
|
||||||
|
def put(c, spr, x, y, rot=0.0, s=1.0, elev=1.2):
|
||||||
|
c.put(spr, x, y, rot, s, None, 1, elev, boil=0)
|
||||||
|
|
||||||
|
|
||||||
|
def side_decor(c):
|
||||||
|
"""Decoration confined to the two bands outside the 4:3 crop (x < 240, x > 1680)."""
|
||||||
|
put(c, spr_plane(MUSTARD), 125, 330, 14, 0.8, 3.0)
|
||||||
|
put(c, spr_plane(CORAL), 110, 790, -10, 0.7, 3.0)
|
||||||
|
put(c, spr_crane(CORAL), 1800, 300, -6, 0.5, 3.0)
|
||||||
|
put(c, spr_plane(TEAL), 1805, 770, -16, 0.75, 3.0)
|
||||||
|
rng = random.Random(7)
|
||||||
|
for band in ((25, OX - 25), (OX + SAFE_W + 25, W - 25)):
|
||||||
|
n = 0
|
||||||
|
while n < 7:
|
||||||
|
x, y = rng.uniform(*band), rng.uniform(60, H - 60)
|
||||||
|
size = rng.randrange(24, 46)
|
||||||
|
if not (band[0] + size / 2 < x < band[1] - size / 2):
|
||||||
|
continue
|
||||||
|
if any(abs(y - yy) < 150 for yy in (330, 790, 300, 770)) and abs(x - (band[0] + band[1]) / 2) < 90:
|
||||||
|
continue # keep clear of the planes / crane
|
||||||
|
put(c, spr_confetti(rng.randrange(4), rng.choice((CORAL, MUSTARD, TEAL, BLUE, PLUM, ORANGE)), size),
|
||||||
|
x, y, rng.uniform(-40, 40), 1, 0.6)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
|
||||||
|
def safe_content(c):
|
||||||
|
"""Everything that matters, laid out inside the centre 1440x1080."""
|
||||||
|
cx = W / 2
|
||||||
|
rng = random.Random(43)
|
||||||
|
n = 0
|
||||||
|
while n < 12: # a few confetti pieces inside the safe area's own edges
|
||||||
|
x, y = rng.uniform(OX + 40, OX + SAFE_W - 40), rng.uniform(40, H - 40)
|
||||||
|
if OX + 90 < x < OX + SAFE_W - 90 and 90 < y < H - 60:
|
||||||
|
continue
|
||||||
|
put(c, spr_confetti(rng.randrange(4), rng.choice((CORAL, MUSTARD, TEAL, BLUE, PLUM, ORANGE)),
|
||||||
|
rng.randrange(24, 46)), x, y, rng.uniform(-40, 40), 1, 0.6)
|
||||||
|
n += 1
|
||||||
|
step = 166
|
||||||
|
for i, ch in enumerate('MobileGL'):
|
||||||
|
put(c, spr_big_tile(ch, TITLE_COLS[i], 0.98), cx - step * 3.5 + step * i, 240 + (5 if i % 2 else -5),
|
||||||
|
(3.5 if i % 2 else -3.5), 1, 1.3)
|
||||||
|
put(c, spr_label('Desktop OpenGL, anywhere', ('fredoka', 64, 'SemiBold'), WHITE, INK, padx=44, pady=20),
|
||||||
|
cx, 465, -1.5, 1, 1.4)
|
||||||
|
put(c, spr_label('How it works, in 5 minutes', ('hand', 42), CORAL, WHITE, padx=28, pady=10),
|
||||||
|
cx, 565, 2, 1, 1.4)
|
||||||
|
y = 835
|
||||||
|
put(c, spr_appwin(), OX + 225, y, -4, 0.5)
|
||||||
|
put(c, spr_arrow(90, INK, 18, 40), OX + 445, y, 0, 1, 0.6)
|
||||||
|
put(c, spr_crane(BRAND), OX + 655, y - 45, -4, 0.85, 3.5)
|
||||||
|
put(c, spr_slab('MobileGL', 230, BRAND, h=64, size=36), OX + 655, y + 75, 2)
|
||||||
|
put(c, spr_arrow(90, INK, 18, 40), OX + 870, y, 0, 1, 0.6)
|
||||||
|
put(c, spr_phone(BG_PEACH), OX + 1035, y + 10, 4, 0.5)
|
||||||
|
put(c, spr_tablet(BG_MINT), OX + 1250, y + 25, -3, 0.52)
|
||||||
|
put(c, spr_bubble('OpenGL ES', ('fredoka', 36, 'SemiBold'), TEAL, WHITE), OX + 1020, y - 140, -3, 1, 1.5)
|
||||||
|
put(c, spr_bubble('Vulkan', ('fredoka', 36, 'SemiBold'), PLUM, WHITE, 'br'), OX + 1285, y - 115, 3, 1, 1.5)
|
||||||
|
put(c, spr_sticker('OpenGL\n4.6 Core', MUSTARD, r=80, size=29, fg=INK), OX + 1310, 462, 12, 1, 1.6)
|
||||||
|
|
||||||
|
|
||||||
|
def cover():
|
||||||
|
frame = background(BG_CREAM).copy()
|
||||||
|
c = Ctx(frame, 100.0, 0, 1)
|
||||||
|
side_decor(c)
|
||||||
|
safe_content(c)
|
||||||
|
return frame.convert('RGB')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
suffix = '' if LANG == 'en' else '_' + LANG
|
||||||
|
img = cover()
|
||||||
|
img.save(os.path.join(OUT, f'cover{suffix}.png'))
|
||||||
|
img.resize((1280, 720), Image.LANCZOS).save(os.path.join(OUT, f'cover{suffix}_1280x720.jpg'), quality=92)
|
||||||
|
if '--guides' in sys.argv: # preview with the 4:3 crop lines drawn in
|
||||||
|
g = img.copy()
|
||||||
|
d = ImageDraw.Draw(g)
|
||||||
|
for x in (OX, OX + SAFE_W):
|
||||||
|
d.line([(x, 0), (x, H)], fill=(220, 0, 0), width=4)
|
||||||
|
g.resize((1280, 720), Image.LANCZOS).save(os.path.join(OUT, f'cover{suffix}_guides.jpg'), quality=88)
|
||||||
|
img.crop((OX, 0, OX + SAFE_W, H)).resize((960, 720), Image.LANCZOS).save(
|
||||||
|
os.path.join(OUT, f'cover{suffix}_crop43.jpg'), quality=88)
|
||||||
|
print('ok')
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""Download the fonts the paper-cutout video uses into ./fonts (all SIL OFL 1.1). Run once before rendering."""
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
GF = 'https://github.com/google/fonts/raw/main/ofl/'
|
||||||
|
FONTS = {
|
||||||
|
'Fredoka.ttf': GF + 'fredoka/Fredoka%5Bwdth,wght%5D.ttf',
|
||||||
|
'PatrickHand.ttf': GF + 'patrickhand/PatrickHand-Regular.ttf',
|
||||||
|
'ZCOOLKuaiLe.ttf': GF + 'zcoolkuaile/ZCOOLKuaiLe-Regular.ttf',
|
||||||
|
'LXGWWenKai.ttf': 'https://github.com/lxgw/LxgwWenKai/releases/download/v1.522/LXGWWenKai-Regular.ttf',
|
||||||
|
}
|
||||||
|
CASCADIA = 'https://github.com/microsoft/cascadia-code/releases/download/v2407.24/CascadiaCode-2407.24.zip'
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url):
|
||||||
|
with urllib.request.urlopen(url) as r:
|
||||||
|
return r.read()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
out = os.path.join(HERE, 'fonts')
|
||||||
|
os.makedirs(out, exist_ok=True)
|
||||||
|
for name, url in FONTS.items():
|
||||||
|
path = os.path.join(out, name)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print('fetching', name)
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(fetch(url))
|
||||||
|
path = os.path.join(out, 'CascadiaMono.ttf')
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print('fetching CascadiaMono.ttf')
|
||||||
|
with zipfile.ZipFile(io.BytesIO(fetch(CASCADIA))) as z, open(path, 'wb') as f:
|
||||||
|
f.write(z.read('ttf/CascadiaMono.ttf'))
|
||||||
|
print('fonts ready in', out)
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
"""Procedural upbeat ukulele/marimba track (120 BPM, C major, I-V-vi-IV) + paper SFX.
|
||||||
|
Usage: python music.py out/mobilegl_intro.json out/music.wav"""
|
||||||
|
import sys, json, functools
|
||||||
|
import numpy as np
|
||||||
|
from scipy import signal
|
||||||
|
from scipy.io import wavfile
|
||||||
|
|
||||||
|
SR = 44100
|
||||||
|
BPM = 120
|
||||||
|
BEAT = 60 / BPM
|
||||||
|
BAR = 4 * BEAT
|
||||||
|
rng_global = np.random.default_rng(42)
|
||||||
|
|
||||||
|
|
||||||
|
def hz(m):
|
||||||
|
return 440.0 * 2 ** ((m - 69) / 12)
|
||||||
|
|
||||||
|
|
||||||
|
def N(s):
|
||||||
|
names = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
|
||||||
|
return 12 * (int(s[-1]) + 1) + names[s[0]] + (1 if '#' in s else 0)
|
||||||
|
|
||||||
|
|
||||||
|
def env_ad(n, a=0.002, release=0.02):
|
||||||
|
e = np.ones(n)
|
||||||
|
na, nr = max(1, int(a * SR)), max(1, int(release * SR))
|
||||||
|
e[:na] = np.linspace(0, 1, na)
|
||||||
|
e[-nr:] *= np.linspace(1, 0, nr)
|
||||||
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- instruments
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def pluck(m, dur=1.4):
|
||||||
|
f = hz(m)
|
||||||
|
P = int(SR / f)
|
||||||
|
total = int(dur * SR)
|
||||||
|
r = np.random.default_rng(m)
|
||||||
|
y = np.zeros(total + 1)
|
||||||
|
burst = r.uniform(-1, 1, P)
|
||||||
|
burst = np.convolve(burst, [0.3, 0.4, 0.3], 'same')
|
||||||
|
burst -= burst.mean()
|
||||||
|
y[1:P + 1] = burst
|
||||||
|
k = P + 1
|
||||||
|
while k < total + 1:
|
||||||
|
e = min(k + P, total + 1)
|
||||||
|
y[k:e] = 0.4985 * (y[k - P:e - P] + y[k - P - 1:e - P - 1])
|
||||||
|
k = e
|
||||||
|
y = y[1:]
|
||||||
|
y *= env_ad(total, 0.001, 0.08)
|
||||||
|
b, a = signal.butter(2, min(0.99, 5200 / (SR / 2)))
|
||||||
|
return signal.lfilter(b, a, y) / (np.abs(y).max() + 1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def mallet(m, dur=0.7):
|
||||||
|
f = hz(m)
|
||||||
|
t = np.arange(int(dur * SR)) / SR
|
||||||
|
y = (np.sin(2 * np.pi * f * t) * np.exp(-t * 5.5)
|
||||||
|
+ 0.32 * np.sin(2 * np.pi * 3.93 * f * t) * np.exp(-t * 22)
|
||||||
|
+ 0.10 * np.sin(2 * np.pi * 9.2 * f * t) * np.exp(-t * 55))
|
||||||
|
return y * env_ad(len(t), 0.0015, 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def bass(m, dur):
|
||||||
|
f = hz(m)
|
||||||
|
t = np.arange(int(dur * SR)) / SR
|
||||||
|
y = np.sin(2 * np.pi * f * t) + 0.35 * np.sin(2 * np.pi * 2 * f * t) + 0.15 * np.sin(2 * np.pi * 3 * f * t)
|
||||||
|
e = (1 - np.exp(-t * 300)) * np.exp(-t * 3.0)
|
||||||
|
return y * e * env_ad(len(t), 0.001, 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def kick():
|
||||||
|
t = np.arange(int(0.35 * SR)) / SR
|
||||||
|
f = 48 + 110 * np.exp(-t * 32)
|
||||||
|
ph = 2 * np.pi * np.cumsum(f) / SR
|
||||||
|
y = np.sin(ph) * np.exp(-t * 11)
|
||||||
|
y[:int(0.004 * SR)] += np.random.default_rng(1).uniform(-0.4, 0.4, int(0.004 * SR))
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def _band(x, lo, hi, order=2):
|
||||||
|
b, a = signal.butter(order, [lo / (SR / 2), min(0.99, hi / (SR / 2))], 'band')
|
||||||
|
return signal.lfilter(b, a, x)
|
||||||
|
|
||||||
|
|
||||||
|
def _hp(x, fc):
|
||||||
|
b, a = signal.butter(2, fc / (SR / 2), 'high')
|
||||||
|
return signal.lfilter(b, a, x)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def clap():
|
||||||
|
n = int(0.25 * SR)
|
||||||
|
t = np.arange(n) / SR
|
||||||
|
r = np.random.default_rng(2)
|
||||||
|
y = np.zeros(n)
|
||||||
|
for k, d in enumerate([0, 0.009, 0.018, 0.027]):
|
||||||
|
s = int(d * SR)
|
||||||
|
dec = 120 if k < 3 else 22
|
||||||
|
y[s:] += r.uniform(-1, 1, n - s) * np.exp(-(t[:n - s]) * dec)
|
||||||
|
y = _band(y, 900, 3200)
|
||||||
|
return y / np.abs(y).max()
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def hat(open_=False):
|
||||||
|
n = int((0.18 if open_ else 0.06) * SR)
|
||||||
|
t = np.arange(n) / SR
|
||||||
|
y = _hp(np.random.default_rng(3).uniform(-1, 1, n), 7000) * np.exp(-t * (18 if open_ else 70))
|
||||||
|
return y / np.abs(y).max()
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def shaker():
|
||||||
|
n = int(0.09 * SR)
|
||||||
|
t = np.arange(n) / SR
|
||||||
|
e = np.minimum(1, t / 0.02) * np.exp(-t * 40)
|
||||||
|
y = _hp(np.random.default_rng(4).uniform(-1, 1, n), 5000) * e
|
||||||
|
return y / np.abs(y).max()
|
||||||
|
|
||||||
|
|
||||||
|
def swoosh(seed):
|
||||||
|
n = int(0.75 * SR)
|
||||||
|
t = np.arange(n) / SR
|
||||||
|
r = np.random.default_rng(seed)
|
||||||
|
x = r.uniform(-1, 1, n)
|
||||||
|
e = np.sin(np.pi * np.clip(t / 0.75, 0, 1)) ** 2.2
|
||||||
|
y = _band(x, 500, 5000) * e
|
||||||
|
crinkle = np.zeros(n)
|
||||||
|
for _ in range(26):
|
||||||
|
s = int(r.uniform(0.05, 0.6) * SR)
|
||||||
|
L = int(0.004 * SR)
|
||||||
|
crinkle[s:s + L] += r.uniform(-1, 1, L) * r.uniform(0.3, 1)
|
||||||
|
y += 0.5 * _hp(crinkle, 2500)
|
||||||
|
return y / np.abs(y).max()
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def tok():
|
||||||
|
t = np.arange(int(0.08 * SR)) / SR
|
||||||
|
f = 700 * np.exp(-t * 12) + 260
|
||||||
|
return np.sin(2 * np.pi * np.cumsum(f) / SR) * np.exp(-t * 45)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- score
|
||||||
|
CHORDS = [ # name, uke voicing (G C E A strings), bass root, chord tones for arps
|
||||||
|
('C', [N('G4'), N('C4'), N('E4'), N('C5')], N('C2'), [N('C5'), N('E5'), N('G5'), N('C6')]),
|
||||||
|
('G', [N('G4'), N('D4'), N('G4'), N('B4')], N('G1'), [N('B4'), N('D5'), N('G5'), N('B5')]),
|
||||||
|
('Am', [N('A4'), N('C4'), N('E4'), N('A4')], N('A1'), [N('C5'), N('E5'), N('A5'), N('C6')]),
|
||||||
|
('F', [N('A4'), N('C4'), N('F4'), N('A4')], N('F1'), [N('C5'), N('F5'), N('A5'), N('C6')]),
|
||||||
|
]
|
||||||
|
MEL_A = [
|
||||||
|
[(0, 'E5', .5), (1, 'G5', .5), (2, 'A5', .5), (2.5, 'G5', .5), (3, 'E5', 1)],
|
||||||
|
[(0, 'D5', .5), (1, 'G5', .5), (1.5, 'B5', .5), (2, 'D6', 1), (3, 'B5', .5), (3.5, 'G5', .5)],
|
||||||
|
[(0, 'C6', .5), (0.5, 'B5', .5), (1, 'A5', 1), (2, 'E5', .5), (3, 'A5', .5), (3.5, 'C6', .5)],
|
||||||
|
[(0, 'A5', 1.5), (1.5, 'G5', .5), (2, 'F5', .5), (2.5, 'E5', .5), (3, 'D5', 1)],
|
||||||
|
]
|
||||||
|
MEL_B = [
|
||||||
|
[(0, 'G5', .5), (0.5, 'G5', .5), (1, 'C6', 1), (2, 'E6', .5), (2.5, 'D6', .5), (3, 'C6', 1)],
|
||||||
|
[(0, 'B5', .5), (1, 'D6', .5), (1.5, 'B5', .5), (2, 'G5', 1), (3, 'A5', .5), (3.5, 'B5', .5)],
|
||||||
|
[(0, 'C6', 1), (1, 'E6', .5), (1.5, 'D6', .5), (2, 'C6', .5), (2.5, 'A5', .5), (3, 'E5', 1)],
|
||||||
|
[(0, 'F5', .5), (0.5, 'A5', .5), (1, 'C6', 1), (2, 'A5', .5), (2.5, 'G5', .5), (3, 'G5', 1)],
|
||||||
|
]
|
||||||
|
STRUM = [(0, 'D'), (1, 'D'), (1.5, 'U'), (2.5, 'U'), (3, 'D'), (3.5, 'U')]
|
||||||
|
BASS = [(0, 0, .45), (1, 0, .2), (1.5, 12, .2), (2, 0, .45), (3, 7, .3), (3.5, 12, .2)]
|
||||||
|
|
||||||
|
|
||||||
|
class Mix:
|
||||||
|
def __init__(self, seconds):
|
||||||
|
self.n = int(seconds * SR) + SR * 3
|
||||||
|
self.buses = {k: np.zeros((self.n, 2)) for k in ('dry', 'wet')}
|
||||||
|
|
||||||
|
def add(self, x, t, gain=1.0, pan=0.0, bus='dry'):
|
||||||
|
s = int(t * SR)
|
||||||
|
if s >= self.n:
|
||||||
|
return
|
||||||
|
x = x[:self.n - s]
|
||||||
|
l, r = np.cos((pan + 1) * np.pi / 4), np.sin((pan + 1) * np.pi / 4)
|
||||||
|
self.buses[bus][s:s + len(x), 0] += x * gain * l * 1.414
|
||||||
|
self.buses[bus][s:s + len(x), 1] += x * gain * r * 1.414
|
||||||
|
|
||||||
|
|
||||||
|
def section(bar, last):
|
||||||
|
if bar >= last:
|
||||||
|
return 'end'
|
||||||
|
if bar < 2:
|
||||||
|
return 'intro0'
|
||||||
|
if bar < 8:
|
||||||
|
return 'intro'
|
||||||
|
if 64 <= bar < 68 or 112 <= bar < 116:
|
||||||
|
return 'break'
|
||||||
|
return 'mel' if ((bar - 8) // 16) % 2 == 0 else 'arp'
|
||||||
|
|
||||||
|
|
||||||
|
def build(meta, out):
|
||||||
|
total = meta['total']
|
||||||
|
mix = Mix(total)
|
||||||
|
last = int((total - 3.6) // BAR) # final hit bar
|
||||||
|
r = np.random.default_rng(9)
|
||||||
|
for bar in range(last + 1):
|
||||||
|
t0 = bar * BAR
|
||||||
|
sec = section(bar, last)
|
||||||
|
name, uke, root, tones = CHORDS[bar % 4]
|
||||||
|
if sec == 'end':
|
||||||
|
for k, m in enumerate(uke):
|
||||||
|
mix.add(pluck(m, 3.0), t0 + 0.012 * k, 0.42, -0.25, 'wet')
|
||||||
|
mix.add(mallet(N('C6'), 2.0), t0, 0.45, 0.2, 'wet')
|
||||||
|
mix.add(mallet(N('G5'), 2.0), t0 + 0.06, 0.3, 0.2, 'wet')
|
||||||
|
mix.add(bass(N('C2'), 2.0), t0, 0.55)
|
||||||
|
mix.add(kick(), t0, 0.7)
|
||||||
|
mix.add(hat(True), t0, 0.25, 0.3)
|
||||||
|
break
|
||||||
|
drums = sec in ('intro', 'mel', 'arp')
|
||||||
|
# ukulele strums
|
||||||
|
for b, d in STRUM:
|
||||||
|
order = uke if d == 'D' else uke[::-1]
|
||||||
|
vel = (0.36 if d == 'D' else 0.24) * (0.8 if sec == 'break' else 1)
|
||||||
|
for k, m in enumerate(order):
|
||||||
|
mix.add(pluck(m), t0 + b * BEAT + 0.011 * k + r.uniform(0, 0.004), vel * r.uniform(0.85, 1.05),
|
||||||
|
-0.25, 'wet')
|
||||||
|
# bass
|
||||||
|
if sec != 'intro0':
|
||||||
|
for b, off, d in BASS:
|
||||||
|
g = 0.42 if sec != 'break' else 0.3
|
||||||
|
mix.add(bass(root + off, round(d * BEAT * 2, 3)), t0 + b * BEAT, g)
|
||||||
|
# drums
|
||||||
|
if drums:
|
||||||
|
for b in (0, 2):
|
||||||
|
mix.add(kick(), t0 + b * BEAT, 0.62)
|
||||||
|
if sec != 'intro' or bar >= 4:
|
||||||
|
for b in (1, 3):
|
||||||
|
mix.add(clap(), t0 + b * BEAT, 0.26, 0.05, 'wet')
|
||||||
|
for e in range(8):
|
||||||
|
mix.add(hat(open_=(e == 7 and bar % 4 == 3)), t0 + e * BEAT / 2,
|
||||||
|
0.045 if e % 2 == 0 else 0.075, 0.3)
|
||||||
|
if sec in ('mel', 'arp'):
|
||||||
|
for s in range(16):
|
||||||
|
mix.add(shaker(), t0 + s * BEAT / 4, 0.045 if s % 2 else 0.025, -0.35)
|
||||||
|
if bar % 8 == 7: # little fill
|
||||||
|
mix.add(kick(), t0 + 3.5 * BEAT, 0.45)
|
||||||
|
mix.add(clap(), t0 + 3.75 * BEAT, 0.16, 0.05, 'wet')
|
||||||
|
if sec == 'break':
|
||||||
|
for s in range(8):
|
||||||
|
mix.add(shaker(), t0 + s * BEAT / 2, 0.05, -0.35)
|
||||||
|
# melody / arps
|
||||||
|
if sec == 'mel' or sec == 'break' and bar % 4 >= 2:
|
||||||
|
phrase = MEL_A if ((bar - 8) // 4) % 2 == 0 else MEL_B
|
||||||
|
for b, note, d in phrase[bar % 4]:
|
||||||
|
g = 0.3 if sec == 'mel' else 0.2
|
||||||
|
mix.add(mallet(N(note), max(0.4, d * BEAT * 1.6)), t0 + b * BEAT, g, 0.2, 'wet')
|
||||||
|
if sec == 'mel' and ((bar - 8) // 16) >= 2: # later: octave sparkle doubling
|
||||||
|
mix.add(mallet(N(note) + 12, 0.4), t0 + b * BEAT, 0.08, 0.35, 'wet')
|
||||||
|
if sec == 'arp':
|
||||||
|
pat = [0, 1, 2, 3, 2, 1, 2, 3, 0, 1, 2, 3, 2, 3, 1, 2]
|
||||||
|
for s, i in enumerate(pat):
|
||||||
|
mix.add(mallet(tones[i], 0.35), t0 + s * BEAT / 4, 0.13 if s % 4 else 0.18, 0.35, 'wet')
|
||||||
|
if bar % 2 == 0: # simple call-back motif
|
||||||
|
for b, note in [(0, tones[3] + 0), (0.5, tones[2]), (1, tones[3])]:
|
||||||
|
mix.add(mallet(note + 12 if note < N('C6') else note, 0.5), t0 + b * BEAT, 0.12, -0.1, 'wet')
|
||||||
|
# SFX
|
||||||
|
for k, w in enumerate(meta['wipes']):
|
||||||
|
mix.add(swoosh(100 + k), w - 0.05, 0.2, 0.4 - 0.8 * (k % 2))
|
||||||
|
for k, p in enumerate(meta['pops']):
|
||||||
|
mix.add(tok(), p + 0.35, 0.14, -0.6 + 0.15 * (k % 8))
|
||||||
|
# reverb on the wet bus
|
||||||
|
ir_n = int(1.3 * SR)
|
||||||
|
t = np.arange(ir_n) / SR
|
||||||
|
ir = np.random.default_rng(7).standard_normal((ir_n, 2)) * np.exp(-t * 4.2)[:, None]
|
||||||
|
ir[:, 0] = _band(ir[:, 0], 200, 6000)
|
||||||
|
ir[:, 1] = _band(ir[:, 1], 200, 6000)
|
||||||
|
ir /= np.sqrt((ir ** 2).sum(axis=0))
|
||||||
|
wet = mix.buses['wet']
|
||||||
|
rev = np.stack([signal.fftconvolve(wet[:, c], ir[:, c])[:mix.n] for c in range(2)], axis=1)
|
||||||
|
y = mix.buses['dry'] + wet + 0.22 * rev
|
||||||
|
y = y[:int(total * SR)]
|
||||||
|
bh, ah = signal.butter(2, 30 / (SR / 2), 'high')
|
||||||
|
y = signal.filtfilt(bh, ah, y, axis=0)
|
||||||
|
y /= np.abs(y).max() / 0.95
|
||||||
|
y = np.tanh(1.4 * y) / np.tanh(1.4)
|
||||||
|
n_in, n_out = int(0.25 * SR), int(1.6 * SR)
|
||||||
|
y[:n_in] *= np.linspace(0, 1, n_in)[:, None]
|
||||||
|
y[-n_out:] *= np.linspace(1, 0, n_out)[:, None] ** 1.5
|
||||||
|
y *= 0.89 / np.abs(y).max()
|
||||||
|
wavfile.write(out, SR, (y * 32767).astype(np.int16))
|
||||||
|
print('wrote', out, len(y) / SR, 's; final hit bar', last, 'at', last * BAR)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
build(json.load(open(sys.argv[1])), sys.argv[2])
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
"""Tiny paper-cutout rendering engine: hand-cut shapes, paper grain, layered shadows,
|
||||||
|
stop-motion jitter, torn-paper wipes. Everything is deterministic so worker processes
|
||||||
|
can rebuild identical sprite caches independently."""
|
||||||
|
import os, re, math, zlib, random, functools
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageChops
|
||||||
|
from scipy import ndimage
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
W, H = 1920, 1080
|
||||||
|
FPS = 12 # stop-motion animation rate (encoded at 24 with doubled frames)
|
||||||
|
SS = 3 # supersampling for shape masks
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- palette
|
||||||
|
INK = (45, 42, 40); WHITE = (253, 250, 243); PAPER = (250, 244, 230)
|
||||||
|
CORAL = (231, 111, 81); TEAL = (42, 157, 143); MUSTARD = (236, 190, 90)
|
||||||
|
NAVY = (38, 70, 83); ORANGE = (244, 162, 97); SKY = (142, 202, 230)
|
||||||
|
BLUE = (52, 140, 200); BRAND = (0, 89, 156); PLUM = (146, 102, 150)
|
||||||
|
GREEN = (118, 178, 92); DIRT = (150, 100, 66); DARK = (58, 62, 72)
|
||||||
|
GREY = (170, 168, 160); PINK = (240, 150, 160); RED = (200, 50, 50)
|
||||||
|
BG_CREAM = (243, 234, 214); BG_SKY = (206, 228, 236); BG_MINT = (214, 234, 218)
|
||||||
|
BG_PEACH = (248, 222, 203); BG_LAV = (226, 219, 238); BG_BUTTER = (246, 233, 192)
|
||||||
|
BG_NAVY = (40, 58, 82)
|
||||||
|
|
||||||
|
|
||||||
|
def shade(c, f):
|
||||||
|
if f >= 1:
|
||||||
|
return tuple(int(v + (255 - v) * (f - 1)) for v in c)
|
||||||
|
return tuple(int(v * f) for v in c)
|
||||||
|
|
||||||
|
|
||||||
|
def crc(*parts):
|
||||||
|
return zlib.crc32('|'.join(map(str, parts)).encode())
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- easing
|
||||||
|
def clamp01(x): return 0.0 if x < 0 else 1.0 if x > 1 else x
|
||||||
|
def lerp(a, b, t): return a + (b - a) * t
|
||||||
|
def prog(t, t0, d): return clamp01((t - t0) / d) if d > 0 else float(t >= t0)
|
||||||
|
def ease_out(p): return 1 - (1 - p) ** 3
|
||||||
|
def ease_in(p): return p ** 3
|
||||||
|
def ease_io(p): return p * p * (3 - 2 * p)
|
||||||
|
|
||||||
|
|
||||||
|
def ease_back(p, s=1.70158):
|
||||||
|
p -= 1
|
||||||
|
return p * p * ((s + 1) * p + s) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def bump(t, t0, d):
|
||||||
|
return math.sin(math.pi * prog(t, t0, d))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- fonts
|
||||||
|
FONT_FILES = {
|
||||||
|
'fredoka': os.path.join(HERE, 'fonts', 'Fredoka.ttf'),
|
||||||
|
'hand': os.path.join(HERE, 'fonts', 'PatrickHand.ttf'),
|
||||||
|
'mono': next((p for p in (os.path.join(HERE, 'fonts', 'CascadiaMono.ttf'), r'C:\Windows\Fonts\CascadiaMono.ttf')
|
||||||
|
if os.path.exists(p)), os.path.join(HERE, 'fonts', 'CascadiaMono.ttf')),
|
||||||
|
'kuaile': os.path.join(HERE, 'fonts', 'ZCOOLKuaiLe.ttf'),
|
||||||
|
'wenkai': os.path.join(HERE, 'fonts', 'LXGWWenKai.ttf'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- localisation
|
||||||
|
LANG = os.environ.get('MGL_LANG', 'en')
|
||||||
|
TR = {}
|
||||||
|
if LANG == 'zh':
|
||||||
|
from zh import TR_ZH as TR
|
||||||
|
CJK_SUB = {'hand': ('wenkai', 0.9), 'mono': ('wenkai', 0.95)}
|
||||||
|
CJK_RICH = {'fredoka': ('kuaile', 0.96)} # Latin runs keep the original face, CJK runs use this
|
||||||
|
|
||||||
|
|
||||||
|
def T(s):
|
||||||
|
return TR.get(s, s) if TR else s
|
||||||
|
|
||||||
|
|
||||||
|
def has_cjk(s):
|
||||||
|
return any('\u3000' <= ch <= '\u9fff' or '\uff00' <= ch <= '\uffef' for ch in s)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve(spec, s):
|
||||||
|
"""Text containing CJK is drawn with a CJK font standing in for the Latin family."""
|
||||||
|
if not has_cjk(s):
|
||||||
|
return spec
|
||||||
|
if spec[0] in CJK_RICH:
|
||||||
|
name, k = CJK_RICH[spec[0]]
|
||||||
|
return ('rich', spec, (name, max(8, int(round(spec[1] * k)))))
|
||||||
|
if spec[0] in CJK_SUB:
|
||||||
|
name, k = CJK_SUB[spec[0]]
|
||||||
|
return (name, max(8, int(round(spec[1] * k))))
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
class RichFont:
|
||||||
|
"""Two faces sharing a baseline: one for Latin runs, one for CJK runs."""
|
||||||
|
|
||||||
|
def __init__(self, lat, cjk, scale):
|
||||||
|
self.fl, self.fc = get_font(lat, scale), get_font(cjk, scale)
|
||||||
|
(al, dl), (ac, dc) = self.fl.getmetrics(), self.fc.getmetrics()
|
||||||
|
self.asc, self.desc = max(al, ac), max(dl, dc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_c(ch):
|
||||||
|
return has_cjk(ch) or '\u2010' <= ch <= '\u206f'
|
||||||
|
|
||||||
|
def runs(self, s):
|
||||||
|
out, cur, curc = [], '', None
|
||||||
|
for ch in s:
|
||||||
|
c = curc if (ch == ' ' and curc is not None) else self._is_c(ch)
|
||||||
|
if curc is None or c == curc:
|
||||||
|
cur, curc = cur + ch, c
|
||||||
|
else:
|
||||||
|
out.append((cur, curc))
|
||||||
|
cur, curc = ch, c
|
||||||
|
if cur:
|
||||||
|
out.append((cur, curc))
|
||||||
|
return [(r, self.fc if c else self.fl) for r, c in out]
|
||||||
|
|
||||||
|
def getlength(self, s):
|
||||||
|
return sum(f.getlength(r) for r, f in self.runs(s))
|
||||||
|
|
||||||
|
def bbox(self, s, spacing, h):
|
||||||
|
lines = s.split('\n')
|
||||||
|
w = max(self.getlength(l) for l in lines)
|
||||||
|
hh = len(lines) * (self.asc + self.desc) + (len(lines) - 1) * spacing
|
||||||
|
return (-w / 2, 0, w / 2, hh) if h == 'm' else (0, 0, w, hh)
|
||||||
|
|
||||||
|
def draw(self, d, x, y, s, spacing, h, v, fill):
|
||||||
|
lines = s.split('\n')
|
||||||
|
lh = self.asc + self.desc
|
||||||
|
top = y - ((len(lines) * lh + (len(lines) - 1) * spacing) / 2 if v == 'm' else 0)
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
xs = x - (self.getlength(ln) / 2 if h == 'm' else 0)
|
||||||
|
base = top + self.asc + i * (lh + spacing)
|
||||||
|
for r, f in self.runs(ln):
|
||||||
|
d.text((xs, base), r, font=f, fill=fill, anchor='ls')
|
||||||
|
xs += f.getlength(r)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def get_font(spec, scale=1):
|
||||||
|
if spec[0] == 'rich':
|
||||||
|
return RichFont(spec[1], spec[2], scale)
|
||||||
|
f = ImageFont.truetype(FONT_FILES[spec[0]], int(round(spec[1] * scale)))
|
||||||
|
if spec[0] == 'fredoka':
|
||||||
|
f.set_variation_by_name(spec[2] if len(spec) > 2 else 'SemiBold')
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
_D = ImageDraw.Draw(Image.new('L', (8, 8)))
|
||||||
|
|
||||||
|
|
||||||
|
def text_bbox(s, font, spacing=6, align='center', h='m'):
|
||||||
|
s = T(s)
|
||||||
|
font = resolve(font, s)
|
||||||
|
if font[0] == 'rich':
|
||||||
|
return get_font(font).bbox(s, spacing, h)
|
||||||
|
return _D.multiline_textbbox((0, 0), s, font=get_font(font), anchor=h + 'a',
|
||||||
|
spacing=spacing, align=align)
|
||||||
|
|
||||||
|
|
||||||
|
def measure(s, font, spacing=6):
|
||||||
|
l, t, r, b = text_bbox(s, font, spacing)
|
||||||
|
return r - l, b - t
|
||||||
|
|
||||||
|
|
||||||
|
_TOK = re.compile(r'[A-Za-z0-9_.+#/×{}\-\u00a0]+|\s+|.', re.S)
|
||||||
|
_NO_START = set(',。、:;!?)》」』”’…—,.;:!?)')
|
||||||
|
_NO_END = set('(《「『“‘(')
|
||||||
|
|
||||||
|
|
||||||
|
_BREAK_AFTER = set(',。、:;!?')
|
||||||
|
|
||||||
|
|
||||||
|
def _cjk_greedy(toks, f, w):
|
||||||
|
out, cur = [], []
|
||||||
|
for tk in toks:
|
||||||
|
line = ''.join(cur)
|
||||||
|
if not line.strip() or f.getlength((line + tk).rstrip()) <= w:
|
||||||
|
cur.append(tk)
|
||||||
|
continue
|
||||||
|
j = None # prefer breaking right after punctuation if the line stays reasonably full
|
||||||
|
for k in range(len(cur) - 1, 0, -1):
|
||||||
|
if cur[k - 1][-1] in _BREAK_AFTER or cur[k - 1].isspace():
|
||||||
|
if f.getlength(''.join(cur[:k]).rstrip()) >= 0.7 * w:
|
||||||
|
j = k
|
||||||
|
break
|
||||||
|
if j:
|
||||||
|
out.append(''.join(cur[:j]).rstrip())
|
||||||
|
cur = cur[j:]
|
||||||
|
else:
|
||||||
|
out.append(line.rstrip())
|
||||||
|
cur = []
|
||||||
|
while cur and cur[0].isspace():
|
||||||
|
cur.pop(0)
|
||||||
|
if cur or not tk.isspace():
|
||||||
|
cur.append(tk)
|
||||||
|
out.append(''.join(cur).rstrip())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_cjk(s, f, maxw):
|
||||||
|
out = []
|
||||||
|
for para in s.split('\n'):
|
||||||
|
toks = []
|
||||||
|
for tk in _TOK.findall(para):
|
||||||
|
if toks and (tk[0] in _NO_START or toks[-1][-1] in _NO_END):
|
||||||
|
toks[-1] += tk
|
||||||
|
else:
|
||||||
|
toks.append(tk)
|
||||||
|
n = len(_cjk_greedy(toks, f, maxw))
|
||||||
|
w = maxw
|
||||||
|
if n > 1: # balance: narrowest width that keeps the same line count
|
||||||
|
lo = maxw * 0.3
|
||||||
|
while w - lo > 4:
|
||||||
|
mid = (lo + w) / 2
|
||||||
|
if len(_cjk_greedy(toks, f, mid)) <= n:
|
||||||
|
w = mid
|
||||||
|
else:
|
||||||
|
lo = mid
|
||||||
|
out += _cjk_greedy(toks, f, w)
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap(s, font, maxw):
|
||||||
|
s = T(s)
|
||||||
|
font = resolve(font, s)
|
||||||
|
f = get_font(font)
|
||||||
|
if has_cjk(s):
|
||||||
|
return wrap_cjk(s, f, maxw)
|
||||||
|
out = []
|
||||||
|
for para in s.split('\n'):
|
||||||
|
line = ''
|
||||||
|
for w in para.split(' '):
|
||||||
|
cand = (line + ' ' + w).strip()
|
||||||
|
if not line or f.getlength(cand) <= maxw:
|
||||||
|
line = cand
|
||||||
|
else:
|
||||||
|
out.append(line)
|
||||||
|
line = w
|
||||||
|
out.append(line)
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- paper grain
|
||||||
|
_GRAIN = None
|
||||||
|
|
||||||
|
|
||||||
|
def grain():
|
||||||
|
global _GRAIN
|
||||||
|
if _GRAIN is None:
|
||||||
|
size = 1024
|
||||||
|
rng = np.random.default_rng(7)
|
||||||
|
ky = np.fft.fftfreq(size)[:, None]
|
||||||
|
kx = np.fft.fftfreq(size)[None, :]
|
||||||
|
k2 = kx ** 2 + ky ** 2
|
||||||
|
|
||||||
|
def periodic(sigma): # gaussian-filtered white noise, seamlessly tileable
|
||||||
|
f = np.fft.fft2(rng.standard_normal((size, size)))
|
||||||
|
o = np.real(np.fft.ifft2(f * np.exp(-k2 * (2 * np.pi * sigma) ** 2 / 2)))
|
||||||
|
return (o / o.std()).astype(np.float32)
|
||||||
|
g = 0.45 * periodic(90) + 0.35 * periodic(24) + 0.3 * periodic(5)
|
||||||
|
g += 0.4 * rng.standard_normal((size, size)).astype(np.float32)
|
||||||
|
fib = Image.new('L', (size, size), 0)
|
||||||
|
d = ImageDraw.Draw(fib)
|
||||||
|
for _ in range(1400):
|
||||||
|
x, y = rng.uniform(0, size, 2)
|
||||||
|
ang = rng.uniform(0, math.pi)
|
||||||
|
L = rng.uniform(5, 26)
|
||||||
|
col = int(rng.uniform(70, 170))
|
||||||
|
for ox in (-size, 0, size):
|
||||||
|
for oy in (-size, 0, size):
|
||||||
|
d.line([(x + ox, y + oy), (x + ox + L * math.cos(ang), y + oy + L * math.sin(ang))],
|
||||||
|
fill=col, width=1)
|
||||||
|
fib = np.asarray(fib.filter(ImageFilter.GaussianBlur(0.5)), np.float32) / 255
|
||||||
|
g = (g - g.mean()) / g.std()
|
||||||
|
_GRAIN = g + fib * 1.4
|
||||||
|
return _GRAIN
|
||||||
|
|
||||||
|
|
||||||
|
def textured(color, w, h, seed, k):
|
||||||
|
c = np.array(color, np.float32)
|
||||||
|
if k <= 0:
|
||||||
|
return np.ascontiguousarray(np.broadcast_to(c.astype(np.uint8), (h, w, 3)))
|
||||||
|
g = grain()
|
||||||
|
r = random.Random(seed)
|
||||||
|
ys = (np.arange(h) + r.randrange(1024)) % 1024
|
||||||
|
xs = (np.arange(w) + r.randrange(1024)) % 1024
|
||||||
|
out = c[None, None, :] * (1 + k * g[np.ix_(ys, xs)][..., None])
|
||||||
|
return np.clip(out, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- shapes
|
||||||
|
def rrect_pts(x, y, w, h, r):
|
||||||
|
r = max(0.0, min(r, w / 2, h / 2))
|
||||||
|
pts = []
|
||||||
|
for cx, cy, a0 in [(x + w - r, y + r, -90), (x + w - r, y + h - r, 0),
|
||||||
|
(x + r, y + h - r, 90), (x + r, y + r, 180)]:
|
||||||
|
for k in range(9):
|
||||||
|
a = math.radians(a0 + 90 * k / 8)
|
||||||
|
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse_pts(cx, cy, rx, ry, n=None):
|
||||||
|
n = n or max(28, int((rx + ry) / 2.5))
|
||||||
|
return [(cx + rx * math.cos(2 * math.pi * k / n), cy + ry * math.sin(2 * math.pi * k / n))
|
||||||
|
for k in range(n)]
|
||||||
|
|
||||||
|
|
||||||
|
def star_pts(cx, cy, r1, r2, n=5, rot=-90):
|
||||||
|
pts = []
|
||||||
|
for k in range(2 * n):
|
||||||
|
r = r1 if k % 2 == 0 else r2
|
||||||
|
a = math.radians(rot + 180 * k / n)
|
||||||
|
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def gear_pts(cx, cy, r1, r2, teeth):
|
||||||
|
pts = []
|
||||||
|
for k in range(teeth * 4):
|
||||||
|
a = 2 * math.pi * k / (teeth * 4)
|
||||||
|
r = r2 if (k % 4) in (1, 2) else r1
|
||||||
|
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def rot_rect_pts(cx, cy, w, h, ang):
|
||||||
|
a = math.radians(ang)
|
||||||
|
ca, sa = math.cos(a), math.sin(a)
|
||||||
|
return [(cx + x * ca - y * sa, cy + x * sa + y * ca)
|
||||||
|
for x, y in [(-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2), (-w / 2, h / 2)]]
|
||||||
|
|
||||||
|
|
||||||
|
def roughen(pts, amp, seed, step=7):
|
||||||
|
if amp <= 0 or len(pts) < 3:
|
||||||
|
return pts
|
||||||
|
p = []
|
||||||
|
n0 = len(pts)
|
||||||
|
for i in range(n0):
|
||||||
|
x0, y0 = pts[i]
|
||||||
|
x1, y1 = pts[(i + 1) % n0]
|
||||||
|
n = max(1, int(math.hypot(x1 - x0, y1 - y0) / step))
|
||||||
|
for k in range(n):
|
||||||
|
p.append((x0 + (x1 - x0) * k / n, y0 + (y1 - y0) * k / n))
|
||||||
|
p = np.array(p, np.float32)
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
a = rng.standard_normal(p.shape).astype(np.float32)
|
||||||
|
a = sum(np.roll(a, s, axis=0) for s in range(-1, 2))
|
||||||
|
b = rng.standard_normal(p.shape).astype(np.float32)
|
||||||
|
b = sum(np.roll(b, s, axis=0) for s in range(-6, 7))
|
||||||
|
a *= 0.6 * amp / (a.std() + 1e-6)
|
||||||
|
b *= 0.9 * amp / (b.std() + 1e-6)
|
||||||
|
return [tuple(v) for v in (p + a + b)]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- art builder
|
||||||
|
class Art:
|
||||||
|
"""Stack of paper layers. Each layer gets grain and casts a soft shadow on layers below."""
|
||||||
|
|
||||||
|
def __init__(self, w, h, key):
|
||||||
|
self.w, self.h, self.key = int(math.ceil(w)), int(math.ceil(h)), key
|
||||||
|
self.ops = []
|
||||||
|
|
||||||
|
def _seed(self):
|
||||||
|
return crc(self.key, len(self.ops))
|
||||||
|
|
||||||
|
def poly(self, pts, color, rough=1.0, shade=True, tex=0.035, opacity=1.0):
|
||||||
|
self.ops.append(dict(kind='poly', pts=list(pts), color=color, rough=rough, shade=shade,
|
||||||
|
tex=tex, opacity=opacity, seed=self._seed()))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rrect(self, x, y, w, h, r, color, **kw):
|
||||||
|
return self.poly(rrect_pts(x, y, w, h, r), color, **kw)
|
||||||
|
|
||||||
|
def ellipse(self, cx, cy, rx, ry, color, **kw):
|
||||||
|
return self.poly(ellipse_pts(cx, cy, rx, ry), color, **kw)
|
||||||
|
|
||||||
|
def circle(self, cx, cy, r, color, **kw):
|
||||||
|
return self.ellipse(cx, cy, r, r, color, **kw)
|
||||||
|
|
||||||
|
def text(self, cx, cy, s, font, color, h='m', v='m', spacing=6, align='center',
|
||||||
|
shade=False, tex=0.0, opacity=1.0):
|
||||||
|
s = T(s)
|
||||||
|
font = resolve(font, s)
|
||||||
|
self.ops.append(dict(kind='text', x=cx, y=cy, s=s, font=font, color=color, h=h, v=v,
|
||||||
|
spacing=spacing, align=align, shade=shade, tex=tex,
|
||||||
|
opacity=opacity, seed=self._seed()))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def lines(self, pls, width, color, shade=False, tex=0.0, caps=True, opacity=1.0):
|
||||||
|
self.ops.append(dict(kind='lines', pls=pls, width=width, color=color, shade=shade,
|
||||||
|
tex=tex, caps=caps, opacity=opacity, seed=self._seed()))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def build(self, pad=30):
|
||||||
|
P = pad
|
||||||
|
w2, h2 = self.w + 2 * P, self.h + 2 * P
|
||||||
|
acc = Image.new('RGBA', (w2, h2), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
def T(x, y):
|
||||||
|
return ((x + P) * SS, (y + P) * SS)
|
||||||
|
|
||||||
|
for op in self.ops:
|
||||||
|
m = Image.new('L', (w2 * SS, h2 * SS), 0)
|
||||||
|
d = ImageDraw.Draw(m)
|
||||||
|
if op['kind'] == 'poly':
|
||||||
|
pts = roughen(op['pts'], op['rough'], op['seed'])
|
||||||
|
d.polygon([T(x, y) for x, y in pts], fill=255)
|
||||||
|
elif op['kind'] == 'text' and op['font'][0] == 'rich':
|
||||||
|
get_font(op['font'], SS).draw(d, *T(op['x'], op['y']), op['s'], op['spacing'] * SS,
|
||||||
|
op['h'], op['v'], 255)
|
||||||
|
elif op['kind'] == 'text':
|
||||||
|
l, t, r, b = text_bbox(op['s'], op['font'], op['spacing'], op['align'], op['h'])
|
||||||
|
x = op['x'] - ((l + r) / 2 if op['h'] == 'm' else l)
|
||||||
|
y = op['y'] - ((t + b) / 2 if op['v'] == 'm' else t)
|
||||||
|
d.multiline_text(T(x, y), op['s'], font=get_font(op['font'], SS), fill=255,
|
||||||
|
anchor=op['h'] + 'a', spacing=op['spacing'] * SS,
|
||||||
|
align=op['align'])
|
||||||
|
else:
|
||||||
|
wd = max(1, int(op['width'] * SS))
|
||||||
|
for pl in op['pls']:
|
||||||
|
q = [T(x, y) for x, y in pl]
|
||||||
|
d.line(q, fill=255, width=wd, joint='curve')
|
||||||
|
if op['caps']:
|
||||||
|
for qx, qy in (q[0], q[-1]):
|
||||||
|
d.ellipse([qx - wd / 2, qy - wd / 2, qx + wd / 2, qy + wd / 2], fill=255)
|
||||||
|
m = m.resize((w2, h2), Image.BOX)
|
||||||
|
if op['opacity'] < 1:
|
||||||
|
o = op['opacity']
|
||||||
|
m = m.point(lambda v: int(v * o))
|
||||||
|
if op['shade']:
|
||||||
|
sh = ImageChops.offset(m.filter(ImageFilter.GaussianBlur(2.4)), 2, 3)
|
||||||
|
sh = ImageChops.multiply(sh, acc.getchannel('A')).point(lambda v: int(v * 0.40))
|
||||||
|
sl = Image.new('RGBA', (w2, h2), (30, 20, 10, 0))
|
||||||
|
sl.putalpha(sh)
|
||||||
|
acc = Image.alpha_composite(acc, sl)
|
||||||
|
layer = Image.fromarray(textured(op['color'], w2, h2, op['seed'], op['tex']), 'RGB')
|
||||||
|
layer = layer.convert('RGBA')
|
||||||
|
layer.putalpha(m)
|
||||||
|
acc = Image.alpha_composite(acc, layer)
|
||||||
|
return Sprite(acc, self.key)
|
||||||
|
|
||||||
|
|
||||||
|
class Sprite:
|
||||||
|
def __init__(self, img, key, shadow_alpha=0.40, blur=6):
|
||||||
|
arr = np.array(img)
|
||||||
|
a = arr[..., 3]
|
||||||
|
hole = a == 0
|
||||||
|
if hole.any() and not hole.all():
|
||||||
|
iy, ix = ndimage.distance_transform_edt(hole, return_distances=False, return_indices=True)
|
||||||
|
for ch in range(3):
|
||||||
|
c = arr[..., ch]
|
||||||
|
c[hole] = c[iy[hole], ix[hole]]
|
||||||
|
self.img = Image.fromarray(arr, 'RGBA')
|
||||||
|
self.key = key
|
||||||
|
sa = img.getchannel('A').filter(ImageFilter.GaussianBlur(blur)).point(
|
||||||
|
lambda v: int(v * shadow_alpha))
|
||||||
|
self.shadow = Image.new('RGBA', img.size, (35, 25, 15, 0))
|
||||||
|
self.shadow.putalpha(sa)
|
||||||
|
self.w, self.h = img.size
|
||||||
|
|
||||||
|
|
||||||
|
def comp(dst, src, x, y):
|
||||||
|
sx0, sy0 = max(0, -x), max(0, -y)
|
||||||
|
dx0, dy0 = max(0, x), max(0, y)
|
||||||
|
w = min(src.width - sx0, dst.width - dx0)
|
||||||
|
h = min(src.height - sy0, dst.height - dy0)
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return
|
||||||
|
if sx0 or sy0 or w < src.width or h < src.height:
|
||||||
|
src = src.crop((sx0, sy0, sx0 + w, sy0 + h))
|
||||||
|
dst.alpha_composite(src, (dx0, dy0))
|
||||||
|
|
||||||
|
|
||||||
|
def _fade(img, alpha):
|
||||||
|
img = img.copy()
|
||||||
|
img.putalpha(img.getchannel('A').point(lambda v: int(v * alpha)))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def place(frame, spr, x, y, rot=0.0, sx=1.0, sy=None, alpha=1.0, elev=1.0, fi=0, boil=1.0,
|
||||||
|
shadow=True):
|
||||||
|
sy = sx if sy is None else sy
|
||||||
|
if alpha <= 0.01 or sx <= 0.02 or sy <= 0.02:
|
||||||
|
return
|
||||||
|
if boil:
|
||||||
|
r = random.Random(crc(spr.key, fi // 2))
|
||||||
|
x += r.uniform(-1.1, 1.1) * boil
|
||||||
|
y += r.uniform(-1.1, 1.1) * boil
|
||||||
|
rot += r.uniform(-0.4, 0.4) * boil
|
||||||
|
img, sh = spr.img, spr.shadow
|
||||||
|
if abs(sx - 1) > 1e-3 or abs(sy - 1) > 1e-3:
|
||||||
|
size = (max(1, int(img.width * sx)), max(1, int(img.height * sy)))
|
||||||
|
img = img.resize(size, Image.BILINEAR)
|
||||||
|
sh = sh.resize(size, Image.BILINEAR) if shadow else sh
|
||||||
|
if abs(rot) > 0.05:
|
||||||
|
img = img.rotate(rot, Image.BICUBIC, expand=True)
|
||||||
|
if shadow:
|
||||||
|
sh = sh.rotate(rot, Image.BILINEAR, expand=True)
|
||||||
|
if alpha < 0.999:
|
||||||
|
img = _fade(img, alpha)
|
||||||
|
if shadow:
|
||||||
|
sh = _fade(sh, alpha)
|
||||||
|
if shadow and elev > 0:
|
||||||
|
comp(frame, sh, int(round(x - sh.width / 2 + 6 * elev)), int(round(y - sh.height / 2 + 9 * elev)))
|
||||||
|
comp(frame, img, int(round(x - img.width / 2)), int(round(y - img.height / 2)))
|
||||||
|
|
||||||
|
|
||||||
|
class Ctx:
|
||||||
|
def __init__(self, frame, t, fi, dur):
|
||||||
|
self.frame, self.t, self.fi, self.dur = frame, t, fi, dur
|
||||||
|
|
||||||
|
def put(self, spr, x, y, rot=0.0, s=1.0, sy=None, alpha=1.0, elev=1.0, boil=1.0, shadow=True):
|
||||||
|
place(self.frame, spr, x, y, rot, s, sy, alpha, elev, self.fi, boil, shadow)
|
||||||
|
|
||||||
|
def _out(self, out, od):
|
||||||
|
if out is None or self.t <= out:
|
||||||
|
return 1.0, 1.0
|
||||||
|
q = prog(self.t, out, od)
|
||||||
|
return 1 - 0.85 * ease_in(q), 1 - q
|
||||||
|
|
||||||
|
def pop(self, spr, x, y, t0, d=0.55, rot=0.0, s=1.0, elev=1.0, out=None, od=0.35, alpha=1.0,
|
||||||
|
shadow=True):
|
||||||
|
if self.t < t0:
|
||||||
|
return False
|
||||||
|
p = prog(self.t, t0, d)
|
||||||
|
e = ease_out(p)
|
||||||
|
sc = s * (0.2 + 0.8 * ease_back(p))
|
||||||
|
sg = 1 if crc(spr.key) % 2 else -1
|
||||||
|
so, ao = self._out(out, od)
|
||||||
|
if ao <= 0.01:
|
||||||
|
return False
|
||||||
|
self.put(spr, x, y, rot + (1 - e) * 14 * sg, sc * so, None, alpha * min(1, p * 4) * ao,
|
||||||
|
elev + 3 * (1 - e), shadow=shadow)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def slide(self, spr, p0, p1, t0, d=0.8, rot=0.0, s=1.0, elev=1.0, arc=0.0, out=None, od=0.35,
|
||||||
|
rot0=None, s0=None, before=False, lift=2.5):
|
||||||
|
if self.t < t0 and not before:
|
||||||
|
return False
|
||||||
|
p = prog(self.t, t0, d)
|
||||||
|
e = ease_io(p)
|
||||||
|
x = lerp(p0[0], p1[0], e)
|
||||||
|
y = lerp(p0[1], p1[1], e) - arc * math.sin(math.pi * e)
|
||||||
|
r = rot if rot0 is None else lerp(rot0, rot, e)
|
||||||
|
sc = s if s0 is None else lerp(s0, s, e)
|
||||||
|
so, ao = self._out(out, od)
|
||||||
|
if ao <= 0.01:
|
||||||
|
return False
|
||||||
|
self.put(spr, x, y, r, sc * so, None, ao, elev + lift * math.sin(math.pi * p))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unfold(self, spr, x, y, t0, d=0.6, rot=0.0, s=1.0, elev=1.0, out=None, od=0.35):
|
||||||
|
if self.t < t0:
|
||||||
|
return False
|
||||||
|
p = prog(self.t, t0, d)
|
||||||
|
so, ao = self._out(out, od)
|
||||||
|
if ao <= 0.01:
|
||||||
|
return False
|
||||||
|
self.put(spr, x, y, rot, s * max(0.03, ease_back(p, 1.3)) * so, s * so, ao * min(1, p * 5),
|
||||||
|
elev + 2 * (1 - p))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- backgrounds / wipe
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def background(color):
|
||||||
|
rgb = textured(color, W, H, crc('bg', color), 0.04).astype(np.float32)
|
||||||
|
yy, xx = np.mgrid[0:H, 0:W]
|
||||||
|
v = ((xx - W / 2) / (W * 0.72)) ** 2 + ((yy - H / 2) / (H * 0.72)) ** 2
|
||||||
|
rgb *= (1 - 0.10 * np.clip(v, 0, 1.6))[..., None]
|
||||||
|
img = Image.fromarray(np.clip(rgb, 0, 255).astype(np.uint8)).convert('RGBA')
|
||||||
|
ov = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||||
|
d = ImageDraw.Draw(ov)
|
||||||
|
for (a, b) in [((0, H * 0.18), (W, H * 0.92)), ((W * 0.64, 0), (W * 0.30, H))]:
|
||||||
|
d.line([a, b], fill=(40, 30, 20, 16), width=3)
|
||||||
|
d.line([(a[0] + 3, a[1] - 3), (b[0] + 3, b[1] - 3)], fill=(255, 255, 255, 26), width=3)
|
||||||
|
return Image.alpha_composite(img, ov.filter(ImageFilter.GaussianBlur(1.2)))
|
||||||
|
|
||||||
|
|
||||||
|
SHEET_M = 110
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def sheet(color):
|
||||||
|
bg = background(color)
|
||||||
|
M = SHEET_M
|
||||||
|
base = Image.new('RGBA', (W + M, H))
|
||||||
|
base.paste(bg, (M, 0))
|
||||||
|
base.paste(bg.crop((0, 0, M, H)).transpose(Image.FLIP_LEFT_RIGHT), (0, 0))
|
||||||
|
rng = random.Random(crc('tear', color))
|
||||||
|
edge, core = [], []
|
||||||
|
for y in range(0, H + 15, 12):
|
||||||
|
x = M * 0.62 + rng.uniform(-14, 14) + 12 * math.sin(y / 85.0)
|
||||||
|
edge.append((x, y))
|
||||||
|
core.append((x - rng.uniform(4, 11), y))
|
||||||
|
mask = Image.new('L', (W + M, H), 0)
|
||||||
|
ImageDraw.Draw(mask).polygon(edge + [(W + M, H), (W + M, 0)], fill=255)
|
||||||
|
cmask = Image.new('L', (W + M, H), 0)
|
||||||
|
ImageDraw.Draw(cmask).polygon(core + [(W + M, H), (W + M, 0)], fill=255)
|
||||||
|
out = Image.new('RGBA', (W + M, H), (0, 0, 0, 0))
|
||||||
|
sh = ImageChops.offset(cmask.filter(ImageFilter.GaussianBlur(9)), -12, 0).point(lambda v: int(v * 0.35))
|
||||||
|
sl = Image.new('RGBA', out.size, (30, 20, 10, 0))
|
||||||
|
sl.putalpha(sh)
|
||||||
|
out = Image.alpha_composite(out, sl)
|
||||||
|
cl = Image.new('RGBA', out.size, shade(color, 1.6) + (255,))
|
||||||
|
cl.putalpha(cmask)
|
||||||
|
out = Image.alpha_composite(out, cl)
|
||||||
|
b2 = base.copy()
|
||||||
|
b2.putalpha(mask)
|
||||||
|
return Image.alpha_composite(out, b2)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_wipe(frame, color, p):
|
||||||
|
x = int(round(lerp(W, -SHEET_M, ease_io(p))))
|
||||||
|
comp(frame, sheet(color), x, 0)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
pillow
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
|||||||
|
"""Simplified-Chinese strings for the MobileGL intro (MGL_LANG=zh). Untranslated strings render as-is."""
|
||||||
|
TR_ZH = {
|
||||||
|
# ---- captions
|
||||||
|
'Run desktop OpenGL apps where there is no desktop OpenGL driver.':
|
||||||
|
'在没有桌面 OpenGL 驱动的地方,也能运行桌面 OpenGL 程序。',
|
||||||
|
"Let's unfold how it works. No deep internals, promise!":
|
||||||
|
'一起把它展开看看吧——保证不钻太深!',
|
||||||
|
'Lots of great software speaks desktop OpenGL: games, creative tools, whole desktops.':
|
||||||
|
'很多优秀的软件都说“桌面 OpenGL”:游戏、创作工具,甚至整个桌面环境。',
|
||||||
|
'But phones and tablets usually ship only OpenGL ES or Vulkan drivers.':
|
||||||
|
'可手机和平板通常只带 OpenGL ES 或 Vulkan 驱动。',
|
||||||
|
"Same GPU, different language: the app's calls have nobody to answer them.":
|
||||||
|
'同一块 GPU,却说着不同的语言:程序发出的调用,没人能接。',
|
||||||
|
'Meet MobileGL: a complete desktop OpenGL library that sits between your app and the driver.':
|
||||||
|
'认识一下 MobileGL:一个完整的桌面 OpenGL 库,位于你的程序和驱动之间。',
|
||||||
|
'Your app sees OpenGL 4.6 Core, with just a few corners still being finished.':
|
||||||
|
'程序看到的是 OpenGL 4.6 Core,只有少数边角还在完善中。',
|
||||||
|
'Underneath, MobileGL speaks whatever the device understands: OpenGL ES or Vulkan.':
|
||||||
|
'在底层,MobileGL 说设备听得懂的语言:OpenGL ES 或 Vulkan。',
|
||||||
|
"Your app doesn't change. It simply loads MobileGL as its OpenGL library.":
|
||||||
|
'程序本身不用改,只需把 MobileGL 当作 OpenGL 库加载。',
|
||||||
|
'Why can this work at all? Every modern GPU API is built from the same ingredients:':
|
||||||
|
'为什么这行得通?因为现代 GPU API 都由同样的“食材”构成:',
|
||||||
|
'buffers, textures, shaders, and state like blending and depth testing.':
|
||||||
|
'缓冲区、纹理、着色器,以及混合、深度测试这类状态。',
|
||||||
|
'OpenGL keeps that state in one big global switchboard you flip one switch at a time.':
|
||||||
|
'OpenGL 把这些状态放在一块巨大的全局开关板上,一次拨一个开关。',
|
||||||
|
'At each draw, MobileGL snapshots the switches that matter into an explicit recipe…':
|
||||||
|
'每次绘制时,MobileGL 把相关的开关拍成快照,写成一份明确的“配方”……',
|
||||||
|
'…that a backend can replay on OpenGL ES or Vulkan.':
|
||||||
|
'……再由后端照着配方,在 OpenGL ES 或 Vulkan 上重现。',
|
||||||
|
'Step 1: the frontend. Every GL call your app makes lands here first.':
|
||||||
|
'第一步:前端。程序发出的每个 GL 调用都先到这里。',
|
||||||
|
'It tracks every bind, every uniform, every switch, just like a desktop driver would.':
|
||||||
|
'它记录每一次绑定、每个 uniform、每个开关,就像桌面驱动那样。',
|
||||||
|
'It checks each call against the spec, so mistakes become proper GL errors, not crashes.':
|
||||||
|
'它按规范检查每个调用,出错时返回标准的 GL 错误,而不是直接崩溃。',
|
||||||
|
'Nothing has touched the GPU yet. The frontend simply knows what the world should look like.':
|
||||||
|
'此时 GPU 还没被碰过。前端只是清楚地知道,画面“应该”是什么样子。',
|
||||||
|
'Shaders get folded too. Your GLSL is compiled once, by glslang…':
|
||||||
|
'着色器也要“折”一下。你的 GLSL 先由 glslang 编译一次……',
|
||||||
|
'…into SPIR-V: a compact, portable shader format.':
|
||||||
|
'……折成 SPIR-V:一种紧凑、可移植的着色器格式。',
|
||||||
|
'Vulkan takes SPIR-V as-is. For OpenGL ES, SPIRV-Cross unfolds it into GLSL ES.':
|
||||||
|
'Vulkan 直接使用 SPIR-V;对 OpenGL ES,则由 SPIRV-Cross 把它展开成 GLSL ES。',
|
||||||
|
'One shader, folded two ways, and your app never notices.':
|
||||||
|
'同一个着色器,两种折法,程序却毫无察觉。',
|
||||||
|
'Step 2: before each draw, the frontend sends the backend only what changed.':
|
||||||
|
'第二步:每次绘制前,前端只把“变了的部分”发给后端。',
|
||||||
|
'Each change is a small record. The draw itself is a verb that uses them.':
|
||||||
|
'每处变化都是一条小小的“记录”,绘制本身则是使用这些记录的“动作”。',
|
||||||
|
'New objects get their handles up front, so creating things never waits for a reply.':
|
||||||
|
'新对象的句柄在前端就分配好,创建东西时不必等待回复。',
|
||||||
|
'Identical state blocks are shared instead of resent. Less to carry, less to check.':
|
||||||
|
'相同的状态块只发一次、共享使用。要搬的少了,要查的也少了。',
|
||||||
|
"The backend never peeks into the frontend's memory. Records are all it gets.":
|
||||||
|
'后端从不偷看前端的内存,它拿到的只有记录。',
|
||||||
|
'Step 3: a backend turns records into real driver calls.':
|
||||||
|
'第三步:后端把记录变成真正的驱动调用。',
|
||||||
|
'Espryt speaks OpenGL ES. Magma speaks Vulkan. You pick one at startup.':
|
||||||
|
'Espryt 说 OpenGL ES,Magma 说 Vulkan,启动时任选其一。',
|
||||||
|
'Each backend is fully independent, so one can improve without disturbing the other.':
|
||||||
|
'两个后端完全独立,改进一个不会影响另一个。',
|
||||||
|
'Same app, same frontend. Choose whatever suits the device best.':
|
||||||
|
'同一个程序、同一个前端,选最适合设备的那个就好。',
|
||||||
|
"Now the fun part: records are just data, so they don't care where the backend lives.":
|
||||||
|
'有趣的部分来了:记录只是数据,它们不在乎后端住在哪里。',
|
||||||
|
'Run everything on one thread, or give frontend and backend a thread each…':
|
||||||
|
'可以全部跑在一个线程里,也可以让前端和后端各占一个线程……',
|
||||||
|
'…put them in two processes that share a ring of memory…':
|
||||||
|
'……或者放进两个进程,共享一圈环形内存……',
|
||||||
|
"…or even on two machines: a PC can drive a phone's GPU over the network.":
|
||||||
|
'……甚至放在两台机器上:电脑可以通过网络驱动手机的 GPU。',
|
||||||
|
'The frontend side is the client. The backend side, next to the GPU, is the server.':
|
||||||
|
'前端这一侧叫客户端,靠近 GPU 的后端一侧叫服务端。',
|
||||||
|
"Splitting up isn't slow. Under heavy load it can even be faster than one thread.":
|
||||||
|
'拆开并不会变慢。重负载下,它甚至比单线程更快。',
|
||||||
|
'The client runs ahead and only waits when it really needs an answer back.':
|
||||||
|
'客户端会一路往前跑,只有真正需要回复时才停下来等。',
|
||||||
|
'On the same machine, big buffers and window images are shared, not copied.':
|
||||||
|
'在同一台机器上,大缓冲区和窗口图像直接共享,不做拷贝。',
|
||||||
|
"And if the server goes away, your app sees 'device lost' instead of crashing.":
|
||||||
|
'就算服务端没了,程序看到的也只是“设备丢失”,而不是崩溃。',
|
||||||
|
'MobileGL grew up on Android: think Minecraft: Java Edition on a phone.':
|
||||||
|
'MobileGL 在 Android 上长大:想象一下在手机上玩 Minecraft Java 版。',
|
||||||
|
'It also runs on Linux, slips into Windows as opengl32.dll, and works on macOS.':
|
||||||
|
'它也能跑在 Linux 上,在 Windows 上化身 opengl32.dll,在 macOS 上同样可用。',
|
||||||
|
'A favorite demo: a whole KDE Plasma desktop from a Linux container, rendered by MobileGL on an Android tablet.':
|
||||||
|
'一个很受欢迎的演示:Linux 容器里的整套 KDE Plasma 桌面,在 Android 平板上由 MobileGL 渲染。',
|
||||||
|
'Same library everywhere. Only the window-system glue changes.':
|
||||||
|
'处处都是同一个库,变的只是窗口系统那层“胶水”。',
|
||||||
|
'MobileGL is free, open source, and still growing.':
|
||||||
|
'MobileGL 自由、开源,而且仍在成长。',
|
||||||
|
'Build it, pick a backend, and point your favorite OpenGL app at it.':
|
||||||
|
'编译它、选一个后端,然后让你喜欢的 OpenGL 程序用上它。',
|
||||||
|
"Something breaks? Tell us. That's the most useful thing you can do.":
|
||||||
|
'遇到问题?告诉我们——这就是你能提供的最大帮助。',
|
||||||
|
# ---- scene tags
|
||||||
|
'The problem': '问题在哪', 'Meet MobileGL': '认识 MobileGL', 'Why it works': '为什么可行',
|
||||||
|
'Step 1 · The frontend': '第一步 · 前端', 'Shaders get folded': '着色器的折法',
|
||||||
|
'Step 2 · Records': '第二步 · 记录', 'Step 3 · Backends': '第三步 · 后端',
|
||||||
|
'Records can travel': '记录可以远行', 'Built for the trip': '为远行而生', 'Where it runs': '在哪里运行',
|
||||||
|
# ---- labels
|
||||||
|
'a desktop OpenGL implementation': '一个桌面 OpenGL 实现',
|
||||||
|
'Your app': '你的程序', 'OpenGL ES driver': 'OpenGL ES 驱动', 'Vulkan driver': 'Vulkan 驱动',
|
||||||
|
'no app\nchanges': '程序\n无需修改',
|
||||||
|
'OpenGL: one big switchboard': 'OpenGL:一块大开关板', 'Explicit recipes': '明确的配方',
|
||||||
|
'buffers': '缓冲区', 'textures': '纹理', 'shaders': '着色器', 'state': '状态',
|
||||||
|
'blend': '混合', 'depth': '深度', 'cull': '剔除', 'stencil': '模板', 'texture 0': '纹理 0',
|
||||||
|
'program': '程序', 'viewport': '视口',
|
||||||
|
'Draw recipe #1': '绘制配方 #1', 'Draw recipe #2': '绘制配方 #2', 'Draw recipe #3': '绘制配方 #3',
|
||||||
|
'shaders: A + B': '着色器:A + B', 'shaders: C + B': '着色器:C + B',
|
||||||
|
'blend: off': '混合:关', 'blend: on': '混合:开', 'depth: less': '深度:less', 'depth: always': '深度:always',
|
||||||
|
'cull: off': '剔除:关', 'vertex format #2': '顶点格式 #2', 'vertex format #5': '顶点格式 #5',
|
||||||
|
'GL state': 'GL 状态', 'draws': '绘制',
|
||||||
|
'Textures': '纹理', 'Uniforms': 'Uniform', 'Blend': '混合', 'Vertex arrays': '顶点数组',
|
||||||
|
'Magma (Vulkan): uses it as-is': 'Magma(Vulkan):直接使用',
|
||||||
|
'Espryt (OpenGL ES): GLSL ES': 'Espryt(OpenGL ES):转成 GLSL ES',
|
||||||
|
'Frontend': '前端', 'Backend': '后端',
|
||||||
|
'texture 7 on unit 0': '纹理 7 绑到单元 0', 'viewport 1920×1080': '视口 1920×1080',
|
||||||
|
'DRAW': '绘制', 'uniform tint': 'uniform:tint', 'texture 12 on unit 1': '纹理 12 绑到单元 1',
|
||||||
|
'new texture': '新纹理', 'only\nchanges': '只发\n变化', 'no\nwaiting': '无需\n等待',
|
||||||
|
'sent\nonce': '只发\n一次', 'blend on · depth less': '混合开 · 深度 less',
|
||||||
|
'speaks OpenGL ES': '说 OpenGL ES', 'speaks Vulkan': '说 Vulkan', 'fully\nseparate': '完全\n独立',
|
||||||
|
'One thread': '单线程', 'Two threads': '双线程', 'Two processes': '双进程', 'Two machines': '两台机器',
|
||||||
|
'spawn + shared memory': 'spawn + 共享内存', 'frontend': '前端', 'backend': '后端',
|
||||||
|
'client': '客户端', 'server': '服务端',
|
||||||
|
'client = frontend, inside your app': '客户端 = 前端,在你的程序里',
|
||||||
|
'server = backend, next to the GPU': '服务端 = 后端,紧挨着 GPU',
|
||||||
|
'Fast split': '拆开也快', 'Run-ahead': '提前跑', 'Zero-copy': '零拷贝', 'Safe failure': '安全失败',
|
||||||
|
'Two threads or processes can even beat one: both sides work at once.':
|
||||||
|
'双线程或双进程甚至能胜过单线程:两边同时干活。',
|
||||||
|
'Keep sending. Wait only for the answers you truly need.': '一直往前发,只等真正需要的回复。',
|
||||||
|
'Big buffers and window images are shared, not copied.': '大缓冲区和窗口图像直接共享,不做拷贝。',
|
||||||
|
"Server gone? Your app sees 'device lost', not a crash.": '服务端没了?程序只会看到“设备丢失”,不会崩溃。',
|
||||||
|
'Its first home: Minecraft: Java Edition via launchers like FoldCraftLauncher.':
|
||||||
|
'最初的家:通过 FoldCraftLauncher 等启动器运行 Minecraft Java 版。',
|
||||||
|
'EGL, GLX, Wayland and glvnd. Even a whole KDE Plasma desktop.':
|
||||||
|
'EGL、GLX、Wayland 和 glvnd,甚至能跑整套 KDE Plasma 桌面。',
|
||||||
|
'A drop-in opengl32.dll that speaks WGL.': '即插即用的 opengl32.dll,支持 WGL。',
|
||||||
|
'CGL and NSOpenGL, on Vulkan via MoltenVK.': 'CGL 与 NSOpenGL,经 MoltenVK 跑在 Vulkan 上。',
|
||||||
|
'KDE Plasma\non a tablet': '平板上的\nKDE Plasma',
|
||||||
|
'free & open source · LGPL-3.0': '自由开源 · LGPL-3.0',
|
||||||
|
'Come fold with us!': '一起来折吧!',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user