Add basic animation support to the mandala page (#75)

This addresses part of #71 by adding some basic animation support to the mandala page.
pull/77/head
Atul Varma 2021-04-03 17:01:13 -04:00 zatwierdzone przez GitHub
rodzic be8b4ec04b
commit 6136253cd6
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
6 zmienionych plików z 130 dodań i 22 usunięć

Wyświetl plik

@ -114,6 +114,10 @@ ul.navbar li:last-child {
overflow: auto;
}
.sidebar .disabled {
color: gray;
}
.sidebar label.checkbox {
display: block;
margin-top: 10px;

Wyświetl plik

@ -1,33 +1,60 @@
import React, { useEffect, useRef, useState } from "react";
type AutoSizingSvgProps = {
padding: number;
bgColor?: string;
sizeToElement?: React.RefObject<HTMLElement>;
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<SVGSVGElement>
) => {
const { bgColor, padding } = props;
(props: AutoSizingSvgProps, ref: React.ForwardedRef<SVGSVGElement>) => {
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<SVGGElement>(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);
}
}
});

Wyświetl plik

@ -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<NumericSliderProps> = (props) => {
const id = props.id || slugify(props.label);
return (
<div className="thingy numeric-slider">
<div
className={classNames("thingy", "numeric-slider", {
disabled: props.disabled,
})}
>
<label htmlFor={id}>{props.label}: </label>
<span className="slider">
<input
@ -23,6 +29,7 @@ export const NumericSlider: React.FC<NumericSliderProps> = (props) => {
max={props.max}
value={props.value}
step={props.step}
disabled={props.disabled}
onChange={(e) => props.onChange(float(e.target.value))}
/>
<span>

Wyświetl plik

@ -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<{
<NumericSlider
id={`${idPrefix}symbolRotation`}
label="Symbol rotation"
disabled={value.animateSymbolRotation}
value={value.symbolRotation}
onChange={(symbolRotation) => onChange({ ...value, symbolRotation })}
{...ROTATION}
/>
<Checkbox
label="Animate symbol rotation"
value={value.animateSymbolRotation}
onChange={(animateSymbolRotation) =>
onChange({ ...value, animateSymbolRotation })
}
/>
<Checkbox
label="Invert every other symbol (applies only to circles with an even number of symbols)"
value={value.invertEveryOtherSymbol}
@ -250,8 +280,31 @@ function getRandomCircleParams(rng: Random): MandalaCircleParams {
};
}
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<SVGSVGElement>(null);
const canvasRef = useRef<HTMLDivElement>(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 = [
<ExtendedMandalaCircle key="first" {...circle1} {...symbolCtx} />,
<ExtendedMandalaCircle
key="first"
{...animateMandalaCircleParams(circle1, frameNumber)}
{...symbolCtx}
/>,
];
if (useTwoCircles) {
circles.push(
<ExtendedMandalaCircle key="second" {...circle2} {...circle2SymbolCtx} />
<ExtendedMandalaCircle
key="second"
{...animateMandalaCircleParams(circle2, frameNumber)}
{...circle2SymbolCtx}
/>
);
if (firstBehindSecond) {
circles.reverse();
@ -330,12 +392,14 @@ export const MandalaPage: React.FC<{}> = () => {
<div
className="canvas"
style={{ backgroundColor: baseCompCtx.background }}
ref={canvasRef}
>
<HoverDebugHelper>
<AutoSizingSvg
padding={20}
ref={svgRef}
bgColor={baseCompCtx.background}
sizeToElement={canvasRef}
>
<SvgTransform transform={svgScale(0.5)}>{circles}</SvgTransform>
</AutoSizingSvg>

5
package-lock.json wygenerowano
Wyświetl plik

@ -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",

Wyświetl plik

@ -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",