Compare commits

..

2 Commits

Author SHA1 Message Date
Surma
7a08815bcf Make emscripten with threads compile 2018-11-02 18:34:23 +00:00
Surma
30e78e8ab7 Attempt at threads for webp encoder 2018-11-01 22:36:38 +00:00
18 changed files with 379 additions and 211 deletions

BIN
codecs/really_big.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 MiB

View File

@@ -14,9 +14,12 @@ echo "============================================="
emcc \ emcc \
${OPTIMIZE} \ ${OPTIMIZE} \
--bind \ --bind \
-s ALLOW_MEMORY_GROWTH=1 \ -D WEBP_USE_THREAD=1 \
-s MODULARIZE=1 \ -s USE_PTHREADS=1 \
-s 'EXPORT_NAME="webp_enc"' \ -s ASSERTIONS=1 \
-s PTHREAD_POOL_SIZE=4 \
-s TOTAL_MEMORY=268435456 \
-s WASM_MEM_MAX=268435456 \
--std=c++11 \ --std=c++11 \
-I node_modules/libwebp \ -I node_modules/libwebp \
-o ./webp_enc.js \ -o ./webp_enc.js \

View File

@@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<script src='webp_enc.js'></script> <script src='webp_enc.js'></script>
<script> <script>
const module = webp_enc(); // const Module = webp_enc();
async function loadImage(src) { async function loadImage(src) {
// Load image // Load image
@@ -17,10 +17,11 @@
return ctx.getImageData(0, 0, img.width, img.height); return ctx.getImageData(0, 0, img.width, img.height);
} }
module.onRuntimeInitialized = async _ => { Module.onRuntimeInitialized = async _ => {
console.log('Version:', module.version().toString(16)); console.log('Version:', Module.version().toString(16));
const image = await loadImage('../example.png'); const image = await loadImage('../really_big.jpg');
const result = module.encode(image.data, image.width, image.height, { let start = performance.now();
const result = Module.encode(image.data, image.width, image.height, {
quality: 75, quality: 75,
target_size: 0, target_size: 0,
target_PSNR: 0, target_PSNR: 0,
@@ -43,16 +44,18 @@
exact: 0, exact: 0,
image_hint: 0, image_hint: 0,
emulate_jpeg_size: 0, emulate_jpeg_size: 0,
thread_level: 0, thread_level: 1,
low_memory: 0, low_memory: 0,
near_lossless: 100, near_lossless: 100,
use_delta_palette: 0, use_delta_palette: 0,
use_sharp_yuv: 0, use_sharp_yuv: 0,
}); });
let stop = performance.now();
console.log('size', result.length); console.log('size', result.length);
const blob = new Blob([result], {type: 'image/webp'}); console.log('time', stop - start);
const blob = new Blob([new Uint8Array(result)], {type: 'image/webp'});
module.free_result(); Module.free_result();
const blobURL = URL.createObjectURL(blob); const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img'); const img = document.createElement('img');

View File

@@ -0,0 +1,192 @@
// Copyright 2015 The Emscripten Authors. All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License. Both these licenses can be
// found in the LICENSE file.
// Pthread Web Worker startup routine:
// This is the entry point file that is loaded first by each Web Worker
// that executes pthreads on the Emscripten application.
// Thread-local:
var threadInfoStruct = 0; // Info area for this thread in Emscripten HEAP (shared). If zero, this worker is not currently hosting an executing pthread.
var selfThreadId = 0; // The ID of this thread. 0 if not hosting a pthread.
var parentThreadId = 0; // The ID of the parent pthread that launched this thread.
var tempDoublePtr = 0; // A temporary memory area for global float and double marshalling operations.
// Thread-local: Each thread has its own allocated stack space.
var STACK_BASE = 0;
var STACKTOP = 0;
var STACK_MAX = 0;
// These are system-wide memory area parameters that are set at main runtime startup in main thread, and stay constant throughout the application.
var buffer; // All pthreads share the same Emscripten HEAP as SharedArrayBuffer with the main execution thread.
var DYNAMICTOP_PTR = 0;
var TOTAL_MEMORY = 0;
var STATICTOP = 0;
var staticSealed = true; // When threads are being initialized, the static memory area has been already sealed a long time ago.
var DYNAMIC_BASE = 0;
var ENVIRONMENT_IS_PTHREAD = true;
// performance.now() is specced to return a wallclock time in msecs since that Web Worker/main thread launched. However for pthreads this can cause
// subtle problems in emscripten_get_now() as this essentially would measure time from pthread_create(), meaning that the clocks between each threads
// would be wildly out of sync. Therefore sync all pthreads to the clock on the main browser thread, so that different threads see a somewhat
// coherent clock across each of them (+/- 0.1msecs in testing)
var __performance_now_clock_drift = 0;
// Cannot use console.log or console.error in a web worker, since that would risk a browser deadlock! https://bugzilla.mozilla.org/show_bug.cgi?id=1049091
// Therefore implement custom logging facility for threads running in a worker, which queue the messages to main thread to print.
var Module = {};
// When error objects propagate from Web Worker to main thread, they lose helpful call stack and thread ID information, so print out errors early here,
// before that happens.
this.addEventListener('error', function(e) {
if (e.message.indexOf('SimulateInfiniteLoop') != -1) return e.preventDefault();
var errorSource = ' in ' + e.filename + ':' + e.lineno + ':' + e.colno;
console.error('Pthread ' + selfThreadId + ' uncaught exception' + (e.filename || e.lineno || e.colno ? errorSource : '') + ': ' + e.message + '. Error object:');
console.error(e.error);
});
function threadPrint() {
var text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
}
function threadPrintErr() {
var text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
console.error(new Error().stack);
}
function threadAlert() {
var text = Array.prototype.slice.call(arguments).join(' ');
postMessage({cmd: 'alert', text: text, threadId: selfThreadId});
}
out = threadPrint;
err = threadPrintErr;
this.alert = threadAlert;
// #if WASM
Module['instantiateWasm'] = function(info, receiveInstance) {
// Instantiate from the module posted from the main thread.
// We can just use sync instantiation in the worker.
instance = new WebAssembly.Instance(Module['wasmModule'], info);
// We don't need the module anymore; new threads will be spawned from the main thread.
delete Module['wasmModule'];
receiveInstance(instance);
return instance.exports;
}
//#endif
this.onmessage = function(e) {
try {
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
// Initialize the thread-local field(s):
tempDoublePtr = e.data.tempDoublePtr;
// Initialize the global "process"-wide fields:
Module['TOTAL_MEMORY'] = TOTAL_MEMORY = e.data.TOTAL_MEMORY;
STATICTOP = e.data.STATICTOP;
DYNAMIC_BASE = e.data.DYNAMIC_BASE;
DYNAMICTOP_PTR = e.data.DYNAMICTOP_PTR;
//#if WASM
if (e.data.wasmModule) {
// Module and memory were sent from main thread
Module['wasmModule'] = e.data.wasmModule;
Module['wasmMemory'] = e.data.wasmMemory;
buffer = Module['wasmMemory'].buffer;
} else {
//#else
buffer = e.data.buffer;
}
//#endif
PthreadWorkerInit = e.data.PthreadWorkerInit;
if (typeof e.data.urlOrBlob === 'string') {
importScripts(e.data.urlOrBlob);
} else {
var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
importScripts(objectUrl);
URL.revokeObjectURL(objectUrl);
}
//#if !ASMFS
if (typeof FS !== 'undefined' && typeof FS.createStandardStreams === 'function') FS.createStandardStreams();
//#endif
postMessage({ cmd: 'loaded' });
} else if (e.data.cmd === 'objectTransfer') {
PThread.receiveObjectTransfer(e.data);
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
__performance_now_clock_drift = performance.now() - e.data.time; // Sync up to the clock of the main thread.
threadInfoStruct = e.data.threadInfoStruct;
__register_pthread_ptr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
assert(threadInfoStruct);
selfThreadId = e.data.selfThreadId;
parentThreadId = e.data.parentThreadId;
assert(selfThreadId);
assert(parentThreadId);
// TODO: Emscripten runtime has these variables twice(!), once outside the asm.js module, and a second time inside the asm.js module.
// Review why that is? Can those get out of sync?
STACK_BASE = STACKTOP = e.data.stackBase;
STACK_MAX = STACK_BASE + e.data.stackSize;
assert(STACK_BASE != 0);
assert(STACK_MAX > STACK_BASE);
Module['establishStackSpace'](e.data.stackBase, e.data.stackBase + e.data.stackSize);
var result = 0;
//#if STACK_OVERFLOW_CHECK
if (typeof writeStackCookie === 'function') writeStackCookie();
//#endif
PThread.receiveObjectTransfer(e.data);
PThread.setThreadStatus(_pthread_self(), 1/*EM_THREAD_STATUS_RUNNING*/);
try {
// pthread entry points are always of signature 'void *ThreadMain(void *arg)'
// Native codebases sometimes spawn threads with other thread entry point signatures,
// such as void ThreadMain(void *arg), void *ThreadMain(), or void ThreadMain().
// That is not acceptable per C/C++ specification, but x86 compiler ABI extensions
// enable that to work. If you find the following line to crash, either change the signature
// to "proper" void *ThreadMain(void *arg) form, or try linking with the Emscripten linker
// flag -s EMULATE_FUNCTION_POINTER_CASTS=1 to add in emulation for this x86 ABI extension.
result = Module['dynCall_ii'](e.data.start_routine, e.data.arg);
//#if STACK_OVERFLOW_CHECK
if (typeof checkStackCookie === 'function') checkStackCookie();
//#endif
} catch(e) {
if (e === 'Canceled!') {
PThread.threadCancel();
return;
} else if (e === 'SimulateInfiniteLoop') {
return;
} else {
Atomics.store(HEAPU32, (threadInfoStruct + 4 /*{{{ C_STRUCTS.pthread.threadExitCode }}}*/ ) >> 2, (e instanceof ExitStatus) ? e.status : -2 /*A custom entry specific to Emscripten denoting that the thread crashed.*/);
Atomics.store(HEAPU32, (threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/ ) >> 2, 1); // Mark the thread as no longer running.
_emscripten_futex_wake(threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/, 0x7FFFFFFF/*INT_MAX*/); // Wake all threads waiting on this thread to finish.
if (!(e instanceof ExitStatus)) throw e;
}
}
// The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves.
// (This is a no-op if explicit pthread_exit() had been called prior.)
PThread.threadExit(result);
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
if (threadInfoStruct && PThread.thisThreadCancelState == 0/*PTHREAD_CANCEL_ENABLE*/) {
PThread.threadCancel();
}
} else if (e.data.target === 'setimmediate') {
// no-op
} else if (e.data.cmd === 'processThreadQueue') {
if (threadInfoStruct) { // If this thread is actually running?
_emscripten_current_thread_process_queued_calls();
}
} else {
err('pthread-main.js received unknown command ' + e.data.cmd);
console.error(e.data);
}
} catch(e) {
console.error('pthread-main.js onmessage() captured an uncaught exception: ' + e);
console.error(e.stack);
throw e;
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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