Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Button to copy settings to the other side #150

Closed
wants to merge 61 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
8403db5
Move gzipped size calculations into a worker and wrap it up in a `<Gz…
developit Aug 10, 2018
9ecbfb6
A couple tweaks for the app welcome (drop files) screen. We don't hav…
developit Aug 10, 2018
522279a
Prettier "pop" effect and styling for the drop zone/indicator.
developit Aug 10, 2018
388075b
Styling for the quantization toggle to make it look like a disclosure…
developit Aug 10, 2018
abc73c7
Add controls bar (zoom in/out/to, background toggle). @todo: extract …
developit Aug 10, 2018
a4a3cb8
When clicking/tapping the image area, give it focus.
developit Aug 10, 2018
89c82d6
Utilities used by this PR
developit Aug 10, 2018
3a36252
Add a `two-up-handle` attribute to the handle for easier styling (cla…
developit Aug 10, 2018
dd72202
Add a dummy comment to test netlify deploy
surma Aug 10, 2018
eef6353
Remove commented-out code.
developit Aug 10, 2018
5a622c8
Fix styling of vertical split (which as it turns out is slightly diff…
developit Aug 10, 2018
aa0d262
Use a composited overlay for the dark background instead of animating…
developit Aug 10, 2018
f32ac8f
Move grayscale styling into `<two-up>` by default, then set colors vi…
developit Aug 10, 2018
4b31b37
Remove commented-out svg fill
developit Aug 10, 2018
99776d1
Remove dummy comment
developit Aug 10, 2018
d17d94e
Change `<GzipSize>` to be `<FileSize>`, add `compress` option that le…
developit Aug 10, 2018
8d9f0c2
Dependency updates
developit Aug 10, 2018
c6ef9e9
Remove color animations from dnd overlay
developit Aug 10, 2018
1bcf82c
Don't use a cyclical import for EncodedImage, instead just specify th…
developit Aug 10, 2018
f195797
Pass source image through to FileSize component so it can compute delta
developit Aug 10, 2018
65e0a5f
Stylize size display with colors based on delta amount/direction
developit Aug 10, 2018
f711b1e
Remove box-shadow animation.
developit Aug 10, 2018
a96d1c5
Merge branch 'master' into ui
developit Aug 10, 2018
bbbeb10
Simplify font stack
developit Aug 16, 2018
31dfdbe
Remove commented out code
developit Aug 16, 2018
117eb80
Remove gzip compression from size component
developit Aug 16, 2018
d76e6c5
Remove memoization bits
developit Aug 16, 2018
b95621b
Use specific flattend props instead of passing large context objects …
developit Aug 16, 2018
ac870d4
Remove unused packages.
developit Aug 16, 2018
4d94cc8
Remove unreachable String case in FileSize, and omit redundant File type
developit Aug 16, 2018
163219a
Simplify calculateSize()
developit Aug 16, 2018
e1fbd2c
Fix types for FileSize!
developit Aug 16, 2018
cf98b7a
Remove FileSize title
developit Aug 16, 2018
385d61b
Make delta variable consistent.
developit Aug 16, 2018
31fc90b
Skip passing compareTo value for original image
developit Aug 16, 2018
bcce5a2
Remove manual focus
developit Aug 16, 2018
d83f845
Fix whitespace
developit Aug 16, 2018
d2da8d9
remove unused keyframes
developit Aug 16, 2018
cef8b4c
remove pointless flex-wrap property
developit Aug 16, 2018
06b2fa9
Remove unused resetZoom() method
developit Aug 16, 2018
8ae382f
Remove pointless flex properties
developit Aug 16, 2018
e962f89
Use `on` prefix for event handling
developit Aug 16, 2018
5838703
Remove pointless justify-self property
developit Aug 16, 2018
5109598
Use an inline SVG for TwoUp's handle icon so it can be colored from o…
developit Aug 16, 2018
ac87c64
Move orientation state up from `<Output>` into `<App>` and share it w…
developit Aug 16, 2018
14ee88f
Make the options panels responsive :)
developit Aug 16, 2018
6ad9d05
Show a plus sign for size increases `(+8%)`
developit Aug 16, 2018
8768212
Use inline SVG for the zoom +/- icons, collect SVG icons into one fil…
developit Aug 16, 2018
aa98359
Fix top/bottom options panels being reversed
developit Aug 16, 2018
0055bf2
remove commented out code
developit Aug 20, 2018
096a4e4
lockfile
developit Aug 20, 2018
8b106a8
Revert quanitzation toggle styles so it's just a checkbox.
developit Aug 20, 2018
e11122b
Remove minimum delta for compare size
developit Aug 20, 2018
2ef8ddb
Rename data prop to file.
developit Aug 20, 2018
e7af112
scale int -> float
developit Aug 20, 2018
78b7197
remove tabIndex
developit Aug 20, 2018
72eadc8
Adding encoding results cache
jakearchibald Aug 22, 2018
16e5b1b
Error isn't always encoding related
jakearchibald Aug 22, 2018
3146d6e
Making size an option was overcomplicating it
jakearchibald Aug 22, 2018
723bd46
Could use destructuring here too
jakearchibald Aug 22, 2018
ac53a9f
Button to copy settings to the other side
jakearchibald Aug 22, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ declare namespace JSX {
interface IntrinsicElements { }
}

declare module 'preact-i18n';
declare module 'preact-material-components-drawer';
declare module 'material-radial-progress';

declare module 'classnames' {
export default function classnames(...args: any[]): string;
}
6,017 changes: 2,248 additions & 3,769 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"devDependencies": {
"@types/node": "^9.6.23",
"@types/pretty-bytes": "^5.1.0",
"@types/webassembly-js-api": "0.0.1",
"babel-loader": "^7.1.5",
"babel-plugin-jsx-pragmatic": "^1.0.2",
Expand Down Expand Up @@ -62,15 +63,10 @@
"webpack-plugin-replace": "^1.1.1"
},
"dependencies": {
"@types/filesize": "^3.6.0",
"classnames": "^2.2.6",
"comlink": "^3.0.3",
"comlink-loader": "^1.0.0",
"filesize": "^3.6.1",
"material-components-web": "^0.32.0",
"preact": "^8.2.9",
"preact-i18n": "^1.2.2",
"preact-material-components": "^1.4.7",
"preact-router": "^2.6.1"
"pretty-bytes": "^5.1.0"
}
}
120 changes: 76 additions & 44 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { h, Component } from 'preact';
import { partial } from 'filesize';

import { bind, linkRef, bitmapToImageData } from '../../lib/util';
import * as style from './style.scss';
import Output from '../Output';
import Options from '../Options';
import { FileDropEvent } from './custom-els/FileDrop';
import './custom-els/FileDrop';
import ResultCache from './result-cache';

import * as quantizer from '../../codecs/imagequant/quantizer';
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
Expand Down Expand Up @@ -37,7 +37,9 @@ import {
import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify';

interface SourceImage {
type Orientation = 'horizontal' | 'vertical';

export interface SourceImage {
file: File;
bmp: ImageBitmap;
data: ImageData;
Expand All @@ -64,14 +66,13 @@ interface State {
images: [EncodedImage, EncodedImage];
loading: boolean;
error?: string;
orientation: Orientation;
}

interface UpdateImageOptions {
skipPreprocessing?: boolean;
}

const filesize = partial({});

async function preprocessImage(
source: SourceImage,
preprocessData: PreprocessorState,
Expand Down Expand Up @@ -113,6 +114,8 @@ async function compressImage(
}

export default class App extends Component<Props, State> {
widthQuery = window.matchMedia('(min-width: 500px)');

state: State = {
loading: false,
images: [
Expand All @@ -131,9 +134,11 @@ export default class App extends Component<Props, State> {
loading: false,
},
],
orientation: this.widthQuery.matches ? 'horizontal' : 'vertical',
};

private snackbar?: SnackBarElement;
snackbar?: SnackBarElement;
readonly encodeCache = new ResultCache();

constructor() {
super();
Expand All @@ -146,6 +151,13 @@ export default class App extends Component<Props, State> {
window.STATE = this.state;
};
}

this.widthQuery.addListener(this.onMobileWidthChange);
}

@bind
onMobileWidthChange() {
this.setState({ orientation: this.widthQuery.matches ? 'horizontal' : 'vertical' });
}

onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
Expand Down Expand Up @@ -206,6 +218,14 @@ export default class App extends Component<Props, State> {
await this.updateFile(file);
}

onCopyToOtherClick(index: 0 | 1) {
const otherIndex = (index + 1) % 2;

this.setState({
images: cleanSet(this.state.images, otherIndex, this.state.images[index]),
});
}

async updateFile(file: File) {
this.setState({ loading: true });
try {
Expand Down Expand Up @@ -242,19 +262,38 @@ export default class App extends Component<Props, State> {
const image = images[index];

let file;
try {
// Special case for identity
if (image.encoderState.type === identity.type) {
file = source.file;
} else {
if (!skipPreprocessing || !image.preprocessed) {
image.preprocessed = await preprocessImage(source, image.preprocessorState);
let preprocessed;
let bmp;
const cacheResult = this.encodeCache.match(source, image.preprocessorState, image.encoderState);

if (cacheResult) {
({ file, preprocessed, bmp } = cacheResult);
} else {
try {
// Special case for identity
if (image.encoderState.type === identity.type) {
({ file, bmp } = source);
} else {
preprocessed = (skipPreprocessing && image.preprocessed)
? image.preprocessed
: await preprocessImage(source, image.preprocessorState);

file = await compressImage(preprocessed, image.encoderState, source.file.name);
bmp = await decodeImage(file);

this.encodeCache.add({
source,
bmp,
preprocessed,
file,
encoderState: image.encoderState,
preprocessorState: image.preprocessorState,
});
}
file = await compressImage(image.preprocessed, image.encoderState, source.file.name);
} catch (err) {
this.showError(`Processing error (type=${image.encoderState.type}): ${err}`);
throw err;
}
} catch (err) {
this.showError(`Encoding error (type=${image.encoderState.type}): ${err}`);
throw err;
}

const latestImage = this.state.images[index];
Expand All @@ -263,17 +302,10 @@ export default class App extends Component<Props, State> {
return;
}

let bmp;
try {
bmp = await decodeImage(file);
} catch (err) {
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err;
}

images = cleanMerge(this.state.images, '' + index, {
images = cleanMerge(this.state.images, index, {
file,
bmp,
preprocessed,
downloadUrl: URL.createObjectURL(file),
loading: images[index].loadingCounter !== loadingCounter,
loadedCounter: loadingCounter,
Expand All @@ -287,38 +319,38 @@ export default class App extends Component<Props, State> {
this.snackbar.showSnackbar({ message: error });
}

render({ }: Props, { loading, images }: State) {
render({ }: Props, { loading, images, source, orientation }: State) {
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
const anyLoading = loading || images.some(image => image.loading);

return (
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
<div id="app" class={style.app}>
<div id="app" class={`${style.app} ${style[orientation]}`}>
{(leftImageBmp && rightImageBmp) ? (
<Output leftImg={leftImageBmp} rightImg={rightImageBmp} />
<Output
orientation={orientation}
leftImg={leftImageBmp}
rightImg={rightImageBmp}
/>
) : (
<div class={style.welcome}>
<h1>Select an image</h1>
<input type="file" onChange={this.onFileChange} />
</div>
)}
{images.map((image, index) => (
<span class={index ? style.rightLabel : style.leftLabel}>
{encoderMap[image.encoderState.type].label}
{(image.downloadUrl && image.file) && (
<a href={image.downloadUrl} download={image.file.name}>🔻</a>
)}
{image.file && ` - ${filesize(image.file.size)}`}
</span>
))}
{images.map((image, index) => (
<div class={style.welcome}>
<h1>Drop, paste or select an image</h1>
<input type="file" onChange={this.onFileChange} />
</div>
)}
{(leftImageBmp && rightImageBmp) && images.map((image, index) => (
<Options
class={index ? style.rightOptions : style.leftOptions}
orientation={orientation}
imageIndex={index}
imageFile={image.file}
sourceImageFile={source && source.file}
downloadUrl={image.downloadUrl}
preprocessorState={image.preprocessorState}
encoderState={image.encoderState}
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
/>
))}
{anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
Expand Down
68 changes: 68 additions & 0 deletions src/components/App/result-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { EncoderState } from '../../codecs/encoders';
import { shallowEqual } from '../../lib/util';
import { SourceImage } from '.';
import { PreprocessorState } from '../../codecs/preprocessors';

import * as identity from '../../codecs/identity/encoder';

interface CacheResult {
preprocessed: ImageData;
bmp: ImageBitmap;
file: File;
}

interface CacheEntry extends CacheResult {
preprocessorState: PreprocessorState;
encoderState: EncoderState;
source: SourceImage;
}

const SIZE = 5;

export default class ResultCache {
private readonly _entries: CacheEntry[] = [];
private _nextIndex: number = 0;

add(entry: CacheEntry) {
if (entry.encoderState.type === identity.type) throw Error('Cannot cache identity encodes');
this._entries[this._nextIndex] = entry;
this._nextIndex = (this._nextIndex + 1) % SIZE;
}

match(
source: SourceImage,
preprocessorState: PreprocessorState,
encoderState: EncoderState,
): CacheResult | undefined {
const matchingEntry = this._entries.find((entry) => {
// Check for quick exits:
if (entry.source !== source) return false;
if (entry.encoderState.type !== encoderState.type) return false;

// Check that each set of options in the preprocessor are the same
for (const prop in preprocessorState) {
if (
!shallowEqual(
(preprocessorState as any)[prop],
(entry.preprocessorState as any)[prop],
)
) return false;
}

// Check detailed encoder options
if (!shallowEqual(encoderState.options, entry.encoderState.options)) return false;

return true;
});

if (matchingEntry) {
return {
bmp: matchingEntry.bmp,
preprocessed: matchingEntry.preprocessed,
file: matchingEntry.file,
};
}

return undefined;
}
}
Loading