// 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) } } }