From 89fa6ef6cafc3fc27e957009728b3bee6fbcbacc Mon Sep 17 00:00:00 2001 From: TMC Date: Thu, 12 Nov 2026 21:25:36 +0100 Subject: [PATCH] Website: quick launch slides the camera in; no white flash The camera now behaves as the page to the left of the first one: a swipe right pushes the home screen (wallpaper included) to the right while the camera follows the finger in from the left edge, and finishes the slide when the finger lets go. Before, the home screen rubber-banded and the camera was revealed by a clip from the edge. Same in the scroll story, the playground gesture and its demo button; in the playground a short swipe springs back, and a swipe left slides the camera out again. The white flash at the end of the camera chapter was a shutter flash over the whole screen; it read as a glitch while scrolling into the next chapter. The shutter button now presses in instead. Checked in Chrome: the camera chapters stepped through in 0.02 steps show no brightness spike; the playground swipe in, swipe back, short swipe and demo button; no console errors or sideways scrolling at 1440 x 900 and 390 x 844. Co-Authored-By: Claude Opus 5.5 --- WEBSITE/script.js | 77 +++++++++++++++++++++++++++++----------------- WEBSITE/styles.css | 6 ++-- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/WEBSITE/script.js b/WEBSITE/script.js index bc7b587696..22901745ec 100644 --- a/WEBSITE/script.js +++ b/WEBSITE/script.js @@ -161,7 +161,6 @@
${pages}
${dots}
-
@@ -189,7 +188,6 @@
VideoPhotoPortrait
${gl('flip')}
-
9:41
@@ -209,8 +207,8 @@ folderId: 'media', pull: 0, // px the pages are dragged sideways page: 0, - camera: 0, - flash: 0, + camera: 0, // the camera is the page left of the first one: 0 home, 1 camera fully in + shutter: 0, sheet: 0, align: 0, // 0 left, .5 center, 1 right weight: DEFAULT_WEIGHT, @@ -246,12 +244,12 @@ this.el = q('.phone'); this.screen = q('.screen'); this.r = { - world: q('.world'), home: q('.home'), pages: q('.pages'), dots: qa('.pagedots i'), glow: q('.glow'), + world: q('.world'), home: q('.home'), pages: q('.pages'), dots: qa('.pagedots i'), fscrim: q('.fscrim'), fcard: q('.fcard'), fgrid: q('.fcard__grid'), ftitle: q('.fcard__title'), drawer: q('.drawer'), drawerIn: q('.drawer__in'), list: q('.list'), alpha: q('.alpha'), letters: qa('.alpha__l'), bubble: q('.bubble'), menu: q('.menu'), menuHide: q('.mi--hide'), snack: q('.snack'), snackT: q('.snack__t'), sheet: q('.sheet'), segAlign: qa('[data-seg="align"] span'), segWeight: qa('[data-seg="weight"] span'), - cam: q('.cam'), flash: q('.cam__flash'), finger: q('.finger'), ring: q('.finger__ring'), + cam: q('.cam'), shutter: q('.cam__shutter'), finger: q('.finger'), ring: q('.finger__ring'), clockLines: qa('.clk__line > span'), clockTime: q('.clk__time'), time: q('[data-time]'), date: q('[data-date]'), stime: q('.sbar__time'), pics: qa('.pic'), rows: new Map(qa('.row').map((el) => [el.dataset.row, el])), @@ -371,13 +369,16 @@ const d = s.drawer; const f = s.folder; - // Wallpaper and home share one layer, so a folder blurs both. - css(r.world, 'transform', f > 0.001 ? `scale(${(1 - 0.035 * f).toFixed(4)})` : ''); + // Wallpaper and home share one layer, so a folder blurs both, and the camera, the page to + // the left of the first one, pushes both to the right as it slides in. + const cam = s.camera; + const slide = cam > 0.0005 ? `translateX(${(cam * W).toFixed(1)}px)` : ''; + const shrink = f > 0.001 ? `scale(${(1 - 0.035 * f).toFixed(4)})` : ''; + css(r.world, 'transform', `${slide} ${shrink}`.trim()); css(r.world, 'filter', f > 0.001 ? `blur(${(f * 16).toFixed(2)}px)` : ''); css(r.home, 'opacity', (1 - d).toFixed(3)); css(r.home, 'transform', d > 0.001 ? `translateY(${(-26 * d).toFixed(1)}px) scale(${(1 - 0.05 * d).toFixed(4)})` : ''); css(r.pages, 'transform', `translateX(${(-s.page * W + s.pull).toFixed(1)}px)`); - css(r.glow, 'opacity', clamp(s.pull / 70).toFixed(3)); const page = Math.round(clamp(s.page - s.pull / W, 0, HOME.length - 1)); r.dots.forEach((dot, i) => dot.classList.toggle('on', i === page)); @@ -435,10 +436,10 @@ css(r.snack, 'transform', `translateY(${((1 - s.snack) * 26).toFixed(1)}px)`); if (r.snackT.textContent !== s.snackText) r.snackT.textContent = s.snackText; - // Camera, revealed from the left edge. - css(r.cam, 'visibility', s.camera > 0.001 ? 'visible' : 'hidden'); - css(r.cam, 'clipPath', `inset(0 ${((1 - s.camera) * 100).toFixed(2)}% 0 0)`); - css(r.flash, 'opacity', s.flash.toFixed(3)); + // Camera, sliding in from the left with the home screen. + css(r.cam, 'visibility', cam > 0.0005 ? 'visible' : 'hidden'); + css(r.cam, 'transform', `translateX(${((cam - 1) * W).toFixed(1)}px)`); + css(r.shutter, 'transform', s.shutter > 0.5 ? 'scale(.86)' : ''); // Clock options sheet and the clock itself. css(r.sheet, 'transform', `translateY(${((1 - s.sheet) * 105).toFixed(2)}%)`); @@ -529,16 +530,17 @@ s.folder = E(t, 0.33, 0.62); break; } - case 4: { // close the folder, pull past the first page, camera + case 4: { // close the folder, swipe right: the camera slides in as the page to the left s.folder = 1 - E(t, 0, 0.15); const drag = E(t, 0.26, 0.55); - F.x = lerp(70, 300, drag); + const reach = 230 / W; // how far the finger drags the screen before letting go + F.x = lerp(70, 70 + 230, drag); F.y = 420; F.o = seg(t, 0.18, 0.25) * (1 - seg(t, 0.55, 0.62)); F.press = t > 0.25 && t < 0.56 ? 1 : 0; - s.pull = rubber(drag * 230) * (1 - E(t, 0.56, 0.7)); - s.camera = E(t, 0.56, 0.82); - s.flash = seg(t, 0.9, 0.92) * (1 - seg(t, 0.93, 0.99)); + // The screen follows the finger, then finishes the slide on its own once it lets go. + s.camera = reach * drag + (1 - reach) * E(t, 0.55, 0.74); + s.shutter = t > 0.86 && t < 0.92 ? 1 : 0; break; } case 5: { // close the camera, hold the clock, restyle it @@ -738,10 +740,10 @@ } const closeFolder = () => to({ folder: 0 }, 300); function launchCamera() { - say('Quick launch. Swipe past the first page and your camera is right there.'); - return to({ pull: 0, page: 0, camera: 1 }, 480, outCubic); + say('Quick launch. Swipe past the first page and your camera is right there. Swipe left to go back.'); + return to({ pull: 0, page: 0, camera: 1 }, 420, outCubic); } - const closeCamera = () => to({ camera: 0 }, 320).then(() => say('Back home. Try the playground buttons, or just play.')); + const closeCamera = () => to({ camera: 0 }, 400).then(() => say('Back home. Try the playground buttons, or just play.')); function openMenu(name) { s.held = name; const ry = ph.rowY(name, s.list); @@ -857,7 +859,7 @@ fingerAt(70, 420); await to({ 'finger.o': 1 }, 160); set({ 'finger.press': 1 }); - await to({ 'finger.x': 300, pull: rubber(230) }, 560); + await to({ 'finger.x': 300, camera: 230 / W }, 560); set({ 'finger.press': 0 }); fingerOff(); if (!alive()) return; @@ -984,11 +986,21 @@ if (!gest.dir && gest.moved) gest.dir = Math.abs(dy) > Math.abs(dx) ? 'v' : 'h'; if (gest.dir === 'v') s.drawer = clamp(-dy / 280); if (gest.dir === 'h') { - const atStart = s.page < 0.5 && dx > 0; - const atEnd = s.page > HOME.length - 1.5 && dx < 0; - s.pull = atStart || atEnd ? rubber(dx) : dx; + if (s.page < 0.5 && dx > 0) { + // Past the first page: the camera follows the finger in from the left. + s.camera = clamp(dx / W); + s.pull = 0; + } else { + const atEnd = s.page > HOME.length - 1.5 && dx < 0; + s.pull = atEnd ? rubber(dx) : dx; + s.camera = 0; + } } break; + case 'camera': + if (!gest.dir && gest.moved) gest.dir = Math.abs(dy) > Math.abs(dx) ? 'v' : 'h'; + if (gest.dir === 'h') s.camera = clamp(1 + Math.min(0, dx) / W); + break; default: break; } @@ -1021,7 +1033,14 @@ if (dy < -36) closeAll().then(() => say('Home. Swipe up from the bottom edge works from anywhere.')); break; case 'camera': - if ((tap && g.target.closest('.cam__close')) || dy < -60) closeCamera(); + if (g.dir === 'h') { + if (s.camera < 0.8) closeCamera(); + else to({ camera: 1 }, 220, outCubic); + } else if ((tap && g.target.closest('.cam__close')) || dy < -60) closeCamera(); + else if (tap) { + set({ shutter: 1 }); + setTimeout(() => set({ shutter: 0 }), 140); + } break; case 'menu': if (tap && g.target.closest('.mi--hide')) hideApp(s.held); @@ -1070,8 +1089,10 @@ if (s.drawer > 0.3) openDrawer(); else to({ drawer: 0 }, 220); } else if (g.dir === 'h') { - if (s.page < 0.5 && dx > 70) launchCamera(); - else if (s.page < 0.5 && dx < -70) to({ page: 1, pull: 0 }, 380).then(() => say('Page two. Swipe back, or past the last page.')); + if (s.page < 0.5 && dx > 0) { + if (s.camera > 0.2) launchCamera(); + else to({ camera: 0 }, 240, outCubic); + } else if (s.page < 0.5 && dx < -70) to({ page: 1, pull: 0 }, 380).then(() => say('Page two. Swipe back, or past the last page.')); else if (s.page > 0.5 && dx > 70) to({ page: 0, pull: 0 }, 380).then(() => say(HELLO)); else to({ pull: 0 }, 260, outCubic); } else if (tap) { diff --git a/WEBSITE/styles.css b/WEBSITE/styles.css index 6cf527731c..e342363c3a 100644 --- a/WEBSITE/styles.css +++ b/WEBSITE/styles.css @@ -609,7 +609,6 @@ section { position: relative; } .pagedots { position: absolute; left: 0; right: 0; top: 748px; display: flex; justify-content: center; gap: 8px; } .pagedots i { width: 7px; height: 7px; border-radius: 50%; background: rgba(255, 255, 255, .45); transition: background .2s, width .2s; } .pagedots i.on { background: #fff; width: 18px; border-radius: 4px; } -.glow { position: absolute; top: 0; bottom: 0; left: -80px; width: 160px; background: radial-gradient(closest-side, rgba(118, 250, 252, .85), rgba(29, 219, 253, .25) 55%, transparent); opacity: 0; pointer-events: none; z-index: 5; } .fscrim { position: absolute; inset: 0; background: #000; opacity: 0; pointer-events: none; z-index: 10; } .fcard { @@ -662,7 +661,7 @@ section { position: relative; } .seg span:first-child { border-left: 0; } .seg span.on { background: #ddd5ff; font-weight: 700; } -.cam { position: absolute; inset: 0; background: #000; z-index: 25; visibility: hidden; clip-path: inset(0 100% 0 0); } +.cam { position: absolute; inset: 0; background: #000; z-index: 25; visibility: hidden; transform: translateX(-100%); } .cam__view { position: absolute; left: 0; right: 0; top: 52px; height: 560px; overflow: hidden; background: @@ -674,12 +673,11 @@ section { position: relative; } .cam__hill--2 { height: 30%; background: #120a36; clip-path: polygon(0 30%, 25% 12%, 48% 40%, 70% 20%, 100% 44%, 100% 100%, 0 100%); } .cam__grid { position: absolute; inset: 0; background: linear-gradient(to right, transparent 33%, rgba(255, 255, 255, .35) 33%, rgba(255, 255, 255, .35) calc(33% + 1px), transparent calc(33% + 1px), transparent 66%, rgba(255, 255, 255, .35) 66%, rgba(255, 255, 255, .35) calc(66% + 1px), transparent calc(66% + 1px)), linear-gradient(to bottom, transparent 33%, rgba(255, 255, 255, .35) 33%, rgba(255, 255, 255, .35) calc(33% + 1px), transparent calc(33% + 1px), transparent 66%, rgba(255, 255, 255, .35) 66%, rgba(255, 255, 255, .35) calc(66% + 1px), transparent calc(66% + 1px)); } .cam__focus { position: absolute; left: 50%; top: 50%; width: 76px; height: 76px; margin: -38px; border: 2px solid #ffe066; border-radius: 6px; } -.cam__flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; } .cam__modes { position: absolute; left: 0; right: 0; top: 632px; display: flex; justify-content: center; gap: 26px; font: 600 13px/1 var(--ui); letter-spacing: .08em; text-transform: uppercase; color: #9d9ab8; } .cam__modes .on { color: var(--ice); } .cam__bar { position: absolute; left: 0; right: 0; top: 684px; display: flex; align-items: center; justify-content: space-between; padding: 0 44px; } .cam__thumb { width: 50px; height: 50px; border-radius: 12px; background: linear-gradient(145deg, #ff9b6a, #4b2ac9); border: 2px solid #fff; } -.cam__shutter { width: 78px; height: 78px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 5px #000, 0 0 0 9px #fff; } +.cam__shutter { width: 78px; height: 78px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 5px #000, 0 0 0 9px #fff; transition: transform .12s ease; } .cam__flip { display: grid; place-items: center; width: 50px; height: 50px; border-radius: 50%; background: #26243a; } .cam__flip .gl { width: 24px; height: 24px; color: #fff; } .cam__close { position: absolute; top: 58px; right: 16px; width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 50%; background: rgba(0, 0, 0, .45); color: #fff; font: 400 26px/1 var(--ui); cursor: pointer; }