feat(studio): a phone's header folds into one button, the open ruler no longer rides the photo
This commit is contained in:
@@ -1,18 +1,16 @@
|
||||
// One finger on the photo, while a chip's ruler is open — the knob moved off
|
||||
// the picture instead of out of the row.
|
||||
// The phone's own gestures in the studio, once a chip's ruler is open — and the
|
||||
// one the ruler is not allowed to take.
|
||||
//
|
||||
// A phone's ruler stands in a column at the foot of the screen and the corner a
|
||||
// reader is judging sits above it, so the hand leaves the number to go and look
|
||||
// at what the number is doing. What this holds down is the trade the stage makes
|
||||
// instead: with a ruler open, one finger dragged across the photo IS that
|
||||
// ruler's track — right for up the scale, left for down it, the range travelling
|
||||
// the photo's whole width, the same travel the row's own slider has — and the
|
||||
// number stops at its own ends. It holds down the two things that could be lost
|
||||
// to it as well: two fingers are still the phone's own zoom (a pinch is not a
|
||||
// value, so the finger's hold is given up the moment the second one lands), and
|
||||
// with the row put away the same drag changes nothing at all. The row's own
|
||||
// track takes a touch anywhere on it too, whatever the engine would have done
|
||||
// with the thumb's 3px box.
|
||||
// reader is judging sits above it, so the ruler is what the hand has to reach:
|
||||
// the number is taken by the touch that lands on the track itself, wherever on
|
||||
// it that is. WebKit begins a slider drag only inside the thumb's own box — a
|
||||
// 3px line here, see `SliderThumbElement::handleTouchStart` — so the middle of
|
||||
// the ruler used to answer nothing on a phone, and the row reads the value from
|
||||
// the touch's own place on the track on every engine instead. What is held down
|
||||
// besides that is that the photo stays the photo's: two fingers zoom it, one
|
||||
// finger dragged across it pans it once it is bigger than its box, and the open
|
||||
// knob moves under none of them.
|
||||
//
|
||||
// npm run build && node scripts/studio-drag-check.mjs
|
||||
// PLAYWRIGHT_CORE=<path to playwright-core/index.mjs> CHROME=<path to a chromium> \
|
||||
@@ -200,11 +198,15 @@ const touch = async (type, points) => {
|
||||
|
||||
// The stage's own zoom, read the way the reader sees it: the transform on the
|
||||
// photo. Untransformed is the fitted photo, which is 1.
|
||||
const scale = () =>
|
||||
// The stage's own transform, read the way the reader sees it: the photo's own
|
||||
// box. Fitted, untransformed is scale 1 and nowhere to pan to.
|
||||
const view = () =>
|
||||
page.$eval('.canvas-wrap img', (e) => {
|
||||
const t = getComputedStyle(e).transform;
|
||||
return t === 'none' ? 1 : Number(new DOMMatrixReadOnly(t).a);
|
||||
const m = t === 'none' ? new DOMMatrixReadOnly() : new DOMMatrixReadOnly(t);
|
||||
return { s: m.a, x: m.e, y: m.f };
|
||||
});
|
||||
const scale = async () => (await view()).s;
|
||||
// The box the photo is fitted to: the drag is measured against its width, the
|
||||
// way the stage measures it.
|
||||
const stageBox = () =>
|
||||
@@ -215,7 +217,6 @@ const stageBox = () =>
|
||||
// The open ruler's number, read off the row's own track: the slider is what the
|
||||
// drag writes, so asking the slider is asking what landed.
|
||||
const rulerValue = () => page.$eval('[data-col="slider"] input[type="range"]', (e) => Number(e.value));
|
||||
const chipText = (key) => page.$eval(`[data-key="${key}"]`, (e) => e.textContent ?? '');
|
||||
|
||||
// One finger: down, walked across in even steps, lifted.
|
||||
const drag = async (from, [dx, dy], steps = 8) => {
|
||||
@@ -262,78 +263,26 @@ step('the catalogue frame is on the phone’s stage, fitted', (await scale()) ==
|
||||
|
||||
// --- a ruler, open over the photo --------------------------------------------
|
||||
// LIGHT's panels are the phone's chips, and a panel's row is a knob: the tap
|
||||
// opens its ruler in the column above the strip (see .col-slider).
|
||||
// opens its ruler above the strip (see .col-slider).
|
||||
await page.click('nav.rail button:has-text("LIGHT")');
|
||||
await page.waitForSelector('[data-key="dev-tone"]', { timeout: 30_000 });
|
||||
await page.click('[data-key="dev-tone"]');
|
||||
await page.waitForSelector('[data-key="contrast"]', { timeout: 30_000 });
|
||||
await page.click('[data-key="contrast"]');
|
||||
await page.waitForSelector('[data-col="slider"] input[type="range"]', { timeout: 30_000 });
|
||||
step('a tap on a knob opens its ruler, the row the photo is about to become', (await rulerValue()) === 0, `CONTRAST ${await rulerValue()}`);
|
||||
|
||||
// --- one finger across the photo ---------------------------------------------
|
||||
// CONTRAST is one of the bipolar knobs the spec draws from -100 to +100 (see
|
||||
// `deepen` in shared/utils/paramDefs), so a quarter of the photo's width is
|
||||
// fifty of them — and the number is on the row, not under the hand.
|
||||
const box = await stageBox();
|
||||
await drag([box.cx, box.cy], [box.w * 0.25, 0]);
|
||||
const quarter = await rulerValue();
|
||||
step('a finger dragged to the right lifts the knob with it', Math.abs(quarter - 50) <= 1, `CONTRAST ${quarter} for a quarter of the width`);
|
||||
step('and the photo stayed where it was — the drag is the value, not the view', (await scale()) === 1, `scale ${await scale()}`);
|
||||
|
||||
// --- the look waits for the hand to come off ---------------------------------
|
||||
// Rendering the look is the one thing in a drag that costs more than a frame, so
|
||||
// it is held while the finger is down: the number moves on the row as the hand
|
||||
// does, the photo's own src does not — not even over a pause a debounce would
|
||||
// have rendered on — and it lands once, when the finger lifts.
|
||||
const srcBefore = await page.$eval('.canvas-wrap img', (e) => e.src);
|
||||
const from = await rulerValue();
|
||||
await touch('touchStart', [[box.cx, box.cy]]);
|
||||
await touch('touchMove', [[box.cx + box.w * 0.1, box.cy]]);
|
||||
await page.waitForTimeout(600);
|
||||
const srcMid = await page.$eval('.canvas-wrap img', (e) => e.src);
|
||||
const live = await rulerValue();
|
||||
step('the number is on the row while the hand is still on the photo', Math.abs(live - (from + 20)) <= 1, `CONTRAST ${from} -> ${live}`);
|
||||
step('and the look itself is not rendered under it', srcMid === srcBefore, srcMid === srcBefore ? 'the photo held' : 'the photo re-rendered mid-drag');
|
||||
await touch('touchMove', [[box.cx + box.w * 0.2, box.cy]]);
|
||||
await touch('touchEnd', []);
|
||||
await page.waitForFunction((before) => document.querySelector('.canvas-wrap img')?.src !== before, srcBefore, { timeout: 30_000 }).catch(() => {});
|
||||
const srcAfter = await page.$eval('.canvas-wrap img', (e) => e.src);
|
||||
step('the look lands once, when the finger lifts', srcAfter !== srcBefore, srcAfter === srcBefore ? 'nothing rendered' : 'one render for the gesture');
|
||||
step('and the knob stopped where the hand left it', Math.abs((await rulerValue()) - (from + 40)) <= 1, `CONTRAST ${await rulerValue()}`);
|
||||
|
||||
// Past the far edge the number stops at its own end rather than running on.
|
||||
await drag([box.cx, box.cy], [box.w * 0.6, 0]);
|
||||
step('dragged past the edge, the knob stops at its own end', (await rulerValue()) === 100, `CONTRAST ${await rulerValue()}`);
|
||||
|
||||
// --- two fingers are still the zoom -----------------------------------------
|
||||
// The second finger is not a second value: the zoom wins, and the knob it was
|
||||
// holding is left exactly where the pinch found it.
|
||||
const held = await rulerValue();
|
||||
await pinch([box.cx, box.cy], 30, 150);
|
||||
const zoomed = await scale();
|
||||
step('two fingers on the photo are still the phone’s own zoom', zoomed >= 2, `scale ${zoomed.toFixed(2)}`);
|
||||
step('and a pinch is not a value — the knob it let go did not move', (await rulerValue()) === held, `CONTRAST ${held} -> ${await rulerValue()}`);
|
||||
await pinch([box.cx, box.cy], 150, 30);
|
||||
await page.waitForTimeout(200);
|
||||
step('and two fingers together put the photo back', (await scale()) === 1, `scale ${(await scale()).toFixed(2)}`);
|
||||
|
||||
// --- and back down the scale -------------------------------------------------
|
||||
await drag([box.cx, box.cy], [-box.w * 0.25, 0]);
|
||||
const down = await rulerValue();
|
||||
step('dragged to the left the knob comes down with it', Math.abs(down - 50) <= 1, `CONTRAST ${down}`);
|
||||
step('a tap on a knob opens its ruler, the row the number is read off', (await rulerValue()) === 0, `CONTRAST ${await rulerValue()}`);
|
||||
|
||||
// --- the hand on the ruler itself --------------------------------------------
|
||||
// The other half of the same trade, and the one an engine can take away: the
|
||||
// number can be had by the hand that lands on the ruler, not only by the one
|
||||
// walking across the photo. WebKit begins a slider drag only when the touch
|
||||
// falls inside the thumb's own box — a 3px line here (see `SliderThumbElement::
|
||||
// handleTouchStart`) — so on a phone the ticks across the track answered a finger
|
||||
// while the middle of the ruler answered nothing. The row reads the value from
|
||||
// the touch's own place on the track, so what is made here is the gesture a
|
||||
// browser's slider would take: an untrusted `PointerEvent`, which Blink's own
|
||||
// slider ignores and the row's own listener does not. The whole gesture is put
|
||||
// down on one pixel of the box it started on, the way a hand is.
|
||||
// The number is had by the hand that lands on the ruler, and that is an
|
||||
// engine's business, not the reader's: WebKit begins a slider drag only when
|
||||
// the touch falls inside the thumb's own box — a 3px line here (see
|
||||
// `SliderThumbElement::handleTouchStart`) — so on a phone the ticks across the
|
||||
// track answered a finger while the middle of the ruler answered nothing. The
|
||||
// row reads the value from the touch's own place on the track instead, on every
|
||||
// engine, so what is made here is the gesture a browser's slider would take: an
|
||||
// untrusted `PointerEvent`, which Blink's own slider ignores and the row's own
|
||||
// listener does not. The whole gesture is put down on one pixel of the box it
|
||||
// started on, the way a hand is.
|
||||
const trackTouch = (rail, fraction, type = 'pointerdown', pointerId = 7, pointerType = 'touch') =>
|
||||
page.$eval(
|
||||
'[data-col="slider"] input[type="range"]',
|
||||
@@ -362,6 +311,8 @@ const trackRect = () =>
|
||||
return { left: r.left, width: r.width, mid: r.top + r.height / 2, value: Number(e.value) };
|
||||
});
|
||||
|
||||
// CONTRAST is one of the bipolar knobs the spec draws from -100 to +100 (see
|
||||
// `deepen` in shared/utils/paramDefs), so 85% of the track is +70.
|
||||
const rail = await trackRect();
|
||||
await trackTouch(rail, 0.85);
|
||||
const onTrack = await rulerValue();
|
||||
@@ -376,23 +327,37 @@ step('a second finger is not a second value', (await rulerValue()) === sole, `CO
|
||||
await trackTouch(rail, 0.2, 'pointermove');
|
||||
step('and the number comes down as the hand goes back', Math.abs((await rulerValue()) + 60) <= 1, `CONTRAST ${await rulerValue()} at a fifth`);
|
||||
await trackTouch(rail, 0.5, 'pointerup');
|
||||
const lifted = await rulerValue();
|
||||
const left = await rulerValue();
|
||||
await trackTouch(rail, 0.95, 'pointermove');
|
||||
step('the number stands where the finger was lifted', (await rulerValue()) === lifted, `CONTRAST ${lifted} -> ${await rulerValue()}`);
|
||||
step('the number stands where the finger was lifted', (await rulerValue()) === left, `CONTRAST ${left} -> ${await rulerValue()}`);
|
||||
await trackTouch(rail, 0.1, 'pointerdown', 11, 'mouse');
|
||||
step('and the mouse keeps the drag the engines already give it', (await rulerValue()) === lifted, `CONTRAST ${lifted} -> ${await rulerValue()}`);
|
||||
step('and the mouse keeps the drag the engines already give it', (await rulerValue()) === left, `CONTRAST ${left} -> ${await rulerValue()}`);
|
||||
|
||||
// --- with the row put away ---------------------------------------------------
|
||||
// The drag belongs to the ruler, not to the photo, and a phone puts the row away
|
||||
// through the path under the strip — the strip it came from is off the screen
|
||||
// while the row stands (see the rule that hides the options column). The same
|
||||
// finger across the same picture then moves nothing at all.
|
||||
const before = await chipText('contrast');
|
||||
await page.click('[data-key="crumb-strip"]');
|
||||
await page.waitForFunction(() => !document.querySelector('[data-col="slider"] input[type="range"]'), null, { timeout: 30_000 });
|
||||
// --- the photo is still the photo's ------------------------------------------
|
||||
// The ruler is not what a finger on the picture moves: the picture is. Zoomed
|
||||
// in, one finger drags it across the box — which is the drag a ruler used to
|
||||
// take for itself, and the reason the view could not be moved by hand while a
|
||||
// ruler was open.
|
||||
const box = await stageBox();
|
||||
await pinch([box.cx, box.cy], 30, 150);
|
||||
const zoomed = await view();
|
||||
step('two fingers on the photo are still the phone’s own zoom', zoomed.s >= 2, `scale ${zoomed.s.toFixed(2)}`);
|
||||
step('and a pinch is not a value — the knob it let go did not move', (await rulerValue()) === left, `CONTRAST ${left} -> ${await rulerValue()}`);
|
||||
await drag([box.cx, box.cy], [box.w * 0.2, 0]);
|
||||
const panned = await view();
|
||||
step('with the photo up close one finger drags it across the box', Math.abs(panned.x - zoomed.x) > 20, `x ${Math.round(zoomed.x)} -> ${Math.round(panned.x)}`);
|
||||
step('and the open ruler’s number is not what that hand was moving', (await rulerValue()) === left, `CONTRAST ${left} -> ${await rulerValue()}`);
|
||||
|
||||
// The view is put back, and a fitted photo has nowhere to go: the same drag
|
||||
// leaves both the picture and the number exactly where they were.
|
||||
await pinch([box.cx, box.cy], 150, 30);
|
||||
await page.waitForTimeout(200);
|
||||
step('and two fingers together put the photo back', (await view()).s === 1, `scale ${(await view()).s.toFixed(2)}`);
|
||||
const fitted = await view();
|
||||
await drag([box.cx, box.cy], [box.w * 0.3, 0]);
|
||||
step('with the ruler put away the same drag changes nothing', (await chipText('contrast')) === before, `${before} -> ${await chipText('contrast')}`);
|
||||
step('and the photo is still fitted, neither panned nor zoomed', (await scale()) === 1, `scale ${await scale()}`);
|
||||
const still = await view();
|
||||
step('a fitted photo has nowhere to pan to, ruler open or not', still.x === fitted.x && still.s === 1, `x ${Math.round(fitted.x)} -> ${Math.round(still.x)}`);
|
||||
step('and the number the ruler is on did not move either', (await rulerValue()) === left, `CONTRAST ${left} -> ${await rulerValue()}`);
|
||||
|
||||
step('no uncaught error', errors.length === 0, errors.join(' | '));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user