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

Add position reset button and update zoom interaction. (#292) (#345) #372

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
129 changes: 94 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,31 @@ 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);
}

// Reset the pinch zoom, which may have an position set from the previous view, after pressing
// the back button.
this.resetPosition();
this.observeIntersection();
}

componentDidUpdate(prevProps: Props, prevState: State) {
Expand Down Expand Up @@ -127,6 +131,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);
surma marked this conversation as resolved.
Show resolved Hide resolved

observer.observe(this.canvasLeft);
}

private leftDrawable(props: Props = this.props): ImageData | undefined {
return props.leftCompressed || (props.source && props.source.processed);
}
Expand All @@ -135,39 +176,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(this.state.scale * 1.25, scaleToOpts);
this.pinchZoomLeft.scaleTo(scaleRatio, 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
Expand Down Expand Up @@ -311,7 +369,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 +383,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
3 changes: 2 additions & 1 deletion src/components/compress/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ export default class Compress extends Component<Props, State> {

const oldRotate = source.inputProcessorState.rotate.rotate;
const newRotate = options.rotate.rotate;
const orientationChanged = oldRotate % 180 !== newRotate % 180;

const orientationChanged = (newRotate === 0) || oldRotate % 180 !== newRotate % 180;
const loadingCounter = this.state.loadingCounter + 1;
// Either processor is good enough here.
const processor = this.leftProcessor;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/offliner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
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