455 lines
10 KiB
JavaScript
455 lines
10 KiB
JavaScript
import { spline } from "https://cdn.skypack.dev/@georgedoescode/spline@1.0.1";
|
|
import { Vector2D } from "https://cdn.skypack.dev/@georgedoescode/vector2d@1.0.1";
|
|
import SimplexNoise from "https://cdn.skypack.dev/simplex-noise@2.4.0";
|
|
import centroid from "https://cdn.skypack.dev/polygon-centroid@1.1.0";
|
|
import polylabel from "https://cdn.skypack.dev/polylabel@1.1.0";
|
|
import * as PIXI from "https://cdn.skypack.dev/pixi.js@5.3.4";
|
|
import Offset from "https://cdn.skypack.dev/polygon-offset@0.3.1";
|
|
import gsap from "https://cdn.skypack.dev/gsap@3.5.1";
|
|
import { PixiPlugin } from "https://cdn.skypack.dev/gsap@3.5.1/PixiPlugin";
|
|
|
|
const simplex = new SimplexNoise();
|
|
const offset = new Offset();
|
|
|
|
gsap.registerPlugin(PixiPlugin);
|
|
PixiPlugin.registerPIXI(PIXI);
|
|
|
|
function random(min, max) {
|
|
return Math.random() * (max - min) + min;
|
|
}
|
|
|
|
function map(n, start1, end1, start2, end2) {
|
|
return (n - start1) / (end1 - start1) * (end2 - start2) + start2;
|
|
}
|
|
|
|
function noise(x, y) {
|
|
return simplex.noise2D(x, y);
|
|
}
|
|
|
|
function getPolygonCentroid(pts) {
|
|
const first = pts[0];
|
|
const last = pts[pts.length - 1];
|
|
|
|
if (first.x != last.x || first.y != last.y) pts.push(first);
|
|
|
|
let twicearea = 0;
|
|
let x = 0;
|
|
let y = 0;
|
|
let nPts = pts.length;
|
|
let p1;
|
|
let p2;
|
|
let f;
|
|
|
|
for (var i = 0, j = nPts - 1; i < nPts; j = i++) {
|
|
p1 = pts[i];
|
|
p2 = pts[j];
|
|
f = p1.x * p2.y - p2.x * p1.y;
|
|
twicearea += f;
|
|
x += (p1.x + p2.x) * f;
|
|
y += (p1.y + p2.y) * f;
|
|
}
|
|
|
|
f = twicearea * 3;
|
|
|
|
return { x: x / f, y: y / f };
|
|
}
|
|
|
|
class MetaSpline {
|
|
constructor(x, y, shape, app) {
|
|
this.app = app;
|
|
|
|
this.pos = new Vector2D(x, y);
|
|
|
|
this.shape = shape;
|
|
this.centroid = new Vector2D(shape.centroid.x, shape.centroid.y);
|
|
|
|
this.points = shape.points;
|
|
|
|
this.graphicsBounds = null;
|
|
|
|
this.originalPoints = this.points.map(p => p.copy());
|
|
|
|
this.pointNoise = [...Array(this.points.length)].map((_, i) => {
|
|
return { x: random(0, 1000), y: random(0, 1000) };
|
|
});
|
|
|
|
this.xOff = random(0, 1000);
|
|
this.baseXOffVertex = random(0.001, 0.005);
|
|
this.xOffVertex = this.baseXOffVertex;
|
|
|
|
this.tension = random(0.4, 0.7);
|
|
|
|
this.container = new PIXI.Container();
|
|
this.graphics = this._createGraphics();
|
|
|
|
this.app.stage.addChild(this.container);
|
|
this.container.addChild(this.graphics);
|
|
|
|
this.container.x = shape.site.x;
|
|
this.container.y = shape.site.y;
|
|
|
|
this.colors = [
|
|
0xd6675a,
|
|
0xe09960,
|
|
0x6092c4,
|
|
0x3c3c3b,
|
|
0x64b473,
|
|
0xd4676b,
|
|
0x96c25b];
|
|
|
|
this.fill = this.colors[~~random(0, this.colors.length)];
|
|
|
|
this._animateIn();
|
|
|
|
// ffffff
|
|
|
|
this._createFill();
|
|
|
|
this.outline = new PIXI.Graphics();
|
|
this.container.addChild(this.outline);
|
|
}
|
|
|
|
_createFill() {
|
|
const graphics1 = new PIXI.Graphics();
|
|
const graphics2 = new PIXI.Graphics();
|
|
|
|
const container1 = new PIXI.Container();
|
|
const container2 = new PIXI.Container();
|
|
|
|
const choices = Object.keys(textures);
|
|
const choice = choices[~~random(0, choices.length)];
|
|
|
|
const mask = new PIXI.Sprite(textures[choice]);
|
|
|
|
mask.x = this.centroid.x - this.shape.bounds.width / 2;
|
|
mask.y = this.centroid.y - this.shape.bounds.height / 2;
|
|
|
|
mask.width = Math.max(this.shape.bounds.width, this.shape.bounds.height);
|
|
mask.height = Math.max(this.shape.bounds.width, this.shape.bounds.height);
|
|
|
|
this.container.addChild(mask);
|
|
|
|
graphics1.x = this.centroid.x;
|
|
graphics1.y = this.centroid.y;
|
|
|
|
const padding = 100;
|
|
|
|
graphics1.beginFill(this.fill);
|
|
graphics1.drawRect(
|
|
-this.shape.bounds.width / 2 - padding,
|
|
-this.shape.bounds.height / 2 - padding,
|
|
this.shape.bounds.width + padding * 2,
|
|
this.shape.bounds.height + padding * 2);
|
|
|
|
graphics1.endFill();
|
|
|
|
graphics2.alpha = 0.25;
|
|
|
|
graphics2.x = this.centroid.x;
|
|
graphics2.y = this.centroid.y;
|
|
|
|
graphics2.beginFill(this.fill);
|
|
graphics2.drawRect(
|
|
-this.shape.bounds.width / 2 - padding,
|
|
-this.shape.bounds.height / 2 - padding,
|
|
this.shape.bounds.width + padding * 2,
|
|
this.shape.bounds.height + padding * 2);
|
|
|
|
graphics2.endFill();
|
|
|
|
container2.addChild(graphics2);
|
|
container1.addChild(graphics1);
|
|
|
|
container2.addChild(container1);
|
|
|
|
this.container.addChild(container2);
|
|
|
|
container1.mask = mask;
|
|
container2.mask = this.graphics;
|
|
}
|
|
|
|
_createContainer() {
|
|
const container = new PIXI.Container();
|
|
|
|
this.app.stage.addChild(container);
|
|
|
|
return container;
|
|
}
|
|
|
|
_createGraphics() {
|
|
const graphics = new PIXI.Graphics();
|
|
|
|
graphics.interactive = true;
|
|
|
|
const handleOver = () => {
|
|
gsap.to(this.container.scale, {
|
|
x: 0.825,
|
|
y: 0.825,
|
|
duration: 1.5,
|
|
ease: "elastic.out(1, 0.5)" });
|
|
|
|
|
|
gsap.to(this, {
|
|
xOffVertex: 0.015,
|
|
tension: 0.9,
|
|
duration: 1.5,
|
|
ease: "elastic.out(1, 0.5)" });
|
|
|
|
};
|
|
|
|
const handleOut = () => {
|
|
gsap.to(this.container.scale, {
|
|
x: 1,
|
|
y: 1,
|
|
duration: 1.5,
|
|
ease: "elastic.out(1, 0.3)" });
|
|
|
|
|
|
gsap.to(this, {
|
|
xOffVertex: this.baseXOffVertex,
|
|
tension: 0.6,
|
|
duration: 1.5,
|
|
ease: "elastic.out(1, 0.3)" });
|
|
|
|
};
|
|
|
|
graphics.on("touchstart", handleOver);
|
|
graphics.on("mouseover", handleOver);
|
|
|
|
graphics.on("touchend", handleOut);
|
|
graphics.on("mouseout", handleOut);
|
|
|
|
graphics.cursor = "pointer";
|
|
|
|
return graphics;
|
|
}
|
|
|
|
_setGraphicsStyles() {
|
|
this.graphics.clear();
|
|
this.outline.clear();
|
|
|
|
this.outline.lineStyle(2, this.fill);
|
|
this.graphics.beginFill(0x000000);
|
|
}
|
|
|
|
_closeGraphicsStyles() {
|
|
this.graphics.endFill();
|
|
}
|
|
|
|
_wobble() {
|
|
for (let i = 0; i < this.points.length; i++) {
|
|
const originalPoint = this.originalPoints[i].copy();
|
|
const currentPoint = this.points[i];
|
|
|
|
const noiseX = noise(this.pointNoise[i].x, this.pointNoise[i].x);
|
|
|
|
const newVal = originalPoint.lerp(
|
|
this.centroid.x,
|
|
this.centroid.y,
|
|
map(noiseX, -1, 1, 0.1, 0.3));
|
|
|
|
|
|
this.points[i] = newVal;
|
|
|
|
this.pointNoise[i].x += this.xOffVertex;
|
|
}
|
|
}
|
|
|
|
_animateIn() {
|
|
this.container.scale.x = 0;
|
|
this.container.scale.y = 0;
|
|
|
|
gsap.
|
|
to(this.container.scale, {
|
|
x: 1,
|
|
y: 1,
|
|
duration: 1,
|
|
ease: "elastic.out(1, 0.825)" }).
|
|
|
|
delay(random(0, 0.75));
|
|
}
|
|
|
|
_animateOut() {
|
|
gsap.
|
|
to(this.container.scale, {
|
|
x: 0,
|
|
y: 0,
|
|
duration: 1,
|
|
ease: "elastic.out(1, 0.825)" }).
|
|
|
|
delay(random(0, 0.75));
|
|
}
|
|
|
|
update() {
|
|
this._wobble();
|
|
}
|
|
|
|
render() {
|
|
this._setGraphicsStyles();
|
|
|
|
spline(this.points, this.tension, true, (CMD, data) => {
|
|
if (CMD === "MOVE") {
|
|
this.graphics.moveTo(...data);
|
|
this.outline.moveTo(...data);
|
|
} else {
|
|
this.graphics.bezierCurveTo(...data);
|
|
this.outline.bezierCurveTo(...data);
|
|
}
|
|
});
|
|
|
|
this._closeGraphicsStyles();
|
|
}
|
|
|
|
clear() {
|
|
this._animateOut();
|
|
|
|
setTimeout(() => {
|
|
this.graphics.clear();
|
|
this.outline.clear();
|
|
}, 2000);
|
|
}}
|
|
|
|
|
|
class VoronoiDiagram {
|
|
constructor(width, height) {
|
|
this.bbox = {
|
|
xl: 0,
|
|
xr: width,
|
|
yt: 0,
|
|
yb: height };
|
|
|
|
|
|
const numPoints = ~~random(10, 25);
|
|
|
|
this.sites = [...Array(numPoints)].map(() => {
|
|
return {
|
|
x: random(0, width),
|
|
y: random(0, height) };
|
|
|
|
});
|
|
|
|
this.instance = new Voronoi();
|
|
|
|
this.diagram = this.instance.compute(this.sites, this.bbox);
|
|
}
|
|
|
|
_filterClosePoints(points) {
|
|
let lastPoint = null;
|
|
|
|
if (points[0].dist(points[points.length - 1]) <= 40) {
|
|
points.shift();
|
|
}
|
|
|
|
return points.filter(point => {
|
|
if (!lastPoint || lastPoint.dist(point) >= 40) {
|
|
lastPoint = point;
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
getShapes() {
|
|
return this.diagram.cells.reduce((acc, cell) => {
|
|
const bounds = cell.getBbox();
|
|
|
|
const shape = {
|
|
site: cell.site,
|
|
points: [],
|
|
bounds: bounds };
|
|
|
|
|
|
const halfEdges = cell.halfedges;
|
|
const nHalfEdges = halfEdges.length;
|
|
|
|
let v = halfEdges[0].getStartpoint();
|
|
|
|
shape.points.push(new Vector2D(v.x, v.y));
|
|
|
|
for (let iHalfEdge = 0; iHalfEdge < nHalfEdges - 1; iHalfEdge++) {
|
|
v = halfEdges[iHalfEdge].getEndpoint();
|
|
|
|
shape.points.push(new Vector2D(v.x, v.y));
|
|
}
|
|
|
|
shape.points.forEach(p => {
|
|
p.x -= shape.site.x;
|
|
p.y -= shape.site.y;
|
|
});
|
|
|
|
shape.points = this._filterClosePoints(shape.points);
|
|
|
|
shape.centroid = getPolygonCentroid(
|
|
shape.points.map(p => p.toObject()));
|
|
|
|
|
|
acc.push(shape);
|
|
|
|
return acc;
|
|
}, []);
|
|
}}
|
|
|
|
|
|
function createPIXIApp(width, height) {
|
|
const app = new PIXI.Application({
|
|
width: width,
|
|
height: height,
|
|
resolution: Math.max(2, window.devicePixelRatio || 1),
|
|
autoDensity: true,
|
|
transparent: true,
|
|
antialias: false });
|
|
|
|
|
|
document.body.appendChild(app.view);
|
|
|
|
console.clear();
|
|
|
|
return app;
|
|
}
|
|
|
|
const canvasWidth = Math.min(window.innerWidth - 32, 640);
|
|
const canvasHeight = Math.min(window.innerHeight - 32, 640);
|
|
|
|
const app = createPIXIApp(canvasWidth, canvasHeight);
|
|
const loader = PIXI.Loader.shared;
|
|
|
|
const textures = {};
|
|
|
|
app.ticker.stop();
|
|
|
|
gsap.ticker.add(() => {
|
|
app.ticker.update();
|
|
});
|
|
|
|
loader.
|
|
add("f1", "https://assets.codepen.io/5367578/f1.png").
|
|
add("f2", "https://assets.codepen.io/5367578/f2.png").
|
|
add("f3", "https://assets.codepen.io/5367578/f3.png").
|
|
add("f4", "https://assets.codepen.io/5367578/f4.png").
|
|
add("f5", "https://assets.codepen.io/5367578/f5.png").
|
|
add("m1", "https://assets.codepen.io/5367578/m1.png").
|
|
add("m2", "https://assets.codepen.io/5367578/m2.png").
|
|
add("m3", "https://assets.codepen.io/5367578/m3.png").
|
|
add("m4", "https://assets.codepen.io/5367578/m4.png");
|
|
|
|
loader.load((loader, resources) => {
|
|
Object.keys(resources).forEach(t => textures[t] = resources[t].texture);
|
|
|
|
window.splines = [];
|
|
|
|
init();
|
|
});
|
|
|
|
function init() {
|
|
const voronoi = new VoronoiDiagram(canvasWidth, canvasHeight);
|
|
|
|
voronoi.getShapes().forEach(shape => {
|
|
splines.push(new MetaSpline(0, 0, shape, app));
|
|
});
|
|
|
|
app.ticker.add(() => {
|
|
splines.forEach(spline => {
|
|
spline.update();
|
|
spline.render();
|
|
});
|
|
});
|
|
} |