diff --git a/src/shared/initial-app/Intro/blob-anim.ts b/src/shared/initial-app/Intro/blob-anim.ts
new file mode 100644
index 00000000..b9b88f92
--- /dev/null
+++ b/src/shared/initial-app/Intro/blob-anim.ts
@@ -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();
+}
diff --git a/src/shared/initial-app/Intro/imgs/logo-with-text.svg b/src/shared/initial-app/Intro/imgs/logo-with-text.svg
new file mode 100644
index 00000000..8ea594f2
--- /dev/null
+++ b/src/shared/initial-app/Intro/imgs/logo-with-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/shared/initial-app/Intro/index.tsx b/src/shared/initial-app/Intro/index.tsx
index cc6b3362..8eef2357 100644
--- a/src/shared/initial-app/Intro/index.tsx
+++ b/src/shared/initial-app/Intro/index.tsx
@@ -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 {
state: State = {};
private fileInput?: HTMLInputElement;
+ private blobCanvas?: HTMLCanvasElement;
private installingViaButton = false;
componentDidMount() {
@@ -77,6 +80,8 @@ export default class Intro extends Component {
// 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 {
this.props.onFile!(file);
};
- private onButtonClick = () => {
+ private onOpenClick = () => {
this.fileInput!.click();
};
@@ -179,19 +184,17 @@ export default class Intro extends Component {
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 {
)}
-
Logo Placeholder
+ {!__PRERENDER__ && (
+
+ )}
+
+
+
-