""" Path-following scroll unroll: the spiral lays down ALONG its arm, led by a 3D curl. The shell's `growth` parameter truncates the spiral along its own arm (not radially), so a sequence of growth stages IS a scroll unrolling along the spiral path: an outer ring stays hidden until the arm reaches it, even if an inner point at the same clock-angle is already laid down. We render N growth stages as a flipbook stacked in one SVG. Each stage is shown in sequence via SMIL (set/animate on opacity), so the laid parchment grows along the arm. A 3D curl object (a small cylinder with a gold highlight and a cast shadow) rides the leading tip via animateMotion along the centerline path, shrinking as it spends its paper, then vanishes at the rim. This is heavier than a single-SVG animation (~1.6MB for 14 stages) but it is the only way to get a genuine path-following reveal of a filled spiral. """ from __future__ import annotations import math import re N_STAGES = 14 UNROLL_DUR = 3.6 # total seconds EYE_X, EYE_Y = 320.0, 335.0 def _centerline_d(svg: str) -> str | None: m = re.search(r' str: """A 3D paper-curl cylinder, sized for how much paper remains (shrinks as the scroll unrolls). Returned as a to be motion-animated.""" # remaining paper ~ (1 - stage_frac); curl radius shrinks from ~16 to ~5 r = 5 + 11 * (1 - stage_frac) return ( f'' # cast shadow on the parchment just behind the curl f'' # the cylinder body (the rolled paper, seen end-on) f'' # inner roll rings (paper thickness) f'' f'' # gold highlight on the rounded leading edge f'' f'' ) def build_unroll_doc(stage_svgs: list[str]) -> str: """Stack growth-stage SVGs as a flipbook + a curl riding the tip. Returns one SVG string with SMIL timing. stage_svgs[i] is the shell at growth (i+1)/N. Each stage is wrapped in a that becomes visible at its time slot and hides when the next appears, so the laid parchment advances along the arm. The final stage stays visible (freeze). """ n = len(stage_svgs) slot = UNROLL_DUR / n # strip the outer wrapper from each stage, keep inner content def inner(s): a = s.find(">", s.find("") return s[a:b] # the background (sky, particles) is identical across stages; take it from # the LAST stage once, render it always-on underneath, and only flip the # SHELL portion. But simplest robust approach: each stage is a full frame; # show one at a time. The sky is identical so no flicker on the backdrop. layers = [] for i, s in enumerate(stage_svgs): content = inner(s) # Each stage is visible only during its own [begin, begin+slot) window. # We drive this with a SINGLE using values/keyTimes over the # whole duration, so it resets cleanly on replay (setCurrentTime(0)). on_t = i / n off_t = (i + 1) / n if i == n - 1: # final stage: turn on at its start and STAY on (freeze full shell) if on_t <= 0: vals, keys = "1;1", "0;1" else: vals = "0;0;1;1" keys = f"0;{on_t:.4f};{on_t:.4f};1" elif i == 0: # first stage: on from 0, off at off_t vals = "1;1;0;0" keys = f"0;{off_t:.4f};{off_t:.4f};1" else: # middle stage: off, on at on_t, off at off_t, off after vals = "0;0;1;1;0;0" keys = f"0;{on_t:.4f};{on_t:.4f};{off_t:.4f};{off_t:.4f};1" anim = ( f'' ) start_op = "1" if i == 0 else "0" layers.append(f'{anim}{content}') # the 3D curl rides the centerline path of the FINAL stage (full spiral), # via animateMotion, shrinking over the unroll, vanishing at the end. cl = _centerline_d(stage_svgs[-1]) curl_layer = "" if cl: curl = _curl_object(0.0) curl_layer = ( f'{curl}' f'' # shrink the curl as it spends paper f'' # vanish at the very end f'' f'' ) W = H = 640 body = "".join(layers) + curl_layer return ( f'{body}' )