From 551b0fa9e43052e3ee74e39c1fb2d2e2a0cd076f Mon Sep 17 00:00:00 2001 From: Steven Petryk Date: Sun, 20 Oct 2024 12:27:25 -0700 Subject: [PATCH] Add Image component --- docs/app/guides/display/images/page.tsx | 26 ++++++++ docs/app/guides/guides.tsx | 2 + .../display/images/ImageExample.tsx | 27 +++++++++ .../guide-examples/display/images/mafs.png | Bin 0 -> 4955 bytes src/display/Image.tsx | 47 +++++++++++++++ src/index.tsx | 3 + src/math.ts | 57 ++++++++++++++++++ 7 files changed, 162 insertions(+) create mode 100644 docs/app/guides/display/images/page.tsx create mode 100644 docs/components/guide-examples/display/images/ImageExample.tsx create mode 100644 docs/components/guide-examples/display/images/mafs.png create mode 100644 src/display/Image.tsx diff --git a/docs/app/guides/display/images/page.tsx b/docs/app/guides/display/images/page.tsx new file mode 100644 index 00000000..59af25ea --- /dev/null +++ b/docs/app/guides/display/images/page.tsx @@ -0,0 +1,26 @@ +import { PropTable } from "components/PropTable" + +import CodeAndExample from "components/CodeAndExample" +import ImageExample from "guide-examples/display/images/ImageExample" +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: "Vectors", +} + +function Vectors() { + return ( + <> +

+ Images in Mafs are just wrappers around the SVG image element, with some + improvements. For exa +

+ + + + + + ) +} + +export default Vectors diff --git a/docs/app/guides/guides.tsx b/docs/app/guides/guides.tsx index 5c47798e..62951ad1 100644 --- a/docs/app/guides/guides.tsx +++ b/docs/app/guides/guides.tsx @@ -10,6 +10,7 @@ import { TextIcon, CursorArrowIcon, PlayIcon, + ImageIcon, } from "@radix-ui/react-icons" import { @@ -62,6 +63,7 @@ export const Guides: Section[] = [ { title: "Plots", icon: FunctionIcon, slug: "plots" }, { title: "Text", icon: TextIcon, slug: "text" }, { title: "Vectors", icon: ArrowTopRightIcon, slug: "vectors" }, + { title: "Images", icon: ImageIcon, slug: "images" }, { separator: true }, { title: "Transform", icon: RotateCounterClockwiseIcon, slug: "transform" }, { title: "Debug", icon: DebugIcon, slug: "debug" }, diff --git a/docs/components/guide-examples/display/images/ImageExample.tsx b/docs/components/guide-examples/display/images/ImageExample.tsx new file mode 100644 index 00000000..272c161e --- /dev/null +++ b/docs/components/guide-examples/display/images/ImageExample.tsx @@ -0,0 +1,27 @@ +"use client" + +import { + Coordinates, + Debug, + Image, + Mafs, + useMovablePoint, +} from "mafs" + +import mafs from "./mafs.png" + +export default function VectorExample() { + return ( + + + + + ) +} diff --git a/docs/components/guide-examples/display/images/mafs.png b/docs/components/guide-examples/display/images/mafs.png new file mode 100644 index 0000000000000000000000000000000000000000..43a8b3d2a902398d813a87cc03f024a480ee092b GIT binary patch literal 4955 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 literal 0 HcmV?d00001 diff --git a/src/display/Image.tsx b/src/display/Image.tsx new file mode 100644 index 00000000..502c2edc --- /dev/null +++ b/src/display/Image.tsx @@ -0,0 +1,47 @@ +import { Anchor, computeAnchor } from "../math" + +export interface ImageProps { + src: string + x: number + y: number + width: number + height: number + anchor?: Anchor + preserveAspectRatio?: string + svgImageProps?: React.SVGProps +} + +export function Image({ + src, + x, + y, + width, + height, + anchor = "bl", + preserveAspectRatio, + svgImageProps, +}: ImageProps) { + const [actualX, actualY] = computeAnchor(anchor, x, y, width, height) + + const scaleX = width < 0 ? -1 : 1 + const scaleY = height < 0 ? -1 : 1 + + console.log(actualX - (width < 0 ? -width : 0)) + + return ( + + ) +} diff --git a/src/index.tsx b/src/index.tsx index ddd6184c..0265dd0b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -35,6 +35,9 @@ export type { PointProps } from "./display/Point" export { Vector } from "./display/Vector" export type { VectorProps } from "./display/Vector" +export { Image } from "./display/Image" +export type { ImageProps } from "./display/Image" + export { Text } from "./display/Text" export type { TextProps, CardinalDirection } from "./display/Text" diff --git a/src/math.ts b/src/math.ts index 34d69fc9..8152b2c0 100644 --- a/src/math.ts +++ b/src/math.ts @@ -1,4 +1,5 @@ export type Interval = [min: number, max: number] +export type Anchor = "tl" | "tc" | "tr" | "cl" | "cc" | "cr" | "bl" | "bc" | "br" export function round(value: number, precision = 0): number { const multiplier = Math.pow(10, precision || 0) @@ -24,3 +25,59 @@ export function range(min: number, max: number, step = 1): number[] { export function clamp(number: number, min: number, max: number): number { return Math.min(Math.max(number, min), max) } + +/** + * Given an anchor and a bounding box (x, y, width, height), compute the x and y coordinates of the + * anchor such that rendering an element at those coordinates will align the element with the anchor. + */ +export function computeAnchor( + anchor: Anchor, + x: number, + y: number, + width: number, + height: number, +): [number, number] { + let actualX = x + let actualY = y + + switch (anchor) { + case "tl": + actualX = x + actualY = -y + break + case "tc": + actualX = x - width / 2 + actualY = -y + break + case "tr": + actualX = x - width + actualY = -y + break + case "cl": + actualX = x + actualY = -y - height / 2 + break + case "cc": + actualX = x - width / 2 + actualY = -y - height / 2 + break + case "cr": + actualX = x - width + actualY = -y - height / 2 + break + case "bl": + actualX = x + actualY = -y - height + break + case "bc": + actualX = x - width / 2 + actualY = -y - height + break + case "br": + actualX = x - width + actualY = -y - height + break + } + + return [actualX, actualY] +}