mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 17:49:52 +00:00
Background blobs
This commit is contained in:
@@ -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,74 +123,188 @@ class CircleBlob {
|
|||||||
return point;
|
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 {
|
class CentralBlobs {
|
||||||
private rotatePos: number = 0;
|
private rotatePos: number = 0;
|
||||||
private blobs = Array.from(
|
private blobs = Array.from(
|
||||||
{ length: 4 },
|
{ length: 4 },
|
||||||
(_, i) => new CircleBlob(sevenPointCircle, startBlobs[i]),
|
(_, i) => new CircleBlob(sevenPointCircle, { startPoints: startBlobs[i] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log(
|
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) => {
|
addEventListener('keyup', (event) => {
|
||||||
if (event.key !== 'b') return;
|
if (event.key !== 'b') return;
|
||||||
console.log(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
this.blobs.map((blob) =>
|
this.blobs.map((blob) =>
|
||||||
blob
|
blob.points.map((points) =>
|
||||||
.frame(0)
|
points.map((point) => Number(point.toFixed(3))),
|
||||||
.map((points) => points.map((point) => Number(point.toFixed(3)))),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(
|
advance(timeDelta: number) {
|
||||||
ctx: CanvasRenderingContext2D,
|
this.rotatePos =
|
||||||
timeDelta: number,
|
(this.rotatePos + timeDelta / centralBlobsRotationTime) % 1;
|
||||||
x: number,
|
for (const blob of this.blobs) blob.advance(timeDelta);
|
||||||
y: number,
|
}
|
||||||
radius: number,
|
|
||||||
color: string,
|
draw(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number) {
|
||||||
opacity: number,
|
|
||||||
) {
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
ctx.scale(radius, radius);
|
ctx.scale(radius, radius);
|
||||||
this.rotatePos = (this.rotatePos + timeDelta / rotationTime) % 1;
|
|
||||||
ctx.rotate(Math.PI * 2 * this.rotatePos);
|
ctx.rotate(Math.PI * 2 * this.rotatePos);
|
||||||
ctx.globalAlpha = opacity;
|
for (const blob of this.blobs) blob.draw(ctx);
|
||||||
ctx.fillStyle = color;
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const blob of this.blobs) {
|
const bgBlobsMinRadius = 20;
|
||||||
const points = blob.frame(timeDelta);
|
const bgBlobsMaxRadius = 60;
|
||||||
ctx.beginPath();
|
const bgBlobsMinAlpha = 0.1;
|
||||||
ctx.moveTo(points[0][2], points[0][3]);
|
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++) {
|
interface BackgroundBlob {
|
||||||
const nextI = i === points.length - 1 ? 0 : i + 1;
|
blob: CircleBlob;
|
||||||
ctx.bezierCurveTo(
|
velocity: number;
|
||||||
points[i][4],
|
spinTime: number;
|
||||||
points[i][5],
|
alpha: number;
|
||||||
points[nextI][0],
|
rotatePos: number;
|
||||||
points[nextI][1],
|
radius: number;
|
||||||
points[nextI][2],
|
x: number;
|
||||||
points[nextI][3],
|
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();
|
const direction = Math.atan2(targetX - bgBlob.x, targetY - bgBlob.y);
|
||||||
ctx.fill();
|
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;
|
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')),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user