mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 00:37:19 +00:00
Options UI (#135)
* Move gzipped size calculations into a worker and wrap it up in a `<GzipSize />` component that will also handle showing % of original size once that info is plumbed * A couple tweaks for the app welcome (drop files) screen. We don't have mocks for this one, but this is at least a minor improvement. * Prettier "pop" effect and styling for the drop zone/indicator. * Styling for the quantization toggle to make it look like a disclosure triangle/button. * Add controls bar (zoom in/out/to, background toggle). @todo: extract into its own component. * When clicking/tapping the image area, give it focus. * Utilities used by this PR * Add a `two-up-handle` attribute to the handle for easier styling (classname gets mangled so it doesn't make for a good public API) * Add a dummy comment to test netlify deploy * Remove commented-out code. * Fix styling of vertical split (which as it turns out is slightly different in the mocks anyway) * Use a composited overlay for the dark background instead of animating background-color * Move grayscale styling into `<two-up>` by default, then set colors via custom properties * Remove commented-out svg fill * Remove dummy comment * Change `<GzipSize>` to be `<FileSize>`, add `compress` option that lets us show gzipped sizes later if we need. Defaults to `false`, and the gzip worker is only lazily instantiated the first time a compressed size calculation is requested. * Dependency updates * Remove color animations from dnd overlay * Don't use a cyclical import for EncodedImage, instead just specify the types of the properties we Options actually uses. * Pass source image through to FileSize component so it can compute delta * Stylize size display with colors based on delta amount/direction * Remove box-shadow animation. * Simplify font stack * Remove commented out code * Remove gzip compression from size component * Remove memoization bits * Use specific flattend props instead of passing large context objects around. * Remove unused packages. * Remove unreachable String case in FileSize, and omit redundant File type * Simplify calculateSize() * Fix types for FileSize! * Remove FileSize title * Make delta variable consistent. * Skip passing compareTo value for original image * Remove manual focus * Fix whitespace * remove unused keyframes * remove pointless flex-wrap property * Remove unused resetZoom() method * Remove pointless flex properties * Use `on` prefix for event handling * Remove pointless justify-self property * Use an inline SVG for TwoUp's handle icon so it can be colored from outside the component.. * Move orientation state up from `<Output>` into `<App>` and share it with `<Options>`. * Make the options panels responsive :) * Show a plus sign for size increases `(+8%)` * Use inline SVG for the zoom +/- icons, collect SVG icons into one file now that I've verified they get tree-shaken properly. * Fix top/bottom options panels being reversed * remove commented out code * lockfile * Revert quanitzation toggle styles so it's just a checkbox. * Remove minimum delta for compare size * Rename data prop to file. * scale int -> float * remove tabIndex * Remove old icon files * Add width to options panels * Add vertical scrolling when options are taller than 80% of the screen height.
This commit is contained in:
committed by
Jake Archibald
parent
54ad30a7ed
commit
32f6f8b941
@@ -3,34 +3,35 @@ import PinchZoom from './custom-els/PinchZoom';
|
||||
import './custom-els/PinchZoom';
|
||||
import './custom-els/TwoUp';
|
||||
import * as style from './style.scss';
|
||||
import { bind, drawBitmapToCanvas, linkRef } from '../../lib/util';
|
||||
import { bind, shallowEqual, drawBitmapToCanvas, linkRef } from '../../lib/util';
|
||||
import { ToggleIcon, AddIcon, RemoveIcon } from '../../lib/icons';
|
||||
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
||||
|
||||
interface Props {
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
leftImg: ImageBitmap;
|
||||
rightImg: ImageBitmap;
|
||||
}
|
||||
|
||||
interface State {
|
||||
verticalTwoUp: boolean;
|
||||
scale: number;
|
||||
editingScale: boolean;
|
||||
altBackground: boolean;
|
||||
}
|
||||
|
||||
export default class Output extends Component<Props, State> {
|
||||
widthQuery = window.matchMedia('(min-width: 500px)');
|
||||
state: State = {
|
||||
verticalTwoUp: !this.widthQuery.matches,
|
||||
scale: 1,
|
||||
editingScale: false,
|
||||
altBackground: false,
|
||||
};
|
||||
canvasLeft?: HTMLCanvasElement;
|
||||
canvasRight?: HTMLCanvasElement;
|
||||
pinchZoomLeft?: PinchZoom;
|
||||
pinchZoomRight?: PinchZoom;
|
||||
scaleInput?: HTMLInputElement;
|
||||
retargetedEvents = new WeakSet<Event>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.widthQuery.addListener(this.onMobileWidthChange);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.canvasLeft) {
|
||||
drawBitmapToCanvas(this.canvasLeft, this.props.leftImg);
|
||||
@@ -40,29 +41,76 @@ export default class Output extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (prevProps.leftImg !== this.props.leftImg && this.canvasLeft) {
|
||||
drawBitmapToCanvas(this.canvasLeft, this.props.leftImg);
|
||||
}
|
||||
if (prevProps.rightImg !== this.props.rightImg && this.canvasRight) {
|
||||
drawBitmapToCanvas(this.canvasRight, this.props.rightImg);
|
||||
}
|
||||
|
||||
const { scale } = this.state;
|
||||
if (scale !== prevState.scale && this.pinchZoomLeft && this.pinchZoomRight) {
|
||||
// @TODO it would be nice if PinchZoom exposed a variant of setTransform() that
|
||||
// preserved translation. It currently only does this for mouse wheel events.
|
||||
this.pinchZoomLeft.setTransform({ scale });
|
||||
this.pinchZoomRight.setTransform({ scale });
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
||||
return this.props.leftImg !== nextProps.leftImg ||
|
||||
this.props.rightImg !== nextProps.rightImg ||
|
||||
this.state.verticalTwoUp !== nextState.verticalTwoUp;
|
||||
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
|
||||
}
|
||||
|
||||
@bind
|
||||
onMobileWidthChange() {
|
||||
this.setState({ verticalTwoUp: !this.widthQuery.matches });
|
||||
toggleBackground() {
|
||||
this.setState({
|
||||
altBackground: !this.state.altBackground,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
zoomIn() {
|
||||
this.setState({
|
||||
scale: Math.min(this.state.scale * 1.25, 100),
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
zoomOut() {
|
||||
this.setState({
|
||||
scale: Math.max(this.state.scale / 1.25, 0.0001),
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
editScale() {
|
||||
this.setState({ editingScale: true }, () => {
|
||||
if (this.scaleInput) this.scaleInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
cancelEditScale() {
|
||||
this.setState({ editingScale: false });
|
||||
}
|
||||
|
||||
@bind
|
||||
onScaleInputChanged(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const percent = parseFloat(target.value);
|
||||
if (isNaN(percent)) return;
|
||||
this.setState({
|
||||
scale: percent / 100,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
onPinchZoomLeftChange(event: Event) {
|
||||
if (!this.pinchZoomRight || !this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
this.setState({
|
||||
scale: this.pinchZoomLeft.scale,
|
||||
});
|
||||
this.pinchZoomRight.setTransform({
|
||||
scale: this.pinchZoomLeft.scale,
|
||||
x: this.pinchZoomLeft.x,
|
||||
@@ -97,11 +145,14 @@ export default class Output extends Component<Props, State> {
|
||||
this.pinchZoomLeft.dispatchEvent(clonedEvent);
|
||||
}
|
||||
|
||||
render({ leftImg, rightImg }: Props, { verticalTwoUp }: State) {
|
||||
render(
|
||||
{ orientation, leftImg, rightImg }: Props,
|
||||
{ scale, editingScale, altBackground }: State,
|
||||
) {
|
||||
return (
|
||||
<div class={style.output}>
|
||||
<div class={`${style.output} ${altBackground ? style.altBackground : ''}`}>
|
||||
<two-up
|
||||
orientation={verticalTwoUp ? 'vertical' : 'horizontal'}
|
||||
orientation={orientation}
|
||||
// Event redirecting. See onRetargetableEvent.
|
||||
onTouchStartCapture={this.onRetargetableEvent}
|
||||
onTouchEndCapture={this.onRetargetableEvent}
|
||||
@@ -110,7 +161,10 @@ export default class Output extends Component<Props, State> {
|
||||
onMouseDownCapture={this.onRetargetableEvent}
|
||||
onWheelCapture={this.onRetargetableEvent}
|
||||
>
|
||||
<pinch-zoom onChange={this.onPinchZoomLeftChange} ref={linkRef(this, 'pinchZoomLeft')}>
|
||||
<pinch-zoom
|
||||
onChange={this.onPinchZoomLeftChange}
|
||||
ref={linkRef(this, 'pinchZoomLeft')}
|
||||
>
|
||||
<canvas
|
||||
class={style.outputCanvas}
|
||||
ref={linkRef(this, 'canvasLeft')}
|
||||
@@ -127,6 +181,39 @@ export default class Output extends Component<Props, State> {
|
||||
/>
|
||||
</pinch-zoom>
|
||||
</two-up>
|
||||
|
||||
<div class={style.controls}>
|
||||
<div class={style.group}>
|
||||
<button class={style.button} onClick={this.zoomOut}>
|
||||
<RemoveIcon />
|
||||
</button>
|
||||
{editingScale ? (
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
max="1000000"
|
||||
ref={linkRef(this, 'scaleInput')}
|
||||
class={style.zoom}
|
||||
value={Math.round(scale * 100)}
|
||||
onInput={this.onScaleInputChanged}
|
||||
onBlur={this.cancelEditScale}
|
||||
/>
|
||||
) : (
|
||||
<span class={style.zoom} tabIndex={0} onFocus={this.editScale}>
|
||||
<strong>{Math.round(scale * 100)}</strong>
|
||||
%
|
||||
</span>
|
||||
)}
|
||||
<button class={style.button} onClick={this.zoomIn}>
|
||||
<AddIcon />
|
||||
</button>
|
||||
</div>
|
||||
<button class={style.button} onClick={this.toggleBackground}>
|
||||
<ToggleIcon />
|
||||
Toggle Background
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user