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 <noreply@anthropic.com>
This commit is contained in:
TMCandClaude Opus 5.5 committed 2026-11-12 21:25:36 +01:00
1 parent d273543070
commit 89fa6ef6ca
2 files changed
+51 -32

No files matched your search

+49 -28
View File
@@ -161,7 +161,6 @@
<div class="world"><div class="wall"></div>
<div class="home"><div class="pages">${pages}</div><div class="pagedots">${dots}</div></div>
</div>
<div class="glow"></div>
<div class="fscrim"></div>
<div class="fcard"><div class="fcard__grid"></div><div class="fcard__title"></div></div>
<div class="drawer"><div class="drawer__in">
@@ -189,7 +188,6 @@
<div class="cam__modes"><span>Video</span><span class="on">Photo</span><span>Portrait</span></div>
<div class="cam__bar"><i class="cam__thumb"></i><i class="cam__shutter"></i><i class="cam__flip">${gl('flip')}</i></div>
<button type="button" class="cam__close" tabindex="-1" aria-label="Close camera">&times;</button>
<i class="cam__flash"></i>
</div>
<div class="sbar"><span class="sbar__time">9:41</span><span class="sbar__icons"><span class="sbar__sig"><i></i><i></i><i></i><i></i></span><span class="sbar__bat"></span></span></div>
<i class="notch"></i>
@@ -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) {
+2 -4
View File
@@ -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; }