Logo and animated blobs

This commit is contained in:
Jake Archibald
2020-11-25 13:39:48 +00:00
parent 49620e4c8f
commit be3ab8b6d1
4 changed files with 254 additions and 15 deletions

View File

@@ -0,0 +1,212 @@
import * as style from './style.css';
/**
* Control point x,y - point x,y - control point x,y
*/
type BlobPoint = [number, number, number, number, number, number];
const maxRandomDistance = 0.25;
function randomisePoint(point: BlobPoint): BlobPoint {
const distance = Math.random() * maxRandomDistance;
const angle = Math.random() * Math.PI * 2;
const xShift = Math.sin(angle) * distance;
const yShift = Math.cos(angle) * distance;
return [
point[0] + xShift,
point[1] + yShift,
point[2] + xShift,
point[3] + yShift,
point[4] + xShift,
point[5] + yShift,
];
}
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;
interface CircleBlobPointState {
basePoint: BlobPoint;
pos: number;
duration: number;
startPoint: BlobPoint;
endPoint: BlobPoint;
}
class CircleBlob {
private animStates: CircleBlobPointState[] = [];
constructor(points: number) {
const anglePerPoint = 360 / points;
const matrix = new DOMMatrix();
const point = new DOMPoint();
const controlDistance = (4 / 3) * Math.tan(Math.PI / (2 * points));
for (let i = 0; i < points; i++) {
point.x = -controlDistance;
point.y = -1;
const cp1 = point.matrixTransform(matrix);
point.x = 0;
point.y = -1;
const p = point.matrixTransform(matrix);
point.x = controlDistance;
point.y = -1;
const cp2 = point.matrixTransform(matrix);
const basePoint: BlobPoint = [cp1.x, cp1.y, p.x, p.y, cp2.x, cp2.y];
this.animStates.push({
basePoint,
pos: 0,
duration: randomDuration(),
startPoint: randomisePoint(basePoint),
endPoint: randomisePoint(basePoint),
});
matrix.rotateSelf(0, 0, anglePerPoint);
}
}
frame(timeDelta: number): BlobPoint[] {
return this.animStates.map((animState) => {
animState.pos += timeDelta / animState.duration;
if (animState.pos >= 1) {
animState.startPoint = animState.endPoint;
animState.pos = 0;
animState.duration = randomDuration();
animState.endPoint = randomisePoint(animState.basePoint);
}
const eased = easeInOutQuad(animState.pos);
const point = animState.startPoint.map((startPoint, i) => {
const endPoint = animState.endPoint[i];
return (endPoint - startPoint) * eased + startPoint;
}) as BlobPoint;
return point;
});
}
}
const rotationTime = 120000;
class CentralBlobs {
private rotatePos: number = 0;
private blobs = Array.from({ length: 4 }, () => new CircleBlob(7));
draw(
ctx: CanvasRenderingContext2D,
timeDelta: number,
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);
for (const blob of this.blobs) {
const points = blob.frame(timeDelta);
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();
}
ctx.restore();
}
}
const deltaMultiplierStep = 0.01;
export function startBlobAnim(canvas: HTMLCanvasElement) {
let lastTime: number;
const ctx = canvas.getContext('2d')!;
const centralBlobs = new CentralBlobs();
const loadImgEl = document.querySelector('.' + style.loadImg)!;
let deltaMultiplier = 1;
let hasFocus = true;
let animating = true;
const focusListener = () => {
hasFocus = true;
if (!animating) startAnim();
};
const blurListener = () => {
hasFocus = false;
};
addEventListener('focus', focusListener);
addEventListener('blur', blurListener);
function destruct() {
removeEventListener('focus', focusListener);
removeEventListener('blur', blurListener);
}
function frame(time: number) {
// Stop the loop if the canvas is gone
if (!canvas.isConnected) {
destruct();
return;
}
// Be kind: If the window isn't focused, bring the animation to a stop.
if (!hasFocus) {
// Bring the anim to a slow stop
deltaMultiplier = Math.max(0, deltaMultiplier - deltaMultiplierStep);
if (deltaMultiplier === 0) {
animating = false;
return;
}
} else if (deltaMultiplier !== 1) {
deltaMultiplier = Math.min(1, deltaMultiplier + deltaMultiplierStep);
}
const delta = (time - lastTime) * deltaMultiplier;
lastTime = time;
const canvasBounds = canvas.getBoundingClientRect();
canvas.width = canvasBounds.width * devicePixelRatio;
canvas.height = canvasBounds.height * devicePixelRatio;
const loadImgBounds = loadImgEl.getBoundingClientRect();
ctx.fillStyle = 'rgba(255, 0, 102, 0.3)';
ctx.scale(devicePixelRatio, devicePixelRatio);
centralBlobs.draw(
ctx,
delta,
loadImgBounds.left - canvasBounds.left + loadImgBounds.width / 2,
loadImgBounds.top - canvasBounds.top + loadImgBounds.height / 2,
(loadImgBounds.width / 2) * (1 - maxRandomDistance),
);
requestAnimationFrame(frame);
}
function startAnim() {
animating = true;
requestAnimationFrame((time: number) => {
lastTime = time;
frame(time);
});
}
startAnim();
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -10,6 +10,7 @@ import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
import logoWithText from 'url:./imgs/logo-with-text.svg';
import * as style from './style.css';
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
import 'shared/initial-app/custom-els/snack-bar';
@@ -41,6 +42,7 @@ const demos = [
},
];
const blobAnimImport = import('./blob-anim');
const installButtonSource = 'introInstallButton-Purple';
const supportsClipboardAPI =
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
@@ -66,6 +68,7 @@ interface State {
export default class Intro extends Component<Props, State> {
state: State = {};
private fileInput?: HTMLInputElement;
private blobCanvas?: HTMLCanvasElement;
private installingViaButton = false;
componentDidMount() {
@@ -77,6 +80,8 @@ export default class Intro extends Component<Props, State> {
// Listen for the appinstalled event, indicating Squoosh has been installed.
window.addEventListener('appinstalled', this.onAppInstalled);
blobAnimImport.then((module) => module.startBlobAnim(this.blobCanvas!));
}
componentWillUnmount() {
@@ -95,7 +100,7 @@ export default class Intro extends Component<Props, State> {
this.props.onFile!(file);
};
private onButtonClick = () => {
private onOpenClick = () => {
this.fileInput!.click();
};
@@ -179,19 +184,17 @@ export default class Intro extends Component<Props, State> {
try {
clipboardItems = await navigator.clipboard.read();
} catch (err) {
this.props.showSnack!(`Cannot access clipboard`);
this.props.showSnack!(`No permission to access clipboard`);
return;
}
const blob = await getImageClipboardItem(clipboardItems);
if (!blob) {
this.props.showSnack!(`No image found`);
this.props.showSnack!(`No image found in the clipboard`);
return;
}
console.log(blob);
this.props.onFile!(new File([blob], 'image.unknown'));
};
@@ -208,13 +211,27 @@ export default class Intro extends Component<Props, State> {
<button onClick={this.onInstallClick}>Install</button>
)}
<div class={style.main}>
<div class={style.logo}>Logo Placeholder</div>
{!__PRERENDER__ && (
<canvas
ref={linkRef(this, 'blobCanvas')}
class={style.blobCanvas}
/>
)}
<h1 class={style.logoContainer}>
<img
class={style.logo}
src={logoWithText}
alt="Squoosh"
width="539"
height="62"
/>
</h1>
<div class={style.loadImg}>
<div
class={style.loadImgContent}
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
>
<button class={style.loadBtn}>
<button class={style.loadBtn} onClick={this.onOpenClick}>
<svg viewBox="0 0 24 24" class={style.loadIcon}>
<path d="M19 7v3h-2V7h-3V5h3V2h2v3h3v2h-3zm-3 4V8h-3V5H5a2 2 0 00-2 2v12c0 1.1.9 2 2 2h12a2 2 0 002-2v-8h-3zM5 19l3-4 2 3 3-4 4 5H5z" />
</svg>

View File

@@ -5,34 +5,43 @@
overscroll-behavior: contain;
}
.blob-canvas {
composes: abs-fill from '../util.css';
width: 100%;
height: 100%;
}
.hide {
display: none;
}
.main {
min-height: 50vh;
min-height: 70vh;
display: grid;
grid-template-rows: max-content max-content;
justify-items: center;
align-content: center;
position: relative;
}
.logo-container {
margin: 8rem 0 -1.2rem;
}
.logo {
font-weight: bold;
font-size: 3rem;
margin: 4rem 0;
transform: translate(-1%, 0);
width: 189px;
height: auto;
}
.load-img {
background: var(--pink);
position: relative;
color: var(--white);
font-style: italic;
font-size: 1.2rem;
--size: 23rem;
border-radius: var(--size);
}
.load-img-content {
--size: 36rem;
width: var(--size);
height: var(--size);
display: grid;