Skip to content

Commit

Permalink
Scenes: put things next to each other
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenpetryk committed Oct 20, 2024
1 parent 1d00248 commit 8ebfc9a
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
32 changes: 32 additions & 0 deletions docs/app/guides/display/scenes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PropTable } from "components/PropTable"

Check failure on line 1 in docs/app/guides/display/scenes/page.tsx

View workflow job for this annotation

GitHub Actions / lint

'PropTable' is defined but never used
import CodeAndExample from "components/CodeAndExample"

import ScenesExample from "guide-examples/display/scenes/ScenesExample"
import Code from "components/Code"

Check failure on line 5 in docs/app/guides/display/scenes/page.tsx

View workflow job for this annotation

GitHub Actions / lint

'Code' is defined but never used

import type { Metadata } from "next"

export const metadata: Metadata = {
title: "Scenes",
}

function ScenesPage() {
return (
<>
{/* <p>
Scenes are a way to create a new coordinate space and show it inside of Mafs. This can be
useful for doing something like showing two visualizations side-by-side.
</p>
<Code source={`import { Scene } from "mafs"`} language="tsx" />
<h2>Basic scene</h2> */}

<CodeAndExample example={ScenesExample} />

{/* <PropTable of={"Scene"} /> */}
</>
)
}

export default ScenesPage
4 changes: 4 additions & 0 deletions docs/app/guides/guides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
RotateCounterClockwiseIcon,
TextIcon,
CursorArrowIcon,
ViewNoneIcon,

Check failure on line 12 in docs/app/guides/guides.tsx

View workflow job for this annotation

GitHub Actions / lint

'ViewNoneIcon' is defined but never used
EnterFullScreenIcon,

Check failure on line 13 in docs/app/guides/guides.tsx

View workflow job for this annotation

GitHub Actions / lint

'EnterFullScreenIcon' is defined but never used
PlayIcon,
} from "@radix-ui/react-icons"

Expand All @@ -21,6 +23,7 @@ import {
TransformContextsIcon,
DebugIcon,
LinearAlgebraIcon,
SceneIcon,
} from "components/icons"

type Section = {
Expand Down Expand Up @@ -53,6 +56,7 @@ export const Guides: Section[] = [
guides: [
{ title: "Mafs", icon: CardStackIcon, slug: "mafs" },
{ title: "Coordinates", icon: GridIcon, slug: "coordinates" },
{ title: "Scenes", icon: SceneIcon, slug: "scenes" },
{ separator: true },
{ title: "Points", icon: DotFilledIcon, slug: "points" },
{ title: "Lines", icon: LinesIcon, slug: "lines" },
Expand Down
83 changes: 83 additions & 0 deletions docs/components/guide-examples/display/scenes/ScenesExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client"

import { clamp } from "lodash"
import {
Circle,
Coordinates,
Mafs,
Plot,
Scene,
Theme,
useMovablePoint,
} from "mafs"

function Scene1({ sceneSize, sceneSpacing }: any) {

Check failure on line 14 in docs/components/guide-examples/display/scenes/ScenesExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const c = useMovablePoint([0, 0], {
constrain: ([x, y]) => [
clamp(x, -10, 10),
clamp(y, -10, 10),
],
})

return (
<Scene
x={-sceneSize - sceneSpacing / 2}
y={-sceneSize / 2}
width={sceneSize}
height={sceneSize}
viewBox={{ x: [-10, 10], y: [-10, 10], padding: 3 }}
preserveAspectRatio={false}
>
<Coordinates.Cartesian
xAxis={{ lines: 5 }}
yAxis={{ lines: 5 }}
/>
<Plot.OfX
y={(x) => Math.sin(x - c.x) + (x - c.x) / 2 + c.y}
color={Theme.blue}
/>
{c.element}
</Scene>
)
}

function Scene2({ sceneSize, sceneSpacing }: any) {

Check failure on line 44 in docs/components/guide-examples/display/scenes/ScenesExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
return (
<Scene
x={sceneSpacing / 2}
y={-sceneSize / 2}
width={sceneSize}
height={sceneSize}
viewBox={{
x: [-10, 10],
y: [-10, 10],
padding: 3,
}}
preserveAspectRatio={false}
>
<Coordinates.Cartesian
xAxis={{ lines: 5 }}
yAxis={{ lines: 5 }}
/>
<Circle center={[0, 0]} radius={5} />
</Scene>
)
}

export default function Example() {
const sceneSize = 250
const sceneSpacing = 50

return (
<Mafs height={300} pan={false}>
<Scene1
sceneSize={sceneSize}
sceneSpacing={sceneSpacing}
/>
<Scene2
sceneSize={sceneSize}
sceneSpacing={sceneSpacing}
/>
</Mafs>
)
}
19 changes: 19 additions & 0 deletions docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,22 @@ export function LinearAlgebraIcon(props: React.SVGProps<SVGSVGElement>) {
</svg>
)
}

export function SceneIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect x="0.5" y="1.5" width="14" height="12" rx="1" stroke="currentColor" />
<path
d="M7.25423 5.3511L4.33043 9.52796C4.19124 9.72679 4.33349 10 4.5762 10H10.4238C10.6665 10 10.8088 9.72679 10.6696 9.52796L7.74577 5.3511C7.62634 5.18048 7.37366 5.18048 7.25423 5.3511Z"
fill="currentColor"
/>
</svg>
)
}
131 changes: 131 additions & 0 deletions src/display/Scene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as React from "react"
import CoordinateContext, { CoordinateContextShape } from "../context/CoordinateContext"
import PaneManager from "../context/PaneContext"

import { round } from "../math"
import { vec } from "../vec"
import { TransformContext } from "../context/TransformContext"
import { SpanContext } from "../context/SpanContext"

export type ScenePropsT = React.PropsWithChildren<{
width?: number | "auto"
height?: number

/** Whether to enable panning with the mouse and keyboard */
pan?: boolean

/**
* Whether to enable zooming with the mouse and keyboard. This can also be an
* object with `min` and `max` properties to set the scale limits.
*
* * `min` should be in the range (0, 1].
* * `max` should be in the range [1, ∞).
*/
zoom?: boolean | { min: number; max: number }

/**
* A way to declare the "area of interest" of your visualizations. Mafs will center and zoom to
* this area.
*/
viewBox?: { x?: vec.Vector2; y?: vec.Vector2; padding?: number }
/**
* Whether to squish the graph to fill the Mafs viewport or to preserve the aspect ratio of the
* coordinate space.
*/
preserveAspectRatio?: "contain" | false

/** Called when the view is clicked on, and passed the point where it was clicked. */
onClick?: (point: vec.Vector2, event: MouseEvent) => void
}>

type SceneProps = {
width: number
height: number
x: number
y: number
} & Required<Pick<ScenePropsT, "viewBox" | "preserveAspectRatio">> &
Pick<ScenePropsT, "children">

export function Scene({ x, y, width, height, viewBox, preserveAspectRatio, children }: SceneProps) {
const padding = viewBox?.padding ?? 0.5
// Default behavior for `preserveAspectRatio == false`
let xMin = (viewBox?.x?.[0] ?? 0) - padding
let xMax = (viewBox?.x?.[1] ?? 0) + padding
let yMin = (viewBox?.y?.[0] ?? 0) - padding
let yMax = (viewBox?.y?.[1] ?? 0) + padding

if (preserveAspectRatio === "contain") {
const aspect = width / height
const aoiAspect = (xMax - xMin) / (yMax - yMin)

if (aoiAspect > aspect) {
const yCenter = (yMax + yMin) / 2
const ySpan = (xMax - xMin) / aspect / 2
yMin = yCenter - ySpan
yMax = yCenter + ySpan
} else {
const xCenter = (xMax + xMin) / 2
const xSpan = ((yMax - yMin) * aspect) / 2
xMin = xCenter - xSpan
xMax = xCenter + xSpan
}
}

const xSpan = xMax - xMin
const ySpan = yMax - yMin

const viewTransform = React.useMemo(() => {
const scaleX = round((1 / xSpan) * width, 5)
const scaleY = round((-1 / ySpan) * height, 5)
return vec.matrixBuilder().scale(scaleX, scaleY).get()
}, [height, width, xSpan, ySpan])

const viewTransformCSS = vec.toCSS(viewTransform)

const coordinateContext = React.useMemo<CoordinateContextShape>(
() => ({ xMin, xMax, yMin, yMax, height, width }),
[xMin, xMax, yMin, yMax, height, width],
)

const id = React.useId()

console.log({ xSpan, ySpan, viewTransformCSS, coordinateContext })

Check failure on line 92 in src/display/Scene.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

return (
<CoordinateContext.Provider value={coordinateContext}>
<SpanContext.Provider value={{ xSpan, ySpan }}>
<TransformContext.Provider
value={{ userTransform: vec.identity, viewTransform: viewTransform }}
>
<PaneManager>
<g
transform={`translate(${x + width / 2} ${-y - height / 2})`}
style={{
...({
"--mafs-view-transform": viewTransformCSS,
"--mafs-user-transform": "translate(0, 0)",
} as React.CSSProperties),
}}
>
<defs>
<clipPath id={`scene-clip-${id}`}>
<rect
x={xMin}
y={yMin}
width={xSpan}
height={ySpan}
fill="white"
style={{ transform: "var(--mafs-view-transform)" }}
/>
</clipPath>
</defs>
<g clipPath={`url(#scene-clip-${id})`}>{children}</g>
</g>
</PaneManager>
</TransformContext.Provider>
</SpanContext.Provider>
</CoordinateContext.Provider>
)
}

Scene.displayName = "Scene"
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type { MafsProps } from "./view/Mafs"
export { Coordinates } from "./display/Coordinates"
export { autoPi as labelPi } from "./display/Coordinates/Cartesian"

export { Scene } from "./display/Scene"

export { Plot } from "./display/Plot"
export type { OfXProps, OfYProps, ParametricProps, VectorFieldProps } from "./display/Plot"

Expand Down

0 comments on commit 8ebfc9a

Please sign in to comment.