@arwes/bgs
Version:
Futuristic Sci-Fi UI Web Framework
186 lines (185 loc) • 6.49 kB
JavaScript
import { easing, createAnimation } from '@arwes/animated';
const defaultProps = {
color: '#777',
quantity: 10,
padding: 50,
xOffset: [0, 0],
yOffset: [-10, -100],
radiusInitial: 4,
radiusOffset: [4, 40],
sets: 5
};
const minmaxOverflow01 = (value) => Math.min(1, Math.max(0, value === 1 ? 1 : value % 1));
const createBackgroundPuffs = (props) => {
const { canvas, animator } = props;
const ctx = canvas.getContext('2d');
if (!ctx) {
return { cancel: () => { } };
}
let resizeObserver;
let transitionControl;
let runningControl;
let unsubscribe;
let runningControlTimeoutId;
let puffsSets = [];
const getSettings = () => ({
...defaultProps,
...props.settingsRef.current
});
const createPuff = (width, height) => {
const { padding, xOffset, yOffset, radiusInitial, radiusOffset } = getSettings();
const x = padding + Math.random() * (width - padding * 2);
const y = padding + Math.random() * (height - padding * 2);
const r = radiusInitial;
const xo = xOffset[0] + Math.random() * xOffset[1];
const yo = yOffset[0] + Math.random() * yOffset[1];
const ro = radiusOffset[0] + Math.random() * radiusOffset[1];
return { x, y, r, xo, yo, ro };
};
const createPuffsSets = (width, height) => {
const { quantity, sets } = getSettings();
const puffsSetQuantity = Math.round(quantity / sets);
return Array(sets)
.fill(null)
.map(() => Array(puffsSetQuantity)
.fill(null)
.map(() => createPuff(width, height)));
};
const resize = () => {
const dpr = Math.min(window.devicePixelRatio || 2, 2);
const { width, height } = canvas.getBoundingClientRect();
if (canvas.width !== width * dpr || canvas.height !== height * dpr) {
canvas.width = width * dpr;
canvas.height = height * dpr;
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
puffsSets = createPuffsSets(width, height);
};
const drawPuffs = (puffs, progress) => {
const { color } = getSettings();
ctx.globalAlpha = progress <= 0.5 ? progress * 2 : -2 * progress + 2;
puffs.forEach((puff) => {
const x = puff.x + progress * puff.xo;
const y = puff.y + progress * puff.yo;
const r = puff.r + progress * puff.ro;
const grd = ctx.createRadialGradient(x, y, 0, x, y, r);
grd.addColorStop(0, color);
grd.addColorStop(1, 'transparent');
ctx.beginPath();
ctx.fillStyle = grd;
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
ctx.closePath();
});
};
const draw = (progress) => {
const { sets } = getSettings();
const { width, height } = canvas;
const puffsSetOffset = 1 / sets;
ctx.clearRect(0, 0, width, height);
puffsSets.forEach((puffs, index) => {
const puffsOffset = puffsSetOffset * index;
const puffsProgress = minmaxOverflow01(progress + puffsOffset);
drawPuffs(puffs, easing.outSine(puffsProgress));
});
};
const run = () => {
if (!animator) {
return;
}
const { duration: { interval = 2, intervalPause = 0 } } = animator.settings;
runningControl?.cancel();
runningControl = createAnimation({
duration: interval,
easing: 'linear',
onUpdate(progress) {
draw(progress);
},
onFinish() {
const emptyDuration = intervalPause * 1000;
window.clearTimeout(runningControlTimeoutId);
runningControlTimeoutId = window.setTimeout(run, emptyDuration);
}
});
};
const setup = () => {
if (typeof window !== 'undefined' && !resizeObserver) {
resizeObserver = new window.ResizeObserver(() => {
resize();
if (!animator) {
draw(1);
}
});
resizeObserver.observe(canvas);
resize();
}
};
const stop = () => {
resizeObserver?.disconnect();
resizeObserver = undefined;
transitionControl?.cancel();
transitionControl = undefined;
runningControl?.cancel();
runningControl = undefined;
window.clearTimeout(runningControlTimeoutId);
runningControlTimeoutId = undefined;
};
const start = () => {
if (!animator) {
setup();
draw(1);
canvas.style.opacity = '1';
return;
}
unsubscribe = animator.subscribe((node) => {
switch (node.state) {
case 'entering': {
setup();
if (runningControl === undefined) {
run();
}
transitionControl?.cancel();
transitionControl = createAnimation({
duration: node.settings.duration.enter,
onUpdate(progress) {
canvas.style.opacity = String(progress);
}
});
break;
}
case 'entered': {
setup();
if (runningControl === undefined) {
run();
}
canvas.style.opacity = '1';
break;
}
case 'exiting': {
transitionControl?.cancel();
transitionControl = createAnimation({
duration: node.settings.duration.exit,
onUpdate(progress) {
canvas.style.opacity = String(1 - progress);
}
});
break;
}
case 'exited': {
stop();
canvas.style.opacity = '0';
break;
}
}
});
};
const cancel = () => {
unsubscribe?.();
stop();
canvas.style.opacity = '0';
};
start();
return Object.freeze({ cancel });
};
export { createBackgroundPuffs };