Appearance
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 ... />
Name | Description | Default |
---|---|---|
ctx | { point: Vector2, setPoint: (point: Vector2) => void } The result of point: The current position setPoint: A callback that is called as the user moves the point. | — |
color | string | var(--mafs-pink) |