Make animation duration changeable (#77)

This adds a slider that allows the duration of the mandala animation to be changed (for #71).

In so doing, it also decouples the animation speed from the display's refresh rate.
pull/78/head
Atul Varma 2021-04-04 08:34:29 -04:00 zatwierdzone przez GitHub
rodzic 2ce4f8a602
commit dc13207b4d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
2 zmienionych plików z 70 dodań i 28 usunięć

45
lib/animation.ts 100644
Wyświetl plik

@ -0,0 +1,45 @@
import { useEffect, useState } from "react";
/**
* A React hook that can be used for animation.
*
* Given a duration in milliseconds, returns the percentage through
* that duration that has passed as a floating-point number
* from 0.0 to 1.0.
*
* Changes in the returned value will be triggered by `requestAnimationFrame`,
* so they will likely be tied to the monitor's refresh rate.
*
* Assumes that the animation is looping, so the percentage will
* reset to zero once it has finished.
*/
export function useAnimationPct(durationMs: number): number {
const [pct, setPct] = useState(0);
const [lastTimestamp, setLastTimestamp] = useState<number | undefined>(
undefined
);
useEffect(() => {
if (!durationMs) {
setPct(0);
setLastTimestamp(undefined);
return;
}
const callback = (timestamp: number) => {
if (typeof lastTimestamp === "number") {
const timeDelta = timestamp - lastTimestamp;
const pctDelta = timeDelta / durationMs;
setPct((pct + pctDelta) % 1.0);
}
setLastTimestamp(timestamp);
};
const timeout = requestAnimationFrame(callback);
return () => {
cancelAnimationFrame(timeout);
};
}, [durationMs, pct, lastTimestamp]);
return pct;
}

Wyświetl plik

@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import { AutoSizingSvg } from "../auto-sizing-svg";
import { ExportWidget } from "../export-svg";
import { HoverDebugHelper } from "../hover-debug-helper";
@ -20,6 +20,7 @@ import {
} from "../svg-composition-context";
import { Page } from "../page";
import { MandalaCircle, MandalaCircleParams } from "../mandala-circle";
import { useAnimationPct } from "../animation";
type ExtendedMandalaCircleParams = MandalaCircleParams & {
scaling: number;
@ -77,6 +78,14 @@ const ROTATION: NumericRange = {
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 }) => {
@ -94,12 +103,12 @@ const ExtendedMandalaCircle: React.FC<
function animateMandalaCircleParams(
value: ExtendedMandalaCircleParams,
frameNumber: number
animPct: number
): ExtendedMandalaCircleParams {
if (value.animateSymbolRotation) {
value = {
...value,
symbolRotation: frameNumber % ROTATION.max,
symbolRotation: animPct * ROTATION.max,
};
}
return value;
@ -195,33 +204,12 @@ 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 [durationSecs, setDurationSecs] = useState(DEFAULT_DURATION_SECS);
const [baseCompCtx, setBaseCompCtx] = useState(createSvgCompositionContext());
const [useTwoCircles, setUseTwoCircles] = useState(false);
const [invertCircle2, setInvertCircle2] = useState(true);
@ -232,7 +220,7 @@ export const MandalaPage: React.FC<{}> = () => {
setCircle2({ ...circle2, ...getRandomCircleParams(rng) });
};
const isAnimated = isAnyMandalaCircleAnimated([circle1, circle2]);
const frameNumber = useAnimation(isAnimated);
const animPct = useAnimationPct(isAnimated ? durationSecs * 1000 : 0);
const symbolCtx = noFillIfShowingSpecs(baseCompCtx);
const circle2SymbolCtx = invertCircle2 ? swapColors(symbolCtx) : symbolCtx;
@ -240,7 +228,7 @@ export const MandalaPage: React.FC<{}> = () => {
const circles = [
<ExtendedMandalaCircle
key="first"
{...animateMandalaCircleParams(circle1, frameNumber)}
{...animateMandalaCircleParams(circle1, animPct)}
{...symbolCtx}
/>,
];
@ -249,7 +237,7 @@ export const MandalaPage: React.FC<{}> = () => {
circles.push(
<ExtendedMandalaCircle
key="second"
{...animateMandalaCircleParams(circle2, frameNumber)}
{...animateMandalaCircleParams(circle2, animPct)}
{...circle2SymbolCtx}
/>
);
@ -297,6 +285,15 @@ export const MandalaPage: React.FC<{}> = () => {
/>
</fieldset>
)}
{isAnimated && (
<NumericSlider
label="Animation loop duration"
valueSuffix="s"
value={durationSecs}
onChange={(duration) => setDurationSecs(duration)}
{...DURATION_SECS}
/>
)}
<div className="thingy">
<button accessKey="r" onClick={randomize}>
<u>R</u>andomize!