State management & UI improvements

This commit is contained in:
Ingvar Stepanyan
2021-07-19 17:26:00 +00:00
parent e3aee15676
commit 826aca5b9d
2 changed files with 148 additions and 87 deletions

32
ui.html
View File

@@ -7,6 +7,24 @@
crossorigin="anonymous" crossorigin="anonymous"
/> />
<style> <style>
.center-parent {
display: flex;
height: 100vh;
}
.center {
margin: auto;
}
#capture {
width: 20ch;
}
#config {
overflow-y: auto;
max-height: 100vh;
}
#config label { #config label {
max-width: 40%; max-width: 40%;
} }
@@ -14,11 +32,6 @@
#config select { #config select {
max-width: 50%; max-width: 50%;
} }
#canvas-holder,
#config {
max-height: 100vh;
}
</style> </style>
<script type="importmap"> <script type="importmap">
{ {
@@ -31,12 +44,5 @@
</script> </script>
<script type="module" src="ui.js"></script> <script type="module" src="ui.js"></script>
</head> </head>
<body> <body></body>
<div class="pure-g">
<div class="pure-u-2-3" id="canvas-holder">
<canvas id="canvas" style="display: flex; margin: auto"></canvas>
</div>
<div id="config" class="pure-u-1-3" style="overflow-y: auto"></div>
</div>
</body>
</html> </html>

203
ui.js
View File

@@ -9,16 +9,21 @@ if (new URLSearchParams(location.search).has('debug')) {
await import('preact/debug'); await import('preact/debug');
} }
let prepareContext;
/** Schedules an exclusive async operation on the global context. */ /** Schedules an exclusive async operation on the global context. */
const scheduleOp = (() => { const scheduleOp = (() => {
let queue = initModule() let queue = (async () => {
.then(Module => new Module.Context()) await new Promise(resolve => {
.then(ctx => { prepareContext = resolve;
addEventListener('beforeunload', e => {
ctx.delete();
});
return ctx;
}); });
let { Context } = await initModule();
let ctx = await new Context();
addEventListener('beforeunload', e => {
ctx.delete();
});
return ctx;
})();
/** /**
* @template T * @template T
@@ -231,9 +236,9 @@ class CaptureButton extends Component {
null, null,
h('input', { h('input', {
type: 'button', type: 'button',
id: 'capture',
class: class:
'pure-input-1-3 pure-button' + 'pure-button' + (this.state.inProgress ? ' pure-button-active' : ''),
(this.state.inProgress ? ' pure-button-active' : ''),
onclick: this.handleCapture, onclick: this.handleCapture,
value: `${this.state.inProgress ? '⌛' : '📷'} Capture image` value: `${this.state.inProgress ? '⌛' : '📷'} Capture image`
}) })
@@ -241,100 +246,150 @@ class CaptureButton extends Component {
} }
} }
/** @extends Component<null, { config: string | Config }> */ /** @typedef {{ type: 'Initial' } | { type: 'Status', message: string } | { type: 'Config', config: Config }} AppState */
class Settings extends Component {
state = { config: '' };
constructor() { /** @extends Component<null, AppState> */
super(); class App extends Component {
state = /** @type {AppState} */ ({ type: 'Initial' });
selectDevice = async () => {
await navigator.usb.requestDevice({
filters: [
{
classCode: 6, // PTP
subclassCode: 1 // MTP
}
]
});
prepareContext();
this.setState({ type: 'Status', message: 'Connecting...' });
(async () => { (async () => {
while (true) { while (true) {
await this.refreshConfig(); await this.refreshConfig();
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
})(); })();
} };
async refreshConfig() { async refreshConfig() {
let config;
try { try {
config = await scheduleOp(context => context.configToJS()); this.setState({
type: 'Config',
config: await scheduleOp(context => context.configToJS())
});
} catch (e) { } catch (e) {
config = String(e); this.setState({
type: 'Status',
message: String(e)
});
} }
this.setState({
config
});
} }
render( render(/** @type {App['props']} */ props, /** @type {App['state']} */ state) {
/** @type {Settings['props']} */ props, switch (state.type) {
/** @type {Settings['state']} */ state case 'Initial':
) { return h(
return typeof state.config === 'string' 'div',
? state.config { class: 'center-parent' },
: h( h('input', {
'form', class: 'center',
{ class: 'pure-form pure-form-aligned' }, type: 'button',
h(CaptureButton, null), onclick: this.selectDevice,
h(Widget, { config: state.config }) value: '🔍 Select camera'
})
); );
case 'Status':
return h(
'div',
{ class: 'center-parent' },
h('div', { class: 'center' }, state.message)
);
case 'Config':
return h(
'div',
{ class: 'pure-g' },
h(
'div',
{ class: 'pure-u-2-3 center-parent', id: 'canvas-holder' },
h(PreviewCanvas, {
id: 'preview-canvas',
class: 'center'
})
),
h(
'div',
{ id: 'config', class: 'pure-u-1-3' },
h(
'form',
{ class: 'pure-form pure-form-aligned' },
h(CaptureButton, null),
h(Widget, { config: state.config })
)
)
);
}
} }
} }
render(h(Settings, null), document.getElementById('config')); render(h(App, null), document.body);
(async () => { class PreviewCanvas extends Component {
// I have no idea why, but if we connect too soon, it just hangs... ref = createRef();
await new Promise(resolve => setTimeout(resolve, 500));
let canvas = /** @type {HTMLCanvasElement} */ ( render(props) {
document.getElementById('canvas') return h('canvas', { ...props, ref: this.ref });
); }
let canvasCtx = canvas.getContext('bitmaprenderer'); async componentDidMount() {
// I have no idea why, but if we connect too soon, it just hangs...
await new Promise(resolve => setTimeout(resolve, 1000));
let ratio = 0; let canvas = /** @type {HTMLCanvasElement} */ (this.ref.current);
let canvasHolder = canvas.parentElement;
while (true) { let canvasCtx = canvas.getContext('bitmaprenderer');
try {
let blob = await scheduleOp(context => context.capturePreviewAsBlob());
// If ratio is known; decode resized image right away - it's a bit faster. let ratio = 0;
// If it isn't known, retrieve entire image to calculate ratio from its dimensions.
let img = await createImageBitmap( while (true) {
blob, try {
ratio let blob = await scheduleOp(context => context.capturePreviewAsBlob());
? {
resizeWidth: canvas.width, // If ratio is known; decode resized image right away - it's a bit faster.
resizeHeight: canvas.height // If it isn't known, retrieve entire image to calculate ratio from its dimensions.
let img = await createImageBitmap(
blob,
ratio
? {
resizeWidth: canvas.width,
resizeHeight: canvas.height
}
: {}
);
if (!ratio) {
ratio = img.width / img.height;
function updateCanvasSize() {
let width = canvasHolder.offsetWidth - 10;
let height = canvasHolder.offsetHeight;
if (height * ratio > width) {
height = width / ratio;
} else {
width = height * ratio;
} }
: {}
);
if (!ratio) {
ratio = img.width / img.height;
let canvasHolder = document.getElementById('canvas-holder');
function updateCanvasSize() { Object.assign(canvas, { width, height });
let width = canvasHolder.offsetWidth - 10;
let height = canvasHolder.offsetHeight;
if (height * ratio > width) {
height = width / ratio;
} else {
width = height * ratio;
} }
Object.assign(canvas, { width, height }); updateCanvasSize();
new ResizeObserver(updateCanvasSize).observe(canvasHolder);
} }
canvasCtx.transferFromImageBitmap(img);
updateCanvasSize(); } catch (e) {
new ResizeObserver(updateCanvasSize).observe(canvasHolder); console.warn(e);
} }
canvasCtx.transferFromImageBitmap(img); await new Promise(resolve => requestAnimationFrame(resolve));
} catch (e) {
console.warn(e);
} }
await new Promise(resolve => requestAnimationFrame(resolve));
} }
})(); }