Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

papers/index: hero anti-pop + non-duplicate front/back slides + bottom fade-to-bg

- guarantee front layer never shows the same slide as the back layer:
remap front slide index by half the slide count and double-check at draw
time with a backActive Set
- prevent first-frame snap: paint one frame before adding `.loaded` so the
bitmap is fully composited the moment the CSS opacity ramp begins; start
the slideshow clock at kick time so cycle phase is deterministic
- add `.hero::after` linear gradient that dissolves the bottom of the hero
into var(--bg) — image area no longer ends on a hard edge against the
page background
- soften the radial corner vignette; bottom is now CSS-driven so the canvas
vignette only feathers the sides/top into the purple letterbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+67 -15
+67 -15
system/public/papers.aesthetic.computer/index.html
··· 409 409 height: 100%; 410 410 display: block; 411 411 opacity: 0; 412 - transition: opacity 1.2s ease-in; 412 + transition: opacity 1.8s ease-out; 413 413 } 414 414 .hero-canvas.loaded { opacity: 1; } 415 + /* Bottom linear fade into the page background. Sits above the canvas 416 + so the hero dissolves into the body color rather than ending on a 417 + hard edge. */ 418 + .hero::after { 419 + content: ''; 420 + position: absolute; 421 + inset: 0; 422 + pointer-events: none; 423 + background: linear-gradient(to bottom, 424 + transparent 0%, 425 + transparent 55%, 426 + var(--bg) 100%); 427 + z-index: 2; 428 + } 415 429 416 430 /* === Prompt-HUD corner label (modeled after give.aesthetic.computer) === */ 417 431 .top-bar { ··· 906 920 return easeInOutSine(x); 907 921 } 908 922 909 - const start = performance.now(); 923 + // Front-layer slide is offset by half the slide count so the 924 + // foreground image is *never* the same one currently dominating 925 + // the background. With 4 slides this means: while back blends 926 + // 0↔1, front shows 2↔3, etc. 927 + const FRONT_OFFSET = Math.floor(SLIDES.length / 2); 928 + // Half-cycle phase shift so the front layer is also peaking 929 + // between back-layer transitions instead of in lockstep. 930 + const FRONT_PHASE_SHIFT = (FRONT_OFFSET * SLIDE_DUR) + (SLIDE_DUR / 2); 931 + 932 + let start = 0; 910 933 let lastFrame = 0; 911 934 function frame(now) { 912 935 if (cssW === 0 || cssH === 0) { resize(); } ··· 914 937 915 938 const elapsed = reduced ? 0 : (now - start); 916 939 const cyclePos = ((elapsed % CYCLE) + CYCLE) % CYCLE; 940 + 941 + // Track which slide indices are active in BACK so we can 942 + // guarantee FRONT never picks the same one (defence in 943 + // depth — the offset already enforces this for 4 slides 944 + // but the check keeps the invariant if SLIDES grows). 945 + const backActive = new Set(); 917 946 918 947 // BACK LAYER — covered, fills full bleed. Each slide gets a 919 948 // triangular alpha envelope so neighbours always blend. ··· 921 950 const slotStart = i * SLIDE_DUR; 922 951 const a = alphaFor(cyclePos, slotStart, SLIDE_DUR); 923 952 if (a <= 0.001) continue; 953 + backActive.add(i); 924 954 const localProg = ((cyclePos - slotStart + CYCLE) % CYCLE) / SLIDE_DUR; 925 955 const prog = Math.max(0, Math.min(1, localProg)); 926 956 drawSlide(images[i], SLIDES[i].focal, SLIDES[i].pan, prog, a, 'cover'); ··· 928 958 929 959 // FRONT LAYER — smaller, multiply-blended so the cream paper 930 960 // edges of the colored-pencil scans dissolve into the back 931 - // layer instead of stamping a hard rectangle. Half a slot 932 - // out of phase so the foreground composition is always a 933 - // *different* slide than the dominant background. 961 + // layer instead of stamping a hard rectangle. Slide indices 962 + // are remapped via FRONT_OFFSET so the foreground is always 963 + // a *different* image than the background. 934 964 ctx.globalCompositeOperation = 'multiply'; 935 - const frontPhase = (cyclePos + SLIDE_DUR / 2) % CYCLE; 965 + const frontPhase = (cyclePos + FRONT_PHASE_SHIFT) % CYCLE; 936 966 for (let i = 0; i < SLIDES.length; i++) { 937 967 const slotStart = i * SLIDE_DUR; 938 968 const a = alphaFor(frontPhase, slotStart, SLIDE_DUR); 939 969 if (a <= 0.001) continue; 970 + const slideIdx = (i + FRONT_OFFSET) % SLIDES.length; 971 + if (backActive.has(slideIdx)) continue; // never duplicate 940 972 const localProg = ((frontPhase - slotStart + CYCLE) % CYCLE) / SLIDE_DUR; 941 973 const prog = Math.max(0, Math.min(1, localProg)); 942 - drawSlide(images[i], SLIDES[i].focal, SLIDES[i].pan, prog, a * 0.85, 'contain-small'); 974 + drawSlide( 975 + images[slideIdx], 976 + SLIDES[slideIdx].focal, 977 + SLIDES[slideIdx].pan, 978 + prog, 979 + a * 0.85, 980 + 'contain-small' 981 + ); 943 982 } 944 983 ctx.globalCompositeOperation = 'source-over'; 945 984 946 - // Soft vignette mask — fade canvas alpha to 0 at edges so 947 - // the purple letterbox bleeds in around the imagery. 985 + // Soft side/top vignette — fades the canvas to transparent at 986 + // the corners so the purple letterbox bleeds in. The bottom 987 + // fade-to-page-bg is handled by .hero::after in CSS so it 988 + // hits the page background even when imagery is opaque here. 948 989 const grad = ctx.createRadialGradient( 949 - cssW / 2, cssH / 2, Math.min(cssW, cssH) * 0.28, 950 - cssW / 2, cssH / 2, Math.max(cssW, cssH) * 0.72 990 + cssW / 2, cssH / 2, Math.min(cssW, cssH) * 0.32, 991 + cssW / 2, cssH / 2, Math.max(cssW, cssH) * 0.78 951 992 ); 952 993 grad.addColorStop(0, 'rgba(0,0,0,0)'); 953 - grad.addColorStop(0.7, 'rgba(0,0,0,0.35)'); 954 - grad.addColorStop(1, 'rgba(0,0,0,1)'); 994 + grad.addColorStop(0.75, 'rgba(0,0,0,0.30)'); 995 + grad.addColorStop(1, 'rgba(0,0,0,0.85)'); 955 996 ctx.globalCompositeOperation = 'destination-out'; 956 997 ctx.fillStyle = grad; 957 998 ctx.fillRect(0, 0, cssW, cssH); ··· 967 1008 function kick() { 968 1009 if (kicked) return; 969 1010 kicked = true; 970 - canvas.classList.add('loaded'); 971 - requestAnimationFrame(frame); 1011 + // Start the cycle clock *now* so slide 0 begins at its 1012 + // peak with the canvas still invisible — by the time the 1013 + // opacity fade-in starts, the bitmap is already fully 1014 + // painted, so the user sees a smooth alpha rise rather 1015 + // than content snapping in mid-transition. 1016 + start = performance.now(); 1017 + // Paint one frame before triggering the CSS fade-in so the 1018 + // canvas has real pixels the moment opacity begins ramping. 1019 + frame(start); 1020 + requestAnimationFrame(() => { 1021 + canvas.classList.add('loaded'); 1022 + requestAnimationFrame(frame); 1023 + }); 972 1024 } 973 1025 let pending = images.length; 974 1026 images.forEach(img => {