forked from external-repos/squoosh
Compare commits
1 Commits
testing
...
prerenderi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf38e5a44 |
18
.babelrc
18
.babelrc
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"env",
|
||||||
|
{
|
||||||
|
"loose": true,
|
||||||
|
"uglify": false,
|
||||||
|
"modules": false,
|
||||||
|
"targets": {
|
||||||
|
"browsers": "last 2 versions"
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"transform-regenerator",
|
||||||
|
"transform-es2015-typeof-symbol"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"transform-decorators-legacy",
|
||||||
"transform-class-properties",
|
"transform-class-properties",
|
||||||
"transform-react-constant-elements",
|
"transform-react-constant-elements",
|
||||||
"transform-react-remove-prop-types",
|
"transform-react-remove-prop-types",
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "squoosh-beta"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,4 +2,3 @@ node_modules
|
|||||||
/build
|
/build
|
||||||
/*.log
|
/*.log
|
||||||
*.scss.d.ts
|
*.scss.d.ts
|
||||||
*.css.d.ts
|
|
||||||
|
|||||||
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
|||||||
FROM selenium/node-chrome:latest
|
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
RUN apt-get update -qqy \
|
|
||||||
&& apt-get -qqy install python git build-essential \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/* \
|
|
||||||
&& rm /bin/sh && ln -s /bin/bash /bin/sh \
|
|
||||||
&& chown seluser /usr/local
|
|
||||||
|
|
||||||
ENV NVM_DIR /usr/local/nvm
|
|
||||||
RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash \
|
|
||||||
&& source $NVM_DIR/nvm.sh \
|
|
||||||
&& nvm install v8
|
|
||||||
|
|
||||||
ENV CHROME_BIN /opt/google/chrome/chrome
|
|
||||||
ENV INSIDE_DOCKER=1
|
|
||||||
|
|
||||||
WORKDIR /usr/src
|
|
||||||
ENTRYPOINT source $NVM_DIR/nvm.sh && npm i && npm test
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Codecs
|
|
||||||
|
|
||||||
This folder contains a self-contained sub-project for each encoder and decoder that squoosh supplies.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Each subproject can be built using [Docker](https://www.docker.com/) the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ npm install
|
|
||||||
$ npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
This will build two files: `<codec name>_<enc or dec>.js` and `<codec name>_<enc or dec>.wasm`. It will most likely be necessary to set [`Module["locateFile"]`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html#affecting-execution) to sucessfully load the `.wasm` file. When the `.js` file is loaded, a global `<codec name>_<enc or dec>` is created with the same API as an [Emscripten `Module`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html).
|
|
||||||
|
|
||||||
Each codec will document its API in its README.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB |
@@ -1,44 +0,0 @@
|
|||||||
# MozJPEG encoder
|
|
||||||
|
|
||||||
- Source: <https://github.com/mozilla/mozjpeg>
|
|
||||||
- Version: v3.3.1
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- Automake
|
|
||||||
- pkg-config
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
See `example.html`
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### `int version()`
|
|
||||||
|
|
||||||
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
|
|
||||||
|
|
||||||
### `uint8_t* create_buffer(int width, int height)`
|
|
||||||
|
|
||||||
Allocates an RGBA buffer for an image with the given dimension.
|
|
||||||
|
|
||||||
### `void destroy_buffer(uint8_t* p)`
|
|
||||||
|
|
||||||
Frees a buffer created with `create_buffer`.
|
|
||||||
|
|
||||||
### `void encode(uint8_t* image_buffer, int image_width, int image_height, int quality)`
|
|
||||||
|
|
||||||
Encodes the given image with given dimension to JPEG. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
|
|
||||||
|
|
||||||
### `void free_result()`
|
|
||||||
|
|
||||||
Frees the result created by `encode()`.
|
|
||||||
|
|
||||||
### `int get_result_pointer()`
|
|
||||||
|
|
||||||
Returns the pointer to the start of the buffer holding the encoded data.
|
|
||||||
|
|
||||||
### `int get_result_size()`
|
|
||||||
|
|
||||||
Returns the length of the buffer holding the encoded data.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<script src='mozjpeg_enc.js'></script>
|
|
||||||
<script>
|
|
||||||
const Module = mozjpeg_enc();
|
|
||||||
|
|
||||||
async function loadImage(src) {
|
|
||||||
// Load image
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = src;
|
|
||||||
await new Promise(resolve => img.onload = resolve);
|
|
||||||
// Make canvas same size as image
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
[canvas.width, canvas.height] = [img.width, img.height];
|
|
||||||
// Draw image onto canvas
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
return ctx.getImageData(0, 0, img.width, img.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async _ => {
|
|
||||||
const api = {
|
|
||||||
version: Module.cwrap('version', 'number', []),
|
|
||||||
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
|
||||||
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
|
||||||
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
|
||||||
free_result: Module.cwrap('free_result', '', ['number']),
|
|
||||||
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
|
||||||
get_result_size: Module.cwrap('get_result_size', 'number', []),
|
|
||||||
};
|
|
||||||
const image = await loadImage('../example.png');
|
|
||||||
const p = api.create_buffer(image.width, image.height);
|
|
||||||
Module.HEAP8.set(image.data, p);
|
|
||||||
api.encode(p, image.width, image.height, 2);
|
|
||||||
const resultPointer = api.get_result_pointer();
|
|
||||||
const resultSize = api.get_result_size();
|
|
||||||
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
|
|
||||||
const result = new Uint8Array(resultView);
|
|
||||||
api.free_result(resultPointer);
|
|
||||||
api.destroy_buffer(p);
|
|
||||||
|
|
||||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = blobURL;
|
|
||||||
document.body.appendChild(img);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
#include "emscripten.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <inttypes.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <setjmp.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include "jpeglib.h"
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
// MozJPEG doesn’t expose a numeric version, so I have to do some fun C macro hackery to turn it into a string. More details here: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
|
|
||||||
#define xstr(s) str(s)
|
|
||||||
#define str(s) #s
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int version() {
|
|
||||||
char buffer[] = xstr(MOZJPEG_VERSION);
|
|
||||||
int version = 0;
|
|
||||||
int last_index = 0;
|
|
||||||
for(int i = 0; i < strlen(buffer); i++) {
|
|
||||||
if(buffer[i] == '.') {
|
|
||||||
buffer[i] = '\0';
|
|
||||||
version = version << 8 | atoi(&buffer[last_index]);
|
|
||||||
buffer[i] = '.';
|
|
||||||
last_index = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
version = version << 8 | atoi(&buffer[last_index]);
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
uint8_t* create_buffer(int width, int height) {
|
|
||||||
return malloc(width * height * 4 * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void destroy_buffer(uint8_t* p) {
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
int result[2];
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void encode(uint8_t* image_buffer, int image_width, int image_height, int quality) {
|
|
||||||
// Manually convert RGBA data into RGB
|
|
||||||
for(int y = 0; y < image_height; y++) {
|
|
||||||
for(int x = 0; x < image_width; x++) {
|
|
||||||
image_buffer[(y*image_width + x)*3 + 0] = image_buffer[(y*image_width + x)*4 + 0];
|
|
||||||
image_buffer[(y*image_width + x)*3 + 1] = image_buffer[(y*image_width + x)*4 + 1];
|
|
||||||
image_buffer[(y*image_width + x)*3 + 2] = image_buffer[(y*image_width + x)*4 + 2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The code below is basically the `write_JPEG_file` function from
|
|
||||||
// https://github.com/mozilla/mozjpeg/blob/master/example.c
|
|
||||||
// I just write to memory instead of a file.
|
|
||||||
|
|
||||||
|
|
||||||
/* This struct contains the JPEG compression parameters and pointers to
|
|
||||||
* working space (which is allocated as needed by the JPEG library).
|
|
||||||
* It is possible to have several such structures, representing multiple
|
|
||||||
* compression/decompression processes, in existence at once. We refer
|
|
||||||
* to any one struct (and its associated working data) as a "JPEG object".
|
|
||||||
*/
|
|
||||||
struct jpeg_compress_struct cinfo;
|
|
||||||
/* This struct represents a JPEG error handler. It is declared separately
|
|
||||||
* because applications often want to supply a specialized error handler
|
|
||||||
* (see the second half of this file for an example). But here we just
|
|
||||||
* take the easy way out and use the standard error handler, which will
|
|
||||||
* print a message on stderr and call exit() if compression fails.
|
|
||||||
* Note that this struct must live as long as the main JPEG parameter
|
|
||||||
* struct, to avoid dangling-pointer problems.
|
|
||||||
*/
|
|
||||||
struct jpeg_error_mgr jerr;
|
|
||||||
/* More stuff */
|
|
||||||
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
|
|
||||||
int row_stride; /* physical row width in image buffer */
|
|
||||||
uint8_t* output;
|
|
||||||
unsigned long size;
|
|
||||||
|
|
||||||
/* Step 1: allocate and initialize JPEG compression object */
|
|
||||||
|
|
||||||
/* We have to set up the error handler first, in case the initialization
|
|
||||||
* step fails. (Unlikely, but it could happen if you are out of memory.)
|
|
||||||
* This routine fills in the contents of struct jerr, and returns jerr's
|
|
||||||
* address which we place into the link field in cinfo.
|
|
||||||
*/
|
|
||||||
cinfo.err = jpeg_std_error(&jerr);
|
|
||||||
/* Now we can initialize the JPEG compression object. */
|
|
||||||
jpeg_create_compress(&cinfo);
|
|
||||||
|
|
||||||
/* Step 2: specify data destination (eg, a file) */
|
|
||||||
/* Note: steps 2 and 3 can be done in either order. */
|
|
||||||
|
|
||||||
/* Here we use the library-supplied code to send compressed data to a
|
|
||||||
* stdio stream. You can also write your own code to do something else.
|
|
||||||
* VERY IMPORTANT: use "b" option to fopen() if you are on a machine that
|
|
||||||
* requires it in order to write binary files.
|
|
||||||
*/
|
|
||||||
// if ((outfile = fopen(filename, "wb")) == NULL) {
|
|
||||||
// fprintf(stderr, "can't open %s\n", filename);
|
|
||||||
// exit(1);
|
|
||||||
// }
|
|
||||||
jpeg_mem_dest(&cinfo, &output, &size);
|
|
||||||
|
|
||||||
/* Step 3: set parameters for compression */
|
|
||||||
|
|
||||||
/* First we supply a description of the input image.
|
|
||||||
* Four fields of the cinfo struct must be filled in:
|
|
||||||
*/
|
|
||||||
cinfo.image_width = image_width; /* image width and height, in pixels */
|
|
||||||
cinfo.image_height = image_height;
|
|
||||||
cinfo.input_components = 3; /* # of color components per pixel */
|
|
||||||
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
|
|
||||||
/* Now use the library's routine to set default compression parameters.
|
|
||||||
* (You must set at least cinfo.in_color_space before calling this,
|
|
||||||
* since the defaults depend on the source color space.)
|
|
||||||
*/
|
|
||||||
jpeg_set_defaults(&cinfo);
|
|
||||||
/* Now you can set any non-default parameters you wish to.
|
|
||||||
* Here we just illustrate the use of quality (quantization table) scaling:
|
|
||||||
*/
|
|
||||||
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
|
|
||||||
|
|
||||||
/* Step 4: Start compressor */
|
|
||||||
|
|
||||||
/* TRUE ensures that we will write a complete interchange-JPEG file.
|
|
||||||
* Pass TRUE unless you are very sure of what you're doing.
|
|
||||||
*/
|
|
||||||
jpeg_start_compress(&cinfo, TRUE);
|
|
||||||
|
|
||||||
/* Step 5: while (scan lines remain to be written) */
|
|
||||||
/* jpeg_write_scanlines(...); */
|
|
||||||
|
|
||||||
/* Here we use the library's state variable cinfo.next_scanline as the
|
|
||||||
* loop counter, so that we don't have to keep track ourselves.
|
|
||||||
* To keep things simple, we pass one scanline per call; you can pass
|
|
||||||
* more if you wish, though.
|
|
||||||
*/
|
|
||||||
row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
|
|
||||||
|
|
||||||
while (cinfo.next_scanline < cinfo.image_height) {
|
|
||||||
/* jpeg_write_scanlines expects an array of pointers to scanlines.
|
|
||||||
* Here the array is only one element long, but you could pass
|
|
||||||
* more than one scanline at a time if that's more convenient.
|
|
||||||
*/
|
|
||||||
row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
|
|
||||||
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Step 6: Finish compression */
|
|
||||||
|
|
||||||
jpeg_finish_compress(&cinfo);
|
|
||||||
/* Step 7: release JPEG compression object */
|
|
||||||
|
|
||||||
result[0] = (int)output;
|
|
||||||
result[1] = size;
|
|
||||||
|
|
||||||
/* This is an important step since it will release a good deal of memory. */
|
|
||||||
jpeg_destroy_compress(&cinfo);
|
|
||||||
|
|
||||||
/* And we're done! */
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void free_result() {
|
|
||||||
free(result[0]); // not sure if this is right with mozjpeg
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_pointer() {
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_size() {
|
|
||||||
return result[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
1
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
1
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
1147
codecs/mozjpeg_enc/package-lock.json
generated
1147
codecs/mozjpeg_enc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "mozjpeg_enc",
|
|
||||||
"scripts": {
|
|
||||||
"install": "napa",
|
|
||||||
"build": "npm run build:library && npm run build:wasm",
|
|
||||||
"build:library": "cd node_modules/mozjpeg && autoreconf -fiv && docker run --rm -v $(pwd):/src trzeci/emscripten emconfigure ./configure --without-simd && docker run --rm -v $(pwd):/src trzeci/emscripten emmake make libjpeg.la",
|
|
||||||
"build:wasm": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"mozjpeg_enc\"' -I node_modules/mozjpeg -o ./mozjpeg_enc.js mozjpeg_enc.c node_modules/mozjpeg/.libs/libjpeg.a"
|
|
||||||
},
|
|
||||||
"napa": {
|
|
||||||
"mozjpeg": "mozilla/mozjpeg#v3.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"napa": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# WebP encoder
|
|
||||||
|
|
||||||
- Source: <https://github.com/webmproject/libwebp>
|
|
||||||
- Version: v0.6.1
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
See `example.html`
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
### `int version()`
|
|
||||||
|
|
||||||
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
|
|
||||||
|
|
||||||
### `uint8_t* create_buffer(int width, int height)`
|
|
||||||
|
|
||||||
Allocates an RGBA buffer for an image with the given dimension.
|
|
||||||
|
|
||||||
### `void destroy_buffer(uint8_t* p)`
|
|
||||||
|
|
||||||
Frees a buffer created with `create_buffer`.
|
|
||||||
|
|
||||||
### `void encode(uint8_t* image_buffer, int image_width, int image_height, float quality)`
|
|
||||||
|
|
||||||
Encodes the given image with given dimension to WebP. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
|
|
||||||
|
|
||||||
### `void free_result()`
|
|
||||||
|
|
||||||
Frees the result created by `encode()`.
|
|
||||||
|
|
||||||
### `int get_result_pointer()`
|
|
||||||
|
|
||||||
Returns the pointer to the start of the buffer holding the encoded data.
|
|
||||||
|
|
||||||
### `int get_result_size()`
|
|
||||||
|
|
||||||
Returns the length of the buffer holding the encoded data.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<script src='webp_enc.js'></script>
|
|
||||||
<script>
|
|
||||||
const Module = webp_enc();
|
|
||||||
|
|
||||||
async function loadImage(src) {
|
|
||||||
// Load image
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = src;
|
|
||||||
await new Promise(resolve => img.onload = resolve);
|
|
||||||
// Make canvas same size as image
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
[canvas.width, canvas.height] = [img.width, img.height];
|
|
||||||
// Draw image onto canvas
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
return ctx.getImageData(0, 0, img.width, img.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async _ => {
|
|
||||||
const api = {
|
|
||||||
version: Module.cwrap('version', 'number', []),
|
|
||||||
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
|
||||||
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
|
||||||
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
|
||||||
free_result: Module.cwrap('free_result', '', ['number']),
|
|
||||||
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
|
||||||
get_result_size: Module.cwrap('get_result_size', 'number', []),
|
|
||||||
};
|
|
||||||
const image = await loadImage('../example.png');
|
|
||||||
const p = api.create_buffer(image.width, image.height);
|
|
||||||
Module.HEAP8.set(image.data, p);
|
|
||||||
api.encode(p, image.width, image.height, 2);
|
|
||||||
const resultPointer = api.get_result_pointer();
|
|
||||||
const resultSize = api.get_result_size();
|
|
||||||
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
|
|
||||||
const result = new Uint8Array(resultView);
|
|
||||||
api.free_result(resultPointer);
|
|
||||||
api.destroy_buffer(p);
|
|
||||||
|
|
||||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = blobURL;
|
|
||||||
document.body.appendChild(img);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
1147
codecs/webp_enc/package-lock.json
generated
1147
codecs/webp_enc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "webp_enc",
|
|
||||||
"scripts": {
|
|
||||||
"install": "napa",
|
|
||||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_enc\"' -I node_modules/libwebp -o ./webp_enc.js webp_enc.c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c"
|
|
||||||
},
|
|
||||||
"napa": {
|
|
||||||
"libwebp": "webmproject/libwebp#v0.6.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"napa": "^3.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#include "emscripten.h"
|
|
||||||
#include "src/webp/encode.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int version() {
|
|
||||||
return WebPGetEncoderVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
uint8_t* create_buffer(int width, int height) {
|
|
||||||
return malloc(width * height * 4 * sizeof(uint8_t));
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void destroy_buffer(uint8_t* p) {
|
|
||||||
free(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
int result[2];
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void encode(uint8_t* img_in, int width, int height, float quality) {
|
|
||||||
uint8_t* img_out;
|
|
||||||
size_t size;
|
|
||||||
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
|
|
||||||
result[0] = (int)img_out;
|
|
||||||
result[1] = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
void free_result() {
|
|
||||||
WebPFree(result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_pointer() {
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
EMSCRIPTEN_KEEPALIVE
|
|
||||||
int get_result_size() {
|
|
||||||
return result[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
107
emscripten-wasm.d.ts
vendored
107
emscripten-wasm.d.ts
vendored
@@ -1,107 +0,0 @@
|
|||||||
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
|
|
||||||
// TODO(@surma): Upstream this?
|
|
||||||
declare namespace EmscriptenWasm {
|
|
||||||
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER";
|
|
||||||
|
|
||||||
// Options object for modularized Emscripten files. Shoe-horned by @surma.
|
|
||||||
// FIXME: This an incomplete definition!
|
|
||||||
interface ModuleOpts {
|
|
||||||
noInitialRun?: boolean;
|
|
||||||
locateFile?: (url: string) => string;
|
|
||||||
onRuntimeInitialized?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Module {
|
|
||||||
print(str: string): void;
|
|
||||||
printErr(str: string): void;
|
|
||||||
arguments: string[];
|
|
||||||
environment: EnvironmentType;
|
|
||||||
preInit: { (): void }[];
|
|
||||||
preRun: { (): void }[];
|
|
||||||
postRun: { (): void }[];
|
|
||||||
preinitializedWebGLContext: WebGLRenderingContext;
|
|
||||||
noInitialRun: boolean;
|
|
||||||
noExitRuntime: boolean;
|
|
||||||
logReadFiles: boolean;
|
|
||||||
filePackagePrefixURL: string;
|
|
||||||
wasmBinary: ArrayBuffer;
|
|
||||||
|
|
||||||
destroy(object: object): void;
|
|
||||||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
|
|
||||||
instantiateWasm(
|
|
||||||
imports: WebAssembly.Imports,
|
|
||||||
successCallback: (module: WebAssembly.Module) => void
|
|
||||||
): WebAssembly.Exports;
|
|
||||||
locateFile(url: string): string;
|
|
||||||
onCustomMessage(event: MessageEvent): void;
|
|
||||||
|
|
||||||
Runtime: any;
|
|
||||||
|
|
||||||
ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any;
|
|
||||||
cwrap(ident: string, returnType: string | null, argTypes: string[]): any;
|
|
||||||
|
|
||||||
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
|
|
||||||
getValue(ptr: number, type: string, noSafe?: boolean): number;
|
|
||||||
|
|
||||||
ALLOC_NORMAL: number;
|
|
||||||
ALLOC_STACK: number;
|
|
||||||
ALLOC_STATIC: number;
|
|
||||||
ALLOC_DYNAMIC: number;
|
|
||||||
ALLOC_NONE: number;
|
|
||||||
|
|
||||||
allocate(slab: any, types: string, allocator: number, ptr: number): number;
|
|
||||||
allocate(slab: any, types: string[], allocator: number, ptr: number): number;
|
|
||||||
|
|
||||||
Pointer_stringify(ptr: number, length?: number): string;
|
|
||||||
UTF16ToString(ptr: number): string;
|
|
||||||
stringToUTF16(str: string, outPtr: number): void;
|
|
||||||
UTF32ToString(ptr: number): string;
|
|
||||||
stringToUTF32(str: string, outPtr: number): void;
|
|
||||||
|
|
||||||
// USE_TYPED_ARRAYS == 1
|
|
||||||
HEAP: Int32Array;
|
|
||||||
IHEAP: Int32Array;
|
|
||||||
FHEAP: Float64Array;
|
|
||||||
|
|
||||||
// USE_TYPED_ARRAYS == 2
|
|
||||||
HEAP8: Int8Array;
|
|
||||||
HEAP16: Int16Array;
|
|
||||||
HEAP32: Int32Array;
|
|
||||||
HEAPU8: Uint8Array;
|
|
||||||
HEAPU16: Uint16Array;
|
|
||||||
HEAPU32: Uint32Array;
|
|
||||||
HEAPF32: Float32Array;
|
|
||||||
HEAPF64: Float64Array;
|
|
||||||
|
|
||||||
TOTAL_STACK: number;
|
|
||||||
TOTAL_MEMORY: number;
|
|
||||||
FAST_MEMORY: number;
|
|
||||||
|
|
||||||
addOnPreRun(cb: () => any): void;
|
|
||||||
addOnInit(cb: () => any): void;
|
|
||||||
addOnPreMain(cb: () => any): void;
|
|
||||||
addOnExit(cb: () => any): void;
|
|
||||||
addOnPostRun(cb: () => any): void;
|
|
||||||
|
|
||||||
// Tools
|
|
||||||
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
|
|
||||||
intArrayToString(array: number[]): string;
|
|
||||||
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
|
||||||
writeArrayToMemory(array: number[], buffer: number): void;
|
|
||||||
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
|
||||||
|
|
||||||
addRunDependency(id: any): void;
|
|
||||||
removeRunDependency(id: any): void;
|
|
||||||
|
|
||||||
|
|
||||||
preloadedImages: any;
|
|
||||||
preloadedAudios: any;
|
|
||||||
|
|
||||||
_malloc(size: number): number;
|
|
||||||
_free(ptr: number): void;
|
|
||||||
|
|
||||||
// Augmentations below by @surma.
|
|
||||||
onRuntimeInitialized: () => void | null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": "build",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
],
|
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "**",
|
|
||||||
"destination": "/index.html"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "Access-Control-Allow-Origin",
|
|
||||||
"value": "*"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "**/*.@(jpg|jpeg|gif|png|ico)",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "Cache-Control",
|
|
||||||
"value": "max-age=7200"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "**/*.@(js|css|json|manifest|map)",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "Cache-Control",
|
|
||||||
"value": "max-age=31536000"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "sw.js",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "Cache-Control",
|
|
||||||
"value": "max-age=0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
113
karma.conf.js
113
karma.conf.js
@@ -1,113 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
function readJsonFile(path) {
|
|
||||||
// TypeScript puts lots of comments in the default `tsconfig.json`, so you
|
|
||||||
// can’t use `require()` to read it. Hence this hack.
|
|
||||||
return eval("(" + fs.readFileSync(path).toString("utf-8") + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeScriptConfig = readJsonFile("./tsconfig.json");
|
|
||||||
const babel = readJsonFile("./.babelrc");
|
|
||||||
|
|
||||||
module.exports = function(config) {
|
|
||||||
const options = {
|
|
||||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
|
||||||
basePath: "",
|
|
||||||
|
|
||||||
// frameworks to use
|
|
||||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
|
||||||
frameworks: ["mocha", "chai", "karma-typescript"],
|
|
||||||
|
|
||||||
// list of files / patterns to load in the browser
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
pattern: "test/**/*.ts",
|
|
||||||
type: "module"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: "src/**/*.ts",
|
|
||||||
included: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
// list of files / patterns to exclude
|
|
||||||
exclude: [],
|
|
||||||
// preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
|
||||||
preprocessors: {
|
|
||||||
"src/**/*.ts": ["karma-typescript", "babel"],
|
|
||||||
"test/**/*.ts": ["karma-typescript", "babel"]
|
|
||||||
},
|
|
||||||
babelPreprocessor: {
|
|
||||||
options: babel
|
|
||||||
},
|
|
||||||
karmaTypescriptConfig: {
|
|
||||||
// Inline `tsconfig.json` so that the right TS libs are loaded
|
|
||||||
...typeScriptConfig,
|
|
||||||
// Coverage is a thing that karma-typescript forces on you and only
|
|
||||||
// creates problems. This is the simplest way of disabling it that I
|
|
||||||
// could find.
|
|
||||||
coverageOptions: {
|
|
||||||
exclude: /.*/
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mime: {
|
|
||||||
// Default mimetype for .ts files is video/mp2t but we need
|
|
||||||
// text/javascript for modules to work.
|
|
||||||
"text/javascript": ["ts"]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
// Load all modules whose name starts with "karma" (usually the default).
|
|
||||||
"karma-*",
|
|
||||||
// We don’t have file extensions on our imports as they are primarily
|
|
||||||
// consumed by webpack. With Karma, however, this turns into a real HTTP
|
|
||||||
// request for a non-existent file. This inline plugin is a middleware
|
|
||||||
// that appends `.ts` to the request URL.
|
|
||||||
{
|
|
||||||
"middleware:redirect_to_ts": [
|
|
||||||
"value",
|
|
||||||
(req, res, next) => {
|
|
||||||
if (req.url.startsWith("/base/src")) {
|
|
||||||
req.url += '.ts';
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Run our middleware before all other middlewares.
|
|
||||||
beforeMiddleware: ["redirect_to_ts"],
|
|
||||||
|
|
||||||
// test results reporter to use
|
|
||||||
// possible values: 'dots', 'progress'
|
|
||||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
|
||||||
reporters: ["progress"],
|
|
||||||
|
|
||||||
// web server port
|
|
||||||
port: 9876,
|
|
||||||
|
|
||||||
// enable / disable colors in the output (reporters and logs)
|
|
||||||
colors: true,
|
|
||||||
|
|
||||||
// level of logging
|
|
||||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
|
|
||||||
// enable / disable watching file and executing tests whenever any file changes
|
|
||||||
autoWatch: false,
|
|
||||||
|
|
||||||
// start these browsers
|
|
||||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
|
||||||
browsers: ["ChromeHeadless"],
|
|
||||||
|
|
||||||
// Continuous Integration mode
|
|
||||||
// if true, Karma captures browsers, runs the tests and exits
|
|
||||||
singleRun: true,
|
|
||||||
|
|
||||||
// These custom files allow us to use ES6 modules in our tests.
|
|
||||||
// Remove these 2 lines (and files) once https://github.com/karma-runner/karma/pull/2834 lands.
|
|
||||||
customContextFile: "test/context.html",
|
|
||||||
customDebugFile: "test/debug.html"
|
|
||||||
};
|
|
||||||
|
|
||||||
config.set(options);
|
|
||||||
};
|
|
||||||
8512
package-lock.json
generated
8512
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -4,12 +4,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "apache-2.0",
|
"license": "apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:mozjpeg_enc": "cd codecs/mozjpeg_enc && npm run build",
|
"start": "webpack serve --hot",
|
||||||
"build:codecs": "npm run build:mozjpeg_enc",
|
|
||||||
"start": "webpack serve --host 0.0.0.0 --hot",
|
|
||||||
"build": "webpack -p",
|
"build": "webpack -p",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src"
|
||||||
"test": "npm run build && mocha -R spec && karma start"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -32,11 +29,7 @@
|
|||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.1.3",
|
|
||||||
"@types/karma": "^1.7.3",
|
|
||||||
"@types/mocha": "^5.2.0",
|
|
||||||
"@types/node": "^9.4.7",
|
"@types/node": "^9.4.7",
|
||||||
"@types/webassembly-js-api": "0.0.1",
|
|
||||||
"babel-loader": "^7.1.4",
|
"babel-loader": "^7.1.4",
|
||||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
@@ -48,7 +41,6 @@
|
|||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
|
||||||
"babel-preset-env": "^1.6.1",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-register": "^6.26.0",
|
"babel-register": "^6.26.0",
|
||||||
"chai": "^4.1.2",
|
|
||||||
"clean-webpack-plugin": "^0.1.19",
|
"clean-webpack-plugin": "^0.1.19",
|
||||||
"copy-webpack-plugin": "^4.5.1",
|
"copy-webpack-plugin": "^4.5.1",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
@@ -60,25 +52,13 @@
|
|||||||
"eslint-plugin-promise": "^3.7.0",
|
"eslint-plugin-promise": "^3.7.0",
|
||||||
"eslint-plugin-react": "^7.7.0",
|
"eslint-plugin-react": "^7.7.0",
|
||||||
"eslint-plugin-standard": "^3.0.1",
|
"eslint-plugin-standard": "^3.0.1",
|
||||||
"exports-loader": "^0.7.0",
|
|
||||||
"express": "^4.16.3",
|
|
||||||
"file-loader": "^1.1.11",
|
|
||||||
"html-webpack-plugin": "^3.0.6",
|
"html-webpack-plugin": "^3.0.6",
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"karma": "^2.0.2",
|
|
||||||
"karma-babel-preprocessor": "^7.0.0",
|
|
||||||
"karma-chai": "^0.1.0",
|
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
|
||||||
"karma-mocha": "^1.3.0",
|
|
||||||
"karma-typescript": "^3.0.12",
|
|
||||||
"loader-utils": "^1.1.0",
|
"loader-utils": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^0.3.0",
|
"mini-css-extract-plugin": "^0.3.0",
|
||||||
"mocha": "^5.2.0",
|
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
||||||
"prettier": "^1.12.1",
|
|
||||||
"progress-bar-webpack-plugin": "^1.11.0",
|
"progress-bar-webpack-plugin": "^1.11.0",
|
||||||
"puppeteer": "^1.3.0",
|
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"sass-loader": "^6.0.7",
|
"sass-loader": "^6.0.7",
|
||||||
"script-ext-html-webpack-plugin": "^2.0.1",
|
"script-ext-html-webpack-plugin": "^2.0.1",
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind, bitmapToImageData } from '../../lib/util';
|
import { bind } from '../../lib/util';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import Output from '../output';
|
import Output from '../output';
|
||||||
|
|
||||||
import {MozJpegEncoder} from '../../lib/codec-wrappers/mozjpeg-enc';
|
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -30,13 +28,8 @@ export default class App extends Component<Props, State> {
|
|||||||
const fileInput = event.target as HTMLInputElement;
|
const fileInput = event.target as HTMLInputElement;
|
||||||
if (!fileInput.files || !fileInput.files[0]) return;
|
if (!fileInput.files || !fileInput.files[0]) return;
|
||||||
// TODO: handle decode error
|
// TODO: handle decode error
|
||||||
const bitmap = await createImageBitmap(fileInput.files[0]);
|
const img = await createImageBitmap(fileInput.files[0]);
|
||||||
const data = await bitmapToImageData(bitmap);
|
this.setState({ img });
|
||||||
const encoder = new MozJpegEncoder();
|
|
||||||
const compressedData = await encoder.encode(data);
|
|
||||||
const blob = new Blob([compressedData], {type: 'image/jpeg'});
|
|
||||||
const compressedImage = await createImageBitmap(blob);
|
|
||||||
this.setState({ img: compressedImage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render({ }: Props, { img }: State) {
|
render({ }: Props, { img }: State) {
|
||||||
@@ -54,4 +47,3 @@ export default class App extends Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
import './styles.css';
|
|
||||||
import { PointerTracker, Pointer } from '../../../../lib/PointerTracker';
|
|
||||||
|
|
||||||
interface Point {
|
|
||||||
clientX: number;
|
|
||||||
clientY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApplyChangeOpts {
|
|
||||||
panX?: number;
|
|
||||||
panY?: number;
|
|
||||||
scaleDiff?: number;
|
|
||||||
originX?: number;
|
|
||||||
originY?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SetTransformOpts {
|
|
||||||
scale?: number;
|
|
||||||
x?: number;
|
|
||||||
y?: number;
|
|
||||||
/**
|
|
||||||
* Fire a 'change' event if values are different to current values
|
|
||||||
*/
|
|
||||||
allowChangeEvent?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDistance (a: Point, b?: Point): number {
|
|
||||||
if (!b) return 0;
|
|
||||||
return Math.sqrt((b.clientX - a.clientX) ** 2 + (b.clientY - a.clientY) ** 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMidpoint (a: Point, b?: Point): Point {
|
|
||||||
if (!b) return a;
|
|
||||||
|
|
||||||
return {
|
|
||||||
clientX: (a.clientX + b.clientX) / 2,
|
|
||||||
clientY: (a.clientY + b.clientY) / 2
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// I'd rather use DOMMatrix/DOMPoint here, but the browser support isn't good enough.
|
|
||||||
// Given that, better to use something everything supports.
|
|
||||||
let cachedSvg: SVGSVGElement;
|
|
||||||
|
|
||||||
function getSVG (): SVGSVGElement {
|
|
||||||
return cachedSvg || (cachedSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMatrix (): SVGMatrix {
|
|
||||||
return getSVG().createSVGMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPoint (): SVGPoint {
|
|
||||||
return getSVG().createSVGPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class PinchZoom extends HTMLElement {
|
|
||||||
// The element that we'll transform.
|
|
||||||
// Ideally this would be shadow DOM, but we don't have the browser
|
|
||||||
// support yet.
|
|
||||||
private _positioningEl?: Element;
|
|
||||||
// Current transform.
|
|
||||||
private _transform: SVGMatrix = createMatrix();
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super();
|
|
||||||
|
|
||||||
// Watch for children changes.
|
|
||||||
// Note this won't fire for initial contents,
|
|
||||||
// so _stageElChange is also called in connectedCallback.
|
|
||||||
new MutationObserver(() => this._stageElChange())
|
|
||||||
.observe(this, { childList: true });
|
|
||||||
|
|
||||||
// Watch for pointers
|
|
||||||
const pointerTracker: PointerTracker = new PointerTracker(this, {
|
|
||||||
start: (pointer, event) => {
|
|
||||||
// We only want to track 2 pointers at most
|
|
||||||
if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false;
|
|
||||||
event.preventDefault();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
move: previousPointers => {
|
|
||||||
this._onPointerMove(previousPointers, pointerTracker.currentPointers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addEventListener('wheel', event => this._onWheel(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback () {
|
|
||||||
this._stageElChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
get x () {
|
|
||||||
return this._transform.e;
|
|
||||||
}
|
|
||||||
|
|
||||||
get y () {
|
|
||||||
return this._transform.f;
|
|
||||||
}
|
|
||||||
|
|
||||||
get scale () {
|
|
||||||
return this._transform.a;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the stage with a given scale/x/y.
|
|
||||||
*/
|
|
||||||
setTransform (opts: SetTransformOpts = {}) {
|
|
||||||
const {
|
|
||||||
scale = this.scale,
|
|
||||||
allowChangeEvent = false
|
|
||||||
} = opts;
|
|
||||||
|
|
||||||
let {
|
|
||||||
x = this.x,
|
|
||||||
y = this.y
|
|
||||||
} = opts;
|
|
||||||
|
|
||||||
// If we don't have an element to position, just set the value as given.
|
|
||||||
// We'll check bounds later.
|
|
||||||
if (!this._positioningEl) {
|
|
||||||
this._updateTransform(scale, x, y, allowChangeEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current layout
|
|
||||||
const thisBounds = this.getBoundingClientRect();
|
|
||||||
const positioningElBounds = this._positioningEl.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Not displayed. May be disconnected or display:none.
|
|
||||||
// Just take the values, and we'll check bounds later.
|
|
||||||
if (!thisBounds.width || !thisBounds.height) {
|
|
||||||
this._updateTransform(scale, x, y, allowChangeEvent);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create points for _positioningEl.
|
|
||||||
let topLeft = createPoint();
|
|
||||||
topLeft.x = positioningElBounds.left - thisBounds.left;
|
|
||||||
topLeft.y = positioningElBounds.top - thisBounds.top;
|
|
||||||
let bottomRight = createPoint();
|
|
||||||
bottomRight.x = positioningElBounds.width + topLeft.x;
|
|
||||||
bottomRight.y = positioningElBounds.height + topLeft.y;
|
|
||||||
|
|
||||||
// Calculate the intended position of _positioningEl.
|
|
||||||
let matrix = createMatrix()
|
|
||||||
.translate(x, y)
|
|
||||||
.scale(scale)
|
|
||||||
// Undo current transform
|
|
||||||
.multiply(this._transform.inverse());
|
|
||||||
|
|
||||||
topLeft = topLeft.matrixTransform(matrix);
|
|
||||||
bottomRight = bottomRight.matrixTransform(matrix);
|
|
||||||
|
|
||||||
// Ensure _positioningEl can't move beyond out-of-bounds.
|
|
||||||
// Correct for x
|
|
||||||
if (topLeft.x > thisBounds.width) {
|
|
||||||
x += thisBounds.width - topLeft.x;
|
|
||||||
} else if (bottomRight.x < 0) {
|
|
||||||
x += -bottomRight.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correct for y
|
|
||||||
if (topLeft.y > thisBounds.height) {
|
|
||||||
y += thisBounds.height - topLeft.y;
|
|
||||||
} else if (bottomRight.y < 0) {
|
|
||||||
y += -bottomRight.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateTransform(scale, x, y, allowChangeEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update transform values without checking bounds. This is only called in setTransform.
|
|
||||||
*/
|
|
||||||
_updateTransform (scale: number, x: number, y: number, allowChangeEvent: boolean) {
|
|
||||||
// Return if there's no change
|
|
||||||
if (
|
|
||||||
scale === this.scale &&
|
|
||||||
x === this.x &&
|
|
||||||
y === this.y
|
|
||||||
) return;
|
|
||||||
|
|
||||||
this._transform.e = x;
|
|
||||||
this._transform.f = y;
|
|
||||||
this._transform.d = this._transform.a = scale;
|
|
||||||
|
|
||||||
this.style.setProperty('--x', this.x + 'px');
|
|
||||||
this.style.setProperty('--y', this.y + 'px');
|
|
||||||
this.style.setProperty('--scale', this.scale + '');
|
|
||||||
|
|
||||||
if (allowChangeEvent) {
|
|
||||||
const event = new Event('change', { bubbles: true });
|
|
||||||
this.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the direct children of this element change.
|
|
||||||
* Until we have have shadow dom support across the board, we
|
|
||||||
* require a single element to be the child of <pinch-zoom>, and
|
|
||||||
* that's the element we pan/scale.
|
|
||||||
*/
|
|
||||||
private _stageElChange () {
|
|
||||||
this._positioningEl = undefined;
|
|
||||||
|
|
||||||
if (this.children.length === 0) {
|
|
||||||
console.warn('There should be at least one child in <pinch-zoom>.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._positioningEl = this.children[0];
|
|
||||||
|
|
||||||
if (this.children.length > 1) {
|
|
||||||
console.warn('<pinch-zoom> must not have more than one child.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a bounds check
|
|
||||||
this.setTransform();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onWheel (event: WheelEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const thisRect = this.getBoundingClientRect();
|
|
||||||
let { deltaY } = event;
|
|
||||||
const { ctrlKey, deltaMode } = event;
|
|
||||||
|
|
||||||
if (deltaMode === 1) { // 1 is "lines", 0 is "pixels"
|
|
||||||
// Firefox uses "lines" for some types of mouse
|
|
||||||
deltaY *= 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ctrlKey is true when pinch-zooming on a trackpad.
|
|
||||||
const divisor = ctrlKey ? 100 : 300;
|
|
||||||
const scaleDiff = 1 - deltaY / divisor;
|
|
||||||
|
|
||||||
this._applyChange({
|
|
||||||
scaleDiff,
|
|
||||||
originX: event.clientX - thisRect.left,
|
|
||||||
originY: event.clientY - thisRect.top
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onPointerMove (previousPointers: Pointer[], currentPointers: Pointer[]) {
|
|
||||||
// Combine next points with previous points
|
|
||||||
const thisRect = this.getBoundingClientRect();
|
|
||||||
|
|
||||||
// For calculating panning movement
|
|
||||||
const prevMidpoint = getMidpoint(previousPointers[0], previousPointers[1]);
|
|
||||||
const newMidpoint = getMidpoint(currentPointers[0], currentPointers[1]);
|
|
||||||
|
|
||||||
// Midpoint within the element
|
|
||||||
const originX = prevMidpoint.clientX - thisRect.left;
|
|
||||||
const originY = prevMidpoint.clientY - thisRect.top;
|
|
||||||
|
|
||||||
// Calculate the desired change in scale
|
|
||||||
const prevDistance = getDistance(previousPointers[0], previousPointers[1]);
|
|
||||||
const newDistance = getDistance(currentPointers[0], currentPointers[1]);
|
|
||||||
const scaleDiff = prevDistance ? newDistance / prevDistance : 1;
|
|
||||||
|
|
||||||
this._applyChange({
|
|
||||||
originX, originY, scaleDiff,
|
|
||||||
panX: newMidpoint.clientX - prevMidpoint.clientX,
|
|
||||||
panY: newMidpoint.clientY - prevMidpoint.clientY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Transform the view & fire a change event */
|
|
||||||
private _applyChange (opts: ApplyChangeOpts = {}) {
|
|
||||||
const {
|
|
||||||
panX = 0, panY = 0,
|
|
||||||
originX = 0, originY = 0,
|
|
||||||
scaleDiff = 1
|
|
||||||
} = opts;
|
|
||||||
|
|
||||||
const matrix = createMatrix()
|
|
||||||
// Translate according to panning.
|
|
||||||
.translate(panX, panY)
|
|
||||||
// Scale about the origin.
|
|
||||||
.translate(originX, originY)
|
|
||||||
.scale(scaleDiff)
|
|
||||||
.translate(-originX, -originY)
|
|
||||||
// Apply current transform.
|
|
||||||
.multiply(this._transform);
|
|
||||||
|
|
||||||
// Convert the transform into basic translate & scale.
|
|
||||||
this.setTransform({
|
|
||||||
scale: matrix.a,
|
|
||||||
x: matrix.e,
|
|
||||||
y: matrix.f,
|
|
||||||
allowChangeEvent: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('pinch-zoom', PinchZoom);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
declare interface CSSStyleDeclaration {
|
|
||||||
willChange: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeScript, you make me sad.
|
|
||||||
// https://github.com/Microsoft/TypeScript/issues/18756
|
|
||||||
interface Window {
|
|
||||||
PointerEvent: typeof PointerEvent;
|
|
||||||
Touch: typeof Touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace JSX {
|
|
||||||
interface IntrinsicElements {
|
|
||||||
'pinch-zoom': HTMLAttributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
pinch-zoom {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
touch-action: none;
|
|
||||||
--scale: 1;
|
|
||||||
--x: 0;
|
|
||||||
--y: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pinch-zoom > * {
|
|
||||||
transform: translate(var(--x), var(--y)) scale(var(--scale));
|
|
||||||
transform-origin: 0 0;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import * as styles from './styles.css';
|
|
||||||
import { PointerTracker, Pointer } from '../../../../lib/PointerTracker';
|
|
||||||
|
|
||||||
const legacyClipCompat = 'legacy-clip-compat';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A split view that the user can adjust. The first child becomes
|
|
||||||
* the left-hand side, and the second child becomes the right-hand side.
|
|
||||||
*/
|
|
||||||
export default class TwoUp extends HTMLElement {
|
|
||||||
static get observedAttributes () { return [legacyClipCompat]; }
|
|
||||||
|
|
||||||
private readonly _handle = document.createElement('div');
|
|
||||||
/**
|
|
||||||
* The position of the split in pixels.
|
|
||||||
*/
|
|
||||||
private _position = 0;
|
|
||||||
/**
|
|
||||||
* The value of _position when the pointer went down.
|
|
||||||
*/
|
|
||||||
private _positionOnPointerStart = 0;
|
|
||||||
/**
|
|
||||||
* Has connectedCallback been called yet?
|
|
||||||
*/
|
|
||||||
private _everConnected = false;
|
|
||||||
|
|
||||||
constructor () {
|
|
||||||
super();
|
|
||||||
this._handle.className = styles.twoUpHandle;
|
|
||||||
|
|
||||||
// Watch for children changes.
|
|
||||||
// Note this won't fire for initial contents,
|
|
||||||
// so _childrenChange is also called in connectedCallback.
|
|
||||||
new MutationObserver(() => this._childrenChange())
|
|
||||||
.observe(this, { childList: true });
|
|
||||||
|
|
||||||
// Watch for pointers on the handle.
|
|
||||||
const pointerTracker: PointerTracker = new PointerTracker(this._handle, {
|
|
||||||
start: (_, event) => {
|
|
||||||
// We only want to track 1 pointer.
|
|
||||||
if (pointerTracker.currentPointers.length === 1) return false;
|
|
||||||
event.preventDefault();
|
|
||||||
this._positionOnPointerStart = this._position;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
move: () => {
|
|
||||||
this._pointerChange(
|
|
||||||
pointerTracker.startPointers[0],
|
|
||||||
pointerTracker.currentPointers[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback () {
|
|
||||||
this._childrenChange();
|
|
||||||
if (!this._everConnected) {
|
|
||||||
// Set the initial position of the handle.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const bounds = this.getBoundingClientRect();
|
|
||||||
this._position = bounds.width / 2;
|
|
||||||
this._setPosition();
|
|
||||||
});
|
|
||||||
this._everConnected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, this element works in browsers that don't support clip-path (Edge).
|
|
||||||
* However, this means you'll have to set the height of this element manually.
|
|
||||||
*/
|
|
||||||
get noClipPathCompat () {
|
|
||||||
return this.hasAttribute(legacyClipCompat);
|
|
||||||
}
|
|
||||||
|
|
||||||
set noClipPathCompat (val: boolean) {
|
|
||||||
if (val) {
|
|
||||||
this.setAttribute(legacyClipCompat, '');
|
|
||||||
} else {
|
|
||||||
this.removeAttribute(legacyClipCompat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when element's child list changes
|
|
||||||
*/
|
|
||||||
private _childrenChange () {
|
|
||||||
// Ensure the handle is the last child.
|
|
||||||
// The CSS depends on this.
|
|
||||||
if (this.lastElementChild !== this._handle) {
|
|
||||||
this.appendChild(this._handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a pointer moves.
|
|
||||||
*/
|
|
||||||
private _pointerChange (startPoint: Pointer, currentPoint: Pointer) {
|
|
||||||
const bounds = this.getBoundingClientRect();
|
|
||||||
this._position = this._positionOnPointerStart + (currentPoint.clientX - startPoint.clientX);
|
|
||||||
// Clamp position to element bounds.
|
|
||||||
this._position = Math.max(0, Math.min(this._position, bounds.width));
|
|
||||||
this._setPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setPosition () {
|
|
||||||
this.style.setProperty('--split-point', `${this._position}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('two-up', TwoUp);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
declare interface CSSStyleDeclaration {
|
|
||||||
willChange: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeScript, you make me sad.
|
|
||||||
// https://github.com/Microsoft/TypeScript/issues/18756
|
|
||||||
interface Window {
|
|
||||||
PointerEvent: typeof PointerEvent;
|
|
||||||
Touch: typeof Touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare namespace JSX {
|
|
||||||
interface IntrinsicElements {
|
|
||||||
"two-up": HTMLAttributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
two-up {
|
|
||||||
display: grid;
|
|
||||||
position: relative;
|
|
||||||
--split-point: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
two-up > * {
|
|
||||||
/* Overlay all children on top of each other, and let
|
|
||||||
two-up's layout contain all of them. */
|
|
||||||
grid-area: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
two-up[legacy-clip-compat] > :not(.twoUpHandle) {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twoUpHandle {
|
|
||||||
touch-action: none;
|
|
||||||
position: relative;
|
|
||||||
width: 10px;
|
|
||||||
background: red;
|
|
||||||
transform: translateX(var(--split-point)) translateX(-50%);
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.twoUpHandle::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 80px;
|
|
||||||
height: 40px;
|
|
||||||
background: red;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
two-up > :nth-child(1):not(.twoUpHandle) {
|
|
||||||
-webkit-clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
|
||||||
clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
two-up > :nth-child(2):not(.twoUpHandle) {
|
|
||||||
-webkit-clip-path: inset(0 0 0 var(--split-point));
|
|
||||||
clip-path: inset(0 0 0 var(--split-point));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Even in legacy-clip-compat, prefer clip-path if it's supported.
|
|
||||||
It performs way better in Safari.
|
|
||||||
*/
|
|
||||||
@supports not ((clip-path: inset(0 0 0 var(--split-point))) or (-webkit-clip-path: inset(0 0 0 var(--split-point)))) {
|
|
||||||
two-up[legacy-clip-compat] > :nth-child(1):not(.twoUpHandle) {
|
|
||||||
clip: rect(auto var(--split-point) auto auto);
|
|
||||||
}
|
|
||||||
|
|
||||||
two-up[legacy-clip-compat] > :nth-child(2):not(.twoUpHandle) {
|
|
||||||
clip: rect(auto auto auto var(--split-point));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import PinchZoom from './custom-els/PinchZoom';
|
// This isn't working.
|
||||||
import './custom-els/PinchZoom';
|
// https://github.com/GoogleChromeLabs/squoosh/issues/14
|
||||||
import './custom-els/TwoUp';
|
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import { bind } from '../../lib/util';
|
|
||||||
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
img: ImageBitmap
|
img: ImageBitmap
|
||||||
@@ -14,89 +11,32 @@ type State = {};
|
|||||||
|
|
||||||
export default class App extends Component<Props, State> {
|
export default class App extends Component<Props, State> {
|
||||||
state: State = {};
|
state: State = {};
|
||||||
canvasLeft?: HTMLCanvasElement;
|
canvas?: HTMLCanvasElement;
|
||||||
canvasRight?: HTMLCanvasElement;
|
|
||||||
pinchZoomLeft?: PinchZoom;
|
|
||||||
pinchZoomRight?: PinchZoom;
|
|
||||||
retargetedEvents = new WeakSet<Event>();
|
|
||||||
|
|
||||||
updateCanvases(img: ImageBitmap) {
|
constructor() {
|
||||||
for (const [i, canvas] of [this.canvasLeft, this.canvasRight].entries()) {
|
super();
|
||||||
if (!canvas) throw Error('Missing canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) throw Error('Expected 2d canvas context');
|
|
||||||
if (i === 1) {
|
|
||||||
// This is temporary, to show the images are different
|
|
||||||
ctx.filter = 'hue-rotate(180deg)';
|
|
||||||
}
|
}
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
updateCanvas(img: ImageBitmap) {
|
||||||
|
if (!this.canvas) return;
|
||||||
|
const ctx = this.canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateCanvases(this.props.img);
|
this.updateCanvas(this.props.img);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate({ img }: Props) {
|
componentDidUpdate({ img }: Props) {
|
||||||
if (img !== this.props.img) this.updateCanvases(this.props.img);
|
if (img !== this.props.img) this.updateCanvas(this.props.img);
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
onPinchZoomLeftChange(event: Event) {
|
|
||||||
if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
|
||||||
this.pinchZoomRight.setTransform({
|
|
||||||
scale: this.pinchZoomLeft.scale,
|
|
||||||
x: this.pinchZoomLeft.x,
|
|
||||||
y: this.pinchZoomLeft.y
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We're using two pinch zoom elements, but we want them to stay in sync. When one moves, we
|
|
||||||
* update the position of the other. However, this is tricky when it comes to multi-touch, when
|
|
||||||
* one finger is on one pinch-zoom, and the other finger is on the other. To overcome this, we
|
|
||||||
* redirect all relevant pointer/touch/mouse events to the first pinch zoom element.
|
|
||||||
*
|
|
||||||
* @param event Event to redirect
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
onRetargetableEvent(event: Event) {
|
|
||||||
const targetEl = event.target as HTMLElement;
|
|
||||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
|
||||||
// If the event is on the handle of the two-up, let it through.
|
|
||||||
if (targetEl.closest('.' + twoUpHandle)) return;
|
|
||||||
// If we've already retargeted this event, let it through.
|
|
||||||
if (this.retargetedEvents.has(event)) return;
|
|
||||||
// Stop the event in its tracks.
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
// Clone the event & dispatch
|
|
||||||
// Some TypeScript trickery needed due to https://github.com/Microsoft/TypeScript/issues/3841
|
|
||||||
const clonedEvent = new (event.constructor as typeof Event)(event.type, event);
|
|
||||||
this.retargetedEvents.add(clonedEvent);
|
|
||||||
this.pinchZoomLeft.dispatchEvent(clonedEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render({ img }: Props, { }: State) {
|
render({ img }: Props, { }: State) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<two-up
|
<canvas ref={c => this.canvas = c as HTMLCanvasElement} width={img.width} height={img.height} />
|
||||||
// Event redirecting. See onRetargetableEvent.
|
|
||||||
onTouchStartCapture={this.onRetargetableEvent}
|
|
||||||
onTouchEndCapture={this.onRetargetableEvent}
|
|
||||||
onTouchMoveCapture={this.onRetargetableEvent}
|
|
||||||
onPointerDownCapture={this.onRetargetableEvent}
|
|
||||||
onMouseDownCapture={this.onRetargetableEvent}
|
|
||||||
onWheelCapture={this.onRetargetableEvent}
|
|
||||||
>
|
|
||||||
<pinch-zoom onChange={this.onPinchZoomLeftChange} ref={p => this.pinchZoomLeft = p as PinchZoom}>
|
|
||||||
<canvas class={style.outputCanvas} ref={c => this.canvasLeft = c as HTMLCanvasElement} width={img.width} height={img.height} />
|
|
||||||
</pinch-zoom>
|
|
||||||
<pinch-zoom ref={p => this.pinchZoomRight = p as PinchZoom}>
|
|
||||||
<canvas class={style.outputCanvas} ref={c => this.canvasRight = c as HTMLCanvasElement} width={img.width} height={img.height} />
|
|
||||||
</pinch-zoom>
|
|
||||||
</two-up>
|
|
||||||
<p>And that's all the app does so far!</p>
|
<p>And that's all the app does so far!</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.outputCanvas {
|
.app h1 {
|
||||||
image-rendering: pixelated;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Squoosh</title>
|
<title>Squoosh</title>
|
||||||
<meta name="description" content="Compress and compare images with different codecs, right in your browser">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
import { bind } from '../util';
|
|
||||||
const enum Button { Left }
|
|
||||||
|
|
||||||
export class Pointer {
|
|
||||||
/** x offset from the top of the document */
|
|
||||||
pageX: number;
|
|
||||||
/** y offset from the top of the document */
|
|
||||||
pageY: number;
|
|
||||||
/** x offset from the top of the viewport */
|
|
||||||
clientX: number;
|
|
||||||
/** y offset from the top of the viewport */
|
|
||||||
clientY: number;
|
|
||||||
/** ID for this pointer */
|
|
||||||
id: number = -1;
|
|
||||||
|
|
||||||
constructor (nativePointer: Touch | PointerEvent | MouseEvent) {
|
|
||||||
this.pageX = nativePointer.pageX;
|
|
||||||
this.pageY = nativePointer.pageY;
|
|
||||||
this.clientX = nativePointer.clientX;
|
|
||||||
this.clientY = nativePointer.clientY;
|
|
||||||
|
|
||||||
if (self.Touch && nativePointer instanceof Touch) {
|
|
||||||
this.id = nativePointer.identifier;
|
|
||||||
} else if (isPointerEvent(nativePointer)) { // is PointerEvent
|
|
||||||
this.id = nativePointer.pointerId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPointerEvent = (event: any): event is PointerEvent =>
|
|
||||||
self.PointerEvent && event instanceof PointerEvent;
|
|
||||||
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
type StartCallback = ((pointer: Pointer, event: TouchEvent | PointerEvent | MouseEvent) => boolean);
|
|
||||||
type MoveCallback = ((previousPointers: Pointer[], event: TouchEvent | PointerEvent | MouseEvent) => void);
|
|
||||||
type EndCallback = ((pointer: Pointer, event: TouchEvent | PointerEvent | MouseEvent) => void);
|
|
||||||
|
|
||||||
interface PointerTrackerCallbacks {
|
|
||||||
/**
|
|
||||||
* Called when a pointer is pressed/touched within the element.
|
|
||||||
*
|
|
||||||
* @param pointer The new pointer.
|
|
||||||
* This pointer isn't included in this.currentPointers or this.startPointers yet.
|
|
||||||
* @param event The event related to this pointer.
|
|
||||||
*
|
|
||||||
* @returns Whether you want to track this pointer as it moves.
|
|
||||||
*/
|
|
||||||
start?: StartCallback;
|
|
||||||
/**
|
|
||||||
* Called when pointers have moved.
|
|
||||||
*
|
|
||||||
* @param previousPointers The state of the pointers before this event.
|
|
||||||
* This contains the same number of pointers, in the same order, as
|
|
||||||
* this.currentPointers and this.startPointers.
|
|
||||||
* @param event The event related to the pointer changes.
|
|
||||||
*/
|
|
||||||
move?: MoveCallback;
|
|
||||||
/**
|
|
||||||
* Called when a pointer is released.
|
|
||||||
*
|
|
||||||
* @param pointer The final state of the pointer that ended. This
|
|
||||||
* pointer is now absent from this.currentPointers and
|
|
||||||
* this.startPointers.
|
|
||||||
* @param event The event related to this pointer.
|
|
||||||
*/
|
|
||||||
end?: EndCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track pointers across a particular element
|
|
||||||
*/
|
|
||||||
export class PointerTracker {
|
|
||||||
/**
|
|
||||||
* State of the tracked pointers when they were pressed/touched.
|
|
||||||
*/
|
|
||||||
readonly startPointers: Pointer[] = [];
|
|
||||||
/**
|
|
||||||
* Latest state of the tracked pointers. Contains the same number
|
|
||||||
* of pointers, and in the same order as this.startPointers.
|
|
||||||
*/
|
|
||||||
readonly currentPointers: Pointer[] = [];
|
|
||||||
|
|
||||||
private _startCallback: StartCallback;
|
|
||||||
private _moveCallback: MoveCallback;
|
|
||||||
private _endCallback: EndCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Track pointers across a particular element
|
|
||||||
*
|
|
||||||
* @param element Element to monitor.
|
|
||||||
* @param callbacks
|
|
||||||
*/
|
|
||||||
constructor (private _element: HTMLElement, callbacks: PointerTrackerCallbacks) {
|
|
||||||
const {
|
|
||||||
start = () => true,
|
|
||||||
move = noop,
|
|
||||||
end = noop
|
|
||||||
} = callbacks;
|
|
||||||
|
|
||||||
this._startCallback = start;
|
|
||||||
this._moveCallback = move;
|
|
||||||
this._endCallback = end;
|
|
||||||
|
|
||||||
// Add listeners
|
|
||||||
if (self.PointerEvent) {
|
|
||||||
this._element.addEventListener('pointerdown', this._pointerStart);
|
|
||||||
} else {
|
|
||||||
this._element.addEventListener('mousedown', this._pointerStart);
|
|
||||||
this._element.addEventListener('touchstart', this._touchStart);
|
|
||||||
this._element.addEventListener('touchmove', this._move);
|
|
||||||
this._element.addEventListener('touchend', this._touchEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call the start callback for this pointer, and track it if the user wants.
|
|
||||||
*
|
|
||||||
* @param pointer Pointer
|
|
||||||
* @param event Related event
|
|
||||||
* @returns Whether the pointer is being tracked.
|
|
||||||
*/
|
|
||||||
private _triggerPointerStart (pointer: Pointer, event: PointerEvent | MouseEvent | TouchEvent): boolean {
|
|
||||||
if (!this._startCallback(pointer, event)) return false;
|
|
||||||
this.currentPointers.push(pointer);
|
|
||||||
this.startPointers.push(pointer);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for mouse/pointer starts. Bound to the class in the constructor.
|
|
||||||
*
|
|
||||||
* @param event This will only be a MouseEvent if the browser doesn't support
|
|
||||||
* pointer events.
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _pointerStart (event: PointerEvent | MouseEvent) {
|
|
||||||
if (event.button !== Button.Left) return;
|
|
||||||
if (!this._triggerPointerStart(new Pointer(event), event)) return;
|
|
||||||
|
|
||||||
// Add listeners for additional events.
|
|
||||||
// The listeners may already exist, but no harm in adding them again.
|
|
||||||
if (isPointerEvent(event)) {
|
|
||||||
this._element.setPointerCapture(event.pointerId);
|
|
||||||
this._element.addEventListener('pointermove', this._move);
|
|
||||||
this._element.addEventListener('pointerup', this._pointerEnd);
|
|
||||||
} else { // MouseEvent
|
|
||||||
window.addEventListener('mousemove', this._move);
|
|
||||||
window.addEventListener('mouseup', this._pointerEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for touchstart. Bound to the class in the constructor.
|
|
||||||
* Only used if the browser doesn't support pointer events.
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _touchStart (event: TouchEvent) {
|
|
||||||
for (const touch of Array.from(event.changedTouches)) {
|
|
||||||
this._triggerPointerStart(new Pointer(touch), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for pointer/mouse/touch move events.
|
|
||||||
* Bound to the class in the constructor.
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _move (event: PointerEvent | MouseEvent | TouchEvent) {
|
|
||||||
const previousPointers = this.currentPointers.slice();
|
|
||||||
const changedPointers = ('changedTouches' in event) ? // Shortcut for 'is touch event'.
|
|
||||||
Array.from(event.changedTouches).map(t => new Pointer(t)) :
|
|
||||||
[new Pointer(event)];
|
|
||||||
|
|
||||||
let shouldCallback = false;
|
|
||||||
|
|
||||||
for (const pointer of changedPointers) {
|
|
||||||
const index = this.currentPointers.findIndex(p => p.id === pointer.id);
|
|
||||||
if (index === -1) continue;
|
|
||||||
shouldCallback = true;
|
|
||||||
this.currentPointers[index] = pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldCallback) return;
|
|
||||||
|
|
||||||
this._moveCallback(previousPointers, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call the end callback for this pointer.
|
|
||||||
*
|
|
||||||
* @param pointer Pointer
|
|
||||||
* @param event Related event
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _triggerPointerEnd (pointer: Pointer, event: PointerEvent | MouseEvent | TouchEvent): boolean {
|
|
||||||
const index = this.currentPointers.findIndex(p => p.id === pointer.id);
|
|
||||||
// Not a pointer we're interested in?
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
this.currentPointers.splice(index, 1);
|
|
||||||
this.startPointers.splice(index, 1);
|
|
||||||
|
|
||||||
this._endCallback(pointer, event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for mouse/pointer ends. Bound to the class in the constructor.
|
|
||||||
* @param event This will only be a MouseEvent if the browser doesn't support
|
|
||||||
* pointer events.
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _pointerEnd (event: PointerEvent | MouseEvent) {
|
|
||||||
if (!this._triggerPointerEnd(new Pointer(event), event)) return;
|
|
||||||
|
|
||||||
if (isPointerEvent(event)) {
|
|
||||||
if (this.currentPointers.length) return;
|
|
||||||
this._element.removeEventListener('pointermove', this._move);
|
|
||||||
this._element.removeEventListener('pointerup', this._pointerEnd);
|
|
||||||
} else { // MouseEvent
|
|
||||||
window.removeEventListener('mousemove', this._move);
|
|
||||||
window.removeEventListener('mouseup', this._pointerEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener for touchend. Bound to the class in the constructor.
|
|
||||||
* Only used if the browser doesn't support pointer events.
|
|
||||||
*/
|
|
||||||
@bind
|
|
||||||
private _touchEnd (event: TouchEvent) {
|
|
||||||
for (const touch of Array.from(event.changedTouches)) {
|
|
||||||
this._triggerPointerEnd(new Pointer(touch), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
src/lib/PointerTracker/missing-types.d.ts
vendored
6
src/lib/PointerTracker/missing-types.d.ts
vendored
@@ -1,6 +0,0 @@
|
|||||||
// TypeScript, you make me sad.
|
|
||||||
// https://github.com/Microsoft/TypeScript/issues/18756
|
|
||||||
interface Window {
|
|
||||||
PointerEvent: typeof PointerEvent;
|
|
||||||
Touch: typeof Touch;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Encoder {
|
|
||||||
encode(data: ImageData): Promise<ArrayBuffer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Decoder {
|
|
||||||
decode(data: ArrayBuffer): Promise<ImageBitmap>;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import {Encoder} from './codec';
|
|
||||||
|
|
||||||
import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
|
|
||||||
// Using require() so TypeScript doesn’t complain about this not being a module.
|
|
||||||
const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm');
|
|
||||||
|
|
||||||
// API exposed by wasm module. Details in the codec’s README.
|
|
||||||
interface ModuleAPI {
|
|
||||||
version(): number;
|
|
||||||
create_buffer(width: number, height: number): number;
|
|
||||||
destroy_buffer(pointer: number): void;
|
|
||||||
encode(buffer: number, width: number, height: number, quality: number): void;
|
|
||||||
free_result(): void;
|
|
||||||
get_result_pointer(): number;
|
|
||||||
get_result_size(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MozJpegEncoder implements Encoder {
|
|
||||||
private emscriptenModule: Promise<EmscriptenWasm.Module>;
|
|
||||||
private api: Promise<ModuleAPI>;
|
|
||||||
constructor() {
|
|
||||||
this.emscriptenModule = new Promise(resolve => {
|
|
||||||
const m = mozjpeg_enc({
|
|
||||||
// Just to be safe, don’t automatically invoke any wasm functions
|
|
||||||
noInitialRun: false,
|
|
||||||
locateFile(url: string): string {
|
|
||||||
// Redirect the request for the wasm binary to whatever webpack gave us.
|
|
||||||
if(url.endsWith('.wasm')) {
|
|
||||||
return wasmBinaryUrl;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
onRuntimeInitialized() {
|
|
||||||
// An Emscripten is a then-able that, for some reason, `then()`s itself,
|
|
||||||
// causing an infite loop when you wrap it in a real promise. Deleten the `then`
|
|
||||||
// prop solves this for now.
|
|
||||||
// See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129
|
|
||||||
// TODO(surma@): File a bug with Emscripten on this.
|
|
||||||
delete (m as any).then;
|
|
||||||
resolve(m);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api = (async () => {
|
|
||||||
// Not sure why, but TypeScript complains that I am using `emscriptenModule` before it’s getting assigned, which is clearly not true :shrug: Using `any`
|
|
||||||
const m = await (this as any).emscriptenModule;
|
|
||||||
return {
|
|
||||||
version: m.cwrap('version', 'number', []),
|
|
||||||
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
|
|
||||||
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
|
|
||||||
encode: m.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
|
||||||
free_result: m.cwrap('free_result', '', []),
|
|
||||||
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
|
|
||||||
get_result_size: m.cwrap('get_result_size', 'number', []),
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
async encode(data: ImageData): Promise<ArrayBuffer> {
|
|
||||||
const m = await this.emscriptenModule;
|
|
||||||
const api = await this.api;
|
|
||||||
|
|
||||||
const p = api.create_buffer(data.width, data.height);
|
|
||||||
m.HEAP8.set(data.data, p);
|
|
||||||
api.encode(p, data.width, data.height, 2);
|
|
||||||
const resultPointer = api.get_result_pointer();
|
|
||||||
const resultSize = api.get_result_size();
|
|
||||||
const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize);
|
|
||||||
const result = new Uint8Array(resultView);
|
|
||||||
api.free_result();
|
|
||||||
api.destroy_buffer(p);
|
|
||||||
|
|
||||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
|
||||||
return result.buffer as ArrayBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,22 +24,3 @@ export function bind(target: any, propertyKey: string, descriptor: PropertyDescr
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns a given `ImageBitmap` into `ImageData`.
|
|
||||||
*/
|
|
||||||
export async function bitmapToImageData(bitmap: ImageBitmap): Promise<ImageData> {
|
|
||||||
// Make canvas same size as image
|
|
||||||
// TODO: Move this off-thread if possible with `OffscreenCanvas` or iFrames?
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = bitmap.width;
|
|
||||||
canvas.height = bitmap.height;
|
|
||||||
// Draw image onto canvas
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
throw new Error("Could not create canvas context");
|
|
||||||
}
|
|
||||||
ctx.drawImage(bitmap, 0, 0);
|
|
||||||
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
22
src/missing-types.d.ts
vendored
22
src/missing-types.d.ts
vendored
@@ -1,22 +0,0 @@
|
|||||||
// PRs to fix this:
|
|
||||||
// https://github.com/developit/preact/pull/1101
|
|
||||||
// https://github.com/developit/preact/pull/1102
|
|
||||||
declare namespace JSX {
|
|
||||||
type PointerEventHandler = EventHandler<PointerEvent>;
|
|
||||||
|
|
||||||
interface DOMAttributes {
|
|
||||||
onTouchStartCapture?: TouchEventHandler;
|
|
||||||
onTouchEndCapture?: TouchEventHandler;
|
|
||||||
onTouchMoveCapture?: TouchEventHandler;
|
|
||||||
|
|
||||||
onPointerDownCapture?: PointerEventHandler;
|
|
||||||
|
|
||||||
onMouseDownCapture?: MouseEventHandler;
|
|
||||||
|
|
||||||
onWheelCapture?: WheelEventHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CanvasRenderingContext2D {
|
|
||||||
filter: string;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!--
|
|
||||||
This file is taken from https://github.com/karma-runner/karma/pull/2834. It allows us to use ES6 module imports in our test suite files.
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
This is the execution context.
|
|
||||||
Loaded within the iframe.
|
|
||||||
Reloaded before every execution run.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title></title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- The scripts need to be in the body DOM element, as some test running frameworks need the body
|
|
||||||
to have already been created so they can insert their magic into it. For example, if loaded
|
|
||||||
before body, Angular Scenario test framework fails to find the body and crashes and burns in
|
|
||||||
an epic manner. -->
|
|
||||||
<script src="context.js"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
// Configure our Karma and set up bindings
|
|
||||||
%CLIENT_CONFIG%
|
|
||||||
window.__karma__.setupContext(window);
|
|
||||||
|
|
||||||
// All served files with the latest timestamps
|
|
||||||
%MAPPINGS%
|
|
||||||
</script>
|
|
||||||
<!-- Dynamically replaced with <script> tags -->
|
|
||||||
%SCRIPTS%
|
|
||||||
<!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too.
|
|
||||||
This ensures all the tests will have been declared before karma tries to run them. -->
|
|
||||||
<script type="module">
|
|
||||||
window.__karma__.loaded();
|
|
||||||
</script>
|
|
||||||
<script nomodule>
|
|
||||||
window.__karma__.loaded();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<!--
|
|
||||||
This file is taken from https://github.com/karma-runner/karma/pull/2834. It allows us to use ES6 module imports in our test suite files.
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
This file is almost the same as context.html - loads all source files,
|
|
||||||
but its purpose is to be loaded in the main frame (not within an iframe),
|
|
||||||
just for immediate execution, without reporting to Karma server.
|
|
||||||
-->
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
%X_UA_COMPATIBLE%
|
|
||||||
<title>Karma DEBUG RUNNER</title>
|
|
||||||
<link href="favicon.ico" rel="icon" type="image/x-icon" />
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- The scripts need to be at the end of body, so that some test running frameworks
|
|
||||||
(Angular Scenario, for example) need the body to be loaded so that it can insert its magic
|
|
||||||
into it. If it is before body, then it fails to find the body and crashes and burns in an epic
|
|
||||||
manner. -->
|
|
||||||
<script src="context.js"></script>
|
|
||||||
<script src="debug.js"></script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
// Configure our Karma
|
|
||||||
%CLIENT_CONFIG%
|
|
||||||
|
|
||||||
// All served files with the latest timestamps
|
|
||||||
%MAPPINGS%
|
|
||||||
</script>
|
|
||||||
<!-- Dynamically replaced with <script> tags -->
|
|
||||||
%SCRIPTS%
|
|
||||||
<!-- Since %SCRIPTS% might include modules, the `loaded()` call needs to be in a module too.
|
|
||||||
This ensures all the tests will have been declared before karma tries to run them. -->
|
|
||||||
<script type="module">
|
|
||||||
window.__karma__.loaded();
|
|
||||||
</script>
|
|
||||||
<script nomodule>
|
|
||||||
window.__karma__.loaded();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const app = express();
|
|
||||||
const http = require("http");
|
|
||||||
const puppeteer = require("puppeteer");
|
|
||||||
const { fingerDown } = require("./finger.js");
|
|
||||||
const { expect } = require("chai");
|
|
||||||
|
|
||||||
async function staticWebServer(path) {
|
|
||||||
// Start a static web server
|
|
||||||
const app = express();
|
|
||||||
app.use(express.static(path));
|
|
||||||
// Port 0 means let the OS select a port
|
|
||||||
const server = http.createServer(app).listen(0, "localhost");
|
|
||||||
await new Promise(resolve => server.on("listening", resolve));
|
|
||||||
|
|
||||||
// Read back the bound address
|
|
||||||
const address = server.address();
|
|
||||||
return { server, address };
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("some e2e test", function() {
|
|
||||||
before(async function() {
|
|
||||||
// Start webserver
|
|
||||||
const { address, server } = await staticWebServer(".");
|
|
||||||
this.address = `http://${address.address}:${address.port}`;
|
|
||||||
this.server = server;
|
|
||||||
|
|
||||||
// Start browser
|
|
||||||
this.browser = await puppeteer.launch();
|
|
||||||
this.page = await this.browser.newPage();
|
|
||||||
await this.page.goto(`${this.address}/test/sample.html`, {
|
|
||||||
waitUntil: "networkidle2"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can tap", async function() {
|
|
||||||
const btn = await this.page.$("button");
|
|
||||||
await btn.tap();
|
|
||||||
const result = await this.page.evaluate(_ => {
|
|
||||||
return window.lol;
|
|
||||||
});
|
|
||||||
expect(result).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can tap manually", async function() {
|
|
||||||
const btn = await this.page.$("button");
|
|
||||||
const box = await btn.boundingBox();
|
|
||||||
const finger = fingerDown(
|
|
||||||
this.page,
|
|
||||||
box.x + box.width / 2,
|
|
||||||
box.y + box.height / 2
|
|
||||||
);
|
|
||||||
finger.up();
|
|
||||||
const result = await this.page.evaluate(_ => {
|
|
||||||
return window.lol;
|
|
||||||
});
|
|
||||||
expect(result).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does some taps", async function() {});
|
|
||||||
|
|
||||||
after(async function() {
|
|
||||||
this.server.close();
|
|
||||||
await this.browser.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
let touchPoints = [];
|
|
||||||
let idCnt = 0;
|
|
||||||
|
|
||||||
class Finger {
|
|
||||||
constructor(point, page) {
|
|
||||||
this._point = point;
|
|
||||||
this._page = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
move(x, y) {
|
|
||||||
if (!this._point) return;
|
|
||||||
Object.assign(this._point, {
|
|
||||||
x: Math.floor(x),
|
|
||||||
y: Math.floor(y)
|
|
||||||
});
|
|
||||||
this._page.touchscreen._client.send("Input.dispatchTouchEvent", {
|
|
||||||
type: "touchMove",
|
|
||||||
touchPoints,
|
|
||||||
modifiers: page._keyboard._modifiers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
up() {
|
|
||||||
if (!this._point) return;
|
|
||||||
const idx = touchPoints.indexOf(this._point);
|
|
||||||
touchPoints = touchPoints.splice(idx, 1);
|
|
||||||
this._point = null;
|
|
||||||
if (touchPoints.length === 0) {
|
|
||||||
this._page.touchscreen._client.send("Input.dispatchTouchEvent", {
|
|
||||||
type: "touchEnd",
|
|
||||||
modifiers: this._page._keyboard._modifiers
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._page.touchscreen._client.send("Input.dispatchTouchEvent", {
|
|
||||||
type: "touchMove",
|
|
||||||
touchPoints,
|
|
||||||
modifiers: this._page._keyboard._modifiers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fingerDown(page, x, y) {
|
|
||||||
const id = idCnt++;
|
|
||||||
const point = {
|
|
||||||
x: Math.round(x),
|
|
||||||
y: Math.round(y),
|
|
||||||
id
|
|
||||||
};
|
|
||||||
touchPoints.push(point);
|
|
||||||
page.touchscreen._client.send("Input.dispatchTouchEvent", {
|
|
||||||
type: "touchStart",
|
|
||||||
touchPoints,
|
|
||||||
modifiers: page._keyboard._modifiers
|
|
||||||
});
|
|
||||||
return new Finger(point, page);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fingerDown
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import {bitmapToImageData} from "../../src/lib/util";
|
|
||||||
|
|
||||||
const expect = chai.expect;
|
|
||||||
|
|
||||||
describe("util.bitmapToImageData", function () {
|
|
||||||
it("is a function", function () {
|
|
||||||
expect(bitmapToImageData).to.be.a('function');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
|
|
||||||
<button>Test me</button>
|
|
||||||
<script>
|
|
||||||
document.querySelector('button').onclick = _ => window.lol = true;
|
|
||||||
</script>
|
|
||||||
@@ -107,23 +107,10 @@ module.exports = function (_, env) {
|
|||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
// Don't respect any Babel RC files found on the filesystem:
|
// Don't respect any Babel RC files found on the filesystem:
|
||||||
options: Object.assign(readJson('.babelrc'), { babelrc: false })
|
options: Object.assign(readJson('.babelrc'), { babelrc: false })
|
||||||
},
|
|
||||||
{
|
|
||||||
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
|
|
||||||
test: /\/codecs\/.*\.js$/,
|
|
||||||
loader: 'exports-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\/codecs\/.*\.wasm$/,
|
|
||||||
// This is needed to make webpack NOT process wasm files.
|
|
||||||
// See https://github.com/webpack/webpack/issues/6725
|
|
||||||
type: 'javascript/auto',
|
|
||||||
loader: 'file-loader',
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.IgnorePlugin(/(fs)/, /\/codecs\//),
|
|
||||||
// Pretty progressbar showing build progress:
|
// Pretty progressbar showing build progress:
|
||||||
new ProgressBarPlugin({
|
new ProgressBarPlugin({
|
||||||
format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m\r',
|
format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m\r',
|
||||||
@@ -152,7 +139,6 @@ module.exports = function (_, env) {
|
|||||||
// See: https://github.com/webpack-contrib/mini-css-extract-plugin
|
// See: https://github.com/webpack-contrib/mini-css-extract-plugin
|
||||||
// See also: https://twitter.com/wsokra/status/970253245733113856
|
// See also: https://twitter.com/wsokra/status/970253245733113856
|
||||||
isProd && new MiniCssExtractPlugin({
|
isProd && new MiniCssExtractPlugin({
|
||||||
filename: '[name].[contenthash:5].css',
|
|
||||||
chunkFilename: '[name].chunk.[contenthash:5].css'
|
chunkFilename: '[name].chunk.[contenthash:5].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user