初版
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { APP_CONFIG } from '@/constants/config'
|
||||
|
||||
type PointerTarget = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
type TravelSceneController = {
|
||||
setStarted: (started: boolean) => void
|
||||
setSpeed: (speed: number) => void
|
||||
setPointerTarget: (pointer: PointerTarget) => void
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
type StarState = {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
vx: number
|
||||
vy: number
|
||||
vz: number
|
||||
ax: number
|
||||
ay: number
|
||||
az: number
|
||||
delay: number
|
||||
baseSize: number
|
||||
baseAlpha: number
|
||||
}
|
||||
|
||||
type SmokeState = {
|
||||
mesh: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial>
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
age: number
|
||||
vx: number
|
||||
vy: number
|
||||
vz: number
|
||||
rotation: number
|
||||
spin: number
|
||||
baseScaleX: number
|
||||
baseScaleY: number
|
||||
opacity: number
|
||||
wobble: number
|
||||
wobbleSpeed: number
|
||||
}
|
||||
|
||||
const STAR_COUNT = APP_CONFIG.starfield.count
|
||||
const STAR_SOURCE_RADIUS = APP_CONFIG.starfield.sourceRadius
|
||||
const STAR_FAR_Z = APP_CONFIG.starfield.farZ
|
||||
const STAR_RESET_Z = APP_CONFIG.starfield.resetZ
|
||||
const STAR_BOUNDS_X = APP_CONFIG.starfield.boundsX
|
||||
const STAR_BOUNDS_Y = APP_CONFIG.starfield.boundsY
|
||||
|
||||
const SMOKE_COUNT = APP_CONFIG.smoke.count
|
||||
const SMOKE_SOURCE_RADIUS = APP_CONFIG.smoke.sourceRadius
|
||||
const SMOKE_FAR_Z = APP_CONFIG.smoke.farZ
|
||||
const SMOKE_RESET_Z = APP_CONFIG.smoke.resetZ
|
||||
const SMOKE_BOUNDS_X = APP_CONFIG.smoke.boundsX
|
||||
const SMOKE_BOUNDS_Y = APP_CONFIG.smoke.boundsY
|
||||
|
||||
const STAR_COLORS = ['#ffffff', '#f3f8ff', '#e8f4ff', '#d7eeff', '#fff7ea'] as const
|
||||
const SMOKE_COLORS = ['#8dd8ff', '#adc4ff', '#d6b5ff', '#ffe7ff'] as const
|
||||
|
||||
const STAR_VERTEX_SHADER = `
|
||||
attribute float aSizeBase;
|
||||
attribute float aAlphaBase;
|
||||
attribute float aProgress;
|
||||
attribute float aTail;
|
||||
attribute vec2 aDir;
|
||||
varying vec3 vColor;
|
||||
varying float vAlpha;
|
||||
varying float vTail;
|
||||
varying vec2 vDir;
|
||||
|
||||
void main() {
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
float scaledProgress = max(aProgress, 0.0);
|
||||
float pointSize = aSizeBase * (0.95 + scaledProgress * 2.85) * ${APP_CONFIG.starfield.pointScale.toFixed(1)} / max(1.0, -mvPosition.z);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
gl_PointSize = clamp(pointSize, ${APP_CONFIG.starfield.pointMinSize.toFixed(1)}, ${APP_CONFIG.starfield.pointMaxSize.toFixed(1)});
|
||||
vColor = color;
|
||||
vAlpha = aAlphaBase * (0.32 + scaledProgress * 0.68);
|
||||
vTail = aTail;
|
||||
vDir = aDir;
|
||||
}
|
||||
`
|
||||
|
||||
const STAR_FRAGMENT_SHADER = `
|
||||
varying vec3 vColor;
|
||||
varying float vAlpha;
|
||||
varying float vTail;
|
||||
varying vec2 vDir;
|
||||
|
||||
void main() {
|
||||
vec2 centered = gl_PointCoord - vec2(0.5);
|
||||
float distanceToCenter = length(centered);
|
||||
float core = 1.0 - smoothstep(0.0, 0.22, distanceToCenter);
|
||||
float halo = 1.0 - smoothstep(0.12, 0.5, distanceToCenter);
|
||||
vec2 dir = length(vDir) > 0.0001 ? normalize(vDir) : vec2(0.0, 1.0);
|
||||
vec2 side = vec2(-dir.y, dir.x);
|
||||
float along = dot(centered, dir);
|
||||
float across = dot(centered, side);
|
||||
float tailLong = 1.0 - smoothstep(-${(0.18 + APP_CONFIG.starfield.trailLength).toFixed(2)}, 0.05, along);
|
||||
float tailThin = 1.0 - smoothstep(${(APP_CONFIG.starfield.trailWidth * 0.35).toFixed(2)}, ${(APP_CONFIG.starfield.trailWidth).toFixed(2)}, abs(across));
|
||||
float tailFade = smoothstep(0.01, 0.52, -along);
|
||||
float tail = tailLong * tailThin * tailFade * vTail;
|
||||
float alpha = (core * 0.95 + halo * 0.45 + tail * 0.85) * vAlpha;
|
||||
|
||||
if (alpha < 0.01) {
|
||||
discard;
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(vColor, alpha);
|
||||
}
|
||||
`
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
function lerp(start: number, end: number, alpha: number) {
|
||||
return start + (end - start) * alpha
|
||||
}
|
||||
|
||||
function smoothstep(edge0: number, edge1: number, value: number) {
|
||||
const t = clamp((value - edge0) / (edge1 - edge0), 0, 1)
|
||||
return t * t * (3 - 2 * t)
|
||||
}
|
||||
|
||||
function randomRange(min: number, max: number) {
|
||||
return min + Math.random() * (max - min)
|
||||
}
|
||||
|
||||
function sampleDisk(radius: number) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const distance = Math.pow(Math.random(), APP_CONFIG.starfield.radialExponent) * radius
|
||||
|
||||
return {
|
||||
x: Math.cos(angle) * distance,
|
||||
y: Math.sin(angle) * distance,
|
||||
}
|
||||
}
|
||||
|
||||
function pickTier() {
|
||||
const roll = Math.random()
|
||||
|
||||
if (roll > 0.84) {
|
||||
return 2
|
||||
}
|
||||
|
||||
if (roll > 0.5) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function createSmokeTexture() {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) {
|
||||
throw new Error('Unable to create smoke texture context.')
|
||||
}
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
context.globalCompositeOperation = 'screen'
|
||||
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
const radius = randomRange(42, 94)
|
||||
const x = randomRange(64, 192)
|
||||
const y = randomRange(64, 192)
|
||||
const gradient = context.createRadialGradient(x, y, radius * 0.12, x, y, radius)
|
||||
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)')
|
||||
gradient.addColorStop(0.4, 'rgba(220, 235, 255, 0.26)')
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
||||
|
||||
context.fillStyle = gradient
|
||||
context.beginPath()
|
||||
context.arc(x, y, radius, 0, Math.PI * 2)
|
||||
context.fill()
|
||||
}
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas)
|
||||
texture.colorSpace = THREE.SRGBColorSpace
|
||||
texture.needsUpdate = true
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
function normalizeDirection(x: number, y: number) {
|
||||
const length = Math.hypot(x, y)
|
||||
|
||||
if (length < 1e-4) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
|
||||
return {
|
||||
x: Math.cos(angle),
|
||||
y: Math.sin(angle),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: x / length,
|
||||
y: y / length,
|
||||
}
|
||||
}
|
||||
|
||||
export function createTravelScene(container: HTMLElement): TravelSceneController {
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, APP_CONFIG.scene.maxPixelRatio))
|
||||
renderer.setSize(container.clientWidth, container.clientHeight)
|
||||
renderer.setClearColor(APP_CONFIG.scene.background, 1)
|
||||
container.appendChild(renderer.domElement)
|
||||
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
APP_CONFIG.camera.fov,
|
||||
container.clientWidth / container.clientHeight,
|
||||
APP_CONFIG.camera.near,
|
||||
APP_CONFIG.camera.far,
|
||||
)
|
||||
camera.position.set(0, 0, APP_CONFIG.camera.initialZ)
|
||||
|
||||
const travelRoot = new THREE.Group()
|
||||
scene.add(travelRoot)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight('#d9efff', 0.45)
|
||||
scene.add(ambientLight)
|
||||
|
||||
const starsGeometry = new THREE.BufferGeometry()
|
||||
const starPositions = new Float32Array(STAR_COUNT * 3)
|
||||
const starColors = new Float32Array(STAR_COUNT * 3)
|
||||
const starProgress = new Float32Array(STAR_COUNT)
|
||||
const starSizes = new Float32Array(STAR_COUNT)
|
||||
const starAlpha = new Float32Array(STAR_COUNT)
|
||||
const starTail = new Float32Array(STAR_COUNT)
|
||||
const starDirections = new Float32Array(STAR_COUNT * 2)
|
||||
const starStates = new Array<StarState>(STAR_COUNT)
|
||||
const starColor = new THREE.Color()
|
||||
|
||||
const smokeGeometry = new THREE.PlaneGeometry(1, 1, 1, 1)
|
||||
const smokeTexture = createSmokeTexture()
|
||||
const smokeStates: SmokeState[] = []
|
||||
|
||||
const pointerTarget = { x: 0, y: 0 }
|
||||
const pointerCurrent = { x: 0, y: 0 }
|
||||
|
||||
let started = false
|
||||
let targetSpeed = 1
|
||||
let currentSpeed = 0
|
||||
let animationFrameId = 0
|
||||
let disposed = false
|
||||
|
||||
const starMaterial = new THREE.ShaderMaterial({
|
||||
vertexShader: STAR_VERTEX_SHADER,
|
||||
fragmentShader: STAR_FRAGMENT_SHADER,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
vertexColors: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
})
|
||||
|
||||
function spawnStar(index: number, seededProgress?: number) {
|
||||
const tier = pickTier()
|
||||
const source = sampleDisk(STAR_SOURCE_RADIUS)
|
||||
const direction = normalizeDirection(
|
||||
source.x + randomRange(-1.3, 1.3),
|
||||
source.y + randomRange(-1.3, 1.3),
|
||||
)
|
||||
const progress = seededProgress ?? 0
|
||||
const tierSpeed = [18, 26, 36][tier]
|
||||
const tierDrift = [3.2, 5.2, 8.2][tier]
|
||||
const tierScale = [0.95, 1.25, 1.65][tier]
|
||||
const z = lerp(STAR_FAR_Z, STAR_RESET_Z - 3, progress)
|
||||
const radialTravel = Math.pow(progress, APP_CONFIG.starfield.progressExponent) * (18 + tier * 10)
|
||||
|
||||
const state: StarState = {
|
||||
x: source.x + direction.x * radialTravel + randomRange(-0.6, 0.6) * progress,
|
||||
y: source.y + direction.y * radialTravel + randomRange(-0.6, 0.6) * progress,
|
||||
z,
|
||||
vx: direction.x * tierDrift * (1 + progress * 0.7),
|
||||
vy: direction.y * tierDrift * (1 + progress * 0.7),
|
||||
vz: tierSpeed * randomRange(0.9, 1.16) * (1 + progress * 0.5),
|
||||
ax: direction.x * randomRange(1.4, 3.6),
|
||||
ay: direction.y * randomRange(1.4, 3.6),
|
||||
az: randomRange(5.5, 10.5),
|
||||
delay: seededProgress === undefined ? randomRange(0.03, 0.42) : randomRange(0, 0.12),
|
||||
baseSize: randomRange(APP_CONFIG.starfield.baseSizeMin, APP_CONFIG.starfield.baseSizeMax) * tierScale,
|
||||
baseAlpha: randomRange(APP_CONFIG.starfield.baseAlphaMin, APP_CONFIG.starfield.baseAlphaMax),
|
||||
}
|
||||
|
||||
starStates[index] = state
|
||||
starSizes[index] = state.baseSize
|
||||
starAlpha[index] = state.baseAlpha
|
||||
starTail[index] = 0
|
||||
starDirections[index * 2] = direction.x
|
||||
starDirections[index * 2 + 1] = direction.y
|
||||
|
||||
starColor.set(STAR_COLORS[Math.floor(Math.random() * STAR_COLORS.length)])
|
||||
starColors[index * 3] = starColor.r
|
||||
starColors[index * 3 + 1] = starColor.g
|
||||
starColors[index * 3 + 2] = starColor.b
|
||||
|
||||
starPositions[index * 3] = state.x
|
||||
starPositions[index * 3 + 1] = state.y
|
||||
starPositions[index * 3 + 2] = state.z
|
||||
starProgress[index] = seededProgress ?? 0
|
||||
}
|
||||
|
||||
function spawnSmoke(mesh?: SmokeState['mesh']) {
|
||||
const source = sampleDisk(SMOKE_SOURCE_RADIUS)
|
||||
const direction = normalizeDirection(
|
||||
source.x + randomRange(-2.2, 2.2),
|
||||
source.y + randomRange(-2.2, 2.2),
|
||||
)
|
||||
const scaleBase = randomRange(5.8, 14.5)
|
||||
const material = mesh
|
||||
? mesh.material
|
||||
: new THREE.MeshBasicMaterial({
|
||||
map: smokeTexture,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
side: THREE.DoubleSide,
|
||||
blending: THREE.AdditiveBlending,
|
||||
})
|
||||
material.color.set(SMOKE_COLORS[Math.floor(Math.random() * SMOKE_COLORS.length)])
|
||||
|
||||
const smokeMesh =
|
||||
mesh ??
|
||||
new THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial>(smokeGeometry, material)
|
||||
|
||||
const state: SmokeState = {
|
||||
mesh: smokeMesh,
|
||||
x: source.x,
|
||||
y: source.y,
|
||||
z: randomRange(SMOKE_FAR_Z, SMOKE_FAR_Z + 16),
|
||||
age: 0,
|
||||
vx: direction.x * randomRange(1.8, 4.6),
|
||||
vy: direction.y * randomRange(1.8, 4.6),
|
||||
vz: randomRange(9.5, 17.5),
|
||||
rotation: randomRange(0, Math.PI * 2),
|
||||
spin: randomRange(-0.16, 0.16),
|
||||
baseScaleX: scaleBase * randomRange(0.8, 1.2),
|
||||
baseScaleY: scaleBase * randomRange(0.75, 1.25),
|
||||
opacity: randomRange(0.05, 0.13),
|
||||
wobble: randomRange(0.5, 1.2),
|
||||
wobbleSpeed: randomRange(0.35, 0.9),
|
||||
}
|
||||
|
||||
smokeMesh.position.set(state.x, state.y, state.z)
|
||||
smokeMesh.scale.set(state.baseScaleX, state.baseScaleY, 1)
|
||||
smokeMesh.material.opacity = 0
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
for (let index = 0; index < STAR_COUNT; index += 1) {
|
||||
spawnStar(index, Math.pow(Math.random(), APP_CONFIG.starfield.seedProgressExponent))
|
||||
}
|
||||
|
||||
starsGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3))
|
||||
starsGeometry.setAttribute('color', new THREE.BufferAttribute(starColors, 3))
|
||||
starsGeometry.setAttribute('aProgress', new THREE.BufferAttribute(starProgress, 1))
|
||||
starsGeometry.setAttribute('aSizeBase', new THREE.BufferAttribute(starSizes, 1))
|
||||
starsGeometry.setAttribute('aAlphaBase', new THREE.BufferAttribute(starAlpha, 1))
|
||||
starsGeometry.setAttribute('aTail', new THREE.BufferAttribute(starTail, 1))
|
||||
starsGeometry.setAttribute('aDir', new THREE.BufferAttribute(starDirections, 2))
|
||||
|
||||
const stars = new THREE.Points(starsGeometry, starMaterial)
|
||||
travelRoot.add(stars)
|
||||
|
||||
for (let index = 0; index < SMOKE_COUNT; index += 1) {
|
||||
const state = spawnSmoke()
|
||||
smokeStates.push(state)
|
||||
travelRoot.add(state.mesh)
|
||||
}
|
||||
|
||||
const clock = new THREE.Clock()
|
||||
|
||||
const onResize = () => {
|
||||
const width = container.clientWidth || window.innerWidth
|
||||
const height = container.clientHeight || window.innerHeight
|
||||
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, APP_CONFIG.scene.maxPixelRatio))
|
||||
renderer.setSize(width, height)
|
||||
}
|
||||
|
||||
function updateStars(delta: number) {
|
||||
const positionAttribute = starsGeometry.getAttribute('position') as THREE.BufferAttribute
|
||||
const progressAttribute = starsGeometry.getAttribute('aProgress') as THREE.BufferAttribute
|
||||
const tailAttribute = starsGeometry.getAttribute('aTail') as THREE.BufferAttribute
|
||||
const directionAttribute = starsGeometry.getAttribute('aDir') as THREE.BufferAttribute
|
||||
|
||||
for (let index = 0; index < STAR_COUNT; index += 1) {
|
||||
const state = starStates[index]
|
||||
|
||||
if (state.delay > 0) {
|
||||
state.delay = Math.max(0, state.delay - delta)
|
||||
progressAttribute.array[index] = 0
|
||||
continue
|
||||
}
|
||||
|
||||
const progress = clamp((state.z - STAR_FAR_Z) / (STAR_RESET_Z - STAR_FAR_Z), 0, 1)
|
||||
const edgeRatio = clamp(
|
||||
Math.max(Math.abs(state.x) / STAR_BOUNDS_X, Math.abs(state.y) / STAR_BOUNDS_Y),
|
||||
0,
|
||||
1.2,
|
||||
)
|
||||
const nearBoost = 1 + Math.pow(progress, 2.2) * APP_CONFIG.starfield.nearFieldBoost
|
||||
const edgeBoost = 1 + Math.pow(edgeRatio, 3.1) * APP_CONFIG.starfield.edgeBoost
|
||||
const burstBoost = 1 + Math.pow(edgeRatio, 4.2) * APP_CONFIG.starfield.edgeBurst
|
||||
|
||||
state.vx += state.ax * delta * APP_CONFIG.starfield.accelerationXY * nearBoost * edgeBoost
|
||||
state.vy += state.ay * delta * APP_CONFIG.starfield.accelerationXY * nearBoost * edgeBoost
|
||||
state.vz += state.az * delta * APP_CONFIG.starfield.accelerationZ * nearBoost * edgeBoost
|
||||
|
||||
state.x += state.vx * delta * burstBoost
|
||||
state.y += state.vy * delta * burstBoost
|
||||
state.z += state.vz * delta * (1 + (burstBoost - 1) * 0.45)
|
||||
|
||||
const nextProgress = clamp((state.z - STAR_FAR_Z) / (STAR_RESET_Z - STAR_FAR_Z), 0, 1)
|
||||
const nextEdgeRatio = clamp(
|
||||
Math.max(Math.abs(state.x) / STAR_BOUNDS_X, Math.abs(state.y) / STAR_BOUNDS_Y),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
const progressTrail = clamp(
|
||||
(nextProgress - APP_CONFIG.starfield.trailStartProgress) / (1 - APP_CONFIG.starfield.trailStartProgress),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
const edgeTrail = Math.pow(nextEdgeRatio, 2.4)
|
||||
const direction = normalizeDirection(state.vx, state.vy)
|
||||
const tailStrength = clamp(
|
||||
Math.max(progressTrail * 0.75, edgeTrail) * APP_CONFIG.starfield.trailStrength,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
const offset = index * 3
|
||||
const directionOffset = index * 2
|
||||
|
||||
if (
|
||||
state.z > STAR_RESET_Z ||
|
||||
Math.abs(state.x) > STAR_BOUNDS_X ||
|
||||
Math.abs(state.y) > STAR_BOUNDS_Y
|
||||
) {
|
||||
spawnStar(index)
|
||||
continue
|
||||
}
|
||||
|
||||
positionAttribute.array[offset] = state.x
|
||||
positionAttribute.array[offset + 1] = state.y
|
||||
positionAttribute.array[offset + 2] = state.z
|
||||
progressAttribute.array[index] = nextProgress
|
||||
tailAttribute.array[index] = tailStrength
|
||||
directionAttribute.array[directionOffset] = direction.x
|
||||
directionAttribute.array[directionOffset + 1] = direction.y
|
||||
}
|
||||
|
||||
positionAttribute.needsUpdate = true
|
||||
progressAttribute.needsUpdate = true
|
||||
tailAttribute.needsUpdate = true
|
||||
directionAttribute.needsUpdate = true
|
||||
}
|
||||
|
||||
function updateSmoke(delta: number, elapsed: number) {
|
||||
for (const state of smokeStates) {
|
||||
state.age += delta
|
||||
state.x += state.vx * delta
|
||||
state.y += state.vy * delta
|
||||
state.z += state.vz * delta
|
||||
state.rotation += state.spin * delta
|
||||
|
||||
if (
|
||||
state.z > SMOKE_RESET_Z ||
|
||||
Math.abs(state.x) > SMOKE_BOUNDS_X ||
|
||||
Math.abs(state.y) > SMOKE_BOUNDS_Y
|
||||
) {
|
||||
const resetState = spawnSmoke(state.mesh)
|
||||
Object.assign(state, resetState)
|
||||
}
|
||||
|
||||
const progress = clamp((state.z - SMOKE_FAR_Z) / (SMOKE_RESET_Z - SMOKE_FAR_Z), 0, 1)
|
||||
const depthFadeIn = smoothstep(0.02, 0.28, progress)
|
||||
const ageFadeIn = smoothstep(0, 0.55, state.age)
|
||||
const fadeIn = depthFadeIn * ageFadeIn
|
||||
const fadeOut = 1 - smoothstep(0.68, 1, progress)
|
||||
const pulse = 1 + Math.sin(elapsed * state.wobbleSpeed + state.rotation) * 0.12 * state.wobble
|
||||
const scale = lerp(0.56, 1.9, progress) * pulse
|
||||
|
||||
state.mesh.position.set(state.x, state.y, state.z)
|
||||
state.mesh.lookAt(camera.position)
|
||||
state.mesh.rotateZ(state.rotation)
|
||||
state.mesh.scale.set(state.baseScaleX * scale, state.baseScaleY * scale, 1)
|
||||
state.mesh.material.opacity = state.opacity * fadeIn * fadeOut
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
|
||||
const deltaSeconds = Math.min(clock.getDelta(), 0.033)
|
||||
const elapsed = clock.elapsedTime
|
||||
|
||||
pointerCurrent.x = lerp(pointerCurrent.x, pointerTarget.x, APP_CONFIG.interaction.pointerSmoothing)
|
||||
pointerCurrent.y = lerp(pointerCurrent.y, pointerTarget.y, APP_CONFIG.interaction.pointerSmoothing)
|
||||
|
||||
const speedGoal = started ? targetSpeed : 0
|
||||
currentSpeed = lerp(currentSpeed, speedGoal, 0.06)
|
||||
|
||||
const motionDelta = deltaSeconds * currentSpeed
|
||||
updateStars(motionDelta)
|
||||
updateSmoke(motionDelta * 0.62, elapsed)
|
||||
|
||||
travelRoot.rotation.z = pointerCurrent.x * 0.03
|
||||
travelRoot.position.x = -pointerCurrent.x * 0.8
|
||||
travelRoot.position.y = -pointerCurrent.y * 0.45
|
||||
|
||||
camera.position.x = pointerCurrent.x * APP_CONFIG.interaction.maxOffset * 2.8
|
||||
camera.position.y = pointerCurrent.y * APP_CONFIG.interaction.maxOffset * 1.8
|
||||
camera.lookAt(pointerCurrent.x * 1.5, pointerCurrent.y * 0.8, -30)
|
||||
|
||||
renderer.render(scene, camera)
|
||||
animationFrameId = window.requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', onResize)
|
||||
render()
|
||||
|
||||
return {
|
||||
setStarted(nextStarted) {
|
||||
started = nextStarted
|
||||
},
|
||||
setSpeed(nextSpeed) {
|
||||
targetSpeed = clamp(nextSpeed, APP_CONFIG.animation.speedMin, APP_CONFIG.animation.speedMax)
|
||||
},
|
||||
setPointerTarget(pointer) {
|
||||
pointerTarget.x = clamp(pointer.x, -1, 1)
|
||||
pointerTarget.y = clamp(pointer.y, -1, 1)
|
||||
},
|
||||
dispose() {
|
||||
if (disposed) {
|
||||
return
|
||||
}
|
||||
|
||||
disposed = true
|
||||
window.cancelAnimationFrame(animationFrameId)
|
||||
window.removeEventListener('resize', onResize)
|
||||
|
||||
for (const smokeState of smokeStates) {
|
||||
smokeState.mesh.material.dispose()
|
||||
travelRoot.remove(smokeState.mesh)
|
||||
}
|
||||
|
||||
starsGeometry.dispose()
|
||||
starMaterial.dispose()
|
||||
smokeGeometry.dispose()
|
||||
smokeTexture.dispose()
|
||||
renderer.dispose()
|
||||
scene.clear()
|
||||
|
||||
if (renderer.domElement.parentElement === container) {
|
||||
container.removeChild(renderer.domElement)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export type { PointerTarget, TravelSceneController }
|
||||
Reference in New Issue
Block a user