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
rodzic
2ce4f8a602
commit
dc13207b4d
|
@ -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;
|
||||
}
|
|
@ -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!
|
||||
|
|
Ładowanie…
Reference in New Issue