dots-animation
Version:
simple module for filling your html container with fancy responsive dots animation
451 lines (447 loc) • 17.3 kB
JavaScript
const defaultOptions = {
expectedFps: 60,
number: null,
density: 0.00005,
dprDependentDensity: true,
dprDependentDimensions: true,
minR: 1,
maxR: 6,
minSpeedX: -0.5,
maxSpeedX: 0.5,
minSpeedY: -0.5,
maxSpeedY: 0.5,
blur: 1,
fill: true,
colorsFill: ["#ffffff", "#fff4c1", "#faefdb"],
opacityFill: null,
opacityFillMin: 0,
opacityFillStep: 0,
stroke: false,
colorsStroke: ["#ffffff"],
opacityStroke: 1,
opacityStrokeMin: 0,
opacityStrokeStep: 0,
drawLines: true,
lineColor: "#717892",
lineLength: 150,
lineWidth: 2,
actionOnClick: true,
actionOnHover: true,
onClickCreate: false,
onClickMove: true,
onHoverMove: true,
onHoverDrawLines: true,
onClickCreateNDots: 10,
onClickMoveRadius: 200,
onHoverMoveRadius: 50,
onHoverLineRadius: 150
};
function getDistance(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
function getRandomInt(min, max) {
return Math.round(Math.random() * (max - min)) + min;
}
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
function hexToRgba(hex, opacity, denominator = 1) {
hex = hex.replace("#", "");
const r = parseInt(hex.substring(0, hex.length / 3), 16);
const g = parseInt(hex.substring(hex.length / 3, 2 * hex.length / 3), 16);
const b = parseInt(hex.substring(2 * hex.length / 3, 3 * hex.length / 3), 16);
return "rgba(" + r + "," + g + "," + b + "," + opacity / denominator + ")";
}
function drawCircle(ctx, x, y, r, colorS, colorF) {
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, true);
if (colorF !== null) {
ctx.fillStyle = colorF;
ctx.fill();
}
if (colorS !== null) {
ctx.strokeStyle = colorS;
ctx.stroke();
}
}
function drawLine(ctx, x1, y1, x2, y2, width, color) {
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
class Dot {
constructor(_canvas, _offset, _x, _y, _xSpeed, _ySpeed, _r, _colorSHex, _colorFHex, _opacitySMin, _opacitySMax, _opacitySStep, _opacityFMin, _opacityFMax, _opacityFStep) {
this._canvas = _canvas;
this._offset = _offset;
this._x = _x;
this._y = _y;
this._xSpeed = _xSpeed;
this._ySpeed = _ySpeed;
this._r = _r;
this._colorSHex = _colorSHex;
this._colorFHex = _colorFHex;
this._opacitySMin = _opacitySMin;
this._opacitySMax = _opacitySMax;
this._opacitySStep = _opacitySStep;
this._opacityFMin = _opacityFMin;
this._opacityFMax = _opacityFMax;
this._opacityFStep = _opacityFStep;
this._opacitySCurrent = _opacitySMax;
this._opacityFCurrent = _opacityFMax;
this._colorS = _colorSHex === null ?
null : hexToRgba(_colorSHex, this._opacitySCurrent, 100);
this._colorF = _colorFHex === null ?
null : hexToRgba(_colorFHex, this._opacityFCurrent, 100);
}
getProps() {
return {
x: this._x,
y: this._y,
r: this._r,
xSpeed: this._xSpeed,
ySpeed: this._ySpeed,
colorS: this._colorS,
colorF: this._colorF
};
}
updatePosition() {
const offset = Math.max(this._offset, this._r);
const xMin = -1 * offset;
const yMin = -1 * offset;
const xMax = this._canvas.width + offset;
const yMax = this._canvas.height + offset;
if (this._x < xMin) {
this._x = xMax;
}
else if (this._x > xMax) {
this._x = xMin;
}
else {
this._x += this._xSpeed;
}
if (this._y < yMin) {
this._y = yMax;
}
else if (this._y > yMax) {
this._y = yMin;
}
else {
this._y += this._ySpeed;
}
}
updateColor() {
if (this._opacitySStep !== 0 && this._colorSHex !== null) {
this._opacitySCurrent += this._opacitySStep;
if (this._opacitySCurrent > this._opacitySMax) {
this._opacitySCurrent = this._opacitySMax;
this._opacitySStep *= -1;
}
else if (this._opacitySCurrent < this._opacitySMin) {
this._opacitySCurrent = this._opacitySMin;
this._opacitySStep *= -1;
}
this._colorS = hexToRgba(this._colorSHex, this._opacityFCurrent, 100);
}
if (this._opacityFStep !== 0 && this._colorFHex !== null) {
this._opacityFCurrent += this._opacityFStep;
if (this._opacityFCurrent > this._opacityFMax) {
this._opacityFCurrent = this._opacityFMax;
this._opacityFStep *= -1;
}
else if (this._opacityFCurrent < this._opacityFMin) {
this._opacityFCurrent = this._opacityFMin;
this._opacityFStep *= -1;
}
this._colorF = hexToRgba(this._colorFHex, this._opacityFCurrent, 100);
}
}
moveTo(position) {
this._x = position.x;
this._y = position.y;
}
}
class DotControl {
constructor(canvas, options) {
this._pauseState = false;
this._array = [];
this._maxNumber = 100;
this._lastDpr = 0;
this._canvas = canvas;
const canvasCtx = this._canvas.getContext("2d");
if (canvasCtx === null) {
throw new Error("Canvas context is null");
}
this._canvasCtx = canvasCtx;
this._options = options;
}
setPauseState(pauseState) {
this._pauseState = pauseState;
}
draw(mousePosition, isClicked) {
const dpr = window.devicePixelRatio;
if (dpr !== this._lastDpr) {
this._array.length = 0;
}
this._lastDpr = dpr;
const isNumberUpdated = this.updateDotNumber();
if (!isNumberUpdated && this._pauseState && !this.isCanvasEmpty()) {
return;
}
this._canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height);
for (const dot of this._array) {
dot.updatePosition();
dot.updateColor();
}
const ratio = this._options.dprDependentDimensions ? dpr : 1;
if (this._options.actionOnHover) {
if (this._options.onHoverDrawLines) {
this.drawLinesToCircleCenter(mousePosition, this._options.onHoverLineRadius * ratio, this._options.lineWidth, this._options.lineColor);
}
if (this._options.onHoverMove) {
this.moveDotsOutOfCircle(mousePosition, this._options.onHoverMoveRadius * ratio);
}
}
if (isClicked && this._options.actionOnClick) {
if (this._options.onClickMove) {
this.moveDotsOutOfCircle(mousePosition, this._options.onClickMoveRadius * ratio);
}
if (this._options.onClickCreate) {
this.dotFactory(this._options.onClickCreateNDots, mousePosition);
}
}
if (this._options.drawLines) {
this.drawLinesBetweenDots();
}
for (const dot of this._array) {
const params = dot.getProps();
drawCircle(this._canvasCtx, params.x, params.y, params.r, params.colorS, params.colorF);
}
}
isCanvasEmpty() {
return !this._canvasCtx
.getImageData(0, 0, this._canvas.width, this._canvas.height)
.data.some(channel => channel !== 0);
}
dotFactory(number, position = null) {
for (let i = 0; i < number; i++) {
const dot = this.createRandomDot(position);
this._array.push(dot);
}
if (this._array.length > this._maxNumber) {
this.deleteEldestDots(this._array.length - this._maxNumber);
}
}
createRandomDot(position) {
let x, y;
if (position) {
x = position.x;
y = position.y;
}
else {
x = getRandomInt(0, this._canvas.width);
y = getRandomInt(0, this._canvas.height);
}
const dimRatio = this._options.dprDependentDimensions ? window.devicePixelRatio : 1;
const offset = this._options.drawLines ? this._options.lineLength * dimRatio : 0;
const xSpeed = getRandomArbitrary(this._options.minSpeedX, this._options.maxSpeedX) * dimRatio;
const ySpeed = getRandomArbitrary(this._options.minSpeedY, this._options.maxSpeedY) * dimRatio;
const radius = getRandomInt(this._options.minR, this._options.maxR) * dimRatio;
let colorS = null;
let colorF = null;
if (this._options.stroke) {
colorS = this._options.colorsStroke[Math.floor(Math.random() *
this._options.colorsStroke.length)];
}
if (this._options.fill) {
colorF = this._options.colorsFill[Math.floor(Math.random() *
this._options.colorsFill.length)];
}
const opacitySMin = this._options.opacityStrokeMin;
const opacitySMax = this._options.opacityStroke ?
Math.max(opacitySMin, this._options.opacityStroke) :
getRandomInt(opacitySMin, 100);
const opacitySStep = this._options.opacityStrokeStep;
const opacityFMin = this._options.opacityFillMin;
const opacityFMax = this._options.opacityFill ?
Math.max(opacityFMin, this._options.opacityFill) :
getRandomInt(opacityFMin, 100);
const opacityFStep = this._options.opacityFillStep;
return new Dot(this._canvas, offset, x, y, xSpeed, ySpeed, radius, colorS, colorF, opacitySMin, opacitySMax, opacitySStep, opacityFMin, opacityFMax, opacityFStep);
}
deleteEldestDots(number) {
this._array = this._array.slice(number);
for (let i = 0; i < number; i++) {
this._array.shift();
}
}
getDotNumber() {
const densityRatio = this._options.dprDependentDensity ? window.devicePixelRatio : 1;
const calculatedNumber = Math.floor(this._canvas.width * this._canvas.height * this._options.density / densityRatio);
return this._options.number ? this._options.number : calculatedNumber;
}
updateDotNumber() {
this._maxNumber = this.getDotNumber();
if (this._maxNumber < this._array.length) {
this.deleteEldestDots(this._array.length - this._maxNumber);
return true;
}
else if (this._maxNumber > this._array.length) {
this.dotFactory(this._maxNumber - this._array.length);
return true;
}
else {
return false;
}
}
getCloseDotPairs(maxDistance) {
const dotArray = this._array;
const closePairs = [];
for (let i = 0; i < dotArray.length; i++) {
for (let j = i; j < dotArray.length; j++) {
const dotIParams = dotArray[i].getProps();
const dotJParams = dotArray[j].getProps();
const distance = Math.floor(getDistance(dotIParams.x, dotIParams.y, dotJParams.x, dotJParams.y));
if (distance <= maxDistance) {
closePairs.push([dotIParams.x, dotIParams.y, dotJParams.x, dotJParams.y, distance]);
}
}
}
return closePairs;
}
getDotsInsideCircle(position, radius) {
const dotsInCircle = [];
for (const dot of this._array) {
const dotParams = dot.getProps();
const distance = getDistance(position.x, position.y, dotParams.x, dotParams.y);
if (distance < radius) {
dotsInCircle.push([dot, distance]);
}
}
return dotsInCircle;
}
moveDotsOutOfCircle(position, radius) {
const dotsInCircle = this.getDotsInsideCircle(position, radius);
for (const item of dotsInCircle) {
const dot = item[0];
const dotParams = dot.getProps();
const distance = item[1];
const x = (dotParams.x - position.x) * (radius / distance) + position.x;
const y = (dotParams.y - position.y) * (radius / distance) + position.y;
dot.moveTo({ x: x, y: y });
}
}
drawLinesBetweenDots() {
const ratio = this._options.dprDependentDimensions ? window.devicePixelRatio : 1;
const lineLength = this._options.lineLength * ratio;
const pairs = this.getCloseDotPairs(lineLength);
const width = this._options.lineWidth;
for (const pair of pairs) {
const opacity = (1 - pair[4] / lineLength) / 2;
const color = hexToRgba(this._options.lineColor, opacity);
drawLine(this._canvasCtx, pair[0], pair[1], pair[2], pair[3], width, color);
}
}
drawLinesToCircleCenter(position, radius, lineWidth, lineColor) {
const dotsInCircle = this.getDotsInsideCircle(position, radius);
for (const item of dotsInCircle) {
const dot = item[0];
const dotParams = dot.getProps();
const opacity = (1 - item[1] / radius);
const color = hexToRgba(lineColor, opacity);
drawLine(this._canvasCtx, position.x, position.y, dotParams.x, dotParams.y, lineWidth, color);
}
}
}
class DotsAnimation {
constructor(parent, canvasId, options, constructor) {
this._timer = undefined;
this._isMouseClicked = false;
this._parent = parent;
this._mousePosition = {
x: 0,
y: 0,
};
this._fps = options.expectedFps;
this._canvas = document.createElement("canvas");
this._canvas.id = canvasId;
this._canvas.style.display = "block";
this._canvas.style.width = "100%";
this._canvas.style.width = "100%";
this._canvas.style.height = "100%";
this._canvas.style.filter = `blur(${options.blur}px)`;
this.resize();
parent.appendChild(this._canvas);
window.addEventListener("resize", () => { this.resize(); });
this._animationControl = DotsAnimation
.animationControlFactory(constructor, this._canvas, options);
}
static animationControlFactory(constructor, canvas, options) {
return new constructor(canvas, options);
}
resize() {
const dpr = window.devicePixelRatio;
this._canvas.width = this._parent.offsetWidth * dpr;
this._canvas.height = this._parent.offsetHeight * dpr;
}
draw() {
this._animationControl.draw(this._mousePosition, this._isMouseClicked);
this._isMouseClicked = false;
}
start() {
this._animationControl.setPauseState(false);
if (this._timer !== undefined) {
return;
}
this._timer = window.setInterval(() => {
window.requestAnimationFrame(() => { this.draw(); }) ||
window.webkitRequestAnimationFrame(() => { this.draw(); });
}, 1000 / this._fps);
window.addEventListener("mousemove", this.onMouseMove.bind(this));
window.addEventListener("click", this.onClick.bind(this));
}
pause() {
this._animationControl.setPauseState(true);
}
stop() {
clearInterval(this._timer);
this._timer = undefined;
const canvasCtx = this._canvas.getContext("2d");
window.setTimeout(() => {
if (canvasCtx !== null) {
canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
}, 20);
}
onClick() {
this._isMouseClicked = true;
}
onMouseMove(e) {
const dpr = window.devicePixelRatio;
const parentRect = this._parent.getBoundingClientRect();
const xRelToDoc = parentRect.left +
document.documentElement.scrollLeft;
const yRelToDoc = parentRect.top +
document.documentElement.scrollTop;
const xDpr = (e.clientX - xRelToDoc + window.pageXOffset) * dpr;
const yDpr = (e.clientY - yRelToDoc + window.pageYOffset) * dpr;
this._mousePosition.x = xDpr;
this._mousePosition.y = yDpr;
}
}
class DotsAnimationFactory {
static createAnimation(containerSelector, canvasId, options = null) {
const finalOptions = Object.assign({}, defaultOptions);
if (options) {
Object.assign(finalOptions, options);
}
const container = document.querySelector(containerSelector);
if (container === null) {
throw new Error("Container is null");
}
return new DotsAnimation(container, canvasId, finalOptions, DotControl);
}
}
export { DotsAnimationFactory };