Squashed from work; the fine-grained history is under tag archive/work-2026-08-26: - vjfx: 104 new presets — the transition lane fills out and the screen family goes wide - vjfx: three lanes land — engine hooks + hold stage, the videomesh engine, and the audio picture - vj: the thumbnail pipeline becomes one honest machine, and effects go livecodable - vj: thumbnails become mp4 — hardware-coded sheets at measured-4K cells, and the bake stops racing the GPU - store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log - store: the ceremony dies — batch publish, one transaction, and the engine stops re-reading its own log - vj: the console grows real transports, and the deck stops lying about reverse - vj: reverse earns a memory, and the effects stop aging - fab: a 3D creation shell and the viewer built on it
93 lines
4.7 KiB
Text
93 lines
4.7 KiB
Text
// TUMBLE AWAY — deck A is thrown: the card pitches and yaws at once while
|
|
// it falls back into the room, flashing its dimmed back face on every half
|
|
// turn, and deck B stands whole behind it the moment it clears.
|
|
//
|
|
// Pattern taught: the plane-in-3D helper (`plane_uv` below, the family's
|
|
// shared block — the reference copy lives in the Perspective doc). Camera
|
|
// at the origin looking down -z, one ray per fragment; the RAY and the EYE
|
|
// are pushed into the plane's own frame by the transposed rotation, where
|
|
// the plane is z = 0 and one divide gives the hit. Two axes turning at
|
|
// once is then just two more cosines — no matrices, no geometry.
|
|
//
|
|
// The back face is FREE: past edge-on the intersection reverses the axis
|
|
// it turned about, so the mirrored picture is already there. It is dimmed
|
|
// and drained of colour, which is what sells card stock rather than glass.
|
|
{
|
|
name: "Tumble Away"
|
|
engine: "transition"
|
|
p0: 0.5
|
|
dials: [
|
|
{name: "TUMBLE", bind: "p0", default: 0.5},
|
|
{name: "FLIP", bind: "p1", default: 0.0},
|
|
{name: "DIP", bind: "p2", default: 0.0}
|
|
]
|
|
shader: draw.DrawVjFxDuo {
|
|
// ---- THE SHARED HELPER: one ray, one rotated video plane --------
|
|
// The plane is the rectangle of half-extents (aspect, 1) * 0.5/f
|
|
// whose HINGE sits at world (piv.x, piv.y, -d), spun about that
|
|
// hinge by the Euler angles `ang` (Rz then Ry then Rx, radians).
|
|
// Returns (plane u, plane v, on-quad 0/1, front-facing 0/1).
|
|
plane_uv: fn(uv: vec2, ang: vec3, piv: vec2, d: float, f: float) -> vec4 {
|
|
let a = self.aspect()
|
|
let c0 = cos(ang.x)
|
|
let s0 = sin(ang.x)
|
|
let c1 = cos(ang.y)
|
|
let s1 = sin(ang.y)
|
|
let c2 = cos(ang.z)
|
|
let s2 = sin(ang.z)
|
|
// The ray through this fragment (y up) and the eye, both
|
|
// measured from the hinge.
|
|
let rd = vec3((uv.x - 0.5) * a, 0.5 - uv.y, 0.0 - f)
|
|
let ro = vec3(0.0 - piv.x, 0.0 - piv.y, d)
|
|
let r1 = vec3(rd.x * c2 + rd.y * s2, rd.y * c2 - rd.x * s2, rd.z)
|
|
let o1 = vec3(ro.x * c2 + ro.y * s2, ro.y * c2 - ro.x * s2, ro.z)
|
|
let r2 = vec3(r1.x * c1 - r1.z * s1, r1.y, r1.x * s1 + r1.z * c1)
|
|
let o2 = vec3(o1.x * c1 - o1.z * s1, o1.y, o1.x * s1 + o1.z * c1)
|
|
let r3 = vec3(r2.x, r2.y * c0 + r2.z * s0, r2.z * c0 - r2.y * s0)
|
|
let o3 = vec3(o2.x, o2.y * c0 + o2.z * s0, o2.z * c0 - o2.y * s0)
|
|
// Intersect z = 0. A ray running parallel to the plane is
|
|
// NUDGED, never divided by zero — it lands far off the quad.
|
|
let den = r3.z + (1.0 - step(0.0001, abs(r3.z))) * 0.001
|
|
let k = 0.0 - o3.z / den
|
|
let hx = o3.x + k * r3.x + piv.x
|
|
let hy = o3.y + k * r3.y + piv.y
|
|
let pu = hx * f / a + 0.5
|
|
let pv = 0.5 - hy * f
|
|
let onq = step(0.0, pu) * step(pu, 1.0) * step(0.0, pv) * step(pv, 1.0)
|
|
* step(0.001, k)
|
|
return vec4(pu, pv, onq, step(0.0, o3.z))
|
|
}
|
|
|
|
trans: fn(uv: vec2, t: float) -> vec4 {
|
|
let tc = clamp(t, 0.0, 1.0)
|
|
// The throw accelerates; the eased beat pulse gives it a shove
|
|
// (pulse is 0..1, so the whole term stays bounded).
|
|
let e = pow(tc, 1.35) * (1.0 + 0.14 * self.time_beat.w)
|
|
let dir = mix(1.0, -1.0, step(0.5, self.user.y))
|
|
let hard = clamp(self.user.x, 0.0, 1.0)
|
|
// Two axes at once, deliberately detuned so the card never
|
|
// repeats the same silhouette on the way out.
|
|
let an = vec3(
|
|
e * (1.0 + 2.4 * hard) * dir,
|
|
e * (0.7 + 1.6 * hard),
|
|
e * 0.55 * dir
|
|
)
|
|
let d = 1.0 + e * 1.9
|
|
let p = self.plane_uv(uv, an, vec2(0.0, 0.0), d, 1.12)
|
|
// Guarantee the far end: the last of the card is gone by t = 1.
|
|
let vis = p.z * (1.0 - smoothstep(0.84, 1.0, tc))
|
|
let ca = self.deck_a(vec2(p.x, p.y))
|
|
// Back face: mirrored for free, dimmed and drained.
|
|
let lum = dot(ca.xyz, vec3(0.299, 0.587, 0.114))
|
|
let back = mix(ca.xyz, vec3(lum, lum, lum), 0.5) * 0.30
|
|
let card = mix(back, ca.xyz, p.w)
|
|
let ed = min(min(p.x, 1.0 - p.x), min(p.y, 1.0 - p.y))
|
|
let rim = (1.0 - smoothstep(0.0, 0.006, ed)) * vis
|
|
let mut c = mix(self.deck_b(uv).xyz, card, vis)
|
|
c = c + vec3(1.0, 1.0, 1.0) * (rim * 0.35)
|
|
// DIP: duck through black mid-tumble (0 = off, the stock look).
|
|
let dim = 1.0 - 4.0 * tc * (1.0 - tc) * self.user.z * 0.9
|
|
return vec4(c * dim, 1.0)
|
|
}
|
|
}
|
|
}
|