Spaces:
Running
Running
Download src/layout.js from bep40/VSEARCH: direct link, hf CLI and curl.
- Browser
- Download file 6.43 kB
-
https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/src/layout.js
- Command line
-
hf download hf://spaces/bep40/VSEARCH/src/layout.js
-
curl -L -o layout.js https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/src/layout.js
6.43 kB
| // 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 }; | |
| } | |