import React, { useContext } from "react"; import { BBox, Point } from "../vendor/bezier-js"; import { getAttachmentTransforms } from "./attach"; import { getBoundingBoxCenter, uniformlyScaleToFit } from "./bounding-box"; import { scalePointXY, subtractPoints } from "./point"; import { AttachmentPointType, PointWithNormal } from "./specs"; import { createSvgSymbolContext, SvgSymbolContent, SvgSymbolContext, SvgSymbolData, swapColors, } from "./svg-symbol"; const DEFAULT_ATTACHMENT_SCALE = 0.5; function getAttachmentPoint( s: SvgSymbolData, type: AttachmentPointType, idx: number = 0 ): PointWithNormal { const { specs } = s; if (!specs) { throw new AttachmentPointError(`Symbol ${s.name} has no specs.`); } const points = specs[type]; if (!(points && points.length > idx)) { throw new AttachmentPointError( `Expected symbol ${s.name} to have at least ${ idx + 1 } ${type} attachment point(s).` ); } return points[idx]; } class AttachmentPointError extends Error {} function safeGetAttachmentPoint( s: SvgSymbolData, type: AttachmentPointType, idx: number = 0 ): PointWithNormal | null { try { return getAttachmentPoint(s, type, idx); } catch (e) { if (e instanceof AttachmentPointError) { console.log(e.message); } else { throw e; } } return null; } export type CreatureContextType = SvgSymbolContext & { attachmentScale: number; parent: SvgSymbolData | null; }; export const CreatureContext = React.createContext({ ...createSvgSymbolContext(), attachmentScale: DEFAULT_ATTACHMENT_SCALE, parent: null, }); export type AttachedCreatureSymbol = CreatureSymbol & { attachTo: AttachmentPointType; indices: number[]; }; export type NestedCreatureSymbol = CreatureSymbol & { indices: number[]; }; export type CreatureSymbol = { data: SvgSymbolData; invertColors: boolean; attachments: AttachedCreatureSymbol[]; nests: NestedCreatureSymbol[]; }; export type CreatureSymbolProps = CreatureSymbol; type NestedCreatureSymbolProps = NestedCreatureSymbol & { parent: SvgSymbolData; }; type AttachedCreatureSymbolProps = AttachedCreatureSymbol & { parent: SvgSymbolData; }; function getNestingTransforms(parent: BBox, child: BBox) { const parentCenter = getBoundingBoxCenter(parent); const childCenter = getBoundingBoxCenter(child); const translation = subtractPoints(parentCenter, childCenter); const uniformScaling = uniformlyScaleToFit(parent, child); const scaling: Point = { x: uniformScaling, y: uniformScaling }; return { translation, transformOrigin: childCenter, scaling }; } type AttachmentTransformProps = { transformOrigin: Point; translate: Point; scale: Point; rotate: number; children: JSX.Element; }; const AttachmentTransform: React.FC = (props) => ( {/** * We originally used "transform-origin" here but that's not currently * supported by Safari. Instead, we'll set the origin of our symbol to * the transform origin, do the transform, and then move our origin back to * the original origin, which is equivalent to setting "transform-origin". **/} {props.children} ); const AttachedCreatureSymbol: React.FC = ({ indices, parent, attachTo, data, ...props }) => { const ctx = useContext(CreatureContext); const children: JSX.Element[] = []; for (let attachIndex of indices) { const parentAp = safeGetAttachmentPoint(parent, attachTo, attachIndex); const ourAp = safeGetAttachmentPoint(data, "anchor"); if (!parentAp || !ourAp) { continue; } // If we're attaching something oriented towards the left, horizontally flip // the attachment image. let xFlip = parentAp.normal.x < 0 ? -1 : 1; // Er, things look weird if we don't inverse the flip logic for // the downward-facing attachments, like legs... if (parentAp.normal.y > 0) { xFlip *= -1; } const t = getAttachmentTransforms(parentAp, { point: ourAp.point, normal: scalePointXY(ourAp.normal, xFlip, 1), }); children.push( ); } return <>{children}; }; const NestedCreatureSymbol: React.FC = ({ indices, parent, data, ...props }) => { const children: JSX.Element[] = []; for (let nestIndex of indices) { const parentNest = (parent.specs?.nesting ?? [])[nestIndex]; if (!parentNest) { console.log( `Parent symbol ${parent.name} has no nesting index ${nestIndex}.` ); continue; } const t = getNestingTransforms(parentNest, data.bbox); children.push( ); } return <>{children}; }; export const CreatureSymbol: React.FC = (props) => { let ctx = useContext(CreatureContext); const { data, attachments, nests } = props; const attachmentCtx: CreatureContextType = { ...ctx, parent: data }; if (props.invertColors) { ctx = swapColors(ctx); } // If we're inverted, then pass our inverted colors on to our // nested children, to maintain color balance in the composition. const nestedCtx: CreatureContextType = { ...ctx, parent: data }; // The attachments should be before our symbol in the DOM so they // appear behind our symbol, while anything nested within our symbol // should be after our symbol so they appear in front of it. return ( <> {attachments.length && ( {attachments.map((a, i) => ( ))} )} {nests.length && ( {nests.map((n, i) => ( ))} )} ); };