onnx-web-upscale / src /components /ComparePanel.vue
notaneimu's picture
update preview, dowscale options
7083032
Raw
History Blame Contribute Delete
7.52 kB
<script setup lang="ts">
import { computed, ref, type CSSProperties } from "vue";
import { useCompareStage } from "../composables/useCompareStage";
import type { ResultInfo } from "../lib/types";
const props = defineProps<{
baseImageUrl: string;
overlayImageUrl: string;
baseLabel: string;
baseResolution: string;
overlayResolution: string;
emptyMessage: string;
resultInfo: ResultInfo;
canDownload: boolean;
statusTone: string;
}>();
const emit = defineEmits<{
download: [];
}>();
const compareStageRef = ref<HTMLElement | null>(null);
const hasBaseImage = computed(() => Boolean(props.baseImageUrl));
const hasOverlayImage = computed(() => Boolean(props.overlayImageUrl));
const { handleWheel, preventContextMenu, startCompareDrag, styleVars, zoomLabel, zoomScale } =
useCompareStage(compareStageRef, hasBaseImage, hasOverlayImage);
const progressFillWidth = computed(() => {
const progressMatch = props.resultInfo.progress.match(/(\d+(?:\.\d+)?)%/);
const rawProgress = progressMatch ? Number.parseFloat(progressMatch[1]) : 0;
const clampedProgress = Number.isFinite(rawProgress) ? Math.min(100, Math.max(0, rawProgress)) : 0;
const tileMatch = props.resultInfo.tiles.match(/^(\d+)\s*\/\s*(\d+)$/);
const totalTiles = tileMatch ? Number.parseInt(tileMatch[2], 10) : 0;
if (totalTiles === 1 && clampedProgress < 100) {
return `${Math.max(clampedProgress, 33.3333)}%`;
}
return `${clampedProgress}%`;
});
const progressFillClass = computed(() => {
if (props.statusTone === "success") {
return "bg-emerald-400/80";
}
if (props.statusTone === "error") {
return "bg-rose-400/80";
}
if (props.statusTone === "busy") {
return "bg-amber-400/80";
}
return "bg-stone-300/90";
});
const checkerboardBackground = [
"linear-gradient(45deg, #f0f0f0 25%, transparent 25%) -16px 0 / 32px 32px",
"linear-gradient(-45deg, #f0f0f0 25%, transparent 25%) -16px 0 / 32px 32px",
"linear-gradient(45deg, transparent 75%, #f0f0f0 75%) -16px 0 / 32px 32px",
"linear-gradient(-45deg, transparent 75%, #f0f0f0 75%) -16px 0 / 32px 32px",
"#fff",
].join(", ");
const compareStageStyle = computed(() => ({
...styleVars.value,
background: checkerboardBackground,
}));
const baseImageStyle = computed<CSSProperties>(() => ({
transform: "scale(var(--zoom))",
transformOrigin: "var(--origin-x) var(--origin-y)",
imageRendering: zoomScale.value > 1 ? "pixelated" : "auto",
}));
const overlayImageStyle = computed<CSSProperties>(() => ({
...baseImageStyle.value,
clipPath: "inset(0 calc(100% - var(--split-adjusted)) 0 0)",
}));
const dividerStyle = {
left: "calc(var(--split) - 1px)",
background: "linear-gradient(180deg, transparent, #666 18%, #333 50%, #666 82%, transparent)",
boxShadow: "0 0 0 1px rgba(0,0,0,0.1), 0 0 12px rgba(0,0,0,0.2)",
};
</script>
<template>
<section class="flex min-h-0 flex-1 flex-col gap-2">
<div
ref="compareStageRef"
class="relative flex-1 overflow-hidden rounded-xl border border-stone-300 bg-white touch-none"
:style="compareStageStyle"
@pointerdown="startCompareDrag"
@contextmenu="preventContextMenu"
@wheel="handleWheel"
>
<img
:src="baseImageUrl"
class="pointer-events-none absolute inset-0 h-full w-full select-none object-contain"
:style="baseImageStyle"
:alt="`${baseLabel} image`"
:hidden="!hasBaseImage"
/>
<img
:src="overlayImageUrl"
class="pointer-events-none absolute inset-0 h-full w-full select-none object-contain"
:style="overlayImageStyle"
alt="Upscaled output"
:hidden="!hasOverlayImage"
/>
<div
class="pointer-events-none absolute inset-y-0 w-0.5"
:style="dividerStyle"
:hidden="!(hasBaseImage && hasOverlayImage)"
></div>
<div
class="pointer-events-none absolute left-3 top-3 flex flex-col gap-0.5 rounded-lg border border-stone-300 bg-white/95 px-2.5 py-1.5 text-xs text-stone-600 shadow-sm"
:hidden="!hasOverlayImage"
>
<div class="font-semibold text-stone-800">Upscaled</div>
<div class="text-[11px] text-stone-500">{{ overlayResolution }}</div>
</div>
<div
class="pointer-events-none absolute right-3 top-3 flex flex-col gap-0.5 rounded-lg border border-stone-300 bg-white/95 px-2.5 py-1.5 text-xs text-stone-600 shadow-sm"
:hidden="!hasBaseImage"
>
<div class="font-semibold text-stone-800">{{ baseLabel }}</div>
<div class="text-[11px] text-stone-500">{{ baseResolution }}</div>
</div>
<div
class="absolute bottom-3 right-3 min-w-14 rounded-lg border border-stone-300 bg-white/95 px-2.5 py-1.5 text-center text-xs font-medium text-stone-600 shadow-sm"
>
{{ zoomLabel }}
</div>
<div
class="absolute inset-0 grid place-items-center bg-white/50 p-8 text-center text-sm text-stone-500"
:hidden="hasBaseImage"
>
{{ emptyMessage }}
</div>
</div>
<div
class="relative overflow-hidden rounded-xl border border-stone-300 bg-white/85 px-4 py-2.5 text-xs text-stone-700 shadow-sm"
>
<div
:class="[
'pointer-events-none absolute left-0 top-0 h-2 transition-[width] duration-300',
progressFillClass,
]"
:style="{ width: progressFillWidth }"
></div>
<div class="relative flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-wrap gap-x-4 gap-y-2">
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Progress:</span>
<span class="text-stone-800">{{ resultInfo.progress }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Tiles:</span>
<span class="text-stone-800">{{ resultInfo.tiles }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Avg/tile:</span>
<span class="text-stone-800">{{ resultInfo.speed }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Run time:</span>
<span class="text-stone-800">{{ resultInfo.totalTime }}</span>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-2">
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Resolution:</span>
<span class="text-stone-800">{{ resultInfo.resolution }}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="font-medium text-stone-500">Size:</span>
<span class="text-stone-800">{{ resultInfo.size }}</span>
</div>
<button
type="button"
class="inline-flex items-center justify-center rounded-md bg-white px-2.5 py-1.5 text-xs font-semibold text-stone-900 ring-1 ring-inset ring-stone-300 transition hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-400 disabled:cursor-not-allowed disabled:bg-stone-100 disabled:text-stone-400 disabled:ring-stone-200"
:disabled="!canDownload"
@click="emit('download')"
>
Download
</button>
</div>
</div>
</div>
</section>
</template>