Styling intro screen and adding demo images.

This commit is contained in:
Jake Archibald
2018-10-12 14:11:10 +01:00
parent a43ea761f5
commit 568b9e9459
24 changed files with 550 additions and 50 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/icon-large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/assets/icon-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -43,7 +43,7 @@ import Intro from '../intro';
type Orientation = 'horizontal' | 'vertical';
export interface SourceImage {
file: File;
file: File | Fileish;
data: ImageData;
vectorImage?: HTMLImageElement;
}
@@ -259,7 +259,7 @@ export default class App extends Component<Props, State> {
}
@bind
async updateFile(file: File) {
async updateFile(file: File | Fileish) {
this.setState({ loading: true });
try {
let data: ImageData;
@@ -268,7 +268,7 @@ export default class App extends Component<Props, State> {
// Special-case SVG. We need to avoid createImageBitmap because of
// https://bugs.chromium.org/p/chromium/issues/detail?id=606319.
// Also, we cache the HTMLImageElement so we can perform vector resizing later.
if (file.type === 'image/svg+xml') {
if (file.type.startsWith('image/svg+xml')) {
vectorImage = await processSvg(file);
data = drawableToImageData(vectorImage);
} else {
@@ -368,6 +368,7 @@ export default class App extends Component<Props, State> {
this.setState({ images });
}
@bind
showError (error: string) {
if (!this.snackbar) throw Error('Snackbar missing');
this.snackbar.showSnackbar({ message: error });
@@ -410,7 +411,7 @@ export default class App extends Component<Props, State> {
))}
</div>
:
<Intro onFile={this.updateFile} />
<Intro onFile={this.updateFile} onError={this.showError} />
}
{anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
<snack-bar ref={linkRef(this, 'snackbar')} />

View File

@@ -10,7 +10,6 @@ Note: These styles are temporary. They will be replaced before going live.
height: 100%;
overflow: hidden;
contain: strict;
display: flex;
}
:global {
@@ -59,6 +58,7 @@ Note: These styles are temporary. They will be replaced before going live.
display: flex;
justify-content: flex-end;
width: 100%;
height: 100%;
&.horizontal {
justify-content: space-between;

View File

@@ -14,7 +14,6 @@ Note: These styles are temporary. They will be replaced before going live.
.output {
@extend %fill;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><path fill="rgba(0,0,0,0.05)" d="M0 0h10v10H0zM10 10h10v10H10z"/></svg>') center repeat;
&:before {
content: '';

View File

@@ -0,0 +1,62 @@
import * as styles from './styles.css';
/**
* A simple spinner. This custom element has no JS API. Just put it in the document, and it'll
* spin. You can configure the following using CSS custom properties:
*
* --size: Size of the spinner element (it's always square). Default: 28px.
* --color: Color of the spinner. Default: #4285f4.
* --stroke-width: Width of the stroke of the spinner. Default: 3px.
* --delay: Once the spinner enters the DOM, how long until it shows. This prevents the spinner
* appearing on the screen for short operations. Default: 300ms.
*/
export default class LoadingSpinner extends HTMLElement {
private _delayTimeout: number = 0;
constructor() {
super();
// Ideally we'd use shadow DOM here, but we're targeting browsers without shadow DOM support.
// You can't set attributes/content in a custom element constructor, so I'm waiting a microtask.
Promise.resolve().then(() => {
this.style.display = 'none';
this.innerHTML = '' +
`<div class="${styles.spinnerContainer}">` +
`<div class="${styles.spinnerLayer}">` +
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerLeft}">` +
`<div class="${styles.spinnerCircle}"></div>` +
'</div>' +
`<div class="${styles.spinnerGapPatch}">` +
`<div class="${styles.spinnerCircle}"></div>` +
'</div>' +
`<div class="${styles.spinnerCircleClipper} ${styles.spinnerRight}">` +
`<div class="${styles.spinnerCircle}"></div>` +
'</div>' +
'</div>' +
'</div>';
});
}
disconnectedCallback() {
this.style.display = 'none';
clearTimeout(this._delayTimeout);
}
connectedCallback() {
const delayStr = getComputedStyle(this).getPropertyValue('--delay').trim();
let delayNum = parseFloat(delayStr);
// If seconds…
if (/\ds$/.test(delayStr)) {
// Convert to ms.
delayNum *= 1000;
}
this._delayTimeout = self.setTimeout(
() => { this.style.display = ''; },
delayNum,
);
}
}
customElements.define('loading-spinner', LoadingSpinner);

View File

@@ -0,0 +1,7 @@
interface LoadingSpinner extends JSX.HTMLAttributes {}
declare namespace JSX {
interface IntrinsicElements {
'loading-spinner': LoadingSpinner;
}
}

View File

@@ -0,0 +1,124 @@
@keyframes spinner-left-spin {
from { transform: rotate(130deg); }
50% { transform: rotate(-5deg); }
to { transform: rotate(130deg); }
}
@keyframes spinner-right-spin {
from { transform: rotate(-130deg); }
50% { transform: rotate(5deg); }
to { transform: rotate(-130deg); }
}
@keyframes spinner-fade-out {
to { opacity: 0; }
}
@keyframes spinner-container-rotate {
to { transform: rotate(360deg) }
}
@keyframes spinner-fill-unfill-rotate {
12.5% { transform: rotate(135deg); } /* 0.5 * ARCSIZE */
25% { transform: rotate(270deg); } /* 1 * ARCSIZE */
37.5% { transform: rotate(405deg); } /* 1.5 * ARCSIZE */
50% { transform: rotate(540deg); } /* 2 * ARCSIZE */
62.5% { transform: rotate(675deg); } /* 2.5 * ARCSIZE */
75% { transform: rotate(810deg); } /* 3 * ARCSIZE */
87.5% { transform: rotate(945deg); } /* 3.5 * ARCSIZE */
to { transform: rotate(1080deg); } /* 4 * ARCSIZE */
}
loading-spinner {
--size: 28px;
--color: #4285f4;
--stroke-width: 3px;
--delay: 300ms;
pointer-events: none;
display: inline-block;
position: relative;
width: var(--size);
height: var(--size);
border-color: var(--color);
}
loading-spinner .spinner-circle {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
height: 100%;
width: 200%;
border-width: var(--stroke-width);
border-style: solid;
border-color: inherit;
border-bottom-color: transparent !important;
border-radius: 50%;
}
/*
Patch the gap that appear between the two adjacent div.circle-clipper while the
spinner is rotating (appears on Chrome 38, Safari 7.1, and IE 11).
*/
loading-spinner .spinner-gap-patch {
position: absolute;
box-sizing: border-box;
top: 0;
left: 45%;
width: 10%;
height: 100%;
overflow: hidden;
border-color: inherit;
}
loading-spinner .spinner-gap-patch .spinner-circle {
width: 1000%;
left: -450%;
}
loading-spinner .spinner-circle-clipper {
display: inline-block;
position: relative;
width: 50%;
height: 100%;
overflow: hidden;
border-color: inherit;
}
loading-spinner .spinner-left .spinner-circle {
border-right-color: transparent !important;
transform: rotate(129deg);
animation: spinner-left-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
}
loading-spinner .spinner-right .spinner-circle {
left: -100%;
border-left-color: transparent !important;
transform: rotate(-129deg);
animation: spinner-right-spin 1333ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
}
loading-spinner.spinner-fadeout {
animation: spinner-fade-out 400ms cubic-bezier(0.4, 0.0, 0.2, 1) forwards;
}
loading-spinner .spinner-container {
width: 100%;
height: 100%;
border-color: inherit;
/* duration: 360 * ARCTIME / (ARCSTARTROT + (360-ARCSIZE)) */
animation: spinner-container-rotate 1568ms linear infinite;
}
loading-spinner .spinner-layer {
position: absolute;
width: 100%;
height: 100%;
border-color: inherit;
/* durations: 4 * ARCTIME */
animation: spinner-fill-unfill-rotate 5332ms cubic-bezier(0.4, 0.0, 0.2, 1) infinite both;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,26 +1,134 @@
import { h, Component } from 'preact';
import { bind, linkRef, Fileish } from '../../lib/util';
import '../custom-els/LoadingSpinner';
import logo from './imgs/logo.svg';
import largePhoto from './imgs/demos/large-photo.jpg';
import artwork from './imgs/demos/artwork.jpg';
import deviceScreen from './imgs/demos/device-screen.png';
import largePhotoIcon from './imgs/demos/large-photo-icon.jpg';
import artworkIcon from './imgs/demos/artwork-icon.jpg';
import deviceScreenIcon from './imgs/demos/device-screen-icon.jpg';
import logoIcon from './imgs/demos/logo-icon.png';
import * as style from './style.scss';
import { bind } from '../../lib/util';
const demos = [
{
description: 'Large photo (2.8mb)',
filename: 'photo.jpg',
url: largePhoto,
iconUrl: largePhotoIcon,
},
{
description: 'Artwork (2.9mb)',
filename: 'art.jpg',
url: artwork,
iconUrl: artworkIcon,
},
{
description: 'Device screen (1.6mb)',
filename: 'pixel3.png',
url: deviceScreen,
iconUrl: deviceScreenIcon,
},
{
description: 'SVG icon (13k)',
filename: 'squoosh.svg',
url: logo,
iconUrl: logoIcon,
},
];
interface Props {
onFile: (file: File) => void;
onFile: (file: File | Fileish) => void;
onError: (error: string) => void;
}
interface State {
fetchingDemoIndex?: number;
}
interface State {}
export default class Intro extends Component<Props, State> {
state: State = {};
private fileInput?: HTMLInputElement;
@bind
onFileChange(event: Event): void {
private onFileChange(event: Event): void {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0];
if (!file) return;
this.props.onFile(file);
}
render({ }: Props, { }: State) {
@bind
private onButtonClick() {
this.fileInput!.click();
}
@bind
private async onDemoClick(index: number, event: Event) {
try {
this.setState({ fetchingDemoIndex: index });
const demo = demos[index];
const blob = await fetch(demo.url).then(r => r.blob());
// Firefox doesn't like content types like 'image/png; charset=UTF-8', which Webpack's dev
// server returns. https://bugzilla.mozilla.org/show_bug.cgi?id=1497925.
const type = /[^;]*/.exec(blob.type)![0];
const file = new Fileish([blob], demo.filename, { type });
this.props.onFile(file);
} catch (err) {
this.setState({ fetchingDemoIndex: undefined });
this.props.onError("Couldn't fetch demo image");
}
}
render({ }: Props, { fetchingDemoIndex }: State) {
return (
<div class={style.welcome}>
<h1>Drop, paste or select an image</h1>
<input type="file" onChange={this.onFileChange} />
<div class={style.intro}>
<div>
<div class={style.logoSizer}>
<div class={style.logoContainer}>
<img src={logo} class={style.logo} alt="Squoosh" />
</div>
</div>
<p class={style.openImageGuide}>
Drag &amp; drop or{' '}
<button class={style.selectButton} onClick={this.onButtonClick}>select an image</button>
<input
class={style.hide}
ref={linkRef(this, 'fileInput')}
type="file"
onChange={this.onFileChange}
/>
</p>
<p>Or try one of these:</p>
<ul class={style.demos}>
{demos.map((demo, i) =>
<li key={demo.url} class={style.demoItem}>
<button class={style.demoButton} onClick={this.onDemoClick.bind(this, i)}>
<div class={style.demo}>
<div class={style.demoImgContainer}>
<div class={style.demoImgAspect}>
<img class={style.demoIcon} src={demo.iconUrl} alt=""/>
{fetchingDemoIndex === i &&
<div class={style.demoLoading}>
<loading-spinner class={style.demoLoadingSpinner}/>
</div>
}
</div>
</div>
<div class={style.demoDescription}>{demo.description}</div>
</div>
</button>
</li>,
)}
</ul>
</div>
<ul class={style.relatedLinks}>
<li><a href="https://github.com/GoogleChromeLabs/squoosh/">View the code</a></li>
<li><a href="https://github.com/GoogleChromeLabs/squoosh/issues">Report a bug</a></li>
</ul>
</div>
);
}

View File

@@ -1,22 +1,191 @@
.welcome {
margin: auto;
text-align: center;
h1 {
font-weight: inherit;
font-size: 150%;
text-align: center;
@font-face {
font-family: 'intro-text';
font-style: normal;
font-weight: 300;
// This only contains the chars for "Drag & drop or"
src: url('data:font/woff2;base64,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') format('woff2');
}
input {
display: inline-block;
width: 16em;
padding: 10px;
@font-face {
font-family: 'intro-text';
font-style: normal;
font-weight: 500;
// Only contains the chars for "select an image"
src: url('data:font/woff2;base64,d09GMgABAAAAAAXMAA4AAAAACwQAAAV5AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbg1IcMAZgAGwRCAqJUId/CxoAATYCJAMwBCAFgnAHIBskCcjEh6dNff8Ou/9Tj9VZGnUhJeqFzWGiVVOkxkTthr9f6kWkRdsBbkIj3YuLaloFZWr7aBg22z7IOoWqBWCW5cZU3GBrh0n+dAcBYAUlzYHAzWTYchqKXEyAT0zOLIS1qqm+B8q2ur4OhEMy0PHHUH8KklaSr8T0mp/EU7kRvXlI1E09HXA1qLN8Djxa0AsSDOg3cJARb9mtQJoSK3gvEn372/gcAigg/gOnbsT/MYv491GTReW4rJC5LA+h5FFclF6QQgoZ5Kx7GbsuGeytUgFClkOomY2Gdake3m9HegkHieAx/a0hBTALsy4jvpxBcnFXUnjC+2ZS5zHnDeEaJVwi+ZWqzOm4Uvgy4k6kGv4kFDVkfjk1gkVRRkk2zlo42PBbRJmG30cClJQjak7BnfQqHza4ITKftQZ/ZMUaEiyy1+mYCh4clKhDA5rQglZ0oG9jw+qiNvT+SfxIXCeuFdeIq8VV4gpxyRaGOl0JiChiCocfc7e93DwZIPvWgPiZJLcJugxyjW7UQyl4TJk6dWqYU7Cn0WQiWnNJCdGeprqjW63fpVS3mKy4YGZ6I3ya4nbIVgM1mwkpNEBzixlNxfPmH7owvdE4973OM9quvk11dwvnzDUy/Zn5S5Ywpn/PeqXBQI2m4lna05CRtsI6+GIENjS9K4jWRHUGYA2ozdZm2Smmf0DI3aqpeNbsJfxe7YdFmcZAn5gXLCFa2/Umqiu017APFhMZ0rfQp4sJX0ZrJ+n9UtAljr5VYWb6oj1MrpvX3qe6u8WRJg0bj7aPkDOa7m+E0Oa9Y8eY/gbRbr+efH7hcO49bMd28fbDVHcUmm3XkozQGKjeBHSJ4TQnI879LIFmF2v/BJuEQlffJPfE9oKayS/PsPE44fvM4MsBESxbuEEV39d5pw6oW4vD6S1WQC3UpSbHNbK0Jikl0bphSs+0CGW8Ew4Kzw7zarmcVz873JHTFhKYay18R8vY0ozPiHPAGyROAqlW5fLj5+HbWBn9TpgekKsOy8N+4dlFfL9i/Nk3+gY1bwzZUAvLVNiFpvqHRenetSoVrgn2obGtPsltEVxEeHJAQFhyBIcHT9rDvTJm06e0TLgase2gd2RffGJNg0o1zrdRyi9s1bYE5bi85cK+o/nUwvBR5+jweEBaSMoCub29fEFISmib9Dn5yl5kVFpoGrPQSmZhafQ7WimttNCH7Cktohb6kFpoEfIsdDF7SgvZTbaY3mKFyLXQh+wpzWE3mUNQQkWnR+lgiW9Afkunej49Nz3sYlI8XFTRdkNhUR5d6h4oOpJc8OjcItMXVqoHW2fSW6ycWuiM8NDYoICouLAZ9BYrwwmhycvKlt4Q8hUlCV5nZ7vOm2ut2rizcFpuWpSrT3K1Z3xinbuHnXBTyGAljV/XzHZaNGu6y7vLDziMpIyUdOBBRXXlxznUQiOoheZsZk9njG8er1mNmz2eOCQ3x9BbLP+Zxt+VrbEz9aWxmimRvyl4/sumyoM/nw+LNzV4/uP0/9T/P5f08cvhl38USAS/xTYs2fL/VNFF0vd+SVsRB/khPwW4SCi5SHhx8fDgVPAiAqIJRQL/EuK5bsRzNnAiezD1i7u2VyHHAKRU+E2YUaA5DUsE2ZfbApAmsJcxjBwmQ2Xk4Y2BqZJ+oxSzsNEogz1O3/xkBMKEBHSiC8PoQStaoEIflPCHL/wQBCUKoUITlMhHP+olt5ojykUPOrEQTWgY+f449KMPKnSiB72jGrLQhEZO24925LtbkG5DndTkD2/4um+NQBEyUIJsRBxX24tcDGmbWtGJjq05jhzTaMgCcR+6EA4f+KAXDay5u6RsL7xJsm3w3mzvFvggB8nI/AYBdVnxNvx/2wA=') format('woff2');
}
@keyframes fade-in {
from { opacity: 0 }
}
.intro {
composes: abs-fill from '../../lib/util.scss';
display: grid;
grid-template-rows: 1fr min-content;
align-items: center;
background: rgba(255, 255, 255, 0.25);
text-align: center;
font-size: 2rem;
-webkit-overflow-scrolling: touch;
overflow: auto;
padding: 20px 0 0;
}
.logo-container {
position: relative;
padding-top: 100%;
}
.logo-sizer {
width: 90%;
max-width: 480px;
margin: 0 auto;
-webkit-appearance: none;
border: 1px solid var(--button-fg);
background: rgba(var(--button-fg-color), 0.1);
}
.logo {
composes: abs-fill from '../../lib/util.scss';
}
.open-image-guide {
font: 300 11vw intro-text;
margin-bottom: 0;
@media (min-width: 460px) {
font-size: 50.6px;
padding: 0 40px;
}
}
.select-button {
composes: unbutton from '../../lib/util.scss';
font-weight: 500;
color: #5D509E;
&:hover,
&:focus {
text-decoration: underline;
}
}
.hide {
display: none;
}
.demos {
display: block;
padding: 0;
border-top: 1px solid #e8e8e8;
margin: 0 auto;
@media (min-width: 400px) {
display: grid;
grid-template-columns: 1fr 1fr;
}
@media (min-width: 580px) {
border-top: none;
width: 523px;
}
@media (min-width: 900px) {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
width: 773px;
}
}
.demo-item {
background: #fff;
display: flex;
border-bottom: 1px solid #e8e8e8;
@media (min-width: 580px) {
border: 1px solid #e8e8e8;
border-radius: 4px;
margin: 3px;
}
}
.demo-button {
composes: unbutton from '../../lib/util.scss';
flex: 1;
&:hover,
&:focus {
background: #f5f5f5;
}
}
.demo {
display: flex;
align-items: center;
padding: 7px;
font-size: 1.3rem;
}
.demo-img-container {
overflow: hidden;
display: block;
width: 47px;
background: #ccc;
border-radius: 3px;
cursor: pointer;
flex: 0 0 auto;
}
.demo-img-aspect {
position: relative;
padding-top: 100%;
}
.demo-icon {
composes: abs-fill from '../../lib/util.scss';
}
.demo-description {
display: flex;
align-items: center;
justify-content: flex-start;
text-align: left;
flex: 1;
padding: 0 10px;
}
.demo-loading {
composes: abs-fill from '../../lib/util.scss';
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
animation: fade-in 300ms ease-in-out;
}
.demo-loading-spinner {
--color: #fff;
}
.related-links {
display: flex;
padding: 0;
justify-content: center;
font-size: 1.3rem;
& li {
display: block;
border-left: 1px solid #000;
padding: 0 0.6em;
&:first-child {
border-left: none;
}
}
& a:link {
color: #5D509E;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}

21
src/lib/util.scss Normal file
View File

@@ -0,0 +1,21 @@
.abs-fill {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.unbutton {
cursor: pointer;
background: none;
border: none;
font: inherit;
padding: 0;
margin: 0;
&:focus {
outline: none;
}
}

View File

@@ -1,22 +1,24 @@
// PRs to fix this:
// https://github.com/developit/preact/pull/1101
// https://github.com/developit/preact/pull/1102
declare namespace JSX {
type PointerEventHandler = EventHandler<PointerEvent>;
interface DOMAttributes {
onTouchStartCapture?: TouchEventHandler;
onTouchEndCapture?: TouchEventHandler;
onTouchMoveCapture?: TouchEventHandler;
onPointerDownCapture?: PointerEventHandler;
onMouseDownCapture?: MouseEventHandler;
onWheelCapture?: WheelEventHandler;
}
}
interface CanvasRenderingContext2D {
filter: string;
}
// Handling file-loader imports:
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.gif' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}

View File

@@ -13,6 +13,8 @@ html, body {
overflow: hidden;
overscroll-behavior: none;
contain: strict;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path d="M1 2V0h1v1H0v1z" fill-opacity=".025"/></svg>');
background-size: 20px 20px;
}
:root {

View File

@@ -156,6 +156,10 @@ module.exports = function (_, env) {
// See https://github.com/webpack/webpack/issues/6725
type: 'javascript/auto',
loader: 'file-loader'
},
{
test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader'
}
]
},