mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
378 lines
15 KiB
Python
378 lines
15 KiB
Python
"""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)])
|