feat(variant): image preview on variant creation with a real sample image

This commit is contained in:
Florian Sylvain committed 2026-10-13 17:29:56 +02:00
1 parent a1cdfce529
commit 04dc151ed7
9 files changed
+314 -40

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
import { onScopeDispose, ref, watch, type Ref } from 'vue'
import sampleUrl from '@/assets/variant-sample.jpg'
import { SAMPLE_SOURCE, type EstimateRequest } from '@/lib/variants/variantEstimate'
import { outputSize } from '@/lib/variants/variantOutput'
import { canRender, drawPlan, flattensTransparency, mimeFor } from '@/lib/variants/variantRender'
const RENDER_DELAY_MS = 150
const PERCENT = 100
export interface RenderedVariant {
url: string
bytes: number
}
let sampleImage: Promise<HTMLImageElement | null> | null = null
function loadSample() {
sampleImage ??= new Promise((resolve) => {
const image = new Image()
image.onload = () => resolve(image)
image.onerror = () => resolve(null)
image.src = sampleUrl
})
return sampleImage
}
function encode(canvas: HTMLCanvasElement, request: EstimateRequest) {
const mime = mimeFor(request.format)
const quality = mime === 'image/png' ? undefined : request.quality / PERCENT
return new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, mime, quality))
}
async function renderVariant(image: HTMLImageElement, request: EstimateRequest) {
const size = outputSize(SAMPLE_SOURCE, request)
if (size === null || !canRender(size)) {
return null
}
const canvas = document.createElement('canvas')
canvas.width = size.width
canvas.height = size.height
const context = canvas.getContext('2d')
if (context === null) {
return null
}
if (flattensTransparency(request.format)) {
context.fillStyle = 'white'
context.fillRect(0, 0, size.width, size.height)
}
const plan = drawPlan(SAMPLE_SOURCE, size, request.fit)
context.drawImage(image, plan.sx, plan.sy, plan.sw, plan.sh, plan.dx, plan.dy, plan.dw, plan.dh)
return encode(canvas, request)
}
export function useVariantRender(request: Ref<EstimateRequest | null>) {
const original = ref<string | null>(null)
const variant = ref<RenderedVariant | null>(null)
let timer: ReturnType<typeof setTimeout> | undefined
let token = 0
function release() {
if (variant.value) {
URL.revokeObjectURL(variant.value.url)
variant.value = null
}
}
async function run(current: EstimateRequest | null, ticket: number) {
const image = await loadSample()
if (image === null || ticket !== token) {
return
}
original.value = sampleUrl
const blob = current === null ? null : await renderVariant(image, current)
if (ticket !== token) {
return
}
release()
if (blob !== null) {
variant.value = { url: URL.createObjectURL(blob), bytes: blob.size }
}
}
function schedule(current: EstimateRequest | null) {
clearTimeout(timer)
const ticket = ++token
timer = setTimeout(() => void run(current, ticket), RENDER_DELAY_MS)
}
watch(request, schedule, { immediate: true, deep: true })
onScopeDispose(() => {
clearTimeout(timer)
token++
release()
})
return { original, variant }
}