Compare commits

..

77 Commits

Author SHA1 Message Date
Jason Miller
9988505b2c asdf 2020-11-30 09:15:10 -05:00
Jake Archibald
0e6610c007 Move back btn 2020-11-27 16:42:53 +00:00
Jake Archibald
74477dfe6b Button size tweak 2020-11-27 15:34:57 +00:00
Jake Archibald
59713b7687 Back button 2020-11-27 15:20:40 +00:00
Jake Archibald
62102fecab two-up styles 2020-11-27 14:48:59 +00:00
Jake Archibald
b1fc4d1c57 Load demo img 2020-11-27 13:26:33 +00:00
Jake Archibald
02e0206285 Fix prerender SVG size 2020-11-27 11:49:10 +00:00
Jake Archibald
8f21972e70 Remove debug stuff 2020-11-27 11:37:20 +00:00
Jake Archibald
8d9ed5ad3e Responsive main section 2020-11-27 11:22:06 +00:00
Jake Archibald
be5a96a42a Responsive demo section 2020-11-27 11:06:31 +00:00
Jake Archibald
eeafef4e14 Replace thumbnails 2020-11-27 10:42:22 +00:00
Jake Archibald
067ce686b5 Tweak size 2020-11-27 10:03:40 +00:00
Jake Archibald
c78ad6c795 Home page with demo loading 2020-11-26 17:02:36 +00:00
Jake Archibald
be818caf48 Install button 2020-11-26 16:08:55 +00:00
Jake Archibald
4abc7f40d1 More CSS optimisation 2020-11-26 16:08:43 +00:00
Jake Archibald
903c8b5d02 Optimise amount of initial CSS 2020-11-26 15:40:36 +00:00
Jake Archibald
4c03ceee04 Footer 2020-11-26 15:13:35 +00:00
Jake Archibald
a2c465abdf Pause time while page is hidden 2020-11-26 14:47:08 +00:00
Jake Archibald
251dc2ce9b Get initial focus 2020-11-26 13:31:23 +00:00
Jake Archibald
e8948167db Fade into the center 2020-11-26 12:34:08 +00:00
Jake Archibald
9e1fb6dfb4 Background blobs 2020-11-26 11:38:50 +00:00
Jake Archibald
2d1a3b543a Get styles from CSS 2020-11-25 16:35:49 +00:00
Jake Archibald
4aaa5ffa78 lol 2020-11-25 15:54:35 +00:00
Jake Archibald
6fa13b919b Update canvas on resize if not updating every frame 2020-11-25 15:30:59 +00:00
Jake Archibald
fcef2b2d3e Initial blob shape 2020-11-25 15:13:53 +00:00
Jake Archibald
4d5761c780 Predictable initial blob position 2020-11-25 14:07:54 +00:00
Jake Archibald
be3ab8b6d1 Logo and animated blobs 2020-11-25 13:39:48 +00:00
Jake Archibald
49620e4c8f Paste button 2020-11-23 15:59:32 +00:00
Jake Archibald
3d1ecc1215 Don't restrict drag & drop to images (so it works with wp2 & JXL) 2020-11-23 14:23:21 +00:00
Jake Archibald
25fb1a9c80 Fix dev server config & redirect /editor 2020-11-23 14:20:41 +00:00
Ingvar Stepanyan
3ae1cf86f5 Autoformat staged C++ and Rust 2020-11-21 03:56:30 +00:00
Luca Versari
a699a5c4dc Fix Lossless+Progressive JXL.
Also limit the workaround for memory usage to large images.
2020-11-20 22:27:49 +00:00
Ingvar Stepanyan
613401c541 Add .gitattributes to fold generated files in PRs 2020-11-20 22:21:21 +00:00
Ingvar Stepanyan
f450373e3f Fix nightly Rust pinning; rebuild Oxi 2020-11-20 22:21:21 +00:00
Ingvar Stepanyan
750872aca6 Delete old oxipng files 2020-11-20 22:21:21 +00:00
Jason Miller
beaabe47dc Merge pull request #858 from GoogleChromeLabs/gh-actions
Switch from Travis to Github Actions
2020-11-20 17:08:51 -05:00
Ingvar Stepanyan
8f7369068c Remove sizereport step
Apparently it doesn't exist anymore, even though it was referenced in Travis.
2020-11-20 21:41:57 +00:00
Ingvar Stepanyan
10bfd60e20 Try to fix multiple OS 2020-11-20 21:39:18 +00:00
Ingvar Stepanyan
7f08348509 Delete .travis.yml 2020-11-20 21:37:38 +00:00
Ingvar Stepanyan
f77ddac652 Add Github Action 2020-11-20 21:37:00 +00:00
Jake Archibald
13631f1cfc Extra Wp2 Options (#853)
* wip

* wip

* Add extra options

* Even more options!

* Update src/features/encoders/wp2/client/index.tsx

Co-authored-by: Surma <surma@surma.dev>

Co-authored-by: Surma <surma@surma.dev>
2020-11-20 16:12:38 +00:00
Jake Archibald
f11e692d58 Unset loading on error. Fixes #855 2020-11-20 16:11:57 +00:00
Jake Archibald
f0221b626d Prettier ignore file 2020-11-20 11:51:43 +00:00
Jake Archibald
10c5ed0495 Don't prettify codec code 2020-11-20 10:48:41 +00:00
Ingvar Stepanyan
d945c79796 Use run-p for cross-platform parallel runs (#850) 2020-11-20 08:53:44 +00:00
Jake Archibald
30b628c1b9 Fixing windows build (#849)
Co-authored-by: Ingvar Stepanyan <rreverser@google.com>
2020-11-19 15:03:51 +00:00
Jake Archibald
6ebf94d1b6 Auto edge filter 2020-11-19 11:35:12 +00:00
Jake Archibald
a229662bed Change JXL defaults 2020-11-19 11:27:03 +00:00
Jake Archibald
e995b445ef Updating node version and lockfile 2020-11-19 11:20:04 +00:00
Jake Archibald
6da590c7d0 Merge branch 'rollup-build' into dev
# Conflicts:
#	_headers.ejs
#	codecs/oxipng/pkg/squoosh_oxipng_bg.js
#	src/codecs/avif/encoder.ts
#	src/codecs/oxipng/encoder.ts
#	src/codecs/processor.ts
#	src/codecs/util.ts
#	src/components/intro/imgs/logo.svg
#	src/missing-types.d.ts
#	webpack.config.js
2020-11-19 11:12:29 +00:00
Jake Archibald
56e10b3aa2 Rollup build 2020-11-19 11:00:23 +00:00
Ingvar Stepanyan
fd87ae7d2a Force-rebuild codecs with -O3
Follow-up to https://github.com/GoogleChromeLabs/squoosh/pull/838.

I have no idea what the JS+Wasm diffs in the original PR even represented if they weren't proper rebuilds, but this time I just removed node_modules in all codecs to enforce a proper, clean rebuild for each C++ codec.

(Still think the speed-ups are worth it.)
2020-11-02 17:37:35 +00:00
Ingvar Stepanyan
5df7dd7590 Update helper.Makefile 2020-11-02 13:54:41 +00:00
Ingvar Stepanyan
013946b137 Pass CODEC_DIR and LIBAOM_DIR via export
Slightly simpler than passing them in HELPER_MAKEFLAGS.
2020-11-02 13:54:41 +00:00
Ingvar Stepanyan
81c183b0d6 Restructure the AVIF directories
Change the way AVIF finds AOM from default ([avif source]/ext/aom) to custom paths. This allows us to avoid unpacking same archives into duplicate folders, and instead make multiple builds from the same source.
2020-11-02 13:54:41 +00:00
Ingvar Stepanyan
f523db6403 Try out new flags for building only AVIF encoder/decoder
See the discussion in https://github.com/AOMediaCodec/libavif/issues/254 where this was implemented.

This allows us to avoid using ERROR_ON_UNDEFINED_SYMBOLS and build a truly separate encoder/decoder libs.
2020-11-02 13:54:41 +00:00
Ingvar Stepanyan
cc6ea9e11c Switch to -O3 for C++ codecs 2020-11-02 12:46:12 +00:00
Cătălin Mariș
bd4b67037b Further optimize logo.svg (#761)
Co-authored-by: Jake Archibald <jaffathecake@gmail.com>
2020-10-15 15:48:56 +01:00
Ingvar Stepanyan
8c5c97e106 Remove obsolete @ts-ignore 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
a9d3bd71b5 Bump oxipng
Integrating some upstream fixes from my branch.
2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
0d0a9b4cdf Add COOP+COEP headers 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
f583770696 Explicitly disable HDR only for encoder 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
bae243ccdb Add feature detection to OxiPNG 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
02c113a68f Point oxipng to a patched version
Some upstream changes required for parallel build to work.
2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
600eead007 Disable parallel feature for non-parallel OxiPNG 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
05416768d5 Update oxipng build system 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
35d31f2324 Add some comments to explain Rust thread glue 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
82fadac70e Fixup import.meta in OxiPNG 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
47f9d22dd8 Switch to crossbeam-channel
Still not perfect due to usage of a static global, but this is much cleaner and more efficient thanks to proper blocking of Workers that wait for new messages instead of a manual spin-loop.
2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
9420dba3bc Parallel OxiPNG improvements
- Refactor to work around Chromium's issue with postMessage queuing. https://bugs.chromium.org/p/chromium/issues/detail?id=1075645
 - Convert codec code to TypeScript.
 - Make separate parallel and non-parallel builds.
 - Switch to nightly Rust for OxiPNG to allow parallel builds (but also reuse it for regular builds to avoid installing two toolchains).
2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
e462875807 Type fix for gesturestart event 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
0747d2c419 Rework fallback for postMessage issue
Now initialise all workers with module+memory separately, and then instead of using postMessage to send thread pointers, push them into a crossbeam-deque on the Rust side.

Rayon already depends on crossbeam-dequeue, so we're not even adding another dependency, and this model allows us to push "tasks" (thread pointers) on the main thread and pop them on worker threads in arbitrary order without sacrificing correctness.
2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
4c658b79ef OxiPNG + threads PoC 2020-10-07 20:42:48 +01:00
Ingvar Stepanyan
685558847f Multithread AVIF PoC 2020-10-07 20:42:48 +01:00
Trevor Manz
63ac34a662 Promisify emscripten modules & fix webp examples (#817) 2020-09-30 00:05:59 +01:00
Surma
42f9e4aed2 Merge pull request #828 from GoogleChromeLabs/create-dir
Ensure node_modules is created
2020-09-16 10:36:11 +01:00
Jake Archibald
e14790f0b9 Ensure node_modules is created 2020-09-16 10:24:20 +01:00
92 changed files with 2328 additions and 9294 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/codecs/**/*.js linguist-generated=true
/codecs/*/pkg*/*.d.ts linguist-generated=true

22
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Node.js CI
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- id: nvmrc
uses: browniebroke/read-nvmrc-action@v1
- uses: actions/setup-node@v1
with:
node-version: '${{ steps.nvmrc.outputs.node_version }}'
- run: npm ci
- run: npm run build

2
.nvmrc
View File

@@ -1 +1 @@
12.18.3
14.15.1

12
.prettierignore Normal file
View File

@@ -0,0 +1,12 @@
codecs
.tmp
node_modules
*.scss.d.ts
*.css.d.ts
build
*.o
# Auto-generated by lib/feature-plugin.js
src/features-worker/index.ts
src/client/lazy-app/worker-bridge/meta.ts
src/client/lazy-app/feature-meta/index.ts

View File

@@ -1,7 +0,0 @@
language: node_js
cache: npm
script: npm run build
after_success: npm run sizereport
os:
- linux
- windows

0
codecs/avif/enc/avif_enc_mt.wasm Executable file → Normal file
View File

View File

@@ -14,12 +14,33 @@ thread_local const val ImageData = val::global("ImageData");
// R, G, B, A
#define COMPONENTS_PER_PIXEL 4
#ifndef JXL_DEBUG_ON_ALL_ERROR
#define JXL_DEBUG_ON_ALL_ERROR 0
#endif
#if JXL_DEBUG_ON_ALL_ERROR
#define EXPECT_TRUE(a) \
if (!(a)) { \
fprintf(stderr, "Assertion failure (%d): %s\n", __LINE__, #a); \
return val::null(); \
}
#define EXPECT_EQ(a, b) \
{ \
int a_ = a; \
int b_ = b; \
if (a_ != b_) { \
fprintf(stderr, "Assertion failure (%d): %s (%d) != %s (%d)\n", __LINE__, #a, a_, #b, b_); \
return val::null(); \
} \
}
#else
#define EXPECT_TRUE(a) \
if (!(a)) { \
return val::null(); \
}
#define EXPECT_EQ(a, b) EXPECT_TRUE((a) == (b));
#endif
val decode(std::string data) {
std::unique_ptr<JxlDecoder,

Binary file not shown.

View File

@@ -38,7 +38,14 @@ val encode(std::string image, int width, int height, JXLOptions options) {
// Reduce memory usage of tree learning for lossless data.
// TODO(veluca93): this is a mitigation for excessive memory usage in the JXL encoder.
cparams.options.nb_repeats = 0.1;
float megapixels = width * height * 0.000001;
if (megapixels > 8) {
cparams.options.nb_repeats = 0.1;
} else if (megapixels > 4) {
cparams.options.nb_repeats = 0.3;
} else {
// default is OK.
}
float quality = options.quality;
@@ -59,8 +66,10 @@ val encode(std::string image, int width, int height, JXLOptions options) {
if (options.progressive) {
cparams.qprogressive_mode = true;
cparams.progressive_dc = 1;
cparams.responsive = 1;
if (!cparams.modular_mode) {
cparams.progressive_dc = 1;
}
}
if (cparams.modular_mode) {

Binary file not shown.

View File

@@ -247,18 +247,18 @@ checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
[[package]]
name = "libdeflate-sys"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e39efa87b84db3e13ff4e2dfac1e57220abcbd7fe8ec44d238f7f4f787cc1f"
checksum = "2f5b1582a0ebf8c55a46166c04d7c66f6bb17add3a6cbf69a082ac2219f31671"
dependencies = [
"cc",
]
[[package]]
name = "libdeflater"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4810980d791f26d470e2d7d91a3d4d22aa3a4b709fb7e9c5e43ee54f83a01f2"
checksum = "93edd93a53970951da84ef733a8b6e30189a8f8a9e19610f69e4cc5bb1f4d654"
dependencies = [
"libdeflate-sys",
]
@@ -359,9 +359,9 @@ checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
[[package]]
name = "oxipng"
version = "4.0.0"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea40b366cecfce76ee3b082e7e6567b82cdef75644a22442ca8584bc666ff4eb"
checksum = "9fefb26bde273c3db896a313151301a69e698a7495ee577fe2168ed7065c29c4"
dependencies = [
"bit-vec",
"byteorder",

4
codecs/oxipng/build.sh Executable file → Normal file
View File

@@ -3,6 +3,8 @@
set -e
rm -rf pkg,{-parallel}
wasm-pack build --target web
wasm-pack build -t web
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' wasm-pack build -t web -d pkg-parallel -- -Z build-std=panic_abort,std --features=parallel
# Workaround https://github.com/rustwasm/wasm-bindgen/issues/2133:
sed -i "s|maybe_memory:|maybe_memory?:|" pkg-parallel/squoosh_oxipng.d.ts
rm pkg{,-parallel}/.gitignore

View File

@@ -1,6 +1,6 @@
{
"name": "oxipng",
"scripts": {
"build": "RUST_IMG=rustlang/rust:8bb115b1090d ../build-rust.sh ./build.sh"
"build": "RUST_IMG=rustlang/rust@sha256:744aeea5a38f95aa7a96ec37269a65f0c6197a1cdd87d6534e12bb869141d807 ../build-rust.sh ./build.sh"
}
}

View File

@@ -1,29 +1,24 @@
/* tslint:disable */
/* eslint-disable */
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data: Uint8Array, level: number): Uint8Array;
/**
* @param {number} num
* @returns {any}
*/
* @param {number} num
* @returns {any}
*/
export function worker_initializer(num: number): any;
/**
*/
*/
export function start_main_thread(): void;
/**
*/
*/
export function start_worker_thread(): void;
export type InitInput =
| RequestInfo
| URL
| Response
| BufferSource
| WebAssembly.Module;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly optimise: (a: number, b: number, c: number, d: number) => void;
@@ -39,15 +34,13 @@ export interface InitOutput {
}
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
* @param {WebAssembly.Memory} maybe_memory
*
* @returns {Promise<InitOutput>}
*/
export default function init(
module_or_path?: InitInput | Promise<InitInput>,
maybe_memory?: WebAssembly.Memory,
): Promise<InitOutput>;
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
* @param {WebAssembly.Memory} maybe_memory
*
* @returns {Promise<InitOutput>}
*/
export default function init (module_or_path?: InitInput | Promise<InitInput>, maybe_memory?: WebAssembly.Memory): Promise<InitOutput>;

View File

@@ -1,3 +1,4 @@
let wasm;
let memory;
@@ -8,189 +9,172 @@ heap.push(undefined, null, true, false);
let heap_next = heap.length;
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
heap[idx] = obj;
return idx;
}
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
});
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.__wbindgen_export_0.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetUint8Memory0;
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.__wbindgen_export_0.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().slice(ptr, ptr + len));
return cachedTextDecoder.decode(getUint8Memory0().slice(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.__wbindgen_export_0.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetInt32Memory0;
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.__wbindgen_export_0.buffer) {
cachegetInt32Memory0 = new Int32Array(wasm.__wbindgen_export_0.buffer);
}
return cachegetInt32Memory0;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data, level) {
try {
const retptr = wasm.__wbindgen_export_1.value - 16;
wasm.__wbindgen_export_1.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_1.value += 16;
}
try {
const retptr = wasm.__wbindgen_export_1.value - 16;
wasm.__wbindgen_export_1.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_1.value += 16;
}
}
function getObject(idx) {
return heap[idx];
}
function getObject(idx) { return heap[idx]; }
function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
const ret = getObject(idx);
dropObject(idx);
return ret;
}
/**
* @param {number} num
* @returns {any}
*/
* @param {number} num
* @returns {any}
*/
export function worker_initializer(num) {
var ret = wasm.worker_initializer(num);
return takeObject(ret);
var ret = wasm.worker_initializer(num);
return takeObject(ret);
}
/**
*/
*/
export function start_main_thread() {
wasm.start_main_thread();
wasm.start_main_thread();
}
/**
*/
*/
export function start_worker_thread() {
wasm.start_worker_thread();
wasm.start_worker_thread();
}
async function load(module, imports, maybe_memory) {
if (typeof Response === 'function' && module instanceof Response) {
memory = imports.wbg.memory = new WebAssembly.Memory({
initial: 17,
maximum: 16384,
shared: true,
});
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn(
'`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n',
e,
);
} else {
throw e;
if (typeof Response === 'function' && module instanceof Response) {
memory = imports.wbg.memory = new WebAssembly.Memory({initial:17,maximum:16384,shared:true});
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
memory = imports.wbg.memory = maybe_memory;
const instance = await WebAssembly.instantiate(module, imports);
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
memory = imports.wbg.memory = maybe_memory;
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
}
async function init(input, maybe_memory) {
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_module = function () {
var ret = init.__wbindgen_wasm_module;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_memory = function () {
var ret = wasm.__wbindgen_export_0;
return addHeapObject(ret);
};
imports.wbg.__wbg_of_6510501edc06d65e = function (arg0, arg1) {
var ret = Array.of(takeObject(arg0), takeObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_module = function() {
var ret = init.__wbindgen_wasm_module;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_memory = function() {
var ret = wasm.__wbindgen_export_0;
return addHeapObject(ret);
};
imports.wbg.__wbg_of_6510501edc06d65e = function(arg0, arg1) {
var ret = Array.of(takeObject(arg0), takeObject(arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input);
}
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports, maybe_memory);
const { instance, module } = await load(await input, imports, maybe_memory);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
wasm.__wbindgen_start();
return wasm;
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
wasm.__wbindgen_start();
return wasm;
}
export default init;

View File

@@ -1,18 +1,13 @@
/* tslint:disable */
/* eslint-disable */
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data: Uint8Array, level: number): Uint8Array;
export type InitInput =
| RequestInfo
| URL
| Response
| BufferSource
| WebAssembly.Module;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
@@ -24,13 +19,12 @@ export interface InitOutput {
}
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init(
module_or_path?: InitInput | Promise<InitInput>,
): Promise<InitOutput>;
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;

View File

@@ -1,126 +1,118 @@
let wasm;
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
});
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory0;
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory0;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
* @param {Uint8Array} data
* @param {number} level
* @returns {Uint8Array}
*/
export function optimise(data, level) {
try {
const retptr = wasm.__wbindgen_export_0.value - 16;
wasm.__wbindgen_export_0.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_0.value += 16;
}
try {
const retptr = wasm.__wbindgen_export_0.value - 16;
wasm.__wbindgen_export_0.value = retptr;
var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
var len0 = WASM_VECTOR_LEN;
wasm.optimise(retptr, ptr0, len0, level);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_export_0.value += 16;
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn(
'`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n',
e,
);
} else {
throw e;
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
}
async function init(input) {
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
if (typeof input === 'undefined') {
input = import.meta.url.replace(/\.js$/, '_bg.wasm');
}
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input);
}
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports);
const { instance, module } = await load(await input, imports);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
return wasm;
return wasm;
}
export default init;

View File

@@ -1,5 +1,5 @@
use wasm_bindgen::prelude::*;
use oxipng::AlphaOptim;
use wasm_bindgen::prelude::*;
mod malloc_shim;
@@ -8,14 +8,14 @@ pub mod parallel;
#[wasm_bindgen]
pub fn optimise(data: &[u8], level: u8) -> Vec<u8> {
let mut options = oxipng::Options::from_preset(level);
options.alphas.insert(AlphaOptim::Black);
options.alphas.insert(AlphaOptim::White);
options.alphas.insert(AlphaOptim::Up);
options.alphas.insert(AlphaOptim::Down);
options.alphas.insert(AlphaOptim::Left);
options.alphas.insert(AlphaOptim::Right);
let mut options = oxipng::Options::from_preset(level);
options.alphas.insert(AlphaOptim::Black);
options.alphas.insert(AlphaOptim::White);
options.alphas.insert(AlphaOptim::Up);
options.alphas.insert(AlphaOptim::Down);
options.alphas.insert(AlphaOptim::Left);
options.alphas.insert(AlphaOptim::Right);
options.deflate = oxipng::Deflaters::Libdeflater;
oxipng::optimize_from_memory(data, &options).unwrap_throw()
options.deflate = oxipng::Deflaters::Libdeflater;
oxipng::optimize_from_memory(data, &options).unwrap_throw()
}

View File

@@ -1,4 +1,4 @@
use crossbeam_channel::{Sender, Receiver, bounded};
use crossbeam_channel::{bounded, Receiver, Sender};
use once_cell::sync::OnceCell;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
@@ -35,7 +35,8 @@ extern "C" {
// shared memory and blocks the current thread until they're all grabbed.
// 4) Provide a `worker_initializer` that is expected to be invoked from various workers,
// reads one `threadPtr` from the shared channel and starts running it.
static CHANNEL: OnceCell<(Sender<rayon::ThreadBuilder>, Receiver<rayon::ThreadBuilder>)> = OnceCell::new();
static CHANNEL: OnceCell<(Sender<rayon::ThreadBuilder>, Receiver<rayon::ThreadBuilder>)> =
OnceCell::new();
#[wasm_bindgen]
pub fn worker_initializer(num: usize) -> JsValue {

View File

@@ -7,7 +7,7 @@ RUN wget -qO- https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/was
FROM $RUST_IMG AS rust
ARG RUST_IMG
RUN rustup target add wasm32-unknown-unknown
RUN if [[ $RUST_IMG = rustlang/rust:* ]] ; then rustup component add rust-src ; fi
RUN case $RUST_IMG in rustlang/rust@*) rustup component add rust-src; esac
COPY --from=wasm-tools /emsdk/upstream/bin/wasm-opt /emsdk/upstream/bin/clang /usr/local/bin/
COPY --from=wasm-tools /emsdk/upstream/lib/ /usr/local/lib/
COPY --from=wasm-tools /emsdk/upstream/emscripten/system/include/libc/ /wasm32/include/

View File

@@ -17,7 +17,10 @@ val decode(std::string buffer) {
int width, height;
std::unique_ptr<uint8_t[]> rgba(
WebPDecodeRGBA((const uint8_t*)buffer.c_str(), buffer.size(), &width, &height));
return rgba ? ImageData.new_(Uint8ClampedArray.new_(typed_memory_view(width * height * 4, rgba.get())), width, height) : val::null();
return rgba ? ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(width * height * 4, rgba.get())),
width, height)
: val::null();
}
EMSCRIPTEN_BINDINGS(my_module) {

View File

@@ -7,7 +7,31 @@ using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string image_in, int image_width, int image_height, WP2::EncoderConfig config) {
struct WP2Options {
float quality;
float alpha_quality;
int speed;
int pass;
int uv_mode;
float sns;
int csp_type;
int error_diffusion;
bool use_random_matrix;
};
val encode(std::string image_in, int image_width, int image_height, WP2Options options) {
WP2::EncoderConfig config = {};
config.quality = options.quality;
config.alpha_quality = options.alpha_quality;
config.speed = options.speed;
config.pass = options.pass;
config.uv_mode = static_cast<WP2::EncoderConfig::UVMode>(options.uv_mode);
config.csp_type = static_cast<WP2::Csp>(options.csp_type);
config.sns = options.sns;
config.error_diffusion = options.error_diffusion;
config.use_random_matrix = options.use_random_matrix;
uint8_t* image_buffer = (uint8_t*)image_in.c_str();
WP2::ArgbBuffer src = WP2::ArgbBuffer();
WP2Status status =
@@ -27,12 +51,16 @@ val encode(std::string image_in, int image_width, int image_height, WP2::Encoder
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<WP2::EncoderConfig>("WP2EncoderConfig")
.field("quality", &WP2::EncoderConfig::quality)
.field("alpha_quality", &WP2::EncoderConfig::alpha_quality)
.field("speed", &WP2::EncoderConfig::speed)
.field("pass", &WP2::EncoderConfig::pass)
.field("sns", &WP2::EncoderConfig::sns);
value_object<WP2Options>("WP2Options")
.field("quality", &WP2Options::quality)
.field("alpha_quality", &WP2Options::alpha_quality)
.field("speed", &WP2Options::speed)
.field("pass", &WP2Options::pass)
.field("uv_mode", &WP2Options::uv_mode)
.field("csp_type", &WP2Options::csp_type)
.field("error_diffusion", &WP2Options::error_diffusion)
.field("use_random_matrix", &WP2Options::use_random_matrix)
.field("sns", &WP2Options::sns);
function("encode", &encode);
}

View File

@@ -4,6 +4,24 @@ export interface EncodeOptions {
speed: number;
pass: number;
sns: number;
uv_mode: UVMode;
csp_type: Csp;
error_diffusion: number;
use_random_matrix: boolean;
}
export const enum UVMode {
UVModeAdapt = 0, // Mix of 420 and 444 (per block)
UVMode420, // All blocks 420
UVMode444, // All blocks 444
UVModeAuto, // Choose any of the above automatically
}
export const enum Csp {
kYCoCg,
kYCbCr,
kCustom,
kYIQ,
}
export interface WP2Module extends EmscriptenWasm.Module {

View File

@@ -576,7 +576,7 @@ var wp2_enc = (function () {
},
});
var ob = {
p: function (a, b, c, d) {
t: function (a, b, c, d) {
A(
'Assertion failed: ' +
C(a) +
@@ -807,7 +807,7 @@ var wp2_enc = (function () {
return [];
});
},
c: function (a, b, c, d, e) {
d: function (a, b, c, d, e) {
function g(l) {
return l;
}
@@ -1000,10 +1000,10 @@ var wp2_enc = (function () {
},
});
},
q: function (a, b, c, d, e, g) {
p: function (a, b, c, d, e, g) {
Fa[a] = { name: U(b), da: Y(c, d), ea: Y(e, g), U: [] };
},
e: function (a, b, c, d, e, g, m, h, k, l) {
c: function (a, b, c, d, e, g, m, h, k, l) {
Fa[a].U.push({
X: U(b),
aa: c,
@@ -1034,7 +1034,7 @@ var wp2_enc = (function () {
m: function (a) {
4 < a && (X[a].T += 1);
},
t: function (a, b, c, d) {
q: function (a, b, c, d) {
a || W('Cannot use deleted val. handle = ' + a);
a = X[a].value;
var e = ib[b];
@@ -1079,7 +1079,7 @@ var wp2_enc = (function () {
u: function (a, b, c) {
D.copyWithin(a, b, b + c);
},
d: function (a) {
e: function (a) {
a >>>= 0;
var b = D.length;
if (2147483648 < a) return !1;

Binary file not shown.

View File

@@ -11,6 +11,7 @@
* limitations under the License.
*/
import rollup from 'rollup';
import * as path from 'path';
const prefix = 'client-bundle:';
const entryPathPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ENTRY_PATH';
@@ -119,9 +120,10 @@ export default function (inputOptions, outputOptions, resolveFileUrl) {
return;
}
const id = entryPointPlaceholderMap.get(num);
const id = path.normalize(entryPointPlaceholderMap.get(num));
const clientEntry = clientOutput.find(
(item) => item.facadeModuleId === id,
(item) =>
item.facadeModuleId && path.normalize(item.facadeModuleId) === id,
);
if (property.startsWith(entryPathPlaceholder)) {

View File

@@ -18,6 +18,8 @@ import {
resolve as resolvePath,
dirname,
normalize as nomalizePath,
sep as pathSep,
posix,
} from 'path';
import postcss from 'postcss';
@@ -39,6 +41,7 @@ const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g');
const appendCssModule = '\0appendCss';
const appendCssSource = `
export default function appendCss(css) {
if (__PRERENDER__) return;
const style = document.createElement('style');
style.textContent = css;
document.head.append(style);
@@ -172,14 +175,15 @@ export default function (resolveFileUrl) {
return `export default ${cssStr};`;
}
if (id.startsWith(addPrefix)) {
const path = id.slice(addPrefix.length);
const path = nomalizePath(id.slice(addPrefix.length));
return (
`import css from 'css:${path}';\n` +
`import css from ${JSON.stringify('css:' + path)};\n` +
`import appendCss from '${appendCssModule}';\n` +
`appendCss(css);\n`
);
}
if (id.endsWith(moduleSuffix)) {
const path = nomalizePath(id.slice(0, -moduleSuffix.length));
if (!pathToResult.has(id)) {
throw Error(`Cannot find ${id} in pathToResult`);
}

View File

@@ -19,17 +19,18 @@ import glob from 'glob';
const globP = promisify(glob);
const moduleId = 'initial-css:';
const initialCssModule = '\0initialCss';
export default function initialCssPlugin() {
return {
name: 'initial-css-plugin',
resolveId(id) {
if (id === moduleId) return moduleId;
if (id === moduleId) return initialCssModule;
},
async load(id) {
if (id !== moduleId) return;
if (id !== initialCssModule) return;
const matches = await globP('shared/initial-app/**/*.css', {
const matches = await globP('shared/prerendered-app/**/*.css', {
nodir: true,
cwd: path.join(process.cwd(), 'src'),
});

View File

@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { posix as pathUtils } from 'path';
import { posix as pathUtils, isAbsolute } from 'path';
export default function resolveDirs(paths) {
const pathBaseDir = paths.map((path) => [
@@ -30,6 +30,7 @@ export default function resolveDirs(paths) {
if (!resolveResult) {
throw new Error(`Couldn't find ${'./' + id}`);
}
if (isAbsolute(resolveResult.id)) return resolveResult.id;
return pathUtils.resolve(resolveResult.id);
},
};

View File

@@ -127,15 +127,6 @@ export default function simpleTS(mainPath, { noBuild, watch } = {}) {
relative(process.cwd(), id),
).replace(extRe, '.js');
console.log(
`simple-ts mapping`,
id,
'to',
newId,
'with outDir',
config.options.outDir,
);
return fsp.readFile(newId, { encoding: 'utf8' });
},
};

8538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,9 @@
"scripts": {
"build": "rollup -c && node lib/move-output.js",
"debug": "node --inspect-brk node_modules/.bin/rollup -c",
"dev": "rollup -cw & npm run serve",
"serve": "serve --config server.json .tmp/build/static"
"dev": "run-p watch serve",
"watch": "rollup -cw",
"serve": "serve --config ../../../serve.json .tmp/build/static"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^15.1.0",
@@ -25,6 +26,7 @@
"lint-staged": "^10.5.1",
"lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.27",
"npm-run-all": "^4.1.5",
"pointer-tracker": "^2.4.0",
"postcss": "^7.0.35",
"postcss-modules": "^3.2.2",
@@ -47,9 +49,9 @@
}
},
"lint-staged": {
"*.{js,css,json,md,ts,tsx}": [
"prettier --write"
]
"*.{js,css,json,md,ts,tsx}": "prettier --write",
"*.{c,h,cpp,hpp}": "clang-format -i",
"*.rs": "rustfmt"
},
"dependencies": {
"wasm-feature-detect": "^1.2.9"

View File

@@ -52,7 +52,7 @@ function jsFileName(chunkInfo) {
const parsedPath = path.parse(chunkInfo.facadeModuleId);
if (parsedPath.name !== 'index') return jsPath;
// Come up with a better name than 'index'
const name = parsedPath.dir.split('/').slice(-1);
const name = parsedPath.dir.split(/\\|\//).slice(-1);
return jsPath.replace('[name]', name);
}

View File

@@ -9,5 +9,6 @@
}
]
}
]
],
"redirects": [{ "source": "/editor", "destination": "/" }]
}

View File

@@ -1,16 +1,16 @@
import type { FileDropEvent } from 'file-drop-element';
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
import type { SnackOptions } from 'shared/initial-app/custom-els/snack-bar';
import type SnackBarElement from 'shared/custom-els/snack-bar';
import type { SnackOptions } from 'shared/custom-els/snack-bar';
import { h, Component } from 'preact';
import { linkRef } from 'shared/initial-app/util';
import { linkRef } from 'shared/prerendered-app/util';
import * as style from './style.css';
import 'add-css:./style.css';
import 'file-drop-element';
import 'shared/initial-app/custom-els/snack-bar';
import Intro from 'shared/initial-app/Intro';
import 'shared/initial-app/custom-els/loading-spinner';
import 'shared/custom-els/snack-bar';
import Intro from 'shared/prerendered-app/Intro';
import 'shared/custom-els/loading-spinner';
const ROUTE_EDITOR = '/editor';
@@ -67,7 +67,7 @@ export default class App extends Component<Props, State> {
// really breaks things on Squoosh, as you can easily end up zooming the UI when you mean to
// zoom the image. Once you've done this, it's really difficult to undo. Anyway, this seems to
// prevent it.
document.body.addEventListener('gesturestart', (event) => {
document.body.addEventListener('gesturestart', (event: any) => {
event.preventDefault();
});
@@ -115,11 +115,7 @@ export default class App extends Component<Props, State> {
return (
<div class={style.app}>
<file-drop
accept="image/*"
onfiledrop={this.onFileDrop}
class={style.drop}
>
<file-drop onfiledrop={this.onFileDrop} class={style.drop}>
{showSpinner ? (
<loading-spinner class={style.appLoader} />
) : isEditorOpen ? (

View File

@@ -1,5 +1,5 @@
/// <reference path="../../../shared/initial-app/custom-els/snack-bar/missing-types.d.ts" />
/// <reference path="../../../shared/initial-app/custom-els/loading-spinner/missing-types.d.ts" />
/// <reference path="../../../shared/custom-els/snack-bar/missing-types.d.ts" />
/// <reference path="../../../shared/custom-els/loading-spinner/missing-types.d.ts" />
import type { FileDropElement, FileDropEvent } from 'file-drop-element';
interface FileDropAttributes extends preact.JSX.HTMLAttributes {

View File

@@ -3,7 +3,7 @@ import * as style from './style.css';
import 'add-css:./style.css';
import RangeInputElement from './custom-els/RangeInput';
import './custom-els/RangeInput';
import { linkRef } from 'shared/initial-app/util';
import { linkRef } from 'shared/prerendered-app/util';
interface Props extends preact.JSX.HTMLAttributes {}
interface State {}

View File

@@ -73,9 +73,14 @@ export default class TwoUp extends HTMLElement {
connectedCallback() {
this._childrenChange();
this._handle.innerHTML = `<div class="${
styles.scrubber
}">${`<svg viewBox="0 0 27 20" fill="currentColor">${'<path d="M17 19.2l9.5-9.6L16.9 0zM9.6 0L0 9.6l9.6 9.6z"/>'}</svg>`}</div>`;
// prettier-ignore
this._handle.innerHTML =
`<div class="${styles.scrubber}">${
`<svg viewBox="0 0 27 20">${
`<path class="${styles.arrowLeft}" d="M9.6 0L0 9.6l9.6 9.6z"/>` +
`<path class="${styles.arrowRight}" d="M17 19.2l9.5-9.6L16.9 0z"/>`
}</svg>
`}</div>`;
if (!this._everConnected) {
this._resetPosition();

View File

@@ -2,12 +2,11 @@ two-up {
display: grid;
position: relative;
--split-point: 0;
--accent-color: #777;
--track-color: var(--accent-color);
--thumb-background: #fff;
--track-color: rgb(0 0 0 / 0.6);
--thumb-background: var(--black);
--thumb-color: var(--accent-color);
--thumb-size: 62px;
--bar-size: 6px;
--bar-size: 9px;
--bar-touch-size: 30px;
}
@@ -37,8 +36,6 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
height: 100%;
width: var(--bar-size);
margin: 0 auto;
box-shadow: inset calc(var(--bar-size) / 2) 0 0 rgba(0, 0, 0, 0.1),
0 1px 4px rgba(0, 0, 0, 0.4);
background: var(--track-color);
}
@@ -47,14 +44,11 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
position: absolute;
top: 50%;
left: 50%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%);
width: var(--thumb-size);
height: calc(var(--thumb-size) * 0.9);
background: var(--thumb-background);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: var(--thumb-size);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
color: var(--thumb-color);
box-sizing: border-box;
padding: 0 calc(var(--thumb-size) * 0.24);
@@ -64,6 +58,14 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
flex: 1;
}
.arrow-left {
fill: var(--pink);
}
.arrow-right {
fill: var(--blue);
}
two-up[orientation='vertical'] .two-up-handle {
width: auto;
height: var(--bar-touch-size);

View File

@@ -10,15 +10,15 @@ import {
ToggleBackgroundIcon,
AddIcon,
RemoveIcon,
BackIcon,
ToggleBackgroundActiveIcon,
RotateIcon,
MoreIcon,
} from '../../icons';
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
import type { PreprocessorState } from '../../feature-meta';
import { cleanSet } from '../../util/clean-modify';
import type { SourceImage } from '../../Compress';
import { linkRef } from 'shared/initial-app/util';
import { linkRef } from 'shared/prerendered-app/util';
interface Props {
source?: SourceImage;
@@ -28,7 +28,6 @@ interface Props {
rightCompressed?: ImageData;
leftImgContain: boolean;
rightImgContain: boolean;
onBack: () => void;
onPreprocessorChange: (newState: PreprocessorState) => void;
}
@@ -36,6 +35,7 @@ interface State {
scale: number;
editingScale: boolean;
altBackground: boolean;
menuOpen: boolean;
}
const scaleToOpts: ScaleToOpts = {
@@ -50,6 +50,7 @@ export default class Output extends Component<Props, State> {
scale: 1,
editingScale: false,
altBackground: false,
menuOpen: false,
};
canvasLeft?: HTMLCanvasElement;
canvasRight?: HTMLCanvasElement;
@@ -162,6 +163,10 @@ export default class Output extends Component<Props, State> {
this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
};
private toggleMenu = () => {
this.setState(({ menuOpen }) => ({ menuOpen: !menuOpen }));
};
private onRotateClick = () => {
const { preprocessorState: inputProcessorState } = this.props;
if (!inputProcessorState) return;
@@ -255,8 +260,8 @@ export default class Output extends Component<Props, State> {
};
render(
{ mobileView, leftImgContain, rightImgContain, source, onBack }: Props,
{ scale, editingScale, altBackground }: State,
{ mobileView, leftImgContain, rightImgContain, source }: Props,
{ scale, editingScale, altBackground, menuOpen }: State,
) {
const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable();
@@ -314,12 +319,6 @@ export default class Output extends Component<Props, State> {
</pinch-zoom>
</two-up>
<div class={style.back}>
<button class={style.button} onClick={onBack}>
<BackIcon />
</button>
</div>
<div class={style.controls}>
<div class={style.zoomControls}>
<button class={style.button} onClick={this.zoomOut}>
@@ -349,26 +348,34 @@ export default class Output extends Component<Props, State> {
<button class={style.button} onClick={this.zoomIn}>
<AddIcon />
</button>
</div>
<div class={style.buttonsNoWrap}>
<button
class={style.button}
onClick={this.onRotateClick}
title="Rotate image"
class={`${style.button} ${style.moreButton} ${
menuOpen ? style.open : ''
}`}
onClick={this.toggleMenu}
>
<RotateIcon />
</button>
<button
class={`${style.button} ${altBackground ? style.active : ''}`}
onClick={this.toggleBackground}
title="Change canvas color"
>
{altBackground ? (
<ToggleBackgroundActiveIcon />
) : (
<ToggleBackgroundIcon />
)}
<MoreIcon />
</button>
<aside class={`${style.menu} ${menuOpen ? style.open : ''}`}>
<button
class={style.button}
onClick={this.onRotateClick}
title="Rotate image"
>
<RotateIcon />
</button>
<button
class={`${style.button} ${altBackground ? style.active : ''}`}
onClick={this.toggleBackground}
title="Change canvas color"
>
{altBackground ? (
<ToggleBackgroundActiveIcon />
) : (
<ToggleBackgroundIcon />
)}
</button>
</aside>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
.output {
composes: abs-fill from '../../../../shared/initial-app/util.css';
composes: abs-fill from global;
&::before {
content: '';
@@ -19,12 +19,12 @@
}
.two-up {
composes: abs-fill from '../../../../shared/initial-app/util.css';
composes: abs-fill from global;
--accent-color: var(--button-fg);
}
.pinch-zoom {
composes: abs-fill from '../../../../shared/initial-app/util.css';
composes: abs-fill from global;
outline: none;
display: flex;
justify-content: center;
@@ -54,6 +54,7 @@
/* Allow clicks to fall through to the pinch zoom area */
pointer-events: none;
& > * {
pointer-events: auto;
}
@@ -76,6 +77,7 @@
border-bottom-left-radius: 0;
margin-left: 0;
}
& :not(:last-child) {
margin-right: 0;
border-right-width: 0;
@@ -90,46 +92,97 @@
align-items: center;
box-sizing: border-box;
margin: 4px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 5px;
background-color: rgba(29, 29, 29, 0.92);
border: 1px solid rgba(0, 0, 0, 0.67);
border-radius: 6px;
line-height: 1;
white-space: nowrap;
height: 36px;
height: 39px;
padding: 0 8px;
cursor: pointer;
/*
@media (min-width: 600px) {
height: 48px;
height: 39px;
padding: 0 16px;
}
*/
&:focus {
box-shadow: 0 0 0 2px var(--button-fg);
box-shadow: 0 0 0 2px #fff;
outline: none;
z-index: 1;
}
}
.button {
color: var(--button-fg);
color: #fff;
&:hover {
background-color: #eee;
/* background-color: #eee; */
background: rgba(40, 40, 40, 0.92);
}
&.active {
background: #34b9eb;
background: rgba(72, 72, 72, 0.92);
color: #fff;
/*
&:hover {
background: #32a3ce;
background: rgba(72, 72, 72, 0.92);
}
*/
}
}
.moreButton {
padding: 0 4px;
&.open {
background: rgba(72, 72, 72, 0.92);
}
}
.menu {
display: inline-block;
position: absolute;
right: 0;
bottom: 100%;
z-index: 100;
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: flex-start;
display: none;
&.open {
display: block;
animation: menuOpen 500ms ease forwards 1;
}
button {
margin-top: 8px;
margin-bottom: 8px;
border-radius: 1.2rem;
}
h5 {
text-transform: uppercase;
font-size: 0.8rem;
color: #fff;
margin: 8px 4px;
padding: 10px 0 0;
}
}
@keyframes menuOpen {
0% {
transform: translateY(10px) scale(0.8);
}
}
.zoom {
color: #625e80;
color: #939393;
cursor: text;
width: 6em;
font: inherit;
@@ -137,7 +190,7 @@
justify-content: center;
&:focus {
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2), 0 0 0 2px var(--button-fg);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2), 0 0 0 2px #fff;
}
}
@@ -145,17 +198,10 @@
position: relative;
top: 1px;
margin: 0 3px 0 0;
color: #888;
color: #fff;
border-bottom: 1px dashed #999;
}
.back {
position: absolute;
top: 0;
left: 0;
padding: 9px;
}
.buttons-no-wrap {
display: flex;
pointer-events: none;

View File

@@ -8,7 +8,7 @@ import {
CopyAcrossIcon,
CopyAcrossIconProps,
} from 'client/lazy-app/icons';
import 'shared/initial-app/custom-els/loading-spinner';
import 'shared/custom-els/loading-spinner';
import { SourceImage } from '../';
interface Props {

View File

@@ -124,7 +124,7 @@
.copy-to-other {
grid-row: 1;
grid-column: copy-button;
composes: unbutton from '../../../../shared/initial-app/util.css';
composes: unbutton from global;
composes: download;
background: #656565;

View File

@@ -30,7 +30,7 @@ import './custom-els/MultiPanel';
import Results from './Results';
import WorkerBridge from '../worker-bridge';
import { resize } from 'features/processors/resize/client';
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
import type SnackBarElement from 'shared/custom-els/snack-bar';
import { CopyAcrossIconProps, ExpandIcon } from '../icons';
export type OutputType = EncoderType | 'identity';
@@ -649,6 +649,7 @@ export default class Compress extends Component<Props, State> {
});
} catch (err) {
if (err.name === 'AbortError') return;
this.setState({ loading: false });
this.props.showSnack(`Preprocessing error: ${err}`);
throw err;
}
@@ -772,6 +773,12 @@ export default class Compress extends Component<Props, State> {
this.activeSideJobs[sideIndex] = undefined;
} catch (err) {
if (err.name === 'AbortError') return;
this.setState((currentState) => {
const sides = cleanMerge(currentState.sides, sideIndex, {
loading: false,
});
return { sides };
});
this.props.showSnack(`Processing error: ${err}`);
throw err;
}
@@ -855,10 +862,22 @@ export default class Compress extends Component<Props, State> {
rightCompressed={rightImageData}
leftImgContain={leftImgContain}
rightImgContain={rightImgContain}
onBack={onBack}
preprocessorState={preprocessorState}
onPreprocessorChange={this.onPreprocessorChange}
/>
<button class={style.back} onClick={onBack}>
<svg viewBox="0 0 61 53.3">
<title>Back</title>
<path
class={style.backBlob}
d="M0 25.6c-.5-7.1 4.1-14.5 10-19.1S23.4.1 32.2 0c8.8 0 19 1.6 24.4 8s5.6 17.8 1.7 27a29.7 29.7 0 01-20.5 18c-8.4 1.5-17.3-2.6-24.5-8S.5 32.6.1 25.6z"
/>
<path
class={style.backX}
d="M41.6 17.1l-2-2.1-8.3 8.2-8.2-8.2-2 2 8.2 8.3-8.3 8.2 2.1 2 8.2-8.1 8.3 8.2 2-2-8.2-8.3z"
/>
</svg>
</button>
{mobileView ? (
<div class={style.options}>
<multi-panel class={style.multiPanel} open-one-only>

View File

@@ -73,3 +73,24 @@
:focus .expand-icon {
fill: #34b9eb;
}
.back {
composes: unbutton from global;
position: absolute;
top: var(--dist);
left: var(--dist);
--dist: 14px;
& > svg {
width: 58px;
}
}
.back-blob {
fill: var(--hot-pink);
opacity: 0.77;
}
.back-x {
fill: var(--white);
}

View File

@@ -37,6 +37,14 @@ export const RotateIcon = (props: preact.JSX.HTMLAttributes) => (
</Icon>
);
export const MoreIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<circle cx="12" cy="6" r="2" fill="#fff" />
<circle cx="12" cy="12" r="2" fill="#fff" />
<circle cx="12" cy="18" r="2" fill="#fff" />
</Icon>
);
export const AddIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
@@ -67,12 +75,6 @@ export const ExpandIcon = (props: preact.JSX.HTMLAttributes) => (
</Icon>
);
export const BackIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M20 11H7.8l5.6-5.6L12 4l-8 8 8 8 1.4-1.4L7.8 13H20v-2z" />
</Icon>
);
const copyAcrossRotations = {
up: 90,
right: 180,

View File

@@ -1,4 +1,4 @@
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
import type SnackBarElement from 'shared/custom-els/snack-bar';
import { get, set } from 'idb-keyval';

View File

@@ -11,7 +11,7 @@
* limitations under the License.
*/
/// <reference path="../../missing-types.d.ts" />
/// <reference path="../shared/initial-app/Intro/missing-types.d.ts" />
/// <reference path="../shared/prerendered-app/Intro/missing-types.d.ts" />
interface Navigator {
readonly standalone: boolean;

View File

@@ -315,9 +315,9 @@ export class Options extends Component<Props, State> {
value={subsample}
onChange={this._inputChange('subsample', 'number')}
>
<option value="1">4:2:0</option>
<option value="1">Half</option>
{/*<option value="2">4:2:2</option>*/}
<option value="3">4:4:4</option>
<option value="3">Off</option>
</Select>
</label>
)}

View File

@@ -27,6 +27,7 @@ interface State {
edgePreservingFilter: number;
lossless: boolean;
slightLoss: boolean;
autoEdgePreservingFilter: boolean;
}
const maxSpeed = 7;
@@ -48,9 +49,10 @@ export class Options extends Component<Props, State> {
effort: maxSpeed - options.speed,
quality: options.quality,
progressive: options.progressive,
edgePreservingFilter: options.epf,
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
lossless: options.quality === 100,
slightLoss: options.lossyPalette,
autoEdgePreservingFilter: options.epf === -1,
};
}
@@ -86,7 +88,9 @@ export class Options extends Component<Props, State> {
speed: maxSpeed - optionState.effort,
quality: optionState.lossless ? 100 : optionState.quality,
progressive: optionState.progressive,
epf: optionState.edgePreservingFilter,
epf: optionState.autoEdgePreservingFilter
? -1
: optionState.edgePreservingFilter,
nearLossless: 0,
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
};
@@ -112,6 +116,7 @@ export class Options extends Component<Props, State> {
edgePreservingFilter,
lossless,
slightLoss,
autoEdgePreservingFilter,
}: State,
) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
@@ -152,16 +157,34 @@ export class Options extends Component<Props, State> {
Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="3"
value={edgePreservingFilter}
onInput={this._inputChange('edgePreservingFilter', 'number')}
>
Edge preserving filter:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
name="autoEdgeFilter"
checked={autoEdgePreservingFilter}
onChange={this._inputChange(
'autoEdgePreservingFilter',
'boolean',
)}
/>
Auto edge filter
</label>
<Expander>
{!autoEdgePreservingFilter && (
<div class={style.optionOneCell}>
<Range
min="0"
max="3"
value={edgePreservingFilter}
onInput={this._inputChange(
'edgePreservingFilter',
'number',
)}
>
Edge preserving filter:
</Range>
</div>
)}
</Expander>
</div>
)}
</Expander>

View File

@@ -18,10 +18,10 @@ export const label = 'JPEG XL (beta)';
export const mimeType = 'image/jpegxl';
export const extension = 'jxl';
export const defaultOptions: EncodeOptions = {
speed: 5,
quality: 50,
speed: 4,
quality: 75,
progressive: false,
epf: 2,
epf: -1,
nearLossless: 0,
lossyPalette: false,
};

View File

@@ -1,9 +1,14 @@
import { EncodeOptions } from '../shared/meta';
import { EncodeOptions, UVMode, Csp } from '../shared/meta';
import { defaultOptions } from '../shared/meta';
import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact';
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util';
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range';
import Select from 'client/lazy-app/Compress/Options/Select';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import linkState from 'linkstate';
export const encode = (
signal: AbortSignal,
@@ -18,93 +23,286 @@ interface Props {
}
interface State {
options: EncodeOptions;
effort: number;
quality: number;
alphaQuality: number;
passes: number;
sns: number;
uvMode: number;
lossless: boolean;
slightLoss: number;
colorSpace: number;
errorDiffusion: number;
useRandomMatrix: boolean;
showAdvanced: boolean;
separateAlpha: boolean;
}
export class Options extends Component<Props, State> {
state: State = {
showAdvanced: false,
};
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
if (state.options && shallowEqual(state.options, props.options)) {
return null;
}
private onChange = (event: Event) => {
const form = (event.currentTarget as HTMLInputElement).closest(
'form',
) as HTMLFormElement;
const { options } = this.props;
const newOptions: EncodeOptions = {
quality: inputFieldValueAsNumber(form.quality, options.quality),
alpha_quality: inputFieldValueAsNumber(
form.alpha_quality,
options.alpha_quality,
),
speed: inputFieldValueAsNumber(form.speed, options.speed),
pass: inputFieldValueAsNumber(form.pass, options.pass),
sns: inputFieldValueAsNumber(form.sns, options.sns),
const { options } = props;
const modifyState: Partial<State> = {
options,
effort: options.speed,
alphaQuality: options.alpha_quality,
passes: options.pass,
sns: options.sns,
uvMode: options.uv_mode,
colorSpace: options.csp_type,
errorDiffusion: options.error_diffusion,
useRandomMatrix: options.use_random_matrix,
separateAlpha: options.quality !== options.alpha_quality,
};
this.props.onChange(newOptions);
// If quality is > 95, it's lossless with slight loss
if (options.quality > 95) {
modifyState.lossless = true;
modifyState.slightLoss = 100 - options.quality;
} else {
modifyState.quality = options.quality;
modifyState.lossless = false;
}
return modifyState;
}
// Other state is set in getDerivedStateFromProps
state: State = {
lossless: false,
slightLoss: 0,
quality: defaultOptions.quality,
showAdvanced: false,
} as State;
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
// Cache the callback for performance
if (!this._inputChangeCallbacks.has(prop)) {
this._inputChangeCallbacks.set(prop, (event: Event) => {
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
const newVal =
type === 'boolean'
? 'checked' in formEl
? formEl.checked
: !!formEl.value
: Number(formEl.value);
const newState: Partial<State> = {
[prop]: newVal,
};
const optionState = {
...this.state,
...newState,
};
const newOptions: EncodeOptions = {
speed: optionState.effort,
quality: optionState.lossless
? 100 - optionState.slightLoss
: optionState.quality,
alpha_quality: optionState.separateAlpha
? optionState.alphaQuality
: optionState.quality,
pass: optionState.passes,
sns: optionState.sns,
uv_mode: optionState.uvMode,
csp_type: optionState.colorSpace,
error_diffusion: optionState.errorDiffusion,
use_random_matrix: optionState.useRandomMatrix,
};
// Updating options, so we don't recalculate in getDerivedStateFromProps.
newState.options = newOptions;
this.setState(newState);
this.props.onChange(newOptions);
});
}
return this._inputChangeCallbacks.get(prop)!;
};
render({ options }: Props) {
render(
{}: Props,
{
effort,
alphaQuality,
passes,
quality,
sns,
uvMode,
lossless,
slightLoss,
colorSpace,
errorDiffusion,
useRandomMatrix,
separateAlpha,
showAdvanced,
}: State,
) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{lossless && (
<div class={style.optionOneCell}>
<Range
min="0"
max="5"
step="0.1"
value={slightLoss}
onInput={this._inputChange('slightLoss', 'number')}
>
Slight loss:
</Range>
</div>
)}
</Expander>
<Expander>
{!lossless && (
<div>
<div class={style.optionOneCell}>
<Range
min="0"
max="95"
step="0.1"
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
/>
Separate alpha quality
</label>
<Expander>
{separateAlpha && (
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={alphaQuality}
onInput={this._inputChange('alphaQuality', 'number')}
>
Alpha Quality:
</Range>
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
</label>
<Expander>
{showAdvanced && (
<div>
<div class={style.optionOneCell}>
<Range
min="1"
max="10"
step="1"
value={passes}
onInput={this._inputChange('passes', 'number')}
>
Passes:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={sns}
onInput={this._inputChange('sns', 'number')}
>
Spatial noise shaping:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={errorDiffusion}
onInput={this._inputChange('errorDiffusion', 'number')}
>
Error diffusion:
</Range>
</div>
<label class={style.optionTextFirst}>
Subsample chroma:
<Select
value={uvMode}
onInput={this._inputChange('uvMode', 'number')}
>
<option value={UVMode.UVModeAuto}>Auto</option>
<option value={UVMode.UVModeAdapt}>Vary</option>
<option value={UVMode.UVMode420}>Half</option>
<option value={UVMode.UVMode444}>Off</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Color space:
<Select
value={colorSpace}
onInput={this._inputChange('colorSpace', 'number')}
>
<option value={Csp.kYCoCg}>YCoCg</option>
<option value={Csp.kYCbCr}>YCbCr</option>
<option value={Csp.kYIQ}>YIQ</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<Checkbox
checked={useRandomMatrix}
onChange={this._inputChange(
'useRandomMatrix',
'boolean',
)}
/>
Random matrix
</label>
</div>
)}
</Expander>
</div>
)}
</Expander>
<div class={style.optionOneCell}>
<Range
name="quality"
min="0"
max="100"
step="1"
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="alpha_quality"
min="0"
max="100"
step="1"
value={options.alpha_quality}
onInput={this.onChange}
>
Alpha Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="speed"
min="0"
max="9"
step="1"
value={options.speed}
onInput={this.onChange}
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Speed:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="pass"
min="1"
max="10"
step="1"
value={options.pass}
onInput={this.onChange}
>
Pass:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="sns"
min="0"
max="100"
step="1"
value={options.sns}
onInput={this.onChange}
>
Spatial noise shaping:
Effort:
</Range>
</div>
</form>

View File

@@ -11,16 +11,21 @@
* limitations under the License.
*/
import type { EncodeOptions } from 'codecs/wp2/enc/wp2_enc';
import { UVMode, Csp } from 'codecs/wp2/enc/wp2_enc';
export { EncodeOptions };
export { EncodeOptions, UVMode, Csp };
export const label = 'WebP v2 (unstable)';
export const mimeType = 'image/webp2';
export const extension = 'wp2';
export const defaultOptions: EncodeOptions = {
quality: 75,
alpha_quality: 100,
alpha_quality: 75,
speed: 5,
pass: 1,
sns: 50,
uv_mode: UVMode.UVModeAuto,
csp_type: Csp.kYCoCg,
error_diffusion: 0,
use_random_matrix: false,
};

View File

@@ -22,7 +22,7 @@ import {
inputFieldChecked,
} from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import { linkRef } from 'shared/initial-app/util';
import { linkRef } from 'shared/prerendered-app/util';
import Select from 'client/lazy-app/Compress/Options/Select';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';

View File

@@ -1,4 +1,5 @@
import * as styles from './styles.css';
import 'add-css:./styles.css';
// So it doesn't cause an error when running in node
const HTMLEl = ((__PRERENDER__

View File

@@ -1,4 +1,5 @@
import * as style from './styles.css';
import 'add-css:./styles.css';
// So it doesn't cause an error when running in node
const HTMLEl = ((__PRERENDER__

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,255 +0,0 @@
import { h, Component } from 'preact';
import { linkRef } from 'shared/initial-app/util';
import '../custom-els/loading-spinner';
import logo from 'url:./imgs/logo.svg';
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
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 * as style from './style.css';
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
import 'shared/initial-app/custom-els/snack-bar';
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,
},
];
const installButtonSource = 'introInstallButton-Purple';
interface Props {
onFile?: (file: File) => void;
showSnack?: SnackBarElement['showSnackbar'];
}
interface State {
fetchingDemoIndex?: number;
beforeInstallEvent?: BeforeInstallPromptEvent;
}
export default class Intro extends Component<Props, State> {
state: State = {};
private fileInput?: HTMLInputElement;
private installingViaButton = false;
constructor() {
super();
if (__PRERENDER__) return;
// Listen for beforeinstallprompt events, indicating Squoosh is installable.
window.addEventListener(
'beforeinstallprompt',
this.onBeforeInstallPromptEvent,
);
// Listen for the appinstalled event, indicating Squoosh has been installed.
window.addEventListener('appinstalled', this.onAppInstalled);
}
private resetFileInput = () => {
this.fileInput!.value = '';
};
private onFileChange = (event: Event): void => {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0];
if (!file) return;
this.resetFileInput();
this.props.onFile!(file);
};
private onButtonClick = () => {
this.fileInput!.click();
};
private onDemoClick = async (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 File([blob], demo.filename, { type });
this.props.onFile!(file);
} catch (err) {
this.setState({ fetchingDemoIndex: undefined });
this.props.showSnack!("Couldn't fetch demo image");
}
};
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
// Don't show the mini-infobar on mobile
event.preventDefault();
// Save the beforeinstallprompt event so it can be called later.
this.setState({ beforeInstallEvent: event });
// Log the event.
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-shown',
nonInteraction: true,
};
ga('send', 'event', gaEventInfo);
};
private onInstallClick = async (event: Event) => {
// Get the deferred beforeinstallprompt event
const beforeInstallEvent = this.state.beforeInstallEvent;
// If there's no deferred prompt, bail.
if (!beforeInstallEvent) return;
this.installingViaButton = true;
// Show the browser install prompt
beforeInstallEvent.prompt();
// Wait for the user to accept or dismiss the install prompt
const { outcome } = await beforeInstallEvent.userChoice;
// Send the analytics data
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-clicked',
eventLabel: installButtonSource,
eventValue: outcome === 'accepted' ? 1 : 0,
};
ga('send', 'event', gaEventInfo);
// If the prompt was dismissed, we aren't going to install via the button.
if (outcome === 'dismissed') {
this.installingViaButton = false;
}
};
private onAppInstalled = () => {
// We don't need the install button, if it's shown
this.setState({ beforeInstallEvent: undefined });
// Don't log analytics if page is not visible
if (document.hidden) {
return;
}
// Try to get the install, if it's not set, use 'browser'
const source = this.installingViaButton ? installButtonSource : 'browser';
ga('send', 'event', 'pwa-install', 'installed', source);
// Clear the install method property
this.installingViaButton = false;
};
render({}: Props, { fetchingDemoIndex, beforeInstallEvent }: State) {
return (
<div class={style.intro}>
<div>
<div class={style.logoSizer}>
<div class={style.logoContainer}>
<img
src={logo}
class={style.logo}
alt="Squoosh"
decoding="async"
/>
</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=""
decoding="async"
/>
{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>
{beforeInstallEvent && (
<button
type="button"
class={style.installButton}
onClick={this.onInstallClick}
>
Install
</button>
)}
<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>
<li>
<a href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy">
Privacy
</a>
</li>
</ul>
</div>
);
}
}

View File

@@ -1,228 +0,0 @@
@font-face {
font-family: 'intro-text';
font-style: normal;
font-weight: 300;
font-display: block;
/* This only contains the chars for "Drag & drop or" */
src: url('data:font/woff2;base64,d09GMgABAAAAAAXcAA4AAAAACowAAAWJAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbg2gcMAZgAGwRCAqIQIcnCxYAATYCJAMoBCAFgwAHIBvqCFEU84FMI2Xh/P3g+Tfn532yQ/IgYz4BrJyhtkkZwFBYAZ49sI63e5v/NnqzIfbADyE0qxOqK8ESLoNNdULHihxbW0W86/qHEk4wT/eHShPRZJYYqUGkQdLSWCeSemZBzwpKyX/LRoAhMEQhqCFBw5RHNCc4hbVn35FsxtTXVHYyo7miu5VN2AW1fwzVauRgXnIGo2IWsYdViUoLu6mms5VFAn+SeQ4eBazfj7QodrMt4oyQHaGADEPRpTbDqJaoTENNK6DpOralUszf6gI/QsAhWZSMKVOirikSJxZRLBVD0S4mB0kTBRwopjZ/mt/2/25+bcSipgiHRmwiFI1g+XhwlshyEAsbJzGiGH+U5whHNgiXooplafI1rMFbmIqjGAPhmcSkVFxeu9hw87aXsGyL+dPE05qUpK2WyaVQcZVW+aDmw3aalLJKNmQORcpZYtBIuTrncN4xXoVZY617TBSsx2T1DHgGU6u4etE04wha1GEwjVkEaDttOrl1FCOwUMxgHnuooJo62ukcWEuc1/aT+dZ8b142t5tbzc3mGnP1EJqVTEGMYTjG14YxtGEEG+0E2axhe6Oa1E8UrDHDFjhTRywYNWrU9JHTlw7RmaslkrrGcTJ+znW4EzzP0zovE4Z5d0hqVhBobftBIKkwL09SOv3hhCuv1Dp9taTeCJ2Mj3KDT8iDng5DkWzPw/UdP8idNDkMnUyOwEauwnYLQeLC7GskNe72QKe97AmuA42E5FjfyYTM+HTdQ+Xqb+q4JvptyKZN1w47qMMwL58fyKZM1U6NXgWlOFdxx7DpXHDTz4UB89WMK3HH3uY7mavFopGF+u36lGlqZsL4ugmbqvZxveycMO+a4uyN3o7GT2qdHpfr6W++kNTn1crdx7Z+FW7PfffTmfnXV/2ivsh5UX93zdlzct6QlSuHSumG3oGNNT9/m9yXnDcnKfsmDx8xUaoKi+uvGs99H2ieUJUg8bTnVwQcDd/SPKwYWDUv+QkpT6MulMrcPTXNWYnIowxvoiwnX+opTMkvzOMGgpNpqnK32CNVwCnassw0BwQwTa0rLS3m1DfIoxx5PIE8SvEmSk3pHSWZiRVKjOOQSylJSHGXkhT/u/tg/Vm9UZQcS59TGb1qjcuuT0925iaaU1vaWpZJM4ukqWWlrdWSIcVNlOImvnrzLn53UpnSLzbGT5lUlpTiKiPJFEmyqywFLtOhcaYJkWkaGe/oGBlnmiIiIiKYpqHxLmdaWg5JpxxHSXpajsuVlkPSvb1JelqOC0pubbAn2A2UsDdYmTmjvbVlgTRhVBSSxpbF1nZD+jvkUR4rcJeSFBp2d19SUsVW5DjkUkoSoITHJ7iJEpZnZaL4OiF7g92DN1mz8b1RiM9RDk9ps9pcanamlnj2ftqbJpHJ0wpkRn2+RJ6qsGflpYrPnxG6A4r9zqGY3qCcqDuhsWGQhoXpQ0663cWFM4qNR0Jxj1R0UBT36pahMneH4NYV27jElOeyAAAACACAABy4uvGyOsj21Y9h3gIA3PuxYAYAuC/7vftf7L+PXunMAQDwBQIAAAIA5vR/HwCvOQ//TzLL7cPIHUC0zMI5v7+tHiVfzWOeSrJKZbFabWGNSnJE+jmsnmTjTZm6kBi9r0aLgm8qNk6t67ATuPlEitG+g+E7in1GMYxCxmIF9YzNJK7lRoSPc6PCD+8fxhp+YjdttDNAJw3UUU83M1jFClaylkpkU08NVZqkh0oaaBLPnaCTNhqpoaok2UkPZqy/JyfpKnVLkhrq6KGZCjpZxTJWqN9uJofD5HGMzSXHLaVbOmuTSnOp6cTQgJlaB6oF7RIITul8N+1sYjnL6aJqqoZ2inaxDIY2s2zwlXUs5zj7OPJmAPao+ZhVHy0A')
format('woff2');
}
@font-face {
font-family: 'intro-text';
font-style: normal;
font-weight: 500;
font-display: block;
/* 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 {
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;
height: 100%;
box-sizing: border-box;
overscroll-behavior: contain;
position: relative;
}
.logo-container {
position: relative;
padding-top: 100%;
}
.logo-sizer {
width: 90%;
max-width: 52vh;
margin: 0 auto;
}
.logo {
composes: abs-fill from '../util.css';
pointer-events: none;
}
.open-image-guide {
font: 300 11vw intro-text, sans-serif;
margin-bottom: 0;
@media (min-width: 460px) {
font-size: 50.6px;
padding: 0 40px;
}
}
.select-button {
composes: unbutton from '../util.css';
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 '../util.css';
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;
flex: 0 0 auto;
}
.demo-img-aspect {
position: relative;
padding-top: 100%;
}
.demo-icon {
composes: abs-fill from '../util.css';
pointer-events: none;
}
.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 '../util.css';
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;
}
.install-button {
composes: unbutton from '../util.css';
&:hover,
&:focus {
background: #504488;
}
background: #5d509e;
border: 1px solid #e8e8e8;
color: #fff;
padding: 14px;
font-size: 1.3rem;
position: absolute;
top: 1rem;
right: 1rem;
animation: fade-in 0.3s ease-in-out;
}
@keyframes fade-in {
from {
opacity: 0;
}
}
.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;
}
}
}

View File

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

View File

@@ -13,3 +13,23 @@
/// <reference path="../../missing-types.d.ts" />
declare const __PRERENDER__: boolean;
interface ResizeObserverCallback {
(entries: ResizeObserverEntry[], observer: ResizeObserver): void;
}
interface ResizeObserverEntry {
readonly target: Element;
readonly contentRect: DOMRectReadOnly;
}
interface ResizeObserver {
observe(target: Element): void;
unobserve(target: Element): void;
disconnect(): void;
}
declare var ResizeObserver: {
prototype: ResizeObserver;
new (callback: ResizeObserverCallback): ResizeObserver;
};

View File

@@ -0,0 +1,401 @@
import * as style from '../style.css';
import { startBlobs } from './meta';
/**
* Control point x,y - point x,y - control point x,y
*/
export type BlobPoint = [number, number, number, number, number, number];
const maxPointDistance = 0.25;
function randomisePoint(point: BlobPoint): BlobPoint {
const distance = Math.random() * maxPointDistance;
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 rand = (min: number, max: number) => Math.random() * (max - min) + min;
interface CircleBlobPointState {
basePoint: BlobPoint;
pos: number;
duration: number;
startPoint: BlobPoint;
endPoint: BlobPoint;
}
/** Bezier points for a seven point circle, to 3 decimal places */
const sevenPointCircle: BlobPoint[] = [
[-0.304, -1, 0, -1, 0.304, -1],
[0.592, -0.861, 0.782, -0.623, 0.972, -0.386],
[1.043, -0.074, 0.975, 0.223, 0.907, 0.519],
[0.708, 0.769, 0.434, 0.901, 0.16, 1.033],
[-0.16, 1.033, -0.434, 0.901, -0.708, 0.769],
[-0.907, 0.519, -0.975, 0.223, -1.043, -0.074],
[-0.972, -0.386, -0.782, -0.623, -0.592, -0.861],
];
/*
// Should it be needed, here's how the above was created:
function createBezierCirclePoints(points: number): BlobPoint[] {
const anglePerPoint = 360 / points;
const matrix = new DOMMatrix();
const point = new DOMPoint();
const controlDistance = (4 / 3) * Math.tan(Math.PI / (2 * points));
return Array.from({ length: 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];
matrix.rotateSelf(0, 0, anglePerPoint);
return basePoint;
});
}
*/
interface CircleBlobOptions {
minDuration?: number;
maxDuration?: number;
startPoints?: BlobPoint[];
}
class CircleBlob {
private animStates: CircleBlobPointState[];
private minDuration: number;
private maxDuration: number;
private points: BlobPoint[];
constructor(
basePoints: BlobPoint[],
{
startPoints = basePoints.map((point) => randomisePoint(point)),
minDuration = 4000,
maxDuration = 11000,
}: CircleBlobOptions = {},
) {
this.points = startPoints;
this.minDuration = minDuration;
this.maxDuration = maxDuration;
this.animStates = basePoints.map((basePoint, i) => ({
basePoint,
pos: 0,
duration: rand(minDuration, maxDuration),
startPoint: startPoints[i],
endPoint: randomisePoint(basePoint),
}));
}
advance(timeDelta: number): void {
this.points = this.animStates.map((animState) => {
animState.pos += timeDelta / animState.duration;
if (animState.pos >= 1) {
animState.startPoint = animState.endPoint;
animState.pos = 0;
animState.duration = rand(this.minDuration, this.maxDuration);
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;
});
}
draw(ctx: CanvasRenderingContext2D) {
const points = this.points;
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();
}
}
const centralBlobsRotationTime = 120000;
class CentralBlobs {
private rotatePos: number = 0;
private blobs = Array.from(
{ length: 4 },
(_, i) => new CircleBlob(sevenPointCircle, { startPoints: startBlobs[i] }),
);
advance(timeDelta: number) {
this.rotatePos =
(this.rotatePos + timeDelta / centralBlobsRotationTime) % 1;
for (const blob of this.blobs) blob.advance(timeDelta);
}
draw(ctx: CanvasRenderingContext2D, x: number, y: number, radius: number) {
ctx.save();
ctx.translate(x, y);
ctx.scale(radius, radius);
ctx.rotate(Math.PI * 2 * this.rotatePos);
for (const blob of this.blobs) blob.draw(ctx);
ctx.restore();
}
}
const bgBlobsMinRadius = 20;
const bgBlobsMaxRadius = 60;
const bgBlobsMinAlpha = 0.1;
const bgBlobsMaxAlpha = 0.8;
const bgBlobsGridSize = 200;
const bgBlobsMinSpinTime = 20000;
const bgBlobsMaxSpinTime = 60000;
const bgBlobsMinVelocity = 0.005;
const bgBlobsMaxVelocity = 0.02;
interface BackgroundBlob {
blob: CircleBlob;
velocity: number;
spinTime: number;
alpha: number;
alphaMultiplier: number;
rotatePos: number;
radius: number;
x: number;
y: number;
}
const bgBlobsAlphaTime = 2000;
class BackgroundBlobs {
private bgBlobs: BackgroundBlob[] = [];
private overallAlphaPos = 0;
constructor(bounds: DOMRect) {
for (let x = 0; x < bounds.width; x += bgBlobsGridSize) {
for (let y = 0; y < bounds.height; y += bgBlobsGridSize) {
this.bgBlobs.push({
blob: new CircleBlob(sevenPointCircle, {
minDuration: 2000,
maxDuration: 5000,
}),
velocity: rand(bgBlobsMinVelocity, bgBlobsMaxVelocity),
alpha:
Math.random() ** 3 * (bgBlobsMaxAlpha - bgBlobsMinAlpha) +
bgBlobsMinAlpha,
alphaMultiplier: 1,
spinTime: rand(bgBlobsMinSpinTime, bgBlobsMaxSpinTime),
rotatePos: 0,
radius:
Math.random() ** 3 * (bgBlobsMaxRadius - bgBlobsMinRadius) +
bgBlobsMinRadius,
x: Math.random() * bgBlobsGridSize + x,
y: Math.random() * bgBlobsGridSize + y,
});
}
}
}
advance(
timeDelta: number,
bounds: DOMRect,
targetX: number,
targetY: number,
targetRadius: number,
) {
if (this.overallAlphaPos !== 1) {
this.overallAlphaPos = Math.min(
1,
this.overallAlphaPos + timeDelta / bgBlobsAlphaTime,
);
}
for (const bgBlob of this.bgBlobs) {
bgBlob.blob.advance(timeDelta);
let dist = Math.hypot(bgBlob.x - targetX, bgBlob.y - targetY);
bgBlob.rotatePos = (bgBlob.rotatePos + timeDelta / bgBlob.spinTime) % 1;
const shiftDist = bgBlob.velocity * timeDelta;
if (dist < 10) {
// Move the circle out to a random edge
switch (Math.floor(Math.random() * 4)) {
case 0: // top
bgBlob.x = Math.random() * bounds.width;
bgBlob.y = -(bgBlob.radius * (1 + maxPointDistance));
break;
case 1: // left
bgBlob.x = -(bgBlob.radius * (1 + maxPointDistance));
bgBlob.y = Math.random() * bounds.height;
break;
case 2: // bottom
bgBlob.x = Math.random() * bounds.width;
bgBlob.y = bounds.height + bgBlob.radius * (1 + maxPointDistance);
break;
case 3: // right
bgBlob.x = bounds.width + bgBlob.radius * (1 + maxPointDistance);
bgBlob.y = Math.random() * bounds.height;
break;
}
}
dist = Math.hypot(bgBlob.x - targetX, bgBlob.y - targetY);
const direction = Math.atan2(targetX - bgBlob.x, targetY - bgBlob.y);
const xShift = Math.sin(direction) * shiftDist;
const yShift = Math.cos(direction) * shiftDist;
bgBlob.x += xShift;
bgBlob.y += yShift;
bgBlob.alphaMultiplier = Math.min(dist / targetRadius, 1);
}
}
draw(ctx: CanvasRenderingContext2D) {
const overallAlpha = easeInOutQuad(this.overallAlphaPos);
for (const bgBlob of this.bgBlobs) {
ctx.save();
ctx.globalAlpha = bgBlob.alpha * bgBlob.alphaMultiplier * overallAlpha;
ctx.translate(bgBlob.x, bgBlob.y);
ctx.scale(bgBlob.radius, bgBlob.radius);
ctx.rotate(Math.PI * 2 * bgBlob.rotatePos);
bgBlob.blob.draw(ctx);
ctx.restore();
}
}
}
const deltaMultiplierStep = 0.01;
export function startBlobAnim(canvas: HTMLCanvasElement) {
let lastTime: number;
const ctx = canvas.getContext('2d')!;
const centralBlobs = new CentralBlobs();
let backgroundBlobs: BackgroundBlobs;
const loadImgEl = document.querySelector('.' + style.loadImg)!;
let hasFocus = document.hasFocus();
let deltaMultiplier = hasFocus ? 1 : 0;
let animating = true;
const visibilityListener = () => {
// 'Pause time' while page is hidden
if (document.visibilityState === 'visible') lastTime = performance.now();
};
const focusListener = () => {
hasFocus = true;
if (!animating) startAnim();
};
const blurListener = () => {
hasFocus = false;
};
new ResizeObserver(() => {
// Redraw for new canvas size
if (!animating) drawFrame(0);
}).observe(canvas);
addEventListener('focus', focusListener);
addEventListener('blur', blurListener);
document.addEventListener('visibilitychange', visibilityListener);
function destruct() {
removeEventListener('focus', focusListener);
removeEventListener('blur', blurListener);
document.removeEventListener('visibilitychange', visibilityListener);
}
function drawFrame(delta: number) {
const canvasBounds = canvas.getBoundingClientRect();
canvas.width = canvasBounds.width * devicePixelRatio;
canvas.height = canvasBounds.height * devicePixelRatio;
const loadImgBounds = loadImgEl.getBoundingClientRect();
const computedStyles = getComputedStyle(canvas);
const blobPink = computedStyles.getPropertyValue('--blob-pink');
const loadImgCenterX =
loadImgBounds.left - canvasBounds.left + loadImgBounds.width / 2;
const loadImgCenterY =
loadImgBounds.top - canvasBounds.top + loadImgBounds.height / 2;
const loadImgRadius = loadImgBounds.height / 2 / (1 + maxPointDistance);
ctx.scale(devicePixelRatio, devicePixelRatio);
if (!backgroundBlobs) backgroundBlobs = new BackgroundBlobs(canvasBounds);
backgroundBlobs.advance(
delta,
canvasBounds,
loadImgCenterX,
loadImgCenterY,
loadImgRadius,
);
centralBlobs.advance(delta);
ctx.globalAlpha = Number(
computedStyles.getPropertyValue('--center-blob-opacity'),
);
ctx.fillStyle = blobPink;
backgroundBlobs.draw(ctx);
centralBlobs.draw(ctx, loadImgCenterX, loadImgCenterY, loadImgRadius);
}
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;
drawFrame(delta);
requestAnimationFrame(frame);
}
function startAnim() {
animating = true;
requestAnimationFrame((time: number) => {
lastTime = time;
frame(time);
});
}
startAnim();
}

View File

@@ -0,0 +1,41 @@
import type { BlobPoint } from '.';
/** Start points, for the shape we use in prerender */
export const startBlobs: BlobPoint[][] = [
[
[-0.232, -1.029, 0.073, -1.029, 0.377, -1.029],
[0.565, -1.098, 0.755, -0.86, 0.945, -0.622],
[0.917, -0.01, 0.849, 0.286, 0.782, 0.583],
[0.85, 0.687, 0.576, 0.819, 0.302, 0.951],
[-0.198, 1.009, -0.472, 0.877, -0.746, 0.745],
[-0.98, 0.513, -1.048, 0.216, -1.116, -0.08],
[-0.964, -0.395, -0.774, -0.633, -0.584, -0.871],
],
[
[-0.505, -1.109, -0.201, -1.109, 0.104, -1.109],
[0.641, -0.684, 0.831, -0.446, 1.02, -0.208],
[1.041, 0.034, 0.973, 0.331, 0.905, 0.628],
[0.734, 0.794, 0.46, 0.926, 0.186, 1.058],
[-0.135, 0.809, -0.409, 0.677, -0.684, 0.545],
[-0.935, 0.404, -1.002, 0.108, -1.07, -0.189],
[-0.883, -0.402, -0.693, -0.64, -0.503, -0.878],
],
[
[-0.376, -1.168, -0.071, -1.168, 0.233, -1.168],
[0.732, -0.956, 0.922, -0.718, 1.112, -0.48],
[1.173, 0.027, 1.105, 0.324, 1.038, 0.621],
[0.707, 0.81, 0.433, 0.943, 0.159, 1.075],
[-0.096, 1.135, -0.37, 1.003, -0.644, 0.871],
[-0.86, 0.457, -0.927, 0.161, -0.995, -0.136],
[-0.87, -0.516, -0.68, -0.754, -0.49, -0.992],
],
[
[-0.309, -0.998, -0.004, -0.998, 0.3, -0.998],
[0.535, -0.852, 0.725, -0.614, 0.915, -0.376],
[1.05, -0.09, 0.982, 0.207, 0.915, 0.504],
[0.659, 0.807, 0.385, 0.939, 0.111, 1.071],
[-0.178, 1.048, -0.452, 0.916, -0.727, 0.784],
[-0.942, 0.582, -1.009, 0.285, -1.077, -0.011],
[-1.141, -0.335, -0.951, -0.573, -0.761, -0.811],
],
];

View File

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29.34 28.61"><path fill="#838383" d="M14.67 0a14.67 14.67 0 00-4.64 28.59c.74.13 1-.32 1-.7l-.02-2.74c-4.08.89-4.94-1.73-4.94-1.73a3.88 3.88 0 00-1.63-2.14c-1.33-.91.1-.9.1-.9A3.08 3.08 0 016.8 21.9a3.12 3.12 0 004.27 1.22 3.12 3.12 0 01.93-1.96c-3.26-.37-6.68-1.63-6.68-7.25a5.68 5.68 0 011.5-3.94 5.27 5.27 0 01.15-3.9S8.2 5.7 11 7.58a13.9 13.9 0 017.34 0c2.8-1.9 4.03-1.5 4.03-1.5a5.27 5.27 0 01.15 3.9 5.67 5.67 0 011.5 3.93c0 5.63-3.42 6.87-6.7 7.24a3.5 3.5 0 011 2.71l-.01 4.03c0 .39.26.85 1 .7A14.67 14.67 0 0014.67 0z"/></svg>

After

Width:  |  Height:  |  Size: 588 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,382 @@
import { h, Component } from 'preact';
import { linkRef } from 'shared/prerendered-app/util';
import '../../custom-els/loading-spinner';
import logo from 'url:./imgs/logo.svg';
import githubLogo from 'url:./imgs/github-logo.svg';
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
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/custom-els/snack-bar';
import 'shared/custom-els/snack-bar';
import { startBlobs } from './blob-anim/meta';
const demos = [
{
description: 'Large photo',
size: '2.8mb',
filename: 'photo.jpg',
url: largePhoto,
iconUrl: largePhotoIcon,
},
{
description: 'Artwork',
size: '2.9mb',
filename: 'art.jpg',
url: artwork,
iconUrl: artworkIcon,
},
{
description: 'Device screen',
size: '1.6mb',
filename: 'pixel3.png',
url: deviceScreen,
iconUrl: deviceScreenIcon,
},
{
description: 'SVG icon',
size: '13k',
filename: 'squoosh.svg',
url: logo,
iconUrl: logoIcon,
},
] as const;
const blobAnimImport =
!__PRERENDER__ && matchMedia('(prefers-reduced-motion: reduce)').matches
? undefined
: import('./blob-anim');
const installButtonSource = 'introInstallButton-Purple';
const supportsClipboardAPI =
!__PRERENDER__ && navigator.clipboard && navigator.clipboard.read;
async function getImageClipboardItem(
items: ClipboardItem[],
): Promise<undefined | Blob> {
for (const item of items) {
const type = item.types.find((type) => type.startsWith('image/'));
if (type) return item.getType(type);
}
}
interface Props {
onFile?: (file: File) => void;
showSnack?: SnackBarElement['showSnackbar'];
}
interface State {
fetchingDemoIndex?: number;
beforeInstallEvent?: BeforeInstallPromptEvent;
showBlobSVG: boolean;
}
export default class Intro extends Component<Props, State> {
state: State = {
showBlobSVG: true,
};
private fileInput?: HTMLInputElement;
private blobCanvas?: HTMLCanvasElement;
private installingViaButton = false;
componentDidMount() {
// Listen for beforeinstallprompt events, indicating Squoosh is installable.
window.addEventListener(
'beforeinstallprompt',
this.onBeforeInstallPromptEvent,
);
// Listen for the appinstalled event, indicating Squoosh has been installed.
window.addEventListener('appinstalled', this.onAppInstalled);
if (blobAnimImport) {
blobAnimImport.then((module) => {
this.setState(
{
showBlobSVG: false,
},
() => module.startBlobAnim(this.blobCanvas!),
);
});
}
// TODO: remove this
const demo = demos[3];
fetch(demo.url)
.then((r) => r.blob())
.then((blob) =>
this.props.onFile!(
new File([blob], demo.filename, { type: blob.type }),
),
);
}
componentWillUnmount() {
window.removeEventListener(
'beforeinstallprompt',
this.onBeforeInstallPromptEvent,
);
window.removeEventListener('appinstalled', this.onAppInstalled);
}
private onFileChange = (event: Event): void => {
const fileInput = event.target as HTMLInputElement;
const file = fileInput.files && fileInput.files[0];
if (!file) return;
this.fileInput!.value = '';
this.props.onFile!(file);
};
private onOpenClick = () => {
this.fileInput!.click();
};
private onDemoClick = async (index: number, event: Event) => {
try {
this.setState({ fetchingDemoIndex: index });
const demo = demos[index];
const blob = await fetch(demo.url).then((r) => r.blob());
const file = new File([blob], demo.filename, { type: blob.type });
this.props.onFile!(file);
} catch (err) {
this.setState({ fetchingDemoIndex: undefined });
this.props.showSnack!("Couldn't fetch demo image");
}
};
private onBeforeInstallPromptEvent = (event: BeforeInstallPromptEvent) => {
// Don't show the mini-infobar on mobile
event.preventDefault();
// Save the beforeinstallprompt event so it can be called later.
this.setState({ beforeInstallEvent: event });
// Log the event.
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-shown',
nonInteraction: true,
};
ga('send', 'event', gaEventInfo);
};
private onInstallClick = async (event: Event) => {
// Get the deferred beforeinstallprompt event
const beforeInstallEvent = this.state.beforeInstallEvent;
// If there's no deferred prompt, bail.
if (!beforeInstallEvent) return;
this.installingViaButton = true;
// Show the browser install prompt
beforeInstallEvent.prompt();
// Wait for the user to accept or dismiss the install prompt
const { outcome } = await beforeInstallEvent.userChoice;
// Send the analytics data
const gaEventInfo = {
eventCategory: 'pwa-install',
eventAction: 'promo-clicked',
eventLabel: installButtonSource,
eventValue: outcome === 'accepted' ? 1 : 0,
};
ga('send', 'event', gaEventInfo);
// If the prompt was dismissed, we aren't going to install via the button.
if (outcome === 'dismissed') {
this.installingViaButton = false;
}
};
private onAppInstalled = () => {
// We don't need the install button, if it's shown
this.setState({ beforeInstallEvent: undefined });
// Don't log analytics if page is not visible
if (document.hidden) return;
// Try to get the install, if it's not set, use 'browser'
const source = this.installingViaButton ? installButtonSource : 'browser';
ga('send', 'event', 'pwa-install', 'installed', source);
// Clear the install method property
this.installingViaButton = false;
};
private onPasteClick = async () => {
let clipboardItems: ClipboardItem[];
try {
clipboardItems = await navigator.clipboard.read();
} catch (err) {
this.props.showSnack!(`No permission to access clipboard`);
return;
}
const blob = await getImageClipboardItem(clipboardItems);
if (!blob) {
this.props.showSnack!(`No image found in the clipboard`);
return;
}
this.props.onFile!(new File([blob], 'image.unknown'));
};
render(
{}: Props,
{ fetchingDemoIndex, beforeInstallEvent, showBlobSVG }: State,
) {
return (
<div class={style.intro}>
<input
class={style.hide}
ref={linkRef(this, 'fileInput')}
type="file"
onChange={this.onFileChange}
/>
<div class={style.main}>
{!__PRERENDER__ && (
<canvas
ref={linkRef(this, 'blobCanvas')}
class={style.blobCanvas}
/>
)}
<h1 class={style.logoContainer}>
<img
class={style.logo}
src={logoWithText}
alt="Squoosh"
width="539"
height="162"
/>
</h1>
<div class={style.loadImg}>
{showBlobSVG && (
<svg
class={style.blobSvg}
viewBox="-1.25 -1.25 2.5 2.5"
preserveAspectRatio="xMidYMid slice"
>
{startBlobs.map((points) => (
<path
d={points
.map((point, i) => {
const nextI = i === points.length - 1 ? 0 : i + 1;
let d = '';
if (i === 0) {
d += `M${point[2]} ${point[3]}`;
}
return (
d +
`C${point[4]} ${point[5]} ${points[nextI][0]} ${points[nextI][1]} ${points[nextI][2]} ${points[nextI][3]}`
);
})
.join('')}
/>
))}
</svg>
)}
<div
class={style.loadImgContent}
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
>
<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>
</button>
<div>
<span class={style.dropText}>Drop </span>OR{' '}
{supportsClipboardAPI ? (
<button class={style.pasteBtn} onClick={this.onPasteClick}>
Paste
</button>
) : (
'Paste'
)}
</div>
</div>
</div>
</div>
<div class={style.demosContainer}>
<svg viewBox="0 0 1920 140" class={style.topWave}>
<path
d="M1920 0l-107 28c-106 29-320 85-533 93-213 7-427-36-640-50s-427 0-533 7L0 85v171h1920z"
class={style.subWave}
/>
<path
d="M0 129l64-26c64-27 192-81 320-75 128 5 256 69 384 64 128-6 256-80 384-91s256 43 384 70c128 26 256 26 320 26h64v96H0z"
class={style.mainWave}
/>
</svg>
<div class={style.contentPadding}>
<p class={style.demoTitle}>
Or <strong>try one</strong> of these:
</p>
<ul class={style.demos}>
{demos.map((demo, i) => (
<li>
<button
class="unbutton"
onClick={(event) => this.onDemoClick(i, event)}
>
<div>
<div class={style.demoIconContainer}>
<img
class={style.demoIcon}
src={demo.iconUrl}
alt={demo.description}
/>
{fetchingDemoIndex === i && (
<div class={style.demoLoader}>
<loading-spinner />
</div>
)}
</div>
<div class={style.demoSize}>{demo.size}</div>
</div>
</button>
</li>
))}
</ul>
</div>
</div>
<div class={style.footer}>
<svg viewBox="0 0 1920 79" class={style.topWave}>
<path
d="M0 59l64-11c64-11 192-34 320-43s256-5 384 4 256 23 384 34 256 21 384 14 256-30 320-41l64-11v94H0z"
class={style.footerWave}
/>
</svg>
<div class={style.contentPadding}>
<footer class={style.footerItems}>
<a
class={style.footerLink}
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
>
Privacy
</a>
<a
class={style.footerLinkWithLogo}
href="https://github.com/GoogleChromeLabs/squoosh"
>
<img src={githubLogo} alt="" width="10" height="10" />
Source on Github
</a>
</footer>
</div>
</div>
{beforeInstallEvent && (
<button class={style.installBtn} onClick={this.onInstallClick}>
Install
</button>
)}
</div>
);
}
}

View File

@@ -29,3 +29,12 @@ interface BeforeInstallPromptEvent extends Event {
interface WindowEventMap {
beforeinstallprompt: BeforeInstallPromptEvent;
}
interface ClipboardItem {
types: string[];
getType(type: string): Promise<Blob>;
}
interface Clipboard {
read(): Promise<ClipboardItem[]>;
}

View File

@@ -0,0 +1,243 @@
.intro {
composes: abs-fill from global;
-webkit-overflow-scrolling: touch;
overflow: auto;
overscroll-behavior: contain;
display: grid;
grid-template-rows: 1fr max-content max-content;
font-size: 1.2rem;
color: var(--dark-text);
}
.blob-canvas {
composes: abs-fill from global;
width: 100%;
height: 100%;
}
.hide {
display: none;
}
.main {
min-height: 541px;
display: grid;
grid-template-rows: max-content max-content;
justify-items: center;
position: relative;
--blob-pink: var(--hot-pink);
--center-blob-opacity: 0.3;
@media (min-width: 600px) {
min-height: 688px;
}
}
.logo-container {
margin: 5rem 0 1rem;
}
.logo {
transform: translate(-1%, 0);
width: 189px;
height: auto;
}
.load-img {
position: relative;
color: var(--white);
font-style: italic;
font-size: 1.2rem;
}
.blob-svg {
composes: abs-fill from global;
width: 100%;
height: 100%;
fill: var(--blob-pink);
& path {
opacity: var(--center-blob-opacity);
}
}
.load-img-content {
position: relative;
--size: 29rem;
max-width: var(--size);
width: 100vw;
height: var(--size);
display: grid;
grid-template-rows: max-content max-content;
justify-items: center;
align-content: center;
gap: 0.7rem;
@media (min-width: 600px) {
--size: 36rem;
}
}
.load-btn {
composes: unbutton from global;
}
.load-icon {
--size: 5rem;
width: var(--size);
height: var(--size);
fill: var(--white);
transform: translate(4.3%, -1%);
}
.paste-btn {
composes: unbutton from global;
text-decoration: underline;
font: inherit;
color: inherit;
}
.demos-container {
position: relative;
background: var(--deep-blue);
padding-bottom: 5.2vw;
}
.top-wave {
position: absolute;
left: 0;
right: 0;
bottom: 100%;
}
.main-wave {
fill: var(--deep-blue);
}
.sub-wave {
fill: var(--light-blue);
}
.footer {
position: relative;
background: var(--light-gray);
}
.footer-wave {
fill: var(--light-gray);
}
.content-padding {
padding: 2rem;
}
.footer-items {
display: grid;
justify-content: end;
grid-auto-columns: max-content;
grid-auto-flow: column;
align-items: center;
gap: 4rem;
}
.footer-link {
text-decoration: none;
color: inherit;
}
.footer-link-with-logo {
composes: footer-link;
display: grid;
grid-template-columns: 1.8em max-content;
align-items: center;
gap: 0.6em;
img {
width: 100%;
height: auto;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
}
.install-btn {
composes: unbutton from global;
position: absolute;
top: 1rem;
right: 1rem;
background: var(--deep-blue);
border-radius: 0.4em;
color: var(--white);
padding: 0.5em 1em;
font-size: 1.6rem;
animation: fade-in 600ms ease-in-out;
}
.demo-title {
color: var(--white);
margin: 0;
font-size: 2rem;
text-align: center;
}
.demos {
display: grid;
gap: 3rem;
justify-items: center;
justify-content: center;
padding: 0;
margin: 3rem auto;
--demo-size: 80px;
grid-template-columns: repeat(auto-fit, var(--demo-size));
@media (min-width: 740px) {
--demo-size: 100px;
gap: 6rem;
}
& > li {
display: block;
}
}
.demo-size {
background: var(--dim-blue);
border-radius: 1000px;
color: var(--white);
width: max-content;
padding: 0.5rem 1.2rem;
margin: 0.7rem auto 0;
}
.demo-icon-container {
border-radius: var(--demo-size);
position: relative;
overflow: hidden;
}
.demo-icon {
width: var(--demo-size);
height: var(--demo-size);
display: block;
}
.demo-loader {
composes: abs-fill from global;
background: rgba(0, 0, 0, 0.5);
display: grid;
justify-content: center;
align-content: center;
animation: fade-in 600ms ease-in-out;
& > loading-spinner {
--color: var(--white);
}
}
.drop-text {
@media (max-width: 599px) {
display: none;
}
}

View File

@@ -0,0 +1,21 @@
html {
--pink: #ff3385;
--hot-pink: #ff0066;
--white: #fff;
--black: #000;
--blue: #5fb4e4;
--dim-blue: #0a7bcc;
--deep-blue: #09f;
--light-blue: #76c8ff;
--light-gray: #eaeaea;
--dark-text: #343a3e;
/* Old stuff: */
--gray-dark: rgba(0, 0, 0, 0.8);
--button-fg-color: 95, 180, 228;
--button-fg: rgb(95, 180, 228);
--negative: rgb(207, 113, 127);
--positive: rgb(149, 212, 159);
}

View File

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

View File

@@ -11,7 +11,7 @@
* limitations under the License.
*/
/// <reference path="../../missing-types.d.ts" />
/// <reference path="../shared/initial-app/Intro/missing-types.d.ts" />
/// <reference path="../shared/prerendered-app/Intro/missing-types.d.ts" />
declare module 'client-bundle:*' {
const url: string;

View File

@@ -25,8 +25,8 @@ body {
height: 100%;
padding: 0;
margin: 0;
font: 12px/1.3 system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica,
sans-serif;
font: 12px/1.3 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue',
Helvetica, Arial, 'Lucida Grande', sans-serif;
overflow: hidden;
overscroll-behavior: none;
contain: strict;
@@ -34,16 +34,6 @@ body {
background-size: 20px 20px;
}
:root {
--gray-dark: rgba(0, 0, 0, 0.8);
--button-fg-color: 95, 180, 228;
--button-fg: rgb(95, 180, 228);
--negative: rgb(207, 113, 127);
--positive: rgb(149, 212, 159);
}
:global(#app) {
position: absolute;
left: 0;

View File

@@ -17,7 +17,7 @@ import initialCss from 'initial-css:';
import { allSrc } from 'client-bundle:client/initial-app';
import favicon from 'url:static-build/assets/favicon.ico';
import { escapeStyleScriptContent } from 'static-build/utils';
import Intro from 'shared/initial-app/Intro';
import Intro from 'shared/prerendered-app/Intro';
interface Props {}