mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 17:49:52 +00:00
Merge branch 'dev' into load-file-async
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -10,15 +10,13 @@ using namespace emscripten;
|
|||||||
thread_local const val Uint8Array = val::global("Uint8Array");
|
thread_local const val Uint8Array = val::global("Uint8Array");
|
||||||
|
|
||||||
struct JXLOptions {
|
struct JXLOptions {
|
||||||
// 1 = slowest
|
int effort;
|
||||||
// 7 = fastest
|
|
||||||
int speed;
|
|
||||||
float quality;
|
float quality;
|
||||||
bool progressive;
|
bool progressive;
|
||||||
int epf;
|
int epf;
|
||||||
int nearLossless;
|
|
||||||
bool lossyPalette;
|
bool lossyPalette;
|
||||||
size_t decodingSpeedTier;
|
size_t decodingSpeedTier;
|
||||||
|
float photonNoiseIso;
|
||||||
};
|
};
|
||||||
|
|
||||||
val encode(std::string image, int width, int height, JXLOptions options) {
|
val encode(std::string image, int width, int height, JXLOptions options) {
|
||||||
@@ -33,11 +31,14 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
|||||||
pool_ptr = &pool;
|
pool_ptr = &pool;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
cparams.epf = options.epf;
|
size_t st = 10 - options.effort;
|
||||||
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
|
cparams.speed_tier = jxl::SpeedTier(st);
|
||||||
cparams.decoding_speed_tier = options.decodingSpeedTier;
|
|
||||||
|
|
||||||
if (options.lossyPalette || options.nearLossless) {
|
cparams.epf = options.epf;
|
||||||
|
cparams.decoding_speed_tier = options.decodingSpeedTier;
|
||||||
|
cparams.photon_noise_iso = options.photonNoiseIso;
|
||||||
|
|
||||||
|
if (options.lossyPalette) {
|
||||||
cparams.lossy_palette = true;
|
cparams.lossy_palette = true;
|
||||||
cparams.palette_colors = 0;
|
cparams.palette_colors = 0;
|
||||||
cparams.options.predictor = jxl::Predictor::Zero;
|
cparams.options.predictor = jxl::Predictor::Zero;
|
||||||
@@ -106,12 +107,12 @@ val encode(std::string image, int width, int height, JXLOptions options) {
|
|||||||
|
|
||||||
EMSCRIPTEN_BINDINGS(my_module) {
|
EMSCRIPTEN_BINDINGS(my_module) {
|
||||||
value_object<JXLOptions>("JXLOptions")
|
value_object<JXLOptions>("JXLOptions")
|
||||||
.field("speed", &JXLOptions::speed)
|
.field("effort", &JXLOptions::effort)
|
||||||
.field("quality", &JXLOptions::quality)
|
.field("quality", &JXLOptions::quality)
|
||||||
.field("progressive", &JXLOptions::progressive)
|
.field("progressive", &JXLOptions::progressive)
|
||||||
.field("nearLossless", &JXLOptions::nearLossless)
|
|
||||||
.field("lossyPalette", &JXLOptions::lossyPalette)
|
.field("lossyPalette", &JXLOptions::lossyPalette)
|
||||||
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
|
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
|
||||||
|
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
|
||||||
.field("epf", &JXLOptions::epf);
|
.field("epf", &JXLOptions::epf);
|
||||||
|
|
||||||
function("encode", &encode);
|
function("encode", &encode);
|
||||||
|
|||||||
4
codecs/jxl/enc/jxl_enc.d.ts
vendored
4
codecs/jxl/enc/jxl_enc.d.ts
vendored
@@ -1,11 +1,11 @@
|
|||||||
export interface EncodeOptions {
|
export interface EncodeOptions {
|
||||||
speed: number;
|
effort: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
progressive: boolean;
|
progressive: boolean;
|
||||||
epf: number;
|
epf: number;
|
||||||
nearLossless: number;
|
|
||||||
lossyPalette: boolean;
|
lossyPalette: boolean;
|
||||||
decodingSpeedTier: number;
|
decodingSpeedTier: number;
|
||||||
|
photonNoiseIso: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JXLModule extends EmscriptenWasm.Module {
|
export interface JXLModule extends EmscriptenWasm.Module {
|
||||||
|
|||||||
2
codecs/jxl/enc/jxl_enc.js
generated
2
codecs/jxl/enc/jxl_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt.js
generated
2
codecs/jxl/enc/jxl_enc_mt.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
2
codecs/jxl/enc/jxl_enc_mt_simd.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
2
codecs/jxl/enc/jxl_node_enc.js
generated
2
codecs/jxl/enc/jxl_node_enc.js
generated
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -158,6 +158,11 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
|
|||||||
if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) {
|
if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) {
|
||||||
cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample;
|
cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample;
|
||||||
cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample;
|
cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample;
|
||||||
|
|
||||||
|
if (opts.chroma_subsample > 2) {
|
||||||
|
// Otherwise encoding fails.
|
||||||
|
jpeg_c_set_int_param(&cinfo, JINT_DC_SCAN_OPT_MODE, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.baseline && opts.progressive) {
|
if (!opts.baseline && opts.progressive) {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -29,14 +29,23 @@ interface ResizeWithAspectParams {
|
|||||||
target_height: number;
|
target_height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResizeInstantiateOptions {
|
export interface ResizeOptions {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
method: string;
|
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3';
|
||||||
premultiply: boolean;
|
premultiply: boolean;
|
||||||
linearRGB: boolean;
|
linearRGB: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuantOptions {
|
||||||
|
numColors: number;
|
||||||
|
dither: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotateOptions {
|
||||||
|
numRotations: number;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// Needed for being able to use ImageData as type in codec types
|
// Needed for being able to use ImageData as type in codec types
|
||||||
type ImageData = import('./image_data.js').default;
|
type ImageData = import('./image_data.js').default;
|
||||||
@@ -52,6 +61,7 @@ import mozEnc from '../../codecs/mozjpeg/enc/mozjpeg_node_enc.js';
|
|||||||
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
|
import mozEncWasm from 'asset-url:../../codecs/mozjpeg/enc/mozjpeg_node_enc.wasm';
|
||||||
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
|
import mozDec from '../../codecs/mozjpeg/dec/mozjpeg_node_dec.js';
|
||||||
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
|
import mozDecWasm from 'asset-url:../../codecs/mozjpeg/dec/mozjpeg_node_dec.wasm';
|
||||||
|
import type { EncodeOptions as MozJPEGEncodeOptions } from '../../codecs/mozjpeg/enc/mozjpeg_enc';
|
||||||
|
|
||||||
// WebP
|
// WebP
|
||||||
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
|
import type { WebPModule as WebPEncodeModule } from '../../codecs/webp/enc/webp_enc';
|
||||||
@@ -59,6 +69,7 @@ import webpEnc from '../../codecs/webp/enc/webp_node_enc.js';
|
|||||||
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
|
import webpEncWasm from 'asset-url:../../codecs/webp/enc/webp_node_enc.wasm';
|
||||||
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
|
import webpDec from '../../codecs/webp/dec/webp_node_dec.js';
|
||||||
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
|
import webpDecWasm from 'asset-url:../../codecs/webp/dec/webp_node_dec.wasm';
|
||||||
|
import type { EncodeOptions as WebPEncodeOptions } from '../../codecs/webp/enc/webp_enc.js';
|
||||||
|
|
||||||
// AVIF
|
// AVIF
|
||||||
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
|
import type { AVIFModule as AVIFEncodeModule } from '../../codecs/avif/enc/avif_enc';
|
||||||
@@ -69,6 +80,7 @@ import avifEncMtWorker from 'chunk-url:../../codecs/avif/enc/avif_node_enc_mt.wo
|
|||||||
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
|
import avifEncMtWasm from 'asset-url:../../codecs/avif/enc/avif_node_enc_mt.wasm';
|
||||||
import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
|
import avifDec from '../../codecs/avif/dec/avif_node_dec.js';
|
||||||
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
|
import avifDecWasm from 'asset-url:../../codecs/avif/dec/avif_node_dec.wasm';
|
||||||
|
import type { EncodeOptions as AvifEncodeOptions } from '../../codecs/avif/enc/avif_enc.js';
|
||||||
|
|
||||||
// JXL
|
// JXL
|
||||||
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
|
import type { JXLModule as JXLEncodeModule } from '../../codecs/jxl/enc/jxl_enc';
|
||||||
@@ -76,6 +88,7 @@ import jxlEnc from '../../codecs/jxl/enc/jxl_node_enc.js';
|
|||||||
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
|
import jxlEncWasm from 'asset-url:../../codecs/jxl/enc/jxl_node_enc.wasm';
|
||||||
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
|
import jxlDec from '../../codecs/jxl/dec/jxl_node_dec.js';
|
||||||
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
|
import jxlDecWasm from 'asset-url:../../codecs/jxl/dec/jxl_node_dec.wasm';
|
||||||
|
import type { EncodeOptions as JxlEncodeOptions } from '../../codecs/jxl/enc/jxl_enc.js';
|
||||||
|
|
||||||
// WP2
|
// WP2
|
||||||
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
|
import type { WP2Module as WP2EncodeModule } from '../../codecs/wp2/enc/wp2_enc';
|
||||||
@@ -83,6 +96,7 @@ import wp2Enc from '../../codecs/wp2/enc/wp2_node_enc.js';
|
|||||||
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
|
import wp2EncWasm from 'asset-url:../../codecs/wp2/enc/wp2_node_enc.wasm';
|
||||||
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
|
import wp2Dec from '../../codecs/wp2/dec/wp2_node_dec.js';
|
||||||
import wp2DecWasm from 'asset-url:../../codecs/wp2/dec/wp2_node_dec.wasm';
|
import wp2DecWasm from 'asset-url:../../codecs/wp2/dec/wp2_node_dec.wasm';
|
||||||
|
import type { EncodeOptions as WP2EncodeOptions } from '../../codecs/wp2/enc/wp2_enc.js';
|
||||||
|
|
||||||
// PNG
|
// PNG
|
||||||
import * as pngEncDec from '../../codecs/png/pkg/squoosh_png.js';
|
import * as pngEncDec from '../../codecs/png/pkg/squoosh_png.js';
|
||||||
@@ -95,6 +109,9 @@ const pngEncDecPromise = pngEncDec.default(
|
|||||||
import * as oxipng from '../../codecs/oxipng/pkg/squoosh_oxipng.js';
|
import * as oxipng from '../../codecs/oxipng/pkg/squoosh_oxipng.js';
|
||||||
import oxipngWasm from 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
|
import oxipngWasm from 'asset-url:../../codecs/oxipng/pkg/squoosh_oxipng_bg.wasm';
|
||||||
const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm)));
|
const oxipngPromise = oxipng.default(fsp.readFile(pathify(oxipngWasm)));
|
||||||
|
interface OxiPngEncodeOptions {
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
import * as resize from '../../codecs/resize/pkg/squoosh_resize.js';
|
import * as resize from '../../codecs/resize/pkg/squoosh_resize.js';
|
||||||
@@ -171,13 +188,7 @@ export const preprocessors = {
|
|||||||
buffer: Uint8Array,
|
buffer: Uint8Array,
|
||||||
input_width: number,
|
input_width: number,
|
||||||
input_height: number,
|
input_height: number,
|
||||||
{
|
{ width, height, method, premultiply, linearRGB }: ResizeOptions,
|
||||||
width,
|
|
||||||
height,
|
|
||||||
method,
|
|
||||||
premultiply,
|
|
||||||
linearRGB,
|
|
||||||
}: ResizeInstantiateOptions,
|
|
||||||
) => {
|
) => {
|
||||||
({ width, height } = resizeWithAspect({
|
({ width, height } = resizeWithAspect({
|
||||||
input_width,
|
input_width,
|
||||||
@@ -218,7 +229,7 @@ export const preprocessors = {
|
|||||||
buffer: Uint8Array,
|
buffer: Uint8Array,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
{ numColors, dither }: { numColors: number; dither: number },
|
{ numColors, dither }: QuantOptions,
|
||||||
) =>
|
) =>
|
||||||
new ImageData(
|
new ImageData(
|
||||||
imageQuant.quantize(buffer, width, height, numColors, dither),
|
imageQuant.quantize(buffer, width, height, numColors, dither),
|
||||||
@@ -239,7 +250,7 @@ export const preprocessors = {
|
|||||||
buffer: Uint8Array,
|
buffer: Uint8Array,
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
{ numRotations }: { numRotations: number },
|
{ numRotations }: RotateOptions,
|
||||||
) => {
|
) => {
|
||||||
const degrees = (numRotations * 90) % 360;
|
const degrees = (numRotations * 90) % 360;
|
||||||
const sameDimensions = degrees == 0 || degrees == 180;
|
const sameDimensions = degrees == 0 || degrees == 180;
|
||||||
@@ -401,13 +412,13 @@ export const codecs = {
|
|||||||
jxlEncWasm,
|
jxlEncWasm,
|
||||||
),
|
),
|
||||||
defaultEncoderOptions: {
|
defaultEncoderOptions: {
|
||||||
speed: 4,
|
effort: 1,
|
||||||
quality: 75,
|
quality: 75,
|
||||||
progressive: false,
|
progressive: false,
|
||||||
epf: -1,
|
epf: -1,
|
||||||
nearLossless: 0,
|
|
||||||
lossyPalette: false,
|
lossyPalette: false,
|
||||||
decodingSpeedTier: 0,
|
decodingSpeedTier: 0,
|
||||||
|
photonNoiseIso: 0,
|
||||||
},
|
},
|
||||||
autoOptimize: {
|
autoOptimize: {
|
||||||
option: 'quality',
|
option: 'quality',
|
||||||
@@ -480,3 +491,12 @@ export const codecs = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export {
|
||||||
|
MozJPEGEncodeOptions,
|
||||||
|
WebPEncodeOptions,
|
||||||
|
AvifEncodeOptions,
|
||||||
|
JxlEncodeOptions,
|
||||||
|
WP2EncodeOptions,
|
||||||
|
OxiPngEncodeOptions,
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
|
|||||||
// These will have changed in the bundling process and
|
// These will have changed in the bundling process and
|
||||||
// we need to inject them here.
|
// we need to inject them here.
|
||||||
if (requestPath.endsWith('.wasm')) return pathify(path);
|
if (requestPath.endsWith('.wasm')) return pathify(path);
|
||||||
if (requestPath.endsWith('.worker.js')) return new URL(workerJS).pathname;
|
if (requestPath.endsWith('.worker.js')) return pathify(workerJS);
|
||||||
return requestPath;
|
return requestPath;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { isMainThread } from 'worker_threads';
|
import { isMainThread } from 'worker_threads';
|
||||||
|
|
||||||
import { codecs as encoders, preprocessors } from './codecs.js';
|
import {
|
||||||
|
AvifEncodeOptions,
|
||||||
|
codecs as encoders,
|
||||||
|
JxlEncodeOptions,
|
||||||
|
MozJPEGEncodeOptions,
|
||||||
|
OxiPngEncodeOptions,
|
||||||
|
preprocessors,
|
||||||
|
QuantOptions,
|
||||||
|
ResizeOptions,
|
||||||
|
RotateOptions,
|
||||||
|
WebPEncodeOptions,
|
||||||
|
WP2EncodeOptions,
|
||||||
|
} from './codecs.js';
|
||||||
import WorkerPool from './worker_pool.js';
|
import WorkerPool from './worker_pool.js';
|
||||||
import { autoOptimize } from './auto-optimizer.js';
|
import { autoOptimize } from './auto-optimizer.js';
|
||||||
import type ImageData from './image_data';
|
import type ImageData from './image_data';
|
||||||
@@ -9,6 +21,12 @@ export { ImagePool, encoders, preprocessors };
|
|||||||
type EncoderKey = keyof typeof encoders;
|
type EncoderKey = keyof typeof encoders;
|
||||||
type PreprocessorKey = keyof typeof preprocessors;
|
type PreprocessorKey = keyof typeof preprocessors;
|
||||||
|
|
||||||
|
type PreprocessOptions = {
|
||||||
|
resize?: ResizeOptions;
|
||||||
|
quant?: QuantOptions;
|
||||||
|
rotate?: RotateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
async function decodeFile({
|
async function decodeFile({
|
||||||
file,
|
file,
|
||||||
}: {
|
}: {
|
||||||
@@ -168,10 +186,10 @@ class Image {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Define one or several preprocessors to use on the image.
|
* Define one or several preprocessors to use on the image.
|
||||||
* @param {object} preprocessOptions - An object with preprocessors to use, and their settings.
|
* @param {PreprocessOptions} preprocessOptions - An object with preprocessors to use, and their settings.
|
||||||
* @returns {Promise<undefined>} - A promise that resolves when all preprocessors have completed their work.
|
* @returns {Promise<undefined>} - A promise that resolves when all preprocessors have completed their work.
|
||||||
*/
|
*/
|
||||||
async preprocess(preprocessOptions = {}) {
|
async preprocess(preprocessOptions: PreprocessOptions = {}) {
|
||||||
for (const [name, options] of Object.entries(preprocessOptions)) {
|
for (const [name, options] of Object.entries(preprocessOptions)) {
|
||||||
if (!Object.keys(preprocessors).includes(name)) {
|
if (!Object.keys(preprocessors).includes(name)) {
|
||||||
throw Error(`Invalid preprocessor "${name}"`);
|
throw Error(`Invalid preprocessor "${name}"`);
|
||||||
@@ -202,7 +220,12 @@ class Image {
|
|||||||
optimizerButteraugliTarget?: number;
|
optimizerButteraugliTarget?: number;
|
||||||
maxOptimizerRounds?: number;
|
maxOptimizerRounds?: number;
|
||||||
} & {
|
} & {
|
||||||
[key in EncoderKey]?: any; // any is okay for now
|
mozjpeg?: Partial<MozJPEGEncodeOptions>;
|
||||||
|
webp?: Partial<WebPEncodeOptions>;
|
||||||
|
avif?: Partial<AvifEncodeOptions>;
|
||||||
|
jxl?: Partial<JxlEncodeOptions>;
|
||||||
|
wp2?: Partial<WP2EncodeOptions>;
|
||||||
|
oxipng?: Partial<OxiPngEncodeOptions>;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { bitmap } = await this.decoded;
|
const { bitmap } = await this.decoded;
|
||||||
|
|||||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -32,7 +32,7 @@
|
|||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
"mime-types": "^2.1.28",
|
"mime-types": "^2.1.28",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"pointer-tracker": "^2.4.0",
|
"pointer-tracker": "^2.5.3",
|
||||||
"postcss": "^7.0.35",
|
"postcss": "^7.0.35",
|
||||||
"postcss-modules": "^3.2.2",
|
"postcss-modules": "^3.2.2",
|
||||||
"postcss-nested": "^4.2.3",
|
"postcss-nested": "^4.2.3",
|
||||||
@@ -48,6 +48,20 @@
|
|||||||
"which": "^2.0.2"
|
"which": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../pointer-tracker": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"husky": "^4.2.5",
|
||||||
|
"lint-staged": "^10.2.11",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"rollup": "^2.23.1",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"rollup-plugin-typescript2": "^0.27.2",
|
||||||
|
"typescript": "^3.9.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.10.4",
|
"version": "7.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||||
@@ -3871,9 +3885,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pointer-tracker": {
|
"node_modules/pointer-tracker": {
|
||||||
"version": "2.4.0",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
|
||||||
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
|
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
@@ -11946,9 +11960,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pointer-tracker": {
|
"pointer-tracker": {
|
||||||
"version": "2.4.0",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
|
||||||
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
|
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
"mime-types": "^2.1.28",
|
"mime-types": "^2.1.28",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"pointer-tracker": "^2.4.0",
|
"pointer-tracker": "^2.5.3",
|
||||||
"postcss": "^7.0.35",
|
"postcss": "^7.0.35",
|
||||||
"postcss-modules": "^3.2.2",
|
"postcss-modules": "^3.2.2",
|
||||||
"postcss-nested": "^4.2.3",
|
"postcss-nested": "^4.2.3",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const REFLECTED_ATTRIBUTES = [
|
|||||||
'disabled',
|
'disabled',
|
||||||
];
|
];
|
||||||
|
|
||||||
function getPrescision(value: string): number {
|
function getPrecision(value: string): number {
|
||||||
const afterDecimal = value.split('.')[1];
|
const afterDecimal = value.split('.')[1];
|
||||||
return afterDecimal ? afterDecimal.length : 0;
|
return afterDecimal ? afterDecimal.length : 0;
|
||||||
}
|
}
|
||||||
@@ -112,18 +112,24 @@ class RangeInputElement extends HTMLElement {
|
|||||||
this.dispatchEvent(retargetted);
|
this.dispatchEvent(retargetted);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _getDisplayValue(value: number): string {
|
||||||
|
if (value >= 10000) return (value / 1000).toFixed(1) + 'k';
|
||||||
|
|
||||||
|
const labelPrecision =
|
||||||
|
Number(this.labelPrecision) || getPrecision(this.step) || 0;
|
||||||
|
return labelPrecision
|
||||||
|
? value.toFixed(labelPrecision)
|
||||||
|
: Math.round(value).toString();
|
||||||
|
}
|
||||||
|
|
||||||
private _update = () => {
|
private _update = () => {
|
||||||
// Not connected?
|
// Not connected?
|
||||||
if (!this._valueDisplay) return;
|
if (!this._valueDisplay) return;
|
||||||
const value = Number(this.value) || 0;
|
const value = Number(this.value) || 0;
|
||||||
const min = Number(this.min) || 0;
|
const min = Number(this.min) || 0;
|
||||||
const max = Number(this.max) || 100;
|
const max = Number(this.max) || 100;
|
||||||
const labelPrecision =
|
|
||||||
Number(this.labelPrecision) || getPrescision(this.step) || 0;
|
|
||||||
const percent = (100 * (value - min)) / (max - min);
|
const percent = (100 * (value - min)) / (max - min);
|
||||||
const displayValue = labelPrecision
|
const displayValue = this._getDisplayValue(value);
|
||||||
? value.toFixed(labelPrecision)
|
|
||||||
: Math.round(value).toString();
|
|
||||||
|
|
||||||
this._valueDisplay!.textContent = displayValue;
|
this._valueDisplay!.textContent = displayValue;
|
||||||
this.style.setProperty('--value-percent', percent + '%');
|
this.style.setProperty('--value-percent', percent + '%');
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
text-decoration-style: dotted;
|
text-decoration-style: dotted;
|
||||||
text-decoration-color: var(--main-theme-color);
|
text-decoration-color: var(--main-theme-color);
|
||||||
text-underline-position: under;
|
text-underline-position: under;
|
||||||
width: 48px;
|
width: 54px;
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.options-scroller {
|
.options-scroller {
|
||||||
--horizontal-padding: 15px;
|
--horizontal-padding: 15px;
|
||||||
border-radius: var(--scroller-radius);
|
border-radius: var(--scroller-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
/* At smaller widths, the multi-panel handles the scrolling */
|
/* At smaller widths, the multi-panel handles the scrolling */
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PointerTracker, { Pointer } from 'pointer-tracker';
|
import PointerTracker, { Pointer } from 'pointer-tracker';
|
||||||
import 'add-css:./styles.css';
|
import 'add-css:./styles.css';
|
||||||
|
import { isSafari } from 'client/lazy-app/util';
|
||||||
|
|
||||||
interface Point {
|
interface Point {
|
||||||
clientX: number;
|
clientX: number;
|
||||||
@@ -81,6 +82,7 @@ function createPoint(): SVGPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MIN_SCALE = 0.01;
|
const MIN_SCALE = 0.01;
|
||||||
|
const MAX_SCALE = 100000;
|
||||||
|
|
||||||
export default class PinchZoom extends HTMLElement {
|
export default class PinchZoom extends HTMLElement {
|
||||||
// The element that we'll transform.
|
// The element that we'll transform.
|
||||||
@@ -104,14 +106,23 @@ export default class PinchZoom extends HTMLElement {
|
|||||||
const pointerTracker: PointerTracker = new PointerTracker(this, {
|
const pointerTracker: PointerTracker = new PointerTracker(this, {
|
||||||
start: (pointer, event) => {
|
start: (pointer, event) => {
|
||||||
// We only want to track 2 pointers at most
|
// We only want to track 2 pointers at most
|
||||||
if (pointerTracker.currentPointers.length === 2 || !this._positioningEl)
|
if (
|
||||||
|
pointerTracker.currentPointers.length === 2 ||
|
||||||
|
!this._positioningEl
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
move: (previousPointers) => {
|
move: (previousPointers) => {
|
||||||
this._onPointerMove(previousPointers, pointerTracker.currentPointers);
|
this._onPointerMove(previousPointers, pointerTracker.currentPointers);
|
||||||
},
|
},
|
||||||
|
// Unfortunately Safari on iOS has a bug where pointer event capturing
|
||||||
|
// doesn't work in some cases, and we hit those cases due to our event
|
||||||
|
// retargeting in pinch-zoom.
|
||||||
|
// https://bugs.webkit.org/show_bug.cgi?id=220196
|
||||||
|
avoidPointerEvents: isSafari,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addEventListener('wheel', (event) => this._onWheel(event));
|
this.addEventListener('wheel', (event) => this._onWheel(event));
|
||||||
@@ -244,6 +255,9 @@ export default class PinchZoom extends HTMLElement {
|
|||||||
// Avoid scaling to zero
|
// Avoid scaling to zero
|
||||||
if (scale < MIN_SCALE) return;
|
if (scale < MIN_SCALE) return;
|
||||||
|
|
||||||
|
// Avoid scaling to very large values
|
||||||
|
if (scale > MAX_SCALE) return;
|
||||||
|
|
||||||
// Return if there's no change
|
// Return if there's no change
|
||||||
if (scale === this.scale && x === this.x && y === this.y) return;
|
if (scale === this.scale && x === this.x && y === this.y) return;
|
||||||
|
|
||||||
@@ -296,9 +310,13 @@ export default class PinchZoom extends HTMLElement {
|
|||||||
deltaY *= 15;
|
deltaY *= 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zoomingOut = deltaY > 0;
|
||||||
|
|
||||||
// ctrlKey is true when pinch-zooming on a trackpad.
|
// ctrlKey is true when pinch-zooming on a trackpad.
|
||||||
const divisor = ctrlKey ? 100 : 300;
|
const divisor = ctrlKey ? 100 : 300;
|
||||||
const scaleDiff = 1 - deltaY / divisor;
|
// when zooming out, invert the delta and the ratio to keep zoom stable
|
||||||
|
const ratio = 1 - (zoomingOut ? -deltaY : deltaY) / divisor;
|
||||||
|
const scaleDiff = zoomingOut ? 1 / ratio : ratio;
|
||||||
|
|
||||||
this._applyChange({
|
this._applyChange({
|
||||||
scaleDiff,
|
scaleDiff,
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import './custom-els/PinchZoom';
|
|||||||
import './custom-els/TwoUp';
|
import './custom-els/TwoUp';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import 'add-css:./style.css';
|
import 'add-css:./style.css';
|
||||||
import { shallowEqual } from '../../util';
|
import { shallowEqual, isSafari } from '../../util';
|
||||||
import {
|
import {
|
||||||
|
ToggleAliasingIcon,
|
||||||
|
ToggleAliasingActiveIcon,
|
||||||
ToggleBackgroundIcon,
|
ToggleBackgroundIcon,
|
||||||
AddIcon,
|
AddIcon,
|
||||||
RemoveIcon,
|
RemoveIcon,
|
||||||
@@ -19,7 +21,6 @@ import { cleanSet } from '../../util/clean-modify';
|
|||||||
import type { SourceImage } from '../../Compress';
|
import type { SourceImage } from '../../Compress';
|
||||||
import { linkRef } from 'shared/prerendered-app/util';
|
import { linkRef } from 'shared/prerendered-app/util';
|
||||||
import { drawDataToCanvas } from 'client/lazy-app/util/canvas';
|
import { drawDataToCanvas } from 'client/lazy-app/util/canvas';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
source?: SourceImage;
|
source?: SourceImage;
|
||||||
preprocessorState?: PreprocessorState;
|
preprocessorState?: PreprocessorState;
|
||||||
@@ -35,6 +36,7 @@ interface State {
|
|||||||
scale: number;
|
scale: number;
|
||||||
editingScale: boolean;
|
editingScale: boolean;
|
||||||
altBackground: boolean;
|
altBackground: boolean;
|
||||||
|
aliasing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleToOpts: ScaleToOpts = {
|
const scaleToOpts: ScaleToOpts = {
|
||||||
@@ -49,6 +51,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
editingScale: false,
|
editingScale: false,
|
||||||
altBackground: false,
|
altBackground: false,
|
||||||
|
aliasing: false,
|
||||||
};
|
};
|
||||||
canvasLeft?: HTMLCanvasElement;
|
canvasLeft?: HTMLCanvasElement;
|
||||||
canvasRight?: HTMLCanvasElement;
|
canvasRight?: HTMLCanvasElement;
|
||||||
@@ -145,6 +148,12 @@ export default class Output extends Component<Props, State> {
|
|||||||
return props.rightCompressed || (props.source && props.source.preprocessed);
|
return props.rightCompressed || (props.source && props.source.preprocessed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private toggleAliasing = () => {
|
||||||
|
this.setState((state) => ({
|
||||||
|
aliasing: !state.aliasing,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
private toggleBackground = () => {
|
private toggleBackground = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
altBackground: !this.state.altBackground,
|
altBackground: !this.state.altBackground,
|
||||||
@@ -255,7 +264,7 @@ export default class Output extends Component<Props, State> {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
{ mobileView, leftImgContain, rightImgContain, source }: Props,
|
{ mobileView, leftImgContain, rightImgContain, source }: Props,
|
||||||
{ scale, editingScale, altBackground }: State,
|
{ scale, editingScale, altBackground, aliasing }: State,
|
||||||
) {
|
) {
|
||||||
const leftDraw = this.leftDrawable();
|
const leftDraw = this.leftDrawable();
|
||||||
const rightDraw = this.rightDrawable();
|
const rightDraw = this.rightDrawable();
|
||||||
@@ -275,7 +284,11 @@ export default class Output extends Component<Props, State> {
|
|||||||
onTouchStartCapture={this.onRetargetableEvent}
|
onTouchStartCapture={this.onRetargetableEvent}
|
||||||
onTouchEndCapture={this.onRetargetableEvent}
|
onTouchEndCapture={this.onRetargetableEvent}
|
||||||
onTouchMoveCapture={this.onRetargetableEvent}
|
onTouchMoveCapture={this.onRetargetableEvent}
|
||||||
onPointerDownCapture={this.onRetargetableEvent}
|
onPointerDownCapture={
|
||||||
|
// We avoid pointer events in our PinchZoom due to a Safari bug.
|
||||||
|
// That means we also need to avoid them here too, else we end up preventing the fallback mouse events.
|
||||||
|
isSafari ? undefined : this.onRetargetableEvent
|
||||||
|
}
|
||||||
onMouseDownCapture={this.onRetargetableEvent}
|
onMouseDownCapture={this.onRetargetableEvent}
|
||||||
onWheelCapture={this.onRetargetableEvent}
|
onWheelCapture={this.onRetargetableEvent}
|
||||||
>
|
>
|
||||||
@@ -285,7 +298,9 @@ export default class Output extends Component<Props, State> {
|
|||||||
ref={linkRef(this, 'pinchZoomLeft')}
|
ref={linkRef(this, 'pinchZoomLeft')}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
class={style.pinchTarget}
|
class={`${style.pinchTarget} ${
|
||||||
|
aliasing ? style.pixelated : ''
|
||||||
|
}`}
|
||||||
ref={linkRef(this, 'canvasLeft')}
|
ref={linkRef(this, 'canvasLeft')}
|
||||||
width={leftDraw && leftDraw.width}
|
width={leftDraw && leftDraw.width}
|
||||||
height={leftDraw && leftDraw.height}
|
height={leftDraw && leftDraw.height}
|
||||||
@@ -301,7 +316,9 @@ export default class Output extends Component<Props, State> {
|
|||||||
ref={linkRef(this, 'pinchZoomRight')}
|
ref={linkRef(this, 'pinchZoomRight')}
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
class={style.pinchTarget}
|
class={`${style.pinchTarget} ${
|
||||||
|
aliasing ? style.pixelated : ''
|
||||||
|
}`}
|
||||||
ref={linkRef(this, 'canvasRight')}
|
ref={linkRef(this, 'canvasRight')}
|
||||||
width={rightDraw && rightDraw.width}
|
width={rightDraw && rightDraw.width}
|
||||||
height={rightDraw && rightDraw.height}
|
height={rightDraw && rightDraw.height}
|
||||||
@@ -345,10 +362,31 @@ export default class Output extends Component<Props, State> {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class={style.buttonGroup}>
|
<div class={style.buttonGroup}>
|
||||||
<button class={style.firstButton} onClick={this.onRotateClick}>
|
<button
|
||||||
|
class={style.firstButton}
|
||||||
|
onClick={this.onRotateClick}
|
||||||
|
title="Rotate"
|
||||||
|
>
|
||||||
<RotateIcon />
|
<RotateIcon />
|
||||||
</button>
|
</button>
|
||||||
<button class={style.lastButton} onClick={this.toggleBackground}>
|
{!isSafari && (
|
||||||
|
<button
|
||||||
|
class={style.button}
|
||||||
|
onClick={this.toggleAliasing}
|
||||||
|
title="Toggle smoothing"
|
||||||
|
>
|
||||||
|
{aliasing ? (
|
||||||
|
<ToggleAliasingActiveIcon />
|
||||||
|
) : (
|
||||||
|
<ToggleAliasingIcon />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class={style.lastButton}
|
||||||
|
onClick={this.toggleBackground}
|
||||||
|
title="Toggle background"
|
||||||
|
>
|
||||||
{altBackground ? (
|
{altBackground ? (
|
||||||
<ToggleBackgroundActiveIcon />
|
<ToggleBackgroundActiveIcon />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -86,8 +86,7 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:focus {
|
&:focus-visible {
|
||||||
/* box-shadow: 0 0 0 2px var(--hot-pink); */
|
|
||||||
box-shadow: 0 0 0 2px #fff;
|
box-shadow: 0 0 0 2px #fff;
|
||||||
outline: none;
|
outline: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -161,3 +160,8 @@ input.zoom {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pixelated {
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,25 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ToggleAliasingIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||||
|
<Icon {...props}>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToggleAliasingActiveIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||||
|
<Icon {...props}>
|
||||||
|
<path d="M12 3h5v2h2v2h2v5h-2V9h-2V7h-2V5h-3V3M21 12v5h-2v2h-2v2h-5v-2h3v-2h2v-2h2v-3h2M12 21H7v-2H5v-2H3v-5h2v3h2v2h2v2h3v2M3 12V7h2V5h2V3h5v2H9v2H7v2H5v3H3" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
|
||||||
export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => (
|
export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||||
<Icon {...props}>
|
<Icon {...props}>
|
||||||
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
|
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
import * as WebCodecs from '../util/web-codecs';
|
import * as WebCodecs from '../util/web-codecs';
|
||||||
import { drawableToImageData } from './canvas';
|
import { drawableToImageData } from './canvas';
|
||||||
|
|
||||||
|
/** If render engine is Safari */
|
||||||
|
export const isSafari =
|
||||||
|
/Safari\//.test(navigator.userAgent) &&
|
||||||
|
!/Chrom(e|ium)\//.test(navigator.userAgent);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare two objects, returning a boolean indicating if
|
* Compare two objects, returning a boolean indicating if
|
||||||
* they have the same properties and strictly equal values.
|
* they have the same properties and strictly equal values.
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ interface State {
|
|||||||
slightLoss: boolean;
|
slightLoss: boolean;
|
||||||
autoEdgePreservingFilter: boolean;
|
autoEdgePreservingFilter: boolean;
|
||||||
decodingSpeedTier: number;
|
decodingSpeedTier: number;
|
||||||
|
photonNoiseIso: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSpeed = 7;
|
|
||||||
|
|
||||||
export class Options extends Component<Props, State> {
|
export class Options extends Component<Props, State> {
|
||||||
static getDerivedStateFromProps(
|
static getDerivedStateFromProps(
|
||||||
props: Props,
|
props: Props,
|
||||||
@@ -47,7 +46,7 @@ export class Options extends Component<Props, State> {
|
|||||||
// Create default form state from options
|
// Create default form state from options
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
effort: maxSpeed - options.speed,
|
effort: options.effort,
|
||||||
quality: options.quality,
|
quality: options.quality,
|
||||||
progressive: options.progressive,
|
progressive: options.progressive,
|
||||||
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
|
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
|
||||||
@@ -55,6 +54,7 @@ export class Options extends Component<Props, State> {
|
|||||||
slightLoss: options.lossyPalette,
|
slightLoss: options.lossyPalette,
|
||||||
autoEdgePreservingFilter: options.epf === -1,
|
autoEdgePreservingFilter: options.epf === -1,
|
||||||
decodingSpeedTier: options.decodingSpeedTier,
|
decodingSpeedTier: options.decodingSpeedTier,
|
||||||
|
photonNoiseIso: options.photonNoiseIso,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +87,15 @@ export class Options extends Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const newOptions: EncodeOptions = {
|
const newOptions: EncodeOptions = {
|
||||||
speed: maxSpeed - optionState.effort,
|
effort: optionState.effort,
|
||||||
quality: optionState.lossless ? 100 : optionState.quality,
|
quality: optionState.lossless ? 100 : optionState.quality,
|
||||||
progressive: optionState.progressive,
|
progressive: optionState.progressive,
|
||||||
epf: optionState.autoEdgePreservingFilter
|
epf: optionState.autoEdgePreservingFilter
|
||||||
? -1
|
? -1
|
||||||
: optionState.edgePreservingFilter,
|
: optionState.edgePreservingFilter,
|
||||||
nearLossless: 0,
|
|
||||||
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
|
||||||
decodingSpeedTier: optionState.decodingSpeedTier,
|
decodingSpeedTier: optionState.decodingSpeedTier,
|
||||||
|
photonNoiseIso: optionState.photonNoiseIso,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||||
@@ -121,6 +121,7 @@ export class Options extends Component<Props, State> {
|
|||||||
slightLoss,
|
slightLoss,
|
||||||
autoEdgePreservingFilter,
|
autoEdgePreservingFilter,
|
||||||
decodingSpeedTier,
|
decodingSpeedTier,
|
||||||
|
photonNoiseIso,
|
||||||
}: State,
|
}: State,
|
||||||
) {
|
) {
|
||||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||||
@@ -164,7 +165,6 @@ export class Options extends Component<Props, State> {
|
|||||||
<label class={style.optionToggle}>
|
<label class={style.optionToggle}>
|
||||||
Auto edge filter
|
Auto edge filter
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="autoEdgeFilter"
|
|
||||||
checked={autoEdgePreservingFilter}
|
checked={autoEdgePreservingFilter}
|
||||||
onChange={this._inputChange(
|
onChange={this._inputChange(
|
||||||
'autoEdgePreservingFilter',
|
'autoEdgePreservingFilter',
|
||||||
@@ -199,6 +199,17 @@ export class Options extends Component<Props, State> {
|
|||||||
Optimise for decoding speed (worse compression):
|
Optimise for decoding speed (worse compression):
|
||||||
</Range>
|
</Range>
|
||||||
</div>
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="50000"
|
||||||
|
step="100"
|
||||||
|
value={photonNoiseIso}
|
||||||
|
onInput={this._inputChange('photonNoiseIso', 'number')}
|
||||||
|
>
|
||||||
|
Noise equivalent to ISO:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Expander>
|
</Expander>
|
||||||
@@ -212,8 +223,8 @@ export class Options extends Component<Props, State> {
|
|||||||
</label>
|
</label>
|
||||||
<div class={style.optionOneCell}>
|
<div class={style.optionOneCell}>
|
||||||
<Range
|
<Range
|
||||||
min="0"
|
min="3"
|
||||||
max={maxSpeed - 1}
|
max="9"
|
||||||
value={effort}
|
value={effort}
|
||||||
onInput={this._inputChange('effort', 'number')}
|
onInput={this._inputChange('effort', 'number')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export const label = 'JPEG XL (beta)';
|
|||||||
export const mimeType = 'image/jxl';
|
export const mimeType = 'image/jxl';
|
||||||
export const extension = 'jxl';
|
export const extension = 'jxl';
|
||||||
export const defaultOptions: EncodeOptions = {
|
export const defaultOptions: EncodeOptions = {
|
||||||
speed: 4,
|
effort: 7,
|
||||||
quality: 75,
|
quality: 75,
|
||||||
progressive: false,
|
progressive: false,
|
||||||
epf: -1,
|
epf: -1,
|
||||||
nearLossless: 0,
|
|
||||||
lossyPalette: false,
|
lossyPalette: false,
|
||||||
decodingSpeedTier: 0,
|
decodingSpeedTier: 0,
|
||||||
|
photonNoiseIso: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
51
src/shared/prerendered-app/Intro/SlideOnScroll/index.tsx
Normal file
51
src/shared/prerendered-app/Intro/SlideOnScroll/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { h, Component, RenderableProps } from 'preact';
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
export default class SlideOnScroll extends Component<Props, State> {
|
||||||
|
private observer?: IntersectionObserver;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||||
|
|
||||||
|
const base = this.base as HTMLElement;
|
||||||
|
let wasOutOfView = false;
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries, observer) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
wasOutOfView = true;
|
||||||
|
base.style.opacity = '0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only transition in if the element was at some point out of view.
|
||||||
|
if (wasOutOfView) {
|
||||||
|
base.style.opacity = '';
|
||||||
|
base.animate(
|
||||||
|
{ offset: 0, opacity: '0', transform: 'translateY(40px)' },
|
||||||
|
{ duration: 300, easing: 'ease' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.observer.observe(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// Have to manually disconnect due to memory leaks in browsers.
|
||||||
|
// One day we'll be able to remove this, and the private property.
|
||||||
|
// https://twitter.com/jaffathecake/status/1405437361643790337
|
||||||
|
if (this.observer) this.observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ children }: RenderableProps<Props>) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg width="498" height="333" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M401.17 125.52C387.072 53.923 324.253.173 248.787.173c-59.916 0-111.954 34.035-137.869 83.841C48.513 90.655 0 143.574 0 207.7c0 68.692 55.77 124.516 124.394 124.516h269.519c57.221 0 103.662-46.486 103.662-103.763 0-54.787-42.502-99.198-96.405-102.933z" fill="#91D3FF" fill-opacity=".3"/><path d="M187.247 121.321l-3.987-3.987-3.481 4.434c-11.519 14.67-18.366 33.15-18.366 53.242 0 48.003 38.882 86.885 86.885 86.885 20.091 0 38.572-6.848 53.242-18.366l4.434-3.482-3.987-3.986-114.74-114.74zM309.348 228.7l3.987 3.986 3.481-4.434c11.519-14.669 18.366-33.15 18.366-53.242 0-48.002-38.882-86.884-86.884-86.884-20.092 0-38.573 6.847-53.242 18.365l-4.435 3.482 3.987 3.986L309.348 228.7zm-158.406-53.69c0-53.739 43.617-97.355 97.356-97.355 53.738 0 97.355 43.616 97.355 97.355 0 53.739-43.617 97.356-97.355 97.356-53.739 0-97.356-43.617-97.356-97.356z" fill="#FF3385" stroke="#FF3385" stroke-width="10"/></svg>
|
||||||
|
After Width: | Height: | Size: 991 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.2 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
@@ -10,12 +10,16 @@ import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
|
|||||||
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
import largePhotoIcon from 'url:./imgs/demos/icon-demo-large-photo.jpg';
|
||||||
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
import artworkIcon from 'url:./imgs/demos/icon-demo-artwork.jpg';
|
||||||
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
import deviceScreenIcon from 'url:./imgs/demos/icon-demo-device-screen.jpg';
|
||||||
|
import smallSectionAsset from 'url:./imgs/info-content/small.svg';
|
||||||
|
import simpleSectionAsset from 'url:./imgs/info-content/simple.svg';
|
||||||
|
import secureSectionAsset from 'url:./imgs/info-content/secure.svg';
|
||||||
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
import logoIcon from 'url:./imgs/demos/icon-demo-logo.png';
|
||||||
import logoWithText from 'url:./imgs/logo-with-text.svg';
|
import logoWithText from 'url:./imgs/logo-with-text.svg';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
import type SnackBarElement from 'shared/custom-els/snack-bar';
|
||||||
import 'shared/custom-els/snack-bar';
|
import 'shared/custom-els/snack-bar';
|
||||||
import { startBlobs } from './blob-anim/meta';
|
import { startBlobs } from './blob-anim/meta';
|
||||||
|
import SlideOnScroll from './SlideOnScroll';
|
||||||
|
|
||||||
const demos = [
|
const demos = [
|
||||||
{
|
{
|
||||||
@@ -336,14 +340,101 @@ export default class Intro extends Component<Props, State> {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={style.footer}>
|
|
||||||
|
<div class={style.bottomWave}>
|
||||||
|
<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.infoWave}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class={style.info}>
|
||||||
|
<div class={style.infoContainer}>
|
||||||
|
<SlideOnScroll>
|
||||||
|
<div class={style.infoContent}>
|
||||||
|
<div class={style.infoTextWrapper}>
|
||||||
|
<h2 class={style.infoTitle}>Small</h2>
|
||||||
|
<p class={style.infoCaption}>
|
||||||
|
Smaller images mean faster load times. Squoosh can reduce
|
||||||
|
file size and maintain high quality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class={style.infoImgWrapper}>
|
||||||
|
<img
|
||||||
|
class={style.infoImg}
|
||||||
|
src={smallSectionAsset}
|
||||||
|
alt="silhouette of a large 1.4 megabyte image shrunk into a smaller 80 kilobyte image"
|
||||||
|
width="536"
|
||||||
|
height="522"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlideOnScroll>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class={style.info}>
|
||||||
|
<div class={style.infoContainer}>
|
||||||
|
<SlideOnScroll>
|
||||||
|
<div class={style.infoContent}>
|
||||||
|
<div class={style.infoTextWrapper}>
|
||||||
|
<h2 class={style.infoTitle}>Simple</h2>
|
||||||
|
<p class={style.infoCaption}>
|
||||||
|
Open your image, inspect the differences, then save
|
||||||
|
instantly. Feeling adventurous? Adjust the settings for even
|
||||||
|
smaller files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class={style.infoImgWrapper}>
|
||||||
|
<img
|
||||||
|
class={style.infoImg}
|
||||||
|
src={simpleSectionAsset}
|
||||||
|
alt="grid of multiple shrunk images displaying various options"
|
||||||
|
width="538"
|
||||||
|
height="384"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlideOnScroll>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class={style.info}>
|
||||||
|
<div class={style.infoContainer}>
|
||||||
|
<SlideOnScroll>
|
||||||
|
<div class={style.infoContent}>
|
||||||
|
<div class={style.infoTextWrapper}>
|
||||||
|
<h2 class={style.infoTitle}>Secure</h2>
|
||||||
|
<p class={style.infoCaption}>
|
||||||
|
Worried about privacy? Images never leave your device since
|
||||||
|
Squoosh does all the work locally.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class={style.infoImgWrapper}>
|
||||||
|
<img
|
||||||
|
class={style.infoImg}
|
||||||
|
src={secureSectionAsset}
|
||||||
|
alt="silhouette of a cloud with a 'no' symbol on top"
|
||||||
|
width="498"
|
||||||
|
height="333"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SlideOnScroll>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class={style.footer}>
|
||||||
|
<div class={style.footerContainer}>
|
||||||
<svg viewBox="0 0 1920 79" class={style.topWave}>
|
<svg viewBox="0 0 1920 79" class={style.topWave}>
|
||||||
<path
|
<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"
|
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}
|
class={style.footerWave}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class={style.contentPadding}>
|
<div class={style.footerPadding}>
|
||||||
<footer class={style.footerItems}>
|
<footer class={style.footerItems}>
|
||||||
<a
|
<a
|
||||||
class={style.footerLink}
|
class={style.footerLink}
|
||||||
@@ -367,6 +458,7 @@ export default class Intro extends Component<Props, State> {
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
{beforeInstallEvent && (
|
{beforeInstallEvent && (
|
||||||
<button class={style.installBtn} onClick={this.onInstallClick}>
|
<button class={style.installBtn} onClick={this.onInstallClick}>
|
||||||
Install
|
Install
|
||||||
|
|||||||
@@ -118,7 +118,114 @@
|
|||||||
fill: var(--light-blue);
|
fill: var(--light-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bottom-wave {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background: var(--white);
|
||||||
|
position: relative;
|
||||||
|
padding: 5em 2em;
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
padding: 5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
'text'
|
||||||
|
'img';
|
||||||
|
|
||||||
|
@media (min-width: 712px) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-areas: 'text img';
|
||||||
|
|
||||||
|
.info:nth-child(even) & {
|
||||||
|
grid-template-areas: 'img text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
color: var(--pink);
|
||||||
|
font-size: 3em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-caption {
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 1.75;
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-link {
|
||||||
|
font-size: 1.25em;
|
||||||
|
text-underline-offset: 0.25em;
|
||||||
|
color: var(--off-black);
|
||||||
|
transition: color 400ms ease-in-out;
|
||||||
|
margin-top: 1em;
|
||||||
|
&:hover {
|
||||||
|
color: var(--dim-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
max-width: 27em;
|
||||||
|
justify-self: center;
|
||||||
|
grid-area: text;
|
||||||
|
|
||||||
|
@media (min-width: 712px) {
|
||||||
|
justify-self: start;
|
||||||
|
.info:nth-child(even) & {
|
||||||
|
text-align: right;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-img-wrapper {
|
||||||
|
grid-area: img;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
|
||||||
|
.info:nth-child(even) & {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-wave {
|
||||||
|
fill: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
background: var(--white);
|
||||||
|
padding-top: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--light-gray);
|
background: var(--light-gray);
|
||||||
}
|
}
|
||||||
@@ -128,7 +235,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-padding {
|
.content-padding {
|
||||||
padding: 2rem;
|
padding: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-padding {
|
||||||
|
padding: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-items {
|
.footer-items {
|
||||||
|
|||||||
@@ -25,18 +25,6 @@ import { lookup as lookupMime } from 'mime-types';
|
|||||||
const manifestSize = ({ width, height }: { width: number; height: number }) =>
|
const manifestSize = ({ width, height }: { width: number; height: number }) =>
|
||||||
`${width}x${height}`;
|
`${width}x${height}`;
|
||||||
|
|
||||||
// Set by Netlify
|
|
||||||
const branch = process.env.BRANCH;
|
|
||||||
|
|
||||||
const branchOriginTrialIds = new Map([
|
|
||||||
[
|
|
||||||
'live',
|
|
||||||
'Aj5GY7W9AHM8di+yvMCajIhLRHoYN7slruwOYXE/Iub5hgmW/r2RQt07vrUuT4eUTkWxcyNCAVkiI+5ugdVW3gAAAABUeyJvcmlnaW4iOiJodHRwczovL3NxdW9vc2guYXBwOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVNpbWQiLCJleHBpcnkiOjE2MjM4MDE1OTl9',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const originTrialId = branchOriginTrialIds.get(branch || '');
|
|
||||||
|
|
||||||
interface Output {
|
interface Output {
|
||||||
[outputPath: string]: string;
|
[outputPath: string]: string;
|
||||||
}
|
}
|
||||||
@@ -110,15 +98,6 @@ const toOutput: Output = {
|
|||||||
/*
|
/*
|
||||||
Cross-Origin-Embedder-Policy: require-corp
|
Cross-Origin-Embedder-Policy: require-corp
|
||||||
Cross-Origin-Opener-Policy: same-origin
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
|
|
||||||
# Origin trial for WebAssembly SIMD.
|
|
||||||
${
|
|
||||||
originTrialId
|
|
||||||
? ` Origin-Trial: ${originTrialId}`
|
|
||||||
: `# Cannot find origin trial ID. process.env.BRANCH is: ${JSON.stringify(
|
|
||||||
branch,
|
|
||||||
)}`
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const Index: FunctionalComponent<Props> = () => (
|
|||||||
<link rel="shortcut icon" href={favicon} />
|
<link rel="shortcut icon" href={favicon} />
|
||||||
<meta name="theme-color" content="#ff3385" />
|
<meta name="theme-color" content="#ff3385" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="canonical" href={siteOrigin} />
|
||||||
<style
|
<style
|
||||||
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(baseCss) }}
|
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(baseCss) }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "./generic-tsconfig.json",
|
"extends": "./generic-tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["esnext", "dom"],
|
"lib": ["esnext", "dom", "dom.iterable"],
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/shared/**/*", "src/static-build/**/*"]
|
"include": ["src/shared/**/*", "src/static-build/**/*"]
|
||||||
|
|||||||
Reference in New Issue
Block a user