Compare commits

..

1 Commits

Author SHA1 Message Date
f877dcaff9 Caught a bit of repetition in our utils 2018-11-05 08:04:47 +00:00
12 changed files with 193 additions and 204 deletions

View File

@ -74,7 +74,7 @@ export default class App extends Component<Props, State> {
render({}: Props, { file, Compress }: State) {
return (
<div id="app" class={style.app}>
<file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}>
<file-drop accept="image/*" onfiledrop={this.onFileDrop}>
{(!file)
? <Intro onFile={this.onIntroPickFile} onError={this.showError} />
: (Compress)

View File

@ -12,14 +12,14 @@ Note: These styles are temporary. They will be replaced before going live.
contain: strict;
}
.drop {
overflow: hidden;
touch-action: none;
height: 100%;
width: 100%;
:global {
file-drop {
overflow: hidden;
touch-action: none;
height:100%;
width:100%;
&:global {
&::after {
&:after {
content: '';
position: absolute;
display: block;
@ -28,20 +28,28 @@ Note: These styles are temporary. They will be replaced before going live.
right: 10px;
bottom: 10px;
border: 2px dashed #fff;
background-color:rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5);
border-radius: 10px;
opacity: 0;
transform: scale(0.95);
transition: all 200ms ease-in;
transition-property: transform, opacity;
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1), background-color 300ms step-end, border-color 300ms step-end;
pointer-events: none;
}
&.drop-valid::after {
&.drop-valid:after,
&.drop-invalid:after {
opacity: 1;
transform: scale(1);
transition-timing-function: ease-out;
transition: opacity 300ms ease, transform 300ms cubic-bezier(.6,2,.6,1);
}
&.drop-valid:after {
background-color:rgba(88, 116, 88, 0.2);
border-color: rgba(65, 129, 65, 0.5);
}
&.drop-invalid:after {
background-color:rgba(119, 85, 85, 0.2);
border-color:rgba(129, 63, 63, 0.5);
}
}
}

View File

@ -145,7 +145,7 @@ export default class PinchZoom extends HTMLElement {
const relativeToEl = (relativeTo === 'content' ? this._positioningEl : this);
// No content element? Fall back to just setting scale
if (!relativeToEl || !this._positioningEl) {
if (!relativeToEl) {
this.setTransform({ scale, allowChangeEvent });
return;
}
@ -157,10 +157,6 @@ export default class PinchZoom extends HTMLElement {
if (relativeTo === 'content') {
originX += this.x;
originY += this.y;
} else {
const currentRect = this._positioningEl.getBoundingClientRect();
originX -= currentRect.left;
originY -= currentRect.top;
}
this._applyChange({

View File

@ -68,14 +68,12 @@ export default class TwoUp extends HTMLElement {
}
connectedCallback() {
this._childrenChange();
this._handle.innerHTML = `<div class="${styles.scrubber}">${
'<svg viewBox="0 0 20 10" fill="currentColor"><path d="M8 0v10L0 5zM12 0v10l8-5z"/></svg>'
}</div>`;
this._childrenChange();
if (!this._everConnected) {
this._handle.innerHTML = `<div class="${styles.scrubber}">${
`<svg viewBox="0 0 27 20" fill="currentColor">${
'<path d="M17 19.2l9.5-9.6L16.9 0zM9.6 0L0 9.6l9.6 9.6z"/>'
}</svg>`
}</div>`;
this._resetPosition();
this._everConnected = true;
}

View File

@ -6,41 +6,29 @@ two-up {
--track-color: var(--accent-color);
--thumb-background: #fff;
--thumb-color: var(--accent-color);
--thumb-size: 62px;
--bar-size: 6px;
--bar-touch-size: 30px;
}
two-up > * {
/* Overlay all children on top of each other, and let two-up's layout contain all of them. */
/* Overlay all children on top of each other, and let
two-up's layout contain all of them. */
grid-area: 1/1;
}
two-up[legacy-clip-compat] > :not(.two-up-handle) {
/* Legacy mode uses clip rather than clip-path (Edge doesn't support clip-path), but clip requires
elements to be positioned absolutely */
two-up[legacy-clip-compat] > :not(.twoUpHandle) {
position: absolute;
}
.two-up-handle {
.twoUpHandle {
touch-action: none;
position: relative;
width: var(--bar-touch-size);
width: 10px;
background: var(--track-color);
transform: translateX(var(--split-point)) translateX(-50%);
box-shadow: inset 4px 0 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
will-change: transform;
cursor: ew-resize;
}
.two-up-handle::before {
content: '';
display: block;
height: 100%;
width: var(--bar-size);
margin: 0 auto;
box-shadow: inset calc(var(--bar-size) / 2) 0 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
background: var(--track-color);
}
.scrubber {
display: flex;
position: absolute;
@ -48,56 +36,51 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
left: 50%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%);
width: var(--thumb-size);
height: calc(var(--thumb-size) * 0.9);
width: 62px;
height: 56px;
background: var(--thumb-background);
border: 1px solid rgba(0,0,0,0.2);
border-radius: calc(var(--thumb-size) * 0.08);
border-radius: 5px;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
color: var(--thumb-color);
box-sizing: border-box;
padding: 0 48%;
}
.scrubber svg {
flex: 1;
margin: 0 10px;
}
two-up[orientation='vertical'] .two-up-handle {
two-up[orientation='vertical'] .twoUpHandle {
width: auto;
height: var(--bar-touch-size);
height: 7px;
transform: translateY(var(--split-point)) translateY(-50%);
box-shadow: inset 0 3px 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
cursor: ns-resize;
}
two-up[orientation='vertical'] .two-up-handle::before {
width: auto;
height: var(--bar-size);
box-shadow: inset 0 calc(var(--bar-size) / 2) 0 rgba(0,0,0,0.1), 0 1px 4px rgba(0,0,0,0.4);
margin: calc((var(--bar-touch-size) - var(--bar-size)) / 2) 0 0 0;
}
two-up[orientation='vertical'] .scrubber {
width: 46px;
height: 40px;
font-size: 18px;
box-shadow: 1px 0 4px rgba(0,0,0,0.1);
transform: translate(-50%, -50%) rotate(-90deg);
}
two-up > :nth-child(1):not(.two-up-handle) {
two-up > :nth-child(1):not(.twoUpHandle) {
-webkit-clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
clip-path: inset(0 calc(100% - var(--split-point)) 0 0);
}
two-up > :nth-child(2):not(.two-up-handle) {
two-up > :nth-child(2):not(.twoUpHandle) {
-webkit-clip-path: inset(0 0 0 var(--split-point));
clip-path: inset(0 0 0 var(--split-point));
}
two-up[orientation='vertical'] > :nth-child(1):not(.two-up-handle) {
two-up[orientation='vertical'] > :nth-child(1):not(.twoUpHandle) {
-webkit-clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
clip-path: inset(0 0 calc(100% - var(--split-point)) 0);
}
two-up[orientation='vertical'] > :nth-child(2):not(.two-up-handle) {
two-up[orientation='vertical'] > :nth-child(2):not(.twoUpHandle) {
-webkit-clip-path: inset(var(--split-point) 0 0 0);
clip-path: inset(var(--split-point) 0 0 0);
}
@ -107,19 +90,19 @@ two-up[orientation='vertical'] > :nth-child(2):not(.two-up-handle) {
It performs way better in Safari.
*/
@supports not ((clip-path: inset(0 0 0 0)) or (-webkit-clip-path: inset(0 0 0 0))) {
two-up[legacy-clip-compat] > :nth-child(1):not(.two-up-handle) {
two-up[legacy-clip-compat] > :nth-child(1):not(.twoUpHandle) {
clip: rect(auto var(--split-point) auto auto);
}
two-up[legacy-clip-compat] > :nth-child(2):not(.two-up-handle) {
two-up[legacy-clip-compat] > :nth-child(2):not(.twoUpHandle) {
clip: rect(auto auto auto var(--split-point));
}
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(1):not(.two-up-handle) {
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(1):not(.twoUpHandle) {
clip: rect(auto auto var(--split-point) auto);
}
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(2):not(.two-up-handle) {
two-up[orientation='vertical'][legacy-clip-compat] > :nth-child(2):not(.twoUpHandle) {
clip: rect(var(--split-point) auto auto auto);
}
}

View File

@ -113,19 +113,14 @@ export default class Output extends Component<Props, State> {
}
@bind
private onScaleValueFocus() {
private editScale() {
this.setState({ editingScale: true }, () => {
if (this.scaleInput) {
// Firefox unfocuses the input straight away unless I force a style calculation here. I have
// no idea why, but it's late and I'm quite tired.
getComputedStyle(this.scaleInput).transform;
this.scaleInput.focus();
}
if (this.scaleInput) this.scaleInput.focus();
});
}
@bind
private onScaleInputBlur() {
private cancelEditScale() {
this.setState({ editingScale: false });
}
@ -193,7 +188,6 @@ export default class Output extends Component<Props, State> {
<div class={`${style.output} ${altBackground ? style.altBackground : ''}`}>
<two-up
legacy-clip-compat
class={style.twoUp}
orientation={orientation}
// Event redirecting. See onRetargetableEvent.
onTouchStartCapture={this.onRetargetableEvent}
@ -204,7 +198,6 @@ export default class Output extends Component<Props, State> {
onWheelCapture={this.onRetargetableEvent}
>
<pinch-zoom
class={style.pinchZoom}
onChange={this.onPinchZoomLeftChange}
ref={linkRef(this, 'pinchZoomLeft')}
>
@ -220,7 +213,7 @@ export default class Output extends Component<Props, State> {
}}
/>
</pinch-zoom>
<pinch-zoom class={style.pinchZoom} ref={linkRef(this, 'pinchZoomRight')}>
<pinch-zoom ref={linkRef(this, 'pinchZoomRight')}>
<canvas
class={style.outputCanvas}
ref={linkRef(this, 'canvasRight')}
@ -236,7 +229,7 @@ export default class Output extends Component<Props, State> {
</two-up>
<div class={style.controls}>
<div class={style.zoomControls}>
<div class={style.group}>
<button class={style.button} onClick={this.zoomOut}>
<RemoveIcon />
</button>
@ -250,11 +243,11 @@ export default class Output extends Component<Props, State> {
class={style.zoom}
value={Math.round(scale * 100)}
onInput={this.onScaleInputChanged}
onBlur={this.onScaleInputBlur}
onBlur={this.cancelEditScale}
/>
) : (
<span class={style.zoom} tabIndex={0} onFocus={this.onScaleValueFocus}>
<span class={style.zoomValue}>{Math.round(scale * 100)}</span>
<span class={style.zoom} tabIndex={0} onFocus={this.editScale}>
<strong>{Math.round(scale * 100)}</strong>
%
</span>
)}

View File

@ -2,68 +2,134 @@
Note: These styles are temporary. They will be replaced before going live.
*/
.output {
composes: abs-fill from '../../lib/util.scss';
%fill {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
contain: strict;
}
&::before {
.output {
@extend %fill;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
@extend %fill;
background: #000;
opacity: 0;
transition: opacity 500ms ease;
}
&.alt-background::before {
opacity: 0.6;
&.altBackground:before {
opacity: .6;
}
}
.two-up {
composes: abs-fill from '../../lib/util.scss';
--accent-color: var(--button-fg);
}
> two-up {
@extend %fill;
--accent-color: var(--button-fg);
.pinch-zoom {
composes: abs-fill from '../../lib/util.scss';
outline: none;
display: flex;
justify-content: center;
align-items: center;
> pinch-zoom {
@extend %fill;
outline: none;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.controls {
position: absolute;
display: flex;
justify-content: center;
top: 0;
left: 0;
right: 0;
left: 220px;
right: 220px;
bottom: 0;
padding: 9px;
overflow: hidden;
flex-wrap: wrap;
contain: content;
@media (min-width: 680px) {
top: auto;
left: 220px;
right: 220px;
bottom: 0;
@media (max-width: 680px) {
top: 0;
bottom: auto;
left: 0;
right: 0;
}
}
.zoom-controls {
display: flex;
> * {
z-index: 2;
}
& :not(:first-child) {
.group {
display: flex;
}
.button,
.zoom {
display: flex;
align-items: center;
flex: 0;
box-sizing: border-box;
height: 48px;
padding: 0 16px;
margin: 4px;
background-color: #fff;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 5px;
line-height: 1;
font-size: 110%;
white-space: nowrap;
&:focus {
box-shadow: 0 0 0 2px var(--button-fg);
outline: none;
z-index: 1;
}
}
.button {
text-transform: uppercase;
color: var(--button-fg);
cursor: pointer;
text-indent: 6px;
}
.button:hover {
background-color: #eee;
}
.zoom {
flex: 0 0 6em;
color: #625E80;
font: inherit;
cursor: text;
width: 6em;
text-align: center;
justify-content: center;
&:focus {
box-shadow: inset 0 1px 4px rgba(0,0,0,0.2), 0 0 0 2px var(--button-fg);
}
strong {
position: relative;
top: 1px;
margin: 0 3px 0 0;
color: #888;
font-weight: normal;
border-bottom: 1px dashed #999;
}
}
.group > :not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-left: 0;
}
& :not(:last-child) {
.group > :not(:last-child) {
margin-right: 0;
border-right-width: 0;
border-top-right-radius: 0;
@ -71,60 +137,6 @@ Note: These styles are temporary. They will be replaced before going live.
}
}
.button,
.zoom {
display: flex;
align-items: center;
box-sizing: border-box;
height: 48px;
padding: 0 16px;
margin: 4px;
background-color: #fff;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 5px;
line-height: 1;
white-space: nowrap;
&:focus {
box-shadow: 0 0 0 2px var(--button-fg);
outline: none;
z-index: 1;
}
}
.button {
text-transform: uppercase;
color: var(--button-fg);
cursor: pointer;
text-indent: 6px;
font-size: 110%;
&:hover {
background-color: #eee;
}
}
.zoom {
color: #625E80;
cursor: text;
width: 6em;
font: inherit;
text-align: center;
justify-content: center;
&:focus {
box-shadow: inset 0 1px 4px rgba(0,0,0,0.2), 0 0 0 2px var(--button-fg);
}
}
.zoom-value {
position: relative;
top: 1px;
margin: 0 3px 0 0;
color: #888;
border-bottom: 1px dashed #999;
}
.output-canvas {
flex-shrink: 0;
}

View File

@ -4,6 +4,7 @@
.option-pair {
display: flex;
justify-content: flex-end;
width: 100%;
height: 100%;
@ -14,6 +15,5 @@
&.vertical {
flex-direction: column;
justify-content: flex-end;
}
}

View File

@ -16,7 +16,6 @@ if (process.env.NODE_ENV === 'development') {
// When an update to any module is received, re-import the app and trigger a full re-render:
module.hot.accept('./components/App', () => {
// The linter doesn't like the capital A in App. It is wrong.
// tslint:disable-next-line variable-name
import('./components/App').then(({ default: App }) => {
root = render(<App />, document.body, root);

View File

@ -2,29 +2,31 @@ import { h } from 'preact';
// tslint:disable:max-line-length variable-name
const Icon = (props: JSX.HTMLAttributes) => (
export interface IconProps extends JSX.HTMLAttributes {}
const Icon = (props: IconProps) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" {...props} />
);
export const DownloadIcon = (props: JSX.HTMLAttributes) => (
export const DownloadIcon = (props: IconProps) => (
<Icon {...props}>
<path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z" />
</Icon>
);
export const ToggleIcon = (props: JSX.HTMLAttributes) => (
export const ToggleIcon = (props: IconProps) => (
<Icon {...props}>
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.89 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9c-1.11 0-2 .9-2 2v10c0 1.1.89 2 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
</Icon>
);
export const AddIcon = (props: JSX.HTMLAttributes) => (
export const AddIcon = (props: IconProps) => (
<Icon {...props}>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</Icon>
);
export const RemoveIcon = (props: JSX.HTMLAttributes) => (
export const RemoveIcon = (props: IconProps) => (
<Icon {...props}>
<path d="M19 13H5v-2h14v2z"/>
</Icon>

View File

@ -5,7 +5,6 @@
width: 100%;
height: 100%;
box-sizing: border-box;
contain: strict;
}
.unbutton {

View File

@ -57,16 +57,32 @@ export async function canvasEncode(data: ImageData, type: string, quality?: numb
return blob;
}
async function decodeImage(url: string): Promise<HTMLImageElement> {
const img = new Image();
img.decoding = 'async';
img.src = url;
const loaded = new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(Error('Image loading error'));
});
if (img.decode) {
// Nice off-thread way supported in Safari/Chrome.
// Safari throws on decode if the source is SVG.
// https://bugs.webkit.org/show_bug.cgi?id=188347
await img.decode().catch(() => null);
}
// Always await loaded, as we may have bailed due to the Safari bug above.
await loaded;
return img;
}
/**
* Attempts to load the given URL as an image.
*/
export function canDecodeImage(data: string): Promise<boolean> {
return new Promise((resolve) => {
const img = document.createElement('img');
img.src = data;
img.onload = _ => resolve(true);
img.onerror = _ => resolve(false);
});
export function canDecodeImage(url: string): Promise<boolean> {
return decodeImage(url).then(() => true, () => false);
}
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
@ -108,24 +124,7 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
const url = URL.createObjectURL(blob);
try {
const img = new Image();
img.decoding = 'async';
img.src = url;
const loaded = new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(Error('Image loading error'));
});
if (img.decode) {
// Nice off-thread way supported in Safari/Chrome.
// Safari throws on decode if the source is SVG.
// https://bugs.webkit.org/show_bug.cgi?id=188347
await img.decode().catch(() => null);
}
// Always await loaded, as we may have bailed due to the Safari bug above.
await loaded;
return img;
return await decodeImage(url);
} finally {
URL.revokeObjectURL(url);
}