From 8ebfc9a74b1607462f80fd224c12de27464ff4b5 Mon Sep 17 00:00:00 2001 From: Steven Petryk Date: Sat, 19 Oct 2024 22:07:19 -0700 Subject: [PATCH] Scenes: put things next to each other --- docs/app/guides/display/scenes/page.tsx | 32 +++++ docs/app/guides/guides.tsx | 4 + .../display/scenes/ScenesExample.tsx | 83 +++++++++++ docs/components/icons.tsx | 19 +++ src/display/Scene.tsx | 131 ++++++++++++++++++ src/index.tsx | 2 + 6 files changed, 271 insertions(+) create mode 100644 docs/app/guides/display/scenes/page.tsx create mode 100644 docs/components/guide-examples/display/scenes/ScenesExample.tsx create mode 100644 src/display/Scene.tsx diff --git a/docs/app/guides/display/scenes/page.tsx b/docs/app/guides/display/scenes/page.tsx new file mode 100644 index 00000000..d1ad0729 --- /dev/null +++ b/docs/app/guides/display/scenes/page.tsx @@ -0,0 +1,32 @@ +import { PropTable } from "components/PropTable" +import CodeAndExample from "components/CodeAndExample" + +import ScenesExample from "guide-examples/display/scenes/ScenesExample" +import Code from "components/Code" + +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Scenes", +} + +function ScenesPage() { + return ( + <> + {/*

+ 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. +

+ + + +

Basic scene

*/} + + + + {/* */} + + ) +} + +export default ScenesPage diff --git a/docs/app/guides/guides.tsx b/docs/app/guides/guides.tsx index 5c47798e..75c12955 100644 --- a/docs/app/guides/guides.tsx +++ b/docs/app/guides/guides.tsx @@ -9,6 +9,8 @@ import { RotateCounterClockwiseIcon, TextIcon, CursorArrowIcon, + ViewNoneIcon, + EnterFullScreenIcon, PlayIcon, } from "@radix-ui/react-icons" @@ -21,6 +23,7 @@ import { TransformContextsIcon, DebugIcon, LinearAlgebraIcon, + SceneIcon, } from "components/icons" type Section = { @@ -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" }, diff --git a/docs/components/guide-examples/display/scenes/ScenesExample.tsx b/docs/components/guide-examples/display/scenes/ScenesExample.tsx new file mode 100644 index 00000000..3d0b395b --- /dev/null +++ b/docs/components/guide-examples/display/scenes/ScenesExample.tsx @@ -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) { + const c = useMovablePoint([0, 0], { + constrain: ([x, y]) => [ + clamp(x, -10, 10), + clamp(y, -10, 10), + ], + }) + + return ( + + + Math.sin(x - c.x) + (x - c.x) / 2 + c.y} + color={Theme.blue} + /> + {c.element} + + ) +} + +function Scene2({ sceneSize, sceneSpacing }: any) { + return ( + + + + + ) +} + +export default function Example() { + const sceneSize = 250 + const sceneSpacing = 50 + + return ( + + + + + ) +} diff --git a/docs/components/icons.tsx b/docs/components/icons.tsx index d94d364c..84e65061 100644 --- a/docs/components/icons.tsx +++ b/docs/components/icons.tsx @@ -284,3 +284,22 @@ export function LinearAlgebraIcon(props: React.SVGProps) { ) } + +export function SceneIcon(props: React.SVGProps) { + return ( + + + + + ) +} diff --git a/src/display/Scene.tsx b/src/display/Scene.tsx new file mode 100644 index 00000000..8d71c990 --- /dev/null +++ b/src/display/Scene.tsx @@ -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 + +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( + () => ({ xMin, xMax, yMin, yMax, height, width }), + [xMin, xMax, yMin, yMax, height, width], + ) + + const id = React.useId() + + console.log({ xSpan, ySpan, viewTransformCSS, coordinateContext }) + + return ( + + + + + + + + + + + {children} + + + + + + ) +} + +Scene.displayName = "Scene" diff --git a/src/index.tsx b/src/index.tsx index ddd6184c..547334e3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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"