Refactorings (#65)

* Refactorings

* Responding to feedback
This commit is contained in:
Jake Archibald
2018-07-01 16:01:42 +01:00
committed by GitHub
parent cc3ed168d8
commit 9add650b75
3 changed files with 75 additions and 50 deletions

View File

@@ -35,6 +35,28 @@ interface State {
error?: string; error?: string;
} }
async function compressImage(
source: SourceImage,
encodeData: EncoderState,
): Promise<ImageBitmap> {
// Special case for identity
if (encodeData.type === identity.type) return source.bmp;
const compressedData = await (() => {
switch (encodeData.type) {
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
default: throw Error(`Unexpected encoder name`);
}
})();
const blob = new Blob([compressedData], {
type: encoderMap[encodeData.type].mimeType,
});
const bitmap = await createImageBitmap(blob);
return bitmap;
}
export default class App extends Component<Props, State> { export default class App extends Component<Props, State> {
state: State = { state: State = {
loading: false, loading: false,
@@ -43,15 +65,15 @@ export default class App extends Component<Props, State> {
encoderState: { type: identity.type, options: identity.defaultOptions }, encoderState: { type: identity.type, options: identity.defaultOptions },
loadingCounter: 0, loadingCounter: 0,
loadedCounter: 0, loadedCounter: 0,
loading: false loading: false,
}, },
{ {
encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions }, encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions },
loadingCounter: 0, loadingCounter: 0,
loadedCounter: 0, loadedCounter: 0,
loading: false loading: false,
} },
] ],
}; };
constructor() { constructor() {
@@ -69,18 +91,18 @@ export default class App extends Component<Props, State> {
onEncoderChange(index: 0 | 1, type: EncoderType, options?: EncoderOptions): void { onEncoderChange(index: 0 | 1, type: EncoderType, options?: EncoderOptions): void {
const images = this.state.images.slice() as [EncodedImage, EncodedImage]; const images = this.state.images.slice() as [EncodedImage, EncodedImage];
const image = images[index]; const oldImage = images[index];
// Some type cheating here. // Some type cheating here.
// encoderMap[type].defaultOptions is always safe. // encoderMap[type].defaultOptions is always safe.
// options should always be correct for the type, but TypeScript isn't smart enough. // options should always be correct for the type, but TypeScript isn't smart enough.
const encoderState: EncoderState = { const encoderState: EncoderState = {
type, type,
options: options ? options : encoderMap[type].defaultOptions options: options ? options : encoderMap[type].defaultOptions,
} as EncoderState; } as EncoderState;
images[index] = { images[index] = {
...image, ...oldImage,
encoderState, encoderState,
}; };
@@ -95,7 +117,9 @@ export default class App extends Component<Props, State> {
const { source, images } = this.state; const { source, images } = this.state;
for (const [i, image] of images.entries()) { for (const [i, image] of images.entries()) {
if (source !== prevState.source || image !== prevState.images[i]) { // The image only needs updated if the encoder settings have changed, or the source has
// changed.
if (source !== prevState.source || image.encoderState !== prevState.images[i].encoderState) {
this.updateImage(i); this.updateImage(i);
} }
} }
@@ -122,6 +146,7 @@ export default class App extends Component<Props, State> {
const bmp = await createImageBitmap(file); const bmp = await createImageBitmap(file);
// compute the corresponding ImageData once since it only changes when the file changes: // compute the corresponding ImageData once since it only changes when the file changes:
const data = await bitmapToImageData(bmp); const data = await bitmapToImageData(bmp);
this.setState({ this.setState({
source: { data, bmp, file }, source: { data, bmp, file },
error: undefined, error: undefined,
@@ -133,57 +158,53 @@ export default class App extends Component<Props, State> {
} }
async updateImage(index: number): Promise<void> { async updateImage(index: number): Promise<void> {
const { source, images } = this.state; const { source } = this.state;
if (!source) return; if (!source) return;
let image = images[index]; let images = this.state.images.slice() as [EncodedImage, EncodedImage];
// Each time we trigger an async encode, the ID changes. // Each time we trigger an async encode, the counter changes.
image.loadingCounter = image.loadingCounter + 1; const loadingCounter = images[index].loadingCounter + 1;
const loadingCounter = image.loadingCounter;
image.loading = true; const image = images[index] = {
this.setState({ }); ...images[index],
loadingCounter,
loading: true,
};
const result = await this.updateCompressedImage(source, image.encoderState); this.setState({ images });
image = this.state.images[index]; let result;
// If a later encode has landed before this one, return.
if (loadingCounter < image.loadedCounter) return;
image.bmp = result;
image.loading = image.loadingCounter !== loadingCounter;
image.loadedCounter = loadingCounter;
this.setState({ });
}
async updateCompressedImage(source: SourceImage, encodeData: EncoderState): Promise<ImageBitmap> {
// Special case for identity
if (encodeData.type === identity.type) return source.bmp;
try { try {
const compressedData = await (() => { result = await compressImage(source, image.encoderState);
switch (encodeData.type) {
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
default: throw Error(`Unexpected encoder name`);
}
})();
const blob = new Blob([compressedData], {
type: encoderMap[encodeData.type].mimeType
});
const bitmap = await createImageBitmap(blob);
this.setState({ error: '' });
return bitmap;
} catch (err) { } catch (err) {
this.setState({ error: `Encoding error (type=${encodeData.type}): ${err}` }); this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err; throw err;
} }
const latestImage = this.state.images[index];
// If a later encode has landed before this one, return.
if (loadingCounter < latestImage.loadedCounter) {
this.setState({ error: '' });
return;
}
images = this.state.images.slice() as [EncodedImage, EncodedImage];
images[index] = {
...images[index],
bmp: result,
loading: image.loadingCounter !== loadingCounter,
loadedCounter: loadingCounter,
};
this.setState({ images, error: '' });
} }
render({ }: Props, { loading, error, images, source }: State) { render({ }: Props, { loading, error, images }: State) {
const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp); const [leftImageBmp, rightImageBmp] = images.map(i => i.bmp);
loading = loading || images.some(image => image.loading); const anyLoading = loading || images.some(image => image.loading);
return ( return (
<file-drop accept="image/*" onfiledrop={this.onFileDrop}> <file-drop accept="image/*" onfiledrop={this.onFileDrop}>
@@ -209,7 +230,7 @@ export default class App extends Component<Props, State> {
onOptionsChange={this.onOptionsChange.bind(this, index)} onOptionsChange={this.onOptionsChange.bind(this, index)}
/> />
))} ))}
{loading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>} {anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}
{error && <span style={{ position: 'fixed', top: 0, left: 0 }}>Error: {error}</span>} {error && <span style={{ position: 'fixed', top: 0, left: 0 }}>Error: {error}</span>}
</div> </div>
</file-drop> </file-drop>

View File

@@ -8,7 +8,7 @@ import { twoUpHandle } from './custom-els/TwoUp/styles.css';
type Props = { type Props = {
leftImg: ImageBitmap, leftImg: ImageBitmap,
rightImg: ImageBitmap rightImg: ImageBitmap,
}; };
type State = {}; type State = {};
@@ -39,13 +39,17 @@ export default class Output extends Component<Props, State> {
} }
} }
shouldComponentUpdate(nextProps: Props) {
return this.props.leftImg !== nextProps.leftImg || this.props.rightImg !== nextProps.rightImg;
}
@bind @bind
onPinchZoomLeftChange(event: Event) { onPinchZoomLeftChange(event: Event) {
if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
this.pinchZoomRight.setTransform({ this.pinchZoomRight.setTransform({
scale: this.pinchZoomLeft.scale, scale: this.pinchZoomLeft.scale,
x: this.pinchZoomLeft.x, x: this.pinchZoomLeft.x,
y: this.pinchZoomLeft.y y: this.pinchZoomLeft.y,
}); });
} }

View File

@@ -44,9 +44,9 @@ export async function bitmapToImageData(bitmap: ImageBitmap): Promise<ImageData>
} }
/** Replace the contents of a canvas with the given bitmap */ /** Replace the contents of a canvas with the given bitmap */
export function drawBitmapToCanvas(canvas: HTMLCanvasElement, img: ImageBitmap) { export function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitmap) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) throw Error('Canvas not initialized'); if (!ctx) throw Error('Canvas not initialized');
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0); ctx.drawImage(bitmap, 0, 0);
} }