transformPagePoint

Use MotionConfig transformPagePoint when a draggable or pannable motion element sits inside a parent with CSS scale. The callback maps browser coordinates into the local units used by your interface.

<script lang="ts">
    import { MotionConfig, motion, type MotionTransformPoint } from '@humanspeak/svelte-motion'

    const zoom = 0.5
    const correctPoint: MotionTransformPoint = ({ x, y }) => ({
        x: x / zoom,
        y: y / zoom
    })
</script>

<div style:transform={`scale(${zoom})`} style:transform-origin="top left">
    <MotionConfig transformPagePoint={correctPoint}>
        <motion.div drag dragMomentum={false}>Drag me</motion.div>
    </MotionConfig>
</div>
<script lang="ts">
    import { MotionConfig, motion, type MotionTransformPoint } from '@humanspeak/svelte-motion'

    const zoom = 0.5
    const correctPoint: MotionTransformPoint = ({ x, y }) => ({
        x: x / zoom,
        y: y / zoom
    })
</script>

<div style:transform={`scale(${zoom})`} style:transform-origin="top left">
    <MotionConfig transformPagePoint={correctPoint}>
        <motion.div drag dragMomentum={false}>Drag me</motion.div>
    </MotionConfig>
</div>

A 100-screen-pixel pointer move at scale(0.5) becomes 200 local units. The browser then renders those 200 units through the parent scale, so the element travels the same 100 screen pixels as the pointer.

Coordinate contract

  • Pointer callbacks receive corrected page-coordinate points. delta is previous-frame-relative, offset is gesture-relative, and distance thresholds and velocity are computed from the retained corrected samples.
  • DOM measurements map the viewport rectangle’s top-left and bottom-right corners. Projection-root scroll is added after that mapping.
  • Element-ref drag constraints map both the dragged element and constraint rectangles before subtracting their edges. Numeric constraints are already local values and are not transformed.
  • Positive uniform and nonuniform scale are supported. Affine translation in the mapping cancels when differences are calculated.

The callback maps two rectangle corners, so it is not arbitrary polygon recovery. Perspective, rotation-aware bounding polygons, automatic ancestor-transform detection, SVG viewBox helpers, and Motion’s React-only transform helper exports are outside this API.

Session capture

The gesture input callback reference is captured at pointerdown. Replacing the MotionConfig function during a drag or pan applies to the next gesture’s pointer samples. Mounted VisualElement measurements and refreshed drag geometry remain live and can read the current config; they are not frozen with the input callback.

A stable callback may read reactive zoom state, but only the latest raw pointer point is transformed again. Earlier samples keep their already-mapped values. Changing the closed-over scale while held can therefore create the same large offset and velocity jumps as Motion: in the verified scale-2-to-4 fixture, pan jumps from (40, 20) to (900, 700) and drag to (894, 710). Keep the mapping fixed for the duration of a gesture unless this retained-history behavior is intentional.

Inheritance and reset

Nested configs inherit transformPagePoint when the child omits the prop. Supply an explicit identity function to opt a subtree out of an ancestor correction:

<script lang="ts">
    const identity = ({ x, y }: { x: number; y: number }) => ({ x, y })
</script>

<MotionConfig transformPagePoint={correctPoint}>
    <ScaledCanvas />
    <MotionConfig transformPagePoint={identity}>
        <UnscaledOverlay />
    </MotionConfig>
</MotionConfig>
<script lang="ts">
    const identity = ({ x, y }: { x: number; y: number }) => ({ x, y })
</script>

<MotionConfig transformPagePoint={correctPoint}>
    <ScaledCanvas />
    <MotionConfig transformPagePoint={identity}>
        <UnscaledOverlay />
    </MotionConfig>
</MotionConfig>

Explicitly passing transformPagePoint={undefined} clears an inherited mapping for that subtree. This prop distinguishes omission from an explicit undefined, matching Motion’s public config behavior.

Constraints and release velocity

Ref constraints are measured in the corrected coordinate space, including resize refreshes. Numeric bounds such as { left: -80, right: 80 } remain exactly those local limits. A second drag’s callback offset starts from zero even though the bound x/y values retain the previous translation.

Velocity uses the corrected retained frame history. The verified scale-2 sequence reports move velocities (0, 0) then (100, 50), followed by terminal velocity (750, 500) at the recorded frame times.

Scroll, layout, and controls

Ordinary pan does not subscribe to ancestor or window scroll as gesture movement. Drag does track scrollable ancestors: a raw ancestor scroll delta is added to drag offset without running that delta through transformPagePoint.

Browser page coordinates can still change at pointerup after document scrolling. In the verified held-scroll fixture, the last move offset and rendered drag translation stay (60, 40), while the terminal callback reports (150, 180). Treat terminal payload and displayed translation as distinct in this case.

Layout projection stays the transform owner. A real 60-local-pixel layout displacement while held compensates bound x from 60 to 0, and the next pointer move produces (20, 40). Imperative snapToCursor also follows Motion’s projection layout calculation. With page scroll (120, 160) in the matched fixture it snaps to bound (-49, -436), then moves to (1, -406); it does not physically center the rendered box under the cursor.

See also