forked from zhongyidian/super_video
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
302 lines
9.1 KiB
Vue
302 lines
9.1 KiB
Vue
<script lang="ts" setup>
|
||
import { ref } from 'vue'
|
||
import { getBackgroundImage } from '@/utils'
|
||
import { ImgButton } from '@/components/Button'
|
||
import { Overlay } from '@/components/Overlay'
|
||
import { useViewfinderB } from './useViewfinderB'
|
||
import { useCanvasCapture } from '../composables/useCanvasCapture'
|
||
import StartOverlay from '../components/StartOverlay.vue'
|
||
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
|
||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||
import { useRouter } from 'vue-router'
|
||
|
||
const router = useRouter()
|
||
const start = ref(false)
|
||
|
||
// 基础逻辑
|
||
const back = () => router.back()
|
||
|
||
// 截图功能
|
||
const { isLoading, showModal, capturedImageUrl, capture } = useCanvasCapture()
|
||
|
||
const handleModalClose = () => {
|
||
showModal.value = false
|
||
}
|
||
|
||
// 自由取景框模式
|
||
const {
|
||
viewfinderContainer,
|
||
viewfinderBox,
|
||
viewfinder,
|
||
startDrag,
|
||
startResize,
|
||
} = useViewfinderB()
|
||
|
||
const showGuide = ref(false)
|
||
const backgroundImage = ref<HTMLImageElement | null>(null)
|
||
|
||
// Canvas 截图配置常量
|
||
const UI_UPDATE_DELAY = 50 // UI 更新延迟(ms)
|
||
const CANVAS_SCALE = 2 // Canvas 缩放比例(2倍高清)
|
||
const GUIDE_SHOW_DELAY = 1000 // 引导显示延迟(ms)
|
||
|
||
/**
|
||
* 截取场景
|
||
* 使用 Canvas 手动绘制,确保移动端兼容性
|
||
*/
|
||
/**
|
||
* 截取场景
|
||
* 使用 Canvas 手动绘制,确保移动端兼容性
|
||
*/
|
||
const captureScene = async () => {
|
||
if (isLoading.value) return
|
||
|
||
await capture({
|
||
width: viewfinder.value.width,
|
||
height: viewfinder.value.height,
|
||
scale: CANVAS_SCALE,
|
||
delay: UI_UPDATE_DELAY,
|
||
onDraw: (ctx) => {
|
||
if (!backgroundImage.value) {
|
||
throw new Error('Background image not found')
|
||
}
|
||
|
||
// 计算图片实际尺寸和设计稿尺寸的缩放比例
|
||
// viewfinder 的坐标是基于设计稿尺寸(1920x1080)
|
||
// 但 backgroundImage 的实际尺寸可能不同
|
||
const imageScaleX = backgroundImage.value.naturalWidth / 1920
|
||
const imageScaleY = backgroundImage.value.naturalHeight / 1080
|
||
|
||
// 绘制背景图片的裁剪区域
|
||
// 将设计稿坐标转换为图片实际坐标
|
||
ctx.drawImage(
|
||
backgroundImage.value,
|
||
viewfinder.value.x * imageScaleX, // 源图 x(转换为实际坐标)
|
||
viewfinder.value.y * imageScaleY, // 源图 y(转换为实际坐标)
|
||
viewfinder.value.width * imageScaleX, // 源图 width(转换为实际坐标)
|
||
viewfinder.value.height * imageScaleY, // 源图 height(转换为实际坐标)
|
||
0, // 目标 x
|
||
0, // 目标 y
|
||
viewfinder.value.width, // 目标 width
|
||
viewfinder.value.height, // 目标 height
|
||
)
|
||
},
|
||
onComplete: () => {
|
||
// 延迟显示引导
|
||
setTimeout(() => {
|
||
showGuide.value = true
|
||
}, GUIDE_SHOW_DELAY)
|
||
},
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 完成引导,跳转到 Vlog 页面
|
||
*/
|
||
const handleComplete = () => {
|
||
showGuide.value = false
|
||
router.push({
|
||
name: 'Interview',
|
||
})
|
||
}
|
||
</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: start
|
||
? getBackgroundImage('interview/shooting/教学楼-清晰.png')
|
||
: getBackgroundImage('interview/shooting/教学楼-模糊.png'),
|
||
}"
|
||
>
|
||
<!-- 标题 -->
|
||
<ImgButton
|
||
image="interview/shooting/教学楼-侧边标题.png"
|
||
class="title-button absolute left-0"
|
||
/>
|
||
|
||
<!-- 返回按钮 -->
|
||
<ImgButton
|
||
image="shooting/返回.png"
|
||
class="back-button absolute right-0 z-50"
|
||
@click="back"
|
||
/>
|
||
|
||
<StartOverlay
|
||
scene="b"
|
||
v-if="!start"
|
||
@start="start = true"
|
||
/>
|
||
|
||
<!-- 取景 -->
|
||
<div
|
||
class="absolute bottom-0 w-full h-full"
|
||
v-else
|
||
>
|
||
<!-- 隐藏的背景图片,用于 Canvas 绘制 -->
|
||
<img
|
||
ref="backgroundImage"
|
||
class="absolute inset-0 w-full h-full object-cover pointer-events-none opacity-0"
|
||
src="@/assets/images/interview/shooting/教学楼-清晰.png"
|
||
alt=""
|
||
crossorigin="anonymous"
|
||
/>
|
||
|
||
<!-- 取景框容器 -->
|
||
<div
|
||
ref="viewfinderContainer"
|
||
class="relative w-full h-full"
|
||
>
|
||
<!-- 四个方向的遮罩层 -->
|
||
<!-- 上遮罩 -->
|
||
<div
|
||
class="mask-layer absolute left-0 right-0 bg-black/60 pointer-events-none z-10"
|
||
:style="{
|
||
top: 0,
|
||
height: viewfinder.y + 'px',
|
||
}"
|
||
></div>
|
||
<!-- 下遮罩 -->
|
||
<div
|
||
class="mask-layer absolute left-0 right-0 bg-black/60 pointer-events-none z-10"
|
||
:style="{
|
||
top: viewfinder.y + viewfinder.height + 'px',
|
||
bottom: 0,
|
||
}"
|
||
></div>
|
||
<!-- 左遮罩 -->
|
||
<div
|
||
class="mask-layer absolute left-0 bg-black/60 pointer-events-none z-10"
|
||
:style="{
|
||
top: viewfinder.y + 'px',
|
||
width: viewfinder.x + 'px',
|
||
height: viewfinder.height + 'px',
|
||
}"
|
||
></div>
|
||
<!-- 右遮罩 -->
|
||
<div
|
||
class="mask-layer absolute right-0 bg-black/60 pointer-events-none z-10"
|
||
:style="{
|
||
top: viewfinder.y + 'px',
|
||
left: viewfinder.x + viewfinder.width + 'px',
|
||
height: viewfinder.height + 'px',
|
||
}"
|
||
></div>
|
||
|
||
<!-- 可拖动调整的取景框 -->
|
||
<div
|
||
ref="viewfinderBox"
|
||
class="viewfinder-frame absolute border-4 border-white cursor-move z-20"
|
||
:style="{
|
||
left: viewfinder.x + 'px',
|
||
top: viewfinder.y + 'px',
|
||
width: viewfinder.width + 'px',
|
||
height: viewfinder.height + 'px',
|
||
}"
|
||
@mousedown="startDrag"
|
||
@touchstart="startDrag"
|
||
>
|
||
<!-- 九宫格辅助线 -->
|
||
<div
|
||
class="grid-lines absolute inset-0 grid grid-cols-3 grid-rows-3"
|
||
>
|
||
<div
|
||
v-for="i in 9"
|
||
:key="i"
|
||
class="border border-white/40"
|
||
:class="{
|
||
'border-t-0': i <= 3,
|
||
'border-b-0': i >= 7,
|
||
'border-l-0': (i - 1) % 3 === 0,
|
||
'border-r-0': i % 3 === 0,
|
||
}"
|
||
></div>
|
||
</div>
|
||
|
||
<!-- 四个角的调整手柄 -->
|
||
<div
|
||
class="resize-handle absolute w-6 h-6 bg-white rounded-full -left-3 -top-3 cursor-nwse-resize"
|
||
@mousedown.stop="startResize('nw', $event)"
|
||
@touchstart.stop="startResize('nw', $event)"
|
||
></div>
|
||
<div
|
||
class="resize-handle absolute w-6 h-6 bg-white rounded-full -right-3 -top-3 cursor-nesw-resize"
|
||
@mousedown.stop="startResize('ne', $event)"
|
||
@touchstart.stop="startResize('ne', $event)"
|
||
></div>
|
||
<div
|
||
class="resize-handle absolute w-6 h-6 bg-white rounded-full -left-3 -bottom-3 cursor-nesw-resize"
|
||
@mousedown.stop="startResize('sw', $event)"
|
||
@touchstart.stop="startResize('sw', $event)"
|
||
></div>
|
||
<div
|
||
class="resize-handle absolute w-6 h-6 bg-white rounded-full -right-3 -bottom-3 cursor-nwse-resize"
|
||
@mousedown.stop="startResize('se', $event)"
|
||
@touchstart.stop="startResize('se', $event)"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部取景按钮 - 跟随取景框 -->
|
||
<div
|
||
class="capture-button-container absolute z-20 -translate-x-1/2 -translate-y-1/2"
|
||
:style="{
|
||
left: viewfinder.x + viewfinder.width / 2 + 'px',
|
||
top: viewfinder.y + viewfinder.height + 'px',
|
||
}"
|
||
>
|
||
<ImgButton
|
||
class="w-36"
|
||
image="shooting/按钮-取景"
|
||
@click="captureScene"
|
||
/>
|
||
</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="正在出片中..."
|
||
/>
|
||
|
||
<!-- 引导步骤 -->
|
||
<GuideSteps
|
||
v-if="showGuide"
|
||
positionOffsetY="80%"
|
||
@complete="handleComplete"
|
||
>
|
||
<GuideStepsItem>
|
||
实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~
|
||
</GuideStepsItem>
|
||
</GuideSteps>
|
||
</div>
|
||
</template>
|