Personal ProjectLive

collins.pictures

a layout engine that curates instead of places.

grids flatten photographs. that's not neutral. it's a choice, and most portfolio sites make it without thinking.

masonry layouts optimize for density. carousels optimize for engagement. neither optimizes for what photographs actually do: create relationships with the images around them.

a panorama next to a portrait creates rhythm. a horizon that continues across two frames creates continuity. the space between shots matters as much as the shots.

Everything visible at once

no slideshow. no pagination. every photograph on one wall, the way you'd hang a show.

large formats anchor the composition. smaller work fills the gaps. the algorithm places boulders first, then sand. constrained elements claim their space before flexible ones settle around them.

Composition Engine

artCompiler.json

each image ships with metadata. which crop ratios work. where the focal point lives. how much the layout can stretch it.

valid ratios per image
crop anchor positions
horizon line for alignment
elasticity score
artCompiler.json
{
  "EX6A9822": {
    "crops": {
      "1:1": { "x": 0.5, "y": 0.4 },
      "2:1": { "x": 0.5, "y": 0.3 },
      "1:2": null
    },
    "horizonLine": 0.35,
    "elasticity": 0.7
  }
}

the engine scores placements. +150 when adjacent horizons align. penalties for vertical seams that break flow. visual weight balanced across the composition. fifty metrics, one score.

move your cursor

The preloader

stars that breathe. orbital drift with spring physics. responds to your cursor.

while you watch, twenty concurrent fetches pull every image variant into cache. the calm is functional.

Panoramas

two transform layers. outer layer drifts automatically, ken burns style. inner layer responds to drag.

nested CSS transforms compose naturally. no state synchronization, no fighting the browser. just math.

Two-Layer Transform
Outer: Ken Burns (0px)Inner: Drag (0px)

drag to pan · nested transforms stack naturally

seed.ts
function seededRandom(seed: number) {
  const m = 2147483647;
  const a = 1103515245;
  const c = 12345;
  let state = seed;

  return () => {
    state = (a * state + c) % m;
    return state / m;
  };
}

const layout = seededRandom(hashSession(sessionId));

Every refresh different

same photographs, new composition. the session ID seeds the random number generator. deterministic chaos.

share a link and they see what you saw. refresh to discover a different arrangement.

every refresh tells a different story.