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:
Jason Miller
2018-09-05 03:21:54 -04:00
committed by Jake Archibald
parent 54ad30a7ed
commit 32f6f8b941
15 changed files with 766 additions and 1757 deletions

View File

@@ -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>
);
}