import React, { useContext, useEffect, useRef, useState } from "react"; import { SvgVocabulary } from "../svg-vocabulary"; import { createSvgSymbolContext, SvgSymbolContent, SvgSymbolContext, SvgSymbolData, } from "../svg-symbol"; import { AttachmentPointType, PointWithNormal } from "../specs"; import { getAttachmentTransforms } from "../attach"; import { scalePointXY } from "../point"; import { Point } from "../../vendor/bezier-js"; import { Random } from "../random"; const SYMBOL_MAP = new Map( SvgVocabulary.map((symbol) => [symbol.name, symbol]) ); function getSymbol(name: string): SvgSymbolData { const symbol = SYMBOL_MAP.get(name); if (!symbol) { throw new Error(`Unable to find the symbol "${name}"!`); } return symbol; } function getAttachmentPoint( s: SvgSymbolData, type: AttachmentPointType, idx: number = 0 ): PointWithNormal { const { specs } = s; if (!specs) { throw new Error(`Symbol ${s.name} has no specs!`); } const points = specs[type]; if (!(points && points.length > idx)) { throw new Error( `Symbol ${s.name} must have at least ${ idx + 1 } ${type} attachment point(s)!` ); } return points[idx]; } type AttachmentChildren = JSX.Element | JSX.Element[]; type CreatureContextType = SvgSymbolContext & { attachmentScale: number; cumulativeScale: number; parent: SvgSymbolData | null; }; const DEFAULT_ATTACHMENT_SCALE = 0.5; const CreatureContext = React.createContext({ ...createSvgSymbolContext(), attachmentScale: DEFAULT_ATTACHMENT_SCALE, cumulativeScale: 1, parent: null, }); type AttachmentIndices = { left?: boolean; right?: boolean; }; type CreatureSymbolProps = AttachmentIndices & { data: SvgSymbolData; children?: AttachmentChildren; attachTo?: AttachmentPointType; }; function getAttachmentIndices(ai: AttachmentIndices): number[] { const result: number[] = []; if (ai.left) { result.push(0); } if (ai.right) { result.push(1); } if (result.length === 0) { result.push(0); } return result; } const CreatureSymbol: React.FC = (props) => { const ctx = useContext(CreatureContext); const { data, attachTo } = props; const ourSymbol = ( <> {props.children && ( {props.children} )} ); if (!attachTo) { return ourSymbol; } const parent = ctx.parent; if (!parent) { throw new Error( `Cannot attach ${props.data.name} because it has no parent!` ); } const attachmentIndices = getAttachmentIndices(props); const children: JSX.Element[] = []; for (let attachIndex of attachmentIndices) { const parentAp = getAttachmentPoint(parent, attachTo, attachIndex); const ourAp = getAttachmentPoint(data, "tail"); // If we're being attached as a tail, we want to actually rotate // the attachment an extra 180 degrees, as the tail attachment // point is facing the opposite direction that we actually // want to orient the tail in. const extraRot = attachTo === "tail" ? 180 : 0; // 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( {ourSymbol} ); } return <>{children}; }; type AttachmentTransformProps = { transformOrigin: Point; translate: Point; scale: Point; rotate: number; children: JSX.Element; }; const AttachmentTransform: React.FC = (props) => ( {props.children} ); type CreatureSymbolWithDefaultProps = Omit & { data?: SvgSymbolData; }; function createCreatureSymbol( name: string ): React.FC { const data = getSymbol(name); return (props) => ; } const Eye = createCreatureSymbol("eye"); const Hand = createCreatureSymbol("hand"); const Arm = createCreatureSymbol("arm"); const Antler = createCreatureSymbol("antler"); const Crown = createCreatureSymbol("crown"); const Wing = createCreatureSymbol("wing"); const MuscleArm = createCreatureSymbol("muscle arm"); const Leg = createCreatureSymbol("leg"); const Tail = createCreatureSymbol("tail"); const EYE_CREATURE = ( ); function randomlyReplaceParts(rng: Random, creature: JSX.Element): JSX.Element { return React.cloneElement(creature, { data: rng.choice(SvgVocabulary), children: React.Children.map(creature.props.children, (child, i) => { return randomlyReplaceParts(rng, child); }), }); } const SVG_PADDING = 5; export const CreaturePage: React.FC<{}> = () => { const ref = useRef(null); const [x, setX] = useState(0); const [y, setY] = useState(0); const [width, setWidth] = useState(1); const [height, setHeight] = useState(1); const [showSpecs, setShowSpecs] = useState(false); const [randomSeed, setRandomSeed] = useState(null); const defaultCtx = useContext(CreatureContext); const ctx: CreatureContextType = { ...defaultCtx, fill: showSpecs ? "none" : defaultCtx.fill, showSpecs, }; const creature = randomSeed === null ? EYE_CREATURE : randomlyReplaceParts(new Random(randomSeed), EYE_CREATURE); useEffect(() => { if (ref.current) { const bbox = ref.current.getBBox(); setX(bbox.x - SVG_PADDING); setY(bbox.y - SVG_PADDING); setWidth(bbox.width + SVG_PADDING * 2); setHeight(bbox.height + SVG_PADDING * 2); } }); return ( <>

Creature!

{creature} ); };