Loading…
a327ex.com

Anchor Website 4

Opus 4.8

Summary

Continuation of the Anchor Website project — reimplementing a327ex.com inside the Anchor 2 engine (a327ex-site/renderer/, a fork of ricochet-template). This session worked through the remaining content-fidelity backlog one item at a time: sidenotes, FFmpeg/libav vendoring, spoilers (Phase E), galleries (M4), a full image-quality overhaul (mipmaps → CPU resample-to-display-size), an image lightbox + prev/next pager, and a deep investigation + partial fix of text rendering vs the browser. Engine work is the shared Anchor2/engine/src/anchor.c.

::sidenote / ::note content-loss fix (item #1):

  • All 5 ::sidenote usages are single-line form (::sidenote <text>), all in /posts/*. The converter's directive else branch emitted a [SIDENOTE] placeholder and discarded args (the actual sentence) — a real content-loss bug.
  • On the live new theme, .sidenote / [role=note] has zero CSS (only the old-posts skin styles it), so a sidenote renders as a plain paragraph. Fix (Option A, chosen over a semantic element type): convert.lua now emits the body as a text/body element via parse_inline for single-line form; block form (::sidenote … ::end) recurses and swallows the ::end. Faithful to the live theme, and reversible since data files regenerate from source.
  • Committed to a327ex-site as 2b45c5e. Reconverted (335 pages). Flagged that ::gallery still leaked [GALLERY]/[END] (handled later in M4).

FFmpeg vendoring decision + libav engine commit (item #2):

  • The libav decoder swap (from a prior session) was uncommitted pending a vendoring decision. Measured: the 5 runtime DLLs total 88 MB (avcodec-61 alone 63 MB); build-time deps (headers + 5 import libs) only ~2.2 MB.
  • Decision = Option B (opt-in, presented A=commit-DLLs / B=script-fetch / C=runtime-load-first): commit the small build deps (matching how box2d/freetype/lua .libs are already vendored); keep the 88 MB DLLs OUT of git (root .gitignore already ignores *.dll); new engine/setup_ffmpeg.bat fetches the pinned BtbN n7.1 LGPL build into the gitignored .ffmpeg-dl/ cache and copies the DLLs next to anchor.exe. build.bat calls it post-build + seeds emoji-ball-battles/tools/ (DLLs only when missing). Removed the dead pl_mpeg.h + fixed a stale comment.
  • Committed to Anchor2 main (9015fec) and pushed; updated PLAN.md + memory.

Phase E — Spoilers (item #3, E1 block + E2 inline, multiple iterations):

  • Live model (from server/templates.lua + new-theme.js): per-line redaction bars, cursor-Y cumulative reveal (X-gated to the column), click = permanent reveal; colors are inverted from the page (dark theme → white island + black text). 4 block spoilers (all in messages/) + 1 inline <span class="spoiler-inline">.
  • E1 (block): new {type='spoiler', children} container; theme.lua got spoiler_bg/spoiler_text/spoiler_link roles (inverted) on both themes; elements.lua draw_text_redacted; canvas.lua layout + draw with reveal logic.
  • E2 (inline): parse_inline recognizes <span class="spoiler-inline">, flags inner runs spoiler_inline=N; layout propagates the flag + computes per-span union boxes; draw_text_element got a merged-bar-per-span path + si_resolve.
  • Iteration 1 (user: "big block not per-paragraph"): the block spoiler's main child was a blockquote (group/quote, 10 paras) and the non-text branch blacked out the whole container as one solid rect. Fixed with draw_redacted_el recursing redaction into nested groups.
  • Iteration 2 (user: "per-line, text hard to read"): line_gap is only ~2–3px, so the PADY had closed the gaps; over-corrected to ascent-height bars (dark gaps between lines → unreadable revealed text). Final: bar height = full line pitch (line.height + line_gap + 1) so bars meet vertically (continuous white within a paragraph, readable), with the ragged right edge showing line structure and element_gap separating paragraphs. Also fixed inline "barcode/ticks" by merging per-token bars into one bar per span per line.
  • Iteration 3 (user: click-reveal only revealed the first paragraph / inline span stayed hover-only): root cause = block spoiler and inline span are independent elements. Implemented Option A: a per-post _reveal_all flag (via the spoiler_reveal_doc global set in canvas_draw) so clicking ANY spoiler reveals every spoiler in that post; scoped per-message on the homepage.

M4 — Galleries (item #4):

  • ::gallery cols=N … ::end body lines are bare image URLs (not ::image). Converter parses each into an image child → {type='gallery', cols=N, children}; elements.lua layout_gallery_element/draw_gallery_element do an N-column grid (8px gap, row height = tallest image, matching the live .gallery CSS). Cleared the [GALLERY]/[END] leak. 5 galleries: "1 in 4" post (cols 2/3) + 3 messages.

Image quality overhaul (the big one):

  • User: downscaled images look jagged/non-antialiased vs the browser. Root cause: texture_load used GL_NEAREST (no averaging). First pass: added an opt-in 'smooth' filter (mipmaps + GL_LINEAR_MIPMAP_LINEAR) — helped but "still poor."
  • Deep dig + answer to "what does the browser do": the browser resamples the bitmap to display size with a high-quality (Lanczos/bicubic) filter, not GPU box-mipmaps. Implemented Option A: vendored stb_image_resize2.h; new engine image_info (header-only dims) + texture_load_fit (decode → stbir_resize_uint8_srgb to target size → upload); framework image_load_fit. Renderer: preload reads dims only; layout sizes from dims; draw lazily resamples each image to its on-screen size (ensure_fit, cached by width). User confirmed: "looks correct now."

Image lightbox + prev/next pager:

  • Click any image (standalone or gallery cell) → full-viewport modal: dark scrim + the image resampled to fit (native if smaller); Esc/click closes; modal handled in update (page frozen, click consumed).
  • Pager (user feature request, not on the original): hovering within 130px of the screen left/right edge shows a / chevron + hand cursor; clicking pages prev/next through that post's images only (via canvas_images(doc) over the captured spoiler_reveal_doc), wrap-around. User refinements: sharper chevron angle; arrows always-visible-but-dim (alpha 80) → near-white (245) on hover.
  • Chevron rendering bug: two semi-transparent layer_line segments doubled-up alpha at the shared tip. Confirmed the engine's sdf_polygon is the iq winding-sign SDF (concave-capable, despite a "convex" comment), so rebuilt the chevron as one filled stroked-V polygon (perpendicular back offsets + tip miter) — uniform alpha, no overlap.

Text rendering — deep dig + partial fix (user-requested investigation):

  • Mapped the full pipeline: FreeType grayscale AA (FT_LOAD_RENDERNORMAL), atlas stores RGB=white, A=coverage with a single baked coverage gamma (1.6), glyph quads blended with plain GL_SRC_ALPHA/ONE_MINUS_SRC_ALPHA onto non-sRGB FBOs (no GL_FRAMEBUFFER_SRGB) = gamma-incorrect.
  • Worked the math both directions: the baked 1.6 lift is right for light-on-dark (dark theme) but actively wrong for dark-on-light (spoilers/light theme) — it over-darkens black edges. That's the "extremely visible on the white spoiler background" issue.
  • Fix implemented: moved the gamma from the atlas bake into a luminance-directional shader remap. New SHAPE_TYPE_TEXT (7.0) + batch_add_glyph_quad so only text is affected (not image sprites); font_blit_to_buffer now stores linear coverage; u_text_gamma uniform (from g_font_gamma); text-only shader branch: cov = mix(1 - pow(1-cov, 1/g), pow(cov, 1/g), Lt) (Lt = text luminance). Light-on-dark is bit-identical to before; dark-on-light gets the inverse correction.
  • Result (user): white-on-black now matches the browser ("can't tell the difference"), but black-on-white still has the same "noise." Ruled out weight (invariant to the gamma change) AND hinting (interp 35 distorted white-on-black, no effect on black-on-white — reverted to 40). Conclusion: the residual is the grayscale-vs-ClearType-subpixel structural difference (visible only on high-contrast white). Matching it needs real subpixel (LCD) AA — a substantial, feasibility-uncertain engine project through the layer-FBO pipeline — deferred to its own future session. The gamma win is kept.

Commits at session end:

  • Anchor2 1d02f12 (image downscaling + directional text gamma), pushed to GitHub.
  • a327ex-site b315da9 (renderer: spoilers, galleries, image pipeline, lightbox + pager, new anchor.exe).
  • Session sealed as Private Session 8.

Still open / deferred: subpixel-AA text (the black-on-white residual); M4 tweet/steam/spotify cards (still [TYPE] placeholders); player polish (CJK subs, fullscreen control scaling, sub-language picker); G-tail (heading-anchor copy, pre copy button, old-posts dark skin); web delivery + M2b full media pull (pre-deploy).

🔒 Only the summary of this log is public. Private because it contains too many website internal details.