import React, { useMemo, useRef, useState } from "react"; import { AutoSizingSvg } from "../../auto-sizing-svg"; import { AnimationRenderer, ExportWidget } from "../../export-svg"; import { HoverDebugHelper } from "../../hover-debug-helper"; import { NumericSlider } from "../../numeric-slider"; import { noFillIfShowingSpecs, SvgSymbolContext, swapColors, } from "../../svg-symbol"; import { VocabularyWidget } from "../../vocabulary-widget"; import { svgRotate, svgScale, SvgTransform } from "../../svg-transform"; import { SvgVocabulary } from "../../svg-vocabulary"; import { isEvenNumber, NumericRange, secsToMsecs } from "../../util"; import { Random } from "../../random"; import { Checkbox } from "../../checkbox"; import { CompositionContextWidget, createSvgCompositionContext, } from "../../svg-composition-context"; import { Page } from "../../page"; import { MandalaCircle, MandalaCircleParams } from "../../mandala-circle"; import { useAnimationPct } from "../../animation"; import { RandomizerWidget } from "../../randomizer-widget"; import { useDebouncedEffect } from "../../use-debounced-effect"; export 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 RADIUS_RANDOM: NumericRange = { min: 100, max: RADIUS.max, 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, }; const DURATION_SECS: NumericRange = { min: 0.5, max: 10, step: 0.1, }; const DEFAULT_DURATION_SECS = 3; const ExtendedMandalaCircle: React.FC< ExtendedMandalaCircleParams & SvgSymbolContext > = ({ scaling, rotation, symbolScaling, symbolRotation, ...props }) => { props = { ...props, symbolTransforms: [svgScale(symbolScaling), svgRotate(symbolRotation)], }; return ( ); }; function animateMandalaCircleParams( value: ExtendedMandalaCircleParams, animPct: number ): ExtendedMandalaCircleParams { if (value.animateSymbolRotation) { const direction = value.data.meta?.rotate_clockwise ? 1 : -1; value = { ...value, symbolRotation: direction * animPct * ROTATION.max, }; } return value; } 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_RANDOM), numSymbols: rng.inRange(NUM_SYMBOLS), invertEveryOtherSymbol: rng.bool(), }; } export const MANDALA_DESIGN_DEFAULTS = { circle1: CIRCLE_1_DEFAULTS, circle2: CIRCLE_2_DEFAULTS, durationSecs: DEFAULT_DURATION_SECS, baseCompCtx: createSvgCompositionContext(), useTwoCircles: false, invertCircle2: true, firstBehind: false, }; export type MandalaDesign = typeof MANDALA_DESIGN_DEFAULTS; function isDesignAnimated({ circle1, circle2 }: MandalaDesign): boolean { return [circle1, circle2].some((value) => value.animateSymbolRotation); } function createAnimationRenderer({ baseCompCtx, invertCircle2, circle1, circle2, useTwoCircles, firstBehind, }: MandalaDesign): AnimationRenderer { const symbolCtx = noFillIfShowingSpecs(baseCompCtx); const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx; return (animPct) => { const circles = [ , ]; if (useTwoCircles) { circles.push( ); if (firstBehind) { circles.reverse(); } } return {circles}; }; } const AnimatedMandala: React.FC<{ config: MandalaDesign; render: AnimationRenderer; }> = ({ config, render }) => { const animPct = useAnimationPct( isDesignAnimated(config) ? secsToMsecs(config.durationSecs) : 0 ); return <>{render(animPct)}; }; /** * A mandala page that starts with the given default mandala configuration. * * The given handler will be called whenever the user changes the * configuration. * * Note that the default is only used to determine the initial state of * the component at mount. Any changes to the prop once the component has * been mounted are ignored. */ export const MandalaPageWithDefaults: React.FC<{ defaults: MandalaDesign; onChange: (defaults: MandalaDesign) => void; }> = ({ defaults, onChange }) => { const svgRef = useRef(null); const canvasRef = useRef(null); const [circle1, setCircle1] = useState(defaults.circle1); const [circle2, setCircle2] = useState(defaults.circle2); const [durationSecs, setDurationSecs] = useState(defaults.durationSecs); const [baseCompCtx, setBaseCompCtx] = useState(defaults.baseCompCtx); const [useTwoCircles, setUseTwoCircles] = useState(defaults.useTwoCircles); const [invertCircle2, setInvertCircle2] = useState(defaults.invertCircle2); const [firstBehind, setFirstBehind] = useState(defaults.firstBehind); const design: MandalaDesign = useMemo( () => ({ circle1, circle2, durationSecs, baseCompCtx, useTwoCircles, invertCircle2, firstBehind, }), [ circle1, circle2, durationSecs, baseCompCtx, useTwoCircles, invertCircle2, firstBehind, ] ); const isAnimated = isDesignAnimated(design); const render = useMemo(() => createAnimationRenderer(design), [design]); useDebouncedEffect(250, () => onChange(design), [onChange, design]); return (
First circle
{useTwoCircles && (
Second circle {" "}
)} {isAnimated && ( setDurationSecs(duration)} {...DURATION_SECS} /> )} setBaseCompCtx({ ...baseCompCtx, ...colors }) } onSymbolsChange={(rng) => { setCircle1({ ...circle1, ...getRandomCircleParams(rng) }); setCircle2({ ...circle2, ...getRandomCircleParams(rng) }); }} />
); };