import React, { useEffect, useRef, useState } from "react"; import { AutoSizingSvg } from "../auto-sizing-svg"; import { getBoundingBoxCenter } from "../bounding-box"; import { ExportWidget } from "../export-svg"; import { HoverDebugHelper } from "../hover-debug-helper"; import { NumericSlider } from "../numeric-slider"; import { noFillIfShowingSpecs, safeGetAttachmentPoint, SvgSymbolContent, SvgSymbolContext, SvgSymbolData, swapColors, } from "../svg-symbol"; import { VocabularyWidget } from "../vocabulary-widget"; import { svgRotate, svgScale, SvgTransform, svgTranslate, } from "../svg-transform"; import { SvgVocabulary } from "../svg-vocabulary"; import { NumericRange, range } from "../util"; import { Random } from "../random"; import { PointWithNormal } from "../specs"; import { getAttachmentTransforms } from "../attach"; import { Checkbox } from "../checkbox"; import { CompositionContextWidget, createSvgCompositionContext, } from "../svg-composition-context"; import { Page } from "../page"; type ExtendedMandalaCircleParams = MandalaCircleParams & { scaling: number; rotation: number; symbolScaling: number; symbolRotation: number; animateSymbolRotation: boolean; }; const CIRCLE_1_DEFAULTS: ExtendedMandalaCircleParams = { data: SvgVocabulary.get("eye"), radius: 300, numSymbols: 5, scaling: 1, rotation: 0, symbolScaling: 1, symbolRotation: 0, invertEveryOtherSymbol: false, animateSymbolRotation: false, }; const CIRCLE_2_DEFAULTS: ExtendedMandalaCircleParams = { data: SvgVocabulary.get("leg"), radius: 0, numSymbols: 3, scaling: 0.5, rotation: 0, symbolScaling: 1, symbolRotation: 0, invertEveryOtherSymbol: false, animateSymbolRotation: false, }; const RADIUS: NumericRange = { min: -500, max: 500, step: 1, }; const NUM_SYMBOLS: NumericRange = { min: 1, max: 20, step: 1, }; const SCALING: NumericRange = { min: 0.1, max: 1, step: 0.05, }; const ROTATION: NumericRange = { min: 0, max: 359, step: 1, }; /** * Returns the anchor point of the given symbol; if it doesn't have * an anchor point, return a reasonable default one by taking the * center of the symbol and having the normal point along the negative * y-axis (i.e., up). */ function getAnchorOrCenter(symbol: SvgSymbolData): PointWithNormal { return ( safeGetAttachmentPoint(symbol, "anchor") || { point: getBoundingBoxCenter(symbol.bbox), normal: { x: 0, y: -1 }, } ); } type MandalaCircleParams = { data: SvgSymbolData; radius: number; numSymbols: number; symbolTransforms?: SvgTransform[]; invertEveryOtherSymbol: boolean; }; type MandalaCircleProps = MandalaCircleParams & SvgSymbolContext; function isEvenNumber(value: number) { return value % 2 === 0; } const MandalaCircle: React.FC = (props) => { const degreesPerItem = 360 / props.numSymbols; const { translation, rotation } = getAttachmentTransforms( { point: { x: 0, y: 0 }, normal: { x: 0, y: -1 }, }, getAnchorOrCenter(props.data) ); const transform: SvgTransform[] = [ // Remember that transforms are applied in reverse order, // so read the following from the end first! // Finally, move the symbol out along the radius of the circle. svgTranslate({ x: 0, y: -props.radius }), // Then apply any individual symbol transformations. ...(props.symbolTransforms || []), // First, re-orient the symbol so its anchor point is at // the origin and facing the proper direction. svgRotate(rotation), svgTranslate(translation), ]; const invertEveryOtherSymbol = isEvenNumber(props.numSymbols) && props.invertEveryOtherSymbol; const symbols = range(props.numSymbols) .reverse() .map((i) => ( {invertEveryOtherSymbol && isEvenNumber(i) ? ( ) : ( )} )); return <>{symbols}; }; const ExtendedMandalaCircle: React.FC< ExtendedMandalaCircleParams & SvgSymbolContext > = ({ scaling, rotation, symbolScaling, symbolRotation, ...props }) => { props = { ...props, symbolTransforms: [svgScale(symbolScaling), svgRotate(symbolRotation)], }; return ( ); }; function animateMandalaCircleParams( value: ExtendedMandalaCircleParams, frameNumber: number ): ExtendedMandalaCircleParams { if (value.animateSymbolRotation) { value = { ...value, symbolRotation: frameNumber % ROTATION.max, }; } return value; } function isAnyMandalaCircleAnimated( values: ExtendedMandalaCircleParams[] ): boolean { return values.some((value) => value.animateSymbolRotation); } const ExtendedMandalaCircleParamsWidget: React.FC<{ idPrefix: string; value: ExtendedMandalaCircleParams; onChange: (value: ExtendedMandalaCircleParams) => void; }> = ({ idPrefix, value, onChange }) => { return (
onChange({ ...value, data })} choices={SvgVocabulary} /> onChange({ ...value, radius })} {...RADIUS} /> onChange({ ...value, numSymbols })} {...NUM_SYMBOLS} /> onChange({ ...value, scaling })} {...SCALING} /> onChange({ ...value, rotation })} {...ROTATION} /> onChange({ ...value, symbolScaling })} {...SCALING} /> onChange({ ...value, symbolRotation })} {...ROTATION} /> onChange({ ...value, animateSymbolRotation }) } /> onChange({ ...value, invertEveryOtherSymbol }) } />
); }; function getRandomCircleParams(rng: Random): MandalaCircleParams { return { data: rng.choice(SvgVocabulary.items), radius: rng.inRange(RADIUS), numSymbols: rng.inRange(NUM_SYMBOLS), invertEveryOtherSymbol: rng.bool(), }; } function useAnimation(isEnabled: boolean): number { const [frameNumber, setFrameNumber] = useState(0); useEffect(() => { if (!isEnabled) { setFrameNumber(0); return; } const callback = () => { setFrameNumber(frameNumber + 1); }; const timeout = requestAnimationFrame(callback); return () => { cancelAnimationFrame(timeout); }; }, [isEnabled, frameNumber]); return frameNumber; } export const MandalaPage: React.FC<{}> = () => { const svgRef = useRef(null); const canvasRef = useRef(null); const [circle1, setCircle1] = useState(CIRCLE_1_DEFAULTS); const [circle2, setCircle2] = useState(CIRCLE_2_DEFAULTS); const [baseCompCtx, setBaseCompCtx] = useState(createSvgCompositionContext()); const [useTwoCircles, setUseTwoCircles] = useState(false); const [invertCircle2, setInvertCircle2] = useState(true); const [firstBehindSecond, setFirstBehindSecond] = useState(false); const randomize = () => { const rng = new Random(Date.now()); setCircle1({ ...circle1, ...getRandomCircleParams(rng) }); setCircle2({ ...circle2, ...getRandomCircleParams(rng) }); }; const isAnimated = isAnyMandalaCircleAnimated([circle1, circle2]); const frameNumber = useAnimation(isAnimated); const symbolCtx = noFillIfShowingSpecs(baseCompCtx); const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx; const circles = [ , ]; if (useTwoCircles) { circles.push( ); if (firstBehindSecond) { circles.reverse(); } } return (
First circle
{useTwoCircles && (
Second circle {" "}
)}
{" "}
{circles}
); };