Spaces:
Running
Running
File size: 6,434 Bytes
c9e39f3 f134575 c9e39f3 0df020e c9e39f3 0df020e c9e39f3 0df020e c9e39f3 0df020e c9e39f3 7ce3b43 c9e39f3 f134575 c9e39f3 f134575 7ce3b43 f134575 7ce3b43 f134575 7ce3b43 f134575 c9e39f3 0df020e f134575 c9e39f3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | // Stable randomness keeps identity and size consistent as photographs move.
export function seed(text) {
let hash = 2166136261;
for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
return (hash >>> 0) / 4294967295;
}
/**
* How many photographs the field holds.
*
* 150 on desktop / 96 on a phone, up from 96 / 56. The raise is deliberate: the
* featured tiles are now 1.5x larger, so they occupy noticeably more of the
* plane than before, and at the old counts the gaps between them read as empty
* space rather than as a dense field. More ordinary tiles fills that space
* without touching the emphasis -- the newest photographs still ring, and there
* are still only ten of those.
*
* A phone is raised less than a desktop because tile AREA is bounded by the
* viewport, not by the tile count: past ~100 the extra tiles fall below their own
* thumbnail size and the field turns to grain.
*/
export function fieldCount(width) { return width < 650 ? 96 : 150; }
/**
* Tile size bounds for the user-facing size control.
*
* Below the lower bound the tiles stop being recognisable as photographs, and
* above the upper bound a single tile covers the search pill and most of the
* viewport, so the field stops reading as a collection at all.
*/
export const MIN_TILE_SCALE = 0.6;
export const MAX_TILE_SCALE = 1.8;
/**
* How far the whole plane may be pushed, in px, before it stops being
* reachable. Larger than half the shortest likely viewport, so a photographer
* can be moved out from under the search pill without being lost off-screen.
*/
export const MAX_FIELD_OFFSET = 320;
export function clampTileScale(value) {
const scale = Number(value);
return Number.isFinite(scale) ? Math.max(MIN_TILE_SCALE, Math.min(MAX_TILE_SCALE, scale)) : 1;
}
export function clampFieldOffset(value) {
const offset = Number(value);
return Number.isFinite(offset) ? Math.max(-MAX_FIELD_OFFSET, Math.min(MAX_FIELD_OFFSET, offset)) : 0;
}
/**
* Size one tile.
*
* `tileScale` is the user's size multiplier and is applied to `unit` BEFORE the
* per-photograph variance, so every tile grows and shrinks together and the
* collection keeps its rhythm instead of the biggest photographs absorbing the
* whole change.
*/
export function dimensions(image, viewportWidth, tileScale = 1) {
const unit = Math.max(68, Math.min(168, viewportWidth / 10.25)) * tileScale;
const area = unit * unit * (0.74 + seed(image.id + 'size') * 0.48);
const ar = Math.max(0.5, Math.min(2, image.ar));
return { width: Math.sqrt(area * ar), height: Math.sqrt(area / ar) };
}
export function scatter(image, index, count, width, height, offsetX = 0, offsetY = 0) {
const columns = Math.ceil(Math.sqrt(count * width / height));
const rows = Math.ceil(count / columns);
return {
x: -35 + (index % columns + 0.5 + (seed(image.id + 'x') - 0.5) * 0.44) * (width + 70) / columns + offsetX,
y: -40 + (Math.floor(index / columns) + 0.5 + (seed(image.id + 'y') - 0.5) * 0.48) * (height + 80) / rows + offsetY,
scale: 0.88 + seed(image.id + 'scale') * 0.20,
};
}
/** One continuous layout for the whole collection, including non-matching photos. */
export function fieldPosition(image, rank, count, width, height, offsetX = 0, offsetY = 0) {
const angle = rank * 2.399963229728653 + 0.54;
const radius = Math.sqrt((rank + 0.6) / count);
// 0.62 instead of 0.68: the top results hug the centre -- and therefore the
// search pill -- more tightly, so the answer to a query sits visibly NEAR
// the box the visitor typed in, not pushed out towards the field's rim.
const x = width / 2 + Math.cos(angle) * radius * width * 0.62;
let y = Math.sin(angle) * radius * height * 0.66;
// Ease the central photographs around the pill, without a separate results layer.
//
// Clearance has to account for the TILE, not just its centre. A featured tile
// is 1.5x its neighbours, so a distance tuned to clear an ordinary tile still
// lets a featured corner land under the pill. `dimensions()` is therefore
// called here for the real half-height, and the push grows with it.
const clearance = Math.max(0, 1 - Math.abs(x - width / 2) / (width * 0.31));
// The tile is scaled by `scale` on top of its CSS size, and that scale reaches
// 1.32 for a central featured tile -- so the clearance has to be computed from
// the RENDERED half-extent. Measuring the unscaled box let a 289px tile clear
// the pill by 221px and still land on it.
const scale = 0.96 + seed(image.id + 'scale') * 0.18 + (1 - radius) * 0.18;
const size = dimensions(image, width, image?.featuredScale || 1);
const half = Math.max(size.width, size.height) / 2 * scale;
// PILL_HALF is the search box's own half-width plus margin, so "far enough" is
// measured from the box rather than from its centreline. The 22px margin
// (previously 34) lets a result settle closer to the box while still clearing
// the glass itself.
const pillHalf = (width < 650 ? 165 : 235) + 22;
// 80/118 was a generous shove; 74/96 keeps the same shape but brings the arc
// in so the top results read as "attached" to the pill rather than banished.
y += Math.sign(y || 1) * clearance * (width < 650 ? 60 : 92) * (1 - radius * 0.45);
// If the tile's own rendered box could still reach the pill, push it clear along
// y. Both conditions must hold: the tile is near the pill's row AND its column
// overlaps the pill's width. Only genuinely-overlapping tiles move, so the arc
// keeps its shape and tiles already clear are left alone.
if (clearance > 0
&& Math.abs(y) < half + 40
&& Math.abs(x - width / 2) < pillHalf + half) {
y += Math.sign(y || 1) * (half + 40 - Math.abs(y));
}
return {
x: x + offsetX, y: height / 2 + y + offsetY,
// The same `scale` the clearance was computed from. Recomputing it here
// would be a second, independently-evaluated expression for one number, and
// the clearance would silently stop matching what is drawn if the two ever
// drifted apart.
scale,
};
}
export function edgePosition(image, width, height, target) {
const dx = target.x - width / 2 || 1;
const dy = target.y - height / 2 || 1;
const factor = Math.min((width / 2 + 210) / Math.abs(dx), (height / 2 + 210) / Math.abs(dy));
return { x: width / 2 + dx * factor, y: height / 2 + dy * factor, scale: target.scale };
}
|