mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
Logo and animated blobs
This commit is contained in:
212
src/shared/initial-app/Intro/blob-anim.ts
Normal file
212
src/shared/initial-app/Intro/blob-anim.ts
Normal 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();
|
||||
}
|
||||
1
src/shared/initial-app/Intro/imgs/logo-with-text.svg
Normal file
1
src/shared/initial-app/Intro/imgs/logo-with-text.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user