From a7cda7ab20da55bce48bbdda150d46949c09d2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Goszczy=C5=84ski?= Date: Fri, 14 Mar 2025 11:56:01 +0100 Subject: [PATCH] render dynamic texture --- .../LaunchWeek/14/TicketCanvas2.tsx | 13 +- apps/www/components/LaunchWeek/14/helpers.ts | 4 +- .../LaunchWeek/14/scenes/TicketScene.ts | 326 +++++++++++++++++- apps/www/components/LaunchWeek/14/styles.css | 1 - 4 files changed, 323 insertions(+), 21 deletions(-) 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; }