From 6136253cd6b85863fed356801c83cdbed1873bab Mon Sep 17 00:00:00 2001 From: Atul Varma Date: Sat, 3 Apr 2021 17:01:13 -0400 Subject: [PATCH] Add basic animation support to the mandala page (#75) This addresses part of #71 by adding some basic animation support to the mandala page. --- index.html | 4 +++ lib/auto-sizing-svg.tsx | 61 +++++++++++++++++++++++--------- lib/numeric-slider.tsx | 9 ++++- lib/pages/mandala-page.tsx | 72 +++++++++++++++++++++++++++++++++++--- package-lock.json | 5 +++ package.json | 1 + 6 files changed, 130 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index 6aa9c9d..9f05c85 100644 --- a/index.html +++ b/index.html @@ -114,6 +114,10 @@ ul.navbar li:last-child { overflow: auto; } +.sidebar .disabled { + color: gray; +} + .sidebar label.checkbox { display: block; margin-top: 10px; diff --git a/lib/auto-sizing-svg.tsx b/lib/auto-sizing-svg.tsx index 853c61c..5135088 100644 --- a/lib/auto-sizing-svg.tsx +++ b/lib/auto-sizing-svg.tsx @@ -1,33 +1,60 @@ import React, { useEffect, useRef, useState } from "react"; +type AutoSizingSvgProps = { + padding: number; + bgColor?: string; + sizeToElement?: React.RefObject; + children: JSX.Element | JSX.Element[]; +}; + +function useResizeHandler(onResize: () => void) { + useEffect(() => { + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }); +} + /** * An SVG element with an optional background color that - * automatically sizes itself to its contents. + * automatically sizes itself to either its contents, or + * if the `sizeToElement` prop is provided, to the given + * container. */ export const AutoSizingSvg = React.forwardRef( - ( - props: { - padding: number; - bgColor?: string; - children: JSX.Element | JSX.Element[]; - }, - ref: React.ForwardedRef - ) => { - const { bgColor, padding } = props; + (props: AutoSizingSvgProps, ref: React.ForwardedRef) => { + const { bgColor, padding, sizeToElement } = props; const [x, setX] = useState(0); const [y, setY] = useState(0); const [width, setWidth] = useState(1); const [height, setHeight] = useState(1); const gRef = useRef(null); + const resizeToElement = () => { + if (sizeToElement?.current) { + const bbox = sizeToElement.current.getBoundingClientRect(); + setX(-bbox.width / 2); + setY(-bbox.height / 2); + setWidth(bbox.width); + setHeight(bbox.height); + return true; + } + return false; + }; + + useResizeHandler(resizeToElement); useEffect(() => { - const svgEl = gRef.current; - if (svgEl) { - const bbox = svgEl.getBBox(); - setX(bbox.x - padding); - setY(bbox.y - padding); - setWidth(bbox.width + padding * 2); - setHeight(bbox.height + padding * 2); + if (!resizeToElement()) { + const svgEl = gRef.current; + if (svgEl) { + const bbox = svgEl.getBBox(); + setX(bbox.x - padding); + setY(bbox.y - padding); + setWidth(bbox.width + padding * 2); + setHeight(bbox.height + padding * 2); + } } }); diff --git a/lib/numeric-slider.tsx b/lib/numeric-slider.tsx index 2f0b24a..6205b44 100644 --- a/lib/numeric-slider.tsx +++ b/lib/numeric-slider.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import React from "react"; import { float, NumericRange, slugify } from "./util"; @@ -7,13 +8,18 @@ export type NumericSliderProps = NumericRange & { onChange: (value: number) => void; value: number; valueSuffix?: string; + disabled?: boolean; }; export const NumericSlider: React.FC = (props) => { const id = props.id || slugify(props.label); return ( -
+
= (props) => { max={props.max} value={props.value} step={props.step} + disabled={props.disabled} onChange={(e) => props.onChange(float(e.target.value))} /> diff --git a/lib/pages/mandala-page.tsx b/lib/pages/mandala-page.tsx index ea75e4c..1783020 100644 --- a/lib/pages/mandala-page.tsx +++ b/lib/pages/mandala-page.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { AutoSizingSvg } from "../auto-sizing-svg"; import { getBoundingBoxCenter } from "../bounding-box"; import { ExportWidget } from "../export-svg"; @@ -36,6 +36,7 @@ type ExtendedMandalaCircleParams = MandalaCircleParams & { rotation: number; symbolScaling: number; symbolRotation: number; + animateSymbolRotation: boolean; }; const CIRCLE_1_DEFAULTS: ExtendedMandalaCircleParams = { @@ -47,6 +48,7 @@ const CIRCLE_1_DEFAULTS: ExtendedMandalaCircleParams = { symbolScaling: 1, symbolRotation: 0, invertEveryOtherSymbol: false, + animateSymbolRotation: false, }; const CIRCLE_2_DEFAULTS: ExtendedMandalaCircleParams = { @@ -58,6 +60,7 @@ const CIRCLE_2_DEFAULTS: ExtendedMandalaCircleParams = { symbolScaling: 1, symbolRotation: 0, invertEveryOtherSymbol: false, + animateSymbolRotation: false, }; const RADIUS: NumericRange = { @@ -174,6 +177,25 @@ const ExtendedMandalaCircle: React.FC< ); }; +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; @@ -226,10 +248,18 @@ const ExtendedMandalaCircleParamsWidget: React.FC<{ onChange({ ...value, symbolRotation })} {...ROTATION} /> + + onChange({ ...value, animateSymbolRotation }) + } + /> { + 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()); @@ -263,18 +316,27 @@ export const MandalaPage: React.FC<{}> = () => { 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(); @@ -330,12 +392,14 @@ export const MandalaPage: React.FC<{}> = () => {
{circles} diff --git a/package-lock.json b/package-lock.json index d63314f..dbe0110 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2703,6 +2703,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", diff --git a/package.json b/package.json index 3c64d16..d837a3b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/react-dom": "^17.0.0", "babel-jest": "^26.6.3", "cheerio": "^1.0.0-rc.5", + "classnames": "^2.3.1", "gh-pages": "^3.1.0", "jest": "^26.6.3", "parcel-bundler": "^1.12.4",