From bb289f54389c114411218d948654af82b5ccd5b6 Mon Sep 17 00:00:00 2001 From: cocos02 Date: Wed, 10 Jun 2026 12:45:41 +0800 Subject: [PATCH] feat: switch hotspot coordinates from container percentage to absolute content pixels --- engine/types.ts | 1 + public/scenes/demo.json | 14 ++++--- src/App.vue | 1 + src/components/HotspotLayer.vue | 74 +++++++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/engine/types.ts b/engine/types.ts index 726c2fa..d9af853 100644 --- a/engine/types.ts +++ b/engine/types.ts @@ -3,6 +3,7 @@ export interface SceneNode { type?: 'video' | 'image' videoUrl: string imageUrl?: string + contentSize?: { w: number; h: number } subtitleUrl?: string subtitles?: Record choices?: Choice[] diff --git a/public/scenes/demo.json b/public/scenes/demo.json index b63f8c1..6f53e8a 100644 --- a/public/scenes/demo.json +++ b/public/scenes/demo.json @@ -112,6 +112,7 @@ "type": "image", "videoUrl": "", "imageUrl": "investigation_site/investigation_scene.jpg", + "contentSize": { "w": 1280, "h": 720 }, "subtitleUrl": "investigation_site/investigation.vtt", "subtitles": { "zh": "investigation_site/investigation.vtt", @@ -123,7 +124,7 @@ "id": "hs_desk", "label": "查看书桌", "targetScene": "desk_detail", - "x": 0.12, "y": 0.20, "width": 0.18, "height": 0.14, + "x": 154, "y": 144, "width": 230, "height": 101, "effects": [ { "type": "add", "target": "investigation", "value": 1 }, { "type": "toggleFlag", "target": "checked_desk" } @@ -133,13 +134,13 @@ "id": "hs_window", "label": "查看窗户", "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", "label": "检查衣柜", "targetScene": "desk_detail", - "x": 0.33, "y": 0.48, "width": 0.10, "height": 0.26, + "x": 422, "y": 346, "width": 128, "height": 187, "conditions": [ { "variable": "investigation", "op": ">=", "value": 1 } ], @@ -154,12 +155,13 @@ "corridor": { "id": "corridor", "videoUrl": "corridor/corridor.mp4", + "contentSize": { "w": 1280, "h": 720 }, "hotspots": [ { "id": "hs_left", "label": "走向左边通道", "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, "effects": [ { "type": "add", "target": "courage", "value": 5 } @@ -169,14 +171,14 @@ "id": "hs_center", "label": "走向中间通道", "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 }, { "id": "hs_right", "label": "走向右边通道", "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 } ] diff --git a/src/App.vue b/src/App.vue index bb66df2..6e36320 100644 --- a/src/App.vue +++ b/src/App.vue @@ -218,6 +218,7 @@ init() :hotspots="store.hotspots" :is-image-scene="store.isImageScene" :image-url="store.currentScene?.imageUrl" + :content-size="store.currentScene?.contentSize ?? null" @click-hotspot="clickHotspot" /> +import { ref, computed, onMounted, onUnmounted } from 'vue' import type { Hotspot } from '@engine/types' const props = defineProps<{ hotspots: Hotspot[] isImageScene: boolean imageUrl?: string | null + contentSize?: { w: number; h: number } | null }>() const emit = defineEmits<{ clickHotspot: [hotspotId: string] }>() -function hsStyle(hs: Hotspot) { - return { - left: `${hs.x * 100}%`, - top: `${hs.y * 100}%`, - width: `${hs.width * 100}%`, - height: `${hs.height * 100}%`, +const layerRef = ref(null) +const layerW = ref(window.innerWidth) +const layerH = ref(window.innerHeight) + +function measure() { + 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(() => { + 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`, + })) +})