Skip to content

Commit

Permalink
Merge pull request #2 from whatsrupp/numeric-controls
Browse files Browse the repository at this point in the history
Numeric controls
  • Loading branch information
whatsrupp authored Jan 5, 2025
2 parents 72500c0 + aa73b85 commit 4d0418a
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 190 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

[![CI](https://github.com/whatsrupp/react-fidget-spinner/actions/workflows/merge-jobs.yml/badge.svg)](https://github.com/morewings/react-library-template/actions/workflows/merge-jobs.yml)
[![Storybook deploy](https://github.com/whatsrupp/react-fidget-spinner/actions/workflows/pages.yml/badge.svg)](https://github.com/whatsrupp/react-fidget-spinner/actions/workflows/pages.yml)
[![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@main/badge/badge-storybook.svg)](https://whatsrupp.github.io/react-fidget-spinner)

[![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@main/badge/badge-storybook.svg)](https://whatsrupp.github.io/react-fidget-spinner/?path=/docs/spinners-fidgetspinner--welcome)
[![npm version](https://img.shields.io/npm/v/react-fidget-spinner.svg)](https://www.npmjs.com/package/react-fidget-spinner)
Turn any react component into a fun clickable fidget spinner.

[![a silly goose](https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExMXFtdWc1eDVxcGd6dzMzbTI2ejV5bm1nbXZqa2w2cDRlM3VnZDZzeSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/p00fPNBjCUAKqogAyr/giphy.gif)](https://whatsrupp.github.io/react-fidget-spinner)
Expand Down Expand Up @@ -32,7 +32,7 @@ const MyFidgetSpinner = () => {

## Where are the full docs?

Interactive examples and full documentation can be found on [Storybook](https://whatsrupp.github.io/react-fidget-spinner).
Interactive examples and full documentation can be found on [Storybook](https://whatsrupp.github.io/react-fidget-spinner/?path=/docs/spinners-fidgetspinner--welcome).

## Features

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-fidget-spinner",
"homepage": "https://github.com/whatsrupp/react-fidget-spinner",
"homepage": "https://whatsrupp.github.io/react-fidget-spinner/?path=/docs/spinners-fidgetspinner--welcome",
"private": false,
"version": "0.1.13",
"type": "module",
Expand Down
115 changes: 56 additions & 59 deletions src/lib/FidgetSpinner/BubbleConfig.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,115 @@
import * as v from 'valibot';

import {EasingSchema} from './toBezierEasing';
import {VariationType, VariationUnit, type NumericControl, NumericControlSchema} from './NumericControl';

export type BubbleConfig = {
/** Whether the bubble spawner is active or not - setting the component as active will stop the animation loop */
active: boolean;
/** The components to use for the bubbles - each bubble will be a random component from this array */
components: React.ReactNode[];
/** The duration of the bubble animation */
durationMs: number;
/** The randomness in the duration of the bubble animation */
durationMsRandomness: number;
durationMs: NumericControl;
/** The ending scale of the bubble */
scaleEnd: number;
/** The randomness in the ending scale of the bubble */
scaleEndRandomness: number;
scaleEnd: NumericControl;
/** The frame rate of the animation */
frameRate: number;
/** The maximum time between spawning bubbles */
maxSpawnIntervalMs: number;
/** The minimum time between spawning bubbles */
minSpawnIntervalMs: number;
/** The callback function that is called when a bubble is removed */
onRemove: () => void;
/** The callback function that is called when a bubble is spawned */
onSpawn: () => void;
/** The ending opacity of the bubble */
opacityEnd: number;
opacityEnd: NumericControl;
/** The bezier curve definition which controls the opacity of the bubble over time */
opacityEasing: [number, number, number, number];
/** The starting opacity of the bubble */
opacityStart: number;
opacityStart: NumericControl;
/** The bezier curve definition which controls the scale of the bubble over time */
scaleEasing: [number, number, number, number];
/** The starting scale of the bubble */
scaleStart: number;
/** The randomness in the starting scale of the bubble */
scaleStartRandomness: number;
scaleStart: NumericControl;
/** The amplitude of the wobble animation */
wobbleAmplitude: number;
/** The randomness in the amplitude of the wobble animation */
wobbleAmplitudeRandomness: number;
wobbleAmplitude: NumericControl;
/** The frequency of the wobble animation */
wobbleFrequency: number;
/** The randomness in the frequency of the wobble animation */
wobbleFrequencyRandomness: number;
wobbleFrequency: NumericControl;
/** The randomness in the x position of the bubble */
xOffsetRandomness: number;
xStart: NumericControl;
/** The bezier curve definition which controls the y position of the bubble over time */
yEasing: [number, number, number, number];
/** The y position of the bubble when it reaches the end of its animation - nb: +ve `y` is up (which is the opposite of the html definition of positive y) */
yEnd: number;
/** The randomness in the y position of the bubble when it reaches the end of its animation */
yRandomness: number;
yEnd: NumericControl;
/** The starting y position of the bubble */
yStart: number;
yStart: NumericControl;
/** The interval between spawns */
spawnIntervalMs: NumericControl;
};

export const BubbleConfigSchema = v.object({
active: v.boolean(),
components: v.array(v.string()),
durationMs: v.pipe(v.number(), v.toMinValue(0)),
durationMsRandomness: v.pipe(v.number(), v.toMinValue(0)),
scaleEnd: v.pipe(v.number(), v.toMinValue(0)),
scaleEndRandomness: v.pipe(v.number(), v.toMinValue(0)),
durationMs: NumericControlSchema,
scaleEnd: NumericControlSchema,
frameRate: v.pipe(v.number(), v.toMinValue(0)),
maxSpawnIntervalMs: v.pipe(v.number(), v.toMinValue(0)),
minSpawnIntervalMs: v.pipe(v.number(), v.toMinValue(0)),
spawnIntervalMs: NumericControlSchema,
onRemove: v.function(),
onSpawn: v.function(),
opacityEasing: EasingSchema,
opacityEnd: v.pipe(v.number(), v.toMinValue(0), v.toMaxValue(1)),
opacityStart: v.pipe(v.number(), v.toMinValue(0), v.toMaxValue(1)),
opacityEnd: NumericControlSchema,
opacityStart: NumericControlSchema,
scaleEasing: EasingSchema,
scaleStart: v.pipe(v.number(), v.toMinValue(0)),
scaleStartRandomness: v.pipe(v.number(), v.toMinValue(0)),
wobbleAmplitude: v.pipe(v.number(), v.toMinValue(0)),
wobbleAmplitudeRandomness: v.pipe(v.number(), v.toMinValue(0)),
wobbleFrequency: v.pipe(v.number(), v.toMinValue(0)),
wobbleFrequencyRandomness: v.pipe(v.number(), v.toMinValue(0)),
xOffsetRandomness: v.pipe(v.number(), v.toMinValue(0)),
scaleStart: NumericControlSchema,
wobbleAmplitude: NumericControlSchema,
wobbleFrequency: NumericControlSchema,
yEasing: EasingSchema,
yEnd: v.number(),
yRandomness: v.pipe(v.number(), v.toMinValue(0)),
yStart: v.number(),
yEnd: NumericControlSchema,
yStart: NumericControlSchema,
xStart: NumericControlSchema,
});

export const defaultBubbleConfig: BubbleConfig = {
active: false,
components: ['💸', '🔥'],
durationMs: 1500,
durationMsRandomness: 1000,
scaleEnd: 2,
scaleEndRandomness: 0.2,
durationMs: {
value: 1000,
variation: {type: VariationType.Plus, unit: VariationUnit.Absolute, value: 1000},
},
scaleEnd: {
value: 2,
variation: {type: VariationType.PlusMinus, unit: VariationUnit.Percent, value: 20},
},
frameRate: 60,
maxSpawnIntervalMs: 1000,
minSpawnIntervalMs: 200,
spawnIntervalMs: {
value: 600,
variation: {type: VariationType.PlusMinus, unit: VariationUnit.Absolute, value: 400},
},
onRemove: () => {},
onSpawn: () => {},
opacityEasing: [0.25, -0.75, 0.8, 1.2],
opacityEnd: 0,
opacityStart: 1,
scaleEasing: [0.25, -0.75, 0.8, 1.2],
scaleStart: 1,
scaleStartRandomness: 0.5,
wobbleAmplitude: 1,
wobbleAmplitudeRandomness: 40,
wobbleFrequency: 0.1,
wobbleFrequencyRandomness: 0.5,
xOffsetRandomness: 100,
scaleStart: {
value: 1,
variation: {type: VariationType.PlusMinus, unit: VariationUnit.Percent, value: 50},
},
wobbleAmplitude: {
value: 1,
variation: {type: VariationType.Plus, unit: VariationUnit.Absolute, value: 40},
},
wobbleFrequency: {
value: 0.1,
variation: {type: VariationType.Plus, unit: VariationUnit.Absolute, value: 0.4},
},
yEasing: [0.25, 0, 0.8, 1.2],
yEnd: 100,
yRandomness: 200,
yEnd: {
value: 100,
variation: {type: VariationType.Plus, unit: VariationUnit.Absolute, value: 200},
},
yStart: 0,
xStart: {
value: 0,
variation: {type: VariationType.PlusMinus, unit: VariationUnit.Absolute, value: 100},
},
};

export const buildBubbleConfig = (bubbleConfigOverrides: Partial<BubbleConfig> = {}) => {
Expand Down
60 changes: 29 additions & 31 deletions src/lib/FidgetSpinner/Bubbles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {BubbleConfig} from './BubbleConfig';
import {buildBubbleConfig} from './BubbleConfig';
import {createId} from './createId';
import classes from './Bubbles.module.css';
import {toNumber} from './NumericControl';

/**
* `Bubbles` is a standalone particle spawner component.
Expand Down Expand Up @@ -39,31 +40,33 @@ import classes from './Bubbles.module.css';
*/
export const Bubbles = (config: Partial<BubbleConfig>) => {
const {
minSpawnIntervalMs,
maxSpawnIntervalMs,
spawnIntervalMs,
// minSpawnIntervalMs,
// maxSpawnIntervalMs,
components,
durationMs,
durationMsRandomness,
// durationMsRandomness,
opacityEasing,
opacityStart,
opacityEnd,
scaleStart,
scaleStartRandomness,
// scaleStartRandomness,
scaleEasing,
scaleEnd,
scaleEndRandomness,
// scaleEndRandomness,
wobbleFrequency,
wobbleFrequencyRandomness,
// wobbleFrequencyRandomness,
wobbleAmplitude,
wobbleAmplitudeRandomness,
xOffsetRandomness,
// wobbleAmplitudeRandomness,
// xOffsetRandomness,
onSpawn,
onRemove,
yEasing,
frameRate,
yStart,
yEnd,
yRandomness,
xStart,
// yRandomness,
active,
} = buildBubbleConfig(config);

Expand Down Expand Up @@ -92,7 +95,7 @@ export const Bubbles = (config: Partial<BubbleConfig>) => {
);

const lastSpawnTime = useRef(performance.now());
const spawnInterval = useRef(minSpawnIntervalMs);
const spawnInterval = useRef(toNumber(spawnIntervalMs));

const spawnLoop = useCallback(() => {
const time = performance.now();
Expand All @@ -101,11 +104,13 @@ export const Bubbles = (config: Partial<BubbleConfig>) => {
if (elapsed > spawnInterval.current) {
lastSpawnTime.current = time;

const newInterval = minSpawnIntervalMs + Math.random() * (maxSpawnIntervalMs - minSpawnIntervalMs);
const newInterval = toNumber(spawnIntervalMs);
spawnInterval.current = newInterval;

const amplitude = wobbleAmplitude + Math.random() * (wobbleAmplitudeRandomness - wobbleAmplitude);
const frequency = wobbleFrequency + Math.random() * (wobbleFrequencyRandomness - wobbleFrequency);
const amplitude = toNumber(wobbleAmplitude);
const frequency = toNumber(wobbleFrequency);

const wobbleDirection = Math.random() < 0.5 ? -1 : 1;

const xWobbleFunction = (timeMs: number) => {
const timeS = timeMs / 1000;
Expand All @@ -114,29 +119,29 @@ export const Bubbles = (config: Partial<BubbleConfig>) => {
Math.cos(timeS * Math.PI * 3.7 * frequency) * amplitude * 0.4 +
Math.sin(timeS * Math.PI * 5.3 * frequency) * amplitude * 0.2;

return wobbleX;
return wobbleDirection * wobbleX;
};

const duration = durationMs + Math.random() * durationMsRandomness;
const duration = toNumber(durationMs);

const yMax = -(yEnd + Math.random() * yRandomness);
const yMax = -toNumber(yEnd);

const id = createId();
const Component = components[Math.floor(Math.random() * components.length)];

const bubbleProps: BubbleProps = {
id,
durationMs: duration,
scaleStart: scaleStart + Math.random() * scaleStartRandomness,
scaleEnd: scaleEnd + Math.random() * scaleEndRandomness,
scaleStart: toNumber(scaleStart),
scaleEnd: toNumber(scaleEnd),
scaleEasing: toBezierEasing(scaleEasing),
opacityStart,
opacityEnd,
opacityStart: toNumber(opacityStart),
opacityEnd: toNumber(opacityEnd),
opacityEasing: toBezierEasing(opacityEasing),
yStart,
yStart: toNumber(yStart),
yEnd: yMax,
yEasing: toBezierEasing(yEasing),
xStart: Math.random() * xOffsetRandomness,
xStart: toNumber(xStart),
xWobbleFunction,
cleanup: () => {
removeBubble(id);
Expand All @@ -150,25 +155,16 @@ export const Bubbles = (config: Partial<BubbleConfig>) => {
addBubble(id, bubbleProps);
}
}, [
minSpawnIntervalMs,
maxSpawnIntervalMs,
wobbleAmplitude,
wobbleAmplitudeRandomness,
wobbleFrequency,
wobbleFrequencyRandomness,
xOffsetRandomness,
durationMs,
durationMsRandomness,
scaleStart,
scaleStartRandomness,
scaleEnd,
scaleEndRandomness,
scaleEasing,
opacityEasing,
opacityStart,
opacityEnd,
yEnd,
yRandomness,
yEasing,
yStart,
components,
Expand All @@ -177,6 +173,8 @@ export const Bubbles = (config: Partial<BubbleConfig>) => {
onRemove,
addBubble,
removeBubble,
xStart,
spawnIntervalMs,
]);

useAnimationFrame(spawnLoop, active);
Expand Down
31 changes: 31 additions & 0 deletions src/lib/FidgetSpinner/ClickConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as v from 'valibot';

import {type NumericControl, NumericControlSchema} from './NumericControl';

export const ClickConfigSchema = v.object({
angularVelocityPerClick: NumericControlSchema,
onSpawn: v.function(),
onRemove: v.function(),
active: v.boolean(),
});

export type ClickConfig = {
angularVelocityPerClick: NumericControl;
onSpawn: () => void;
onRemove: () => void;
active: boolean;
};

export const defaultClickConfig: ClickConfig = {
angularVelocityPerClick: Math.PI * 2,
onSpawn: () => {},
onRemove: () => {},
active: true,
};

export const buildClickConfig = (clickConfigOverrides: Partial<ClickConfig> = {}) => {
return v.parse(ClickConfigSchema, {
...defaultClickConfig,
...clickConfigOverrides,
});
};
Loading

0 comments on commit 4d0418a

Please sign in to comment.