(Almost the) rest of the redesign (#880)

* Load demo img

* two-up styles

* Back button

* Button size tweak

* Move back btn

* Move options and back button into a single grid

* Simpler max height

* Responsive grid

* Feed index into options

* Option heading themes

* More option styles

* Changing checkbox position

* Theme range input & use transforms

* Range input underline theme

* Checkbox color

* Add toggle

* Reorder

* Arrow revealer

* Round two-up thumb

* Don't bundle CSS urls starting #

* Results in progress

* Fix Safari bugs

* Download blobs

* Loading spinner

* Hook up download button

* Different style for original image

* Mobile design for results

* Remove demo auto-loader

* Remove redundant colors

* Sticky headings
This commit is contained in:
Jake Archibald
2020-12-09 11:47:23 +00:00
committed by GitHub
parent 12889d9d50
commit fec826b106
36 changed files with 903 additions and 497 deletions

View File

@@ -90,7 +90,7 @@ export default function (resolveFileUrl) {
}),
postCSSUrl({
url: ({ relativePath, url }) => {
if (/^(https?|data):/.test(url)) return url;
if (/^((https?|data):|#)/.test(url)) return url;
const parsedPath = parsePath(relativePath);
const source = readFileSync(
resolvePath(dirname(path), relativePath),

6
package-lock.json generated
View File

@@ -6056,12 +6056,6 @@
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true
},
"pretty-bytes": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz",
"integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==",
"dev": true
},
"pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",

View File

@@ -38,7 +38,6 @@
"preact": "^10.5.5",
"preact-render-to-string": "^5.1.11",
"prettier": "^2.1.2",
"pretty-bytes": "^5.4.1",
"rollup": "^2.33.1",
"rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2",

View File

@@ -18,5 +18,5 @@
}
.checked {
fill: #34b9eb;
fill: var(--main-theme-color);
}

View File

@@ -1,4 +1,3 @@
import PointerTracker from 'pointer-tracker';
import * as style from './style.css';
import 'add-css:./style.css';
@@ -28,7 +27,7 @@ function getPrescision(value: string): number {
class RangeInputElement extends HTMLElement {
private _input: HTMLInputElement;
private _valueDisplay?: HTMLDivElement;
private _valueDisplay?: HTMLSpanElement;
private _ignoreChange = false;
static get observedAttributes() {
@@ -41,15 +40,22 @@ class RangeInputElement extends HTMLElement {
this._input.type = 'range';
this._input.className = style.input;
const tracker = new PointerTracker(this._input, {
start: (): boolean => {
if (tracker.currentPointers.length !== 0) return false;
let activePointer: number | undefined;
// Not using pointer-tracker here due to https://bugs.webkit.org/show_bug.cgi?id=219636.
this.addEventListener('pointerdown', (event) => {
if (activePointer) return;
activePointer = event.pointerId;
this._input.classList.add(style.touchActive);
return true;
},
end: () => {
const pointerUp = (event: PointerEvent) => {
if (event.pointerId !== activePointer) return;
activePointer = undefined;
this._input.classList.remove(style.touchActive);
},
window.removeEventListener('pointerup', pointerUp);
window.removeEventListener('pointercancel', pointerUp);
};
window.addEventListener('pointerup', pointerUp);
window.addEventListener('pointercancel', pointerUp);
});
for (const event of RETARGETED_EVENTS) {
@@ -66,13 +72,13 @@ class RangeInputElement extends HTMLElement {
this.innerHTML =
`<div class="${style.thumbWrapper}">` +
`<div class="${style.thumb}"></div>` +
`<div class="${style.valueDisplay}"></div>` +
`<div class="${style.valueDisplay}"><svg width="32" height="62"><path d="M27.3 27.3C25 29.6 17 35.8 17 43v3c0 3 2.5 5 3.2 5.8a6 6 0 1 1-8.5 0C12.6 51 15 49 15 46v-3c0-7.2-8-13.4-10.3-15.7A16 16 0 0 1 16 0a16 16 0 0 1 11.3 27.3z"/></svg><span></span></div>` +
'</div>';
this.insertBefore(this._input, this.firstChild);
this._valueDisplay = this.querySelector(
'.' + style.valueDisplay,
) as HTMLDivElement;
'.' + style.valueDisplay + ' > span',
) as HTMLSpanElement;
// Set inline styles (this is useful when used with frameworks which might clear inline styles)
this._update();
}

View File

@@ -23,10 +23,8 @@ range-input::before {
width: 100%;
height: 2px;
border-radius: 1px;
box-shadow: 0 -0.5px 0 rgba(0, 0, 0, 0.3),
inset 0 0.5px 0 rgba(255, 255, 255, 0.2), 0 0.5px 0 rgba(255, 255, 255, 0.3);
background: linear-gradient(#34b9eb, #218ab1) 0 / var(--value-percent, 0%)
100% no-repeat #eee;
background: linear-gradient(var(--main-theme-color), var(--main-theme-color))
0 / var(--value-percent, 0%) 100% no-repeat var(--medium-light-gray);
}
.input {
@@ -41,14 +39,12 @@ range-input::before {
pointer-events: none;
position: absolute;
bottom: 3px;
left: var(--value-percent, 0%);
left: 0;
margin-left: -6px;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><circle cx="5" cy="5" r="1" fill="%235D509E" /></svg>')
center no-repeat #34b9eb;
background: var(--main-theme-color);
border-radius: 50%;
width: 12px;
height: 12px;
box-shadow: 0 0.5px 2px rgba(0, 0, 0, 0.3);
}
.thumb-wrapper {
@@ -58,21 +54,19 @@ range-input::before {
bottom: 0;
height: 0;
overflow: visible;
transform: translate(var(--value-percent, 0%), 0);
}
.value-display {
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="62" fill="none"><path fill="%2334B9EB" d="M27.3 27.3C25 29.6 17 35.8 17 43v3c0 3 2.5 5 3.2 5.8a6 6 0 1 1-8.5 0C12.6 51 15 49 15 46v-3c0-7.2-8-13.4-10.3-15.7A16 16 0 0 1 16 0a16 16 0 0 1 11.3 27.3z"/><circle cx="16" cy="56" r="1" fill="%235D509E"/></svg>')
top center no-repeat;
position: absolute;
box-sizing: border-box;
left: var(--value-percent, 0%);
left: 0;
bottom: 3px;
width: 32px;
height: 62px;
text-align: center;
padding: 8px 3px 0;
margin: 0 0 0 -16px;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
transform-origin: 50% 90%;
opacity: 0.0001;
transform: scale(0.2);
@@ -86,6 +80,19 @@ range-input::before {
will-change: transform;
pointer-events: none;
overflow: hidden;
> svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
fill: var(--main-theme-color);
}
> span {
position: relative;
}
}
.touch-active + .thumb-wrapper .value-display {

View File

@@ -33,6 +33,7 @@
box-sizing: border-box;
text-decoration: underline;
text-decoration-style: dotted;
text-decoration-color: var(--main-theme-color);
text-underline-position: under;
width: 48px;
position: relative;

View File

@@ -0,0 +1,21 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import { Arrow } from '../../../icons';
interface Props extends preact.JSX.HTMLAttributes {}
interface State {}
export default class Revealer extends Component<Props, State> {
render(props: Props) {
return (
<div class={style.checkbox}>
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
<input class={style.realCheckbox} type="checkbox" {...props} />
<div class={style.arrow}>
<Arrow />
</div>
</div>
);
}
}

View File

@@ -0,0 +1,29 @@
.checkbox {
display: inline-block;
position: relative;
}
.arrow {
width: 10px;
height: 10px;
fill: var(--white);
transition: transform 200ms ease;
transform: rotate(-90deg);
svg {
width: 100%;
height: 100%;
display: block;
}
}
.real-checkbox {
top: 0;
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .arrow {
transform: none;
}
}

View File

@@ -1,6 +1,7 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import { Arrow } from 'client/lazy-app/icons';
interface Props extends preact.JSX.HTMLAttributes {
large?: boolean;
@@ -18,9 +19,9 @@ export default class Select extends Component<Props, State> {
class={`${style.builtinSelect} ${large ? style.large : ''}`}
{...otherProps}
/>
<svg class={style.arrow} viewBox="0 0 10 5">
<path d="M0 0l5 5 5-5z" />
</svg>
<div class={style.arrow}>
<Arrow />
</div>
</div>
);
}

View File

@@ -3,10 +3,12 @@
}
.builtin-select {
background: #2f2f2f;
background: var(--black);
border-radius: 4px;
font: inherit;
padding: 4px 25px 4px 10px;
padding: 7px 0;
padding-right: 25px;
padding-left: 10px;
-webkit-appearance: none;
-moz-appearance: none;
border: none;
@@ -21,11 +23,12 @@
transform: translateY(-50%);
fill: #fff;
width: 10px;
pointer-events: none;
}
.large {
padding: 10px 35px 10px 10px;
background: #151515;
background: var(--dark-gray);
& .arrow {
right: 13px;

View File

@@ -0,0 +1,22 @@
import { h, Component } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
interface Props extends preact.JSX.HTMLAttributes {}
interface State {}
export default class Toggle extends Component<Props, State> {
render(props: Props) {
return (
<div class={style.checkbox}>
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
<input class={style.realCheckbox} type="checkbox" {...props} />
<div class={style.track}>
<div class={style.thumbTrack}>
<div class={style.thumb}></div>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,55 @@
.checkbox {
display: inline-block;
position: relative;
}
.track {
--thumb-size: 14px;
background: var(--black);
border-radius: 1000px;
width: 24px;
padding: 3px calc(var(--thumb-size) / 2 + 3px);
}
.thumb {
position: relative;
width: var(--thumb-size);
height: var(--thumb-size);
background: var(--less-light-gray);
border-radius: 100%;
transform: translateX(calc(var(--thumb-size) / -2));
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--main-theme-color);
opacity: 0;
transition: opacity 200ms ease;
}
}
.thumb-track {
transition: transform 200ms ease;
}
.real-checkbox {
top: 0;
position: absolute;
opacity: 0;
pointer-events: none;
&:checked + .track {
.thumb-track {
transform: translateX(100%);
}
.thumb::before {
opacity: 1;
}
}
}

View File

@@ -14,18 +14,20 @@ import {
} from '../../feature-meta';
import Expander from './Expander';
import Checkbox from './Checkbox';
import Toggle from './Toggle';
import Select from './Select';
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
interface Props {
index: 0 | 1;
mobileView: boolean;
source?: SourceImage;
encoderState?: EncoderState;
processorState: ProcessorState;
onEncoderTypeChange(newType: OutputType): void;
onEncoderOptionsChange(newOptions: EncoderOptions): void;
onProcessorOptionsChange(newOptions: ProcessorState): void;
onEncoderTypeChange(index: 0 | 1, newType: OutputType): void;
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
}
interface State {
@@ -73,7 +75,7 @@ export default class Options extends Component<Props, State> {
// The select element only has values matching encoder types,
// so 'as' is safe here.
const type = el.value as OutputType;
this.props.onEncoderTypeChange(type);
this.props.onEncoderTypeChange(this.props.index, type);
};
private onProcessorEnabledChange = (event: Event) => {
@@ -81,24 +83,31 @@ export default class Options extends Component<Props, State> {
const processor = el.name.split('.')[0] as keyof ProcessorState;
this.props.onProcessorOptionsChange(
this.props.index,
cleanSet(this.props.processorState, `${processor}.enabled`, el.checked),
);
};
private onQuantizerOptionsChange = (opts: ProcessorOptions['quantize']) => {
this.props.onProcessorOptionsChange(
this.props.index,
cleanMerge(this.props.processorState, 'quantize', opts),
);
};
private onResizeOptionsChange = (opts: ProcessorOptions['resize']) => {
this.props.onProcessorOptionsChange(
this.props.index,
cleanMerge(this.props.processorState, 'resize', opts),
);
};
private onEncoderOptionsChange = (newOptions: EncoderOptions) => {
this.props.onEncoderOptionsChange(this.props.index, newOptions);
};
render(
{ source, encoderState, processorState, onEncoderOptionsChange }: Props,
{ source, encoderState, processorState }: Props,
{ supportedEncoderMap }: State,
) {
const encoder = encoderState && encoderMap[encoderState.type];
@@ -106,18 +115,24 @@ export default class Options extends Component<Props, State> {
encoder && 'Options' in encoder ? encoder.Options : undefined;
return (
<div class={style.optionsScroller}>
<div
class={
style.optionsScroller +
' ' +
(encoderState ? '' : style.originalImage)
}
>
<Expander>
{!encoderState ? null : (
<div>
<h3 class={style.optionsTitle}>Edit</h3>
<label class={style.sectionEnabler}>
<Checkbox
Resize
<Toggle
name="resize.enable"
checked={!!processorState.resize.enabled}
onChange={this.onProcessorEnabledChange}
/>
Resize
</label>
<Expander>
{processorState.resize.enabled ? (
@@ -132,12 +147,12 @@ export default class Options extends Component<Props, State> {
</Expander>
<label class={style.sectionEnabler}>
<Checkbox
Reduce palette
<Toggle
name="quantize.enable"
checked={!!processorState.quantize.enabled}
onChange={this.onProcessorEnabledChange}
/>
Reduce palette
</label>
<Expander>
{processorState.quantize.enabled ? (
@@ -180,7 +195,7 @@ export default class Options extends Component<Props, State> {
// the correct type, but typescript isn't smart enough.
encoderState!.options as any
}
onChange={onEncoderOptionsChange}
onChange={this.onEncoderOptionsChange}
/>
)}
</Expander>

View File

@@ -1,58 +1,83 @@
.options-scroller {
--horizontal-padding: 15px;
border-radius: var(--scroller-radius);
/* At smaller widths, the multi-panel handles the scrolling */
@media (min-width: 600px) {
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
--horizontal-padding: 15px;
}
}
.options-title {
background: rgba(0, 0, 0, 0.9);
background-color: var(--main-theme-color);
color: var(--header-text-color);
margin: 0;
padding: 10px var(--horizontal-padding);
font-weight: normal;
font-weight: bold;
font-size: 1.4rem;
border-bottom: 1px solid #000;
border-bottom: 1px solid var(--off-black);
transition: all 300ms ease-in-out;
transition-property: background-color, color;
position: sticky;
top: 0;
z-index: 1;
}
.original-image .options-title {
background-color: var(--black);
color: var(--white);
}
.option-text-first {
display: grid;
align-items: center;
grid-template-columns: 87px 1fr;
grid-gap: 0.7em;
gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.option-toggle {
cursor: pointer;
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.option-reveal {
composes: option-toggle;
grid-template-columns: auto 1fr;
gap: 1em;
}
.option-one-cell {
display: grid;
grid-template-columns: 1fr;
padding: 10px var(--horizontal-padding);
}
.option-input-first,
.section-enabler {
cursor: pointer;
display: grid;
align-items: center;
grid-template-columns: auto 1fr;
grid-gap: 0.7em;
padding: 10px var(--horizontal-padding);
}
.section-enabler {
background: rgba(0, 0, 0, 0.8);
composes: option-toggle;
background: var(--dark-gray);
padding: 15px var(--horizontal-padding);
border-bottom: 1px solid var(--off-black);
}
.options-section {
background: rgba(0, 0, 0, 0.7);
background: var(--off-black);
}
.text-field {
background: #fff;
color: #000;
background: var(--white);
color: var(--black);
font: inherit;
border: none;
padding: 2px 0 2px 10px;
padding: 6px 0 6px 10px;
width: 100%;
box-sizing: border-box;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
border-radius: 4px;
}

View File

@@ -73,9 +73,14 @@ export default class TwoUp extends HTMLElement {
connectedCallback() {
this._childrenChange();
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>`;
// prettier-ignore
this._handle.innerHTML =
`<div class="${styles.scrubber}">${
`<svg viewBox="0 0 27 20">${
`<path class="${styles.arrowLeft}" d="M9.6 0L0 9.6l9.6 9.6z"/>` +
`<path class="${styles.arrowRight}" d="M17 19.2l9.5-9.6L16.9 0z"/>`
}</svg>
`}</div>`;
if (!this._everConnected) {
this._resetPosition();

View File

@@ -2,12 +2,11 @@ two-up {
display: grid;
position: relative;
--split-point: 0;
--accent-color: #777;
--track-color: var(--accent-color);
--thumb-background: #fff;
--track-color: rgb(0 0 0 / 0.6);
--thumb-background: var(--black);
--thumb-color: var(--accent-color);
--thumb-size: 62px;
--bar-size: 6px;
--bar-size: 9px;
--bar-touch-size: 30px;
}
@@ -37,8 +36,6 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
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);
}
@@ -47,14 +44,11 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
position: absolute;
top: 50%;
left: 50%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%);
width: var(--thumb-size);
height: calc(var(--thumb-size) * 0.9);
height: var(--thumb-size);
background: var(--thumb-background);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: var(--thumb-size);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
color: var(--thumb-color);
box-sizing: border-box;
padding: 0 calc(var(--thumb-size) * 0.24);
@@ -64,6 +58,14 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
flex: 1;
}
.arrow-left {
fill: var(--pink);
}
.arrow-right {
fill: var(--blue);
}
two-up[orientation='vertical'] .two-up-handle {
width: auto;
height: var(--bar-touch-size);

View File

@@ -10,7 +10,6 @@ import {
ToggleBackgroundIcon,
AddIcon,
RemoveIcon,
BackIcon,
ToggleBackgroundActiveIcon,
RotateIcon,
} from '../../icons';
@@ -28,7 +27,6 @@ interface Props {
rightCompressed?: ImageData;
leftImgContain: boolean;
rightImgContain: boolean;
onBack: () => void;
onPreprocessorChange: (newState: PreprocessorState) => void;
}
@@ -255,7 +253,7 @@ export default class Output extends Component<Props, State> {
};
render(
{ mobileView, leftImgContain, rightImgContain, source, onBack }: Props,
{ mobileView, leftImgContain, rightImgContain, source }: Props,
{ scale, editingScale, altBackground }: State,
) {
const leftDraw = this.leftDrawable();
@@ -314,12 +312,6 @@ export default class Output extends Component<Props, State> {
</pinch-zoom>
</two-up>
<div class={style.back}>
<button class={style.button} onClick={onBack}>
<BackIcon />
</button>
</div>
<div class={style.controls}>
<div class={style.zoomControls}>
<button class={style.button} onClick={this.zoomOut}>

View File

@@ -1,5 +1,5 @@
.output {
composes: abs-fill from global;
display: contents;
&::before {
content: '';
@@ -9,18 +9,17 @@
right: 0;
bottom: 0;
background: #000;
opacity: 0;
opacity: 0.8;
transition: opacity 500ms ease;
}
&.alt-background::before {
opacity: 0.6;
opacity: 0;
}
}
.two-up {
composes: abs-fill from global;
--accent-color: var(--button-fg);
}
.pinch-zoom {
@@ -41,16 +40,15 @@
}
.controls {
position: absolute;
display: flex;
justify-content: center;
top: 0;
left: 0;
right: 0;
padding: 9px 84px;
overflow: hidden;
flex-wrap: wrap;
contain: content;
grid-area: header;
align-self: center;
padding: 9px 66px;
position: relative;
/* Allow clicks to fall through to the pinch zoom area */
pointer-events: none;
@@ -60,11 +58,9 @@
@media (min-width: 860px) {
padding: 9px;
top: auto;
left: 320px;
right: 320px;
bottom: 0;
flex-wrap: wrap-reverse;
grid-area: viewportOpts;
align-self: end;
}
}
@@ -149,13 +145,6 @@
border-bottom: 1px dashed #999;
}
.back {
position: absolute;
top: 0;
left: 0;
padding: 9px;
}
.buttons-no-wrap {
display: flex;
pointer-events: none;

View File

@@ -1,43 +0,0 @@
import { h, Component } from 'preact';
import prettyBytes from 'pretty-bytes';
import * as style from './style.css';
interface Props {
blob: Blob;
compareTo?: Blob;
}
interface State {}
export default class FileSize extends Component<Props, State> {
render({ blob, compareTo }: Props) {
let comparison: preact.JSX.Element | undefined;
if (compareTo) {
const delta = blob.size / compareTo.size;
if (delta > 1) {
const percent = Math.round((delta - 1) * 100) + '%';
comparison = (
<span class={`${style.sizeDelta} ${style.sizeIncrease}`}>
{percent === '0%' ? 'slightly' : percent} bigger
</span>
);
} else if (delta < 1) {
const percent = Math.round((1 - delta) * 100) + '%';
comparison = (
<span class={`${style.sizeDelta} ${style.sizeDecrease}`}>
{percent === '0%' ? 'slightly' : percent} smaller
</span>
);
} else {
comparison = <span class={style.sizeDelta}>no change</span>;
}
}
return (
<span>
{prettyBytes(blob.size)} {comparison}
</span>
);
}
}

View File

@@ -1,37 +1,25 @@
import { h, Component, ComponentChildren, ComponentChild } from 'preact';
import { h, Component, Fragment } from 'preact';
import * as style from './style.css';
import 'add-css:./style.css';
import FileSize from './FileSize';
import {
DownloadIcon,
CopyAcrossIcon,
CopyAcrossIconProps,
} from 'client/lazy-app/icons';
import 'shared/custom-els/loading-spinner';
import { SourceImage } from '../';
import prettyBytes from './pretty-bytes';
import { Arrow, DownloadIcon } from 'client/lazy-app/icons';
interface Props {
loading: boolean;
source?: SourceImage;
imageFile?: File;
downloadUrl?: string;
children: ComponentChildren;
copyDirection: CopyAcrossIconProps['copyDirection'];
buttonPosition: keyof typeof buttonPositionClass;
onCopyToOtherClick(): void;
flipSide: boolean;
typeLabel: string;
}
interface State {
showLoadingState: boolean;
}
const buttonPositionClass = {
'stack-right': style.stackRight,
'download-right': style.downloadRight,
'download-left': style.downloadLeft,
};
const loadingReactionDelay = 500;
export default class Results extends Component<Props, State> {
@@ -56,11 +44,6 @@ export default class Results extends Component<Props, State> {
}
}
private onCopyToOtherClick = (event: Event) => {
event.preventDefault();
this.props.onCopyToOtherClick();
};
private onDownload = () => {
// GA cant do floats. So we round to ints. We're deliberately rounding to nearest kilobyte to
// avoid cases where exact image sizes leak something interesting about the user.
@@ -76,59 +59,83 @@ export default class Results extends Component<Props, State> {
};
render(
{
source,
imageFile,
downloadUrl,
children,
copyDirection,
buttonPosition,
}: Props,
{ source, imageFile, downloadUrl, flipSide, typeLabel }: Props,
{ showLoadingState }: State,
) {
return (
<div class={`${style.results} ${buttonPositionClass[buttonPosition]}`}>
<div class={style.resultData}>
{children ? <div class={style.resultTitle}>{children}</div> : null}
{!imageFile || showLoadingState ? (
'Working…'
) : (
<FileSize
blob={imageFile}
compareTo={
source && imageFile !== source.file ? source.file : undefined
const prettySize = imageFile && prettyBytes(imageFile.size);
const isOriginal = !source || !imageFile || source.file === imageFile;
let diff;
let percent;
if (source && imageFile) {
diff = imageFile.size / source.file.size;
const absolutePercent = Math.round(Math.abs(diff) * 100);
percent = diff > 1 ? absolutePercent - 100 : 100 - absolutePercent;
}
/>
return (
<div
class={
(flipSide ? style.resultsRight : style.resultsLeft) +
' ' +
(isOriginal ? style.isOriginal : '')
}
>
<div class={style.expandArrow}>
<Arrow />
</div>
<div class={style.bubble}>
<div class={style.bubbleInner}>
<div class={style.sizeInfo}>
<div class={style.fileSize}>
{prettySize ? (
<Fragment>
{prettySize.value}{' '}
<span class={style.unit}>{prettySize.unit}</span>
<span class={style.typeLabel}> {typeLabel}</span>
</Fragment>
) : (
'…'
)}
</div>
<button
class={style.copyToOther}
title="Copy settings to other side"
onClick={this.onCopyToOtherClick}
</div>
<div class={style.percentInfo}>
<svg
viewBox="0 0 1 2"
class={style.bigArrow}
preserveAspectRatio="none"
>
<CopyAcrossIcon
class={style.copyIcon}
copyDirection={copyDirection}
/>
</button>
<div class={style.download}>
{downloadUrl && imageFile && (
<path d="M1 0v2L0 1z" />
</svg>
<div class={style.percentOutput}>
{diff && diff !== 1 && (
<span class={style.sizeDirection}>
{diff < 1 ? '↓' : '↑'}
</span>
)}
<span class={style.sizeValue}>{percent || 0}</span>
<span class={style.percentChar}>%</span>
</div>
</div>
</div>
</div>
<a
class={`${style.downloadLink} ${
showLoadingState ? style.downloadLinkDisable : ''
}`}
class={showLoadingState ? style.downloadDisable : style.download}
href={downloadUrl}
download={imageFile.name}
download={imageFile ? imageFile.name : ''}
title="Download"
onClick={this.onDownload}
>
<DownloadIcon class={style.downloadIcon} />
</a>
)}
{showLoadingState && <loading-spinner class={style.spinner} />}
<svg class={style.downloadBlobs} viewBox="0 0 89.6 86.9">
<title>Download</title>
<path d="M27.3 72c-8-4-15.6-12.3-16.9-21-1.2-8.7 4-17.8 10.5-26s14.4-15.6 24-16 21.2 6 28.6 16.5c7.4 10.5 10.8 25 6.6 34S64.1 71.8 54 73.6c-10.2 2-18.7 2.3-26.7-1.6z" />
<path d="M19.8 24.8c4.3-7.8 13-15 21.8-15.7 8.7-.8 17.5 4.8 25.4 11.8 7.8 6.9 14.8 15.2 14.7 24.9s-7.1 20.7-18 27.6c-10.8 6.8-25.5 9.5-34.2 4.8S18.1 61.6 16.7 51.4c-1.3-10.3-1.3-18.8 3-26.6z" />
</svg>
<div class={style.downloadIcon}>
<DownloadIcon />
</div>
{showLoadingState && <loading-spinner />}
</a>
</div>
);
}

View File

@@ -0,0 +1,27 @@
// Based on https://www.npmjs.com/package/pretty-bytes
// Modified so the units are returned separately.
const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
interface PrettyBytesResult {
value: string;
unit: string;
}
export default function prettyBytes(number: number): PrettyBytesResult {
const isNegative = number < 0;
const prefix = isNegative ? '-' : '';
if (isNegative) number = -number;
if (number < 1) return { value: prefix + number, unit: UNITS[0] };
const exponent = Math.min(
Math.floor(Math.log10(number) / 3),
UNITS.length - 1,
);
return {
unit: UNITS[exponent],
value: prefix + (number / Math.pow(1000, exponent)).toPrecision(3),
};
}

View File

@@ -1,3 +1,12 @@
@font-face {
font-family: 'Roboto Mono Numbers';
font-style: normal;
font-weight: 700;
/* Just 0132456789. https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@700&text=0123456789 */
src: url('data:font/woff;base64,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')
format('woff');
}
@keyframes action-enter {
from {
transform: rotate(-90deg);
@@ -15,117 +24,336 @@
}
.results {
--download-overflow-size: 9px;
background: rgba(0, 0, 0, 0.67);
border-radius: 5px;
display: grid;
grid-template-columns: [text] 1fr [copy-button] auto [download-button] auto;
background: rgba(0, 0, 0, 0.9);
font-size: 1rem;
@media (min-width: 400px) {
font-size: 1.2rem;
}
grid-template-columns: max-content [bubble] 1fr [download] max-content;
@media (min-width: 600px) {
font-size: 1.4rem;
}
&:focus {
outline: none;
}
}
.result-data {
grid-row: 1;
grid-column: text;
display: flex;
--download-overflow-size: 30px;
background: none;
border-radius: none;
grid-template-columns: [download] auto [bubble] 1fr;
align-items: center;
padding: 0 10px;
white-space: nowrap;
overflow: hidden;
}
.download-right {
grid-template-columns: [copy-button] auto [text] 1fr [download-button] auto;
}
.download-left {
grid-template-columns: [download-button] auto [text] 1fr [copy-button] auto;
}
.stack-right {
& .result-data {
padding: 0 15px;
margin-bottom: calc(var(--download-overflow-size) / 2);
}
}
.result-title {
display: flex;
align-items: center;
margin-right: 0.4em;
.expand-arrow {
fill: var(--white);
transform: rotate(180deg);
margin: 0 1rem;
align-self: center;
@media (min-width: 600px) {
display: none;
}
:focus & {
fill: var(--main-theme-color);
}
[content-expanded] & {
transform: none;
}
svg {
display: block;
--size: 15px;
width: var(--size);
height: var(--size);
}
}
.size-delta {
font-size: 0.8em;
font-style: italic;
.file-size {
}
.bubble {
align-self: center;
@media (min-width: 600px) {
position: relative;
top: -1px;
margin-left: 0.3em;
width: max-content;
grid-row: 1;
grid-column: bubble;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='186.5' height='280.3' viewBox='0 0 186.5 280.3'%3E%3Cpath fill='rgba(30,31,29,0.69)' d='M181.5 0H16.4a5 5 0 00-5 5v134L0 146.5h11.4v128.8a5 5 0 005 5h165.1a5 5 0 005-5V5a5 5 0 00-5-5z'/%3E%3Cpath fill='rgba(0,0,0,0.23)' d='M16.4 1a4 4 0 00-4 4v134.5l-.5.3-8.6 5.7h9v129.8a4 4 0 004 4h165.2a4 4 0 004-4V5a4 4 0 00-4-4H16.4m0-1h165.1a5 5 0 015 5v270.3a5 5 0 01-5 5H16.4a5 5 0 01-5-5V146.5H0l11.4-7.5V5a5 5 0 015-5z'/%3E%3C/svg%3E");
border-image-slice: 12 12 12 17 fill;
border-image-width: 12px 12px 12px 17px;
border-image-repeat: repeat;
}
}
}
.size-increase {
color: #e35050;
.bubble-inner {
display: grid;
grid-template-columns: [size-info] 1fr [percent-info] auto;
@media (min-width: 600px) {
position: relative;
--main-padding: 1px;
--speech-padding: 2.1rem;
padding: var(--main-padding) var(--main-padding) var(--main-padding)
var(--speech-padding);
gap: 0.9rem;
}
}
.size-decrease {
color: #50e3c2;
.unit {
color: var(--main-theme-color);
}
.type-label {
@media (min-width: 600px) {
display: none;
}
}
.size-info {
background: var(--dark-gray);
border-radius: 19px;
align-self: center;
justify-self: start;
grid-column: size-info;
grid-row: 1;
justify-self: start;
padding: 0.6rem 1.2rem;
margin: 0.4rem 0;
@media (min-width: 600px) {
border-radius: none;
background: none;
padding: 0;
margin: 0;
}
}
.percent-info {
align-self: center;
margin-left: 1rem;
margin-right: 0.3rem;
@media (min-width: 600px) {
margin: 0;
display: grid;
--arrow-width: 16px;
grid-template-columns: [arrow] var(--arrow-width) [data] auto;
grid-column: percent-info;
grid-row: 1;
--shadow-direction: -1px;
filter: drop-shadow(var(--shadow-direction) 0 0 rgba(0, 0, 0, 0.67));
}
}
.big-arrow {
display: none;
@media (min-width: 600px) {
display: block;
width: 100%;
fill: var(--main-theme-color);
grid-column: arrow;
grid-row: 1;
align-self: stretch;
}
}
.percent-output {
grid-column: data;
grid-row: 1;
display: grid;
grid-template-columns: auto auto auto;
line-height: 1;
@media (min-width: 600px) {
background: var(--main-theme-color);
--radius: 4px;
border-radius: 0 var(--radius) var(--radius) 0;
--padding-arrow-side: 0.6rem;
--padding-other-side: 1.1rem;
padding: 0.7rem var(--padding-other-side);
padding-left: var(--padding-arrow-side);
}
}
.size-direction {
font-weight: 700;
align-self: center;
font-family: sans-serif;
opacity: 0.76;
text-shadow: 0 2px rgba(0, 0, 0, 0.3);
font-size: 1.5rem;
position: relative;
top: 3px;
}
.size-value {
font-family: 'Roboto Mono Numbers';
font-size: 2.6rem;
text-shadow: 0 2px rgba(0, 0, 0, 0.3);
}
.percent-char {
align-self: start;
position: relative;
top: 4px;
opacity: 0.76;
margin-left: 0.2rem;
}
.download {
--size: 59px;
width: calc(var(--size) + var(--download-overflow-size));
height: calc(var(--size) + var(--download-overflow-size));
position: relative;
grid-row: 1;
grid-column: download-button;
background: #34b9eb;
--size: 38px;
width: var(--size);
height: var(--size);
grid-column: download;
margin: calc(var(--download-overflow-size) / -2) 0;
margin-right: calc(var(--download-overflow-size) / -3);
display: grid;
align-items: center;
justify-items: center;
align-self: center;
@media (min-width: 600px) {
--size: 63px;
}
loading-spinner {
grid-area: 1 / 1;
position: relative;
--color: var(--white);
--size: 21px;
top: 0px;
left: 1px;
@media (min-width: 600px) {
top: -1px;
left: 2px;
--size: 28px;
}
}
}
.download-link {
.download-blobs {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
path {
fill: var(--hot-theme-color);
opacity: 0.7;
}
}
.download-icon {
grid-area: 1 / 1;
svg {
--size: 19px;
width: var(--size);
height: var(--size);
fill: var(--white);
position: relative;
top: 3px;
left: 1px;
animation: action-enter 0.2s;
grid-area: 1/1;
@media (min-width: 600px) {
--size: 27px;
top: 2px;
left: 2px;
}
}
}
.download-link-disable {
.download-disable {
composes: download;
pointer-events: none;
.download-icon svg {
opacity: 0;
transform: rotate(90deg);
animation: action-leave 0.2s;
}
}
.download-icon,
.copy-icon {
color: #fff;
display: block;
--size: 24px;
width: var(--size);
height: var(--size);
padding: 7px;
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.7));
.results-left {
composes: results;
}
.spinner {
--color: #fff;
--delay: 0;
--size: 22px;
grid-area: 1/1;
.results-right {
composes: results;
@media (min-width: 600px) {
grid-template-columns: [bubble] 1fr [download] auto;
}
.bubble {
@media (min-width: 600px) {
justify-self: end;
&::before {
transform: scaleX(-1);
}
}
}
.download {
margin-left: calc(var(--download-overflow-size) / -3);
margin-right: 0;
}
.bubble-inner {
@media (min-width: 600px) {
padding: var(--main-padding) var(--speech-padding) var(--main-padding)
var(--main-padding);
grid-template-columns: [percent-info] auto [size-info] 1fr;
}
}
.percent-info {
@media (min-width: 600px) {
grid-template-columns: [data] auto [arrow] var(--arrow-width);
--shadow-direction: 1px;
}
}
.percent-output {
@media (min-width: 600px) {
border-radius: var(--radius) 0 0 var(--radius);
padding-left: var(--padding-other-side);
padding-right: var(--padding-arrow-side);
}
}
.big-arrow {
transform: scaleX(-1);
}
}
.copy-to-other {
grid-row: 1;
grid-column: copy-button;
composes: unbutton from global;
composes: download;
background: #656565;
.is-original {
.big-arrow {
fill: transparent;
}
.percent-output {
background: none;
}
.download-blobs path {
fill: var(--black);
}
.unit {
color: var(--white);
opacity: 0.76;
}
}

View File

@@ -1,6 +1,6 @@
.panel-heading {
background: gray;
}
.panel-content {
height: 0px;
overflow: auto;

View File

@@ -31,7 +31,7 @@ import Results from './Results';
import WorkerBridge from '../worker-bridge';
import { resize } from 'features/processors/resize/client';
import type SnackBarElement from 'shared/custom-els/snack-bar';
import { CopyAcrossIconProps, ExpandIcon } from '../icons';
import { Arrow, ExpandIcon } from '../icons';
export type OutputType = EncoderType | 'identity';
@@ -319,7 +319,7 @@ export default class Compress extends Component<Props, State> {
this.setState({ mobileView: this.widthQuery.matches });
};
private onEncoderTypeChange(index: 0 | 1, newType: OutputType): void {
private onEncoderTypeChange = (index: 0 | 1, newType: OutputType): void => {
this.setState({
sides: cleanSet(
this.state.sides,
@@ -332,12 +332,12 @@ export default class Compress extends Component<Props, State> {
},
),
});
}
};
private onProcessorOptionsChange(
private onProcessorOptionsChange = (
index: 0 | 1,
options: ProcessorState,
): void {
): void => {
this.setState({
sides: cleanSet(
this.state.sides,
@@ -345,9 +345,12 @@ export default class Compress extends Component<Props, State> {
options,
),
});
}
};
private onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void {
private onEncoderOptionsChange = (
index: 0 | 1,
options: EncoderOptions,
): void => {
this.setState({
sides: cleanSet(
this.state.sides,
@@ -355,7 +358,7 @@ export default class Compress extends Component<Props, State> {
options,
),
});
}
};
componentWillReceiveProps(nextProps: Props): void {
if (nextProps.file !== this.props.file) {
@@ -794,50 +797,30 @@ export default class Compress extends Component<Props, State> {
const options = sides.map((side, index) => (
<Options
index={index as 0 | 1}
source={source}
mobileView={mobileView}
processorState={side.latestSettings.processorState}
encoderState={side.latestSettings.encoderState}
onEncoderTypeChange={this.onEncoderTypeChange.bind(
this,
index as 0 | 1,
)}
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(
this,
index as 0 | 1,
)}
onProcessorOptionsChange={this.onProcessorOptionsChange.bind(
this,
index as 0 | 1,
)}
onEncoderTypeChange={this.onEncoderTypeChange}
onEncoderOptionsChange={this.onEncoderOptionsChange}
onProcessorOptionsChange={this.onProcessorOptionsChange}
/>
));
const copyDirections = (mobileView
? ['down', 'up']
: ['right', 'left']) as CopyAcrossIconProps['copyDirection'][];
const results = sides.map((side, index) => (
<Results
downloadUrl={side.downloadUrl}
imageFile={side.file}
source={source}
loading={loading || side.loading}
copyDirection={copyDirections[index]}
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index as 0 | 1)}
buttonPosition={mobileView ? 'stack-right' : buttonPositions[index]}
>
{!mobileView
? null
: [
<ExpandIcon class={style.expandIcon} key="expand-icon" />,
`${resultTitles[index]} (${
flipSide={mobileView || index === 1}
typeLabel={
side.latestSettings.encoderState
? encoderMap[side.latestSettings.encoderState.type].meta.label
: 'Original Image'
})`,
]}
</Results>
}
/>
));
// For rendering, we ideally want the settings that were used to create the
@@ -862,26 +845,38 @@ export default class Compress extends Component<Props, State> {
rightCompressed={rightImageData}
leftImgContain={leftImgContain}
rightImgContain={rightImgContain}
onBack={onBack}
preprocessorState={preprocessorState}
onPreprocessorChange={this.onPreprocessorChange}
/>
<button class={style.back} onClick={onBack}>
<svg viewBox="0 0 61 53.3">
<title>Back</title>
<path
class={style.backBlob}
d="M0 25.6c-.5-7.1 4.1-14.5 10-19.1S23.4.1 32.2 0c8.8 0 19 1.6 24.4 8s5.6 17.8 1.7 27a29.7 29.7 0 01-20.5 18c-8.4 1.5-17.3-2.6-24.5-8S.5 32.6.1 25.6z"
/>
<path
class={style.backX}
d="M41.6 17.1l-2-2.1-8.3 8.2-8.2-8.2-2 2 8.2 8.3-8.3 8.2 2.1 2 8.2-8.1 8.3 8.2 2-2-8.2-8.3z"
/>
</svg>
</button>
{mobileView ? (
<div class={style.options}>
<multi-panel class={style.multiPanel} open-one-only>
{results[0]}
{options[0]}
{results[1]}
{options[1]}
<div class={style.options1Theme}>{results[0]}</div>
<div class={style.options1Theme}>{options[0]}</div>
<div class={style.options2Theme}>{results[1]}</div>
<div class={style.options2Theme}>{options[1]}</div>
</multi-panel>
</div>
) : (
[
<div class={style.options} key="options0">
<div class={style.options1} key="options1">
{options[0]}
{results[0]}
</div>,
<div class={style.options} key="options1">
<div class={style.options2} key="options2">
{options[1]}
{results[1]}
</div>,

View File

@@ -3,39 +3,77 @@
height: 100%;
contain: strict;
display: grid;
align-items: end;
align-content: end;
grid-template-rows: 1fr auto;
grid-template-rows: max-content 1fr;
grid-template-areas:
'header'
'opts';
--options-radius: 7px;
@media (min-width: 600px) {
grid-template-columns: 1fr auto;
grid-template-rows: 100%;
grid-template-rows: max-content 1fr;
grid-template-columns: max-content 1fr max-content;
grid-template-areas:
'header header header'
'optsLeft viewportOpts optsRight';
}
}
.options {
position: relative;
color: #fff;
opacity: 0.9;
font-size: 1.2rem;
display: flex;
flex-flow: column;
max-width: 400px;
margin: 0 auto;
width: calc(100% - 60px);
max-height: calc(100% - 104px);
max-height: 100%;
overflow: hidden;
grid-area: opts;
display: grid;
grid-template-rows: 1fr max-content;
align-content: end;
align-self: end;
@media (min-width: 600px) {
max-height: calc(100% - 75px);
width: 300px;
margin: 0;
}
}
@media (min-width: 860px) {
max-height: calc(100% - 40px);
.options-1-theme {
--main-theme-color: var(--pink);
--hot-theme-color: var(--hot-pink);
--header-text-color: var(--white);
--scroller-radius: var(--options-radius) var(--options-radius) 0 0;
@media (min-width: 600px) {
--scroller-radius: 0 var(--options-radius) var(--options-radius) 0;
}
}
.options-2-theme {
--main-theme-color: var(--blue);
--hot-theme-color: var(--deep-blue);
--header-text-color: var(--dark-text);
--scroller-radius: var(--options-radius) var(--options-radius) 0 0;
@media (min-width: 600px) {
--scroller-radius: var(--options-radius) 0 0 var(--options-radius);
}
}
.options-1 {
composes: options;
composes: options-1-theme;
grid-area: optsLeft;
}
.options-2 {
composes: options;
composes: options-2-theme;
grid-area: optsRight;
}
.multi-panel {
position: relative;
display: flex;
@@ -61,15 +99,32 @@
}
}
.expand-icon {
transform: rotate(180deg);
margin-left: -12px;
.back {
composes: unbutton from global;
position: relative;
grid-area: header;
margin: 9px;
justify-self: start;
align-self: start;
& > svg {
width: 47px;
}
@media (min-width: 600px) {
margin: 14px;
& > svg {
width: 58px;
}
}
}
[content-expanded] .expand-icon {
transform: none;
.back-blob {
fill: var(--hot-pink);
opacity: 0.77;
}
:focus .expand-icon {
fill: #34b9eb;
.back-x {
fill: var(--white);
}

View File

@@ -11,12 +11,6 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
/>
);
export const DownloadIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M19 12v7H5v-7H3v7c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-7h-2zm-6 .7l2.6-2.6 1.4 1.4-5 5-5-5 1.4-1.4 2.6 2.6V3h2z" />
</Icon>
);
export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />
@@ -67,42 +61,14 @@ export const ExpandIcon = (props: preact.JSX.HTMLAttributes) => (
</Icon>
);
export const BackIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M20 11H7.8l5.6-5.6L12 4l-8 8 8 8 1.4-1.4L7.8 13H20v-2z" />
</Icon>
export const Arrow = () => (
<svg viewBox="0 -1.95 9.8 9.8">
<path d="M8.2.2a1 1 0 011.4 1.4l-4 4a1 1 0 01-1.4 0l-4-4A1 1 0 011.6.2l3.3 3.3L8.2.2z" />
</svg>
);
const copyAcrossRotations = {
up: 90,
right: 180,
down: -90,
left: 0,
};
export interface CopyAcrossIconProps extends preact.JSX.HTMLAttributes {
copyDirection: keyof typeof copyAcrossRotations;
}
export const CopyAcrossIcon = (props: CopyAcrossIconProps) => {
const { copyDirection, ...otherProps } = props;
const id = 'point-' + copyDirection;
const rotation = copyAcrossRotations[copyDirection];
return (
<Icon {...otherProps}>
<defs>
<clipPath id={id}>
<path
d="M-12-12v24h24v-24zM4.5 2h-4v3l-5-5 5-5v3h4z"
transform={`translate(12 13) rotate(${rotation})`}
/>
</clipPath>
</defs>
<path
clip-path={`url(#${id})`}
d="M19 3h-4.2c-.4-1.2-1.5-2-2.8-2s-2.4.8-2.8 2H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm-7 0a1 1 0 0 1 0 2c-.6 0-1-.4-1-1s.4-1 1-1z"
/>
</Icon>
);
};
export const DownloadIcon = () => (
<svg viewBox="0 0 23.9 24.9">
<path d="M6.6 2.7h-4v13.2h2.7A2.7 2.7 0 018 18.6a2.7 2.7 0 002.6 2.6h2.7a2.7 2.7 0 002.6-2.6 2.7 2.7 0 012.7-2.7h2.6V2.7h-4a1.3 1.3 0 110-2.7h4A2.7 2.7 0 0124 2.7v18.5a2.7 2.7 0 01-2.7 2.7H2.7A2.7 2.7 0 010 21.2V2.7A2.7 2.7 0 012.7 0h4a1.3 1.3 0 010 2.7zm4 7.4V1.3a1.3 1.3 0 112.7 0v8.8L15 8.4a1.3 1.3 0 011.9 1.8l-4 4a1.3 1.3 0 01-1.9 0l-4-4A1.3 1.3 0 019 8.4z" />
</svg>
);

View File

@@ -8,6 +8,7 @@ import Expander from 'client/lazy-app/Compress/Options/Expander';
import Select from 'client/lazy-app/Compress/Options/Select';
import Range from 'client/lazy-app/Compress/Options/Range';
import linkState from 'linkstate';
import Revealer from 'client/lazy-app/Compress/Options/Revealer';
export const encode = (
signal: AbortSignal,
@@ -209,12 +210,12 @@ export class Options extends Component<Props, State> {
) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Lossless
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{!lossless && (
@@ -242,22 +243,22 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Separate alpha quality
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
/>
Separate alpha quality
</label>
<Expander>
{separateAlpha && (
<div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Lossless alpha
<Checkbox
checked={losslessAlpha}
onChange={this._inputChange('losslessAlpha', 'boolean')}
/>
Lossless alpha
</label>
<Expander>
{!losslessAlpha && (
@@ -288,23 +289,23 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
<label class={style.optionReveal}>
<Revealer
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
Advanced settings
</label>
<Expander>
{showAdvanced && (
<div>
{/*<label class={style.optionInputFirst}>
{/*<label class={style.optionToggle}>
Grayscale
<Checkbox
data-set-state="grayscale"
checked={grayscale}
onChange={this._inputChange('grayscale', 'boolean')}
/>
Grayscale
</label>*/}
<Expander>
{!grayscale && !lossless && (

View File

@@ -123,23 +123,23 @@ export class Options extends Component<Props, State> {
// gathering the data.
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Lossless
<Checkbox
name="lossless"
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{lossless && (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Slight loss
<Checkbox
name="slightLoss"
checked={slightLoss}
onChange={this._inputChange('slightLoss', 'boolean')}
/>
Slight loss
</label>
)}
</Expander>
@@ -157,7 +157,8 @@ export class Options extends Component<Props, State> {
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Auto edge filter
<Checkbox
name="autoEdgeFilter"
checked={autoEdgePreservingFilter}
@@ -166,7 +167,6 @@ export class Options extends Component<Props, State> {
'boolean',
)}
/>
Auto edge filter
</label>
<Expander>
{!autoEdgePreservingFilter && (
@@ -188,13 +188,13 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Progressive rendering
<Checkbox
name="progressive"
checked={progressive}
onChange={this._inputChange('progressive', 'boolean')}
/>
Progressive rendering
</label>
<div class={style.optionOneCell}>
<Range

View File

@@ -12,6 +12,7 @@ import Range from 'client/lazy-app/Compress/Options/Range';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import Select from 'client/lazy-app/Compress/Options/Select';
import Revealer from 'client/lazy-app/Compress/Options/Revealer';
export function encode(
signal: AbortSignal,
@@ -116,12 +117,12 @@ export class Options extends Component<Props, State> {
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
<label class={style.optionReveal}>
<Revealer
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
Advanced settings
</label>
<Expander>
{showAdvanced ? (
@@ -141,13 +142,13 @@ export class Options extends Component<Props, State> {
<Expander>
{options.color_space === MozJpegColorSpace.YCbCr ? (
<div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Auto subsample chroma
<Checkbox
name="auto_subsample"
checked={options.auto_subsample}
onChange={this.onChange}
/>
Auto subsample chroma
</label>
<Expander>
{options.auto_subsample ? null : (
@@ -164,13 +165,13 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Separate chroma quality
<Checkbox
name="separate_chroma_quality"
checked={options.separate_chroma_quality}
onChange={this.onChange}
/>
Separate chroma quality
</label>
<Expander>
{options.separate_chroma_quality ? (
@@ -190,35 +191,35 @@ export class Options extends Component<Props, State> {
</div>
) : null}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Pointless spec compliance
<Checkbox
name="baseline"
checked={options.baseline}
onChange={this.onChange}
/>
Pointless spec compliance
</label>
<Expander>
{options.baseline ? null : (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Progressive rendering
<Checkbox
name="progressive"
checked={options.progressive}
onChange={this.onChange}
/>
Progressive rendering
</label>
)}
</Expander>
<Expander>
{options.baseline ? (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Optimize Huffman table
<Checkbox
name="optimize_coding"
checked={options.optimize_coding}
onChange={this.onChange}
/>
Optimize Huffman table
</label>
) : null}
</Expander>
@@ -251,33 +252,33 @@ export class Options extends Component<Props, State> {
<option value="8">Peterson et al</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Trellis multipass
<Checkbox
name="trellis_multipass"
checked={options.trellis_multipass}
onChange={this.onChange}
/>
Trellis multipass
</label>
<Expander>
{options.trellis_multipass ? (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Optimize zero block runs
<Checkbox
name="trellis_opt_zero"
checked={options.trellis_opt_zero}
onChange={this.onChange}
/>
Optimize zero block runs
</label>
) : null}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Optimize after trellis quantization
<Checkbox
name="trellis_opt_table"
checked={options.trellis_opt_table}
onChange={this.onChange}
/>
Optimize after trellis quantization
</label>
<div class={style.optionOneCell}>
<Range

View File

@@ -12,6 +12,7 @@ import Range from 'client/lazy-app/Compress/Options/Range';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import Select from 'client/lazy-app/Compress/Options/Select';
import Revealer from 'client/lazy-app/Compress/Options/Revealer';
export const encode = (
signal: AbortSignal,
@@ -179,7 +180,8 @@ export class Options extends Component<Props, State> {
Slight loss:
</Range>
</div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Discrete tone image
{/*
Although there are 3 different kinds of image hint, webp only
seems to do something with the 'graph' type, and I don't really
@@ -190,7 +192,6 @@ export class Options extends Component<Props, State> {
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
onChange={this.onChange}
/>
Discrete tone image
</label>
</div>
);
@@ -224,23 +225,23 @@ export class Options extends Component<Props, State> {
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
<label class={style.optionReveal}>
<Revealer
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
Advanced settings
</label>
<Expander>
{showAdvanced ? (
<div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Compress alpha
<Checkbox
name="alpha_compression"
checked={!!options.alpha_compression}
onChange={this.onChange}
/>
Compress alpha
</label>
<div class={style.optionOneCell}>
<Range
@@ -264,13 +265,13 @@ export class Options extends Component<Props, State> {
Alpha filter quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Auto adjust filter strength
<Checkbox
name="autofilter"
checked={!!options.autofilter}
onChange={this.onChange}
/>
Auto adjust filter strength
</label>
<Expander>
{options.autofilter ? null : (
@@ -287,13 +288,13 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Strong filter
<Checkbox
name="filter_type"
checked={!!options.filter_type}
onChange={this.onChange}
/>
Strong filter
</label>
<div class={style.optionOneCell}>
<Range
@@ -306,13 +307,13 @@ export class Options extends Component<Props, State> {
Filter sharpness:
</Range>
</div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Sharp RGBYUV conversion
<Checkbox
name="use_sharp_yuv"
checked={!!options.use_sharp_yuv}
onChange={this.onChange}
/>
Sharp RGBYUV conversion
</label>
<div class={style.optionOneCell}>
<Range
@@ -382,24 +383,24 @@ export class Options extends Component<Props, State> {
// gathering the data.
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Lossless
<Checkbox
name="lossless"
checked={!!options.lossless}
onChange={this.onChange}
/>
Lossless
</label>
{options.lossless
? this._losslessSpecificOptions(options)
: this._lossySpecificOptions(options)}
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Preserve transparent data
<Checkbox
name="exact"
checked={!!options.exact}
onChange={this.onChange}
/>
Preserve transparent data
</label>
</form>
);

View File

@@ -9,6 +9,7 @@ import Select from 'client/lazy-app/Compress/Options/Select';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import linkState from 'linkstate';
import Revealer from 'client/lazy-app/Compress/Options/Revealer';
export const encode = (
signal: AbortSignal,
@@ -154,12 +155,12 @@ export class Options extends Component<Props, State> {
) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Lossless
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{lossless && (
@@ -190,12 +191,12 @@ export class Options extends Component<Props, State> {
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Separate alpha quality
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
/>
Separate alpha quality
</label>
<Expander>
{separateAlpha && (
@@ -212,12 +213,12 @@ export class Options extends Component<Props, State> {
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
<label class={style.optionReveal}>
<Revealer
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
Advanced settings
</label>
<Expander>
{showAdvanced && (
@@ -278,7 +279,8 @@ export class Options extends Component<Props, State> {
<option value={Csp.kYIQ}>YIQ</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Random matrix
<Checkbox
checked={useRandomMatrix}
onChange={this._inputChange(
@@ -286,7 +288,6 @@ export class Options extends Component<Props, State> {
'boolean',
)}
/>
Random matrix
</label>
</div>
)}

View File

@@ -285,33 +285,33 @@ export class Options extends Component<Props, State> {
</label>
<Expander>
{isWorkerOptions(options) ? (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Premultiply alpha channel
<Checkbox
name="premultiply"
checked={options.premultiply}
onChange={this.onChange}
/>
Premultiply alpha channel
</label>
) : null}
{isWorkerOptions(options) ? (
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Linear RGB
<Checkbox
name="linearRGB"
checked={options.linearRGB}
onChange={this.onChange}
/>
Linear RGB
</label>
) : null}
</Expander>
<label class={style.optionInputFirst}>
<label class={style.optionToggle}>
Maintain aspect ratio
<Checkbox
name="maintainAspect"
checked={maintainAspect}
onChange={linkState(this, 'maintainAspect')}
/>
Maintain aspect ratio
</label>
<Expander>
{maintainAspect ? null : (

View File

@@ -46,7 +46,7 @@ const demos = [
url: logo,
iconUrl: logoIcon,
},
];
] as const;
const blobAnimImport =
!__PRERENDER__ && matchMedia('(prefers-reduced-motion: reduce)').matches

View File

@@ -6,7 +6,7 @@
display: grid;
grid-template-rows: 1fr max-content max-content;
font-size: 1.2rem;
color: var(--dark-text);
color: var(--dim-text);
}
.blob-canvas {

View File

@@ -2,18 +2,19 @@ html {
--pink: #ff3385;
--hot-pink: #ff0066;
--white: #fff;
--black: #000;
--off-black: #1d1d1d;
--blue: #5fb4e4;
--dim-blue: #0a7bcc;
--deep-blue: #09f;
--light-blue: #76c8ff;
--less-light-gray: #bcbcbc;
--medium-light-gray: #d1d1d1;
--light-gray: #eaeaea;
--dark-text: #343a3e;
--dark-gray: #333;
--dim-text: #343a3e;
--dark-text: #142630;
/* Old stuff: */
--gray-dark: rgba(0, 0, 0, 0.8);
--button-fg-color: 95, 180, 228;
--button-fg: rgb(95, 180, 228);
--negative: rgb(207, 113, 127);
--positive: rgb(149, 212, 159);
}