From 9e1fb6dfb453167ee7cea045812c69a07c8723fb Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Thu, 26 Nov 2020 11:38:50 +0000 Subject: [PATCH] Background blobs --- .../initial-app/Intro/blob-anim/index.ts | 248 ++++++++++++++---- src/shared/initial-app/Intro/style.css | 2 +- 2 files changed, 199 insertions(+), 51 deletions(-) diff --git a/src/shared/initial-app/Intro/blob-anim/index.ts b/src/shared/initial-app/Intro/blob-anim/index.ts index 66c83117..ee86a0b8 100644 --- a/src/shared/initial-app/Intro/blob-anim/index.ts +++ b/src/shared/initial-app/Intro/blob-anim/index.ts @@ -6,10 +6,10 @@ import { startBlobs } from './meta'; */ export type BlobPoint = [number, number, number, number, number, number]; -const maxRandomDistance = 0.25; +const maxPointDistance = 0.25; function randomisePoint(point: BlobPoint): BlobPoint { - const distance = Math.random() * maxRandomDistance; + const distance = Math.random() * maxPointDistance; const angle = Math.random() * Math.PI * 2; const xShift = Math.sin(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; } -const randomDuration = () => Math.random() * 5000 + 4000; +const rand = (min: number, max: number) => Math.random() * (max - min) + min; interface CircleBlobPointState { basePoint: BlobPoint; @@ -72,29 +72,45 @@ function createBezierCirclePoints(points: number): BlobPoint[] { } */ +interface CircleBlobOptions { + minDuration?: number; + maxDuration?: number; + startPoints?: BlobPoint[]; +} + class CircleBlob { - private animStates: CircleBlobPointState[] = []; + private animStates: CircleBlobPointState[]; + private minDuration: number; + private maxDuration: number; + public points: BlobPoint[]; constructor( 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) => ({ basePoint, pos: 0, - duration: randomDuration(), + duration: rand(minDuration, maxDuration), startPoint: startPoints[i], endPoint: randomisePoint(basePoint), })); } - frame(timeDelta: number): BlobPoint[] { - return this.animStates.map((animState) => { + advance(timeDelta: number): void { + this.points = this.animStates.map((animState) => { animState.pos += timeDelta / animState.duration; if (animState.pos >= 1) { animState.startPoint = animState.endPoint; animState.pos = 0; - animState.duration = randomDuration(); + animState.duration = rand(this.minDuration, this.maxDuration); animState.endPoint = randomisePoint(animState.basePoint); } const eased = easeInOutQuad(animState.pos); @@ -107,74 +123,188 @@ class CircleBlob { return point; }); } + + draw(ctx: CanvasRenderingContext2D) { + const points = this.points; + ctx.beginPath(); + ctx.moveTo(points[0][2], points[0][3]); + + for (let i = 0; i < points.length; i++) { + const nextI = i === points.length - 1 ? 0 : i + 1; + ctx.bezierCurveTo( + points[i][4], + points[i][5], + points[nextI][0], + points[nextI][1], + points[nextI][2], + points[nextI][3], + ); + } + + ctx.closePath(); + ctx.fill(); + } } -const rotationTime = 120000; +const centralBlobsRotationTime = 120000; class CentralBlobs { private rotatePos: number = 0; private blobs = Array.from( { length: 4 }, - (_, i) => new CircleBlob(sevenPointCircle, startBlobs[i]), + (_, 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`, + `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 - .frame(0) - .map((points) => points.map((point) => Number(point.toFixed(3)))), + blob.points.map((points) => + points.map((point) => Number(point.toFixed(3))), + ), ), ), ); }); } - draw( - ctx: CanvasRenderingContext2D, - timeDelta: number, - x: number, - y: number, - radius: number, - color: string, - opacity: number, - ) { + 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); - 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) blob.draw(ctx); + ctx.restore(); + } +} - for (const blob of this.blobs) { - const points = blob.frame(timeDelta); - ctx.beginPath(); - ctx.moveTo(points[0][2], points[0][3]); +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; - for (let i = 0; i < points.length; i++) { - const nextI = i === points.length - 1 ? 0 : i + 1; - ctx.bezierCurveTo( - points[i][4], - points[i][5], - points[nextI][0], - points[nextI][1], - points[nextI][2], - points[nextI][3], - ); +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; + } } - ctx.closePath(); - ctx.fill(); + 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; } + } - ctx.restore(); + 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(); + } } } @@ -184,6 +314,7 @@ export function startBlobAnim(canvas: HTMLCanvasElement) { let lastTime: number; const ctx = canvas.getContext('2d')!; const centralBlobs = new CentralBlobs(); + let backgroundBlobs: BackgroundBlobs; const loadImgEl = document.querySelector('.' + style.loadImg)!; let deltaMultiplier = 1; let hasFocus = true; @@ -217,17 +348,34 @@ export function startBlobAnim(canvas: HTMLCanvasElement) { const loadImgBounds = loadImgEl.getBoundingClientRect(); const computedStyles = getComputedStyle(canvas); 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); + 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( ctx, - delta, - loadImgBounds.left - canvasBounds.left + loadImgBounds.width / 2, - loadImgBounds.top - canvasBounds.top + loadImgBounds.height / 2, - loadImgBounds.height / 2 / (1 + maxRandomDistance), - blobPink, - Number(computedStyles.getPropertyValue('--center-blob-opacity')), + loadImgCenterX, + loadImgCenterY, + loadImgBounds.height / 2 / (1 + maxPointDistance), ); } diff --git a/src/shared/initial-app/Intro/style.css b/src/shared/initial-app/Intro/style.css index 0c187840..b8f47935 100644 --- a/src/shared/initial-app/Intro/style.css +++ b/src/shared/initial-app/Intro/style.css @@ -22,7 +22,7 @@ justify-items: center; position: relative; --blob-pink: var(--hot-pink); - --center-blob-opacity: 0.3; + --center-blob-opacity: 0.6; } .logo-container {