feat(fc2a): add ImageCanvas (pan/zoom) and VideoCanvas (with format gate)

ImageCanvas mounts an <img> wrapped in the usePanZoom composable —
spreading its handlers onto the container element. Zoom resets when src
changes so prev/next nav doesn't carry zoom state into the next image.

VideoCanvas plays MP4/WebM/Ogg/MOV natively. Non-playable formats render
a friendly explainer with a download fallback, naming the actual MIME so
operators know what they're dealing with, and pointing forward at FC-2e
where transcoding lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 12:33:41 -04:00
parent 8358d09348
commit 0faeebf3aa
2 changed files with 109 additions and 0 deletions
@@ -0,0 +1,50 @@
<template>
<div
class="fc-canvas"
:class="{ 'fc-canvas--zoomed': panZoom.state.scale > 1 }"
@wheel="panZoom.handlers.onWheel"
@pointerdown="panZoom.handlers.onPointerDown"
@pointermove="panZoom.handlers.onPointerMove"
@pointerup="panZoom.handlers.onPointerUp"
@click="panZoom.handlers.onClick"
>
<img
:src="src" :alt="alt"
:style="{
transform: `translate(${panZoom.state.x}px, ${panZoom.state.y}px) scale(${panZoom.state.scale})`
}"
draggable="false"
>
</div>
</template>
<script setup>
import { watch } from 'vue'
import { usePanZoom } from '../../composables/usePanZoom.js'
const props = defineProps({ src: String, alt: String })
const panZoom = usePanZoom()
// Reset zoom when the src changes (prev/next nav).
watch(() => props.src, () => panZoom.reset())
</script>
<style scoped>
.fc-canvas {
flex: 1;
position: relative;
background: rgb(var(--v-theme-background));
overflow: hidden;
cursor: zoom-in;
user-select: none;
display: flex; align-items: center; justify-content: center;
}
.fc-canvas--zoomed { cursor: grab; }
.fc-canvas--zoomed:active { cursor: grabbing; }
.fc-canvas img {
max-width: 100%; max-height: 100%;
transform-origin: center;
transition: transform 100ms ease-out;
pointer-events: none;
}
</style>