Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scenes: put things next to each other #172

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
TransformContextsIcon,
DebugIcon,
LinearAlgebraIcon,
SceneIcon,
} from "components/icons"

type Section = {
Expand Down Expand Up @@ -53,6 +56,7 @@
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
Loading