From a52ad3844707ec9dfcf512108254e4a97ea89ae1 Mon Sep 17 00:00:00 2001 From: Surma <surma@surma.link> Date: Mon, 25 Feb 2019 19:31:32 +0000 Subject: [PATCH] Add position reset button and update zoom interaction Fix set-rotation and HMR issues Fix offliner.ts dev env issue. #342 Reset position only if image gets totally lost from the viewport Migrate service worker existenceny check before its register call --- package.json | 1 + src/components/Output/index.tsx | 127 +++++++++++++++++++++++--------- src/lib/offliner.ts | 2 + src/missing-types.d.ts | 2 + 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 535b09be5..bd6fae5fd 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "gzip-size": "5.0.0", "html-webpack-plugin": "3.2.0", "husky": "1.3.1", + "intersection-observer": "^0.5.1", "idb-keyval": "3.1.0", "linkstate": "1.1.1", "loader-utils": "1.2.3", diff --git a/src/components/Output/index.tsx b/src/components/Output/index.tsx index 36ba4120f..1f0a8428c 100644 --- a/src/components/Output/index.tsx +++ b/src/components/Output/index.tsx @@ -34,6 +34,12 @@ interface State { scale: number; editingScale: boolean; altBackground: boolean; + isIntersecting: boolean; +} + +interface IntersectionObserverEntry { + readonly intersectionRatio: number; + readonly isIntersecting: boolean; } const scaleToOpts: ScaleToOpts = { @@ -48,33 +54,29 @@ export default class Output extends Component<Props, State> { scale: 1, editingScale: false, altBackground: false, + isIntersecting: true, }; canvasLeft?: HTMLCanvasElement; canvasRight?: HTMLCanvasElement; pinchZoomLeft?: PinchZoom; pinchZoomRight?: PinchZoom; scaleInput?: HTMLInputElement; + threshold: number = 0; retargetedEvents = new WeakSet<Event>(); componentDidMount() { const leftDraw = this.leftDrawable(); const rightDraw = this.rightDrawable(); - // Reset the pinch zoom, which may have an position set from the previous view, after pressing - // the back button. - this.pinchZoomLeft!.setTransform({ - allowChangeEvent: true, - x: 0, - y: 0, - scale: 1, - }); - if (this.canvasLeft && leftDraw) { drawDataToCanvas(this.canvasLeft, leftDraw); } if (this.canvasRight && rightDraw) { drawDataToCanvas(this.canvasRight, rightDraw); } + + this.initializeImage(); + this.observeIntersection(); } componentDidUpdate(prevProps: Props, prevState: State) { @@ -127,6 +129,43 @@ export default class Output extends Component<Props, State> { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } + @bind + private async initializeImage() { + await this.setRotation(true)(); + this.setZoom(1)(); + this.resetPosition(); + this.setState({ altBackground: false }); + } + + @bind + private handleIntersect(entries: IntersectionObserverEntry[]) { + entries.forEach((entry: IntersectionObserverEntry) => { + // Value of isIntersecting is depended on threshold value on chrome. + // However, for safari, firefox and polyfill we just need to check also intersectionRatio. + // Realized different behavior: https://github.com/w3c/IntersectionObserver/issues/345 + const isIntersecting = entry.isIntersecting && entry.intersectionRatio > this.threshold; + this.setState({ isIntersecting }); + }); + } + + @bind + private async observeIntersection() { + if (!('intersectionObserver' in window)) { + await import('intersection-observer'); + } + + if (!this.pinchZoomLeft || !this.canvasLeft) return; + + const options = { + root: this.pinchZoomLeft, + rootMargin: '0px', + threshold: this.threshold, + }; + const observer = new IntersectionObserver(this.handleIntersect, options); + + observer.observe(this.canvasLeft); + } + private leftDrawable(props: Props = this.props): ImageData | undefined { return props.leftCompressed || (props.source && props.source.processed); } @@ -135,39 +174,56 @@ export default class Output extends Component<Props, State> { return props.rightCompressed || (props.source && props.source.processed); } + // initial coordinates depends on the current scale and dimensions of the image. @bind - private toggleBackground() { - this.setState({ - altBackground: !this.state.altBackground, - }); + private resetPosition(scaleRatio: number = this.state.scale) { + if (this.canvasLeft) { + const { width, height } = this.canvasLeft; + + this.pinchZoomLeft!.setTransform({ + allowChangeEvent: true, + x: (width / 2) * (1 - scaleRatio), + y: (height / 2) * (1 - scaleRatio), + scale: scaleRatio, + }); + } } - @bind - private zoomIn() { - if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); + private setZoom(scaleRatio: number = 1) { + return () => { + if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); + + this.pinchZoomLeft.scaleTo(scaleRatio, scaleToOpts); - this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts); + // Now, reset position will be triggered when the image + // has been lost from the viewport with 0.2 threshold. + if (!this.state.isIntersecting) { + this.resetPosition(scaleRatio); + } + }; } @bind - private zoomOut() { - if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element'); - - this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts); + private toggleBackground() { + this.setState({ + altBackground: !this.state.altBackground, + }); } @bind - private onRotateClick() { - const { inputProcessorState } = this.props; - if (!inputProcessorState) return; - - const newState = cleanSet( - inputProcessorState, - 'rotate.rotate', - (inputProcessorState.rotate.rotate + 90) % 360, - ); - - this.props.onInputProcessorChange(newState); + private setRotation(resetRotation?: boolean) { + return async() => { + const { inputProcessorState } = this.props; + if (!inputProcessorState) return; + + const newState = cleanSet( + inputProcessorState, + 'rotate.rotate', + resetRotation ? 0 : (inputProcessorState.rotate.rotate + 90) % 360, + ); + + return this.props.onInputProcessorChange(newState); + }; } @bind @@ -311,7 +367,7 @@ export default class Output extends Component<Props, State> { <div class={style.controls}> <div class={style.zoomControls}> - <button class={style.button} onClick={this.zoomOut}> + <button class={style.button} onClick={this.setZoom(this.state.scale / 1.25)}> <RemoveIcon /> </button> {editingScale ? ( @@ -325,6 +381,7 @@ export default class Output extends Component<Props, State> { value={Math.round(scale * 100)} onInput={this.onScaleInputChanged} onBlur={this.onScaleInputBlur} + onDblClick={this.initializeImage} /> ) : ( <span class={style.zoom} tabIndex={0} onFocus={this.onScaleValueFocus}> @@ -332,12 +389,12 @@ export default class Output extends Component<Props, State> { % </span> )} - <button class={style.button} onClick={this.zoomIn}> + <button class={style.button} onClick={this.setZoom(this.state.scale * 1.25)}> <AddIcon /> </button> </div> <div class={style.buttonsNoWrap}> - <button class={style.button} onClick={this.onRotateClick} title="Rotate image"> + <button class={style.button} onClick={this.setRotation()} title="Rotate image"> <RotateIcon /> </button> <button diff --git a/src/lib/offliner.ts b/src/lib/offliner.ts index 5c6e30c66..e881d320a 100644 --- a/src/lib/offliner.ts +++ b/src/lib/offliner.ts @@ -45,6 +45,8 @@ export async function offliner(showSnack: SnackBarElement['showSnackbar']) { // This needs to be a typeof because Webpack. if (typeof PRERENDER === 'boolean') return; + if (!('serviceWorker' in navigator)) return; + if (process.env.NODE_ENV === 'production') { navigator.serviceWorker.register('../sw'); } diff --git a/src/missing-types.d.ts b/src/missing-types.d.ts index 8d8366ae8..715ff632a 100644 --- a/src/missing-types.d.ts +++ b/src/missing-types.d.ts @@ -2,6 +2,8 @@ interface CanvasRenderingContext2D { filter: string; } +declare module 'intersection-observer'; + // Handling file-loader imports: declare module '*.png' { const content: string;