99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
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 }
|
|
}
|