hipinis's picture
Add files using upload-large-folder tool
b7ed1ef verified
Raw
History Blame Contribute Delete
25.9 kB
import * as THREE from 'three'
import type { CameraState, CameraWidgetOptions } from './types'
export class CameraWidget {
private container: HTMLElement
private state: CameraState
private onStateChange?: (state: CameraState) => void
private scene!: THREE.Scene
private camera!: THREE.PerspectiveCamera
private previewCamera!: THREE.PerspectiveCamera
private renderer!: THREE.WebGLRenderer
private activeCamera!: THREE.Camera
private cameraIndicator!: THREE.Mesh
private camGlow!: THREE.Mesh
private azimuthHandle!: THREE.Mesh
private azGlow!: THREE.Mesh
private elevationHandle!: THREE.Mesh
private elGlow!: THREE.Mesh
private distanceHandle!: THREE.Mesh
private distGlow!: THREE.Mesh
private glowRing!: THREE.Mesh
private imagePlane!: THREE.Mesh
private imageFrame!: THREE.LineSegments
private planeMat!: THREE.MeshBasicMaterial
private distanceTube: THREE.Mesh | null = null
private azimuthRing!: THREE.Mesh
private elevationArc!: THREE.Mesh
private gridHelper!: THREE.GridHelper
private readonly CENTER = new THREE.Vector3(0, 0.5, 0)
private readonly AZIMUTH_RADIUS = 1.8
private readonly ELEVATION_RADIUS = 1.4
private readonly ELEV_ARC_X = -0.8
private liveAzimuth = 0
private liveElevation = 0
private liveDistance = 5
private isDragging = false
private dragTarget: string | null = null
private hoveredHandle: { mesh: THREE.Mesh; glow: THREE.Mesh; name: string } | null = null
private raycaster = new THREE.Raycaster()
private mouse = new THREE.Vector2()
private useCameraView = false
private isOrbitDragging = false
private orbitStartX = 0
private orbitStartY = 0
private orbitStartAzimuth = 0
private orbitStartElevation = 0
private animationId: number | null = null
private time = 0
constructor(options: CameraWidgetOptions) {
this.container = options.container
this.onStateChange = options.onStateChange
this.state = {
azimuth: options.initialState?.azimuth ?? 0,
elevation: options.initialState?.elevation ?? 0,
distance: options.initialState?.distance ?? 5,
imageUrl: options.initialState?.imageUrl ?? null
}
this.liveAzimuth = this.state.azimuth
this.liveElevation = this.state.elevation
this.liveDistance = this.state.distance
this.initThreeJS()
this.bindEvents()
this.animate()
}
private initThreeJS(): void {
const width = this.container.clientWidth || 300
const height = this.container.clientHeight || 300
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x0a0a0f)
this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
this.camera.position.set(4, 3.5, 4)
this.camera.lookAt(0, 0.3, 0)
this.previewCamera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100)
this.activeCamera = this.camera
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
this.renderer.setSize(width, height, false)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
this.renderer.outputColorSpace = THREE.SRGBColorSpace
this.container.appendChild(this.renderer.domElement)
const canvas = this.renderer.domElement
canvas.style.position = 'absolute'
canvas.style.top = '0'
canvas.style.left = '0'
canvas.style.width = '100%'
canvas.style.height = '100%'
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
this.scene.add(ambientLight)
const mainLight = new THREE.DirectionalLight(0xffffff, 0.8)
mainLight.position.set(5, 10, 5)
this.scene.add(mainLight)
const fillLight = new THREE.DirectionalLight(0xE93D82, 0.3)
fillLight.position.set(-5, 5, -5)
this.scene.add(fillLight)
this.gridHelper = new THREE.GridHelper(5, 20, 0x1a1a2e, 0x12121a)
this.gridHelper.position.y = -0.01
this.scene.add(this.gridHelper)
this.createSubject()
this.createCameraIndicator()
this.createAzimuthRing()
this.createElevationArc()
this.createDistanceHandle()
this.updateVisuals()
}
private createGridTexture(): THREE.CanvasTexture {
const canvas = document.createElement('canvas')
const size = 256
canvas.width = size
canvas.height = size
const ctx = canvas.getContext('2d')!
ctx.fillStyle = '#1a1a2a'
ctx.fillRect(0, 0, size, size)
ctx.strokeStyle = '#2a2a3a'
ctx.lineWidth = 1
const gridSize = 16
for (let i = 0; i <= size; i += gridSize) {
ctx.beginPath()
ctx.moveTo(i, 0)
ctx.lineTo(i, size)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(0, i)
ctx.lineTo(size, i)
ctx.stroke()
}
const texture = new THREE.CanvasTexture(canvas)
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
texture.repeat.set(4, 4)
return texture
}
private createSubject(): void {
const cardThickness = 0.02
const cardGeo = new THREE.BoxGeometry(1.2, 1.2, cardThickness)
const frontMat = new THREE.MeshBasicMaterial({ color: 0x3a3a4a })
const backMat = new THREE.MeshBasicMaterial({ map: this.createGridTexture() })
const edgeMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2a })
const cardMaterials = [edgeMat, edgeMat, edgeMat, edgeMat, frontMat, backMat]
this.imagePlane = new THREE.Mesh(cardGeo, cardMaterials)
this.imagePlane.position.copy(this.CENTER)
this.scene.add(this.imagePlane)
this.planeMat = frontMat
const frameGeo = new THREE.EdgesGeometry(cardGeo)
const frameMat = new THREE.LineBasicMaterial({ color: 0xE93D82 })
this.imageFrame = new THREE.LineSegments(frameGeo, frameMat)
this.imageFrame.position.copy(this.CENTER)
this.scene.add(this.imageFrame)
const glowRingGeo = new THREE.RingGeometry(0.55, 0.58, 64)
const glowRingMat = new THREE.MeshBasicMaterial({
color: 0xE93D82,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide
})
this.glowRing = new THREE.Mesh(glowRingGeo, glowRingMat)
this.glowRing.position.set(0, 0.01, 0)
this.glowRing.rotation.x = -Math.PI / 2
this.scene.add(this.glowRing)
}
private createCameraIndicator(): void {
const camGeo = new THREE.ConeGeometry(0.15, 0.4, 4)
const camMat = new THREE.MeshStandardMaterial({
color: 0xE93D82,
emissive: 0xE93D82,
emissiveIntensity: 0.5,
metalness: 0.8,
roughness: 0.2
})
this.cameraIndicator = new THREE.Mesh(camGeo, camMat)
this.scene.add(this.cameraIndicator)
const camGlowGeo = new THREE.SphereGeometry(0.08, 16, 16)
const camGlowMat = new THREE.MeshBasicMaterial({
color: 0xff6ba8,
transparent: true,
opacity: 0.8
})
this.camGlow = new THREE.Mesh(camGlowGeo, camGlowMat)
this.scene.add(this.camGlow)
}
private createAzimuthRing(): void {
const azRingGeo = new THREE.TorusGeometry(this.AZIMUTH_RADIUS, 0.04, 16, 100)
const azRingMat = new THREE.MeshBasicMaterial({
color: 0xE93D82,
transparent: true,
opacity: 0.7
})
this.azimuthRing = new THREE.Mesh(azRingGeo, azRingMat)
this.azimuthRing.rotation.x = Math.PI / 2
this.azimuthRing.position.y = 0.02
this.scene.add(this.azimuthRing)
const azHandleGeo = new THREE.SphereGeometry(0.16, 32, 32)
const azHandleMat = new THREE.MeshStandardMaterial({
color: 0xE93D82,
emissive: 0xE93D82,
emissiveIntensity: 0.6,
metalness: 0.3,
roughness: 0.4
})
this.azimuthHandle = new THREE.Mesh(azHandleGeo, azHandleMat)
this.scene.add(this.azimuthHandle)
const azGlowGeo = new THREE.SphereGeometry(0.22, 16, 16)
const azGlowMat = new THREE.MeshBasicMaterial({
color: 0xE93D82,
transparent: true,
opacity: 0.2
})
this.azGlow = new THREE.Mesh(azGlowGeo, azGlowMat)
this.scene.add(this.azGlow)
}
private createElevationArc(): void {
const arcPoints: THREE.Vector3[] = []
for (let i = 0; i <= 32; i++) {
const angle = (-30 + (90 * i / 32)) * Math.PI / 180
arcPoints.push(new THREE.Vector3(
this.ELEV_ARC_X,
this.ELEVATION_RADIUS * Math.sin(angle) + this.CENTER.y,
this.ELEVATION_RADIUS * Math.cos(angle)
))
}
const arcCurve = new THREE.CatmullRomCurve3(arcPoints)
const elArcGeo = new THREE.TubeGeometry(arcCurve, 32, 0.04, 8, false)
const elArcMat = new THREE.MeshBasicMaterial({
color: 0x00FFD0,
transparent: true,
opacity: 0.8
})
this.elevationArc = new THREE.Mesh(elArcGeo, elArcMat)
this.scene.add(this.elevationArc)
const elHandleGeo = new THREE.SphereGeometry(0.16, 32, 32)
const elHandleMat = new THREE.MeshStandardMaterial({
color: 0x00FFD0,
emissive: 0x00FFD0,
emissiveIntensity: 0.6,
metalness: 0.3,
roughness: 0.4
})
this.elevationHandle = new THREE.Mesh(elHandleGeo, elHandleMat)
this.scene.add(this.elevationHandle)
const elGlowGeo = new THREE.SphereGeometry(0.22, 16, 16)
const elGlowMat = new THREE.MeshBasicMaterial({
color: 0x00FFD0,
transparent: true,
opacity: 0.2
})
this.elGlow = new THREE.Mesh(elGlowGeo, elGlowMat)
this.scene.add(this.elGlow)
}
private createDistanceHandle(): void {
const distHandleGeo = new THREE.SphereGeometry(0.15, 32, 32)
const distHandleMat = new THREE.MeshStandardMaterial({
color: 0xFFB800,
emissive: 0xFFB800,
emissiveIntensity: 0.7,
metalness: 0.5,
roughness: 0.3
})
this.distanceHandle = new THREE.Mesh(distHandleGeo, distHandleMat)
this.scene.add(this.distanceHandle)
const distGlowGeo = new THREE.SphereGeometry(0.22, 16, 16)
const distGlowMat = new THREE.MeshBasicMaterial({
color: 0xFFB800,
transparent: true,
opacity: 0.25
})
this.distGlow = new THREE.Mesh(distGlowGeo, distGlowMat)
this.scene.add(this.distGlow)
}
private updateDistanceLine(start: THREE.Vector3, end: THREE.Vector3): void {
if (this.distanceTube) {
this.scene.remove(this.distanceTube)
this.distanceTube.geometry.dispose()
;(this.distanceTube.material as THREE.Material).dispose()
}
const path = new THREE.LineCurve3(start, end)
const tubeGeo = new THREE.TubeGeometry(path, 1, 0.025, 8, false)
const tubeMat = new THREE.MeshBasicMaterial({
color: 0xFFB800,
transparent: true,
opacity: 0.8
})
this.distanceTube = new THREE.Mesh(tubeGeo, tubeMat)
this.scene.add(this.distanceTube)
}
private updateVisuals(): void {
const azRad = (this.liveAzimuth * Math.PI) / 180
const elRad = (this.liveElevation * Math.PI) / 180
const visualDist = 2.6 - (this.liveDistance / 10) * 2.0
const camX = visualDist * Math.sin(azRad) * Math.cos(elRad)
const camY = this.CENTER.y + visualDist * Math.sin(elRad)
const camZ = visualDist * Math.cos(azRad) * Math.cos(elRad)
this.cameraIndicator.position.set(camX, camY, camZ)
this.cameraIndicator.lookAt(this.CENTER)
this.cameraIndicator.rotateX(Math.PI / 2)
this.camGlow.position.copy(this.cameraIndicator.position)
const azX = this.AZIMUTH_RADIUS * Math.sin(azRad)
const azZ = this.AZIMUTH_RADIUS * Math.cos(azRad)
this.azimuthHandle.position.set(azX, 0.16, azZ)
this.azGlow.position.copy(this.azimuthHandle.position)
const elY = this.CENTER.y + this.ELEVATION_RADIUS * Math.sin(elRad)
const elZ = this.ELEVATION_RADIUS * Math.cos(elRad)
this.elevationHandle.position.set(this.ELEV_ARC_X, elY, elZ)
this.elGlow.position.copy(this.elevationHandle.position)
const distT = 0.15 + ((10 - this.liveDistance) / 10) * 0.7
this.distanceHandle.position.lerpVectors(this.CENTER, this.cameraIndicator.position, distT)
this.distGlow.position.copy(this.distanceHandle.position)
this.updateDistanceLine(this.CENTER.clone(), this.cameraIndicator.position.clone())
this.previewCamera.position.copy(this.cameraIndicator.position)
this.previewCamera.lookAt(this.CENTER)
this.glowRing.rotation.z += 0.005
}
private bindEvents(): void {
const canvas = this.renderer.domElement
canvas.addEventListener('mousedown', this.onPointerDown.bind(this))
canvas.addEventListener('mousemove', this.onPointerMove.bind(this))
canvas.addEventListener('mouseup', this.onPointerUp.bind(this))
canvas.addEventListener('mouseleave', this.onPointerUp.bind(this))
canvas.addEventListener('touchstart', (e) => {
e.preventDefault()
this.onPointerDown({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY } as MouseEvent)
}, { passive: false })
canvas.addEventListener('touchmove', (e) => {
e.preventDefault()
this.onPointerMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY } as MouseEvent)
}, { passive: false })
canvas.addEventListener('touchend', () => this.onPointerUp())
canvas.addEventListener('wheel', this.onWheel.bind(this), { passive: false })
const resizeObserver = new ResizeObserver(() => {
this.onResize()
})
resizeObserver.observe(this.container)
}
private getMousePos(event: MouseEvent): void {
const rect = this.renderer.domElement.getBoundingClientRect()
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
}
private setHandleScale(handle: THREE.Mesh, glow: THREE.Mesh | null, scale: number): void {
handle.scale.setScalar(scale)
if (glow) glow.scale.setScalar(scale)
}
private onPointerDown(event: MouseEvent): void {
this.getMousePos(event)
if (this.useCameraView) {
this.isOrbitDragging = true
this.orbitStartX = event.clientX
this.orbitStartY = event.clientY
this.orbitStartAzimuth = this.liveAzimuth
this.orbitStartElevation = this.liveElevation
this.renderer.domElement.style.cursor = 'grabbing'
return
}
this.raycaster.setFromCamera(this.mouse, this.camera)
const handles = [
{ mesh: this.azimuthHandle, glow: this.azGlow, name: 'azimuth' },
{ mesh: this.elevationHandle, glow: this.elGlow, name: 'elevation' },
{ mesh: this.distanceHandle, glow: this.distGlow, name: 'distance' }
]
for (const h of handles) {
if (this.raycaster.intersectObject(h.mesh).length > 0) {
this.isDragging = true
this.dragTarget = h.name
this.setHandleScale(h.mesh, h.glow, 1.3)
this.renderer.domElement.style.cursor = 'grabbing'
return
}
}
}
private onPointerMove(event: MouseEvent): void {
this.getMousePos(event)
if (this.useCameraView && this.isOrbitDragging) {
const deltaX = event.clientX - this.orbitStartX
const deltaY = event.clientY - this.orbitStartY
const sensitivity = 0.5
let newAzimuth = this.orbitStartAzimuth - deltaX * sensitivity
while (newAzimuth < 0) newAzimuth += 360
while (newAzimuth >= 360) newAzimuth -= 360
this.liveAzimuth = newAzimuth
this.state.azimuth = Math.round(this.liveAzimuth)
let newElevation = this.orbitStartElevation + deltaY * sensitivity
newElevation = Math.max(-30, Math.min(60, newElevation))
this.liveElevation = newElevation
this.state.elevation = Math.round(this.liveElevation)
this.updateVisuals()
this.notifyStateChange()
return
}
this.raycaster.setFromCamera(this.mouse, this.camera)
if (!this.isDragging) {
const handles = [
{ mesh: this.azimuthHandle, glow: this.azGlow, name: 'azimuth' },
{ mesh: this.elevationHandle, glow: this.elGlow, name: 'elevation' },
{ mesh: this.distanceHandle, glow: this.distGlow, name: 'distance' }
]
let foundHover: typeof handles[0] | null = null
for (const h of handles) {
if (this.raycaster.intersectObject(h.mesh).length > 0) {
foundHover = h
break
}
}
if (this.hoveredHandle && this.hoveredHandle !== foundHover) {
this.setHandleScale(this.hoveredHandle.mesh, this.hoveredHandle.glow, 1.0)
}
if (foundHover) {
this.setHandleScale(foundHover.mesh, foundHover.glow, 1.15)
this.renderer.domElement.style.cursor = 'grab'
this.hoveredHandle = foundHover
} else {
this.renderer.domElement.style.cursor = 'default'
this.hoveredHandle = null
}
return
}
const plane = new THREE.Plane()
const intersect = new THREE.Vector3()
if (this.dragTarget === 'azimuth') {
plane.setFromNormalAndCoplanarPoint(new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 0))
if (this.raycaster.ray.intersectPlane(plane, intersect)) {
let angle = Math.atan2(intersect.x, intersect.z) * (180 / Math.PI)
if (angle < 0) angle += 360
this.liveAzimuth = Math.max(0, Math.min(360, angle))
this.state.azimuth = Math.round(this.liveAzimuth)
this.updateVisuals()
this.notifyStateChange()
}
} else if (this.dragTarget === 'elevation') {
const elevPlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), -this.ELEV_ARC_X)
if (this.raycaster.ray.intersectPlane(elevPlane, intersect)) {
const relY = intersect.y - this.CENTER.y
const relZ = intersect.z
let angle = Math.atan2(relY, relZ) * (180 / Math.PI)
angle = Math.max(-30, Math.min(60, angle))
this.liveElevation = angle
this.state.elevation = Math.round(this.liveElevation)
this.updateVisuals()
this.notifyStateChange()
}
} else if (this.dragTarget === 'distance') {
const newDist = 5 - this.mouse.y * 5
this.liveDistance = Math.max(0, Math.min(10, newDist))
this.state.distance = Math.round(this.liveDistance * 10) / 10
this.updateVisuals()
this.notifyStateChange()
}
}
private onPointerUp(): void {
if (this.isOrbitDragging) {
this.isOrbitDragging = false
this.renderer.domElement.style.cursor = this.useCameraView ? 'grab' : 'default'
return
}
if (this.isDragging) {
const handles = [
{ mesh: this.azimuthHandle, glow: this.azGlow },
{ mesh: this.elevationHandle, glow: this.elGlow },
{ mesh: this.distanceHandle, glow: this.distGlow }
]
handles.forEach(h => this.setHandleScale(h.mesh, h.glow, 1.0))
}
this.isDragging = false
this.dragTarget = null
this.renderer.domElement.style.cursor = 'default'
}
private onWheel(event: WheelEvent): void {
if (!this.useCameraView) return
event.preventDefault()
const sensitivity = 0.01
let newDistance = this.liveDistance - event.deltaY * sensitivity
newDistance = Math.max(0, Math.min(10, newDistance))
this.liveDistance = newDistance
this.state.distance = Math.round(this.liveDistance * 10) / 10
this.updateVisuals()
this.notifyStateChange()
}
private onResize(): void {
const w = this.container.clientWidth
const h = this.container.clientHeight
if (w === 0 || h === 0) return
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.previewCamera.aspect = w / h
this.previewCamera.updateProjectionMatrix()
this.renderer.setSize(w, h, false)
}
private animate(): void {
this.animationId = requestAnimationFrame(() => this.animate())
this.time += 0.01
const pulse = 1 + Math.sin(this.time * 2) * 0.03
this.camGlow.scale.setScalar(pulse)
this.glowRing.rotation.z += 0.003
this.renderer.render(this.scene, this.activeCamera)
}
private notifyStateChange(): void {
if (this.onStateChange) {
this.onStateChange({ ...this.state })
}
}
public generatePrompt(): string {
const hAngle = this.state.azimuth % 360
let hDirection: string
if (hAngle < 22.5 || hAngle >= 337.5) {
hDirection = "front view"
} else if (hAngle < 67.5) {
hDirection = "front-right quarter view"
} else if (hAngle < 112.5) {
hDirection = "right side view"
} else if (hAngle < 157.5) {
hDirection = "back-right quarter view"
} else if (hAngle < 202.5) {
hDirection = "back view"
} else if (hAngle < 247.5) {
hDirection = "back-left quarter view"
} else if (hAngle < 292.5) {
hDirection = "left side view"
} else {
hDirection = "front-left quarter view"
}
let vDirection: string
if (this.state.elevation < -15) {
vDirection = "low-angle shot"
} else if (this.state.elevation < 15) {
vDirection = "eye-level shot"
} else if (this.state.elevation < 45) {
vDirection = "elevated shot"
} else {
vDirection = "high-angle shot"
}
let distance: string
if (this.state.distance < 2) {
distance = "wide shot"
} else if (this.state.distance < 6) {
distance = "medium shot"
} else {
distance = "close-up"
}
return `<sks> ${hDirection} ${vDirection} ${distance}`
}
public setState(newState: Partial<CameraState>): void {
if (newState.azimuth !== undefined) {
this.state.azimuth = newState.azimuth
this.liveAzimuth = newState.azimuth
}
if (newState.elevation !== undefined) {
this.state.elevation = newState.elevation
this.liveElevation = newState.elevation
}
if (newState.distance !== undefined) {
this.state.distance = newState.distance
this.liveDistance = newState.distance
}
if (newState.imageUrl !== undefined) {
this.state.imageUrl = newState.imageUrl
this.updateImage(newState.imageUrl)
}
this.updateVisuals()
}
public getState(): CameraState {
return { ...this.state }
}
public getPrompt(): string {
return this.generatePrompt()
}
public resetToDefaults(): void {
this.state.azimuth = 0
this.state.elevation = 0
this.state.distance = 5.0
this.liveAzimuth = 0
this.liveElevation = 0
this.liveDistance = 5.0
this.updateVisuals()
this.notifyStateChange()
}
public setCameraView(enabled: boolean): void {
this.useCameraView = enabled
this.isOrbitDragging = false
if (this.useCameraView) {
this.activeCamera = this.previewCamera
this.azimuthRing.visible = false
this.azimuthHandle.visible = false
this.azGlow.visible = false
this.elevationArc.visible = false
this.elevationHandle.visible = false
this.elGlow.visible = false
this.distanceHandle.visible = false
this.distGlow.visible = false
if (this.distanceTube) this.distanceTube.visible = false
this.cameraIndicator.visible = false
this.camGlow.visible = false
this.glowRing.visible = false
this.gridHelper.visible = false
this.imageFrame.visible = false
this.renderer.domElement.style.cursor = 'grab'
} else {
this.activeCamera = this.camera
this.azimuthRing.visible = true
this.azimuthHandle.visible = true
this.azGlow.visible = true
this.elevationArc.visible = true
this.elevationHandle.visible = true
this.elGlow.visible = true
this.distanceHandle.visible = true
this.distGlow.visible = true
if (this.distanceTube) this.distanceTube.visible = true
this.cameraIndicator.visible = true
this.camGlow.visible = true
this.glowRing.visible = true
this.gridHelper.visible = true
this.imageFrame.visible = true
this.renderer.domElement.style.cursor = 'default'
}
}
public updateImage(url: string | null): void {
if (url) {
const img = new Image()
if (!url.startsWith('data:')) {
img.crossOrigin = 'anonymous'
}
img.onload = () => {
const tex = new THREE.Texture(img)
tex.colorSpace = THREE.SRGBColorSpace
tex.needsUpdate = true
this.planeMat.map = tex
this.planeMat.color.set(0xffffff)
this.planeMat.needsUpdate = true
const ar = img.width / img.height
const maxSize = 1.5
let scaleX: number, scaleY: number
if (ar > 1) {
scaleX = maxSize
scaleY = maxSize / ar
} else {
scaleY = maxSize
scaleX = maxSize * ar
}
this.imagePlane.scale.set(scaleX, scaleY, 1)
this.imageFrame.scale.set(scaleX, scaleY, 1)
}
img.onerror = () => {
this.planeMat.map = null
this.planeMat.color.set(0xE93D82)
this.planeMat.needsUpdate = true
}
img.src = url
} else {
this.planeMat.map = null
this.planeMat.color.set(0x3a3a4a)
this.planeMat.needsUpdate = true
this.imagePlane.scale.set(1, 1, 1)
this.imageFrame.scale.set(1, 1, 1)
}
}
public dispose(): void {
if (this.animationId !== null) {
try {
window.cancelAnimationFrame(this.animationId)
} catch {
}
this.animationId = null
}
try {
this.renderer.dispose()
} catch {
}
try {
this.scene.clear()
} catch {
}
}
}