forked from zhongyidian/super_video
feat: 对接各个视频数据
This commit is contained in:
1 parent
6103453e32
commit
6ab98c4764
339 files changed
+17316
-1531
No files matched your search
@@ -0,0 +1,330 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { getBackgroundImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useViewfinderA } from './useViewfinderA'
|
||||
import { useCanvasCapture } from '../composables/useCanvasCapture'
|
||||
import StartOverlay from '../components/StartOverlay.vue'
|
||||
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const start = ref(false)
|
||||
|
||||
// 基础逻辑
|
||||
const back = () => router.back()
|
||||
const handleFinish = () => router.push({ name: 'FilmFilmingLocationB' })
|
||||
|
||||
// 截图功能
|
||||
const { isLoading, showModal, capturedImageUrl, capture } = useCanvasCapture()
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
// 手机模式取景
|
||||
const {
|
||||
currentZoom,
|
||||
isCapturing,
|
||||
isDragging,
|
||||
setZoom,
|
||||
startDrag,
|
||||
onDrag,
|
||||
endDrag,
|
||||
getTransform,
|
||||
currentOffsetX,
|
||||
currentOffsetY,
|
||||
handleWheel
|
||||
} = useViewfinderA()
|
||||
|
||||
// 刻度盘配置常量
|
||||
const DIAL_STEP_DEG = 30 // 1x 到 2x 之间的角度差(度)
|
||||
const ZOOM_MIN = 1
|
||||
const ZOOM_MAX = 3
|
||||
const ZOOM_CENTER = 2
|
||||
const TICK_STEP = 0.1 // 刻度步长
|
||||
const MAJOR_TICK_INTERVAL = 5 // 主刻度间隔(0.5x)
|
||||
const INTEGER_TICK_INTERVAL = 10 // 整数刻度间隔(1.0x)
|
||||
|
||||
// 刻度数据类型
|
||||
interface DialTick {
|
||||
zoom: number
|
||||
angle: number
|
||||
isMajor: boolean
|
||||
isInteger: boolean
|
||||
label: string
|
||||
}
|
||||
|
||||
// 计算圆盘旋转角度
|
||||
// 目标:将当前 zoom 对应的刻度旋转到 0 度位置
|
||||
// 3x 在上方(-30deg),1x 在下方(+30deg)
|
||||
const dialRotation = computed(() => {
|
||||
return (currentZoom.value - ZOOM_CENTER) * DIAL_STEP_DEG
|
||||
})
|
||||
|
||||
// 格式化缩放倍数显示
|
||||
const formattedZoom = computed(() => {
|
||||
return currentZoom.value.toFixed(1) + 'x'
|
||||
})
|
||||
|
||||
// 生成刻度数据
|
||||
const dialTicks = computed<DialTick[]>(() => {
|
||||
const ticks: DialTick[] = []
|
||||
const minTick = ZOOM_MIN / TICK_STEP
|
||||
const maxTick = ZOOM_MAX / TICK_STEP
|
||||
|
||||
for (let z = minTick; z <= maxTick; z += 1) {
|
||||
const zoom = z * TICK_STEP
|
||||
// 刻度相对于中心(2x)的角度
|
||||
// 3x -> -30deg(上方),1x -> +30deg(下方)
|
||||
const angle = -(zoom - ZOOM_CENTER) * DIAL_STEP_DEG
|
||||
|
||||
const isMajor = z % MAJOR_TICK_INTERVAL === 0
|
||||
const isInteger = z % INTEGER_TICK_INTERVAL === 0
|
||||
|
||||
ticks.push({
|
||||
zoom,
|
||||
angle,
|
||||
isMajor,
|
||||
isInteger,
|
||||
label: isInteger ? zoom + 'x' : ''
|
||||
})
|
||||
}
|
||||
return ticks
|
||||
})
|
||||
|
||||
const photoImage = ref<HTMLImageElement | null>(null)
|
||||
|
||||
// Canvas 截图配置常量
|
||||
const CANVAS_WIDTH = 871
|
||||
const CANVAS_HEIGHT = 420
|
||||
const CANVAS_SCALE = 2 // 2倍高清
|
||||
const FLASH_DURATION = 100 // 闪光动画时长(ms)
|
||||
const UI_UPDATE_DELAY = 50 // UI 更新延迟(ms)
|
||||
const RESULT_DISPLAY_DELAY = 300 // 结果显示延迟(ms)
|
||||
|
||||
/**
|
||||
* 拍照功能
|
||||
* 使用 Canvas 手动绘制图片,避免 DOM 截图的兼容性问题
|
||||
*/
|
||||
/**
|
||||
* 拍照功能
|
||||
* 使用 Canvas 手动绘制图片,避免 DOM 截图的兼容性问题
|
||||
*/
|
||||
const capturePhoto = async () => {
|
||||
if (isLoading.value) return
|
||||
|
||||
isCapturing.value = true
|
||||
await new Promise(resolve => setTimeout(resolve, FLASH_DURATION))
|
||||
|
||||
await capture({
|
||||
width: CANVAS_WIDTH,
|
||||
height: CANVAS_HEIGHT,
|
||||
scale: CANVAS_SCALE,
|
||||
delay: UI_UPDATE_DELAY,
|
||||
resultDelay: RESULT_DISPLAY_DELAY,
|
||||
onDraw: ctx => {
|
||||
if (!photoImage.value) {
|
||||
throw new Error('Photo image not found')
|
||||
}
|
||||
|
||||
// 计算变换
|
||||
// 1. 移动到中心
|
||||
ctx.translate(CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2)
|
||||
// 2. 应用拖拽偏移
|
||||
ctx.translate(currentOffsetX.value, currentOffsetY.value)
|
||||
// 3. 应用缩放
|
||||
ctx.scale(currentZoom.value, currentZoom.value)
|
||||
// 4. 移回左上角绘制
|
||||
ctx.drawImage(
|
||||
photoImage.value,
|
||||
-CANVAS_WIDTH / 2,
|
||||
-CANVAS_HEIGHT / 2,
|
||||
CANVAS_WIDTH,
|
||||
CANVAS_HEIGHT
|
||||
)
|
||||
},
|
||||
onComplete: () => {
|
||||
isCapturing.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div
|
||||
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
|
||||
:style="{ backgroundImage: getBackgroundImage('film/shooting/龙美术馆模糊底图.png') }"
|
||||
>
|
||||
<ImgButton image="shooting/标题-取景地a.png" class="title-button absolute left-0" />
|
||||
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
|
||||
|
||||
<p
|
||||
class="absolute left-1/2 -translate-x-1/2 top-[260px] text-red-500 text-4xl leading-normal font-bold"
|
||||
>
|
||||
操作说明:在手机取景框区域,滚动鼠标滚轮可缩放画面,按住鼠标左键拖动可调整画面位置。
|
||||
</p>
|
||||
|
||||
<StartOverlay scene="a" v-if="!start" @start="start = true" />
|
||||
|
||||
<!-- 手机拍照界面 -->
|
||||
<div class="absolute bottom-0 w-full h-[766px]" v-else>
|
||||
<img
|
||||
class="phone-frame absolute w-full h-[766px] z-10 pointer-events-none"
|
||||
src="@/assets/images/shooting/shoot.png"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<!-- 拍摄区域 -->
|
||||
<div
|
||||
class="photo-wrapper absolute pointer-events-none top-[19.2%] left-[48.3%] -translate-x-1/2 w-[871px] h-[420px]"
|
||||
>
|
||||
<div class="photo-container w-full h-full overflow-hidden relative">
|
||||
<!-- 拍摄图片 -->
|
||||
<img
|
||||
ref="photoImage"
|
||||
class="absolute pointer-events-auto w-[871px] h-[420px] ease-out select-none cursor-move touch-none"
|
||||
:class="{
|
||||
'transition-transform duration-300': !isDragging,
|
||||
'cursor-grab': !isDragging,
|
||||
'cursor-grabbing': isDragging
|
||||
}"
|
||||
:style="{ transform: getTransform() }"
|
||||
src="@/assets/images/film/shooting/龙美术馆清晰图.png"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="onDrag"
|
||||
@mouseup="endDrag"
|
||||
@mouseleave="endDrag"
|
||||
@wheel="handleWheel"
|
||||
@touchstart="startDrag"
|
||||
@touchmove="onDrag"
|
||||
@touchend="endDrag"
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
<!-- 拍照闪光效果 -->
|
||||
<div
|
||||
v-if="isCapturing"
|
||||
class="flash-overlay absolute inset-0 bg-white animate-flash"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="shooting-decor relative pointer-events-none top-[19.2%] left-[48.3%] -translate-x-1/2 w-[871px] h-[420px] bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/拍摄装饰.png') }"
|
||||
>
|
||||
<!-- 仿苹果半圆盘缩放控件 -->
|
||||
<div
|
||||
class="zoom-dial-container absolute pointer-events-auto w-[150px] h-[300px] top-1/2 -translate-y-1/2 -rotate-2 right-[179px] overflow-hidden select-none mask-gradient"
|
||||
>
|
||||
<!-- 指示器 (固定在中心) -->
|
||||
<div
|
||||
class="absolute top-1/2 left-0 w-full -translate-y-1/2 z-20 flex items-center pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="w-0 h-0 border-t-[8px] border-t-transparent border-l-[14px] border-l-yellow-400 border-b-[8px] border-b-transparent ml-3"
|
||||
></div>
|
||||
<div
|
||||
class="ml-3 bg-black/70 backdrop-blur-md text-yellow-400 font-bold px-3 py-1 rounded text-lg shadow-lg"
|
||||
>
|
||||
{{ formattedZoom }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 旋转圆盘 -->
|
||||
<!-- 圆心在容器右侧外部,形成左侧弧形 -->
|
||||
<div
|
||||
class="dial-wheel absolute top-1/2 right-[-480px] w-[600px] h-[600px] rounded-full bg-black/30 backdrop-blur-md border-[5px] border-white/20 transition-transform duration-100 ease-out origin-center cursor-grab active:cursor-grabbing"
|
||||
:style="{ transform: `translateY(-50%) rotate(${dialRotation}deg)` }"
|
||||
>
|
||||
<div
|
||||
v-for="tick in dialTicks"
|
||||
:key="tick.zoom"
|
||||
class="absolute top-1/2 left-0 w-full h-0 flex justify-start items-center origin-center pointer-events-auto"
|
||||
:style="{ transform: `translate(0, -50%) rotate(${tick.angle}deg)` }"
|
||||
@click.stop="setZoom(tick.zoom)"
|
||||
>
|
||||
<!-- 刻度线本体 (位于圆盘左边缘) -->
|
||||
<div
|
||||
class="absolute left-3 bg-white/90 transition-all duration-200"
|
||||
:class="{
|
||||
'w-10 h-1 opacity-90': tick.isInteger,
|
||||
'w-6 h-0.5 opacity-60': !tick.isInteger && tick.isMajor,
|
||||
'w-3 h-[1px] opacity-40': !tick.isMajor
|
||||
}"
|
||||
></div>
|
||||
<!-- 刻度文字 -->
|
||||
<div
|
||||
v-if="tick.isInteger"
|
||||
class="absolute left-[52px] text-white/90 font-bold text-lg drop-shadow-md"
|
||||
:style="{ transform: `rotate(${-tick.angle - dialRotation}deg)` }"
|
||||
>
|
||||
<span
|
||||
:class="{ 'text-yellow-400 scale-110': currentZoom === tick.zoom }"
|
||||
class="block transition-all duration-200"
|
||||
>
|
||||
{{ tick.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍摄按钮 -->
|
||||
<ImgButton
|
||||
image="shooting/按钮-拍摄.png"
|
||||
class="capture-button absolute pointer-events-auto right-14 top-1/2 -translate-y-1/2 transition-transform active:scale-95"
|
||||
@click="capturePhoto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果弹窗 -->
|
||||
<Overlay :visible="showModal" @close="handleModalClose">
|
||||
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
|
||||
<!-- 截图内容 -->
|
||||
<div v-if="capturedImageUrl" class="bg-white p-2 rounded-md -rotate-6">
|
||||
<img :src="capturedImageUrl" alt="拍摄结果" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
|
||||
<ImgButton image="shooting/按钮-前往下一个取景地" @click="handleFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
|
||||
<LoadingOverlay :visible="isLoading" text="正在出片中..." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@keyframes flash {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-flash {
|
||||
animation: flash 0.3s ease-out;
|
||||
}
|
||||
|
||||
.mask-gradient {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
import { ref } from 'vue'
|
||||
import { getTouchDistance, getClientPosition } from '../utils/touchGestures'
|
||||
|
||||
/**
|
||||
* 手机拍照模式的取景逻辑
|
||||
* 支持缩放和拖拽功能
|
||||
*/
|
||||
export function useViewfinderA() {
|
||||
// 缩放限制
|
||||
const MIN_ZOOM = 1
|
||||
const MAX_ZOOM = 3
|
||||
|
||||
// 缩放级别 (保留用于点击按钮)
|
||||
const zoomLevels = [3, 2, 1]
|
||||
const currentZoom = ref(1)
|
||||
const isCapturing = ref(false) // 拍照闪光效果
|
||||
|
||||
// 拖拽相关状态
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartY = ref(0)
|
||||
const offsetX = ref(0)
|
||||
const offsetY = ref(0)
|
||||
const currentOffsetX = ref(0)
|
||||
const currentOffsetY = ref(0)
|
||||
|
||||
// 双指缩放相关
|
||||
const isPinching = ref(false)
|
||||
const pinchStartDistance = ref(0)
|
||||
const pinchStartZoom = ref(1)
|
||||
|
||||
// 计算边界限制
|
||||
const limitOffset = () => {
|
||||
const containerWidth = 871
|
||||
const containerHeight = 420
|
||||
const scale = currentZoom.value
|
||||
|
||||
// 计算图片缩放后的尺寸
|
||||
const scaledWidth = containerWidth * scale
|
||||
const scaledHeight = containerHeight * scale
|
||||
|
||||
// 计算最大偏移量(图片边缘不能超出容器中心)
|
||||
const maxOffsetX = (scaledWidth - containerWidth) / 2
|
||||
const maxOffsetY = (scaledHeight - containerHeight) / 2
|
||||
|
||||
// 限制偏移量
|
||||
currentOffsetX.value = Math.max(-maxOffsetX, Math.min(maxOffsetX, currentOffsetX.value))
|
||||
currentOffsetY.value = Math.max(-maxOffsetY, Math.min(maxOffsetY, currentOffsetY.value))
|
||||
}
|
||||
|
||||
// 设置缩放级别
|
||||
const setZoom = (zoom: number) => {
|
||||
currentZoom.value = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom))
|
||||
limitOffset()
|
||||
}
|
||||
|
||||
// 鼠标滚轮缩放
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
// 滚轮向下是正数(缩小),向上是负数(放大)
|
||||
const delta = -e.deltaY * 0.001
|
||||
const newZoom = currentZoom.value + delta
|
||||
setZoom(newZoom)
|
||||
}
|
||||
|
||||
// 开始拖拽 / 触摸
|
||||
const startDrag = (e: MouseEvent | TouchEvent) => {
|
||||
// 双指缩放
|
||||
if ('touches' in e && e.touches.length === 2) {
|
||||
isPinching.value = true
|
||||
isDragging.value = false
|
||||
pinchStartDistance.value = getTouchDistance(e.touches[0], e.touches[1])
|
||||
pinchStartZoom.value = currentZoom.value
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// 单指拖拽
|
||||
if ('touches' in e && e.touches.length !== 1) return
|
||||
|
||||
isDragging.value = true
|
||||
isPinching.value = false
|
||||
|
||||
const { x: clientX, y: clientY } = getClientPosition(e)
|
||||
|
||||
dragStartX.value = clientX
|
||||
dragStartY.value = clientY
|
||||
offsetX.value = currentOffsetX.value
|
||||
offsetY.value = currentOffsetY.value
|
||||
}
|
||||
|
||||
// 拖拽中 / 缩放中
|
||||
const onDrag = (e: MouseEvent | TouchEvent) => {
|
||||
// 处理双指缩放
|
||||
if (isPinching.value && 'touches' in e && e.touches.length === 2) {
|
||||
const currentDistance = getTouchDistance(e.touches[0], e.touches[1])
|
||||
const scale = currentDistance / pinchStartDistance.value
|
||||
setZoom(pinchStartZoom.value * scale)
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
if (!isDragging.value) return
|
||||
|
||||
const { x: clientX, y: clientY } = getClientPosition(e)
|
||||
|
||||
const deltaX = clientX - dragStartX.value
|
||||
const deltaY = clientY - dragStartY.value
|
||||
|
||||
currentOffsetX.value = offsetX.value + deltaX
|
||||
currentOffsetY.value = offsetY.value + deltaY
|
||||
|
||||
limitOffset()
|
||||
|
||||
// 防止滚动
|
||||
if (e.cancelable) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
// 结束拖拽
|
||||
const endDrag = () => {
|
||||
isDragging.value = false
|
||||
isPinching.value = false
|
||||
}
|
||||
|
||||
// 计算transform样式
|
||||
const getTransform = () => {
|
||||
return `translate(${currentOffsetX.value}px, ${currentOffsetY.value}px) scale(${currentZoom.value})`
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
zoomLevels,
|
||||
currentZoom,
|
||||
isCapturing,
|
||||
isDragging,
|
||||
currentOffsetX,
|
||||
currentOffsetY,
|
||||
|
||||
// 方法
|
||||
setZoom,
|
||||
startDrag,
|
||||
onDrag,
|
||||
endDrag,
|
||||
handleWheel,
|
||||
getTransform
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user