Skip to content

Instantly share code, notes, and snippets.

@lukesmurray
Created January 8, 2022 16:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lukesmurray/bd25218ed554361c58ccbce2eb49e0cc to your computer and use it in GitHub Desktop.
Save lukesmurray/bd25218ed554361c58ccbce2eb49e0cc to your computer and use it in GitHub Desktop.
background website animation
import paper from "paper";
import RBush from "rbush";
import knn from "rbush-knn";
// settings
// limit at which points are connected
// limit at which things are viewed by the mouse
/**
* radius in which dots are considered nearby
*/
const nearbyDotRadius = 0.1;
/**
* maximum radius at which lines can be connected
*/
const nearbyNearbyDotRadius = Math.pow(nearbyDotRadius, 2);
/**
* dot opacity fall of due to distance radius
*/
const dotOpacityFallOffRadius = 0.12;
/**
* maximum opacity for a dot
*/
const maxDotOpacity = 0.6;
const numberOfDots = 1000;
/**
* maximum radius of a dot
*/
const dotMaxRadius = 2;
/**
* maximum velocity of a dot
*/
const dotMaxVelocity = 0.05;
/**
* width of line strokesj
*/
const lineWidth = 0.5;
/**
* line opacity fall of due to distance radius
*/
const lineOpacityFallOffRadius = nearbyDotRadius;
const maxLines = 300;
/**
* minimum opacity where lines are removed
*/
const minLineOpacity = 0.0;
const LINE_COLOR = "#f563ff";
const DOT_COLOR = "#d4d4d4";
/**
* Shuffles array in place. ES6 version
* @param {Array} a items An array containing the items.
*/
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// map of lines
const tree = new RBush();
const lineMap = new Map();
const usedLines = new Map();
let mousePosition;
let mouseDelta;
let _lastId = 0;
const nextId = () => {
return _lastId++;
};
class Dot {
constructor(paper) {
this.paper = paper;
this.id = nextId();
// center is in 0-1 space
this.center = paper.Point.random();
this.radius = Math.random() * dotMaxRadius;
// velocity
this.vx = Math.random() * dotMaxVelocity * (Math.random() < 0.5 ? 1 : -1);
this.vy = Math.random() * dotMaxVelocity * (Math.random() < 0.5 ? 1 : -1);
// the path to draw
this.path = paper.Path.Circle(this.center, this.radius);
// style
this.path.fillColor = DOT_COLOR;
this.opacity = 0;
this.opacityOffset = Math.min(Math.random() + 0.25, 0);
this.path.visible = false;
this.treeItem = {
minX: this.center.x,
minY: this.center.y,
maxX: this.center.x,
maxY: this.center.y,
item: this
};
tree.insert(this.treeItem);
this.move = this.move.bind(this);
this.onFrame = this.onFrame.bind(this);
this.style = this.style.bind(this);
this.updateTreeItem = this.updateTreeItem.bind(this);
}
move(delta) {
// move by velocity
if (this.center.x < 0 || this.center.x > 1) {
this.vx *= -1.0;
}
if (this.center.y < 0 || this.center.y > 1) {
this.vy *= -1.0;
}
this.center = this.center.add(
new this.paper.Point(this.vx, this.vy).multiply(delta)
);
this.updateTreeItem();
}
updateTreeItem() {
tree.remove(this.treeItem);
this.treeItem.minX = this.center.x;
this.treeItem.minY = this.center.y;
this.treeItem.maxX = this.center.x;
this.treeItem.maxY = this.center.y;
tree.insert(this.treeItem);
}
style() {
// set opacity to 1 in mouse radius
this.opacity = Math.max(
maxDotOpacity - this.center.getDistance(mousePosition) / dotOpacityFallOffRadius,
this.opacity
);
// decrease opacity over time
this.opacity = Math.max(0, this.opacity - 0.01 - 0.001 * mouseDelta.length);
// this.opacity = Math.max(0, this.opacity - 0.01 - 0.0001 * mouseDelta.length);
this.path.opacity = this.opacity;
this.path.visible = this.opacity !== 0;
}
onFrame(event) {
this.move(event.delta);
this.path.position = this.center.multiply(
this.paper.view.bounds.bottomRight
);
this.style();
}
}
class Line {
static lineId(dotFrom, dotTo) {
return dotFrom.id + "-" + dotTo.id;
}
static createOrGetLine(paper, dotFrom, dotTo) {
if (usedLines.size < maxLines) {
const lineID = Line.lineId(dotFrom, dotTo);
const existingLine = lineMap.get(lineID);
if (existingLine !== undefined) {
return existingLine;
}
const newLine = new Line(paper, dotFrom, dotTo, () => {
lineMap.delete(lineID);
usedLines.delete(lineID);
});
lineMap.set(lineID, newLine);
usedLines.set(lineID, newLine);
return newLine;
}
}
constructor(paper, dotFrom, dotTo, handleRemove) {
this.paper = paper;
this.dotFrom = dotFrom;
this.dotTo = dotTo;
this.id = Line.lineId(dotFrom, dotTo);
this.path = this.paper.Path.Line(this.dotFrom, this.dotTo);
this.path.strokeColor = LINE_COLOR;
this.path.strokeWidth = lineWidth;
this.handleRemove = handleRemove;
this.move = this.move.bind(this);
this.onFrame = this.onFrame.bind(this);
this.style = this.style.bind(this);
this.opacity = 0;
this.delta = mouseDelta;
}
move(delta) {
this.path.segments[0].point = this.dotFrom.center.multiply(
this.paper.view.bounds.bottomRight
);
this.path.segments[1].point = this.dotTo.center.multiply(
this.paper.view.bounds.bottomRight
);
}
style() {
const dotFromDistFromMouse = this.dotFrom.center.getDistance(mousePosition);
const dotToDistFromMouse = this.dotTo.center.getDistance(mousePosition);
const lineLength = this.dotFrom.center.getDistance(this.dotTo.center);
const maxDistanceFromMouse = Math.max(
dotToDistFromMouse,
dotFromDistFromMouse
);
this.opacity = Math.max(
Math.min((this.delta.length * 3) / 4, 4 / 5) -
maxDistanceFromMouse / lineOpacityFallOffRadius,
this.opacity
);
this.opacity -= 0.005 * lineLength + 0.001 * this.delta.length;
this.opacity = Math.max(0, this.opacity);
if (this.opacity <= minLineOpacity) {
this.remove();
}
this.path.opacity = this.opacity;
}
onFrame(event) {
this.move(event.delta);
this.style();
}
remove() {
this.handleRemove();
this.path.remove();
}
}
// Only executed our code once the DOM is ready.
export default function() {
const c = getCanvasId();
paper.setup(c);
const tool = new paper.Tool();
const dots = Array(numberOfDots)
.fill(0)
.map(() => new Dot(paper));
// set the initial mouse position
resetMouseState();
// on move update the mouse position
tool.onMouseMove = function(event) {
mousePosition = event.point.divide(paper.view.bounds.bottomRight);
mousePosition = mousePosition.add(
new paper.Point({x: 0, y: -40 / paper.view.bounds.bottomRight.y})
);
mouseDelta = event.delta;
};
// on leave set the mouse position back to center
paper.view.onMouseLeave = function(event) {
// timeout to avoid onMove overwriting
setTimeout(() => {
resetMouseState();
}, 50);
};
paper.view.onFrame = function(event) {
// paper js kills long running frames this avoids large deltas
if (event.delta > 0.25) {
return;
}
const nearbyDots = (
knn(
tree,
mousePosition.x,
mousePosition.y,
Infinity,
() => true,
nearbyDotRadius
).map((d) => d.item)
).reverse();
nearbyDots.forEach((dotFrom) => {
shuffle(
knn(
tree,
dotFrom.center.x,
dotFrom.center.y,
Infinity,
() => true,
nearbyNearbyDotRadius
)
.map((d) => d.item)
.filter((d) => d.id > dotFrom.id)
)
.slice(0, 1)
.forEach((dotTo) => {
Line.createOrGetLine(paper, dotFrom, dotTo);
});
});
dots.forEach((d) => d.onFrame(event));
lineMap.forEach((v) => v.onFrame(event));
};
}
function resetMouseState() {
mousePosition = defaultMousePosition();
mouseDelta = defaultMouseDelta();
}
function defaultMouseDelta() {
return new paper.Point(1, 0);
}
function defaultMousePosition() {
return paper.view.bounds.bottomRight
.subtract(paper.view.bounds.bottomRight.multiply(0.1))
.divide(paper.view.bounds.bottomRight);
}
/**
* Returns the id of the canvas
* @returns {string}
*/
function getCanvasId() {
return "animationCanvas";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment