Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

papers/index: layered crossfade + multiply blend + purple letterbox + title shadow

- top-bar logo gets hard-edge text-shadow (--logo-shadow flips per theme) for
contrast over the hero
- hero letterbox swapped from cream to var(--purple); canvas fades in only
after all slides have loaded (no pop-in)
- slideshow now renders two layers every frame: cover-scaled back layer +
smaller multiply-blended front layer half a slot out of phase, so the
white paper edges of the colored-pencil scans dissolve softly and the
hero is always crossfading between two compositions
- radial destination-out vignette feathers the canvas edges so the purple
background bleeds in around the imagery

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

+106 -29
+106 -29
system/public/papers.aesthetic.computer/index.html
··· 32 32 --box-bg: rgba(255,255,255,0.03); 33 33 --box-border: rgba(255,255,255,0.1); 34 34 --col-divider: rgba(255,255,255,0.08); 35 + --logo-shadow: #000; 35 36 } 36 37 37 38 @media (prefers-color-scheme: light) { ··· 42 43 --box-bg: rgba(0,0,0,0.03); 43 44 --box-border: rgba(0,0,0,0.1); 44 45 --col-divider: rgba(0,0,0,0.08); 46 + --logo-shadow: #fff; 45 47 } 46 48 } 47 49 :root.light-mode { ··· 51 53 --box-bg: rgba(0,0,0,0.03); 52 54 --box-border: rgba(0,0,0,0.1); 53 55 --col-divider: rgba(0,0,0,0.08); 56 + --logo-shadow: #fff; 54 57 } 55 58 56 59 * { margin: 0; padding: 0; box-sizing: border-box; } ··· 396 399 aspect-ratio: 1536 / 1024; 397 400 max-height: 78vh; 398 401 min-height: 280px; 399 - /* Warm cream letterbox — matches the cream paper the colored-pencil 400 - images fade into, so contain-mode gaps on wide viewports read as 401 - intentional paper margin rather than negative space. */ 402 - background-color: #efe2c8; 402 + /* Purple letterbox the colored-pencil images soft-fade into. */ 403 + background-color: var(--purple); 403 404 } 404 405 .hero-canvas { 405 406 position: absolute; ··· 407 408 width: 100%; 408 409 height: 100%; 409 410 display: block; 411 + opacity: 0; 412 + transition: opacity 1.2s ease-in; 410 413 } 414 + .hero-canvas.loaded { opacity: 1; } 411 415 412 416 /* === Prompt-HUD corner label (modeled after give.aesthetic.computer) === */ 413 417 .top-bar { ··· 433 437 font-weight: 700; 434 438 font-size: 1.05em; 435 439 color: var(--text); 440 + text-shadow: 441 + 1px 1px 0 var(--logo-shadow), 442 + -1px 1px 0 var(--logo-shadow), 443 + 1px -1px 0 var(--logo-shadow), 444 + -1px -1px 0 var(--logo-shadow), 445 + 0 0 8px rgba(0,0,0,0.35); 436 446 } 437 447 .top-bar .logo-sep { 438 448 font-family: 'Berkeley Mono Variable', monospace; 439 449 color: var(--dim); 440 450 margin: 0 0.3em; 451 + text-shadow: 452 + 1px 1px 0 var(--logo-shadow), 453 + -1px 1px 0 var(--logo-shadow), 454 + 1px -1px 0 var(--logo-shadow), 455 + -1px -1px 0 var(--logo-shadow); 441 456 } 442 457 .top-bar .logo-ac { 443 458 font-family: 'YWFTProcessing-Regular', sans-serif; 444 459 font-size: 1.15em; 445 460 color: var(--pink); 461 + text-shadow: 462 + 1px 1px 0 var(--logo-shadow), 463 + -1px 1px 0 var(--logo-shadow), 464 + 1px -1px 0 var(--logo-shadow), 465 + -1px -1px 0 var(--logo-shadow), 466 + 0 0 8px rgba(0,0,0,0.35); 446 467 } 447 468 .top-bar .logo-dot { color: var(--cyan); } 448 469 ··· 827 848 { src: '/papers-header-v3.png', focal: { x: 0.50, y: 0.62 }, pan: { from: { s: 1.06, x: 0, y: 0 }, to: { s: 1.00, x: 0.02, y: -0.01 } } }, 828 849 { src: '/papers-header-v1.png', focal: { x: 0.46, y: 0.42 }, pan: { from: { s: 1.04, x: 0, y: 0 }, to: { s: 1.10, x: -0.01, y: 0.02 } } }, 829 850 ]; 830 - const SLIDE_DUR = 7000; 831 - const FADE_DUR = 1500; 851 + const SLIDE_DUR = 9000; 832 852 const CYCLE = SLIDES.length * SLIDE_DUR; 833 853 834 854 const images = SLIDES.map(s => { const i = new Image(); i.src = s.src; return i; }); ··· 847 867 848 868 function easeInOutSine(t) { return -(Math.cos(Math.PI * t) - 1) / 2; } 849 869 850 - function drawSlide(img, focal, pan, progress, alpha) { 870 + // mode: 'cover' fills the canvas (background layer), 871 + // 'contain-small' is a smaller in-front composition 872 + function drawSlide(img, focal, pan, progress, alpha, mode) { 851 873 if (!img.complete || !img.naturalWidth) return; 852 874 const t = easeInOutSine(progress); 853 875 const sc = pan.from.s + (pan.to.s - pan.from.s) * t; 854 876 const tx = pan.from.x + (pan.to.x - pan.from.x) * t; 855 877 const ty = pan.from.y + (pan.to.y - pan.from.y) * t; 856 - // CONTAIN baseline so the entire image is visible at sc=1.0; 857 - // sc > 1.0 zooms past contain, the focal anchor keeps the 858 - // important content (face / laptop) centered on screen. 859 - const baseScale = Math.min(cssW / img.naturalWidth, cssH / img.naturalHeight); 878 + let baseScale; 879 + if (mode === 'cover') { 880 + baseScale = Math.max(cssW / img.naturalWidth, cssH / img.naturalHeight) * 1.18; 881 + } else { 882 + baseScale = Math.min(cssW / img.naturalWidth, cssH / img.naturalHeight) * 0.82; 883 + } 860 884 const scale = baseScale * sc; 861 885 const dw = img.naturalWidth * scale; 862 886 const dh = img.naturalHeight * scale; 863 - // Anchor focal point at canvas center, then add small pan delta 864 887 const dx = (cssW / 2) - (img.naturalWidth * focal.x * scale) + tx * cssW; 865 888 const dy = (cssH / 2) - (img.naturalHeight * focal.y * scale) + ty * cssH; 866 889 ctx.save(); ··· 869 892 ctx.restore(); 870 893 } 871 894 895 + // Triangular envelope peaking at center of each slide window — 896 + // every layer is always crossfading with its neighbours. 897 + function alphaFor(globalT, slotStart, slotDur) { 898 + const half = slotDur / 2; 899 + const d = Math.min( 900 + Math.abs(globalT - slotStart), 901 + Math.abs(globalT - slotStart - CYCLE), 902 + Math.abs(globalT - slotStart + CYCLE) 903 + ); 904 + if (d >= slotDur) return 0; 905 + const x = 1 - d / slotDur; 906 + return easeInOutSine(x); 907 + } 908 + 872 909 const start = performance.now(); 873 910 let lastFrame = 0; 874 911 function frame(now) { ··· 877 914 878 915 const elapsed = reduced ? 0 : (now - start); 879 916 const cyclePos = ((elapsed % CYCLE) + CYCLE) % CYCLE; 880 - const idx = Math.floor(cyclePos / SLIDE_DUR); 881 - const slideElapsed = cyclePos - idx * SLIDE_DUR; 882 - const slideProg = slideElapsed / SLIDE_DUR; 883 - const fadeStartProg = (SLIDE_DUR - FADE_DUR) / SLIDE_DUR; 917 + 918 + // BACK LAYER — covered, fills full bleed. Each slide gets a 919 + // triangular alpha envelope so neighbours always blend. 920 + for (let i = 0; i < SLIDES.length; i++) { 921 + const slotStart = i * SLIDE_DUR; 922 + const a = alphaFor(cyclePos, slotStart, SLIDE_DUR); 923 + if (a <= 0.001) continue; 924 + const localProg = ((cyclePos - slotStart + CYCLE) % CYCLE) / SLIDE_DUR; 925 + const prog = Math.max(0, Math.min(1, localProg)); 926 + drawSlide(images[i], SLIDES[i].focal, SLIDES[i].pan, prog, a, 'cover'); 927 + } 884 928 885 - if (slideProg > fadeStartProg) { 886 - // Crossfade: draw incoming next slide underneath at full alpha, 887 - // current slide on top fading out 888 - const fadeT = (slideProg - fadeStartProg) / (1 - fadeStartProg); 889 - const nextIdx = (idx + 1) % SLIDES.length; 890 - drawSlide(images[nextIdx], SLIDES[nextIdx].focal, SLIDES[nextIdx].pan, 0, 1); 891 - drawSlide(images[idx], SLIDES[idx].focal, SLIDES[idx].pan, slideProg, 1 - fadeT); 892 - } else { 893 - drawSlide(images[idx], SLIDES[idx].focal, SLIDES[idx].pan, slideProg, 1); 929 + // FRONT LAYER — smaller, multiply-blended so the cream paper 930 + // 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. 934 + ctx.globalCompositeOperation = 'multiply'; 935 + const frontPhase = (cyclePos + SLIDE_DUR / 2) % CYCLE; 936 + for (let i = 0; i < SLIDES.length; i++) { 937 + const slotStart = i * SLIDE_DUR; 938 + const a = alphaFor(frontPhase, slotStart, SLIDE_DUR); 939 + if (a <= 0.001) continue; 940 + const localProg = ((frontPhase - slotStart + CYCLE) % CYCLE) / SLIDE_DUR; 941 + 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'); 894 943 } 944 + ctx.globalCompositeOperation = 'source-over'; 945 + 946 + // Soft vignette mask — fade canvas alpha to 0 at edges so 947 + // the purple letterbox bleeds in around the imagery. 948 + 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 951 + ); 952 + 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)'); 955 + ctx.globalCompositeOperation = 'destination-out'; 956 + ctx.fillStyle = grad; 957 + ctx.fillRect(0, 0, cssW, cssH); 958 + ctx.globalCompositeOperation = 'source-over'; 895 959 896 960 if (!reduced || lastFrame === 0) requestAnimationFrame(frame); 897 961 lastFrame = now; 898 962 } 899 963 900 - // Kick off as soon as ANY image is ready (others load progressively) 964 + // Wait for ALL images before kicking off — prevents pop-in of 965 + // late-arriving slides as they load mid-cycle. 901 966 let kicked = false; 902 - function kick() { if (!kicked) { kicked = true; requestAnimationFrame(frame); } } 967 + function kick() { 968 + if (kicked) return; 969 + kicked = true; 970 + canvas.classList.add('loaded'); 971 + requestAnimationFrame(frame); 972 + } 973 + let pending = images.length; 903 974 images.forEach(img => { 904 - if (img.complete && img.naturalWidth) kick(); 905 - else img.addEventListener('load', kick, { once: true }); 975 + const done = () => { if (--pending <= 0) kick(); }; 976 + if (img.complete && img.naturalWidth) done(); 977 + else { 978 + img.addEventListener('load', done, { once: true }); 979 + img.addEventListener('error', done, { once: true }); 980 + } 906 981 }); 982 + // Safety fallback — if something stalls, kick after 4s anyway. 983 + setTimeout(kick, 4000); 907 984 })(); 908 985 909 986 // Cache-bust version — updated by papermill deploy