feat: switch hotspot coordinates from container percentage to absolute content pixels
This commit is contained in:
@@ -3,6 +3,7 @@ export interface SceneNode {
|
|||||||
type?: 'video' | 'image'
|
type?: 'video' | 'image'
|
||||||
videoUrl: string
|
videoUrl: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
contentSize?: { w: number; h: number }
|
||||||
subtitleUrl?: string
|
subtitleUrl?: string
|
||||||
subtitles?: Record<string, string>
|
subtitles?: Record<string, string>
|
||||||
choices?: Choice[]
|
choices?: Choice[]
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"type": "image",
|
"type": "image",
|
||||||
"videoUrl": "",
|
"videoUrl": "",
|
||||||
"imageUrl": "investigation_site/investigation_scene.jpg",
|
"imageUrl": "investigation_site/investigation_scene.jpg",
|
||||||
|
"contentSize": { "w": 1280, "h": 720 },
|
||||||
"subtitleUrl": "investigation_site/investigation.vtt",
|
"subtitleUrl": "investigation_site/investigation.vtt",
|
||||||
"subtitles": {
|
"subtitles": {
|
||||||
"zh": "investigation_site/investigation.vtt",
|
"zh": "investigation_site/investigation.vtt",
|
||||||
@@ -123,7 +124,7 @@
|
|||||||
"id": "hs_desk",
|
"id": "hs_desk",
|
||||||
"label": "查看书桌",
|
"label": "查看书桌",
|
||||||
"targetScene": "desk_detail",
|
"targetScene": "desk_detail",
|
||||||
"x": 0.12, "y": 0.20, "width": 0.18, "height": 0.14,
|
"x": 154, "y": 144, "width": 230, "height": 101,
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "investigation", "value": 1 },
|
{ "type": "add", "target": "investigation", "value": 1 },
|
||||||
{ "type": "toggleFlag", "target": "checked_desk" }
|
{ "type": "toggleFlag", "target": "checked_desk" }
|
||||||
@@ -133,13 +134,13 @@
|
|||||||
"id": "hs_window",
|
"id": "hs_window",
|
||||||
"label": "查看窗户",
|
"label": "查看窗户",
|
||||||
"targetScene": "corridor",
|
"targetScene": "corridor",
|
||||||
"x": 0.47, "y": 0.06, "width": 0.15, "height": 0.28
|
"x": 602, "y": 43, "width": 192, "height": 202
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hs_closet",
|
"id": "hs_closet",
|
||||||
"label": "检查衣柜",
|
"label": "检查衣柜",
|
||||||
"targetScene": "desk_detail",
|
"targetScene": "desk_detail",
|
||||||
"x": 0.33, "y": 0.48, "width": 0.10, "height": 0.26,
|
"x": 422, "y": 346, "width": 128, "height": 187,
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{ "variable": "investigation", "op": ">=", "value": 1 }
|
{ "variable": "investigation", "op": ">=", "value": 1 }
|
||||||
],
|
],
|
||||||
@@ -154,12 +155,13 @@
|
|||||||
"corridor": {
|
"corridor": {
|
||||||
"id": "corridor",
|
"id": "corridor",
|
||||||
"videoUrl": "corridor/corridor.mp4",
|
"videoUrl": "corridor/corridor.mp4",
|
||||||
|
"contentSize": { "w": 1280, "h": 720 },
|
||||||
"hotspots": [
|
"hotspots": [
|
||||||
{
|
{
|
||||||
"id": "hs_left",
|
"id": "hs_left",
|
||||||
"label": "走向左边通道",
|
"label": "走向左边通道",
|
||||||
"targetScene": "left_door",
|
"targetScene": "left_door",
|
||||||
"x": 0.02, "y": 0.30, "width": 0.30, "height": 0.45,
|
"x": 26, "y": 216, "width": 384, "height": 324,
|
||||||
"showAt": 1.5,
|
"showAt": 1.5,
|
||||||
"effects": [
|
"effects": [
|
||||||
{ "type": "add", "target": "courage", "value": 5 }
|
{ "type": "add", "target": "courage", "value": 5 }
|
||||||
@@ -169,14 +171,14 @@
|
|||||||
"id": "hs_center",
|
"id": "hs_center",
|
||||||
"label": "走向中间通道",
|
"label": "走向中间通道",
|
||||||
"targetScene": "trust_ending",
|
"targetScene": "trust_ending",
|
||||||
"x": 0.33, "y": 0.25, "width": 0.34, "height": 0.55,
|
"x": 422, "y": 180, "width": 435, "height": 396,
|
||||||
"showAt": 3.0
|
"showAt": 3.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hs_right",
|
"id": "hs_right",
|
||||||
"label": "走向右边通道",
|
"label": "走向右边通道",
|
||||||
"targetScene": "alone_ending",
|
"targetScene": "alone_ending",
|
||||||
"x": 0.68, "y": 0.30, "width": 0.30, "height": 0.45,
|
"x": 870, "y": 216, "width": 384, "height": 324,
|
||||||
"showAt": 5.0
|
"showAt": 5.0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ init()
|
|||||||
:hotspots="store.hotspots"
|
:hotspots="store.hotspots"
|
||||||
:is-image-scene="store.isImageScene"
|
:is-image-scene="store.isImageScene"
|
||||||
:image-url="store.currentScene?.imageUrl"
|
:image-url="store.currentScene?.imageUrl"
|
||||||
|
:content-size="store.currentScene?.contentSize ?? null"
|
||||||
@click-hotspot="clickHotspot"
|
@click-hotspot="clickHotspot"
|
||||||
/>
|
/>
|
||||||
<Subtitles
|
<Subtitles
|
||||||
|
|||||||
@@ -1,35 +1,91 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import type { Hotspot } from '@engine/types'
|
import type { Hotspot } from '@engine/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
hotspots: Hotspot[]
|
hotspots: Hotspot[]
|
||||||
isImageScene: boolean
|
isImageScene: boolean
|
||||||
imageUrl?: string | null
|
imageUrl?: string | null
|
||||||
|
contentSize?: { w: number; h: number } | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
clickHotspot: [hotspotId: string]
|
clickHotspot: [hotspotId: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function hsStyle(hs: Hotspot) {
|
const layerRef = ref<HTMLDivElement | null>(null)
|
||||||
return {
|
const layerW = ref(window.innerWidth)
|
||||||
left: `${hs.x * 100}%`,
|
const layerH = ref(window.innerHeight)
|
||||||
top: `${hs.y * 100}%`,
|
|
||||||
width: `${hs.width * 100}%`,
|
function measure() {
|
||||||
height: `${hs.height * 100}%`,
|
if (layerRef.value) {
|
||||||
|
layerW.value = layerRef.value.clientWidth
|
||||||
|
layerH.value = layerRef.value.clientHeight
|
||||||
|
} else {
|
||||||
|
layerW.value = window.innerWidth
|
||||||
|
layerH.value = window.innerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resizeObs: ResizeObserver | null = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
measure()
|
||||||
|
resizeObs = new ResizeObserver(() => measure())
|
||||||
|
if (layerRef.value) resizeObs.observe(layerRef.value)
|
||||||
|
window.addEventListener('resize', measure)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObs?.disconnect()
|
||||||
|
window.removeEventListener('resize', measure)
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Rect {
|
||||||
|
left: string
|
||||||
|
top: string
|
||||||
|
width: string
|
||||||
|
height: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotspotRects = computed<Rect[]>(() => {
|
||||||
|
const cw = layerW.value
|
||||||
|
const ch = layerH.value
|
||||||
|
const cs = props.contentSize
|
||||||
|
|
||||||
|
if (!cs || !cs.w || !cs.h) {
|
||||||
|
return props.hotspots.map((hs) => ({
|
||||||
|
left: `${(hs.x / (cs?.w || 1)) * 100}%`,
|
||||||
|
top: `${(hs.y / (cs?.h || 1)) * 100}%`,
|
||||||
|
width: `${(hs.width / (cs?.w || 1)) * 100}%`,
|
||||||
|
height: `${(hs.height / (cs?.h || 1)) * 100}%`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = Math.min(cw / cs.w, ch / cs.h)
|
||||||
|
const renderW = cs.w * scale
|
||||||
|
const renderH = cs.h * scale
|
||||||
|
const ox = (cw - renderW) / 2
|
||||||
|
const oy = (ch - renderH) / 2
|
||||||
|
|
||||||
|
return props.hotspots.map((hs) => ({
|
||||||
|
left: `${ox + hs.x * scale}px`,
|
||||||
|
top: `${oy + hs.y * scale}px`,
|
||||||
|
width: `${hs.width * scale}px`,
|
||||||
|
height: `${hs.height * scale}px`,
|
||||||
|
}))
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="hotspot-layer" v-if="(hotspots.length > 0) || (isImageScene && imageUrl)">
|
<div ref="layerRef" class="hotspot-layer" v-if="(hotspots.length > 0) || (isImageScene && imageUrl)">
|
||||||
<img v-if="isImageScene && imageUrl" :src="imageUrl" class="hotspot-image" />
|
<img v-if="isImageScene && imageUrl" :src="imageUrl" class="hotspot-image" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="hs in hotspots"
|
v-for="(hs, i) in hotspots"
|
||||||
:key="hs.id"
|
:key="hs.id"
|
||||||
class="hotspot-rect"
|
class="hotspot-rect"
|
||||||
:style="hsStyle(hs)"
|
:style="hotspotRects[i] as any"
|
||||||
@click.stop="emit('clickHotspot', hs.id)"
|
@click.stop="emit('clickHotspot', hs.id)"
|
||||||
:title="hs.label"
|
:title="hs.label"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user