Transform contexts
At its core, Mafs is just SVG with two contextual transforms. Those transforms correspond to two things:
- The view transform, which maps from world space to pixel space.
- The user transform, which is imposed by the Transform component.
The general approach is that, to render a point (x, y)
, you must first apply the user transform (because, well, the user is trying to move your component in some way), and then the view transform (so that it gets rendered by the SVG renderer in the right spot).
Mafs provides these transforms through two means:
- The
--mafs-view-transform
and--mafs-user-transform
CSS custom properties, which can be applied to an SVG element'sstyle
attribute. - The
useTransformContext
hook, which returns an object containing theviewTransform
matrix and theuserTransform
matrix.
Components can mix and match these two approaches depending on needs. For example, the Text component transforms its anchor point in JavaScript, and doesn't apply any CSS transforms, because that would distort the text itself. On the other hand, the Ellipse component almost entirely relies on CSS transforms internally.
Accessing transforms in CSS
Here's an example of a custom component that uses the CSS transforms approach to render a delicious little PizzaSlice
. The slice is wrapped in Debug.TransformWidget
component so that you can try applying some user transforms it.
import { Mafs, CartesianCoordinates, Debug } from "mafs"
import * as React from "react"
function Example() {
return (
<Mafs viewBox={{ y: [-1, 1], x: [-1, 1] }}>
<CartesianCoordinates />
<Debug.TransformWidget>
<PizzaSlice />
</Debug.TransformWidget>
</Mafs>
)
}
function PizzaSlice() {
const maskId = `pizza-slice-mask-${React.useId()}`
return (
<g
style={{
transform: `var(--mafs-view-transform) var(--mafs-user-transform)`,
}}
>
<defs>
<mask id={maskId}>
<polyline points={`0,0 ${1},${1 / 2} ${1},${-1 / 2}`} fill="white" />
</mask>
</defs>
<g mask={`url(#${maskId})`}>
<circle cx={0} cy={0} r={1} fill="brown" />
<circle cx={0} cy={0} r={1 * 0.85} fill="yellow" />
<circle cx={0.4} cy={1 * 0.1} r={0.11} fill="red" />
<circle cx={0.2} cy={-1 * 0.1} r={0.09} fill="red" />
<circle cx={0.5} cy={-1 * 0.15} r={0.1} fill="red" />
<circle cx={0.7} cy={1 * 0.05} r={0.11} fill="red" />
<circle cx={0.65} cy={1 * 0.35} r={0.1} fill="red" />
<circle cx={0.65} cy={-1 * 0.37} r={0.08} fill="red" />
</g>
</g>
)
}
This is an example of a component that gets entirely transformed by the user and view transforms. The pizza slice can end up totally distorted. For cases where you want to preserve the aspect ratio or pixel size of your component, you likely need to use the hooks approach.
Accessing transforms in JavaScript
Here's an example of a custom component that uses the hooks approach to render a grid of points. Because we want the grid's points to have a radius of 3 pixels (regardless of the viewport or any transforms), we use the useTransformContext
hook to get the user and view transforms and apply them to the circles' x
and y
coordinates, but not to their radius (which is in pixels). We also cannot use the CSS transforms approach here, because that would distort each circle.
import { CartesianCoordinates, Debug, Mafs, useTransformContext, vec } from "mafs"
function Example() {
return (
<Mafs viewBox={{ y: [-1, 5], x: [-1, 6] }}>
<CartesianCoordinates />
<Debug.TransformWidget>
<PointCloud />
</Debug.TransformWidget>
</Mafs>
)
}
function PointCloud() {
const { userTransform, viewTransform } =
useTransformContext()
const size = 5
const perAxis = 10
const points: { at: vec.Vector2; color: string }[] = []
for (let i = 0; i <= size; i += size / perAxis) {
for (let j = 0; j <= size; j += size / perAxis) {
const userTransformedPoint = vec.transform([i, j], userTransform)
const viewTransformedPoint = vec.transform(userTransformedPoint, viewTransform)
const h = (360 * (i + j)) / (size * 2)
const s = 100
// If h is blueish, make the point lighter
const l = h > 200 && h < 300 ? 70 : 50
points.push({
at: viewTransformedPoint,
color: `hsl(${h} ${s}% ${l}%)`,
})
}
}
return (
<>
{points.map(({ at: [x, y], color }) => {
return (
<circle
key={`${x},${y}`}
cx={x}
cy={y}
r={3}
fill={color}
className="mafs-shadow"
/>
)
})}
</>
)
}