Files
supabase/apps/www/components/LaunchWeek/14/utils/SceneRenderer.ts
T
94e919d417 lw14 ticketing #34444
* mamba init

* shader-updates

* feat: add better bloom

* cleanup

* Fix GLTF model loading to respect original material properties

Signed-off-by: Piotr Goszczyński <pg@tonik.com>

* Fix GLTF model loading to respect original material properties

Signed-off-by: Piotr Goszczyński <pg@tonik.com>

* stash changes

* Update font to Arial in mamba ticket component

Signed-off-by: Piotr Goszczyński <pg@tonik.com>

* Fix wireframe size in debug mode

Signed-off-by: Piotr Goszczyński <pg@tonik.com>

* Fix wireframe size in debug mode by attaching to child mesh instead of model

Signed-off-by: Piotr Goszczyński <pg@tonik.com>

* fix scaling

* fix

* feat: load png with color material

* fix: spline material

* add toggling of effects

* stash

* feat: ticket model

* load better model

* strcture

* fix rotation speed

* rotation

* fix rotation

* new model

* prepare final designs

* add color palette

* feat: add fonts and rendering

* render dynamic texture

* load all textures

* fix: color sace

* feat: secret model

* improve perf

* postprocessing

* fix: output pass

* improve shaders

* increase glitches

* cleanup

* update colors

* remove unused files

* add header

* feat: ui

* fix: raycaster

* feat: add button toggling

* Improve ui

* fix corners positoins

* add tunnel effect

* fix: dates

* add dynamic rocket position rendering

* add different logs

* feat: add migrations and connect to real data

* chore: state cleanup to use reducer

Migrate logic to centralized reducer for ease of debugging

* feat: add hud layout

* add grain effect to hud

* limit glitching, move page to main url

* add vignette effect

* fix: desktop layout

* remove vertical space

* feat: save secret state on ticket upgrade

* feat: make platinum version take priority over secret

* add mobile header

* remove unnecessary allocations

* fix: resize hud

* add page scaling for mobile layouts

* add ticket scaling based on resolution

* adjust mobile styles

* fix: resize texture distortion

* add gauges mobile breaking points

* feat: mobile claim layout

* mobile styles

* Show ticket referer

* rename files

* update copy links

* add partymode part 1

* feat: og images, lw14

* add presence tracking

* add live updates of guages

* Update meetups label

* update label

* username ticket lw14

* add proper subscription on first render

* remove green edge

* feat: remove species & planet from ticket scene

* change urls

* feat/mamba-assets: update assets

* feat: fix shader grain

* add color to shader

* remove vertical space

* fix: og image generation

* fix referal and removing of access token

* make urls nicer

* feat/mamba-assets: dynamic line break + dynamic background

* small adjustments

* add trimming to ogimages

* minor: missing font file

* fix og image line wrapping

* remove dot from meetups

* improve ticket quality

* upss, forgot to commit one file

* feat add dynamic og image linking

* update static site url

* fix low mobile resolution

* add banners

* simplify banner loading

* add missing fonts

* fix name breaking to prioritize spaces

* update quality based on devicePixelRatio

* fix button positioning

* revert: feature flags disabling

* update start date

* limit shader effects and disable axis, potential fix to chrome freeze

* feat: init mamba 01

* create narrow ticket scene

* narrow scene for ticket and fix follow mouse

* fix dates

* alternative version

* updated ticket page

* remove migrations

* remove files

* prettier

* prettier

* Fix TS and prettier issues

* Fix TS issues

* Test

* Remove console log

* fix updating ticket on share

* chore: lw cleanup

* fix ticket layout

* fix chrome crashes

* lower crt intensity

* tunnel improvements

* fix tunnel effect

* chore: z-index and prettier fix

* chore: padding

* fix double tunnel

* chore: font on banners

---------

Signed-off-by: Piotr Goszczyński <pg@tonik.com>
Co-authored-by: dztonik <dz@tonik.com>
Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
2025-03-27 15:55:47 +00:00

296 lines
8.8 KiB
TypeScript

import {
ColorManagement,
HalfFloatType,
PerspectiveCamera,
Scene,
SRGBColorSpace,
WebGLRenderer,
WebGLRenderTarget,
} from 'three'
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
export interface MousePositionState {
clientX: number
clientY: number
isWithinContainer: boolean
containerX: number
containerY: number
mouseIntensity: number
}
export interface BaseScene {
getId(): string
/**
* Initialize the scene with the given context
*/
setup(context: SceneRenderer): Promise<Scene | void>
/**
* Update the scene for each animation frame
*/
update(context: SceneRenderer, dt?: number): void
/**
* Clean up resources when the scene is destroyed
*/
cleanup(): void
resize(ev: UIEvent): void
/**
* Handle device pixel ratio changes
*/
devicePixelRatioChanged?(newPixelRatio: number, oldPixelRatio: number): void
}
class SceneRenderer {
renderer: WebGLRenderer
composer: EffectComposer
camera: PerspectiveCamera
mainThreeJsScene: Scene | null = null
activeScenes: BaseScene[] = []
cachedContainerBBox: DOMRect
mousePositionState: MousePositionState
mouseIntensityDecay = 0.98
mouseIntensityGainRate = 0.003
currentPixelRatio: number
private _resizeHandler: ((ev: UIEvent) => void) | null = null
private _mouseMoveHandler: ((ev: MouseEvent) => void) | null = null
private _mediaQueryListHandler: ((ev: MediaQueryListEvent) => void) | null = null
private _isDisposed = false
private _isInitialized = false
constructor(
public container: HTMLElement,
private waitFor?: { init: Promise<void>; renderer: SceneRenderer }[],
private uuid?: string
) {
this.renderer = new WebGLRenderer({ antialias: true, alpha: true })
this.composer = new EffectComposer(this.renderer)
this.camera = new PerspectiveCamera(
75,
this.container.clientWidth / this.container.clientHeight,
0.1,
1000
)
this.mousePositionState = {
clientX: 0,
clientY: 0,
isWithinContainer: false,
containerX: 0,
containerY: 0,
mouseIntensity: 0,
}
this.cachedContainerBBox = this.container.getBoundingClientRect()
this.currentPixelRatio = window.devicePixelRatio
}
async init(sceneInitializer: () => Promise<void>) {
// console.log('SCENE RENDERER: Init call', this.waitFor?.length, this.uuid)
await Promise.allSettled(
this.waitFor?.filter((t) => t.renderer !== this).map((t) => t.init) || []
)
if (this._isDisposed) {
// console.log('SCENE RENDERER: Already disposed before sceneInitializer', this.uuid)
return
}
// console.log('SCENE RENDERER: Waited for all pending inits', this.waitFor?.length, this.uuid)
await sceneInitializer()
if (this._isDisposed) {
// console.log('SCENE RENDERER: Already disposed after sceneInitializer', this.uuid)
return
}
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.renderer.setClearColor(0x000000, 0) // Set transparent background
this.renderer.autoClear = false // Prevent automatic clearing
this.renderer.outputColorSpace = SRGBColorSpace
ColorManagement.enabled = true
// Initialize composer with correct size and pixel ratio
this.composer.setSize(this.container.clientWidth, this.container.clientHeight)
this.composer.setPixelRatio(window.devicePixelRatio)
this.container.appendChild(this.renderer.domElement)
this._resizeHandler = this._resize.bind(this)
window.addEventListener('resize', this._resizeHandler)
this._mouseMoveHandler = this._updateMousePosition.bind(this)
window.addEventListener('mousemove', this._mouseMoveHandler)
// Disable it since it brakes Chrome browser
// // Add device pixel ratio change listener
// this._mediaQueryListHandler = this._handleDevicePixelRatioChange.bind(this)
// window
// .matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
// .addEventListener('change', this._mediaQueryListHandler)
}
async activateScene(scene: BaseScene, main?: boolean) {
if (this._isDisposed) {
// console.log('SCENE RENDERER: Already disposed before activateScene', this.uuid)
return
}
const threeScene = await scene.setup(this)
if (this._isDisposed) {
// console.log('SCENE RENDERER: Already disposed after activateScene', this.uuid)
return
}
if (main && threeScene) {
this.mainThreeJsScene = threeScene
}
const newId = scene.getId()
const existingSceneId = this.activeScenes.findIndex((s) => s.getId() === newId)
if (existingSceneId > -1) {
const sceneToRemove = this.activeScenes[existingSceneId]
this.activeScenes[existingSceneId] = scene
sceneToRemove.cleanup()
} else {
this.activeScenes.push(scene)
}
}
animate(time?: number) {
// Clear the renderer before rendering
this.renderer.clear()
this._decayMouseIntensity()
for (const activeScene of this.activeScenes) {
activeScene.update(this, time)
}
this.composer.render(time)
}
cleanup() {
// console.log('SCENE RENDERER: Cleanup', this.uuid)
this._isDisposed = true
this.composer.dispose()
this.renderer.dispose()
try {
this.container.removeChild(this.renderer.domElement)
} catch (e) {
console.warn(e)
}
if (this._resizeHandler) {
window.removeEventListener('resize', this._resizeHandler)
this._resizeHandler = null
}
if (this._mouseMoveHandler) {
window.removeEventListener('mousemove', this._mouseMoveHandler)
this._mouseMoveHandler = null
}
if (this._mediaQueryListHandler) {
window
.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
.removeEventListener('change', this._mediaQueryListHandler)
this._mediaQueryListHandler = null
}
if (this.waitFor) {
// Mutate array in place instead of replacing reference
this.waitFor.splice(
0,
this.waitFor.length,
...this.waitFor.filter((t) => t.renderer === this)
)
}
}
private _resize(ev: UIEvent) {
this.camera.aspect = this.container.clientWidth / this.container.clientHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight)
this.composer.setSize(this.container.clientWidth, this.container.clientHeight)
this.cachedContainerBBox = this.container.getBoundingClientRect()
for (const activeScene of this.activeScenes) {
activeScene.resize(ev)
}
}
private _handleDevicePixelRatioChange(ev: MediaQueryListEvent) {
const oldPixelRatio = this.currentPixelRatio
const newPixelRatio = window.devicePixelRatio
// Update renderer and composer pixel ratio
this.renderer.setPixelRatio(newPixelRatio)
this.composer.setPixelRatio(newPixelRatio)
// Store the new pixel ratio
this.currentPixelRatio = newPixelRatio
// Notify all active scenes about the pixel ratio change
for (const activeScene of this.activeScenes) {
if (typeof activeScene.devicePixelRatioChanged === 'function') {
activeScene.devicePixelRatioChanged(newPixelRatio, oldPixelRatio)
}
}
// console.log(
// `SCENE RENDERER: Device pixel ratio changed from ${oldPixelRatio} to ${newPixelRatio}`
// )
}
private _decayMouseIntensity() {
if (this.mousePositionState.mouseIntensity > 0) {
this.mousePositionState.mouseIntensity *= this.mouseIntensityDecay
if (this.mousePositionState.mouseIntensity < 0.0000001) {
this.mousePositionState.mouseIntensity = 0
}
}
}
private _updateMousePosition(ev: MouseEvent) {
this.cachedContainerBBox = this.container.getBoundingClientRect()
const dx = ev.clientX - this.mousePositionState.clientX
const dy = ev.clientY - this.mousePositionState.clientY
const distance = Math.sqrt(dx * dx + dy * dy)
// Update intensity based on movement (clamped between 0 and 1)
this.mousePositionState.mouseIntensity = Math.min(
1.0,
Math.max(0, this.mousePositionState.mouseIntensity + distance * this.mouseIntensityGainRate)
)
// Update last position
this.mousePositionState.clientX = ev.clientX
this.mousePositionState.clientY = ev.clientY
const rect = this.cachedContainerBBox
if (!rect) {
return
}
const isWithinContainer =
ev.clientX >= rect.left &&
ev.clientX <= rect.right &&
ev.clientY >= rect.top &&
ev.clientY <= rect.bottom
this.mousePositionState.isWithinContainer = isWithinContainer
this.mousePositionState.containerX = ((ev.clientX - rect.left) / rect.width) * 2 - 1
this.mousePositionState.containerY = -((ev.clientY - rect.top) / rect.height) * 2 + 1
}
}
export default SceneRenderer