Skip to content

Commit

Permalink
Add position reset button and update zoom interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
surma committed Feb 25, 2019
1 parent 36ed21b commit 9c81312
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 35 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
132 changes: 97 additions & 35 deletions src/components/Output/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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.5;
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) {
Expand Down Expand Up @@ -127,6 +129,44 @@ export default class Output extends Component<Props, State> {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}

@bind
private async initializeImage() {
await this.setRotation(360)();
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 > (1 - this.threshold);

this.setState({ isIntersecting });
});
}

@bind
private async observeIntersection() {
if (!this.pinchZoomLeft || !this.canvasLeft) return;

if (!('intersectionObserver' in window)) {
await import('intersection-observer');
}

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);
}
Expand All @@ -135,39 +175,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.5 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(initialRotation?: number) {
return async() => {
const { inputProcessorState } = this.props;
if (!inputProcessorState) return;

const newState = cleanSet(
inputProcessorState,
'rotate.rotate',
initialRotation || (inputProcessorState.rotate.rotate + 90) % 360,
);

return this.props.onInputProcessorChange(newState);
};
}

@bind
Expand Down Expand Up @@ -227,6 +284,10 @@ export default class Output extends Component<Props, State> {
if (event.type !== 'wheel' && targetEl.closest(`.${twoUpHandle}`)) return;
// If we've already retargeted this event, let it through.
if (this.retargetedEvents.has(event)) return;
// reset the zoom and position of the image if it's a double click event.
if (event.type === 'dblclick') {
this.initializeImage();
}
// Stop the event in its tracks.
event.stopImmediatePropagation();
event.preventDefault();
Expand Down Expand Up @@ -311,7 +372,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 ? (
Expand All @@ -325,19 +386,20 @@ 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}>
<span class={style.zoomValue}>{Math.round(scale * 100)}</span>
%
</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
Expand Down
2 changes: 2 additions & 0 deletions src/missing-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ interface CanvasRenderingContext2D {
filter: string;
}

declare module 'intersection-observer';

// Handling file-loader imports:
declare module '*.png' {
const content: string;
Expand Down

0 comments on commit 9c81312

Please sign in to comment.