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
::sidenoteusages are single-line form (::sidenote <text>), all in/posts/*. The converter's directiveelsebranch emitted a[SIDENOTE]placeholder and discardedargs(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.luanow emits the body as atext/bodyelement viaparse_inlinefor 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::gallerystill 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.gitignorealready ignores*.dll); newengine/setup_ffmpeg.batfetches the pinned BtbN n7.1 LGPL build into the gitignored.ffmpeg-dl/cache and copies the DLLs next toanchor.exe.build.batcalls it post-build + seedsemoji-ball-battles/tools/(DLLs only when missing). Removed the deadpl_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 inmessages/) + 1 inline<span class="spoiler-inline">. - E1 (block): new
{type='spoiler', children}container;theme.luagotspoiler_bg/spoiler_text/spoiler_linkroles (inverted) on both themes;elements.luadraw_text_redacted;canvas.lualayout + draw with reveal logic. - E2 (inline):
parse_inlinerecognizes<span class="spoiler-inline">, flags inner runsspoiler_inline=N; layout propagates the flag + computes per-span union boxes;draw_text_elementgot 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 withdraw_redacted_elrecursing redaction into nested groups. - Iteration 2 (user: "per-line, text hard to read"):
line_gapis only ~2–3px, so thePADYhad 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 andelement_gapseparating 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_allflag (via thespoiler_reveal_docglobal set incanvas_draw) so clicking ANY spoiler reveals every spoiler in that post; scoped per-message on the homepage.
M4 — Galleries (item #4):
::gallery cols=N … ::endbody lines are bare image URLs (not::image). Converter parses each into an image child →{type='gallery', cols=N, children};elements.lualayout_gallery_element/draw_gallery_elementdo an N-column grid (8px gap, row height = tallest image, matching the live.galleryCSS). 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_loadusedGL_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 engineimage_info(header-only dims) +texture_load_fit(decode →stbir_resize_uint8_srgbto target size → upload); frameworkimage_load_fit. Renderer:preloadreads 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 (viacanvas_images(doc)over the capturedspoiler_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_linesegments doubled-up alpha at the shared tip. Confirmed the engine'ssdf_polygonis 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_RENDER→NORMAL), atlas storesRGB=white, A=coveragewith a single baked coverage gamma (1.6), glyph quads blended with plainGL_SRC_ALPHA/ONE_MINUS_SRC_ALPHAonto non-sRGB FBOs (noGL_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_quadso only text is affected (not image sprites);font_blit_to_buffernow stores linear coverage;u_text_gammauniform (fromg_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.