feat(gui): 增强显示屏预览效果。

This commit is contained in:
Ivan Li 2023-03-21 23:42:02 +08:00
parent 9ed2fa8b53
commit 3ede04c31b
4 changed files with 156 additions and 37 deletions

View File

@ -15,7 +15,7 @@ function App() {
}); });
return ( return (
<div class="container"> <div>
<DisplayListContainer> <DisplayListContainer>
{displayStore.displays.map((display) => { {displayStore.displays.map((display) => {
return <DisplayView display={display} />; return <DisplayView display={display} />;

View File

@ -1,15 +1,45 @@
import { createEffect, createMemo, createSignal, on, ParentComponent } from 'solid-js'; import {
createEffect,
createSignal,
onCleanup,
onMount,
ParentComponent,
} from 'solid-js';
import { displayStore, setDisplayStore } from '../stores/display.store'; import { displayStore, setDisplayStore } from '../stores/display.store';
export const DisplayListContainer: ParentComponent = (props) => { export const DisplayListContainer: ParentComponent = (props) => {
let root: HTMLElement;
const [olStyle, setOlStyle] = createSignal({ const [olStyle, setOlStyle] = createSignal({
top: '0px', top: '0px',
left: '0px', left: '0px',
}); });
const [rootStyle, setRootStyle] = createSignal({ const [rootStyle, setRootStyle] = createSignal({
width: '100%', // width: '100%',
height: '100%', height: '100%',
}); });
const [bound, setBound] = createSignal({
left: 0,
top: 0,
right: 100,
bottom: 100,
});
const resetSize = () => {
const _bound = bound();
setDisplayStore({
viewScale: root.clientWidth / (_bound.right - _bound.left),
});
setOlStyle({
top: `${-_bound.top * displayStore.viewScale}px`,
left: `${-_bound.left * displayStore.viewScale}px`,
});
setRootStyle({
height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`,
});
};
createEffect(() => { createEffect(() => {
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x)); const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
@ -23,22 +53,27 @@ export const DisplayListContainer: ParentComponent = (props) => {
...displayStore.displays.map((display) => display.y + display.height), ...displayStore.displays.map((display) => display.y + display.height),
); );
setDisplayStore({ setBound({
viewScale: 1200 / (boundRight - boundLeft), left: boundLeft,
top: boundTop,
right: boundRight,
bottom: boundBottom,
});
let observer: ResizeObserver;
onMount(() => {
observer = new ResizeObserver(resetSize);
observer.observe(root);
}); });
setOlStyle({ onCleanup(() => {
top: `${-boundTop * displayStore.viewScale}px`, observer?.unobserve(root);
left: `${-boundLeft * displayStore.viewScale}px`,
});
setRootStyle({
width: `${(boundRight - boundLeft) * displayStore.viewScale}px`,
height: `${(boundBottom - boundTop) * displayStore.viewScale}px`,
}); });
}); });
createEffect(() => {});
return ( return (
<section class="relative bg-gray-400/30" style={rootStyle()}> <section ref={root!} class="relative bg-gray-400/30" style={rootStyle()}>
<ol class="absolute bg-gray-700" style={olStyle()}> <ol class="absolute bg-gray-700" style={olStyle()}>
{props.children} {props.children}
</ol> </ol>

View File

@ -16,18 +16,29 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
const style = createMemo(() => ({ const style = createMemo(() => ({
top: `${props.display.y * displayStore.viewScale}px`, top: `${props.display.y * displayStore.viewScale}px`,
left: `${props.display.x * displayStore.viewScale}px`, left: `${props.display.x * displayStore.viewScale}px`,
height: `${size().height}px`,
width: `${size().width}px`,
})); }));
return ( return (
<section class="absolute bg-gray-300" style={style()}> <section
class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
style={style()}
>
<ScreenView <ScreenView
class="row-start-2 col-start-2"
displayId={props.display.id} displayId={props.display.id}
height={size().height} style={{
width={size().width} 'object-fit': 'contain',
}}
/> />
<DisplayInfoPanel <DisplayInfoPanel
display={props.display} display={props.display}
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black" class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
/> />
<div class="row-start-1 col-start-2">Test</div>
<div class="row-start-2 col-start-1">Test</div>
<div class="row-start-2 col-start-3">Test</div>
<div class="row-start-3 col-start-2">Test</div>
</section> </section>
); );
}; };

View File

@ -13,9 +13,7 @@ import {
type ScreenViewProps = { type ScreenViewProps = {
displayId: number; displayId: number;
height: number; } & JSX.HTMLAttributes<HTMLDivElement>;
width: number;
} & Omit<JSX.HTMLAttributes<HTMLCanvasElement>, 'height' | 'width'>;
async function subscribeScreenshotUpdate(displayId: number) { async function subscribeScreenshotUpdate(displayId: number) {
await invoke('subscribe_encoded_screenshot_updated', { await invoke('subscribe_encoded_screenshot_updated', {
@ -26,7 +24,63 @@ async function subscribeScreenshotUpdate(displayId: number) {
export const ScreenView: Component<ScreenViewProps> = (props) => { export const ScreenView: Component<ScreenViewProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['displayId']); const [localProps, rootProps] = splitProps(props, ['displayId']);
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let root: HTMLDivElement;
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null); const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
const [drawInfo, setDrawInfo] = createSignal({
drawX: 0,
drawY: 0,
drawWidth: 0,
drawHeight: 0,
});
const [imageData, setImageData] = createSignal<{
buffer: Uint8ClampedArray;
width: number;
height: number;
} | null>(null);
const resetSize = () => {
const aspectRatio = canvas.width / canvas.height;
const drawWidth = Math.round(
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
);
const drawHeight = Math.round(
Math.min(root.clientHeight, root.clientWidth / aspectRatio),
);
const drawX = Math.round((root.clientWidth - drawWidth) / 2);
const drawY = Math.round((root.clientHeight - drawHeight) / 2);
setDrawInfo({
drawX,
drawY,
drawWidth,
drawHeight,
});
canvas.width = root.clientWidth;
canvas.height = root.clientHeight;
draw(true);
};
const draw = (cached: boolean = false) => {
const { drawX, drawY, drawWidth, drawHeight } = drawInfo();
let _ctx = ctx();
let raw = imageData();
if (_ctx && raw) {
_ctx.clearRect(0, 0, canvas.width, canvas.height);
if (cached) {
for (let i = 3; i < raw.buffer.length; i += 8) {
raw.buffer[i] = Math.floor(raw.buffer[i] * 0.7);
}
}
const img = new ImageData(raw.buffer, raw.width, raw.height);
_ctx.putImageData(img, drawX, drawY);
}
};
createEffect(() => { createEffect(() => {
const unlisten = listen<{ const unlisten = listen<{
base64_image: string; base64_image: string;
@ -34,9 +88,10 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
height: number; height: number;
width: number; width: number;
}>('encoded-screenshot-updated', (event) => { }>('encoded-screenshot-updated', (event) => {
const { drawWidth, drawHeight } = drawInfo();
if (event.payload.display_id === localProps.displayId) { if (event.payload.display_id === localProps.displayId) {
const url = convertFileSrc( const url = convertFileSrc(
`displays/${localProps.displayId}?width=${canvas.width}&height=${canvas.height}`, `displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
'ambient-light', 'ambient-light',
); );
fetch(url, { fetch(url, {
@ -44,18 +99,17 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}) })
.then((res) => res.body?.getReader().read()) .then((res) => res.body?.getReader().read())
.then((buffer) => { .then((buffer) => {
console.log(buffer?.value?.length); // console.log(buffer?.value?.length);
if (buffer?.value) {
let _ctx = ctx(); setImageData({
if (_ctx && buffer?.value) { buffer: new Uint8ClampedArray(buffer?.value),
_ctx.clearRect(0, 0, canvas.width, canvas.height); width: drawWidth,
const img = new ImageData( height: drawHeight,
new Uint8ClampedArray(buffer.value), });
canvas.width, } else {
canvas.height, setImageData(null);
);
_ctx.putImageData(img, 0, 0);
} }
draw();
}); });
} }
@ -63,10 +117,6 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}); });
subscribeScreenshotUpdate(localProps.displayId); subscribeScreenshotUpdate(localProps.displayId);
onMount(() => {
setCtx(canvas.getContext('2d'));
});
onCleanup(() => { onCleanup(() => {
unlisten.then((unlisten) => { unlisten.then((unlisten) => {
unlisten(); unlisten();
@ -74,5 +124,28 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
}); });
}); });
return <canvas ref={canvas!} class="object-contain" {...rootProps} />; createEffect(() => {
let resizeObserver: ResizeObserver;
onMount(() => {
setCtx(canvas.getContext('2d'));
new ResizeObserver(() => {
resetSize();
}).observe(root);
});
onCleanup(() => {
resizeObserver?.unobserve(root);
});
});
return (
<div
ref={root!}
{...rootProps}
class={'overflow-hidden h-full w-full ' + rootProps.class}
>
<canvas ref={canvas!} />
</div>
);
}; };