diff --git a/apps/www/components/LaunchWeek/14/TicketCanvas2.tsx b/apps/www/components/LaunchWeek/14/TicketCanvas2.tsx
index bff0f6c70a1..56045f71918 100644
--- a/apps/www/components/LaunchWeek/14/TicketCanvas2.tsx
+++ b/apps/www/components/LaunchWeek/14/TicketCanvas2.tsx
@@ -17,6 +17,7 @@ interface TicketCanvasProps {
className?: string
onUpgrade?: () => void
}
+
const TicketCanvas = ({
secret,
platinum,
@@ -33,11 +34,19 @@ const TicketCanvas = ({
const sceneRenderer = new SceneRenderer(container)
+
sceneRef.current = new TicketScene({
defaultSecret: secret,
defaultPlatinum: platinum,
user,
startDate,
+ onSeatChartButtonClicked: () => {
+ sceneRef.current?.showBackSide()
+ sceneRef.current?.upgradeToSecret()
+ },
+ onGoBackButtonClicked: () => {
+ sceneRef.current?.showFrontSide()
+ },
})
sceneRenderer.activateScene(sceneRef.current)
@@ -60,8 +69,8 @@ const TicketCanvas = ({
{
- sceneRef.current?.showSecondFace()
+ onClick={(e) => {
+ sceneRef.current?.click(e.nativeEvent)
}}
/>
diff --git a/apps/www/components/LaunchWeek/14/helpers.ts b/apps/www/components/LaunchWeek/14/helpers.ts
index 0a99498dfa1..27835dbc1a9 100644
--- a/apps/www/components/LaunchWeek/14/helpers.ts
+++ b/apps/www/components/LaunchWeek/14/helpers.ts
@@ -529,6 +529,6 @@ function createTextureForObject(object: THREE.Object3D, text: string) {
return textMesh
}
-export function colorObjToRgb(color: {rgb:number, alpha:number}) {
- return `rgba(${(color.rgb >> 16) & 255} / ${color.alpha})`
+export function colorObjToRgb(color: { rgb: number; alpha: number }) {
+ return `rgb(${(color.rgb >> 16) & 255} ${(color.rgb >> 8) & 255} ${(color.rgb >> 0) & 255} / ${color.alpha})`
}
diff --git a/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts b/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts
index 6c339dda399..e118b888056 100644
--- a/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts
+++ b/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts
@@ -27,7 +27,9 @@ interface TicketSceneOptions {
name?: string
ticketNumber?: number
}
- onUpgradeToSecret?: () => {}
+ onSeatChartButtonClicked?: () => void
+ onWebsiteButtonClicked?: () => void
+ onGoBackButtonClicked?: () => void
}
interface MousePositionState {
@@ -46,7 +48,7 @@ class TicketScene implements BaseScene {
textureImages = {
basic: {
- back: '/images/launchweek/14/back-basic-ticket-textrue.png',
+ back: '/images/launchweek/14/back-basic-ticket-texture.png',
front: '/images/launchweek/14/front-basic-ticket-texture.png',
bgColor: { rgb: 0x202020, alpha: 1 },
@@ -57,7 +59,7 @@ class TicketScene implements BaseScene {
transparentBg: { rgb: 0x000000, alpha: 0 },
},
secret: {
- back: '/images/launchweek/14/back-secret-ticket-textrue.png',
+ back: '/images/launchweek/14/back-secret-ticket-texture.png',
front: '/images/launchweek/14/front-secret-ticket-texture.png',
bgColor: { rgb: 0x050505, alpha: 1 },
@@ -68,7 +70,7 @@ class TicketScene implements BaseScene {
transparentBg: { rgb: 0x2cf494, alpha: 0.4 },
},
platinum: {
- back: '/images/launchweek/14/back-platinum-ticket-textrue.png',
+ back: '/images/launchweek/14/back-platinum-ticket-texture.png',
front: '/images/launchweek/14/front-platinum-ticket-texture.png',
bgColor: { rgb: 0x050505, alpha: 1 },
@@ -83,12 +85,12 @@ class TicketScene implements BaseScene {
typography = {
main: {
family: 'Departure Mono',
- relativeSize: 73 / 1400,
+ relativeSize: 100 / 1400,
},
ticketNumber: {
family: 'Nippo-Variable',
weight: 400,
- relativeSize: 85.26 / 1400,
+ relativeSize: 125 / 1400,
},
}
@@ -115,7 +117,24 @@ class TicketScene implements BaseScene {
},
}
- fontUrl = ''
+ fonts: ConstructorParameters[] = [
+ [
+ 'Departure Mono',
+ 'url("/fonts/launchweek/14/DepartureMono-Regular.woff2") format("woff2")',
+ {
+ weight: '400',
+ style: 'normal',
+ },
+ ],
+ [
+ 'Nippo-Variable',
+ 'url("/fonts/launchweek/14/Nippo-Variable.woff2") format("woff2")',
+ {
+ weight: '400 700',
+ style: 'normal',
+ },
+ ],
+ ]
state: TicketSceneState
@@ -123,6 +142,7 @@ class TicketScene implements BaseScene {
mousePosition: undefined as MousePositionState | undefined,
containerBBox: undefined as DOMRect | undefined,
naturalPosition: new Vector3(0, 0, 0),
+ fontsLoaded: false,
}
private _sceneConfig = {
@@ -149,6 +169,15 @@ class TicketScene implements BaseScene {
'TicketBackWebsiteButton',
] as const
private static TEXTURE_PIXEL_DENSITY_FACTOR = 400
+ private texturePlaneMapping: { [key in AvailableTextures]?: 'back' | 'front' | 'edge' } = {
+ TicketFront: 'front',
+ TicketBack: 'back',
+ TicketEdge: 'edge',
+ TicketFrontWebsiteButton: 'front',
+ TicketFrontSeatChartButton: 'front',
+ TicketBackGoBackButton: 'back',
+ TicketBackWebsiteButton: 'back',
+ }
private _textureCanvases: {
[key in AvailableTextures]?: { canvas: HTMLCanvasElement; context: CanvasRenderingContext2D }
@@ -164,9 +193,9 @@ class TicketScene implements BaseScene {
startDate: options.startDate,
ticketNumber: options.user.ticketNumber || 0,
texts: {
- username: options.user.name || '',
- species: '',
- earth: '',
+ username: "Goszczu" ?? options.user.name ?? '',
+ species: 'Modern Human',
+ earth: 'Earth',
// Start assigning seats from A001
seatCode: (466561 + (options.user.ticketNumber || 0)).toString(36),
},
@@ -177,13 +206,15 @@ class TicketScene implements BaseScene {
this._sceneRenderer = context
this._internalState.containerBBox = context.container.getBoundingClientRect()
+ // Load fonts before loading the model
+ await this._loadFonts()
+
const gltf = await loadGLTFModel(this.sceneUrl)
this._ticket = gltf.scene as unknown as Scene
this._setCamera(context.camera)
this._modelRenderPass = new RenderPass(this._ticket, context.camera)
- context.composer.addPass(this._modelRenderPass)
// Set renderer clear color to make background visible
context.renderer.setClearColor(0x000000, 0)
@@ -205,6 +236,9 @@ class TicketScene implements BaseScene {
this._setupNamedObjects()
this._setupTextureCanvases()
+ await this._loadTextures()
+
+ context.composer.addPass(this._modelRenderPass)
}
update(context: SceneRenderer, dt?: number): void {
@@ -228,13 +262,57 @@ class TicketScene implements BaseScene {
return
}
- showSecondFace() {
- this.state.frontside = !this.state.frontside
- this._setSecretTextures()
+ showFrontSide() {
+ this.state.frontside = true
}
+ showBackSide() {
+ this.state.frontside = false
+ }
+
+ upgradeToSecret() {
+ this._setSecretTextures()
+ this.state.secret = true
+ }
+
+ // In your click method
click(e: MouseEvent) {
this._updateMousePosition(e)
+
+ if (!this._internalState.mousePosition?.isWithinContainer || !this._sceneRenderer) return
+
+ // Set up raycaster
+ this.raycaster.setFromCamera(
+ new THREE.Vector2(
+ this._internalState.mousePosition.containerX,
+ this._internalState.mousePosition.containerY
+ ),
+ this._sceneRenderer.camera
+ )
+
+ // Get all meshes to check for intersection
+ const meshes = Object.values(this._namedMeshes).filter(Boolean) as THREE.Mesh[]
+
+ // Check for intersections
+ const intersects = this.raycaster.intersectObjects(meshes)
+
+ if (intersects.length > 0) {
+ const clickedMesh = intersects[0].object as THREE.Mesh
+
+ // Handle click based on which mesh was clicked
+ if (clickedMesh === this._namedMeshes.TicketFrontWebsiteButton) {
+ this.options.onWebsiteButtonClicked?.()
+ // Add your action here
+ } else if (clickedMesh === this._namedMeshes.TicketFrontSeatChartButton) {
+ this.options.onSeatChartButtonClicked?.()
+ // Add your action here
+ } else if (clickedMesh === this._namedMeshes.TicketBackGoBackButton) {
+ this.options.onGoBackButtonClicked?.()
+ } else if (clickedMesh === this._namedMeshes.TicketBackWebsiteButton) {
+ this.options.onWebsiteButtonClicked?.()
+ // Add your action here
+ }
+ }
}
private _updateTicketToFollowMouse(scene: THREE.Scene, dt?: number) {
@@ -332,7 +410,21 @@ class TicketScene implements BaseScene {
private _setupTextureCanvases() {
for (const [name, mesh] of Object.entries(this._namedMeshes)) {
- const localBox = new THREE.Box3().setFromObject(mesh)
+ const planeType = this.texturePlaneMapping[name as AvailableTextures]
+ let referenceMesh: THREE.Mesh | undefined
+ if (planeType === 'front') {
+ referenceMesh = this._namedMeshes.TicketFront
+ } else if (planeType === 'back') {
+ referenceMesh = this._namedMeshes.TicketBack
+ } else if (planeType === 'edge') {
+ referenceMesh = this._namedMeshes.TicketEdge
+ }
+
+ if (!referenceMesh) {
+ throw new Error(`Could not find reference mesh for texture ${name}`)
+ }
+
+ const localBox = new THREE.Box3().setFromObject(referenceMesh)
const localSize = localBox.getSize(new THREE.Vector3())
// Get world scale
@@ -392,6 +484,10 @@ class TicketScene implements BaseScene {
}
private _setSecretTextures() {
+ if (this.state.secret) {
+ return
+ }
+
if (!this._textureCanvases.TicketFront) {
throw new Error('TicketFront texture canvas is not set')
}
@@ -404,6 +500,7 @@ class TicketScene implements BaseScene {
// context.fillStyle = colorObjToRgb(this.textureImages.secret.textNeonColor)
context.fillStyle = 'white'
const fontSize = this.typography.main.relativeSize * canvas.height
+ console.log({ fontSize })
context.font = `400 ${fontSize}px ${this.typography.main.family}`
context.textAlign = 'left'
context.textBaseline = 'top'
@@ -417,7 +514,8 @@ class TicketScene implements BaseScene {
!Array.isArray(this._namedMeshes.TicketFront.material) &&
this._namedMeshes.TicketFront.material instanceof THREE.MeshStandardMaterial
) {
- const texture = new THREE.CanvasTexture(canvas, this._namedMeshes.TicketFront.material.map?.mapping)
+ const texture = new THREE.CanvasTexture(canvas)
+ texture.flipY = false
texture.needsUpdate = true
// Get the existing material
@@ -485,6 +583,202 @@ class TicketScene implements BaseScene {
}
}
}
+
+ private _loadTextures() {
+ // Load textures for each named mesh
+ // Create a texture loader
+ const textureLoader = new THREE.TextureLoader()
+ const loadingPromises: Promise[] = []
+
+ // Determine which texture set to use based on ticket type
+ const textureSet = this.state.secret
+ ? this.textureImages.secret
+ : this.state.platinum
+ ? this.textureImages.platinum
+ : this.textureImages.basic
+
+ // Map of which image to use for each mesh
+ const textureImageMap: Partial> = {
+ TicketFront: textureSet.front,
+ TicketBack: textureSet.back,
+ TicketFrontWebsiteButton: textureSet.front,
+ TicketBackGoBackButton: textureSet.back,
+ TicketFrontSeatChartButton: textureSet.front,
+ TicketBackWebsiteButton: textureSet.back,
+ // Add mappings for buttons if they have separate textures
+ // Or they can reuse the front/back textures with different UV coordinates
+ }
+
+ // Preload all textures and prepare canvases
+ for (const [name, mesh] of Object.entries(this._namedMeshes)) {
+ if (!mesh || !(mesh instanceof THREE.Mesh)) continue
+ if (!mesh.material || !(mesh.material instanceof THREE.MeshStandardMaterial)) continue
+
+ const textureKey = name as AvailableTextures
+ const textureCanvas = this._textureCanvases[textureKey]
+
+ if (!textureCanvas) continue
+ const { canvas, context } = textureCanvas
+
+ // If we have an image for this texture
+ const imageUrl = textureImageMap[textureKey]
+ if (imageUrl) {
+ // Create a loading promise for this texture
+ const loadPromise = new Promise((resolve) => {
+ textureLoader.load(
+ imageUrl,
+ (loadedTexture) => {
+ // Create an image from the loaded texture
+ const image = loadedTexture.image
+
+ // Draw the loaded image onto the canvas as the base
+ context.clearRect(0, 0, canvas.width, canvas.height)
+ context.drawImage(image, 0, 0, canvas.width, canvas.height)
+
+ // Now we can draw additional content on top
+ this._drawCustomContentOnTexture(textureKey, context, canvas)
+
+ // Create a texture from the combined canvas
+ const combinedTexture = new THREE.CanvasTexture(canvas)
+ combinedTexture.flipY = false
+ combinedTexture.needsUpdate = true
+
+ // Apply the texture to the mesh
+ if (!(mesh.material instanceof THREE.MeshStandardMaterial)) {
+ throw new Error(`Material for mesh ${name} is not a MeshStandardMaterial`)
+ }
+
+ mesh.material.map = combinedTexture
+ mesh.material.needsUpdate = true
+
+ resolve()
+ },
+ undefined, // onProgress callback
+ (error) => {
+ console.error(`Error loading texture ${imageUrl}:`, error)
+ resolve() // Resolve anyway to not block other textures
+ }
+ )
+ })
+
+ loadingPromises.push(loadPromise)
+ } else {
+ // For meshes without base images, just draw custom content
+ context.clearRect(0, 0, canvas.width, canvas.height)
+ this._drawCustomContentOnTexture(textureKey, context, canvas)
+
+ const texture = new THREE.CanvasTexture(canvas)
+ texture.flipY = false
+ texture.needsUpdate = true
+
+ mesh.material.map = texture
+ mesh.material.needsUpdate = true
+ }
+ }
+
+ // Return a promise that resolves when all textures are loaded
+ return Promise.all(loadingPromises)
+ }
+
+ // Method to draw custom content on each texture type
+ private _drawCustomContentOnTexture(
+ textureKey: AvailableTextures,
+ context: CanvasRenderingContext2D,
+ canvas: HTMLCanvasElement
+ ) {
+ // Use fallback fonts if custom fonts failed to load
+ const mainFontFamily = this._internalState.fontsLoaded
+ ? this.typography.main.family
+ : 'monospace'
+
+ const ticketNumberFontFamily = this._internalState.fontsLoaded
+ ? this.typography.ticketNumber.family
+ : 'monospace'
+
+ console.log({ mainFontFamily, ticketNumberFontFamily })
+
+ // Get the appropriate color scheme based on ticket type
+ const colors = this.state.secret
+ ? this.textureImages.secret
+ : this.state.platinum
+ ? this.textureImages.platinum
+ : this.textureImages.basic
+
+ switch (textureKey) {
+ case 'TicketFront':
+ // Draw username
+ context.fillStyle = colorObjToRgb(colors.textColor)
+ const fontSize = this.typography.main.relativeSize * canvas.height
+ context.font = `400 ${fontSize}px ${mainFontFamily}`
+ context.textAlign = 'left'
+ context.textBaseline = 'top'
+ context.fillText(
+ this.state.texts.username,
+ this.texts.user.x * canvas.width,
+ this.texts.user.y * canvas.height
+ )
+
+ // Draw species
+ context.fillText(
+ this.state.texts.species,
+ this.texts.species.x * canvas.width,
+ this.texts.species.y * canvas.height
+ )
+
+ // Draw planet
+ context.fillText(
+ this.state.texts.earth,
+ this.texts.planet.x * canvas.width,
+ this.texts.planet.y * canvas.height
+ )
+
+ // Draw ticket number with different font
+ const ticketNumberFontSize = this.typography.ticketNumber.relativeSize * canvas.height
+ context.font = `${this.typography.ticketNumber.weight} ${ticketNumberFontSize}px ${ticketNumberFontFamily}`
+ context.fillText(
+ this.state.texts.seatCode.toUpperCase(),
+ this.texts.ticketNumber.x * canvas.width,
+ this.texts.ticketNumber.y * canvas.height
+ )
+ break
+
+ case 'TicketBack':
+ // Add any custom content for the back of the ticket
+ break
+
+ // Handle other texture types as needed
+ case 'TicketFrontWebsiteButton':
+ case 'TicketFrontSeatChartButton':
+ case 'TicketBackGoBackButton':
+ case 'TicketBackWebsiteButton':
+ // These might not need custom content if the base image is sufficient
+ break
+ }
+ }
+
+ private async _loadFonts(): Promise {
+ try {
+ // Define the fonts to load
+ const fontFaces = this.fonts.map(f => new FontFace(...f))
+ // Load all fonts
+ await Promise.all(
+ fontFaces.map(async (font) => {
+ const loadedFont = await font.load()
+ // Add the loaded font to the document
+ document.fonts.add(loadedFont)
+ return loadedFont
+ })
+ )
+
+ // All fonts loaded successfully
+ this._internalState.fontsLoaded = true
+ } catch (error) {
+ // Handle font loading errors
+ console.error('Failed to load fonts:', error)
+ // Still continue to not block rendering, but with a warning
+ this._internalState.fontsLoaded = false
+ }
+ }
}
export default TicketScene
diff --git a/apps/www/components/LaunchWeek/14/styles.css b/apps/www/components/LaunchWeek/14/styles.css
index aa0b6cf6240..e5bb052a095 100644
--- a/apps/www/components/LaunchWeek/14/styles.css
+++ b/apps/www/components/LaunchWeek/14/styles.css
@@ -5,7 +5,6 @@
url('/fonts/launchweek/14/Nippo-Variable.woff') format('woff'),
url('/fonts/launchweek/14/Nippo-Variable.ttf') format('truetype');
font-weight: 200 700;
- font-display: swap;
font-style: normal;
}