minecraft-renderer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -0
- package/dist/index.html +83 -0
- package/dist/static/image/arrow.6f27b59f.png +0 -0
- package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
- package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
- package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
- package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
- package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
- package/dist/static/js/365.f05233ab.js +8462 -0
- package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
- package/dist/static/js/async/738.efa27644.js +1 -0
- package/dist/static/js/index.092ec5be.js +56 -0
- package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
- package/dist/static/js/lib-react.5c9129e0.js +2 -0
- package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
- package/package.json +104 -0
- package/src/assets/destroy_stage_0.png +0 -0
- package/src/assets/destroy_stage_1.png +0 -0
- package/src/assets/destroy_stage_2.png +0 -0
- package/src/assets/destroy_stage_3.png +0 -0
- package/src/assets/destroy_stage_4.png +0 -0
- package/src/assets/destroy_stage_5.png +0 -0
- package/src/assets/destroy_stage_6.png +0 -0
- package/src/assets/destroy_stage_7.png +0 -0
- package/src/assets/destroy_stage_8.png +0 -0
- package/src/assets/destroy_stage_9.png +0 -0
- package/src/examples/README.md +146 -0
- package/src/examples/appViewerExample.ts +205 -0
- package/src/examples/initialMenuStart.ts +161 -0
- package/src/graphicsBackend/appViewer.ts +297 -0
- package/src/graphicsBackend/config.ts +119 -0
- package/src/graphicsBackend/index.ts +10 -0
- package/src/graphicsBackend/playerState.ts +61 -0
- package/src/graphicsBackend/types.ts +143 -0
- package/src/index.ts +97 -0
- package/src/lib/DebugGui.ts +190 -0
- package/src/lib/animationController.ts +85 -0
- package/src/lib/buildSharedConfig.mjs +1 -0
- package/src/lib/cameraBobbing.ts +94 -0
- package/src/lib/canvas2DOverlay.example.ts +361 -0
- package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
- package/src/lib/canvas2DOverlay.ts +381 -0
- package/src/lib/cleanupDecorator.ts +29 -0
- package/src/lib/createPlayerObject.ts +55 -0
- package/src/lib/frameTimingCollector.ts +164 -0
- package/src/lib/guiRenderer.ts +283 -0
- package/src/lib/items.ts +140 -0
- package/src/lib/mesherlogReader.ts +131 -0
- package/src/lib/moreBlockDataGenerated.json +714 -0
- package/src/lib/preflatMap.json +1741 -0
- package/src/lib/simpleUtils.ts +40 -0
- package/src/lib/smoothSwitcher.ts +168 -0
- package/src/lib/spiral.ts +29 -0
- package/src/lib/ui/newStats.ts +120 -0
- package/src/lib/utils/proxy.ts +23 -0
- package/src/lib/utils/skins.ts +63 -0
- package/src/lib/utils.ts +76 -0
- package/src/lib/workerProxy.ts +342 -0
- package/src/lib/worldrendererCommon.ts +1088 -0
- package/src/mesher/mesher.ts +253 -0
- package/src/mesher/models.ts +769 -0
- package/src/mesher/modelsGeometryCommon.ts +142 -0
- package/src/mesher/shared.ts +80 -0
- package/src/mesher/standaloneRenderer.ts +270 -0
- package/src/mesher/test/a.ts +3 -0
- package/src/mesher/test/mesherTester.ts +76 -0
- package/src/mesher/test/playground.ts +19 -0
- package/src/mesher/test/test-perf.ts +74 -0
- package/src/mesher/test/tests.test.ts +56 -0
- package/src/mesher/world.ts +294 -0
- package/src/mesher/worldConstants.ts +1 -0
- package/src/modules/index.ts +11 -0
- package/src/modules/starfield.ts +313 -0
- package/src/modules/types.ts +110 -0
- package/src/playerState/playerState.ts +78 -0
- package/src/playerState/types.ts +36 -0
- package/src/playground/allEntitiesDebug.ts +170 -0
- package/src/playground/baseScene.ts +587 -0
- package/src/playground/mobileControls.tsx +268 -0
- package/src/playground/playground.html +83 -0
- package/src/playground/playground.ts +11 -0
- package/src/playground/playgroundUi.tsx +140 -0
- package/src/playground/reactUtils.ts +71 -0
- package/src/playground/scenes/allEntities.ts +13 -0
- package/src/playground/scenes/entities.ts +37 -0
- package/src/playground/scenes/floorRandom.ts +33 -0
- package/src/playground/scenes/frequentUpdates.ts +148 -0
- package/src/playground/scenes/geometryExport.ts +142 -0
- package/src/playground/scenes/index.ts +12 -0
- package/src/playground/scenes/lightingStarfield.ts +40 -0
- package/src/playground/scenes/main.ts +313 -0
- package/src/playground/scenes/railsCobweb.ts +14 -0
- package/src/playground/scenes/rotationIssue.ts +7 -0
- package/src/playground/scenes/slabsOptimization.ts +16 -0
- package/src/playground/scenes/transparencyIssue.ts +11 -0
- package/src/playground/shared.ts +79 -0
- package/src/resourcesManager/index.ts +5 -0
- package/src/resourcesManager/resourcesManager.ts +314 -0
- package/src/shims/minecraftData.ts +41 -0
- package/src/sign-renderer/index.html +21 -0
- package/src/sign-renderer/index.ts +216 -0
- package/src/sign-renderer/noop.js +1 -0
- package/src/sign-renderer/playground.ts +38 -0
- package/src/sign-renderer/tests.test.ts +69 -0
- package/src/sign-renderer/vite.config.ts +10 -0
- package/src/three/appShared.ts +75 -0
- package/src/three/bannerRenderer.ts +275 -0
- package/src/three/cameraShake.ts +120 -0
- package/src/three/cinimaticScript.ts +350 -0
- package/src/three/documentRenderer.ts +491 -0
- package/src/three/entities.ts +1580 -0
- package/src/three/entity/EntityMesh.ts +707 -0
- package/src/three/entity/animations.js +171 -0
- package/src/three/entity/armorModels.json +204 -0
- package/src/three/entity/armorModels.ts +36 -0
- package/src/three/entity/entities.json +6230 -0
- package/src/three/entity/exportedModels.js +38 -0
- package/src/three/entity/externalTextures.json +1 -0
- package/src/three/entity/models/allay.obj +325 -0
- package/src/three/entity/models/arrow.obj +60 -0
- package/src/three/entity/models/axolotl.obj +509 -0
- package/src/three/entity/models/blaze.obj +601 -0
- package/src/three/entity/models/boat.obj +417 -0
- package/src/three/entity/models/camel.obj +1061 -0
- package/src/three/entity/models/cat.obj +509 -0
- package/src/three/entity/models/chicken.obj +371 -0
- package/src/three/entity/models/cod.obj +371 -0
- package/src/three/entity/models/creeper.obj +279 -0
- package/src/three/entity/models/dolphin.obj +371 -0
- package/src/three/entity/models/ender_dragon.obj +2993 -0
- package/src/three/entity/models/enderman.obj +325 -0
- package/src/three/entity/models/endermite.obj +187 -0
- package/src/three/entity/models/fox.obj +463 -0
- package/src/three/entity/models/frog.obj +739 -0
- package/src/three/entity/models/ghast.obj +463 -0
- package/src/three/entity/models/goat.obj +601 -0
- package/src/three/entity/models/guardian.obj +1015 -0
- package/src/three/entity/models/horse.obj +1061 -0
- package/src/three/entity/models/llama.obj +509 -0
- package/src/three/entity/models/minecart.obj +233 -0
- package/src/three/entity/models/parrot.obj +509 -0
- package/src/three/entity/models/piglin.obj +739 -0
- package/src/three/entity/models/pillager.obj +371 -0
- package/src/three/entity/models/rabbit.obj +555 -0
- package/src/three/entity/models/sheep.obj +555 -0
- package/src/three/entity/models/shulker.obj +141 -0
- package/src/three/entity/models/sniffer.obj +693 -0
- package/src/three/entity/models/spider.obj +509 -0
- package/src/three/entity/models/tadpole.obj +95 -0
- package/src/three/entity/models/turtle.obj +371 -0
- package/src/three/entity/models/vex.obj +325 -0
- package/src/three/entity/models/villager.obj +509 -0
- package/src/three/entity/models/warden.obj +463 -0
- package/src/three/entity/models/witch.obj +647 -0
- package/src/three/entity/models/wolf.obj +509 -0
- package/src/three/entity/models/zombie_villager.obj +463 -0
- package/src/three/entity/objModels.js +1 -0
- package/src/three/fireworks.ts +661 -0
- package/src/three/fireworksRenderer.ts +434 -0
- package/src/three/globals.d.ts +7 -0
- package/src/three/graphicsBackend.ts +274 -0
- package/src/three/graphicsBackendOffThread.ts +107 -0
- package/src/three/hand.ts +89 -0
- package/src/three/holdingBlock.ts +926 -0
- package/src/three/index.ts +20 -0
- package/src/three/itemMesh.ts +427 -0
- package/src/three/modules.d.ts +14 -0
- package/src/three/panorama.ts +308 -0
- package/src/three/panoramaShared.ts +1 -0
- package/src/three/renderSlot.ts +82 -0
- package/src/three/skyboxRenderer.ts +406 -0
- package/src/three/starField.ts +13 -0
- package/src/three/threeJsMedia.ts +731 -0
- package/src/three/threeJsMethods.ts +15 -0
- package/src/three/threeJsParticles.ts +160 -0
- package/src/three/threeJsSound.ts +95 -0
- package/src/three/threeJsUtils.ts +90 -0
- package/src/three/waypointSprite.ts +435 -0
- package/src/three/waypoints.ts +163 -0
- package/src/three/world/cursorBlock.ts +172 -0
- package/src/three/world/vr.ts +257 -0
- package/src/three/worldGeometryExport.ts +259 -0
- package/src/three/worldGeometryHandler.ts +279 -0
- package/src/three/worldRendererThree.ts +1381 -0
- package/src/worldView/index.ts +6 -0
- package/src/worldView/types.ts +66 -0
- package/src/worldView/worldView.ts +424 -0
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { WorldRendererThree } from './worldRendererThree'
|
|
3
|
+
import { ThreeJsSound } from './threeJsSound'
|
|
4
|
+
|
|
5
|
+
type ControlModeConfig = {
|
|
6
|
+
mouseButton: 'both' | 'left' | 'right'
|
|
7
|
+
controlMode: 'play_pause' | 'play_if_ended' | 'toggle_mute'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MediaProperties {
|
|
11
|
+
position: { x: number, y: number, z: number }
|
|
12
|
+
size: { width: number, height: number }
|
|
13
|
+
src: string
|
|
14
|
+
rotation?: 0 | 1 | 2 | 3 // 0-3 for 0°, 90°, 180°, 270°
|
|
15
|
+
doubleSide?: boolean
|
|
16
|
+
background?: number // Hexadecimal color (e.g., 0x000000 for black)
|
|
17
|
+
opacity?: number // 0-1 value for transparency
|
|
18
|
+
uvMapping?: { startU: number, endU: number, startV: number, endV: number }
|
|
19
|
+
allowOrigins?: string[] | boolean
|
|
20
|
+
loop?: boolean
|
|
21
|
+
volume?: number
|
|
22
|
+
autoPlay?: boolean
|
|
23
|
+
imageOverride?: boolean
|
|
24
|
+
allowLighting?: boolean
|
|
25
|
+
controlMode?: ControlModeConfig
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MediaData {
|
|
29
|
+
mesh: THREE.Object3D
|
|
30
|
+
props: MediaProperties
|
|
31
|
+
video: HTMLVideoElement | undefined
|
|
32
|
+
pausedBecuaseHidden: boolean
|
|
33
|
+
texture: THREE.Texture
|
|
34
|
+
updateUVMapping: (config: {
|
|
35
|
+
startU: number
|
|
36
|
+
endU: number
|
|
37
|
+
startV: number
|
|
38
|
+
endV: number
|
|
39
|
+
}) => void
|
|
40
|
+
positionalAudio?: THREE.PositionalAudio
|
|
41
|
+
hadAutoPlayError?: boolean
|
|
42
|
+
ended?: boolean
|
|
43
|
+
handleError: (err: Error) => void
|
|
44
|
+
destroyed?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ThreeJsMedia {
|
|
48
|
+
customMedia = new Map<string, MediaData>()
|
|
49
|
+
|
|
50
|
+
constructor(private readonly worldRenderer: WorldRendererThree) {
|
|
51
|
+
this.worldRenderer.onWorldSwitched.push(() => {
|
|
52
|
+
this.onWorldGone()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.worldRenderer.onRender.push(() => {
|
|
56
|
+
this.render()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
this.worldRenderer.onReactiveConfigUpdated('volume', () => {
|
|
60
|
+
const { volume } = this.worldRenderer.worldRendererConfig
|
|
61
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
62
|
+
if (videoData.positionalAudio) {
|
|
63
|
+
videoData.positionalAudio.setVolume((videoData.props.volume ?? 1) * volume)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onWorldGone() {
|
|
70
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
71
|
+
this.destroyMedia(id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onWorldStop() {
|
|
76
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
77
|
+
this.setVideoPlaying(id, false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private createErrorTexture(width: number, height: number, background = 0xff_ff_ff, error = 'Failed to load'): THREE.CanvasTexture {
|
|
82
|
+
const canvas = new OffscreenCanvas(100, 100)
|
|
83
|
+
const MAX_DIMENSION = 100
|
|
84
|
+
|
|
85
|
+
canvas.width = MAX_DIMENSION
|
|
86
|
+
canvas.height = MAX_DIMENSION
|
|
87
|
+
|
|
88
|
+
const ctx = canvas.getContext('2d')
|
|
89
|
+
if (!ctx) return new THREE.CanvasTexture(canvas)
|
|
90
|
+
|
|
91
|
+
// Clear with transparent background
|
|
92
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
93
|
+
|
|
94
|
+
// Add background color
|
|
95
|
+
ctx.fillStyle = `rgba(${background >> 16 & 255}, ${background >> 8 & 255}, ${background & 255}, 1)`
|
|
96
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
97
|
+
|
|
98
|
+
// Add red text with size relative to canvas dimensions
|
|
99
|
+
ctx.fillStyle = '#ff0000'
|
|
100
|
+
ctx.font = 'bold 10px sans-serif'
|
|
101
|
+
ctx.textAlign = 'center'
|
|
102
|
+
ctx.textBaseline = 'middle'
|
|
103
|
+
ctx.fillText(error, canvas.width / 2, canvas.height / 2, canvas.width)
|
|
104
|
+
|
|
105
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
106
|
+
texture.minFilter = THREE.LinearFilter
|
|
107
|
+
texture.magFilter = THREE.LinearFilter
|
|
108
|
+
return texture
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private createBackgroundTexture(width: number, height: number, color = 0x00_00_00, opacity = 1): THREE.CanvasTexture {
|
|
112
|
+
const canvas = new OffscreenCanvas(1, 1)
|
|
113
|
+
canvas.width = 1
|
|
114
|
+
canvas.height = 1
|
|
115
|
+
|
|
116
|
+
const ctx = canvas.getContext('2d')
|
|
117
|
+
if (!ctx) return new THREE.CanvasTexture(canvas)
|
|
118
|
+
|
|
119
|
+
// Convert hex color to rgba
|
|
120
|
+
const r = (color >> 16) & 255
|
|
121
|
+
const g = (color >> 8) & 255
|
|
122
|
+
const b = color & 255
|
|
123
|
+
|
|
124
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`
|
|
125
|
+
ctx.fillRect(0, 0, 1, 1)
|
|
126
|
+
|
|
127
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
128
|
+
texture.minFilter = THREE.NearestFilter
|
|
129
|
+
texture.magFilter = THREE.NearestFilter
|
|
130
|
+
return texture
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
validateOrigin(src: string, allowOrigins: string[] | boolean) {
|
|
134
|
+
if (allowOrigins === true) return true
|
|
135
|
+
if (allowOrigins === false) return false
|
|
136
|
+
const url = new URL(src)
|
|
137
|
+
return allowOrigins.some(origin => url.origin.endsWith(origin))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onPageInteraction() {
|
|
141
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
142
|
+
if (videoData.hadAutoPlayError) {
|
|
143
|
+
videoData.hadAutoPlayError = false
|
|
144
|
+
this.playVideo(id)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
addMedia(id: string, props: MediaProperties) {
|
|
150
|
+
// if (!props.imageOverride && this.customMedia.has(id)) {
|
|
151
|
+
// console.warn('Media already exists, destroying it', id)
|
|
152
|
+
// debugger
|
|
153
|
+
// }
|
|
154
|
+
const originalProps = structuredClone(props)
|
|
155
|
+
this.destroyMedia(id)
|
|
156
|
+
|
|
157
|
+
let headCheck = false
|
|
158
|
+
if (props.src.startsWith('https://disk.yandex.ru/i/')) {
|
|
159
|
+
headCheck = true
|
|
160
|
+
props.src = `/ya-image?url=${props.src}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { scene } = this.worldRenderer
|
|
164
|
+
|
|
165
|
+
const originSecurityError = props.allowOrigins !== undefined && !this.validateOrigin(props.src, props.allowOrigins)
|
|
166
|
+
if (originSecurityError) {
|
|
167
|
+
console.warn('Remote resource blocked due to security policy', props.src, 'allowed origins:', props.allowOrigins, 'you can control it with `remoteContentNotSameOrigin` option')
|
|
168
|
+
props.src = ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check content type for Yandex Disk URLs
|
|
172
|
+
const isImage = props.src.endsWith('.png') || props.src.endsWith('.jpg') || props.src.endsWith('.jpeg') || props.imageOverride
|
|
173
|
+
if (headCheck && !props.imageOverride) {
|
|
174
|
+
// Fetch headers only to check content type
|
|
175
|
+
fetch(props.src, { method: 'HEAD' })
|
|
176
|
+
.then(response => {
|
|
177
|
+
const contentType = response.headers.get('content-type')
|
|
178
|
+
if (contentType?.startsWith('image/')) {
|
|
179
|
+
// If it's a video, recreate the media with video element
|
|
180
|
+
this.destroyMedia(id)
|
|
181
|
+
this.addMedia(id, { ...originalProps, src: props.src, imageOverride: true })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
.catch(console.error)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let video: HTMLVideoElement | undefined
|
|
188
|
+
let positionalAudio: THREE.PositionalAudio | undefined
|
|
189
|
+
if (!isImage) {
|
|
190
|
+
video = document.createElement('video')
|
|
191
|
+
video.src = props.src.endsWith('.gif') ? props.src.replace('.gif', '.mp4') : props.src
|
|
192
|
+
video.loop = props.loop ?? true
|
|
193
|
+
const volume = (props.volume ?? 1) * this.worldRenderer.worldRendererConfig.volume
|
|
194
|
+
video.volume = Math.min(volume, 1)
|
|
195
|
+
video.playsInline = true
|
|
196
|
+
video.crossOrigin = 'anonymous'
|
|
197
|
+
|
|
198
|
+
// Create positional audio
|
|
199
|
+
const soundSystem = this.worldRenderer.soundSystem as ThreeJsSound
|
|
200
|
+
soundSystem.initAudioListener()
|
|
201
|
+
if (!soundSystem.audioListener) throw new Error('Audio listener not initialized')
|
|
202
|
+
positionalAudio = new THREE.PositionalAudio(soundSystem.audioListener)
|
|
203
|
+
positionalAudio.setRefDistance(6)
|
|
204
|
+
positionalAudio.setVolume(volume)
|
|
205
|
+
scene.add(positionalAudio)
|
|
206
|
+
positionalAudio.position.set(props.position.x, props.position.y, props.position.z)
|
|
207
|
+
|
|
208
|
+
// Connect video to positional audio
|
|
209
|
+
positionalAudio.setMediaElementSource(video)
|
|
210
|
+
positionalAudio.connect()
|
|
211
|
+
|
|
212
|
+
video.addEventListener('pause', () => {
|
|
213
|
+
positionalAudio?.pause()
|
|
214
|
+
globalThis.tempSendVideoStop?.(id, 'paused', video!.currentTime)
|
|
215
|
+
})
|
|
216
|
+
video.addEventListener('play', () => {
|
|
217
|
+
positionalAudio?.play()
|
|
218
|
+
globalThis.tempSendVideoPlay?.(id)
|
|
219
|
+
})
|
|
220
|
+
video.addEventListener('seeked', () => {
|
|
221
|
+
if (positionalAudio && video) {
|
|
222
|
+
positionalAudio.offset = video.currentTime
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
video.addEventListener('stalled', () => {
|
|
226
|
+
globalThis.tempSendVideoStop?.(id, 'stalled', video!.currentTime)
|
|
227
|
+
})
|
|
228
|
+
video.addEventListener('waiting', () => {
|
|
229
|
+
globalThis.tempSendVideoStop?.(id, 'waiting', video!.currentTime)
|
|
230
|
+
})
|
|
231
|
+
video.addEventListener('error', ({ error }) => {
|
|
232
|
+
globalThis.tempSendVideoStop?.(id, `error: ${error}`, video!.currentTime)
|
|
233
|
+
})
|
|
234
|
+
video.addEventListener('ended', () => {
|
|
235
|
+
globalThis.tempSendVideoStop?.(id, 'ended', video!.currentTime)
|
|
236
|
+
if (!props.loop) {
|
|
237
|
+
video!.currentTime = 0
|
|
238
|
+
videoData.ended = true
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
// Create background texture first
|
|
245
|
+
const backgroundTexture = this.createBackgroundTexture(
|
|
246
|
+
props.size.width,
|
|
247
|
+
props.size.height,
|
|
248
|
+
props.background,
|
|
249
|
+
// props.opacity ?? 1
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const handleError = (text?: string) => {
|
|
253
|
+
const errorTexture = this.createErrorTexture(props.size.width, props.size.height, props.background, text)
|
|
254
|
+
material.map = errorTexture
|
|
255
|
+
material.needsUpdate = true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Create a plane geometry with configurable UV mapping
|
|
259
|
+
const geometry = new THREE.PlaneGeometry(1, 1)
|
|
260
|
+
|
|
261
|
+
// Create material with initial properties using background texture
|
|
262
|
+
const MaterialClass = props.allowLighting ? THREE.MeshLambertMaterial : THREE.MeshBasicMaterial
|
|
263
|
+
const material = new MaterialClass({
|
|
264
|
+
map: backgroundTexture,
|
|
265
|
+
transparent: true,
|
|
266
|
+
side: props.doubleSide ? THREE.DoubleSide : THREE.FrontSide,
|
|
267
|
+
alphaTest: 0.1
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const texture = video
|
|
271
|
+
? new THREE.VideoTexture(video)
|
|
272
|
+
: new THREE.TextureLoader().load(props.src, () => {
|
|
273
|
+
if (this.customMedia.get(id)?.texture === texture) {
|
|
274
|
+
material.map = texture
|
|
275
|
+
material.needsUpdate = true
|
|
276
|
+
}
|
|
277
|
+
}, undefined, () => handleError()) // todo cache
|
|
278
|
+
texture.minFilter = THREE.NearestFilter
|
|
279
|
+
texture.magFilter = THREE.NearestFilter
|
|
280
|
+
// texture.format = THREE.RGBAFormat
|
|
281
|
+
// texture.colorSpace = THREE.SRGBColorSpace
|
|
282
|
+
texture.generateMipmaps = false
|
|
283
|
+
|
|
284
|
+
// Create inner mesh for offsets
|
|
285
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
286
|
+
|
|
287
|
+
const { mesh: panel } = this.positionMeshExact(mesh, THREE.MathUtils.degToRad((props.rotation ?? 0) * 90), props.position, props.size.width, props.size.height)
|
|
288
|
+
|
|
289
|
+
scene.add(panel)
|
|
290
|
+
|
|
291
|
+
if (video) {
|
|
292
|
+
// Update texture in animation loop regardless of autoPlay
|
|
293
|
+
mesh.onBeforeRender = () => {
|
|
294
|
+
if (video.readyState === video.HAVE_ENOUGH_DATA && (!video.paused || !videoData?.hadAutoPlayError)) {
|
|
295
|
+
if (material.map !== texture) {
|
|
296
|
+
material.map = texture
|
|
297
|
+
material.needsUpdate = true
|
|
298
|
+
}
|
|
299
|
+
texture.needsUpdate = true
|
|
300
|
+
|
|
301
|
+
// Sync audio position with video position
|
|
302
|
+
if (positionalAudio) {
|
|
303
|
+
positionalAudio.position.copy(panel.position)
|
|
304
|
+
positionalAudio.rotation.copy(panel.rotation)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// UV mapping configuration
|
|
311
|
+
const updateUVMapping = (config: { startU: number, endU: number, startV: number, endV: number }) => {
|
|
312
|
+
const uvs = geometry.attributes.uv.array as Float32Array
|
|
313
|
+
uvs[0] = config.startU
|
|
314
|
+
uvs[1] = config.startV
|
|
315
|
+
uvs[2] = config.endU
|
|
316
|
+
uvs[3] = config.startV
|
|
317
|
+
uvs[4] = config.endU
|
|
318
|
+
uvs[5] = config.endV
|
|
319
|
+
uvs[6] = config.startU
|
|
320
|
+
uvs[7] = config.endV
|
|
321
|
+
geometry.attributes.uv.needsUpdate = true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Apply initial UV mapping if provided
|
|
325
|
+
if (props.uvMapping) {
|
|
326
|
+
updateUVMapping(props.uvMapping)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const videoData: MediaData = {
|
|
330
|
+
mesh: panel,
|
|
331
|
+
video,
|
|
332
|
+
texture,
|
|
333
|
+
updateUVMapping,
|
|
334
|
+
positionalAudio,
|
|
335
|
+
props: originalProps,
|
|
336
|
+
hadAutoPlayError: false,
|
|
337
|
+
pausedBecuaseHidden: false,
|
|
338
|
+
ended: false,
|
|
339
|
+
handleError(err: Error) {
|
|
340
|
+
if (videoData.destroyed) return
|
|
341
|
+
console.error(`Failed to play video ${id}:`, err)
|
|
342
|
+
// TODO!
|
|
343
|
+
const t = /* translate ?? */(txt => txt)
|
|
344
|
+
handleError(err.name === 'NotAllowedError' || err.name === 'AbortError' ? t('Waiting for user interaction') : t('Failed to auto play'))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Store video data
|
|
348
|
+
this.customMedia.set(id, videoData)
|
|
349
|
+
|
|
350
|
+
if (video && props.autoPlay) {
|
|
351
|
+
// Start playing the video
|
|
352
|
+
this.playVideo(id, true)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return id
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
playVideo(id: string, fromAutoPlay = false) {
|
|
359
|
+
const videoData = this.customMedia.get(id)
|
|
360
|
+
if (videoData?.video) {
|
|
361
|
+
// TODO! resolve issue with time
|
|
362
|
+
if (!fromAutoPlay && videoData.positionalAudio && videoData.video.currentTime < 1) {
|
|
363
|
+
// workaround: audio has to be recreated
|
|
364
|
+
const prevTime = videoData.video.currentTime
|
|
365
|
+
this.addMedia(id, videoData.props)
|
|
366
|
+
videoData.video.currentTime = prevTime
|
|
367
|
+
if (!videoData.props.autoPlay) {
|
|
368
|
+
this.playVideo(id, true)
|
|
369
|
+
}
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
void videoData.video.play()
|
|
374
|
+
.then(() => {
|
|
375
|
+
videoData.hadAutoPlayError = false
|
|
376
|
+
console.log(`Playing video ${id}`)
|
|
377
|
+
})
|
|
378
|
+
.catch(err => {
|
|
379
|
+
if (videoData.pausedBecuaseHidden) return
|
|
380
|
+
if (err.name === 'NotAllowedError' || err.name === 'AbortError' || err.message?.includes('not allowed') && !videoData.pausedBecuaseHidden) {
|
|
381
|
+
videoData.hadAutoPlayError = true
|
|
382
|
+
}
|
|
383
|
+
videoData.handleError(err)
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
render() {
|
|
389
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
390
|
+
const currentVisible = videoData.mesh.visible
|
|
391
|
+
videoData.mesh.visible = this.worldRenderer.shouldObjectVisible(videoData.mesh) && !videoData.mesh['forceHide']
|
|
392
|
+
if (currentVisible !== videoData.mesh.visible && videoData.video) {
|
|
393
|
+
const isNowVisible = videoData.mesh.visible
|
|
394
|
+
if (isNowVisible) {
|
|
395
|
+
if (videoData.pausedBecuaseHidden) {
|
|
396
|
+
this.playVideo(id)
|
|
397
|
+
videoData.pausedBecuaseHidden = false
|
|
398
|
+
}
|
|
399
|
+
} else if (!videoData.video.paused) {
|
|
400
|
+
videoData.video.pause()
|
|
401
|
+
videoData.pausedBecuaseHidden = true
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
setVideoPlaying(id: string, playing: boolean) {
|
|
408
|
+
const videoData = this.customMedia.get(id)
|
|
409
|
+
if (videoData?.video) {
|
|
410
|
+
if (playing) {
|
|
411
|
+
this.playVideo(id)
|
|
412
|
+
} else {
|
|
413
|
+
videoData.video.pause()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
setVideoSeeking(id: string, seconds: number) {
|
|
419
|
+
const videoData = this.customMedia.get(id)
|
|
420
|
+
if (videoData?.video) {
|
|
421
|
+
videoData.video.currentTime = seconds
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setVideoVolume(id: string, volume: number) {
|
|
426
|
+
const videoData = this.customMedia.get(id)
|
|
427
|
+
if (videoData?.video) {
|
|
428
|
+
videoData.video.volume = volume
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
setVideoSpeed(id: string, speed: number) {
|
|
433
|
+
const videoData = this.customMedia.get(id)
|
|
434
|
+
if (videoData?.video) {
|
|
435
|
+
videoData.video.playbackRate = speed
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
setControlMode(id: string, mouseButton: 'both' | 'left' | 'right', controlMode: 'play_pause' | 'play_if_ended') {
|
|
440
|
+
const videoData = this.customMedia.get(id)
|
|
441
|
+
if (videoData?.video) {
|
|
442
|
+
videoData.props.controlMode = {
|
|
443
|
+
mouseButton,
|
|
444
|
+
controlMode
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
destroyMedia(id: string) {
|
|
450
|
+
const { scene } = this.worldRenderer
|
|
451
|
+
const mediaData = this.customMedia.get(id)
|
|
452
|
+
if (mediaData) {
|
|
453
|
+
mediaData.destroyed = true
|
|
454
|
+
|
|
455
|
+
if (mediaData.video) {
|
|
456
|
+
mediaData.video.pause()
|
|
457
|
+
mediaData.video.src = ''
|
|
458
|
+
mediaData.video.remove()
|
|
459
|
+
}
|
|
460
|
+
if (mediaData.positionalAudio) {
|
|
461
|
+
// mediaData.positionalAudio.stop()
|
|
462
|
+
// mediaData.positionalAudio.disconnect()
|
|
463
|
+
scene.remove(mediaData.positionalAudio)
|
|
464
|
+
}
|
|
465
|
+
scene.remove(mediaData.mesh)
|
|
466
|
+
mediaData.texture.dispose()
|
|
467
|
+
|
|
468
|
+
// Get the inner mesh from the group
|
|
469
|
+
const mesh = mediaData.mesh.children[0] as THREE.Mesh
|
|
470
|
+
if (mesh) {
|
|
471
|
+
mesh.geometry.dispose()
|
|
472
|
+
if (mesh.material instanceof THREE.Material) {
|
|
473
|
+
mesh.material.dispose()
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.customMedia.delete(id)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Positions a mesh exactly at startPosition and extends it along the rotation direction
|
|
483
|
+
* with the specified width and height
|
|
484
|
+
*
|
|
485
|
+
* @param mesh The mesh to position
|
|
486
|
+
* @param rotation Rotation in radians (applied to Y axis)
|
|
487
|
+
* @param startPosition The exact starting position (corner) of the mesh
|
|
488
|
+
* @param width Width of the mesh
|
|
489
|
+
* @param height Height of the mesh
|
|
490
|
+
* @param depth Depth of the mesh (default: 1)
|
|
491
|
+
* @returns The positioned mesh for chaining
|
|
492
|
+
*/
|
|
493
|
+
positionMeshExact(
|
|
494
|
+
mesh: THREE.Mesh,
|
|
495
|
+
rotation: number,
|
|
496
|
+
startPosition: { x: number, y: number, z: number },
|
|
497
|
+
width: number,
|
|
498
|
+
height: number,
|
|
499
|
+
depth = 1
|
|
500
|
+
) {
|
|
501
|
+
// avoid z-fighting with the ground plane
|
|
502
|
+
if (rotation === 0) {
|
|
503
|
+
startPosition.z += 0.001
|
|
504
|
+
}
|
|
505
|
+
if (rotation === Math.PI / 2) {
|
|
506
|
+
startPosition.x -= 0.001
|
|
507
|
+
}
|
|
508
|
+
if (rotation === Math.PI) {
|
|
509
|
+
startPosition.z -= 0.001
|
|
510
|
+
}
|
|
511
|
+
if (rotation === 3 * Math.PI / 2) {
|
|
512
|
+
startPosition.x += 0.001
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// rotation normalize coordinates
|
|
516
|
+
if (rotation === 0) {
|
|
517
|
+
startPosition.z += 1
|
|
518
|
+
}
|
|
519
|
+
if (rotation === Math.PI) {
|
|
520
|
+
startPosition.x += 1
|
|
521
|
+
}
|
|
522
|
+
if (rotation === 3 * Math.PI / 2) {
|
|
523
|
+
startPosition.z += 1
|
|
524
|
+
startPosition.x += 1
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
// First, clean up any previous transformations
|
|
529
|
+
mesh.matrix.identity()
|
|
530
|
+
mesh.position.set(0, 0, 0)
|
|
531
|
+
mesh.rotation.set(0, 0, 0)
|
|
532
|
+
mesh.scale.set(1, 1, 1)
|
|
533
|
+
|
|
534
|
+
// By default, PlaneGeometry creates a plane in the XY plane (facing +Z)
|
|
535
|
+
// We need to set up the proper orientation for our use case
|
|
536
|
+
// Rotate the plane to face the correct direction based on the rotation parameter
|
|
537
|
+
mesh.rotateY(rotation)
|
|
538
|
+
if (rotation === Math.PI / 2 || rotation === 3 * Math.PI / 2) {
|
|
539
|
+
mesh.rotateZ(-Math.PI)
|
|
540
|
+
mesh.rotateX(-Math.PI)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Scale it to the desired size
|
|
544
|
+
mesh.scale.set(width, height, depth)
|
|
545
|
+
|
|
546
|
+
// For a PlaneGeometry, if we want the corner at the origin, we need to offset
|
|
547
|
+
// by half the dimensions after scaling
|
|
548
|
+
mesh.geometry.translate(0.5, 0.5, 0)
|
|
549
|
+
mesh.geometry.attributes.position.needsUpdate = true
|
|
550
|
+
|
|
551
|
+
// Now place the mesh at the start position
|
|
552
|
+
mesh.position.set(startPosition.x, startPosition.y, startPosition.z)
|
|
553
|
+
|
|
554
|
+
// Create a group to hold our mesh and markers
|
|
555
|
+
const debugGroup = new THREE.Group()
|
|
556
|
+
debugGroup.add(mesh)
|
|
557
|
+
|
|
558
|
+
// Add a marker at the starting position (should be exactly at pos)
|
|
559
|
+
const startMarker = new THREE.Mesh(
|
|
560
|
+
new THREE.BoxGeometry(0.1, 0.1, 0.1),
|
|
561
|
+
new THREE.MeshBasicMaterial({ color: 0xff_00_00 })
|
|
562
|
+
)
|
|
563
|
+
startMarker.position.copy(new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z))
|
|
564
|
+
debugGroup.add(startMarker)
|
|
565
|
+
|
|
566
|
+
// Add a marker at the end position (width units away in the rotated direction)
|
|
567
|
+
const endX = startPosition.x + Math.cos(rotation) * width
|
|
568
|
+
const endZ = startPosition.z + Math.sin(rotation) * width
|
|
569
|
+
const endYMarker = new THREE.Mesh(
|
|
570
|
+
new THREE.BoxGeometry(0.1, 0.1, 0.1),
|
|
571
|
+
new THREE.MeshBasicMaterial({ color: 0x00_00_ff })
|
|
572
|
+
)
|
|
573
|
+
endYMarker.position.set(startPosition.x, startPosition.y + height, startPosition.z)
|
|
574
|
+
debugGroup.add(endYMarker)
|
|
575
|
+
|
|
576
|
+
// Add a marker at the width endpoint
|
|
577
|
+
const endWidthMarker = new THREE.Mesh(
|
|
578
|
+
new THREE.BoxGeometry(0.1, 0.1, 0.1),
|
|
579
|
+
new THREE.MeshBasicMaterial({ color: 0xff_ff_00 })
|
|
580
|
+
)
|
|
581
|
+
endWidthMarker.position.set(endX, startPosition.y, endZ)
|
|
582
|
+
debugGroup.add(endWidthMarker)
|
|
583
|
+
|
|
584
|
+
// Add a marker at the corner diagonal endpoint (both width and height)
|
|
585
|
+
const endCornerMarker = new THREE.Mesh(
|
|
586
|
+
new THREE.BoxGeometry(0.1, 0.1, 0.1),
|
|
587
|
+
new THREE.MeshBasicMaterial({ color: 0xff_00_ff })
|
|
588
|
+
)
|
|
589
|
+
endCornerMarker.position.set(endX, startPosition.y + height, endZ)
|
|
590
|
+
debugGroup.add(endCornerMarker)
|
|
591
|
+
|
|
592
|
+
// Also add a visual helper to show the rotation direction
|
|
593
|
+
const directionHelper = new THREE.ArrowHelper(
|
|
594
|
+
new THREE.Vector3(Math.cos(rotation), 0, Math.sin(rotation)),
|
|
595
|
+
new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z),
|
|
596
|
+
1,
|
|
597
|
+
0xff_00_00
|
|
598
|
+
)
|
|
599
|
+
debugGroup.add(directionHelper)
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
mesh,
|
|
603
|
+
debugGroup
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
createTestCanvasTexture() {
|
|
608
|
+
const canvas = new OffscreenCanvas(100, 100)
|
|
609
|
+
canvas.width = 100
|
|
610
|
+
canvas.height = 100
|
|
611
|
+
const ctx = canvas.getContext('2d')
|
|
612
|
+
if (!ctx) return null
|
|
613
|
+
ctx.font = '10px Arial'
|
|
614
|
+
ctx.fillStyle = 'red'
|
|
615
|
+
ctx.fillText('Hello World', 0, 10) // at
|
|
616
|
+
return new THREE.CanvasTexture(canvas)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Creates a test mesh that demonstrates the exact positioning
|
|
621
|
+
*/
|
|
622
|
+
// addTestMeshExact(rotationNum: number) {
|
|
623
|
+
// const pos = window.cursorBlockRel().position
|
|
624
|
+
// console.log('Creating exact positioned test mesh at:', pos)
|
|
625
|
+
|
|
626
|
+
// // Create a plane mesh with a wireframe to visualize boundaries
|
|
627
|
+
// const plane = new THREE.Mesh(
|
|
628
|
+
// new THREE.PlaneGeometry(1, 1),
|
|
629
|
+
// new THREE.MeshBasicMaterial({
|
|
630
|
+
// // side: THREE.DoubleSide,
|
|
631
|
+
// map: this.createTestCanvasTexture()
|
|
632
|
+
// })
|
|
633
|
+
// )
|
|
634
|
+
|
|
635
|
+
// const width = 2
|
|
636
|
+
// const height = 1
|
|
637
|
+
// const rotation = THREE.MathUtils.degToRad(rotationNum * 90) // 90 degrees in radians
|
|
638
|
+
|
|
639
|
+
// // Position the mesh exactly where we want it
|
|
640
|
+
// const { debugGroup } = this.positionMeshExact(plane, rotation, pos, width, height)
|
|
641
|
+
|
|
642
|
+
// this.worldRenderer.scene.add(debugGroup)
|
|
643
|
+
// console.log('Exact test mesh added with dimensions:', width, height, 'and rotation:', rotation)
|
|
644
|
+
// }
|
|
645
|
+
|
|
646
|
+
lastCheck = 0
|
|
647
|
+
THROTTLE_TIME = 100
|
|
648
|
+
tryIntersectMedia() {
|
|
649
|
+
// hack: need to optimize this by pulling only in distance of interaction instead and throttle
|
|
650
|
+
if (Date.now() - this.lastCheck < this.THROTTLE_TIME) return
|
|
651
|
+
if (this.customMedia.size === 0) {
|
|
652
|
+
this.worldRenderer.reactiveState.world.intersectMedia = null
|
|
653
|
+
this.worldRenderer['debugVideo'] = null
|
|
654
|
+
this.worldRenderer.cursorBlock.cursorLinesHidden = false
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
this.lastCheck = Date.now()
|
|
658
|
+
|
|
659
|
+
const { camera, scene } = this.worldRenderer
|
|
660
|
+
const raycaster = new THREE.Raycaster()
|
|
661
|
+
|
|
662
|
+
// Get mouse position at center of screen
|
|
663
|
+
const mouse = new THREE.Vector2(0, 0)
|
|
664
|
+
|
|
665
|
+
// Update the raycaster
|
|
666
|
+
raycaster.setFromCamera(mouse, camera)
|
|
667
|
+
|
|
668
|
+
// Check intersection with all objects in scene
|
|
669
|
+
const intersects = raycaster.intersectObjects(scene.children, true)
|
|
670
|
+
if (intersects.length > 0) {
|
|
671
|
+
const intersection = intersects[0]
|
|
672
|
+
const intersectedObject = intersection.object
|
|
673
|
+
|
|
674
|
+
// Find if this object belongs to any media
|
|
675
|
+
for (const [id, videoData] of this.customMedia.entries()) {
|
|
676
|
+
// Check if the intersected object is part of our media mesh
|
|
677
|
+
if (intersectedObject === videoData.mesh ||
|
|
678
|
+
videoData.mesh.children.includes(intersectedObject)) {
|
|
679
|
+
const { uv } = intersection
|
|
680
|
+
if (uv) {
|
|
681
|
+
const result = {
|
|
682
|
+
id,
|
|
683
|
+
x: uv.x,
|
|
684
|
+
y: uv.y
|
|
685
|
+
}
|
|
686
|
+
this.worldRenderer.reactiveState.world.intersectMedia = result
|
|
687
|
+
this.worldRenderer['debugVideo'] = videoData
|
|
688
|
+
this.worldRenderer.cursorBlock.cursorLinesHidden = true
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// No media intersection found
|
|
696
|
+
this.worldRenderer.reactiveState.world.intersectMedia = null
|
|
697
|
+
this.worldRenderer['debugVideo'] = null
|
|
698
|
+
this.worldRenderer.cursorBlock.cursorLinesHidden = false
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
handleUserClick(button: 'left' | 'right') {
|
|
702
|
+
const intersecting = this.worldRenderer.reactiveState.world.intersectMedia
|
|
703
|
+
if (intersecting) {
|
|
704
|
+
const { id, x, y } = intersecting
|
|
705
|
+
const videoData = this.customMedia.get(id)
|
|
706
|
+
const controlMode = videoData?.props.controlMode
|
|
707
|
+
if (videoData?.video && (controlMode?.mouseButton === 'both' || controlMode?.mouseButton === button)) {
|
|
708
|
+
switch (controlMode?.controlMode) {
|
|
709
|
+
case 'play_pause': {
|
|
710
|
+
this.setVideoPlaying(id, videoData.video.paused)
|
|
711
|
+
|
|
712
|
+
break
|
|
713
|
+
}
|
|
714
|
+
case 'play_if_ended': {
|
|
715
|
+
if (videoData.ended) {
|
|
716
|
+
this.setVideoPlaying(id, true)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
break
|
|
720
|
+
}
|
|
721
|
+
case 'toggle_mute': {
|
|
722
|
+
videoData.video.muted = !videoData.video.muted
|
|
723
|
+
|
|
724
|
+
break
|
|
725
|
+
}
|
|
726
|
+
// No default
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|