Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
15 kB · 463 lines
HTML
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>justanothersystem.org — Jeffrey Alan Scudder</title> <meta name="author" content="Jeffrey Alan Scudder"> <meta name="description" content="justanothersystem.org — Stretched paintings by Jeffrey Alan Scudder."> <link rel="icon" href="https://aesthetic.computer/favicon.ico">
<!-- Social sharing meta tags --> <meta property="og:title" content="justanothersystem.org" /> <meta property="og:description" content="Stretched paintings by Jeffrey Alan Scudder." /> <meta property="og:url" content="https://justanothersystem.org" /> <meta property="og:type" content="website" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="justanothersystem.org" /> <meta name="twitter:description" content="Stretched paintings by Jeffrey Alan Scudder." /> <meta name="twitter:site" content="@whistlegraph" />
<style> @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap');
:root { --bg: #000; --fg: #f0f0f0; --dim: #666; --pink-dot: #ff4ca8; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
body { font-family: 'IBM Plex Mono', monospace; background: var(--bg); color: var(--fg); }
/* Slideshow fills the viewport */ .slideshow { position: fixed; inset: 0; z-index: 0; }
.slide { position: absolute; inset: 0; opacity: 0; transition: opacity 1.6s ease-in-out; will-change: opacity, transform; }
.slide.active { opacity: 1; }
.slide img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; filter: contrast(1.05) saturate(1.08); image-rendering: -webkit-optimize-contrast; }
/* Ken Burns animations — 4 variations */ @keyframes kb-a { 0% { transform: scale(1.0) translate(0%, 0%); } 100% { transform: scale(1.15) translate(2%, 1.5%); } }
@keyframes kb-b { 0% { transform: scale(1.15) translate(2%, -1%); } 100% { transform: scale(1.0) translate(-1%, 1%); } }
@keyframes kb-c { 0% { transform: scale(1.0) translate(-1.5%, 1%); } 100% { transform: scale(1.12) translate(1%, -1.5%); } }
@keyframes kb-d { 0% { transform: scale(1.12) translate(1%, 1.5%); } 100% { transform: scale(1.0) translate(-2%, -1%); } }
.slide.kb-a img { animation: kb-a 8s ease-in-out both; } .slide.kb-b img { animation: kb-b 8s ease-in-out both; } .slide.kb-c img { animation: kb-c 8s ease-in-out both; } .slide.kb-d img { animation: kb-d 8s ease-in-out both; }
/* Header overlay */ .overlay { position: fixed; inset: 0; z-index: 1; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; padding: 1.5rem; }
.overlay a, .overlay button { pointer-events: auto; }
.top-bar { display: flex; justify-content: space-between; align-items: flex-start; }
.site-title { font-size: 1.6rem; font-weight: 500; letter-spacing: -0.02em; text-shadow: 0 2px 4px rgba(0,0,0,0.9), 0 4px 12px rgba(0,0,0,0.6), 0 0 30px rgba(0,0,0,0.5); }
.site-title a { color: var(--fg); text-decoration: none; }
/* Bottom bar */ .bottom-bar { display: flex; justify-content: flex-end; align-items: flex-end; }
.location { font-size: 1.6rem; color: var(--fg); text-shadow: 0 2px 4px rgba(0,0,0,0.9), 0 4px 12px rgba(0,0,0,0.6); text-align: right; text-decoration: none; transition: opacity 0.8s ease; }
.location:empty { opacity: 0; }
.progress-bar { position: fixed; bottom: 0; left: 0; height: 2px; background: rgba(255,255,255,0.3); transition: width linear; z-index: 2; } </style></head><body> <div class="slideshow" id="slideshow"></div>
<div class="overlay"> <div class="top-bar"> <h1 class="site-title"><a href="/cv">Jeffrey Alan Scudder</a></h1> </div> <div class="bottom-bar"> <a href="/cv" class="location" id="location"></a> </div> </div>
<div class="progress-bar" id="progressBar"></div>
<script> const ASSET_BASE = 'https://assets.aesthetic.computer/justanothersystem/slideshow/'; const MANIFEST_URL = '/manifest-updated.json'; const SLIDE_DURATION = 8000; const ZOOM_DURATION = 4000; // ms for perspective zoom-out const KB_CLASSES = ['kb-a', 'kb-b', 'kb-c', 'kb-d'];
const slideshowEl = document.getElementById('slideshow'); const locationEl = document.getElementById('location'); const progressBar = document.getElementById('progressBar');
let slides = []; let currentIndex = 0; let slideTimer = null;
function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; }
function formatDate(dateStr) { if (!dateStr) return ''; const [y, m, d] = dateStr.split('-'); const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return `${months[parseInt(m, 10) - 1]} ${parseInt(d, 10)}, ${y}`; }
// Compute CSS matrix3d that maps source quad to destination quad. // corners: [[x%,y%], ...] TL, TR, BR, BL of artwork in image. // We want to transform the image so those corners fill the viewport, // then animate back to identity (no transform). function computePerspectiveTransform(corners, imgW, imgH, vpW, vpH) { // Source points (artwork corners in pixels) const src = corners.map(([xp, yp]) => [xp / 100 * imgW, yp / 100 * imgH]);
// Compute artwork aspect ratio from corners const artW = Math.sqrt((src[1][0]-src[0][0])**2 + (src[1][1]-src[0][1])**2); const artH = Math.sqrt((src[3][0]-src[0][0])**2 + (src[3][1]-src[0][1])**2); const artAspect = artW / artH; const vpAspect = vpW / vpH;
// Destination: letterboxed to fill viewport let dstW, dstH, dstX, dstY; if (artAspect > vpAspect) { dstW = vpW; dstH = vpW / artAspect; dstX = 0; dstY = (vpH - dstH) / 2; } else { dstH = vpH; dstW = vpH * artAspect; dstX = (vpW - dstW) / 2; dstY = 0; }
const dst = [ [dstX, dstY], [dstX + dstW, dstY], [dstX + dstW, dstY + dstH], [dstX, dstY + dstH] ];
// Solve perspective transform using adjugate method return getMatrix3d(src, dst, imgW, imgH); }
// Compute a CSS matrix3d string from 4 source to 4 dest point pairs function getMatrix3d(src, dst, w, h) { // Use a general projective mapping approach // Map normalized coords through the transform function solve8(s, d) { // 8-parameter perspective transform const A = []; const b = []; for (let i = 0; i < 4; i++) { A.push([s[i][0], s[i][1], 1, 0, 0, 0, -d[i][0]*s[i][0], -d[i][0]*s[i][1]]); A.push([0, 0, 0, s[i][0], s[i][1], 1, -d[i][1]*s[i][0], -d[i][1]*s[i][1]]); b.push(d[i][0]); b.push(d[i][1]); } return solveLinear(A, b); }
function solveLinear(A, b) { const n = b.length; const aug = A.map((row, i) => [...row, b[i]]); for (let col = 0; col < n; col++) { let maxRow = col; for (let row = col + 1; row < n; row++) { if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) maxRow = row; } [aug[col], aug[maxRow]] = [aug[maxRow], aug[col]]; const pivot = aug[col][col]; if (Math.abs(pivot) < 1e-10) continue; for (let j = col; j <= n; j++) aug[col][j] /= pivot; for (let row = 0; row < n; row++) { if (row === col) continue; const f = aug[row][col]; for (let j = col; j <= n; j++) aug[row][j] -= f * aug[col][j]; } } return aug.map(row => row[n]); }
const params = solve8(src, dst); if (!params || params.some(isNaN)) return null;
const [a, b, c, d, e, f, g, hh] = params;
// CSS matrix3d is column-major 4x4 // Our 2D projective matrix is: // [a b c] which in 4x4 becomes: // [d e f] [a d 0 g] // [g h 1] [b e 0 h] // [0 0 1 0] // [c f 0 1] return `matrix3d(${a},${d},0,${g}, ${b},${e},0,${hh}, 0,0,1,0, ${c},${f},0,1)`; }
async function init() { try { const res = await fetch(MANIFEST_URL); const data = await res.json(); slides = shuffle([...data.images]); } catch (e) { console.error('Failed to load manifest:', e); return; } if (slides.length === 0) return;
for (let i = 0; i < 2; i++) { const slide = document.createElement('div'); slide.className = 'slide'; slide.innerHTML = '<img>'; slideshowEl.appendChild(slide); }
showSlide(0); }
function showSlide(index) { currentIndex = index % slides.length; const slideEls = slideshowEl.querySelectorAll('.slide'); const activeSlot = currentIndex % 2; const inactiveSlot = 1 - activeSlot;
const activeSlide = slideEls[activeSlot]; const inactiveSlide = slideEls[inactiveSlot];
const entry = slides[currentIndex]; const filename = typeof entry === 'string' ? entry : entry.file; const img = activeSlide.querySelector('img');
// Reset any previous transform activeSlide.style.transform = ''; activeSlide.style.transition = '';
img.onload = () => { const vpW = window.innerWidth; const vpH = window.innerHeight;
if (entry.corners) { // Debug: draw quad overlay inside the slide (moves with transforms)
// Compute center and size of artwork quad in viewport coords // Account for object-fit: cover positioning const imgAspect = img.naturalWidth / img.naturalHeight; const vpAspect = vpW / vpH; let coverW, coverH, offX, offY; if (imgAspect > vpAspect) { coverH = vpH; coverW = vpH * imgAspect; offX = (vpW - coverW) / 2; offY = 0; } else { coverW = vpW; coverH = vpW / imgAspect; offX = 0; offY = (vpH - coverH) / 2; }
// Map quad center from image % to viewport px const cx = entry.corners.reduce((s, c) => s + c[0], 0) / 4; const cy = entry.corners.reduce((s, c) => s + c[1], 0) / 4; const centerX = offX + (cx / 100) * coverW; const centerY = offY + (cy / 100) * coverH;
// Quad bounding box in viewport px const xs = entry.corners.map(c => offX + (c[0] / 100) * coverW); const ys = entry.corners.map(c => offY + (c[1] / 100) * coverH); const quadW = Math.max(...xs) - Math.min(...xs); const quadH = Math.max(...ys) - Math.min(...ys);
// Scale to fill viewport from artwork const scale = Math.max(vpW / quadW, vpH / quadH);
// Translate so artwork center is at viewport center const tx = vpW / 2 - centerX; const ty = vpH / 2 - centerY;
activeSlide.style.transformOrigin = `${centerX}px ${centerY}px`; activeSlide.style.transition = 'none'; activeSlide.style.transform = `scale(${scale}) translate(${tx / scale}px, ${ty / scale}px)`; activeSlide.offsetHeight; // force layout // Animate to identity (zoom out to show full context) activeSlide.style.transition = `transform ${ZOOM_DURATION}ms cubic-bezier(0.25, 0.1, 0.25, 1)`; activeSlide.style.transform = 'none'; } else { // No corners: use Ken Burns KB_CLASSES.forEach(c => activeSlide.classList.remove(c)); activeSlide.classList.add(KB_CLASSES[currentIndex % KB_CLASSES.length]); } };
img.src = ASSET_BASE + filename;
// Crossfade activeSlide.classList.add('active'); inactiveSlide.classList.remove('active');
// Update location/date const parts = []; if (entry.place) parts.push(entry.place); if (entry.date) parts.push(formatDate(entry.date)); locationEl.textContent = parts.join(' \u2014 ');
// Progress bar startProgress();
// Preload next const nextIndex = (currentIndex + 1) % slides.length; const nextEntry = slides[nextIndex]; const nextFile = typeof nextEntry === 'string' ? nextEntry : nextEntry.file; const preload = new Image(); preload.src = ASSET_BASE + nextFile;
// Schedule next clearTimeout(slideTimer); if (currentIndex + 1 < slides.length) { slideTimer = setTimeout(() => showSlide(currentIndex + 1), SLIDE_DURATION); } }
function startProgress() { progressBar.style.transition = 'none'; progressBar.style.width = '0%'; progressBar.offsetWidth; progressBar.style.transition = `width ${SLIDE_DURATION}ms linear`; progressBar.style.width = '100%'; }
document.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); clearTimeout(slideTimer); if (currentIndex + 1 < slides.length) showSlide(currentIndex + 1); } else if (e.key === 'ArrowLeft') { e.preventDefault(); clearTimeout(slideTimer); if (currentIndex > 0) showSlide(currentIndex - 1); } });
let touchStartX = 0; document.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', (e) => { const dx = e.changedTouches[0].clientX - touchStartX; if (Math.abs(dx) > 50) { clearTimeout(slideTimer); if (dx < 0 && currentIndex + 1 < slides.length) { showSlide(currentIndex + 1); } else if (dx > 0 && currentIndex > 0) { showSlide(currentIndex - 1); } } }, { passive: true });
init(); </script></body></html>