import { PerspectiveCamera , Vector4 , MathUtils } from 'three' ; import { Tween , Easing } from '@tweenjs/tween.js' ; import { getMaxFullScreenDepthForPlane , toAbsolutePosition } from './background-camera-utils' ; import { PlaneMesh } from './background' ; import { TransitionConfig , LoopableTransitionConfig } from './transition' ; import { clamp } from './utils' ; interface CameraPosition { x ? : number ; y ? : number ; z ? : number ; } interface CameraPositionWithRotation extends CameraPosition { zr ? : number ; } type CameraOffset = CameraPositionWithRotation ; interface CameraPositionTween { x : number ; y : number ; z : number ; } interface CameraRotationTween { zr : number ; } interface CameraSwayTween { offsetX : number ; offsetY : number ; offsetZ : number ; offsetZR : number ; } const CameraZoomRange = 0.9 ; class BackgroundCamera { private _plane : PlaneMesh ; public readonly camera : PerspectiveCamera ; private readonly _position : Vector4 = new Vector4 ( 0 , 0 , 1 , 0 ) ; private readonly _positionOffset : Vector4 = new Vector4 ( 0 , 0 , 0 , 0 ) ; private readonly _positionWithOffset : Vector4 = this . _position . clone ( ) ; private _positionTransition : Tween < CameraPositionTween > = new Tween ( { x : 0 , y : 0 , z : 0 } ) ; private _rotationTransition : Tween < CameraRotationTween > = new Tween ( { zr : 0 } ) ; private readonly _swayOffset = new Vector4 ( 0 , 0 , 0 , 0 ) ; private _swayTransition : Tween < CameraSwayTween > = new Tween ( { offsetX : 0 , offsetY : 0 , offsetZ : 0 , offsetZR : 0 } ) ; param { PlaneMesh } plane param { Number } width param { Number } height constructor ( plane , width , height ) { this . _plane = plane ; this . camera = new PerspectiveCamera ( 35 , width / height , 0.001 ) ; } returns get position ( ) : CameraPositionWithRotation { const { x , y , z , w : zr } = this . _position ; return { x , y , z , zr } ; } returns get positionOffset ( ) : CameraPositionWithRotation { const { x , y , z , w : zr } = this . _positionOffset ; return { x , y , z , zr } ; } returns isMoving ( ) { return this . _positionTransition . isPlaying ( ) ; } returns isRotating ( ) { return this . _rotationTransition . isPlaying ( ) ; } returns isSwaying ( ) { return this . _swayTransition . isPlaying ( ) ; } param { number } width param { number } height setSize ( width , height ) { this . camera . aspect = width / height ; this . camera . updateProjectionMatrix ( ) ; } param { CameraPositionWithRotation } offset offset ( offset ) { const { x = 0 , y = 0 , z = 0 , zr = 0 } = offset ; this . _positionOffset . set ( x , y , z , zr ) ; } param { CameraOffset | boolean } offset param { LoopableTransitionConfig } transition sway ( offset , transition = { } ) { if ( typeof offset === 'boolean' ) { if ( ! offset ) { this . _swayTransition . stop ( ) ; } return ; } this . _swayTransition . stop ( ) ; const { loop = false , duration = 0 , delay = 0 , easing = Easing . Linear . None , onInit = ( ) => ( { } ) , onStart = ( ) => ( { } ) , onUpdate = ( ) => ( { } ) , onComplete = ( ) => ( { } ) , onStop = ( ) => ( { } ) , } = transition ; const { x = 0 , y = 0 , z = 0 , zr = 0 } = offset ; const zrInRadians = MathUtils . degToRad ( zr ) ; const xPosition = clamp ( this . _position . x + this . _positionOffset . x , 0 , 1 ) ; const xMin = Math . max ( 0 , xPosition - x ) ; const xMax = Math . min ( 1 , xPosition + x ) ; const xRange = xMax - xMin ; const xOffset = ( xMin + xRange * Math . random ( ) ) - xPosition ; const yPosition = clamp ( this . _position . y + this . _positionOffset . y , 0 , 1 ) ; const yMin = Math . max ( 0 , yPosition - y ) ; const yMax = Math . min ( 1 , yPosition + y ) ; const yRange = yMax - yMin ; const yOffset = ( yMin + yRange * Math . random ( ) ) - yPosition ; const zPosition = clamp ( this . _position . z + this . _positionOffset . z , 0 , 1 ) ; const zMin = Math . max ( 0 , zPosition - z ) ; const zMax = Math . min ( 1 , zPosition + z ) ; const zRange = zMax - zMin ; const zOffset = ( zMin + zRange * Math . random ( ) ) - zPosition ; onInit ( ) ; this . _swayTransition = new Tween ( { offsetX : this . _swayOffset . x , offsetY : this . _swayOffset . y , offsetZ : this . _swayOffset . z , offsetZR : this . _swayOffset . w , } ) . to ( { offsetX : xOffset , offsetY : yOffset , offsetZ : zOffset , offsetZR : - zrInRadians + Math . random ( ) * zrInRadians * 2 , } , duration * 1000 ) . easing ( easing ) . onStart ( onStart ) . onUpdate ( ( { offsetX , offsetY , offsetZ , offsetZR } ) => { this . _swayOffset . set ( offsetX , offsetY , offsetZ , offsetZR ) ; onUpdate ( ) ; } ) . onComplete ( ( ) => { if ( loop ) { this . sway ( offset , transition ) ; } onComplete ( ) ; } ) . onStop ( onStop ) . delay ( delay * 1000 ) . start ( ) ; } param { number | boolean } angle param { TransitionConfig } transition rotate ( angle , transition = { } ) { if ( typeof angle === 'boolean' ) { if ( ! angle ) { this . _rotationTransition . stop ( ) ; } return ; } this . _rotationTransition . stop ( ) ; const { duration = 0 , delay = 0 , easing = Easing . Linear . None , onInit = ( ) => ( { } ) , onStart = ( ) => ( { } ) , onUpdate = ( ) => ( { } ) , onComplete = ( ) => ( { } ) , onStop = ( ) => ( { } ) , } = transition ; const angleInRadians = MathUtils . degToRad ( angle ) ; onInit ( ) ; if ( duration > 0 || delay > 0 ) { this . _rotationTransition = new Tween ( { zr : this . _position . w } ) . to ( { zr : angleInRadians } , duration * 1000 ) . easing ( easing ) . onStart ( onStart ) . onUpdate ( ( { zr } ) => { this . _position . set ( this . _position . x , this . _position . y , this . _position . z , zr ) ; onUpdate ( ) ; } ) . onComplete ( onComplete ) . onStop ( onStop ) . delay ( delay * 1000 ) . start ( ) ; } else { this . _position . set ( this . _position . x , this . _position . y , this . _position . z , angleInRadians ) ; } } param { CameraPosition | boolean } position param { TransitionConfig } transition move ( position , transition = { } ) { if ( typeof position === 'boolean' ) { if ( ! position ) { this . _positionTransition . stop ( ) ; } return ; } this . _positionTransition . stop ( ) ; const { x : currentX , y : currentY , z : currentZ } = this . _position ; const { x = currentX , y = currentY , z = currentZ } = position ; const { duration = 0 , delay = 0 , easing = Easing . Linear . None , onInit = ( ) => ( { } ) , onStart = ( ) => ( { } ) , onUpdate = ( ) => ( { } ) , onComplete = ( ) => ( { } ) , onStop = ( ) => ( { } ) , } = transition ; onInit ( ) ; if ( duration > 0 || delay > 0 ) { this . _positionTransition = new Tween ( { x : currentX , y : currentY , z : currentZ } ) . to ( { x , y , z } , duration * 1000 ) . easing ( easing ) . onStart ( onStart ) . onUpdate ( ( { x , y , z } ) => { this . _position . set ( x , y , z , this . _position . w ) ; onUpdate ( ) ; } ) . onComplete ( onComplete ) . onStop ( onStop ) . delay ( delay * 1000 ) . start ( ) ; } else { this . _position . set ( x , y , z , this . _position . w ) ; } } /**
     * Updates the camera position. Should be called on every render frame.
     */ update ( ) { const swayScale = this . _positionWithOffset . z / getMaxFullScreenDepthForPlane ( this . _plane , this . camera , this . camera . rotation . z ) ; this . _positionWithOffset . set ( clamp ( this . _position . x + this . _positionOffset . x + this . _swayOffset . x * swayScale , 0 , 1 ) , clamp ( this . _position . y + this . _positionOffset . y + this . _swayOffset . y * swayScale , 0 , 1 ) , clamp ( ( this . _position . z + this . _positionOffset . z + this . _swayOffset . z ) * CameraZoomRange + ( 1.0 - CameraZoomRange ) , 0 , 1 ) , this . _position . w + MathUtils . degToRad ( this . _positionOffset . w ) + this . _swayOffset . w ) ; const { x : absoluteX , y : absoluteY , z : absoluteDepth } = toAbsolutePosition ( this . _plane , this . camera , this . _positionWithOffset ) ; this . camera . position . set ( absoluteX , absoluteY , absoluteDepth ) ; this . camera . rotation . z = this . _position . w + MathUtils . degToRad ( this . _positionOffset . w ) + this . _swayOffset . w ; } /**
     * Disposes this object. Call when this object is no longer needed, otherwise leaks may occur.
     */ dispose ( ) { this . sway ( false ) ; this . move ( false ) ; this . rotate ( false ) ; } } export { CameraPosition , CameraPositionWithRotation , CameraOffset , BackgroundCamera , } ; export default BackgroundCamera ;