Compare commits
26 Commits
dependabot
...
preprocess
Author | SHA1 | Date | |
---|---|---|---|
97f4de4cc4 | |||
41bb419577 | |||
bf84ebcf91 | |||
2054e7a433 | |||
0802a74602 | |||
8356838a01 | |||
154c1ed5fb | |||
b019687907 | |||
bf8b78657d | |||
6b2e15ab37 | |||
ea170fdc6d | |||
7ab27cfa97 | |||
58a24bd277 | |||
f1b0a8f3e2 | |||
dc49f361ea | |||
0797ce99c0 | |||
a88d562f36 | |||
793c8b2574 | |||
0226b87670 | |||
c8d31321d5 | |||
6d5a743c3f | |||
148fcc308f | |||
002c9d987a | |||
2ddd9ade2d | |||
c53c803baa | |||
edfd353c18 |
@ -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
6
package-lock.json
generated
@ -6036,9 +6036,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.5.5",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.5.tgz",
|
||||
"integrity": "sha512-5ONLNH1SXMzzbQoExZX4TELemNt+TEDb622xXFNfZngjjM9qtrzseJt+EfiUu4TZ6EJ95X5sE1ES4yqHFSIdhg==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.5.7.tgz",
|
||||
"integrity": "sha512-4oEpz75t/0UNcwmcsjk+BIcDdk68oao+7kxcpc1hQPNs2Oo3ZL9xFz8UBf350mxk/VEdD41L5b4l2dE3Ug3RYg==",
|
||||
"dev": true
|
||||
},
|
||||
"preact-render-to-string": {
|
||||
|
@ -35,7 +35,7 @@
|
||||
"postcss-nested": "^4.2.3",
|
||||
"postcss-simple-vars": "^5.0.2",
|
||||
"postcss-url": "^8.0.0",
|
||||
"preact": "^10.5.5",
|
||||
"preact": "^10.5.7",
|
||||
"preact-render-to-string": "^5.1.11",
|
||||
"prettier": "^2.1.2",
|
||||
"pretty-bytes": "^5.4.1",
|
||||
|
37
src/client/lazy-app/Compress/CanvasImage.tsx
Normal file
37
src/client/lazy-app/Compress/CanvasImage.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { h, Component, createRef } from 'preact';
|
||||
import { drawDataToCanvas } from '../util';
|
||||
|
||||
export interface CanvasImageProps
|
||||
extends h.JSX.HTMLAttributes<HTMLCanvasElement> {
|
||||
image?: ImageData;
|
||||
}
|
||||
|
||||
export default class CanvasImage extends Component<CanvasImageProps> {
|
||||
canvas = createRef<HTMLCanvasElement>();
|
||||
componentDidUpdate(prevProps: CanvasImageProps) {
|
||||
if (this.props.image !== prevProps.image) {
|
||||
this.draw(this.props.image);
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
if (this.props.image) {
|
||||
this.draw(this.props.image);
|
||||
}
|
||||
}
|
||||
draw(image?: ImageData) {
|
||||
const canvas = this.canvas.current;
|
||||
if (!canvas) return;
|
||||
if (!image) canvas.getContext('2d');
|
||||
else drawDataToCanvas(canvas, image);
|
||||
}
|
||||
render({ image, ...props }: CanvasImageProps) {
|
||||
return (
|
||||
<canvas
|
||||
ref={this.canvas}
|
||||
width={image?.width}
|
||||
height={image?.height}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
54
src/client/lazy-app/Compress/ClickOutsideDetector.tsx
Normal file
54
src/client/lazy-app/Compress/ClickOutsideDetector.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import {
|
||||
Component,
|
||||
cloneElement,
|
||||
createRef,
|
||||
toChildArray,
|
||||
ComponentChildren,
|
||||
RefObject,
|
||||
} from 'preact';
|
||||
|
||||
interface Props {
|
||||
children: ComponentChildren;
|
||||
onClick?(e: MouseEvent | KeyboardEvent): void;
|
||||
}
|
||||
|
||||
export class ClickOutsideDetector extends Component<Props> {
|
||||
private _roots: RefObject<Element>[] = [];
|
||||
|
||||
private handleClick = (e: MouseEvent) => {
|
||||
let target = e.target as Node;
|
||||
// check if the click came from within any of our child elements:
|
||||
for (const { current: root } of this._roots) {
|
||||
if (root && (root === target || root.contains(target))) return;
|
||||
}
|
||||
const { onClick } = this.props;
|
||||
if (onClick) onClick(e);
|
||||
};
|
||||
|
||||
private handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
const { onClick } = this.props;
|
||||
if (onClick) onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
addEventListener('click', this.handleClick, { passive: true });
|
||||
addEventListener('keydown', this.handleKey, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
removeEventListener('click', this.handleClick);
|
||||
removeEventListener('keydown', this.handleKey);
|
||||
}
|
||||
|
||||
render({ children }: Props) {
|
||||
this._roots = [];
|
||||
return toChildArray(children).map((child) => {
|
||||
if (typeof child !== 'object') return child;
|
||||
const ref = createRef();
|
||||
this._roots.push(ref);
|
||||
return cloneElement(child, { ref });
|
||||
});
|
||||
}
|
||||
}
|
84
src/client/lazy-app/Compress/Flyout/index.tsx
Normal file
84
src/client/lazy-app/Compress/Flyout/index.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { h, cloneElement, Component, VNode, createRef, ComponentChildren, ComponentProps } from "preact";
|
||||
import { ClickOutsideDetector } from "../ClickOutsideDetector";
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
|
||||
type Anchor = 'left' | 'right' | 'top' | 'bottom';
|
||||
|
||||
interface Props extends ComponentProps<'aside'> {
|
||||
showing?: boolean;
|
||||
direction?: 'up' | 'down';
|
||||
anchor?: Anchor | Anchor[];
|
||||
toggle?: VNode;
|
||||
children?: ComponentChildren;
|
||||
}
|
||||
|
||||
interface State {
|
||||
showing: boolean;
|
||||
}
|
||||
|
||||
export default class Flyout extends Component<Props, State> {
|
||||
state = {
|
||||
showing: this.props.showing === true
|
||||
};
|
||||
|
||||
private menu = createRef<HTMLElement>();
|
||||
|
||||
private hide = () => {
|
||||
this.setState({ showing: false });
|
||||
};
|
||||
|
||||
private toggle = () => {
|
||||
this.setState({ showing: !this.state.showing });
|
||||
};
|
||||
|
||||
componentWillReceiveProps({ showing }: Props) {
|
||||
if (showing !== this.props.showing) {
|
||||
this.setState({ showing });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (this.state.showing && !prevState.showing) {
|
||||
const menu = this.menu.current;
|
||||
if (menu) {
|
||||
let toFocus = menu.firstElementChild;
|
||||
for (let child of menu.children) {
|
||||
if (child.hasAttribute('autofocus')) {
|
||||
toFocus = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// @ts-ignore-next
|
||||
if (toFocus) toFocus.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render({ direction, anchor, toggle, children, ...props }: Props, { showing }: State) {
|
||||
const toggleProps = {
|
||||
flyoutOpen: showing,
|
||||
onClick: this.toggle
|
||||
};
|
||||
|
||||
const anchorText = Array.isArray(anchor) ? anchor.join(' ') : anchor;
|
||||
|
||||
return (
|
||||
<span class={style.wrap} data-flyout-open={showing ? '' : undefined}>
|
||||
<ClickOutsideDetector onClick={this.hide}>
|
||||
{toggle && cloneElement(toggle, toggleProps)}
|
||||
|
||||
<aside
|
||||
{...props}
|
||||
ref={this.menu}
|
||||
hidden={!showing}
|
||||
data-anchor={anchorText}
|
||||
data-direction={direction}
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
</ClickOutsideDetector>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
70
src/client/lazy-app/Compress/Flyout/style.css
Normal file
70
src/client/lazy-app/Compress/Flyout/style.css
Normal file
@ -0,0 +1,70 @@
|
||||
.wrap {
|
||||
display: inline;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wrap > aside:last-of-type {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
overflow: visible;
|
||||
outline: none;
|
||||
will-change: transform, opacity;
|
||||
animation: menuOpen 350ms ease forwards 1;
|
||||
--flyout-offset-y: -20px;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* align to the right edge */
|
||||
&[data-anchor*='right'] {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* open to the left */
|
||||
&[data-direction*='left'] {
|
||||
right: 0;
|
||||
left: auto;
|
||||
&[anchor*='right'] {
|
||||
right: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* align to the top edge */
|
||||
&[data-anchor*='top'] {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* open to the left */
|
||||
&[data-direction*='up'] {
|
||||
bottom: 100%;
|
||||
top: auto;
|
||||
flex-direction: column-reverse;
|
||||
--flyout-offset-y: 20px;
|
||||
&[data-anchor*='bottom'] {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@media (min-width: 860px) {
|
||||
flex-direction: column-reverse;
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@keyframes menuOpen {
|
||||
0% {
|
||||
transform: translateY(var(--flyout-offset-y, 0));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
@ -18,5 +18,5 @@
|
||||
}
|
||||
|
||||
.checked {
|
||||
fill: #34b9eb;
|
||||
fill: var(--main-theme-color);
|
||||
}
|
||||
|
@ -28,7 +28,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() {
|
||||
@ -66,13 +66,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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
21
src/client/lazy-app/Compress/Options/Revealer/index.tsx
Normal file
21
src/client/lazy-app/Compress/Options/Revealer/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
29
src/client/lazy-app/Compress/Options/Revealer/style.css
Normal file
29
src/client/lazy-app/Compress/Options/Revealer/style.css
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
@ -25,7 +27,7 @@
|
||||
|
||||
.large {
|
||||
padding: 10px 35px 10px 10px;
|
||||
background: #151515;
|
||||
background: var(--dark-gray);
|
||||
|
||||
& .arrow {
|
||||
right: 13px;
|
||||
|
22
src/client/lazy-app/Compress/Options/Toggle/index.tsx
Normal file
22
src/client/lazy-app/Compress/Options/Toggle/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
55
src/client/lazy-app/Compress/Options/Toggle/style.css
Normal file
55
src/client/lazy-app/Compress/Options/Toggle/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -3,56 +3,73 @@
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
--horizontal-padding: 15px;
|
||||
border-radius: var(--scroller-radius);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { h, createRef, Component, Fragment } from 'preact';
|
||||
import type PinchZoom from './custom-els/PinchZoom';
|
||||
import type { ScaleToOpts } from './custom-els/PinchZoom';
|
||||
import './custom-els/PinchZoom';
|
||||
@ -10,32 +10,36 @@ import {
|
||||
ToggleBackgroundIcon,
|
||||
AddIcon,
|
||||
RemoveIcon,
|
||||
BackIcon,
|
||||
ToggleBackgroundActiveIcon,
|
||||
RotateIcon,
|
||||
MoreIcon,
|
||||
} from '../../icons';
|
||||
import { twoUpHandle } from './custom-els/TwoUp/styles.css';
|
||||
import type { PreprocessorState } from '../../feature-meta';
|
||||
import { cleanSet } from '../../util/clean-modify';
|
||||
import type { SourceImage } from '../../Compress';
|
||||
import { linkRef } from 'shared/prerendered-app/util';
|
||||
import Flyout from '../Flyout';
|
||||
|
||||
interface Props {
|
||||
source?: SourceImage;
|
||||
preprocessorState?: PreprocessorState;
|
||||
hidden?: boolean;
|
||||
mobileView: boolean;
|
||||
leftCompressed?: ImageData;
|
||||
rightCompressed?: ImageData;
|
||||
leftImgContain: boolean;
|
||||
rightImgContain: boolean;
|
||||
onBack: () => void;
|
||||
onPreprocessorChange: (newState: PreprocessorState) => void;
|
||||
onPreprocessorChange?: (newState: PreprocessorState) => void;
|
||||
onShowPreprocessorTransforms?: () => void;
|
||||
onToggleBackground?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
scale: number;
|
||||
editingScale: boolean;
|
||||
altBackground: boolean;
|
||||
transform: boolean;
|
||||
menuOpen: boolean;
|
||||
}
|
||||
|
||||
const scaleToOpts: ScaleToOpts = {
|
||||
@ -50,12 +54,15 @@ export default class Output extends Component<Props, State> {
|
||||
scale: 1,
|
||||
editingScale: false,
|
||||
altBackground: false,
|
||||
transform: false,
|
||||
menuOpen: false,
|
||||
};
|
||||
canvasLeft?: HTMLCanvasElement;
|
||||
canvasRight?: HTMLCanvasElement;
|
||||
pinchZoomLeft?: PinchZoom;
|
||||
pinchZoomRight?: PinchZoom;
|
||||
scaleInput?: HTMLInputElement;
|
||||
flyout = createRef<Flyout>();
|
||||
retargetedEvents = new WeakSet<Event>();
|
||||
|
||||
componentDidMount() {
|
||||
@ -146,12 +153,6 @@ export default class Output extends Component<Props, State> {
|
||||
return props.rightCompressed || (props.source && props.source.preprocessed);
|
||||
}
|
||||
|
||||
private toggleBackground = () => {
|
||||
this.setState({
|
||||
altBackground: !this.state.altBackground,
|
||||
});
|
||||
};
|
||||
|
||||
private zoomIn = () => {
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
this.pinchZoomLeft.scaleTo(this.state.scale * 1.25, scaleToOpts);
|
||||
@ -162,17 +163,36 @@ export default class Output extends Component<Props, State> {
|
||||
this.pinchZoomLeft.scaleTo(this.state.scale / 1.25, scaleToOpts);
|
||||
};
|
||||
|
||||
private onRotateClick = () => {
|
||||
const { preprocessorState: inputProcessorState } = this.props;
|
||||
if (!inputProcessorState) return;
|
||||
|
||||
const newState = cleanSet(
|
||||
inputProcessorState,
|
||||
'rotate.rotate',
|
||||
(inputProcessorState.rotate.rotate + 90) % 360,
|
||||
private fitToViewport = () => {
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
const img = this.props.source?.preprocessed;
|
||||
if (!img) return;
|
||||
const scale = Number(
|
||||
Math.min(
|
||||
(window.innerWidth - 20) / img.width,
|
||||
(window.innerHeight - 20) / img.height,
|
||||
).toFixed(2),
|
||||
);
|
||||
this.pinchZoomLeft.scaleTo(Number(scale.toFixed(2)), scaleToOpts);
|
||||
this.recenter();
|
||||
// this.hideMenu();
|
||||
};
|
||||
|
||||
this.props.onPreprocessorChange(newState);
|
||||
private zoomTo2x = () => {
|
||||
if (!this.pinchZoomLeft) throw Error('Missing pinch-zoom element');
|
||||
this.pinchZoomLeft.scaleTo(0.5, scaleToOpts);
|
||||
this.recenter();
|
||||
};
|
||||
|
||||
private recenter = () => {
|
||||
const img = this.props.source?.preprocessed;
|
||||
if (!img || !this.pinchZoomLeft) return;
|
||||
let scale = this.pinchZoomLeft.scale;
|
||||
this.pinchZoomLeft.setTransform({
|
||||
x: (img.width - img.width * scale) / 2,
|
||||
y: (img.height - img.height * scale) / 2,
|
||||
allowChangeEvent: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onScaleValueFocus = () => {
|
||||
@ -255,8 +275,16 @@ export default class Output extends Component<Props, State> {
|
||||
};
|
||||
|
||||
render(
|
||||
{ mobileView, leftImgContain, rightImgContain, source, onBack }: Props,
|
||||
{ scale, editingScale, altBackground }: State,
|
||||
{
|
||||
source,
|
||||
mobileView,
|
||||
hidden,
|
||||
leftImgContain,
|
||||
rightImgContain,
|
||||
onShowPreprocessorTransforms,
|
||||
onToggleBackground,
|
||||
}: Props,
|
||||
{ scale, editingScale }: State,
|
||||
) {
|
||||
const leftDraw = this.leftDrawable();
|
||||
const rightDraw = this.rightDrawable();
|
||||
@ -264,63 +292,60 @@ export default class Output extends Component<Props, State> {
|
||||
const originalImage = source && source.preprocessed;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`${style.output} ${altBackground ? style.altBackground : ''}`}
|
||||
>
|
||||
<two-up
|
||||
legacy-clip-compat
|
||||
class={style.twoUp}
|
||||
orientation={mobileView ? 'vertical' : 'horizontal'}
|
||||
// Event redirecting. See onRetargetableEvent.
|
||||
onTouchStartCapture={this.onRetargetableEvent}
|
||||
onTouchEndCapture={this.onRetargetableEvent}
|
||||
onTouchMoveCapture={this.onRetargetableEvent}
|
||||
onPointerDownCapture={this.onRetargetableEvent}
|
||||
onMouseDownCapture={this.onRetargetableEvent}
|
||||
onWheelCapture={this.onRetargetableEvent}
|
||||
>
|
||||
<pinch-zoom
|
||||
class={style.pinchZoom}
|
||||
onChange={this.onPinchZoomLeftChange}
|
||||
ref={linkRef(this, 'pinchZoomLeft')}
|
||||
<Fragment>
|
||||
<div class={style.output} hidden={hidden}>
|
||||
<two-up
|
||||
legacy-clip-compat
|
||||
class={style.twoUp}
|
||||
orientation={mobileView ? 'vertical' : 'horizontal'}
|
||||
// Event redirecting. See onRetargetableEvent.
|
||||
onTouchStartCapture={this.onRetargetableEvent}
|
||||
onTouchEndCapture={this.onRetargetableEvent}
|
||||
onTouchMoveCapture={this.onRetargetableEvent}
|
||||
onPointerDownCapture={this.onRetargetableEvent}
|
||||
onMouseDownCapture={this.onRetargetableEvent}
|
||||
onWheelCapture={this.onRetargetableEvent}
|
||||
>
|
||||
<canvas
|
||||
class={style.pinchTarget}
|
||||
ref={linkRef(this, 'canvasLeft')}
|
||||
width={leftDraw && leftDraw.width}
|
||||
height={leftDraw && leftDraw.height}
|
||||
style={{
|
||||
width: originalImage ? originalImage.width : '',
|
||||
height: originalImage ? originalImage.height : '',
|
||||
objectFit: leftImgContain ? 'contain' : '',
|
||||
}}
|
||||
/>
|
||||
</pinch-zoom>
|
||||
<pinch-zoom
|
||||
class={style.pinchZoom}
|
||||
ref={linkRef(this, 'pinchZoomRight')}
|
||||
>
|
||||
<canvas
|
||||
class={style.pinchTarget}
|
||||
ref={linkRef(this, 'canvasRight')}
|
||||
width={rightDraw && rightDraw.width}
|
||||
height={rightDraw && rightDraw.height}
|
||||
style={{
|
||||
width: originalImage ? originalImage.width : '',
|
||||
height: originalImage ? originalImage.height : '',
|
||||
objectFit: rightImgContain ? 'contain' : '',
|
||||
}}
|
||||
/>
|
||||
</pinch-zoom>
|
||||
</two-up>
|
||||
|
||||
<div class={style.back}>
|
||||
<button class={style.button} onClick={onBack}>
|
||||
<BackIcon />
|
||||
</button>
|
||||
<pinch-zoom
|
||||
class={style.pinchZoom}
|
||||
onChange={this.onPinchZoomLeftChange}
|
||||
ref={linkRef(this, 'pinchZoomLeft')}
|
||||
>
|
||||
<canvas
|
||||
class={style.pinchTarget}
|
||||
ref={linkRef(this, 'canvasLeft')}
|
||||
width={leftDraw && leftDraw.width}
|
||||
height={leftDraw && leftDraw.height}
|
||||
style={{
|
||||
width: originalImage ? originalImage.width : '',
|
||||
height: originalImage ? originalImage.height : '',
|
||||
objectFit: leftImgContain ? 'contain' : undefined,
|
||||
}}
|
||||
/>
|
||||
</pinch-zoom>
|
||||
<pinch-zoom
|
||||
class={style.pinchZoom}
|
||||
ref={linkRef(this, 'pinchZoomRight')}
|
||||
>
|
||||
<canvas
|
||||
class={style.pinchTarget}
|
||||
ref={linkRef(this, 'canvasRight')}
|
||||
width={rightDraw && rightDraw.width}
|
||||
height={rightDraw && rightDraw.height}
|
||||
style={{
|
||||
width: originalImage ? originalImage.width : '',
|
||||
height: originalImage ? originalImage.height : '',
|
||||
objectFit: rightImgContain ? 'contain' : undefined,
|
||||
}}
|
||||
/>
|
||||
</pinch-zoom>
|
||||
</two-up>
|
||||
</div>
|
||||
|
||||
<div class={style.controls}>
|
||||
<div
|
||||
class={style.controls}
|
||||
hidden={hidden}
|
||||
>
|
||||
<div class={style.zoomControls}>
|
||||
<button class={style.button} onClick={this.zoomOut}>
|
||||
<RemoveIcon />
|
||||
@ -349,29 +374,39 @@ export default class Output extends Component<Props, State> {
|
||||
<button class={style.button} onClick={this.zoomIn}>
|
||||
<AddIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div class={style.buttonsNoWrap}>
|
||||
<button
|
||||
class={style.button}
|
||||
onClick={this.onRotateClick}
|
||||
title="Rotate image"
|
||||
|
||||
<Flyout
|
||||
class={style.menu}
|
||||
showing={hidden ? false : undefined}
|
||||
anchor="right"
|
||||
direction={mobileView ? 'down' : 'up'}
|
||||
toggle={
|
||||
<button class={`${style.button} ${style.moreButton}`}>
|
||||
<MoreIcon />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<RotateIcon />
|
||||
</button>
|
||||
<button
|
||||
class={`${style.button} ${altBackground ? style.active : ''}`}
|
||||
onClick={this.toggleBackground}
|
||||
title="Change canvas color"
|
||||
>
|
||||
{altBackground ? (
|
||||
<ToggleBackgroundActiveIcon />
|
||||
) : (
|
||||
<button class={style.button} onClick={onShowPreprocessorTransforms}>
|
||||
<RotateIcon /> Rotate & Transform
|
||||
</button>
|
||||
<button class={style.button} onClick={this.fitToViewport}>
|
||||
Fit to viewport
|
||||
</button>
|
||||
<button class={style.button} onClick={this.zoomTo2x}>
|
||||
Simulate retina
|
||||
</button>
|
||||
<button class={style.button} onClick={this.recenter}>
|
||||
Re-center
|
||||
</button>
|
||||
<button class={style.button} onClick={onToggleBackground}>
|
||||
<ToggleBackgroundIcon />
|
||||
)}
|
||||
</button>
|
||||
{' '}
|
||||
Change canvas color
|
||||
</button>
|
||||
</Flyout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,8 @@
|
||||
.output {
|
||||
composes: abs-fill from global;
|
||||
display: contents;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease;
|
||||
}
|
||||
|
||||
&.alt-background::before {
|
||||
opacity: 0.6;
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,42 +29,55 @@
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 9px 84px;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
grid-area: header;
|
||||
align-self: center;
|
||||
padding: 9px 66px;
|
||||
/* Had to disable containment because of the overflow menu. */
|
||||
/*
|
||||
contain: content;
|
||||
overflow: hidden;
|
||||
*/
|
||||
transition: transform 500ms ease;
|
||||
|
||||
/* Allow clicks to fall through to the pinch zoom area */
|
||||
pointer-events: none;
|
||||
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
padding: 9px;
|
||||
top: auto;
|
||||
left: 320px;
|
||||
right: 320px;
|
||||
bottom: 0;
|
||||
flex-wrap: wrap-reverse;
|
||||
grid-area: viewportOpts;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
&[hidden] {
|
||||
visibility: visible;
|
||||
transform: translateY(-200%);
|
||||
|
||||
@media (min-width: 860px) {
|
||||
transform: translateY(200%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
|
||||
& :not(:first-child) {
|
||||
& > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
& :not(:last-child) {
|
||||
|
||||
& > :not(:nth-last-child(2)) {
|
||||
margin-right: 0;
|
||||
border-right-width: 0;
|
||||
border-top-right-radius: 0;
|
||||
@ -90,72 +91,72 @@
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
margin: 4px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
line-height: 1;
|
||||
background-color: rgba(29, 29, 29, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.67);
|
||||
border-radius: 6px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
height: 36px;
|
||||
height: 39px;
|
||||
padding: 0 8px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: 600px) {
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--button-fg);
|
||||
/* box-shadow: 0 0 0 2px var(--hot-pink); */
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--button-fg);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
background: rgba(50, 50, 50, 0.92);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #34b9eb;
|
||||
background: rgba(72, 72, 72, 0.92);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #32a3ce;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zoom {
|
||||
color: #625e80;
|
||||
cursor: text;
|
||||
width: 6em;
|
||||
width: 7rem;
|
||||
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);
|
||||
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2), 0 0 0 2px #fff;
|
||||
}
|
||||
}
|
||||
span.zoom {
|
||||
color: #939393;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 100;
|
||||
}
|
||||
input.zoom {
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
text-indent: 3px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.zoom-value {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin: 0 3px 0 0;
|
||||
color: #888;
|
||||
padding: 0 2px;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
border-bottom: 1px dashed #999;
|
||||
}
|
||||
|
||||
.back {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.buttons-no-wrap {
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
@ -164,3 +165,64 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/** Three-dot menu */
|
||||
.moreButton {
|
||||
padding: 0 4px;
|
||||
|
||||
& > svg {
|
||||
transform-origin: center;
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
[data-flyout-open] {
|
||||
.moreButton {
|
||||
background: rgba(82, 82, 82, 0.92);
|
||||
|
||||
& > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(50, 50, 50, 0.4);
|
||||
backdrop-filter: blur(2px) contrast(70%);
|
||||
animation: menuShimFadeIn 350ms ease forwards 1;
|
||||
will-change: opacity;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menuShimFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
button {
|
||||
margin: 8px 0;
|
||||
border-radius: 2rem;
|
||||
padding: 0 16px;
|
||||
|
||||
& > svg {
|
||||
position: relative;
|
||||
left: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
margin: 8px 4px;
|
||||
padding: 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
388
src/client/lazy-app/Compress/Transform/Cropper/index.tsx
Normal file
388
src/client/lazy-app/Compress/Transform/Cropper/index.tsx
Normal file
@ -0,0 +1,388 @@
|
||||
import { h, Component, ComponentChildren } from 'preact';
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import { shallowEqual } from 'client/lazy-app/util';
|
||||
|
||||
export interface CropBox {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
// Minimum CropBox size
|
||||
const MIN_SIZE = 2;
|
||||
|
||||
export interface Props {
|
||||
size: { width: number; height: number };
|
||||
scale?: number;
|
||||
lockAspect?: boolean;
|
||||
crop: CropBox;
|
||||
onChange?(crop: CropBox): void;
|
||||
}
|
||||
|
||||
type Edge = keyof CropBox;
|
||||
|
||||
interface PointerTrack {
|
||||
x: number;
|
||||
y: number;
|
||||
edges: { edge: Edge; value: number }[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
crop: CropBox;
|
||||
pan: boolean;
|
||||
}
|
||||
|
||||
export default class Cropper extends Component<Props, State> {
|
||||
private pointers = new Map<number, PointerTrack>();
|
||||
|
||||
state = {
|
||||
crop: this.normalizeCrop({ ...this.props.crop }),
|
||||
pan: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
||||
if (!shallowEqual(nextState, this.state)) return true;
|
||||
const { size, scale, lockAspect, crop } = this.props;
|
||||
return (
|
||||
size.width !== nextProps.size.width ||
|
||||
size.height !== nextProps.size.height ||
|
||||
scale !== nextProps.scale ||
|
||||
lockAspect !== nextProps.lockAspect ||
|
||||
!shallowEqual(crop, nextProps.crop)
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ crop }: Props, nextState: State) {
|
||||
const current = nextState.crop || this.state.crop;
|
||||
if (crop !== this.props.crop && !shallowEqual(crop, current)) {
|
||||
// this.setState({ crop: nextProps.crop });
|
||||
this.setCrop(crop);
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeCrop(crop: CropBox) {
|
||||
crop.left = Math.round(Math.max(0, crop.left));
|
||||
crop.top = Math.round(Math.max(0, crop.top));
|
||||
crop.right = Math.round(Math.max(0, crop.right));
|
||||
crop.bottom = Math.round(Math.max(0, crop.bottom));
|
||||
return crop;
|
||||
}
|
||||
|
||||
private setCrop(cropUpdate: Partial<CropBox>) {
|
||||
const crop = this.normalizeCrop({ ...this.state.crop, ...cropUpdate });
|
||||
// ignore crop updates that normalize to the same values
|
||||
const old = this.state.crop;
|
||||
if (
|
||||
crop.left === old.left &&
|
||||
crop.right === old.right &&
|
||||
crop.top === old.top &&
|
||||
crop.bottom === old.bottom
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// crop.left = Math.max(0, crop.left) | 0;
|
||||
// crop.top = Math.max(0, crop.top) | 0;
|
||||
// crop.right = Math.max(0, crop.right) | 0;
|
||||
// crop.bottom = Math.max(0, crop.bottom) | 0;
|
||||
this.setState({ crop });
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(crop);
|
||||
}
|
||||
}
|
||||
|
||||
private onPointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0 || this.state.pan) return;
|
||||
|
||||
const target = event.target as SVGElement;
|
||||
const edgeAttr = target.getAttribute('data-edge');
|
||||
if (edgeAttr) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const edges = edgeAttr.split(/ *, */) as Edge[];
|
||||
// console.log(this.props.lockAspect);
|
||||
if (this.props.lockAspect && edges.length === 1) return;
|
||||
|
||||
this.pointers.set(event.pointerId, {
|
||||
x: event.x,
|
||||
y: event.y,
|
||||
edges: edges.map((edge) => ({ edge, value: this.state.crop[edge] })),
|
||||
});
|
||||
target.setPointerCapture(event.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
private onPointerMove = (event: PointerEvent) => {
|
||||
const target = event.target as SVGElement;
|
||||
const down = this.pointers.get(event.pointerId);
|
||||
if (down && target.hasPointerCapture(event.pointerId)) {
|
||||
const { size } = this.props;
|
||||
const oldCrop = this.state.crop;
|
||||
const aspect =
|
||||
(size.width - oldCrop.left - oldCrop.right) /
|
||||
(size.height - oldCrop.top - oldCrop.bottom);
|
||||
const scale = this.props.scale || 1;
|
||||
let dx = (event.x - down.x) / scale;
|
||||
let dy = (event.y - down.y) / scale;
|
||||
// console.log(this.props.lockAspect, aspect);
|
||||
if (this.props.lockAspect) {
|
||||
const dir = (dx + dy) / 2;
|
||||
dx = dir * aspect;
|
||||
dy = dir / aspect;
|
||||
}
|
||||
const crop: Partial<CropBox> = {};
|
||||
for (const { edge, value } of down.edges) {
|
||||
let edgeValue = value;
|
||||
switch (edge) {
|
||||
case 'left':
|
||||
edgeValue += dx;
|
||||
break;
|
||||
case 'right':
|
||||
edgeValue -= dx;
|
||||
break;
|
||||
case 'top':
|
||||
edgeValue += dy;
|
||||
break;
|
||||
case 'bottom':
|
||||
edgeValue -= dy;
|
||||
break;
|
||||
}
|
||||
crop[edge] = edgeValue;
|
||||
}
|
||||
// Prevent MOVE from resizing the cropbox:
|
||||
if (crop.left && crop.right) {
|
||||
if (crop.left < 0) crop.right += crop.left;
|
||||
if (crop.right < 0) crop.left += crop.right;
|
||||
} else {
|
||||
// enforce minimum 1px cropbox width
|
||||
if (crop.left)
|
||||
crop.left = Math.min(
|
||||
crop.left,
|
||||
size.width - oldCrop.right - MIN_SIZE,
|
||||
);
|
||||
if (crop.right)
|
||||
crop.right = Math.min(
|
||||
crop.right,
|
||||
size.width - oldCrop.left - MIN_SIZE,
|
||||
);
|
||||
}
|
||||
if (crop.top && crop.bottom) {
|
||||
if (crop.top < 0) crop.bottom += crop.top;
|
||||
if (crop.bottom < 0) crop.top += crop.bottom;
|
||||
} else {
|
||||
// enforce minimum 1px cropbox height
|
||||
if (crop.top)
|
||||
crop.top = Math.min(
|
||||
crop.top,
|
||||
size.height - oldCrop.bottom - MIN_SIZE,
|
||||
);
|
||||
if (crop.bottom)
|
||||
crop.bottom = Math.min(
|
||||
crop.bottom,
|
||||
size.height - oldCrop.top - MIN_SIZE,
|
||||
);
|
||||
}
|
||||
this.setCrop(crop);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onPointerUp = (event: PointerEvent) => {
|
||||
const target = event.target as SVGElement;
|
||||
const down = this.pointers.get(event.pointerId);
|
||||
if (down && target.hasPointerCapture(event.pointerId)) {
|
||||
this.onPointerMove(event);
|
||||
target.releasePointerCapture(event.pointerId);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.pointers.delete(event.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === ' ') {
|
||||
if (!this.state.pan) {
|
||||
this.setState({ pan: true });
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.key === ' ') this.setState({ pan: false });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
addEventListener('keydown', this.onKeyDown);
|
||||
addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
addEventListener('keydown', this.onKeyDown);
|
||||
addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
render({ size, scale }: Props, { crop, pan }: State) {
|
||||
const x = crop.left;
|
||||
const y = crop.top;
|
||||
const width = size.width - crop.left - crop.right;
|
||||
const height = size.height - crop.top - crop.bottom;
|
||||
// const x = crop.left.toFixed(2);
|
||||
// const y = crop.top.toFixed(2);
|
||||
// const width = (size.width - crop.left - crop.right).toFixed(2);
|
||||
// const height = (size.height - crop.top - crop.bottom).toFixed(2);
|
||||
|
||||
return (
|
||||
<svg
|
||||
class={`${style.cropper} ${pan ? style.pan : ''}`}
|
||||
width={size.width + 20}
|
||||
height={size.height + 20}
|
||||
viewBox={`-10 -10 ${size.width + 20} ${size.height + 20}`}
|
||||
style={{
|
||||
// this is hack to force style invalidation in Chrome
|
||||
zoom: (scale || 1).toFixed(3),
|
||||
}}
|
||||
onPointerDown={this.onPointerDown}
|
||||
onPointerMove={this.onPointerMove}
|
||||
onPointerUp={this.onPointerUp}
|
||||
>
|
||||
<defs>
|
||||
{/*
|
||||
<clipPath id="bg">
|
||||
<rect x={x} y={y} width={width} height={height} />
|
||||
</clipPath>
|
||||
*/}
|
||||
{/*
|
||||
<filter id="shadow" x="-2" y="-2" width="4" height="4">
|
||||
<feDropShadow
|
||||
dx="0"
|
||||
dy="0.5"
|
||||
stdDeviation="1.5"
|
||||
flood-color="#000"
|
||||
/>
|
||||
</filter>
|
||||
<filter id="shadow2" x="-2" y="-2" width="4" height="4">
|
||||
<feDropShadow
|
||||
dx="0"
|
||||
dy="0.25"
|
||||
stdDeviation="0.5"
|
||||
flood-color="rgba(0,0,0,0.5)"
|
||||
/>
|
||||
</filter>
|
||||
*/}
|
||||
</defs>
|
||||
<rect
|
||||
class={style.background}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
// mask="url(#bg)"
|
||||
// clip-path="url(#bg)"
|
||||
// style={{
|
||||
// clipPath: `polygon(0 0, 0 100%, 100% 100%, 100% 0, 0 0, ${x}px ${y}px, ${x+width}px ${y}px, ${x+width}px ${y+height}px, ${x}px ${y+height}px, ${x}px ${y}px)`
|
||||
// }}
|
||||
clip-path={`polygon(0 0, 0 100%, 100% 100%, 100% 0, 0 0, ${x}px ${y}px, ${
|
||||
x + width
|
||||
}px ${y}px, ${x + width}px ${y + height}px, ${x}px ${
|
||||
y + height
|
||||
}px, ${x}px ${y}px)`}
|
||||
/>
|
||||
<svg x={x} y={y} width={width} height={height}>
|
||||
<Freezer>
|
||||
<rect
|
||||
id="box"
|
||||
class={style.cropbox}
|
||||
data-edge="left,right,top,bottom"
|
||||
width="100%"
|
||||
height="100%"
|
||||
// filter="url(#shadow2)"
|
||||
/>
|
||||
|
||||
<rect class={style.edge} data-edge="top" width="100%" />
|
||||
<rect class={style.edge} data-edge="bottom" width="100%" y="100%" />
|
||||
<rect class={style.edge} data-edge="left" height="100%" />
|
||||
<rect class={style.edge} data-edge="right" height="100%" x="100%" />
|
||||
|
||||
<circle
|
||||
class={style.corner}
|
||||
data-edge="left,top"
|
||||
// filter="url(#shadow)"
|
||||
/>
|
||||
<circle
|
||||
class={style.corner}
|
||||
data-edge="right,top"
|
||||
cx="100%"
|
||||
// filter="url(#shadow)"
|
||||
/>
|
||||
<circle
|
||||
class={style.corner}
|
||||
data-edge="right,bottom"
|
||||
cx="100%"
|
||||
cy="100%"
|
||||
// filter="url(#shadow)"
|
||||
/>
|
||||
<circle
|
||||
class={style.corner}
|
||||
data-edge="left,bottom"
|
||||
cy="100%"
|
||||
// filter="url(#shadow)"
|
||||
/>
|
||||
</Freezer>
|
||||
</svg>
|
||||
{/*
|
||||
<rect
|
||||
id="box"
|
||||
class={style.cropbox}
|
||||
data-edge="left,right,top,bottom"
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<rect
|
||||
class={`${style.edge} ${style.top}`}
|
||||
data-edge="top"
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
/>
|
||||
<rect
|
||||
class={`${style.edge} ${style.bottom}`}
|
||||
data-edge="bottom"
|
||||
x={x}
|
||||
y={size.height - crop.bottom}
|
||||
width={width}
|
||||
/>
|
||||
<rect
|
||||
class={`${style.edge} ${style.left}`}
|
||||
data-edge="left"
|
||||
x={x}
|
||||
y={y}
|
||||
height={height}
|
||||
/>
|
||||
<rect
|
||||
class={`${style.edge} ${style.right}`}
|
||||
data-edge="right"
|
||||
x={size.width - crop.right}
|
||||
y={y}
|
||||
height={height}
|
||||
/>
|
||||
*/}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface FreezerProps {
|
||||
children: ComponentChildren;
|
||||
}
|
||||
class Freezer extends Component<FreezerProps> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
render({ children }: FreezerProps) {
|
||||
return children;
|
||||
}
|
||||
}
|
119
src/client/lazy-app/Compress/Transform/Cropper/style.css
Normal file
119
src/client/lazy-app/Compress/Transform/Cropper/style.css
Normal file
@ -0,0 +1,119 @@
|
||||
.cropper {
|
||||
position: absolute;
|
||||
left: calc(-10px / var(--scale, 1));
|
||||
top: calc(-10px / var(--scale, 1));
|
||||
right: calc(-10px / var(--scale, 1));
|
||||
bottom: calc(-10px / var(--scale, 1));
|
||||
shape-rendering: crispedges;
|
||||
overflow: hidden;
|
||||
contain: strict;
|
||||
transform-origin: 0 0;
|
||||
transform: scale(calc(1 / var(--scale))) !important;
|
||||
zoom: var(--scale, 1);
|
||||
|
||||
&.pan {
|
||||
cursor: grabbing;
|
||||
& * {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > svg {
|
||||
margin: -10px;
|
||||
padding: 10px;
|
||||
overflow: visible;
|
||||
contain: strict;
|
||||
/* overflow: visible; */
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
pointer-events: none;
|
||||
fill: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.cropbox {
|
||||
fill: none;
|
||||
stroke: white;
|
||||
stroke-width: calc(1.5 / var(--scale, 1));
|
||||
stroke-dasharray: calc(5 / var(--scale, 1)), calc(5 / var(--scale, 1));
|
||||
stroke-dashoffset: 50%;
|
||||
/* Accept pointer input even though this is unpainted transparent */
|
||||
pointer-events: all;
|
||||
cursor: move;
|
||||
|
||||
/* animation: ants 1s linear forwards infinite; */
|
||||
}
|
||||
/*
|
||||
@keyframes ants {
|
||||
0% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: -12; }
|
||||
}
|
||||
*/
|
||||
|
||||
.edge {
|
||||
fill: #aaa;
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease;
|
||||
z-index: 2;
|
||||
pointer-events: all;
|
||||
--edge-width: calc(10px / var(--scale, 1));
|
||||
|
||||
@media (max-width: 779px) {
|
||||
--edge-width: calc(20px / var(--scale, 1));
|
||||
fill: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
|
||||
&[data-edge='left'],
|
||||
&[data-edge='right'] {
|
||||
cursor: ew-resize;
|
||||
transform: translate(calc(var(--edge-width, 10px) / -2), 0);
|
||||
width: var(--edge-width, 10px);
|
||||
}
|
||||
&[data-edge='top'],
|
||||
&[data-edge='bottom'] {
|
||||
cursor: ns-resize;
|
||||
transform: translate(0, calc(var(--edge-width, 10px) / -2));
|
||||
height: var(--edge-width, 10px);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
opacity: 0.1;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.corner {
|
||||
r: calc(4 / var(--scale, 1));
|
||||
stroke-width: calc(4 / var(--scale, 1));
|
||||
stroke: rgba(225, 225, 225, 0.01);
|
||||
fill: white;
|
||||
shape-rendering: geometricprecision;
|
||||
pointer-events: all;
|
||||
transition: fill 250ms ease, stroke 250ms ease;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
stroke: rgba(225, 225, 225, 0.5);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@media (max-width: 779px) {
|
||||
r: calc(10 / var(--scale, 1));
|
||||
stroke-width: calc(2 / var(--scale, 1));
|
||||
}
|
||||
|
||||
&[data-edge='left,top'] {
|
||||
cursor: nw-resize;
|
||||
}
|
||||
&[data-edge='right,top'] {
|
||||
cursor: ne-resize;
|
||||
}
|
||||
&[data-edge='right,bottom'] {
|
||||
cursor: se-resize;
|
||||
}
|
||||
&[data-edge='left,bottom'] {
|
||||
cursor: sw-resize;
|
||||
}
|
||||
}
|
617
src/client/lazy-app/Compress/Transform/index.tsx
Normal file
617
src/client/lazy-app/Compress/Transform/index.tsx
Normal file
@ -0,0 +1,617 @@
|
||||
import {
|
||||
h,
|
||||
Component,
|
||||
Fragment,
|
||||
createRef,
|
||||
FunctionComponent,
|
||||
ComponentChildren,
|
||||
} from 'preact';
|
||||
import type {
|
||||
default as PinchZoom,
|
||||
ScaleToOpts,
|
||||
} from '../Output/custom-els/PinchZoom';
|
||||
import '../Output/custom-els/PinchZoom';
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import {
|
||||
AddIcon,
|
||||
CheckmarkIcon,
|
||||
CompareIcon,
|
||||
FlipHorizontallyIcon,
|
||||
FlipVerticallyIcon,
|
||||
RemoveIcon,
|
||||
RotateClockwiseIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
SwapIcon,
|
||||
} from '../../icons';
|
||||
import { cleanSet } from '../../util/clean-modify';
|
||||
import type { SourceImage } from '../../Compress';
|
||||
import { PreprocessorState } from 'client/lazy-app/feature-meta';
|
||||
import Cropper, { CropBox } from './Cropper';
|
||||
import CanvasImage from '../CanvasImage';
|
||||
import Expander from '../Options/Expander';
|
||||
import Select from '../Options/Select';
|
||||
import Checkbox from '../Options/Checkbox';
|
||||
|
||||
const ROTATE_ORIENTATIONS = [0, 90, 180, 270] as const;
|
||||
|
||||
const cropPresets = {
|
||||
square: {
|
||||
name: 'Square',
|
||||
ratio: 1,
|
||||
},
|
||||
'4:3': {
|
||||
name: '4:3',
|
||||
ratio: 4 / 3,
|
||||
},
|
||||
'16:9': {
|
||||
name: '16:9',
|
||||
ratio: 16 / 9,
|
||||
},
|
||||
'16:10': {
|
||||
name: '16:10',
|
||||
ratio: 16 / 10,
|
||||
},
|
||||
};
|
||||
|
||||
type CropPresetId = keyof typeof cropPresets;
|
||||
|
||||
interface Props {
|
||||
source: SourceImage;
|
||||
preprocessorState: PreprocessorState;
|
||||
mobileView: boolean;
|
||||
onCancel?(): void;
|
||||
onSave?(e: { preprocessorState: PreprocessorState }): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
scale: number;
|
||||
editingScale: boolean;
|
||||
rotate: typeof ROTATE_ORIENTATIONS[number];
|
||||
// crop: false | CropBox;
|
||||
crop: CropBox;
|
||||
cropPreset: keyof typeof cropPresets | undefined;
|
||||
lockAspect: boolean;
|
||||
flip: PreprocessorState['flip'];
|
||||
}
|
||||
|
||||
const scaleToOpts: ScaleToOpts = {
|
||||
originX: '50%',
|
||||
originY: '50%',
|
||||
relativeTo: 'container',
|
||||
allowChangeEvent: true,
|
||||
};
|
||||
|
||||
export default class Transform extends Component<Props, State> {
|
||||
state: State = {
|
||||
scale: 1,
|
||||
editingScale: false,
|
||||
cropPreset: undefined,
|
||||
lockAspect: false,
|
||||
...this.fromPreprocessorState(this.props.preprocessorState),
|
||||
};
|
||||
pinchZoom = createRef<PinchZoom>();
|
||||
scaleInput = createRef<HTMLInputElement>();
|
||||
|
||||
// static getDerivedStateFromProps({ source, preprocessorState }: Props) {
|
||||
// return {
|
||||
// rotate: preprocessorState.rotate.rotate || 0,
|
||||
// crop: preprocessorState.crop || false,
|
||||
// flip: preprocessorState.flip || { horizontal: false, vertical: false },
|
||||
// };
|
||||
// }
|
||||
|
||||
componentWillReceiveProps(
|
||||
{ source, preprocessorState }: Props,
|
||||
{ crop, cropPreset }: State,
|
||||
) {
|
||||
if (preprocessorState !== this.props.preprocessorState) {
|
||||
this.setState(this.fromPreprocessorState(preprocessorState));
|
||||
}
|
||||
const { width, height } = source.decoded;
|
||||
const cropWidth = width - crop.left - crop.right;
|
||||
const cropHeight = height - crop.top - crop.bottom;
|
||||
for (const [id, preset] of Object.entries(cropPresets)) {
|
||||
if (cropHeight * preset.ratio === cropWidth) {
|
||||
if (cropPreset !== id) {
|
||||
this.setState({ cropPreset: id as CropPresetId });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fromPreprocessorState(preprocessorState?: PreprocessorState) {
|
||||
const state: Pick<State, 'rotate' | 'crop' | 'flip'> = {
|
||||
rotate: preprocessorState ? preprocessorState.rotate.rotate : 0,
|
||||
crop: Object.assign(
|
||||
{
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
(preprocessorState && preprocessorState.crop) || {},
|
||||
),
|
||||
flip: Object.assign(
|
||||
{
|
||||
horizontal: false,
|
||||
vertical: false,
|
||||
},
|
||||
(preprocessorState && preprocessorState.flip) || {},
|
||||
),
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
private save = () => {
|
||||
const { preprocessorState, onSave } = this.props;
|
||||
const { rotate, crop, flip } = this.state;
|
||||
|
||||
let newState = cleanSet(preprocessorState, 'rotate.rotate', rotate);
|
||||
newState = cleanSet(newState, 'crop', crop);
|
||||
newState = cleanSet(newState, 'flip', flip);
|
||||
|
||||
console.log('u', JSON.parse(JSON.stringify(newState)));
|
||||
|
||||
if (onSave) onSave({ preprocessorState: newState });
|
||||
};
|
||||
|
||||
private cancel = () => {
|
||||
const { onCancel, onSave } = this.props;
|
||||
if (onCancel) onCancel();
|
||||
else if (onSave)
|
||||
onSave({ preprocessorState: this.props.preprocessorState });
|
||||
};
|
||||
|
||||
// private fitToViewport = () => {
|
||||
// const pinchZoom = this.pinchZoom.current;
|
||||
// const img = this.props.source?.preprocessed;
|
||||
// if (!img || !pinchZoom) return;
|
||||
// const scale = Number(Math.min(
|
||||
// (window.innerWidth - 20) / img.width,
|
||||
// (window.innerHeight - 20) / img.height
|
||||
// ).toFixed(2));
|
||||
// pinchZoom.scaleTo(Number(scale.toFixed(2)), { allowChangeEvent: true });
|
||||
// this.recenter();
|
||||
// };
|
||||
|
||||
// private recenter = () => {
|
||||
// const pinchZoom = this.pinchZoom.current;
|
||||
// const img = this.props.source?.preprocessed;
|
||||
// if (!img || !pinchZoom) return;
|
||||
// pinchZoom.setTransform({
|
||||
// x: (img.width - img.width * pinchZoom.scale) / 2,
|
||||
// y: (img.height - img.height * pinchZoom.scale) / 2,
|
||||
// allowChangeEvent: true
|
||||
// });
|
||||
// };
|
||||
|
||||
private zoomIn = () => {
|
||||
if (!this.pinchZoom.current) throw Error('Missing pinch-zoom element');
|
||||
this.pinchZoom.current.scaleTo(this.state.scale * 1.25, scaleToOpts);
|
||||
};
|
||||
|
||||
private zoomOut = () => {
|
||||
if (!this.pinchZoom.current) throw Error('Missing pinch-zoom element');
|
||||
this.pinchZoom.current.scaleTo(this.state.scale / 1.25, scaleToOpts);
|
||||
};
|
||||
|
||||
private onScaleValueFocus = () => {
|
||||
this.setState({ editingScale: true }, () => {
|
||||
if (this.scaleInput.current) {
|
||||
// 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.current).transform;
|
||||
this.scaleInput.current.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
private onScaleInputBlur = () => {
|
||||
this.setState({ editingScale: false });
|
||||
};
|
||||
|
||||
private onScaleInputChanged = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const percent = parseFloat(target.value);
|
||||
if (isNaN(percent)) return;
|
||||
if (!this.pinchZoom.current) throw Error('Missing pinch-zoom element');
|
||||
|
||||
this.pinchZoom.current.scaleTo(percent / 100, scaleToOpts);
|
||||
};
|
||||
|
||||
private onPinchZoomChange = () => {
|
||||
if (!this.pinchZoom.current) throw Error('Missing pinch-zoom element');
|
||||
this.setState({
|
||||
scale: this.pinchZoom.current.scale,
|
||||
});
|
||||
};
|
||||
|
||||
private onCropChange = (crop: CropBox) => {
|
||||
this.setState({ crop });
|
||||
};
|
||||
|
||||
private onCropPresetChange = (event: Event) => {
|
||||
const { value } = event.target as HTMLSelectElement;
|
||||
// @ts-ignore-next
|
||||
const cropPreset = cropPresets[value];
|
||||
this.setState({
|
||||
cropPreset,
|
||||
lockAspect: true,
|
||||
});
|
||||
};
|
||||
|
||||
private swapCropDimensions = () => {
|
||||
const { width, height } = this.props.source.decoded;
|
||||
let { left, right, top, bottom } = this.state.crop;
|
||||
const cropWidth = width - left - right;
|
||||
const cropHeight = height - top - bottom;
|
||||
const centerX = left - right;
|
||||
const centerY = top - bottom;
|
||||
const crop = {
|
||||
top: (width - cropWidth) / 2 + centerY / 2,
|
||||
bottom: (width - cropWidth) / 2 - centerY / 2,
|
||||
left: (height - cropHeight) / 2 + centerX / 2,
|
||||
right: (height - cropHeight) / 2 - centerX / 2,
|
||||
};
|
||||
this.setCrop(crop);
|
||||
};
|
||||
|
||||
private setCrop(crop: CropBox) {
|
||||
if (crop.top < 0) {
|
||||
crop.bottom += crop.top;
|
||||
crop.top = 0;
|
||||
}
|
||||
if (crop.bottom < 0) {
|
||||
crop.top += crop.bottom;
|
||||
crop.bottom = 0;
|
||||
}
|
||||
if (crop.left < 0) {
|
||||
crop.right += crop.left;
|
||||
crop.left = 0;
|
||||
}
|
||||
if (crop.right < 0) {
|
||||
crop.left += crop.right;
|
||||
crop.right = 0;
|
||||
}
|
||||
this.setState({ crop });
|
||||
}
|
||||
|
||||
// yeah these could just += 90
|
||||
private rotateClockwise = () => {
|
||||
let { rotate, crop } = this.state;
|
||||
this.setState({
|
||||
rotate: ((rotate + 90) % 360) as typeof ROTATE_ORIENTATIONS[number],
|
||||
crop: {
|
||||
top: crop.left,
|
||||
left: crop.bottom,
|
||||
bottom: crop.right,
|
||||
right: crop.top,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private rotateCounterClockwise = () => {
|
||||
let { rotate, crop } = this.state;
|
||||
this.setState({
|
||||
rotate: (rotate
|
||||
? rotate - 90
|
||||
: 270) as typeof ROTATE_ORIENTATIONS[number],
|
||||
crop: {
|
||||
top: crop.right,
|
||||
right: crop.bottom,
|
||||
bottom: crop.left,
|
||||
left: crop.top,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private flipHorizontally = () => {
|
||||
const { horizontal, vertical } = this.state.flip;
|
||||
this.setState({ flip: { horizontal: !horizontal, vertical } });
|
||||
};
|
||||
|
||||
private flipVertically = () => {
|
||||
const { horizontal, vertical } = this.state.flip;
|
||||
this.setState({ flip: { horizontal, vertical: !vertical } });
|
||||
};
|
||||
|
||||
// private update = (event: Event) => {
|
||||
// const { name, value } = event.target as HTMLInputElement;
|
||||
// const state = cleanSet(this.state, name, value);
|
||||
// this.setState(state);
|
||||
// };
|
||||
|
||||
private toggleLockAspect = () => {
|
||||
this.setState({ lockAspect: !this.state.lockAspect });
|
||||
};
|
||||
|
||||
private setCropWidth = (
|
||||
event: preact.JSX.TargetedEvent<HTMLInputElement, Event>,
|
||||
) => {
|
||||
const { width, height } = this.props.source.decoded;
|
||||
const newWidth = Math.min(width, parseInt(event.currentTarget.value, 10));
|
||||
let { top, right, bottom, left } = this.state.crop;
|
||||
const aspect = (width - left - right) / (height - top - bottom);
|
||||
right = width - newWidth - left;
|
||||
if (this.state.lockAspect) {
|
||||
const newHeight = newWidth / aspect;
|
||||
if (newHeight > height) return;
|
||||
bottom = height - newHeight - top;
|
||||
}
|
||||
this.setCrop({ top, right, bottom, left });
|
||||
};
|
||||
|
||||
private setCropHeight = (
|
||||
event: preact.JSX.TargetedEvent<HTMLInputElement, Event>,
|
||||
) => {
|
||||
const { width, height } = this.props.source.decoded;
|
||||
const newHeight = Math.min(height, parseInt(event.currentTarget.value, 10));
|
||||
let { top, right, bottom, left } = this.state.crop;
|
||||
const aspect = (width - left - right) / (height - top - bottom);
|
||||
bottom = height - newHeight - top;
|
||||
if (this.state.lockAspect) {
|
||||
const newWidth = newHeight * aspect;
|
||||
if (newWidth > width) return;
|
||||
right = width - newWidth - left;
|
||||
}
|
||||
this.setCrop({ top, right, bottom, left });
|
||||
};
|
||||
|
||||
// private onRotateClick = () => {
|
||||
// const { preprocessorState: inputProcessorState } = this.props;
|
||||
// if (!inputProcessorState) return;
|
||||
// const newState = cleanSet(
|
||||
// inputProcessorState,
|
||||
// 'rotate.rotate',
|
||||
// (inputProcessorState.rotate.rotate + 90) % 360,
|
||||
// );
|
||||
// this.props.onPreprocessorChange(newState);
|
||||
// };
|
||||
|
||||
render(
|
||||
{ mobileView, source }: Props,
|
||||
{ scale, editingScale, rotate, flip, crop, cropPreset, lockAspect }: State,
|
||||
) {
|
||||
const image = source.decoded;
|
||||
|
||||
const width = source.decoded.width - crop.left - crop.right;
|
||||
const height = source.decoded.height - crop.top - crop.bottom;
|
||||
|
||||
let transform =
|
||||
`rotate(${rotate}deg) ` +
|
||||
`scale(${flip.horizontal ? -1 : 1}, ${flip.vertical ? -1 : 1})`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CancelButton onClick={this.cancel} />
|
||||
<SaveButton onClick={this.save} />
|
||||
|
||||
<div class={style.transform}>
|
||||
<pinch-zoom
|
||||
class={style.pinchZoom}
|
||||
onChange={this.onPinchZoomChange}
|
||||
ref={this.pinchZoom}
|
||||
>
|
||||
{/* <Backdrop width={image.width} height={image.height} /> */}
|
||||
<div
|
||||
class={style.wrap}
|
||||
style={{
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
}}
|
||||
>
|
||||
<CanvasImage
|
||||
class={style.pinchTarget}
|
||||
image={image}
|
||||
style={{ transform }}
|
||||
/>
|
||||
{crop && (
|
||||
<Cropper
|
||||
size={{ width: image.width, height: image.height }}
|
||||
scale={scale}
|
||||
lockAspect={lockAspect}
|
||||
crop={crop}
|
||||
onChange={this.onCropChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</pinch-zoom>
|
||||
</div>
|
||||
|
||||
<div class={style.controls}>
|
||||
<div class={style.zoomControls}>
|
||||
<button class={style.button} onClick={this.zoomOut}>
|
||||
<RemoveIcon />
|
||||
</button>
|
||||
{editingScale ? (
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
max="1000000"
|
||||
ref={this.scaleInput}
|
||||
class={style.zoom}
|
||||
value={Math.round(scale * 100)}
|
||||
onInput={this.onScaleInputChanged}
|
||||
onBlur={this.onScaleInputBlur}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
class={style.zoom}
|
||||
tabIndex={0}
|
||||
onFocus={this.onScaleValueFocus}
|
||||
>
|
||||
<span class={style.zoomValue}>{Math.round(scale * 100)}</span>%
|
||||
</span>
|
||||
)}
|
||||
<button class={style.button} onClick={this.zoomIn}>
|
||||
<AddIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={style.options}>
|
||||
<h3 class={style.optionsTitle}>Modify Source</h3>
|
||||
|
||||
<div class={style.optionsSection}>
|
||||
<h4 class={style.optionsSectionTitle}>Crop</h4>
|
||||
<div class={style.optionOneCell}>
|
||||
<Select
|
||||
large
|
||||
value={cropPreset}
|
||||
onChange={this.onCropPresetChange}
|
||||
>
|
||||
<option value="">Custom</option>
|
||||
{Object.entries(cropPresets).map(([type, preset]) => (
|
||||
<option value={type}>{preset.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<label class={style.optionCheckbox}>
|
||||
<Checkbox checked={lockAspect} onClick={this.toggleLockAspect} />
|
||||
Lock aspect-ratio
|
||||
</label>
|
||||
<div class={style.optionsDimensions}>
|
||||
<input
|
||||
type="number"
|
||||
name="width"
|
||||
value={width}
|
||||
title="Crop width"
|
||||
onInput={this.setCropWidth}
|
||||
/>
|
||||
<button
|
||||
class={style.optionsButton}
|
||||
title="swap"
|
||||
onClick={this.swapCropDimensions}
|
||||
>
|
||||
<SwapIcon />
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
name="height"
|
||||
value={height}
|
||||
title="Crop height"
|
||||
onInput={this.setCropHeight}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={style.optionButtonRow}>
|
||||
Flip
|
||||
<button
|
||||
class={style.optionsButton}
|
||||
data-active={flip.vertical}
|
||||
title="Flip vertically"
|
||||
onClick={this.flipVertically}
|
||||
>
|
||||
<FlipVerticallyIcon />
|
||||
</button>
|
||||
<button
|
||||
class={style.optionsButton}
|
||||
data-active={flip.horizontal}
|
||||
title="Flip horizontally"
|
||||
onClick={this.flipHorizontally}
|
||||
>
|
||||
<FlipHorizontallyIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={style.optionButtonRow}>
|
||||
Rotate
|
||||
<button
|
||||
class={style.optionsButton}
|
||||
title="Rotate clockwise"
|
||||
onClick={this.rotateClockwise}
|
||||
>
|
||||
<RotateClockwiseIcon />
|
||||
</button>
|
||||
<button
|
||||
class={style.optionsButton}
|
||||
title="Rotate counter-clockwise"
|
||||
onClick={this.rotateCounterClockwise}
|
||||
>
|
||||
<RotateCounterClockwiseIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CancelButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<button class={style.cancel} onClick={onClick}>
|
||||
<svg viewBox="0 0 80 80" width="80" height="80">
|
||||
<path d="M8.06 40.98c-.53-7.1 4.05-14.52 9.98-19.1s13.32-6.35 22.13-6.43c8.84-.12 19.12 1.51 24.4 7.97s5.6 17.74 1.68 26.97c-3.89 9.26-11.97 16.45-20.46 18-8.43 1.55-17.28-2.62-24.5-8.08S8.54 48.08 8.07 40.98z" />
|
||||
</svg>
|
||||
<CompareIcon class={style.icon} />
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
const SaveButton = ({ onClick }: { onClick: () => void }) => (
|
||||
<button class={style.save} onClick={onClick}>
|
||||
<svg viewBox="0 0 89 87" width="89" height="87">
|
||||
<path
|
||||
fill="#0c99ff"
|
||||
opacity=".7"
|
||||
d="M27.3 71.9c-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.7 54 73.5c-10.2 2-18.7 2.3-26.7-1.6z"
|
||||
/>
|
||||
<path
|
||||
fill="#0c99ff"
|
||||
opacity=".7"
|
||||
d="M14.6 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.8 24.9s-7.2 20.7-18 27.6c-10.9 6.8-25.6 9.5-34.3 4.8S13 61.6 11.6 51.4c-1.3-10.3-1.3-18.8 3-26.6z"
|
||||
/>
|
||||
</svg>
|
||||
<CheckmarkIcon class={style.icon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
interface BackdropProps {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
class Backdrop extends Component<BackdropProps> {
|
||||
shouldComponentUpdate({ width, height }: BackdropProps) {
|
||||
return width !== this.props.width || height !== this.props.height;
|
||||
}
|
||||
|
||||
/** @TODO this could at least use clip-path */
|
||||
render({ width, height }: BackdropProps) {
|
||||
const transform =
|
||||
`transform-origin: 50% 50%; transform: translate(var(--x), var(--y)) ` +
|
||||
`translate(-${width / 2}px, -${height / 2}px) ` +
|
||||
`scale(calc(var(--scale, 1) * 0.99999));`;
|
||||
return (
|
||||
<svg
|
||||
class={style.backdrop}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
width="100%"
|
||||
height="100%"
|
||||
shape-rendering="optimizeSpeed"
|
||||
>
|
||||
<mask id="bgmask">
|
||||
<rect width="100%" height="100%" fill="white" />
|
||||
<rect
|
||||
style={transform}
|
||||
width={width}
|
||||
height={height}
|
||||
x="50%"
|
||||
y="50%"
|
||||
fill="black"
|
||||
/>
|
||||
</mask>
|
||||
<rect
|
||||
class={style.backdropArea}
|
||||
width="100%"
|
||||
height="100%"
|
||||
mask="url(#bgmask)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
351
src/client/lazy-app/Compress/Transform/style.css
Normal file
351
src/client/lazy-app/Compress/Transform/style.css
Normal file
@ -0,0 +1,351 @@
|
||||
.transform {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
overflow: visible;
|
||||
|
||||
/*
|
||||
& > canvas {
|
||||
transition: transform 150ms ease;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: none !important;
|
||||
will-change: initial !important;
|
||||
contain: strict;
|
||||
|
||||
& * {
|
||||
contain: strict;
|
||||
}
|
||||
/* background: rgba(255, 0, 0, 0.5); */
|
||||
}
|
||||
|
||||
.backdropArea {
|
||||
fill: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.pinch-zoom {
|
||||
composes: abs-fill from global;
|
||||
outline: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pinch-target {
|
||||
/* This fixes a severe painting bug in Chrome.
|
||||
* We should try to remove this once the issue is fixed.
|
||||
* https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10 */
|
||||
will-change: auto;
|
||||
/* Prevent the image becoming misshapen due to default flexbox layout. */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cancel,
|
||||
.save {
|
||||
composes: unbutton from global;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.save {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
|
||||
& > * {
|
||||
grid-area: 1/1/1/1;
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* @TODO use grid */
|
||||
.cancel {
|
||||
fill: rgba(0, 0, 0, 0.4);
|
||||
|
||||
& > svg:not(.icon) {
|
||||
display: block;
|
||||
margin: -8px 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
top: 22px;
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
& > span {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 1rem;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
font-size: 80%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
fill: rgba(0, 0, 0, 0.9);
|
||||
|
||||
& > span {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 78px;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
max-width: 250px;
|
||||
margin: 0;
|
||||
width: calc(100% - 60px);
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
align-self: end;
|
||||
border-radius: var(--options-radius) 0 0 var(--options-radius);
|
||||
animation: slideInFromRight 500ms ease-out forwards 1;
|
||||
--horizontal-padding: 15px;
|
||||
--main-theme-color: var(--blue);
|
||||
}
|
||||
@keyframes slideInFromRight {
|
||||
0% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.options-title {
|
||||
background-color: var(--main-theme-color);
|
||||
color: var(--dark-text);
|
||||
margin: 0;
|
||||
padding: 10px var(--horizontal-padding);
|
||||
font-weight: bold;
|
||||
font-size: 1.4rem;
|
||||
border-bottom: 1px solid var(--off-black);
|
||||
}
|
||||
|
||||
.options-section {
|
||||
padding: 5px 0;
|
||||
background: var(--off-black);
|
||||
}
|
||||
|
||||
.options-section-title {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
padding: 5px var(--horizontal-padding);
|
||||
}
|
||||
|
||||
.option-base {
|
||||
display: grid;
|
||||
gap: 0.7em;
|
||||
align-items: center;
|
||||
padding: 5px var(--horizontal-padding);
|
||||
}
|
||||
|
||||
.options-button {
|
||||
composes: unbutton from global;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--dark-gray);
|
||||
color: var(--white);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--off-black);
|
||||
border-color: var(--med-gray);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
background-color: var(--dark-gray);
|
||||
border-color: var(--med-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.options-dimensions {
|
||||
composes: option-base;
|
||||
grid-template-columns: 1fr 0fr 1fr;
|
||||
|
||||
input {
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
font: inherit;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-one-cell {
|
||||
composes: option-base;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.option-button-row {
|
||||
composes: option-base;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.option-checkbox {
|
||||
composes: option-base;
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
/** Zoom controls */
|
||||
.controls {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 9px 84px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
/* Allow clicks to fall through to the pinch zoom area */
|
||||
pointer-events: none;
|
||||
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
padding: 9px;
|
||||
top: auto;
|
||||
left: 320px;
|
||||
right: 320px;
|
||||
bottom: 0;
|
||||
flex-wrap: wrap-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
|
||||
& > :not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
& > :not(:last-child) {
|
||||
margin-right: 0;
|
||||
border-right-width: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button,
|
||||
.zoom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
margin: 4px;
|
||||
background-color: rgba(29, 29, 29, 0.92);
|
||||
border: 1px solid rgba(0, 0, 0, 0.67);
|
||||
border-radius: 6px;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
height: 39px;
|
||||
padding: 0 8px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
|
||||
/*
|
||||
@media (min-width: 600px) {
|
||||
height: 39px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
*/
|
||||
|
||||
&:focus {
|
||||
/* box-shadow: 0 0 0 2px var(--hot-pink); */
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(50, 50, 50, 0.92);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(72, 72, 72, 0.92);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom {
|
||||
cursor: text;
|
||||
width: 7rem;
|
||||
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 #fff;
|
||||
}
|
||||
}
|
||||
span.zoom {
|
||||
color: #939393;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 100;
|
||||
}
|
||||
input.zoom {
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
text-indent: 3px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.zoom-value {
|
||||
margin: 0 3px 0 0;
|
||||
padding: 0 2px;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.05rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
border-bottom: 1px dashed #999;
|
||||
}
|
||||
|
||||
.buttons-no-wrap {
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ 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 Transform from './Transform';
|
||||
|
||||
export type OutputType = EncoderType | 'identity';
|
||||
|
||||
@ -68,8 +69,11 @@ interface State {
|
||||
sides: [Side, Side];
|
||||
/** Source image load */
|
||||
loading: boolean;
|
||||
/** Showing preprocessor transformations modal */
|
||||
transform: boolean;
|
||||
error?: string;
|
||||
mobileView: boolean;
|
||||
altBackground: boolean;
|
||||
preprocessorState: PreprocessorState;
|
||||
encodedPreprocessorState?: PreprocessorState;
|
||||
}
|
||||
@ -125,13 +129,18 @@ async function preprocessImage(
|
||||
): Promise<ImageData> {
|
||||
assertSignal(signal);
|
||||
let processedData = data;
|
||||
const { rotate, flip, crop } = preprocessorState;
|
||||
|
||||
if (preprocessorState.rotate.rotate !== 0) {
|
||||
processedData = await workerBridge.rotate(
|
||||
signal,
|
||||
processedData,
|
||||
preprocessorState.rotate,
|
||||
);
|
||||
if (rotate.rotate !== 0) {
|
||||
processedData = await workerBridge.rotate(signal, processedData, rotate);
|
||||
}
|
||||
|
||||
if (flip && (flip.horizontal || flip.vertical)) {
|
||||
processedData = await workerBridge.flip(signal, processedData, flip);
|
||||
}
|
||||
|
||||
if (crop && (crop.left || crop.top || crop.right || crop.bottom)) {
|
||||
processedData = await workerBridge.crop(signal, processedData, crop);
|
||||
}
|
||||
|
||||
return processedData;
|
||||
@ -273,6 +282,9 @@ export default class Compress extends Component<Props, State> {
|
||||
state: State = {
|
||||
source: undefined,
|
||||
loading: false,
|
||||
/** @TODO remove this */
|
||||
// transform: true,
|
||||
transform: false,
|
||||
preprocessorState: defaultPreprocessorState,
|
||||
sides: [
|
||||
{
|
||||
@ -294,6 +306,7 @@ export default class Compress extends Component<Props, State> {
|
||||
},
|
||||
],
|
||||
mobileView: this.widthQuery.matches,
|
||||
altBackground: false,
|
||||
};
|
||||
|
||||
private readonly encodeCache = new ResultCache();
|
||||
@ -319,7 +332,13 @@ export default class Compress extends Component<Props, State> {
|
||||
this.setState({ mobileView: this.widthQuery.matches });
|
||||
};
|
||||
|
||||
private onEncoderTypeChange(index: 0 | 1, newType: OutputType): void {
|
||||
private toggleBackground = () => {
|
||||
this.setState({
|
||||
altBackground: !this.state.altBackground,
|
||||
});
|
||||
};
|
||||
|
||||
private onEncoderTypeChange = (index: 0 | 1, newType: OutputType): void => {
|
||||
this.setState({
|
||||
sides: cleanSet(
|
||||
this.state.sides,
|
||||
@ -332,12 +351,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 +364,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 +377,21 @@ export default class Compress extends Component<Props, State> {
|
||||
options,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private showPreprocessorTransforms = () => {
|
||||
this.setState({ transform: true });
|
||||
};
|
||||
|
||||
private onTransformUpdated = ({
|
||||
preprocessorState,
|
||||
}: { preprocessorState?: PreprocessorState } = {}) => {
|
||||
console.log('onTransformUpdated', preprocessorState);
|
||||
if (preprocessorState) {
|
||||
this.onPreprocessorChange(preprocessorState);
|
||||
}
|
||||
this.setState({ transform: false });
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps: Props): void {
|
||||
if (nextProps.file !== this.props.file) {
|
||||
@ -787,29 +823,31 @@ export default class Compress extends Component<Props, State> {
|
||||
|
||||
render(
|
||||
{ onBack }: Props,
|
||||
{ loading, sides, source, mobileView, preprocessorState }: State,
|
||||
{
|
||||
loading,
|
||||
sides,
|
||||
source,
|
||||
mobileView,
|
||||
altBackground,
|
||||
transform,
|
||||
preprocessorState,
|
||||
}: State,
|
||||
) {
|
||||
const [leftSide, rightSide] = sides;
|
||||
const [leftImageData, rightImageData] = sides.map((i) => i.data);
|
||||
|
||||
transform = (source && source.decoded && transform) || false;
|
||||
|
||||
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}
|
||||
/>
|
||||
));
|
||||
|
||||
@ -854,39 +892,68 @@ export default class Compress extends Component<Props, State> {
|
||||
rightDisplaySettings.processorState.resize.fitMethod === 'contain';
|
||||
|
||||
return (
|
||||
<div class={style.compress}>
|
||||
<div
|
||||
class={`${style.compress} ${transform ? style.transforming : ''} ${
|
||||
altBackground ? style.altBackground : ''
|
||||
}`}
|
||||
>
|
||||
<Output
|
||||
hidden={transform}
|
||||
source={source}
|
||||
mobileView={mobileView}
|
||||
leftCompressed={leftImageData}
|
||||
rightCompressed={rightImageData}
|
||||
leftImgContain={leftImgContain}
|
||||
rightImgContain={rightImgContain}
|
||||
onBack={onBack}
|
||||
preprocessorState={preprocessorState}
|
||||
onPreprocessorChange={this.onPreprocessorChange}
|
||||
onShowPreprocessorTransforms={this.showPreprocessorTransforms}
|
||||
onToggleBackground={this.toggleBackground}
|
||||
/>
|
||||
<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]}
|
||||
<div class={style.options1Theme}>{options[0]}</div>
|
||||
{results[1]}
|
||||
{options[1]}
|
||||
<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>,
|
||||
]
|
||||
)}
|
||||
|
||||
{transform && (
|
||||
<Transform
|
||||
mobileView={mobileView}
|
||||
source={source!}
|
||||
preprocessorState={preprocessorState!}
|
||||
onSave={this.onTransformUpdated}
|
||||
onCancel={this.onTransformUpdated}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,39 +3,110 @@
|
||||
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';
|
||||
}
|
||||
|
||||
/* darker squares background */
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #000;
|
||||
opacity: 0.8;
|
||||
transition: opacity 500ms ease;
|
||||
}
|
||||
|
||||
&.alt-background::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* transformation is modal and we sweep away the comparison UI */
|
||||
&.transforming {
|
||||
& > .options {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
& > .options + .options {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
& > .back {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
align-self: end;
|
||||
grid-area: opts;
|
||||
transition: transform 500ms ease;
|
||||
|
||||
@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);
|
||||
--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);
|
||||
--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;
|
||||
@ -73,3 +144,33 @@
|
||||
:focus .expand-icon {
|
||||
fill: #34b9eb;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.back-blob {
|
||||
fill: var(--hot-pink);
|
||||
opacity: 0.77;
|
||||
}
|
||||
|
||||
.back-x {
|
||||
fill: var(--white);
|
||||
}
|
||||
|
@ -11,6 +11,50 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const SwapIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M9.01 14H2v2h7.01v3L13 15l-3.99-4zm5.98-1v-3H22V8h-7.01V5L11 9z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const FlipVerticallyIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M21 9V7h-2v2zM9 5V3H7v2zM5 21h14a2 2 0 002-2v-4h-2v4H5v-4H3v4a2 2 0 002 2zM3 5h2V3a2 2 0 00-2 2zm20 8v-2H1v2zm-6-8V3h-2v2zM5 9V7H3v2zm8-4V3h-2v2zm8 0a2 2 0 00-2-2v2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const FlipHorizontallyIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M15 21h2v-2h-2zm4-12h2V7h-2zM3 5v14a2 2 0 002 2h4v-2H5V5h4V3H5a2 2 0 00-2 2zm16-2v2h2a2 2 0 00-2-2zm-8 20h2V1h-2zm8-6h2v-2h-2zM15 5h2V3h-2zm4 8h2v-2h-2zm0 8a2 2 0 002-2h-2z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const RotateClockwiseIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M16.05 5.34l-5.2-5.2v3.5a9.12 9.12 0 000 18.1v-2.3a6.84 6.84 0 010-13.5v4.47zm5 6.22a9.03 9.03 0 00-1.85-4.44l-1.62 1.62a6.63 6.63 0 011.16 2.82zm-7.91 7.87v2.31a9.05 9.05 0 004.45-1.84l-1.64-1.64a6.6 6.6 0 01-2.81 1.18zm4.44-2.76l1.62 1.61a9.03 9.03 0 001.85-4.44h-2.3a6.73 6.73 0 01-1.17 2.83z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const RotateCounterClockwiseIcon = (
|
||||
props: preact.JSX.HTMLAttributes,
|
||||
) => (
|
||||
<Icon {...props}>
|
||||
<path d="M7.95 5.34l5.19-5.2v3.5a9.12 9.12 0 010 18.1v-2.3a6.84 6.84 0 000-13.5v4.47zm-5 6.22A9.03 9.03 0 014.8 7.12l1.62 1.62a6.63 6.63 0 00-1.17 2.82zm7.9 7.87v2.31A9.05 9.05 0 016.4 19.9l1.65-1.64a6.6 6.6 0 002.8 1.17zm-4.43-2.76L4.8 18.28a9.03 9.03 0 01-1.85-4.44h2.3a6.73 6.73 0 001.17 2.83z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const CheckmarkIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M9.76 17.56l-4.55-4.55-1.52 1.52 6.07 6.08 13-13.02-1.51-1.52z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const CompareIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M9.77 1.94h-5.6a2.24 2.24 0 00-2.22 2.25v15.65a2.24 2.24 0 002.24 2.23h5.59v2.24h2.23V-.31H9.78zm0 16.77h-5.6l5.6-6.7zM19.83 1.94h-5.6v2.25h5.6v14.53l-5.6-6.7v10.05h5.6a2.24 2.24 0 002.23-2.23V4.18a2.24 2.24 0 00-2.23-2.24z" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
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" />
|
||||
@ -37,6 +81,14 @@ export const RotateIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const MoreIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<circle cx="12" cy="6" r="2" fill="#fff" />
|
||||
<circle cx="12" cy="12" r="2" fill="#fff" />
|
||||
<circle cx="12" cy="18" r="2" fill="#fff" />
|
||||
</Icon>
|
||||
);
|
||||
|
||||
export const AddIcon = (props: preact.JSX.HTMLAttributes) => (
|
||||
<Icon {...props}>
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
@ -67,10 +119,10 @@ 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 = {
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 RGB→YUV conversion
|
||||
<Checkbox
|
||||
name="use_sharp_yuv"
|
||||
checked={!!options.use_sharp_yuv}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
Sharp RGB→YUV 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>
|
||||
);
|
||||
|
@ -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>
|
||||
)}
|
||||
|
25
src/features/preprocessors/crop/shared/meta.ts
Normal file
25
src/features/preprocessors/crop/shared/meta.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface Options {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
};
|
13
src/features/preprocessors/crop/shared/missing-types.d.ts
vendored
Normal file
13
src/features/preprocessors/crop/shared/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
63
src/features/preprocessors/crop/worker/crop.ts
Normal file
63
src/features/preprocessors/crop/worker/crop.ts
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Options } from '../shared/meta';
|
||||
|
||||
export default async function crop(
|
||||
data: ImageData,
|
||||
opts: Options,
|
||||
): Promise<ImageData> {
|
||||
const { left, top, right, bottom } = opts;
|
||||
const source = data.data;
|
||||
const { width, height } = data;
|
||||
const cols = width * 4;
|
||||
|
||||
const newWidth = width - left - right;
|
||||
const newHeight = height - top - bottom;
|
||||
const len = newWidth * newHeight * 4;
|
||||
const pixels = new Uint8ClampedArray(len);
|
||||
|
||||
for (let y = 0; y < newHeight; y++) {
|
||||
for (let x = 0; x < newWidth; x++) {
|
||||
let i = y * cols + x * 4;
|
||||
let j = (top + y) * cols + (left + x) * 4;
|
||||
pixels[i] = source[j];
|
||||
pixels[i + 1] = source[j + 1];
|
||||
pixels[i + 2] = source[j + 2];
|
||||
pixels[i + 3] = source[j + 3];
|
||||
}
|
||||
}
|
||||
|
||||
// let sourceX = left;
|
||||
// let sourceY = top;
|
||||
// let x = 0;
|
||||
// let y = 0;
|
||||
// let i = 0;
|
||||
// while (i < len) {
|
||||
// let from = sourceY * cols + sourceX * 4;
|
||||
|
||||
// pixels[i++] = source[from++];
|
||||
// pixels[i++] = source[from++];
|
||||
// pixels[i++] = source[from++];
|
||||
// pixels[i++] = source[from];
|
||||
|
||||
// if (++x === newWidth) {
|
||||
// x = 0;
|
||||
// y++;
|
||||
|
||||
// sourceX = left;
|
||||
// sourceY++;
|
||||
// }
|
||||
// }
|
||||
|
||||
return new ImageData(pixels, newWidth, newHeight);
|
||||
}
|
13
src/features/preprocessors/crop/worker/missing-types.d.ts
vendored
Normal file
13
src/features/preprocessors/crop/worker/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
21
src/features/preprocessors/flip/shared/meta.ts
Normal file
21
src/features/preprocessors/flip/shared/meta.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface Options {
|
||||
horizontal: boolean;
|
||||
vertical: boolean;
|
||||
}
|
||||
|
||||
export const defaultOptions: Options = {
|
||||
horizontal: false,
|
||||
vertical: false,
|
||||
};
|
13
src/features/preprocessors/flip/shared/missing-types.d.ts
vendored
Normal file
13
src/features/preprocessors/flip/shared/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
68
src/features/preprocessors/flip/worker/flip.ts
Normal file
68
src/features/preprocessors/flip/worker/flip.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Options } from '../shared/meta';
|
||||
|
||||
export default async function flip(
|
||||
data: ImageData,
|
||||
opts: Options,
|
||||
): Promise<ImageData> {
|
||||
const { vertical, horizontal } = opts;
|
||||
const source = data.data;
|
||||
const len = source.length;
|
||||
const pixels = new Uint8ClampedArray(len);
|
||||
const { width, height } = data;
|
||||
|
||||
let i = 0;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
const cols = width * 4;
|
||||
while (i < len) {
|
||||
let from = vertical ? (height - y) * cols + x * 4 : i;
|
||||
if (horizontal) from = from - x * 4 + cols - x * 4; // todo: reduce
|
||||
|
||||
pixels[i++] = source[from++];
|
||||
pixels[i++] = source[from++];
|
||||
pixels[i++] = source[from++];
|
||||
pixels[i++] = source[from];
|
||||
|
||||
if (++x === width) {
|
||||
x = 0;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
function swap(a: number, b: number) {
|
||||
let tmp = pixels[a];
|
||||
pixels[a] = pixels[b];
|
||||
pixels[b] = tmp;
|
||||
}
|
||||
function swapRgba(a: number, b: number) {
|
||||
swap(a, b);
|
||||
swap(a+1, b+1);
|
||||
swap(a+2, b+2);
|
||||
swap(a+3, b+3);
|
||||
}
|
||||
const COLS = data.width * 4;
|
||||
// for (let y = 0, y2 = (data.height - 1); y < y2; y+=4, y2-=4) {
|
||||
for (let y = 0; y < data.height; y++) {
|
||||
for (let x = 0, x2 = COLS - 4; x < x2; x+=4, x2-=4) {
|
||||
const offsetX = y * COLS;
|
||||
const offsetY = (opts.vertical ? (data.height - y) : y) * COLS;
|
||||
const flippedX = opts.horizontal ? x2 : x;
|
||||
swapRgba(offsetX + x, offsetY + x2);
|
||||
}
|
||||
}
|
||||
*/
|
||||
return new ImageData(pixels, data.width, data.height);
|
||||
}
|
13
src/features/preprocessors/flip/worker/missing-types.d.ts
vendored
Normal file
13
src/features/preprocessors/flip/worker/missing-types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
@ -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 : (
|
||||
|
@ -46,7 +46,7 @@ const demos = [
|
||||
url: logo,
|
||||
iconUrl: logoIcon,
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
const blobAnimImport =
|
||||
!__PRERENDER__ && matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
@ -103,6 +103,16 @@ export default class Intro extends Component<Props, State> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: remove this
|
||||
const demo = demos[3];
|
||||
fetch(demo.url)
|
||||
.then((r) => r.blob())
|
||||
.then((blob) =>
|
||||
this.props.onFile!(
|
||||
new File([blob], demo.filename, { type: blob.type }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -273,7 +283,7 @@ export default class Intro extends Component<Props, State> {
|
||||
)}
|
||||
<div
|
||||
class={style.loadImgContent}
|
||||
style={{ visibility: __PRERENDER__ ? 'hidden' : '' }}
|
||||
style={{ visibility: __PRERENDER__ ? 'hidden' : undefined }}
|
||||
>
|
||||
<button class={style.loadBtn} onClick={this.onOpenClick}>
|
||||
<svg viewBox="0 0 24 24" class={style.loadIcon}>
|
||||
|
@ -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 {
|
||||
|
@ -2,11 +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;
|
||||
--med-gray: #555;
|
||||
--dark-gray: #333;
|
||||
--dim-text: #343a3e;
|
||||
--dark-text: #142630;
|
||||
|
||||
/* Old stuff: */
|
||||
--gray-dark: rgba(0, 0, 0, 0.8);
|
||||
|
Reference in New Issue
Block a user