makepad/apps/vj/resources/effects/216_trans_card_flip.splash
Admin cb49b032df vjfx: 104 presets, engine hooks + hold stage, the videomesh engine, the audio picture, livecodable effects, and mp4 thumbnail sheets
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
2026-08-26 08:49:48 +02:00

113 lines
6.1 KiB
Text

// CARD FLIP — the frame is a wall of cards, deck A printed on the front
// of every one and deck B on the back. The fader turns them: each card
// takes its own half turn about its own horizontal axis, and the takeoffs
// are staggered so the wall changes over in a ripple instead of a snap.
//
// Pattern taught: the plane-in-3D helper (`plane_uv` below, the family's
// shared block — the reference copy lives in the Perspective doc) with a
// PER-CARD hinge. The pivot goes in as world coordinates and comes back
// out after the intersection, so a wall of independently spinning cards
// costs one helper call per fragment — the card that owns a fragment is
// the cell it falls in, and a card foreshortened off its own cell simply
// clips, which is what opens the dark seams mid-flip.
//
// THE STAGGER IS QUANTISED ON THE CELL ID — a hash of WHICH card it is,
// never of the clock — and the sweep is driven by t alone, so the wall
// looks the same on every pass of the fader and at any point in a set.
// The v flip past edge-on comes free with the intersection (that is what
// makes a turn read as a turn); deck B is sampled with it undone about the
// card's own centre, so the back of every card is the right way up.
{
name: "Card Flip"
engine: "transition"
p0: 0.5 p1: 0.5
dials: [
{name: "CARDS", bind: "p0", default: 0.5},
{name: "SPREAD", bind: "p1", default: 0.5},
{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)
let fl = 1.12
let a = self.aspect()
// CARDS: 3..10 columns; the rows follow the aspect so a card
// is roughly square.
let nx = floor(mix(3.0, 10.0, clamp(self.user.x, 0.0, 1.0)) + 0.5)
let ny = max(floor(nx / a + 0.5), 2.0)
let cell = floor(vec2(uv.x * nx, uv.y * ny))
let cu = (cell.x + 0.5) / nx
let cv = (cell.y + 0.5) / ny
// The card's signature: a stable hash of the CELL ID.
let h = fract(sin(dot(cell, vec2(127.1, 311.7))) * 43758.5453)
// SPREAD widens the takeoff window. At t = 0 every card is
// still face A, at t = 1 every card has finished its turn —
// both ends of the sweep are exact.
let lead = 0.25 + 1.5 * clamp(self.user.y, 0.0, 1.0)
let tt = clamp(tc * (1.0 + lead) - h * lead, 0.0, 1.0)
// Half a turn about the card's own horizontal axis.
let ax = tt * 3.1415927
// The hinge: this card's centre, in world units.
let piv = vec2((cu - 0.5) * a / fl, (0.5 - cv) / fl)
let p = self.plane_uv(uv, vec3(ax, 0.0, 0.0), piv, 1.0, fl)
// Clip to THIS card's cell — a neighbour leaning into it is
// that neighbour's fragment to draw, not ours.
let hit = p.z * step(abs(p.x - cu), 0.5 / nx) * step(abs(p.y - cv), 0.5 / ny)
// Front = deck A. Back = deck B with the intersection's v flip
// undone about the card's own centre line.
let front = self.deck_a(vec2(p.x, p.y)).xyz
let back = self.deck_b(vec2(p.x, 2.0 * cv - p.y)).xyz
let face = mix(back, front, p.w)
// Lambert-ish shading — exactly 1 flat, dark edge-on — plus a
// GLINT as the card passes through edge-on, hardest on the
// beat (the eased pulse is 0..1, and the glint is zero at both
// ends of the turn, so the ends stay pixel-clean).
let edge_on = 1.0 - abs(cos(ax))
let sh = 0.55 + 0.45 * abs(cos(ax))
let glint = pow(edge_on, 8.0) * (0.25 + 0.55 * self.time_beat.w)
// Behind the wall: deck B in shadow, so the seams that open
// between foreshortened cards read as depth, not as holes.
let mut c = mix(self.deck_b(uv).xyz * 0.3, face * sh, hit)
c = c + vec3(1.0, 1.0, 1.0) * (glint * hit)
// DIP: duck through black mid-flip (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)
}
}
}