Background blobs

This commit is contained in:
Jake Archibald
2020-11-26 11:38:50 +00:00
parent 2d1a3b543a
commit 9e1fb6dfb4
2 changed files with 199 additions and 51 deletions

View File

@@ -6,10 +6,10 @@ import { startBlobs } from './meta';
*/ */
export type BlobPoint = [number, number, number, number, number, number]; export type BlobPoint = [number, number, number, number, number, number];
const maxRandomDistance = 0.25; const maxPointDistance = 0.25;
function randomisePoint(point: BlobPoint): BlobPoint { function randomisePoint(point: BlobPoint): BlobPoint {
const distance = Math.random() * maxRandomDistance; const distance = Math.random() * maxPointDistance;
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
const xShift = Math.sin(angle) * distance; const xShift = Math.sin(angle) * distance;
const yShift = Math.cos(angle) * distance; const yShift = Math.cos(angle) * distance;
@@ -27,7 +27,7 @@ function easeInOutQuad(x: number): number {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
} }
const randomDuration = () => Math.random() * 5000 + 4000; const rand = (min: number, max: number) => Math.random() * (max - min) + min;
interface CircleBlobPointState { interface CircleBlobPointState {
basePoint: BlobPoint; basePoint: BlobPoint;
@@ -72,29 +72,45 @@ function createBezierCirclePoints(points: number): BlobPoint[] {
} }
*/ */
interface CircleBlobOptions {
minDuration?: number;
maxDuration?: number;
startPoints?: BlobPoint[];
}
class CircleBlob { class CircleBlob {
private animStates: CircleBlobPointState[] = []; private animStates: CircleBlobPointState[];
private minDuration: number;
private maxDuration: number;
public points: BlobPoint[];
constructor( constructor(
basePoints: BlobPoint[], basePoints: BlobPoint[],
startPoints: BlobPoint[] = basePoints.map((point) => randomisePoint(point)), {
startPoints = basePoints.map((point) => randomisePoint(point)),
minDuration = 4000,
maxDuration = 11000,
}: CircleBlobOptions = {},
) { ) {
this.points = startPoints;
this.minDuration = minDuration;
this.maxDuration = maxDuration;
this.animStates = basePoints.map((basePoint, i) => ({ this.animStates = basePoints.map((basePoint, i) => ({
basePoint, basePoint,
pos: 0, pos: 0,
duration: randomDuration(), duration: rand(minDuration, maxDuration),
startPoint: startPoints[i], startPoint: startPoints[i],
endPoint: randomisePoint(basePoint), endPoint: randomisePoint(basePoint),
})); }));
} }
frame(timeDelta: number): BlobPoint[] { advance(timeDelta: number): void {
return this.animStates.map((animState) => { this.points = this.animStates.map((animState) => {
animState.pos += timeDelta / animState.duration; animState.pos += timeDelta / animState.duration;
if (animState.pos >= 1) { if (animState.pos >= 1) {
animState.startPoint = animState.endPoint; animState.startPoint = animState.endPoint;
animState.pos = 0; animState.pos = 0;
animState.duration = randomDuration(); animState.duration = rand(this.minDuration, this.maxDuration);
animState.endPoint = randomisePoint(animState.basePoint); animState.endPoint = randomisePoint(animState.basePoint);
} }
const eased = easeInOutQuad(animState.pos); const eased = easeInOutQuad(animState.pos);
@@ -107,54 +123,9 @@ class CircleBlob {
return point; return point;
}); });
} }
}
const rotationTime = 120000; draw(ctx: CanvasRenderingContext2D) {
const points = this.points;
class CentralBlobs {
private rotatePos: number = 0;
private blobs = Array.from(
{ length: 4 },
(_, i) => new CircleBlob(sevenPointCircle, startBlobs[i]),
);
constructor() {
console.log(
`WARNING: There's a debug key listener here that must be removed before going live`,
);
addEventListener('keyup', (event) => {
if (event.key !== 'b') return;
console.log(
JSON.stringify(
this.blobs.map((blob) =>
blob
.frame(0)
.map((points) => points.map((point) => Number(point.toFixed(3)))),
),
),
);
});
}
draw(
ctx: CanvasRenderingContext2D,
timeDelta: number,
x: number,
y: number,
radius: number,
color: string,
opacity: number,
) {
ctx.save();
ctx.translate(x, y);
ctx.scale(radius, radius);
this.rotatePos = (this.rotatePos + timeDelta / rotationTime) % 1;
ctx.rotate(Math.PI * 2 * this.rotatePos);
ctx.globalAlpha = opacity;
ctx.fillStyle = color;
for (const blob of this.blobs) {
const points = blob.frame(timeDelta);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(points[0][2], points[0][3]); ctx.moveTo(points[0][2], points[0][3]);
@@ -173,17 +144,177 @@ class CentralBlobs {
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
} }
}
const centralBlobsRotationTime = 120000;
class CentralBlobs {
private rotatePos: number = 0;
private blobs = Array.from(
{ length: 4 },
(_, i) => new CircleBlob(sevenPointCircle, { startPoints: startBlobs[i] }),
);
constructor() {
console.log(
`WARNING: There's a debug key listener here that must be removed before going live - also change CircleBlob.points to private`,
);
addEventListener('keyup', (event) => {
if (event.key !== 'b') return;
console.log(
JSON.stringify(
this.blobs.map((blob) =>
blob.points.map((points) =>
points.map((point) => Number(point.toFixed(3))),
),
),
),
);
});
}
advance(timeDelta: number) {
this.rotatePos =
(this.rotatePos + timeDelta / centralBlobsRotationTime) % 1;
for (const blob of this.blobs) blob.advance(timeDelta);
}
draw(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number) {
ctx.save();
ctx.translate(x, y);
ctx.scale(radius, radius);
ctx.rotate(Math.PI * 2 * this.rotatePos);
for (const blob of this.blobs) blob.draw(ctx);
ctx.restore(); ctx.restore();
} }
} }
const bgBlobsMinRadius = 20;
const bgBlobsMaxRadius = 60;
const bgBlobsMinAlpha = 0.1;
const bgBlobsMaxAlpha = 0.8;
const bgBlobsGridSize = 200;
const bgBlobsMinSpinTime = 20000;
const bgBlobsMaxSpinTime = 60000;
const bgBlobsMinVelocity = 0.005;
const bgBlobsMaxVelocity = 0.02;
interface BackgroundBlob {
blob: CircleBlob;
velocity: number;
spinTime: number;
alpha: number;
rotatePos: number;
radius: number;
x: number;
y: number;
}
const bgBlobsAlphaTime = 2000;
class BackgroundBlobs {
private bgBlobs: BackgroundBlob[] = [];
private overallAlphaPos = 0;
constructor(bounds: DOMRect) {
for (let x = 0; x < bounds.width; x += bgBlobsGridSize) {
for (let y = 0; y < bounds.height; y += bgBlobsGridSize) {
this.bgBlobs.push({
blob: new CircleBlob(sevenPointCircle, {
minDuration: 2000,
maxDuration: 5000,
}),
velocity:
Math.random() * (bgBlobsMaxVelocity - bgBlobsMinVelocity) +
bgBlobsMinVelocity,
alpha:
Math.random() ** 3 * (bgBlobsMaxAlpha - bgBlobsMinAlpha) +
bgBlobsMinAlpha,
spinTime:
Math.random() * (bgBlobsMaxSpinTime - bgBlobsMinSpinTime) +
bgBlobsMinSpinTime,
rotatePos: 0,
radius:
Math.random() ** 3 * (bgBlobsMaxRadius - bgBlobsMinRadius) +
bgBlobsMinRadius,
x: Math.random() * bgBlobsGridSize + x,
y: Math.random() * bgBlobsGridSize + y,
});
}
}
}
advance(
timeDelta: number,
bounds: DOMRect,
targetX: number,
targetY: number,
) {
if (this.overallAlphaPos !== 1) {
this.overallAlphaPos = Math.min(
1,
this.overallAlphaPos + timeDelta / bgBlobsAlphaTime,
);
}
for (const bgBlob of this.bgBlobs) {
bgBlob.blob.advance(timeDelta);
const dist = Math.hypot(bgBlob.x - targetX, bgBlob.y - targetY);
bgBlob.rotatePos = (bgBlob.rotatePos + timeDelta / bgBlob.spinTime) % 1;
const shiftDist = bgBlob.velocity * timeDelta;
if (dist < 10) {
// Move the circle out to a random edge
const tlbr = Math.floor(Math.random() * 4);
switch (tlbr) {
case 0: // top
bgBlob.x = Math.random() * bounds.width;
bgBlob.y = -(bgBlob.radius * (1 + maxPointDistance));
break;
case 1: // left
bgBlob.x = -(bgBlob.radius * (1 + maxPointDistance));
bgBlob.y = Math.random() * bounds.height;
break;
case 2: // bottom
bgBlob.x = Math.random() * bounds.width;
bgBlob.y = bounds.height + bgBlob.radius * (1 + maxPointDistance);
break;
case 3: // right
bgBlob.x = bounds.width + bgBlob.radius * (1 + maxPointDistance);
bgBlob.y = Math.random() * bounds.height;
break;
}
}
const direction = Math.atan2(targetX - bgBlob.x, targetY - bgBlob.y);
const xShift = Math.sin(direction) * shiftDist;
const yShift = Math.cos(direction) * shiftDist;
bgBlob.x += xShift;
bgBlob.y += yShift;
}
}
draw(ctx: CanvasRenderingContext2D) {
const overallAlpha = easeInOutQuad(this.overallAlphaPos);
for (const bgBlob of this.bgBlobs) {
ctx.save();
ctx.globalAlpha = bgBlob.alpha * overallAlpha;
ctx.translate(bgBlob.x, bgBlob.y);
ctx.scale(bgBlob.radius, bgBlob.radius);
ctx.rotate(Math.PI * 2 * bgBlob.rotatePos);
bgBlob.blob.draw(ctx);
ctx.restore();
}
}
}
const deltaMultiplierStep = 0.01; const deltaMultiplierStep = 0.01;
export function startBlobAnim(canvas: HTMLCanvasElement) { export function startBlobAnim(canvas: HTMLCanvasElement) {
let lastTime: number; let lastTime: number;
const ctx = canvas.getContext('2d')!; const ctx = canvas.getContext('2d')!;
const centralBlobs = new CentralBlobs(); const centralBlobs = new CentralBlobs();
let backgroundBlobs: BackgroundBlobs;
const loadImgEl = document.querySelector('.' + style.loadImg)!; const loadImgEl = document.querySelector('.' + style.loadImg)!;
let deltaMultiplier = 1; let deltaMultiplier = 1;
let hasFocus = true; let hasFocus = true;
@@ -217,17 +348,34 @@ export function startBlobAnim(canvas: HTMLCanvasElement) {
const loadImgBounds = loadImgEl.getBoundingClientRect(); const loadImgBounds = loadImgEl.getBoundingClientRect();
const computedStyles = getComputedStyle(canvas); const computedStyles = getComputedStyle(canvas);
const blobPink = computedStyles.getPropertyValue('--blob-pink'); const blobPink = computedStyles.getPropertyValue('--blob-pink');
const loadImgCenterX =
loadImgBounds.left - canvasBounds.left + loadImgBounds.width / 2;
const loadImgCenterY =
loadImgBounds.top - canvasBounds.top + loadImgBounds.height / 2;
ctx.scale(devicePixelRatio, devicePixelRatio); ctx.scale(devicePixelRatio, devicePixelRatio);
if (!backgroundBlobs) backgroundBlobs = new BackgroundBlobs(canvasBounds);
backgroundBlobs.advance(
delta,
canvasBounds,
loadImgCenterX,
loadImgCenterY,
);
centralBlobs.advance(delta);
ctx.globalAlpha = Number(
computedStyles.getPropertyValue('--center-blob-opacity'),
);
ctx.fillStyle = blobPink;
backgroundBlobs.draw(ctx);
centralBlobs.draw( centralBlobs.draw(
ctx, ctx,
delta, loadImgCenterX,
loadImgBounds.left - canvasBounds.left + loadImgBounds.width / 2, loadImgCenterY,
loadImgBounds.top - canvasBounds.top + loadImgBounds.height / 2, loadImgBounds.height / 2 / (1 + maxPointDistance),
loadImgBounds.height / 2 / (1 + maxRandomDistance),
blobPink,
Number(computedStyles.getPropertyValue('--center-blob-opacity')),
); );
} }

View File

@@ -22,7 +22,7 @@
justify-items: center; justify-items: center;
position: relative; position: relative;
--blob-pink: var(--hot-pink); --blob-pink: var(--hot-pink);
--center-blob-opacity: 0.3; --center-blob-opacity: 0.6;
} }
.logo-container { .logo-container {