Gestures

Solid 2Unstable

Arrange a room, at any zoom

Zoom the floor plan in or out, then drag the sofa, the table and the lamp about: each stays under your pointer, and inside the room.

Live demo
Sofa
Table
Lamp

Reduced motion: Nothing changes: a piece follows your pointer, and nothing moves by itself.

Why

A plan you can zoom is drawn with a CSS transform, so the pointer moves in the page's pixels while a piece moves in the plan's: at 140% a piece would run ahead of your pointer, and at 60% lag behind it. The pieces sit under a WardicConfig whose transformPagePoint is correctParentTransform(() => plan), which turns each point of a drag back through the plan's zoom, so a piece stays under your pointer at any zoom.

Each piece's dragConstraints is the plan itself. Its box is measured through the same mapping, so a piece stops at the room's walls, whatever the zoom.

The code

Motion's correctParentTransform takes a React ref to the plan; Wardic's takes the element, or an accessor of it.

examples/arrange-room/Demo.tsx
import { correctParentTransform, drag, LazyWardic, w, WardicConfig } from "@wardic/solid/unstable/v2"
import { createSignal, For } from "solid-js"

const PIECES = [
  { id: "sofa", name: "Sofa", x: 24, y: 24, width: 120, height: 52 },
  { id: "table", name: "Table", x: 64, y: 112, width: 72, height: 44 },
  { id: "lamp", name: "Lamp", x: 236, y: 32, width: 44, height: 44 },
] as const

/** The demo, with this page's `drag` beside the shell's Features (#526): a page without a drag ships none. */
export default function ArrangeRoom() {
  return (
    <LazyWardic features={[drag]}>
      <ArrangeRoomDemo />
    </LazyWardic>
  )
}

function ArrangeRoomDemo() {
  // The plan is zoomed with a CSS transform, so the pointer moves in the page's pixels while a
  // piece moves in the plan's: at 140% a piece would run ahead of the pointer, at 60% lag behind.
  const [zoom, setZoom] = createSignal(1)
  let plan!: HTMLDivElement

  return (
    <div class="room-demo">
      <label class="room-zoom">
        <span>Zoom</span>
        <input type="range" min="0.6" max="1.4" step="0.1" value={zoom()} onInput={(event) => setZoom(Number(event.currentTarget.value))} />
        <output>{Math.round(zoom() * 100)}%</output>
      </label>
      <div class="room-frame">
        <div class="room-plan" ref={plan} style={{ transform: `scale(${zoom()})` }}>
          {/* correctParentTransform turns each pointer point back through the plan's zoom, so a piece
              stays under the pointer at any zoom, and its constraints are measured in the plan's pixels. */}
          <WardicConfig transformPagePoint={correctParentTransform(() => plan)}>
            <For each={PIECES}>
              {(piece) => (
                <w.div
                  class="room-piece"
                  data-id={piece.id}
                  drag
                  dragConstraints={() => plan}
                  dragElastic={0.1}
                  dragMomentum={false}
                  whileDrag={{ scale: 1.05 }}
                  style={{ left: `${piece.x}px`, top: `${piece.y}px`, width: `${piece.width}px`, height: `${piece.height}px` }}
                >
                  {piece.name}
                </w.div>
              )}
            </For>
          </WardicConfig>
        </div>
      </div>
    </div>
  )
}
examples/arrange-room/Demo.tsx
"use client"
import { correctParentTransform, m, MotionConfig } from "motion/react"
import { useRef, useState } from "react"

const PIECES = [
  { id: "sofa", name: "Sofa", x: 24, y: 24, width: 120, height: 52 },
  { id: "table", name: "Table", x: 64, y: 112, width: 72, height: 44 },
  { id: "lamp", name: "Lamp", x: 236, y: 32, width: 44, height: 44 },
] as const

export default function ArrangeRoom() {
  // The plan is zoomed with a CSS transform, so the pointer moves in the page's pixels while a
  // piece moves in the plan's: at 140% a piece would run ahead of the pointer, at 60% lag behind.
  const [zoom, setZoom] = useState(1)
  const plan = useRef<HTMLDivElement>(null)

  return (
    <div className="room-demo">
      <label className="room-zoom">
        <span>Zoom</span>
        <input type="range" min="0.6" max="1.4" step="0.1" value={zoom} onChange={(event) => setZoom(Number(event.currentTarget.value))} />
        <output>{Math.round(zoom * 100)}%</output>
      </label>
      <div className="room-frame">
        <div className="room-plan" ref={plan} style={{ transform: `scale(${zoom})` }}>
          {/* correctParentTransform turns each pointer point back through the plan's zoom, so a piece
              stays under the pointer at any zoom, and its constraints are measured in the plan's pixels. */}
          <MotionConfig transformPagePoint={correctParentTransform(plan)}>
            {PIECES.map((piece) => (
              <m.div
                key={piece.id}
                className="room-piece"
                data-id={piece.id}
                drag
                dragConstraints={plan}
                dragElastic={0.1}
                dragMomentum={false}
                whileDrag={{ scale: 1.05 }}
                style={{ left: piece.x, top: piece.y, width: piece.width, height: piece.height }}
              >
                {piece.name}
              </m.div>
            ))}
          </MotionConfig>
        </div>
      </div>
    </div>
  )
}
Run Motion's version →