diff --git a/lib/creature-symbol-factory.tsx b/lib/creature-symbol-factory.tsx new file mode 100644 index 0000000..1285486 --- /dev/null +++ b/lib/creature-symbol-factory.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { + AttachedCreatureSymbol, + CreatureSymbol, + NestedCreatureSymbol, +} from "./creature-symbol"; +import { AttachmentPointType } from "./specs"; +import { SvgSymbolData } from "./svg-symbol"; + +type AttachmentIndices = { + left?: boolean; + right?: boolean; +}; + +type AttachmentChildren = JSX.Element | JSX.Element[]; + +type SimpleCreatureSymbolProps = AttachmentIndices & { + nestInside?: boolean; + children?: AttachmentChildren; + attachTo?: AttachmentPointType; + indices?: number[]; +}; + +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; +} + +type SplitCreatureSymbolChildren = { + attachments: JSX.Element[]; + nests: JSX.Element[]; +}; + +function splitCreatureSymbolChildren( + children?: AttachmentChildren +): SplitCreatureSymbolChildren { + const result: SplitCreatureSymbolChildren = { + attachments: [], + nests: [], + }; + if (!children) return result; + + React.Children.forEach(children, (child) => { + if (child.props.nestInside) { + result.nests.push(child); + } else { + result.attachments.push(child); + } + }); + + return result; +} + +type SimpleCreatureSymbolFC = React.FC & { + creatureSymbolData: SvgSymbolData; +}; + +/** + * Create a factory that can be used to return React components to + * render a ``. + */ +export function createCreatureSymbolFactory( + getSymbol: (name: string) => SvgSymbolData +) { + /** + * Returns a React component that renders a ``, using the symbol + * with the given name as its default data. + */ + return function createCreatureSymbol( + name: string + ): React.FC { + const data = getSymbol(name); + const Component: SimpleCreatureSymbolFC = (props) => { + const symbol = getCreatureSymbol(data, props); + return ; + }; + Component.creatureSymbolData = data; + return Component; + }; +} + +function isSimpleCreatureSymbolFC(fn: any): fn is SimpleCreatureSymbolFC { + return !!fn.creatureSymbolData; +} + +function extractNestedCreatureSymbol(el: JSX.Element): NestedCreatureSymbol { + const base = extractCreatureSymbolFromElement(el); + const props: SimpleCreatureSymbolProps = el.props; + const indices = props.indices || getAttachmentIndices(props); + const result: NestedCreatureSymbol = { + ...base, + indices, + }; + return result; +} + +function extractAttachedCreatureSymbol( + el: JSX.Element +): AttachedCreatureSymbol { + const base = extractNestedCreatureSymbol(el); + const props: SimpleCreatureSymbolProps = el.props; + const { attachTo } = props; + if (!attachTo) { + throw new Error("Expected attachment to have `attachTo` prop!"); + } + const result: AttachedCreatureSymbol = { + ...base, + attachTo, + }; + return result; +} + +function getCreatureSymbol( + data: SvgSymbolData, + props: SimpleCreatureSymbolProps +): CreatureSymbol { + const { attachments, nests } = splitCreatureSymbolChildren(props.children); + const result: CreatureSymbol = { + data, + attachments: attachments.map(extractAttachedCreatureSymbol), + nests: nests.map(extractNestedCreatureSymbol), + }; + return result; +} + +export function extractCreatureSymbolFromElement( + el: JSX.Element +): CreatureSymbol { + if (isSimpleCreatureSymbolFC(el.type)) { + return getCreatureSymbol(el.type.creatureSymbolData, el.props); + } + throw new Error("Found unknown component type!"); +} diff --git a/lib/creature-symbol.tsx b/lib/creature-symbol.tsx index 7d79a76..c8b8b6f 100644 --- a/lib/creature-symbol.tsx +++ b/lib/creature-symbol.tsx @@ -1,8 +1,8 @@ import React, { useContext } from "react"; -import { getAttachmentTransforms } from "./attach"; -import { scalePointXY, subtractPoints } from "./point"; 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, @@ -54,8 +54,6 @@ function safeGetAttachmentPoint( return null; } -type AttachmentChildren = JSX.Element | JSX.Element[]; - export type CreatureContextType = SvgSymbolContext & { attachmentScale: number; parent: SvgSymbolData | null; @@ -67,206 +65,29 @@ export const CreatureContext = React.createContext({ parent: null, }); -type AttachmentIndices = { - left?: boolean; - right?: boolean; -}; - -export type CreatureSymbolProps = AttachmentIndices & { - data: SvgSymbolData; - nestInside?: boolean; - children?: AttachmentChildren; - attachTo?: AttachmentPointType; - indices?: number[]; -}; - -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; -} - -type SplitCreatureSymbolChildren = { - attachments: JSX.Element[]; - nests: JSX.Element[]; -}; - -function splitCreatureSymbolChildren( - children?: AttachmentChildren -): SplitCreatureSymbolChildren { - const result: SplitCreatureSymbolChildren = { - attachments: [], - nests: [], - }; - if (!children) return result; - - React.Children.forEach(children, (child) => { - if (child.props.nestInside) { - result.nests.push(child); - } else { - result.attachments.push(child); - } - }); - - return result; -} - -type ChildCreatureSymbolProps = { - symbol: JSX.Element; - data: SvgSymbolData; - parent: SvgSymbolData; +export type AttachedCreatureSymbol = CreatureSymbol & { + attachTo: AttachmentPointType; indices: number[]; }; -const NestedCreatureSymbol: React.FC = ({ - symbol, - data, - parent, - indices, -}) => { - 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( - - - {symbol} - - - ); - } - - return <>{children}; +export type NestedCreatureSymbol = CreatureSymbol & { + indices: number[]; }; -const AttachedCreatureSymbol: React.FC< - ChildCreatureSymbolProps & { - attachTo: AttachmentPointType; - } -> = ({ symbol, data, parent, indices, attachTo }) => { - 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( - - - {symbol} - - - ); - } - - return <>{children}; +export type CreatureSymbol = { + data: SvgSymbolData; + attachments: AttachedCreatureSymbol[]; + nests: NestedCreatureSymbol[]; }; -export const CreatureSymbol: React.FC = (props) => { - const ctx = useContext(CreatureContext); - const { data, attachTo, nestInside } = props; - const childCtx: CreatureContextType = { ...ctx, parent: data }; - const { nests, attachments } = splitCreatureSymbolChildren(props.children); +export type CreatureSymbolProps = CreatureSymbol; - // 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. - const symbol = ( - <> - {attachments.length && ( - - {attachments} - - )} - - {nests.length && ( - - {nests} - - )} - - ); +type NestedCreatureSymbolProps = NestedCreatureSymbol & { + parent: SvgSymbolData; +}; - if (!(attachTo || nestInside)) { - return symbol; - } - - const parent = ctx.parent; - if (!parent) { - throw new Error( - `Cannot attach/nest ${props.data.name} because it has no parent!` - ); - } - - const childProps: ChildCreatureSymbolProps = { - parent, - symbol, - data, - indices: props.indices || getAttachmentIndices(props), - }; - - if (attachTo) { - return ; - } - - return ; +type AttachedCreatureSymbolProps = AttachedCreatureSymbol & { + parent: SvgSymbolData; }; function getNestingTransforms(parent: BBox, child: BBox) { @@ -310,3 +131,126 @@ const AttachmentTransform: React.FC = (props) => ( ); + +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) => { + const ctx = useContext(CreatureContext); + const { data, attachments, nests } = props; + const childCtx: 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) => ( + + ))} + + )} + + ); +}; diff --git a/lib/pages/creature-page.tsx b/lib/pages/creature-page.tsx index eaa64ec..4fce00a 100644 --- a/lib/pages/creature-page.tsx +++ b/lib/pages/creature-page.tsx @@ -8,11 +8,15 @@ import { range } from "../util"; import { AutoSizingSvg } from "../auto-sizing-svg"; import { exportSvg } from "../export-svg"; +import { + createCreatureSymbolFactory, + extractCreatureSymbolFromElement, +} from "../creature-symbol-factory"; import { CreatureContext, CreatureContextType, CreatureSymbol, - CreatureSymbolProps, + NestedCreatureSymbol, } from "../creature-symbol"; import { HoverDebugHelper } from "../hover-debug-helper"; @@ -57,13 +61,21 @@ function getSymbol(name: string): SvgSymbolData { * Can return an empty array e.g. if the parent symbol doesn't have * any nesting areas. */ -function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] { +function getNestingChildren( + parent: SvgSymbolData, + rng: Random +): NestedCreatureSymbol[] { const { meta, specs } = parent; if (meta?.always_nest && specs?.nesting) { const indices = range(specs.nesting.length); const child = rng.choice(NESTED_SYMBOLS); return [ - , + { + data: child, + attachments: [], + nests: [], + indices, + }, ]; } return []; @@ -77,9 +89,13 @@ function getNestingChildren(parent: SvgSymbolData, rng: Random): JSX.Element[] { function getSymbolWithAttachments( numAttachmentKinds: number, rng: Random -): JSX.Element { - const children: JSX.Element[] = []; +): CreatureSymbol { const root = rng.choice(ROOT_SYMBOLS); + const result: CreatureSymbol = { + data: root, + attachments: [], + nests: getNestingChildren(root, rng), + }; if (root.specs) { const attachmentKinds = rng.uniqueChoices( Array.from(iterAttachmentPoints(root.specs)) @@ -90,60 +106,39 @@ function getSymbolWithAttachments( for (let kind of attachmentKinds) { const attachment = rng.choice(ATTACHMENT_SYMBOLS); const indices = range(root.specs[kind]?.length ?? 0); - children.push( - - ); + result.attachments.push({ + data: attachment, + attachTo: kind, + indices, + attachments: [], + nests: getNestingChildren(attachment, rng), + }); } } - children.push(...getNestingChildren(root, rng)); - return ; + return result; } -/** - * A creature symbol that comes with default (but overrideable) symbol data. - * This makes it easy to use the symbol in JSX, but also easy to dynamically - * replace the symbol with a different one. - */ -type CreatureSymbolWithDefaultProps = Omit & { - data?: SvgSymbolData; -}; +const symbol = createCreatureSymbolFactory(getSymbol); -/** - * Returns a React component that renders a ``, using the symbol - * with the given name as its default data. - */ -function createCreatureSymbol( - name: string -): React.FC { - const data = getSymbol(name); - return (props) => ; -} +const Eye = symbol("eye"); -const Eye = createCreatureSymbol("eye"); +const Hand = symbol("hand"); -const Hand = createCreatureSymbol("hand"); +const Arm = symbol("arm"); -const Arm = createCreatureSymbol("arm"); +const Antler = symbol("antler"); -const Antler = createCreatureSymbol("antler"); +const Crown = symbol("crown"); -const Crown = createCreatureSymbol("crown"); +const Wing = symbol("wing"); -const Wing = createCreatureSymbol("wing"); +const MuscleArm = symbol("muscle_arm"); -const MuscleArm = createCreatureSymbol("muscle_arm"); +const Leg = symbol("leg"); -const Leg = createCreatureSymbol("leg"); +const Tail = symbol("tail"); -const Tail = createCreatureSymbol("tail"); - -const Lightning = createCreatureSymbol("lightning"); +const Lightning = symbol("lightning"); const EYE_CREATURE = ( @@ -165,22 +160,28 @@ const EYE_CREATURE = ( ); +const EYE_CREATURE_SYMBOL = extractCreatureSymbolFromElement(EYE_CREATURE); + /** * Randomly replace all the parts of the given creature. Note that this * might end up logging some console messages about not being able to find * attachment/nesting indices, because it doesn't really check to make * sure the final creature structure is fully valid. */ -function randomlyReplaceParts(rng: Random, creature: JSX.Element): JSX.Element { - return React.cloneElement(creature, { +function randomlyReplaceParts( + rng: Random, + creature: T +): T { + const result: T = { + ...creature, data: rng.choice(SvgVocabulary), - children: React.Children.map(creature.props.children, (child, i) => { - return randomlyReplaceParts(rng, child); - }), - }); + attachments: creature.attachments.map((a) => randomlyReplaceParts(rng, a)), + nests: creature.nests.map((n) => randomlyReplaceParts(rng, n)), + }; + return result; } -type CreatureGenerator = (rng: Random) => JSX.Element; +type CreatureGenerator = (rng: Random) => CreatureSymbol; /** * Each index of this array represents the algorithm we use to @@ -191,7 +192,7 @@ type CreatureGenerator = (rng: Random) => JSX.Element; */ const COMPLEXITY_LEVEL_GENERATORS: CreatureGenerator[] = [ ...range(5).map((i) => getSymbolWithAttachments.bind(null, i)), - (rng) => randomlyReplaceParts(rng, EYE_CREATURE), + (rng) => randomlyReplaceParts(rng, EYE_CREATURE_SYMBOL), ]; const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1; @@ -221,7 +222,7 @@ export const CreaturePage: React.FC<{}> = () => { }; const creature = randomSeed === null - ? EYE_CREATURE + ? EYE_CREATURE_SYMBOL : COMPLEXITY_LEVEL_GENERATORS[complexity](new Random(randomSeed)); const handleSvgExport = () => exportSvg(getDownloadFilename(randomSeed), svgRef); @@ -262,7 +263,9 @@ export const CreaturePage: React.FC<{}> = () => { - {creature} + + +