Anchor Website 15
Fable 5
Summary
THE CUTOVER SESSION: finished the Box2D letter-drop (full rotated video controls + the reset escalation ladder), posted the announcement, and flipped a327ex.com to the engine renderer — the site now lives on Anchor 2 with the Lua server underneath as the SEO layer. Then a long post-cutover hardening tail: content-hashed bundle caching, the video seek/CORS architecture, and several web-only fixes.
Rotated video controls in the drop (the "too hard" feature, done in ~120 lines):
- s14 had rejected in-place controls on fallen video cards as too hard ("controls are axis-aligned so they don't fit a rotated card"). Fresh analysis found three structural facts that made it nearly free: (1)
video_controls_geom(r)derives every control rect purely from the card rect; (2) the drop already re-draws whole cards through a rotatinglayer_pushtransform, so rotated RENDERING of controls/title/popups costs nothing; (3) the stashed hit-rects (el._rect,_time_rect,_title_rect,_yt_btn_rect) are doc-space during the drop — self-consistent with a doc-space mouse. - The entire feature reduced to inverse-transforming the mouse into the fallen body's frame:
drop_local_mouse= p = center + R(-a)·(m − draw_pos), verified against the engine's[c -s; s c]matrix inlayer_push. video_controls_update(el, r, mx, my)gained optional mouse params; newdrop_video_update()runs the full unmodified controls bar for the active card each fall-frame (seek/volume/speed/CC/time-toggle/title all work rotated) + topmost-wins click routing on other video cards (activate / watch-on-YouTube / placeholder cards). Newdoc_mouse_position()override in elements.lua keeps draw-side hover visuals correct (set per-body indraw_drop); draw-side clicks are suppressed during the drop (ui_mouse_pressedcleared around the element redraws) — this also fixed a latent ghost-click bug (clicking a video's pre-drop position started playback).- Fullscreen escapes rotation: the normal axis-aligned player draws over the scene (update + draw gates handle fullscreen during the drop), Esc back to the pile. Owner: "works wonderfully."
The reset mechanic (increment 3) + the announcement test post:
- The
[this](#drop)link stays STATIC:draw_text_elementrecords_link_rectswith url#dropintodrop_linksduring capture;classify_glyphroutes their glyphs todrop.statics, drawn upright + underlined ON TOP of the pile (a settling card can't hide the escape hatch). - Records refactor: every drop record carries hx/hy (home center) + dx/dy/da (current draw transform — physics-synced in 'fall' via
drop_sync_physics, interpolated in 'return' viadrop_return_step);draw_dropreads ONLY records.drop_return_beginsamples start transforms then DESTROYS all bodies + walls immediately (the tween is pure draw-side); 0.8s smoothstep home, angles unwind ≤ half-turn (Box2D v3b2Rot_GetAngleis normalized). At p=1 the untouched page resumes; a playing video plays straight through. - Tried easeOutBack overshoot on request, owner reverted to smoothstep at 0.8s (from 0.6).
- Local test post: found the owner's verbatim announcement text in the s14 vault log (
private-session-18.md); realized "the video below (the Box3D one)" = the REAL Box3D announcement message (2026-07-01-114939,::youtube jr_Fzl2XwKU— Erin Catto's "Introducing Box3D") that sits below the announcement in the feed. Wrotemessages/drop-test.md(untracked; convert readsmessages/*.md), pulledjr_Fzl2XwKUlocally (220MB, the s14 "pre-existing drift" id).
Feel iteration on the drop physics (owner-directed):
drop_push: random sideways velocity on non-letter bodies, scaled INVERSELY with sqrt(area) (~mass) — heavy cards drift, light avatars fling; 0.4–1.0 magnitude roll, 400 px/s cap,DROP_PUSH=60000. Later boosted 2.5× for videos/tweets and added to divider bars (feather-light → they max the cap).- The
#droplink became SOLID as well as static (one static wall box per fragment — letters pile on the word). - The Box3D video (
DROP_HERO='jr_Fzl2XwKU') launches UPWARD 900–1200 px/s with a guaranteed 4–7 rad/s spin. Tried making the launch radial from the blast; owner reverted to straight-up. - BLAST EPICENTER: the phrase "absolutely blown the fuck out!" (
drop_blast_phraseglobal) —draw_text_elementmatches it as a consecutive word-token run (survives line wrap) and records per-word rects; its glyphs explode radially 1400–2000 px/s + a 250 px/s up-kick + drop_spin(30); video spin variation doubled (drop_spin(1.6)).
The escalation ladder (hunt mode + the bomb):
drop.loopscounts completed resets. Loops 10–19 = HUNT: the word falls too — ONE rigid body per link fragment (drop.words; glyphs as fixed offsets drawn rigidly with the block, underline rides it; first version fell as individual letters, owner asked for a solid word block). Reset = click the fallen block (rotated hit-test,HUNT_PAD=10).- Loops ≥20 = BOMB: clicking the hunted word calls
drop_explodeinstead of resetting — radial velocity kick to every body from the word's position, linear falloff (BLAST_R=700,BLAST_V=2600), the word itself launches in a random direction. Repeatable; reload is the only way back. - Sidebar caret fix: the disclosure chevrons are
layer_linestrokes — invisible to the glyph capture — so they vanished during the drop.sidebar_caretbecame a global stroke-draw; carets are captured intodrop_caretsand become tiny letter bodies that tumble/tween like everything else.
⭐ ENGINE BUG: the 20-click freeze = shape-pool corruption (Anchor2 859e43d):
- Owner: after exactly 20 clicks (10 loops) everything stopped being interactable. Diagnosis: the per-shape user-data pool in anchor.c was a BUMP allocator (
shape_user_data_pool[shape_user_data_count++],MAX_SHAPE_USER_DATA=4096) with NO recycling and (except chains) NO bounds check —b2DestroyBodyfreed the Box2D shapes but not the slots. ~400 shapes per drop cycle × 10 loops crossed 4096 and wrote past the array into adjacent statics (memory corruption → everything dead, page looked normal). - Fix: a free list —
shape_user_data_alloc()(recycled slots first, then bump, NULL on true exhaustion → clean luaL_error) +shape_user_data_recycle()called fromphysics_destroy_body(walks the body's shapes, 32-cap convention) andphysics_shape_destroy; both world-reset sites clear the free count. REUSABLE for any Anchor2 game that churns bodies.
Staging deploy + ship prep + THE FLIP:
- Touch-scroll modal fix (found pre-deploy): the drain sat in the normal update path; the drop/lightbox/fullscreen branches return before it, so mobile swipes during any modal accumulated in the engine counter and landed as one violent scroll on close. Drain moved to the top of
update(), modal branches discard. - anchor.c committed to Anchor2:
1903561(glyph capture +layer_draw_glyph_c), then859e43d(pool fix), later565ba1b(media base). Renderer committed to a327ex-site (d7cd5e5+ follow-ups).deploy.sh --content --engineshipped everything to staging; owner verified on desktop + phone ("not chugging"). - Ship prep: F9 trigger removed (bind + both input_pressed checks),
drop-test.mddeleted, the REAL announcement posted via the /msg flow (messages/2026-07-01-213155.md, owner's final wording — he revised it twice in-session; single paragraph,[this](#drop)), renderer synced. - THE FLIP:
sudo cp deploy/nginx-a327ex-engine.conf /etc/nginx/sites-available/a327ex && nginx -t && reloadon the VPS. Verified: engine-boot.js injected in root HTML, renderer.data/anchor.wasm 200 at the apex, per-page server-rendered<title>s intact (SEO layer), lazy /data + /media-cache→R2 proxy OK. REVERT = re-applydeploy/nginx-a327ex.conf.
Post-cutover hardening:
- Tab title: the engine's SDL window title ("a327ex site renderer") stomped
document.titleon web. Boot title → 'a327ex.com';engine_set_title(existing binding) on home/article/settings navigation matching the Lua site's exact format ("home — a327ex.com", "— a327ex.com"). - ⭐ Content-hashed bundle caching: deploy.sh stages
renderer-<md5:10>.data/anchor-<h>.wasm+ pre-compressed.gz(12.2→6.8 MB, gzip -6) and rewrites the staged engine-boot.js's newASSET_VERlocateFile map (tools/ source keeps plain dev names); nginx serves hashed namesmax-age=1y immutable+gzip_static; plain names stay no-cache as fallback/verify pair; prune keeps 2 newest. A redeploy = a NEW name → no purging ever, no loader/bundle mismatch. The bundle changes on essentially every post (message bodies + index are bundled). ⚠ The CF Cache Rule making/renderer-*+/anchor-*edge-eligible was NOT yet added at session end (assets show DYNAMIC). - The "slow desktop loads" episode: measured from the owner's own connection — 12MB through CF = 95s at 128KB/s, direct to VPS = 8s at 1.5MB/s; later the CF path recovered to 8.7MB/s with no changes. Verdict: transient CF↔origin route congestion (owner's "other websites kinda slow too"). Mobile was instant via immutable browser cache / different PoP. The caching work makes the site robust to recurrence.
- ⭐ VIDEO SEEK: two-layer fix. Symptom: seeking to 5:00 took minutes, then everything BEFORE 5:00 was instant = sequential download. Root cause: CF's behavior for a Range request on an UNCACHED-but-CACHEABLE object (mp4 = default-cached extension) is to pull the WHOLE file from origin to fill its cache, serving the client's offset only when the stream reaches it. (1) Interim (no dashboard): nginx
mapaddsCache-Control: no-storeto proxied/media-cache/*.mp4→ CF BYPASS → ranges forward (verified: 544MB video, 70% seek, 0.79s TTFB; posters/subs keep edge caching). (2) Proper: web<video>streams STRAIGHT from R2 — engineweb_video_createhonorsModule.__mediaBase(set only by the prod engine-boot.js → local dev stays same-origin) +el.crossOrigin='anonymous'(WebGL texImage2D taint); owner added the R2 bucket CORS policy via dashboard (PutBucketCors via the rclone S3 creds failed AccessDenied — object-scoped token) + purged the pre-CORS edge-cached copies (CF cache doesn't vary on Origin — stale headerless copies would fail CORS-mode loads). Native R2 ranges (0.2s) + edge caching; VPS out of the video byte path. - Web aspect-fit fix:
<video>dims arrive async (loadedmetadata) → the load-timevideo_fit_cardsaw 0×0 and filled the card → square videos (Sid & Daydreams) stretched to 16:9._fit_pendingflag set when the fit is blind;update()re-fits (card or fullscreen) oncevideo_get_sizeturns real. Desktop (sync libav) unaffected. - Hand-cursor fix:
drop_video_update's active-video branch overwrote the hand0 flag (hand = video_ctrl_hot or over_active) instead of OR-ing — hovering the staticthisshowed an arrow whenever any video was active. One-word fix (hand = hand or ...). - The owner deleted the
new.a327ex.comstaging DNS record (post-cutover duplicate mirror).
Remaining (polish only): the CF Cache Rule for the hashed bundle (one dashboard step); per-post fidelity bucket (8 dead-embed fallback cards + visual-divergence sweep); music-video DMCA decision; phone-rotation re-fit; Korean subtitle fallback font; web-video polish (WebAudio graph, play-start flash, texSubImage2D); optional bundle split (assets-vs-data packs).
🔒 Only the summary of this log is public. Private because it contains too many website internal details.