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(); }); }); }