Custom layouts

Write your own place(d) and get any shape you like.

A layout is a function. It takes how far an item is from the center and returns where to put it. Every preset that ships is written against it.

ts
type Layout = (d: number) => Placement

interface Placement {
  x: number
  y: number
  z: number        // pixels toward the viewer; also decides paint order
  rotate: number   // degrees in the plane of the screen
  rotateX?: number // degrees tipped toward you
  rotateY?: number // degrees turned toward you
  scale: number
  opacity: number
}

d is signed and continuous, in item units. 0 is the centered item, 1 is the next one along, -0.5 is halfway back toward the previous one. It is a float because the track spends most of its time between two items.

A wave

Six lines of arithmetic.

wave.jsts
const wave = (d) => ({
  x: d * 88,
  y: Math.sin(d * 0.8) * 40,
  z: -Math.abs(d),
  rotate: Math.cos(d * 0.8) * 10,
  scale: 1 - Math.min(Math.abs(d) / 5, 1) * 0.35,
  opacity: 1 - Math.min(Math.abs(d) / 5, 1) * 0.7,
})

createPicker(el, { items, renderItem, layout: wave, spacing: 88 })
The physics, virtualization and input all still apply.

Pass spacing too

The layout decides where items go; spacing decides how far a finger has to travel to move the track by one item. Loopem cannot infer one from the other, so a custom layout should pass whatever matches its own step.

Packaging one as a preset

loopem/layouts exports functions that return a partial options object, not a bare Layout, because a shape usually needs its own spacing, perspective and overscan to render correctly. If you build something reusable, do the same:

ts
export const wave = ({ step = 88, height = 40 } = {}) => ({
  layout: (d) => ({ /* ... */ }),
  spacing: step,
  overscan: 3,
})