mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-15 18:19:47 +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 artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
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 * as style from './style.css';
|
||||||
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
||||||
import '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 installButtonSource = 'introInstallButton-Purple';
|
||||||
const supportsClipboardAPI =
|
const supportsClipboardAPI =
|
||||||
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
|
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
|
||||||
@@ -66,6 +68,7 @@ interface State {
|
|||||||
export default class Intro extends Component<Props, State> {
|
export default class Intro extends Component<Props, State> {
|
||||||
state: State = {};
|
state: State = {};
|
||||||
private fileInput?: HTMLInputElement;
|
private fileInput?: HTMLInputElement;
|
||||||
|
private blobCanvas?: HTMLCanvasElement;
|
||||||
private installingViaButton = false;
|
private installingViaButton = false;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -77,6 +80,8 @@ export default class Intro extends Component<Props, State> {
|
|||||||
|
|
||||||
// Listen for the appinstalled event, indicating Squoosh has been installed.
|
// Listen for the appinstalled event, indicating Squoosh has been installed.
|
||||||
window.addEventListener('appinstalled', this.onAppInstalled);
|
window.addEventListener('appinstalled', this.onAppInstalled);
|
||||||
|
|
||||||
|
blobAnimImport.then((module) => module.startBlobAnim(this.blobCanvas!));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -95,7 +100,7 @@ export default class Intro extends Component<Props, State> {
|
|||||||
this.props.onFile!(file);
|
this.props.onFile!(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onButtonClick = () => {
|
private onOpenClick = () => {
|
||||||
this.fileInput!.click();
|
this.fileInput!.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,19 +184,17 @@ export default class Intro extends Component<Props, State> {
|
|||||||
try {
|
try {
|
||||||
clipboardItems = await navigator.clipboard.read();
|
clipboardItems = await navigator.clipboard.read();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.props.showSnack!(`Cannot access clipboard`);
|
this.props.showSnack!(`No permission to access clipboard`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await getImageClipboardItem(clipboardItems);
|
const blob = await getImageClipboardItem(clipboardItems);
|
||||||
|
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
this.props.showSnack!(`No image found`);
|
this.props.showSnack!(`No image found in the clipboard`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(blob);
|
|
||||||
|
|
||||||
this.props.onFile!(new File([blob], 'image.unknown'));
|
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>
|
<button onClick={this.onInstallClick}>Install</button>
|
||||||
)}
|
)}
|
||||||
<div class={style.main}>
|
<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.loadImg}>
|
||||||
<div
|
<div
|
||||||
class={style.loadImgContent}
|
class={style.loadImgContent}
|
||||||
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
|
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
|
||||||
>
|
>
|
||||||
<button class={style.loadBtn}>
|
<button class={style.loadBtn} onClick={this.onOpenClick}>
|
||||||
<svg viewBox="0 0 24 24" class={style.loadIcon}>
|
<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" />
|
<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>
|
</svg>
|
||||||
|
|||||||
@@ -5,34 +5,43 @@
|
|||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blob-canvas {
|
||||||
|
composes: abs-fill from '../util.css';
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hide {
|
.hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
min-height: 50vh;
|
min-height: 70vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: max-content max-content;
|
grid-template-rows: max-content max-content;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
align-content: center;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
margin: 8rem 0 -1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-weight: bold;
|
transform: translate(-1%, 0);
|
||||||
font-size: 3rem;
|
width: 189px;
|
||||||
margin: 4rem 0;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-img {
|
.load-img {
|
||||||
background: var(--pink);
|
position: relative;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
--size: 23rem;
|
|
||||||
border-radius: var(--size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-img-content {
|
.load-img-content {
|
||||||
|
--size: 36rem;
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user