Skip to content
On this page

Movable points

Movable points can be dragged around the coordinate plane, or moved via the keyboard. They're the cornerstone of lots of interactions.

They can be unconstrained (allowed to move freely), constrained horizontally or vertically, or constrained to an arbitrary function. This example constrains movement horizontally:

Code
vue
<script setup lang="ts">
import { Mafs, Cartesian, OfX, Point, MovablePoint, useMovablePoint, AlignType } from 'mafs-vue'
import range from 'lodash/range'
import { computed } from 'vue'

const fn = (x: number) => (x / 2) ** 2
const sep = useMovablePoint([1, 0], {
    constrain: AlignType.HORIZONGTAL
})
const n = 10
const points = computed(() =>
    sep.point[0] != 0
        ? range(-n * sep.point[0], (n + 0.5) * sep.point[0], sep.point[0])
        : []
)
</script>
<template>
    <Mafs :height="300" :viewBox="{ x: [0, 0], y: [-1.3, 4.7] }">
        <Cartesian />
        <OfX :y="fn" :stroked="{ opacity: 0.25 }" />
        <Point v-for="(x, index) in points" :key="index" :x="x" :y="fn(x)" />
        <MovablePoint :ctx="sep" />
    </Mafs>
</template>

Constraints

Beyond constraining horizontally or vertically, points can also be constrained to arbitrary paths. This is done by passing a function to constrain. The function is expected to take a point (x, y), which is where the user is trying to move to, and to return a new point, (x', y'), which is where the point should actually go.

To demonstrate this, imagine constraining a point to "snap" to the nearest whole number point. We take where the user is trying to move to, and round it to the nearest whole number.

vue
useMovablePoint([0, 0], {
    constrain: ([x, y]) => [Math.round(x), Math.round(y)]
})

Another common use case is to constrain motion to be circular—vec.withMag comes in handy there.

vue
useMovablePoint([0, 0], {
    // Constrain \`point\` to move in a circle of radius 1
    constrain: (point) => vec.withMag(point, 1)
})

You can also constrain a point to follow a straight line by setting constrain to AlignType.HORIZONGTAL" or AlignType.VERTICAL.

Mafs may call constrain more than once when the user moves a point using the arrow keys, so it should be side-effect free.

Transformations and constraints

When wrapping a Movable Point in a Transform, the point will be transformed too. However, your constrain function will be passed the untransformed point, and its return value will be transformed back into the currently applied transform. In other words, Mafs takes care of the math for you.

Let's see a more complex example where we combine more interesting constraint functions with transforms. On the left, we have a point that can only move in whole-number increments within a square, and on the right, a point that can only move in π/16 increments in a circle.

Code
vue
<script setup lang="ts">
import { Mafs, Cartesian, Transform, Circle, Polygon, Vector, MovablePoint, useMovablePoint, Theme, vec } from 'mafs-vue'
import clamp from 'lodash/clamp'

const gridMotion = useMovablePoint([1, 1], {
    // Constrain this point to whole numbers inside of a rectangle
    constrain: ([x, y]) => [
        clamp(Math.round(x), -2, 2),
        clamp(Math.round(y), -2, 2),
    ],
})

const radius = 2
const radialMotion = useMovablePoint([0, radius], {
    // Constrain this point to specific angles from the center
    constrain: (point) => {
        const angle = Math.atan2(point[1], point[0])
        const snap = Math.PI / 16
        const roundedAngle = Math.round(angle / snap) * snap
        return vec.rotate([radius, 0], roundedAngle)
    },
})
</script>
<template>
    <Mafs :height="200" :viewBox="{ x: [-8, 8], y: [-2, 2] }">
        <Cartesian />
        <Transform :translate="[-3, 0]">
            <Vector :tail="[0, 0]" :tip="gridMotion.point" />
            <Polygon :points="[[-2, -2], [2, -2], [2, 2], [-2, 2]]" :filled="{ color: Theme.blue }" />
            <MovablePoint :ctx="gridMotion" />
        </Transform>
        <Transform :translate="[3, 0]">
            <Vector :tail="[0, 0]" :tip="radialMotion.point" />
            <Circle :center="[0, 0]" :radius="radius" :filled="{ color: Theme.blue, fillOpacity: 0 }" />
            <MovablePoint :ctx="radialMotion" />
        </Transform>
    </Mafs>
</template>

Using MovablePoint directly Advanced

useMovablePoint is a hook that helps you instantiate and manage the state of a MovablePoint. However, if need be, you can also use MovablePoint directly. This can be useful if you need to work with a dynamic number of movable points (since the Vue "rules of hooks" ban you from dynamically calling hooks).

Code
vue
<script setup lang="ts">
import { Mafs, Cartesian, Segment, MovablePoint, useMovablePoint, Theme, vec } from 'mafs-vue'
import range from 'lodash/range'
import { computed } from 'vue'

const start = useMovablePoint([-3, -1])
const end = useMovablePoint([3, 1])

const betweenPoints = computed(() => {
    const length = vec.dist(start.point, end.point)
    return range(1, length - 0.5, 1).map((t) => vec.lerp(start.point, end.point, t / length))
})
function shift(shiftBy: vec.Vector2) {
    start.setPoint(vec.add(start.point, shiftBy))
    end.setPoint(vec.add(end.point, shiftBy))
}
</script>
<template>
    <Mafs>
        <Cartesian />
        <Segment :point1="start.point" :point2="end.point" />
        <MovablePoint :ctx="start" />
        <MovablePoint v-for="(point, index) in betweenPoints" :key="index" :color="Theme.blue"
            :ctx="{ point: point, setPoint: (newPoint: vec.Vector2) => shift(vec.sub(newPoint, point)) }" />
        <MovablePoint :ctx="end" />
    </Mafs>
</template>

Props <MovablePoint ... />

NameDescriptionDefault
ctx{ point: Vector2, setPoint: (point: Vector2) => void }

The result of useMovablePoint.

point: The current position [x, y] of the point.

setPoint: A callback that is called as the user moves the point.

colorstring
var(--mafs-pink)