Mafs

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:

import { Mafs, FunctionGraph, Point, CartesianCoordinates, useMovablePoint } from "mafs"
import range from "lodash.range"

function PointsAlongFunction() {
  const fn = (x: number) => (x / 2) ** 2
  const sep = useMovablePoint([1, 0], {
    constrain: "horizontal",
  })

  const points =
    sep.x != 0
      ? range(0, 10 * sep.x, sep.x).concat(
          range(0, -10 * sep.x, -sep.x)
        )
      : []

  return (
    <Mafs yAxisExtent={[-1.3, 4.7]}>
      <CartesianCoordinates />

      <FunctionGraph.OfX y={fn} opacity={0.25} />
      {points.map((x, index) => (
        <Point x={x} y={fn(x)} key={index} />
      ))}
      {sep.element}
    </Mafs>
  )
}

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.

The simplest example of this is, in fact, constraining movement horizontally and vertically. When you pass "horizontal" or "vertical", Mafs is internally using functions like this to constrain the point:

  • Horizontal constraint: ([newX, _]) => [newX, y]
  • Vertical constraint: ([_, newY]) => [x, newY]

You can also round point positions to make points move discretely rather than continuously. The following example plays with that idea both for linear positions and angles.

import { Mafs, Vector, CartesianCoordinates, useMovablePoint, Circle, Polygon } from "mafs"
import * as vec from "vec-la"
import clamp from "lodash.clamp"

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

  const radius = 2
  const shift = vec.matrixBuilder().translate(3, 0).get()
  const radialMotion = useMovablePoint([0, radius], {
    // Constrain this point to specific angles from the center
    constrain: (point) => {
      const angle = Math.PI / 2 - Math.atan2(...point)
      const snap = Math.PI / 16
      const roundedAngle = Math.round(angle / snap) * snap
      return vec.rotate([radius, 0], roundedAngle)
    },
    // (More on transforms in the next section)
    transform: shift,
  })

  return (
    <Mafs
      xAxisExtent={[-8.5, 8.5]}
      yAxisExtent={[-3, 3]}
    >
      <CartesianCoordinates xAxis={{ labels: false }} />

      <Vector tail={[-3, 0]} tip={gridMotion.point} />

      <Polygon
        points={[[-5, -2], [-1, -2], [-1, 2], [-5, 2]]}
        fillOpacity={0}
        strokeOpacity={0.5}
        strokeStyle="dashed"
      />
      <Circle
        center={vec.transform([0, 0], shift)}
        radius={radius}
        fillOpacity={0}
        strokeOpacity={0.5}
        strokeStyle="dashed"
      />
      <Vector
        tail={vec.transform([0, 0], shift)}
        tip={vec.transform(radialMotion.point, shift)}
      />

      {gridMotion.element}
      {radialMotion.element}
    </Mafs>
  )
}

Transforms

Transforms are a way to separate a point's visual location from its mathematical one. This is great if you want to define a point's position in terms of other points.

When transforming a point, its constraints are also transformed—your constraint function, if any, will receive the point's actual position, not its visual one. This means that if you rotate a point using a transformation, and you've constrained the point horizontally (for example), the horizontal constraint will be rotated, too.

This next example exploits this behavior to great effect. The center (orange) point is used to translate the other 3 points, so they "follow" the center point. Then, the outer (blue) point, which rotates the ellipse, applies a rotation to the width and height points. All along the way, every point can be treated as if it hasn't moved at all, keeping the math easy for us and letting Mafs do all the linear algebra.

import { Mafs, Ellipse, Circle, CartesianCoordinates, useMovablePoint, Theme, } from "mafs"
import * as vec from "vec-la"

function MovableEllipse() {
  const rotationHintRadius = 3

  // This center point translates everything else.
  const center = useMovablePoint([0, 0], {
    color: Theme.orange,
  })
  const translation = vec
    .matrixBuilder()
    .translate(center.x, center.y)
    .get()

  // This outer point rotates the ellipse, and
  // is also translated by the center point.
  // Its position is used to build a rotation matrix.
  const rotate = useMovablePoint([rotationHintRadius, 0], {
    color: Theme.blue,
    transform: translation,
    // Constrain this point to only move in a circle
    constrain: (position) =>
      vec.scale(vec.norm(position), rotationHintRadius),
  })
  const angle = Math.PI / 2 - Math.atan2(...rotate.point)
  const rotation = vec.matrixBuilder().rotate(angle).get()

  // Lastly, these two points are rotated and translated
  // according to the outer two points.
  const width = useMovablePoint([2, 0], {
    transform: vec.composeTransform(translation, rotation),
    constrain: "horizontal",
  })
  const height = useMovablePoint([0, 1], {
    transform: vec.composeTransform(translation, rotation),
    constrain: "vertical",
  })

  return (
    <Mafs>
      <CartesianCoordinates />

      {/*
       * Display a little hint that the
       * point is meant to move radially
       */}
      <Circle
        center={center.point}
        radius={rotationHintRadius}
        strokeStyle="dashed"
        strokeOpacity={0.3}
        fillOpacity={0}
      />

      <Ellipse
        center={center.point}
        radius={[Math.abs(width.x), Math.abs(height.y)]}
        angle={angle}
      />

      {center.element}
      {width.element}
      {height.element}
      {rotate.element}
    </Mafs>
  )
}