Last active
April 3, 2025 14:34
-
-
Save benallfree/02e06095b29adfd8ff13c15d3be0961a to your computer and use it in GitHub Desktop.
Revisions
-
benallfree revised this gist
Apr 3, 2025 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,8 +1,10 @@ /** * Vibeverse * ----------------------- * The Vibeverse (x.com/hashtag/vibeverse)is an interconnected network of 3D web games with portals to and from each game. * You can even bring your custom player avatar along with you. * * Credit to @levelsio for the original implementation of this portal system. * ----------------------- * Integration guide for connecting your experience to the Vibeverse metaverse. * -
benallfree revised this gist
Apr 3, 2025 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,7 @@ /** * Vibeverse * ----------------------- * The Vibeverse (https://x.com/hashtag/vibejam) is an interconnected network of 3D web games with portals to and from each game. * You can even bring your custom player avatar along with you. * ----------------------- * Integration guide for connecting your experience to the Vibeverse metaverse. -
benallfree renamed this gist
Apr 3, 2025 . 1 changed file with 320 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,10 +1,13 @@ /** * Vibeverse * ----------------------- * The Vibeverse is an interconnected network of 3D web games with portals to and from each game. * You can even bring your custom player avatar along with you. * ----------------------- * Integration guide for connecting your experience to the Vibeverse metaverse. * * Quick Start: * 1. Import and initialize Vibeverse with your Three.js scene, camera and socket * 2. Call createPortals() to add portal meshes to your scene * * Portal Behavior: @@ -18,6 +21,13 @@ * - username: Player identifier passed between experiences * - color: Avatar color preference * * Warp Effect: * The portal system includes a configurable warp effect when transitioning between portals. * You can: * - Disable it by passing null as warpConfig in the constructor * - Customize it by passing your own warp configuration * - Use default settings by not specifying warpConfig * * Splash Page Bypass: * If your experience has a splash/loading page, you can check if the user is coming * from Vibeverse by using the isVibeverse() helper: @@ -51,11 +61,19 @@ * */ /** * Helper to check if user is coming from Vibeverse * @returns {boolean} True if user is coming from Vibeverse portal */ export function isVibeverse() { return !!refUrl() } /** * Gets the referring URL from URL parameters * @returns {string|null} The referring URL with https:// prefix, or null if not present * @private */ const refUrl = () => { const params = new URLSearchParams(window.location.search) const refUrl = params.get('ref') @@ -71,8 +89,62 @@ const refUrl = () => { import * as THREE from 'three' /** * Default configuration for the warp effect * @typedef {Object} WarpConfig * @property {number} lineCount - Number of lines in the tunnel * @property {number} pointsPerLine - Points making up each line * @property {number} tunnelRadius - Initial radius of the tunnel * @property {number} tunnelExpansion - How much the tunnel expands per point * @property {number} minSpeed - Minimum base speed * @property {number} speedVariation - Random variation in speed * @property {number} oscillationSpeed - Speed of the sine wave oscillation * @property {number} minSpeedFactor - Minimum speed factor during oscillation * @property {number} lineLength - Length between points in a line * @property {number} resetDistance - Distance at which lines reset * @property {number} resetOffset - How far back lines reset to * @property {number} cameraSpeed - Speed at which camera moves forward * @property {number} cameraAcceleration - How quickly camera speeds up */ export const DEFAULT_WARP_CONFIG = { lineCount: 1000, pointsPerLine: 8, tunnelRadius: 2, tunnelExpansion: 0.5, minSpeed: 0.8, speedVariation: 0.2, oscillationSpeed: 2, minSpeedFactor: 0.5, lineLength: 3, resetDistance: 20, resetOffset: 20, cameraSpeed: 2.0, cameraAcceleration: 0.1, } /** * @typedef {Object} VibeverseOptions * @property {string} [labelText] - Custom text to display above the portal * @property {string} [labelColor] - Color of the portal label text * @property {THREE.Euler} [lookAt] - Rotation to apply to the portal * @property {Object} [position] - Position coordinates for the portal * @property {number} position.x - X coordinate * @property {number} position.y - Y coordinate * @property {number} position.z - Z coordinate */ /** * Vibeverse class for managing interconnected 3D web game portals */ export class Vibeverse { /** * Creates a new Vibeverse instance * @param {THREE.Scene} scene - Three.js scene * @param {THREE.Camera} camera - Three.js camera * @param {WebSocket} socket - WebSocket connection * @param {WarpConfig|null} [warpConfig=DEFAULT_WARP_CONFIG] - Configuration for warp effect */ constructor(scene, camera, socket, warpConfig = DEFAULT_WARP_CONFIG) { this.scene = scene this.camera = camera this.socket = socket @@ -81,8 +153,26 @@ export class PortalManager { this.startPortalBox = null this.exitPortalBox = null this.playerCheckInterval = null this.enableWarpEffect = warpConfig !== null // Enable warp effect if config is provided this.warpEffect = null this.isWarping = false // Warp effect configuration - use provided config or defaults this.warpConfig = warpConfig || DEFAULT_WARP_CONFIG // Create a vector to store camera's forward direction this.cameraDirection = new THREE.Vector3() this.currentCameraSpeed = 0 } /** * Creates a portal mesh with specified properties * @param {number} [radius=6] - Radius of the portal * @param {number} [color=0xff0000] - Color of the portal (hex) * @param {VibeverseOptions} [options={}] - Additional portal options * @returns {THREE.Group} Portal mesh group * @private */ createPortalMesh(radius = 6, color = 0xff0000, options = {}) { const portal = new THREE.Group() @@ -216,6 +306,15 @@ export class PortalManager { return wrapper } /** * Creates a start portal that returns users to their previous location * @param {number} [x=0] - X coordinate * @param {number} [y=0] - Y coordinate * @param {number} [z=0] - Z coordinate * @param {number} [radius=6] - Portal radius * @param {VibeverseOptions} [options={}] - Additional portal options * @returns {THREE.Group} Start portal mesh group */ createStartPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { const portal = this.createPortalMesh(radius, 0xff0000, { labelText: options.labelText || 'Go back', @@ -232,6 +331,15 @@ export class PortalManager { return portal } /** * Creates an exit portal that takes users to Vibeverse * @param {number} [x=0] - X coordinate * @param {number} [y=0] - Y coordinate * @param {number} [z=0] - Z coordinate * @param {number} [radius=6] - Portal radius * @param {VibeverseOptions} [options={}] - Additional portal options * @returns {THREE.Group} Exit portal mesh group */ createExitPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { const portal = this.createPortalMesh(radius, 0x00ff00, { labelText: options.labelText || 'To Vibeverse', @@ -248,6 +356,10 @@ export class PortalManager { return portal } /** * Animates the start portal particles * @private */ animateStartPortal() { if (!this.startPortal || !this.startPortal.userData) return @@ -264,6 +376,10 @@ export class PortalManager { requestAnimationFrame(this.animateStartPortal.bind(this)) } /** * Animates the exit portal particles * @private */ animateExitPortal() { if (!this.exitPortal || !this.exitPortal.userData) return @@ -280,6 +396,10 @@ export class PortalManager { requestAnimationFrame(this.animateExitPortal.bind(this)) } /** * Checks for player collisions with portals * @param {Object} player - Player object with position property */ checkPortalCollisions(player) { if (!player) return @@ -302,9 +422,175 @@ export class PortalManager { } } /** * Creates the warp effect geometry and materials * @returns {THREE.LineSegments} Warp effect mesh * @private */ createWarpEffect() { const config = this.warpConfig const geometry = new THREE.BufferGeometry() const positions = new Float32Array(config.lineCount * config.pointsPerLine * 3) const velocities = new Float32Array(config.lineCount * 3) const colors = new Float32Array(config.lineCount * config.pointsPerLine * 3) const timingOffsets = new Float32Array(config.lineCount) const matrix = new THREE.Matrix4() const basePosition = new THREE.Vector3() const transformedPosition = new THREE.Vector3() for (let i = 0; i < config.lineCount; i++) { const theta = (i / config.lineCount) * Math.PI * 2 for (let j = 0; j < config.pointsPerLine; j++) { const idx = (i * config.pointsPerLine + j) * 3 const z = j * config.lineLength const radius = config.tunnelRadius + j * config.tunnelExpansion positions[idx] = Math.cos(theta) * radius positions[idx + 1] = Math.sin(theta) * radius positions[idx + 2] = z const intensity = 1 - (j / (config.pointsPerLine - 1)) * 0.9 colors[idx] = intensity colors[idx + 1] = intensity colors[idx + 2] = 1 } const lineIdx = i * 3 velocities[lineIdx] = 0 velocities[lineIdx + 1] = 0 velocities[lineIdx + 2] = config.minSpeed + Math.random() * config.speedVariation // Now positive for opposite direction timingOffsets[i] = Math.random() * Math.PI * 2 } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) const material = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 1, linewidth: 1.5, }) const lines = new THREE.LineSegments(geometry, material) lines.userData.velocities = velocities lines.userData.timingOffsets = timingOffsets lines.userData.basePosition = basePosition lines.userData.transformedPosition = transformedPosition lines.userData.matrix = matrix lines.userData.startTime = Date.now() lines.visible = false lines.position.copy(this.camera.position) this.scene.add(lines) return lines } /** * Starts the warp effect animation */ startWarpEffect() { if (!this.enableWarpEffect || this.isWarping) return this.isWarping = true this.currentCameraSpeed = 0 if (!this.warpEffect) { this.warpEffect = this.createWarpEffect() } this.warpEffect.visible = true this.warpEffect.material.opacity = 1 const animate = () => { if (!this.isWarping) return const config = this.warpConfig const positions = this.warpEffect.geometry.attributes.position.array const velocities = this.warpEffect.userData.velocities const timingOffsets = this.warpEffect.userData.timingOffsets const basePosition = this.warpEffect.userData.basePosition const transformedPosition = this.warpEffect.userData.transformedPosition const matrix = this.warpEffect.userData.matrix const currentTime = Date.now() const elapsedTime = (currentTime - this.warpEffect.userData.startTime) / 1000 // Update camera position // Get camera's current forward direction this.camera.getWorldDirection(this.cameraDirection) // Accelerate camera movement this.currentCameraSpeed = Math.min(config.cameraSpeed, this.currentCameraSpeed + config.cameraAcceleration) // Move camera forward in its current direction this.camera.position.addScaledVector(this.cameraDirection, this.currentCameraSpeed) // Update warp effect position to follow camera this.warpEffect.position.copy(this.camera.position) matrix.copy(this.camera.matrix) for (let i = 0; i < velocities.length / 3; i++) { const baseVelocity = velocities[i * 3 + 2] const theta = (i / (velocities.length / 3)) * Math.PI * 2 const timingOffset = timingOffsets[i] const movementFactor = (Math.sin(elapsedTime * config.oscillationSpeed + timingOffset) + 1) * 0.5 const currentVelocity = baseVelocity * (config.minSpeedFactor + movementFactor * (1 - config.minSpeedFactor)) for (let j = 0; j < config.pointsPerLine; j++) { const posIdx = (i * config.pointsPerLine + j) * 3 basePosition.set(positions[posIdx], positions[posIdx + 1], positions[posIdx + 2]) positions[posIdx + 2] += currentVelocity if (positions[posIdx + 2] > config.resetDistance) { for (let k = 0; k < config.pointsPerLine; k++) { const resetIdx = (i * config.pointsPerLine + k) * 3 const z = k * config.lineLength - config.resetOffset const radius = config.tunnelRadius + k * config.tunnelExpansion positions[resetIdx] = Math.cos(theta) * radius positions[resetIdx + 1] = Math.sin(theta) * radius positions[resetIdx + 2] = z } } } } this.warpEffect.quaternion.copy(this.camera.quaternion) this.warpEffect.geometry.attributes.position.needsUpdate = true requestAnimationFrame(animate) } requestAnimationFrame(animate) } /** * Stops the warp effect animation */ stopWarpEffect() { if (!this.warpEffect || !this.isWarping) return this.warpEffect.visible = false this.isWarping = false this.currentCameraSpeed = 0 } /** * Handles player entry into start portal * @private */ handleStartPortalEntry() { const url = refUrl() if (url) { if (this.enableWarpEffect) { this.startWarpEffect() } const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() for (const [key, value] of currentParams) { @@ -317,6 +603,10 @@ export class PortalManager { } } /** * Handles player entry into exit portal * @private */ handleExitPortalEntry() { const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() @@ -346,9 +636,23 @@ export class PortalManager { document.body.appendChild(iframe) } if (this.enableWarpEffect) { this.startWarpEffect() } window.location.href = nextPage } /** * Creates both start and exit portals * @param {number} [x=45] - X coordinate * @param {number} [y=0] - Y coordinate * @param {number} [z=45] - Z coordinate * @param {number} [radius=6] - Portal radius * @param {VibeverseOptions} [options={}] - Additional portal options * @returns {Object} Object containing both portal references * @property {THREE.Group} exitPortal - The exit portal mesh * @property {THREE.Group|null} startPortal - The start portal mesh (null if not created) */ createPortals(x = 45, y = 0, z = 45, radius = 6, options = {}) { // Create exit portal with default settings const exitPortal = this.createExitPortal(x, y, z, radius, { @@ -372,7 +676,18 @@ export class PortalManager { } } /** * Updates portal state */ update() { // No need to update collision boxes every frame, we update them in checkPortalCollisions } /** * Toggles the warp effect on/off * @param {boolean} enable - Whether to enable the warp effect */ toggleWarpEffect(enable) { this.enableWarpEffect = enable } } -
benallfree renamed this gist
Apr 3, 2025 . 1 changed file with 139 additions and 163 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,34 +1,78 @@ /** * Vibeverse Portal Standard * ----------------------- * Integration guide for connecting your experience to the Vibeverse metaverse. * * Quick Start: * 1. Import and initialize PortalManager with your Three.js scene, camera and socket * 2. Call createPortals() to add portal meshes to your scene * * Portal Behavior: * - Exit Portal (green): Takes users to Vibeverse * - Start Portal (red): Only appears when 'ref' URL param is present * - The Start Portal returns users to the referring experience * * URL Parameters: * - ref: The referring URL to return to (e.g. ?ref=yourgame.com) * - portal: Set to 'true' when coming from another experience * - username: Player identifier passed between experiences * - color: Avatar color preference * * Splash Page Bypass: * If your experience has a splash/loading page, you can check if the user is coming * from Vibeverse by using the isVibeverse() helper: * * ```js * if (isVibeverse()) { * // Skip splash and load directly into experience * } * ``` * * Audio Handling: * When bypassing the splash screen, you should keep audio muted initially and only * start it after the first user interaction to comply with browser autoplay policies. * Here's an example of how to handle this: * * ```js * // Setup one-time interaction handler * const enableAudioOnInteraction = (event) => { * // Remove listeners since we only need this once * document.removeEventListener('click', enableAudioOnInteraction) * document.removeEventListener('touchstart', enableAudioOnInteraction) * * // Start your audio here * // yourAudioSystem.start() * } * * // Add listeners for both mouse and touch events * document.addEventListener('click', enableAudioOnInteraction) * document.addEventListener('touchstart', enableAudioOnInteraction) * ``` * */ // Helper to check if user is coming from Vibeverse export function isVibeverse() { return !!refUrl() } const refUrl = () => { const params = new URLSearchParams(window.location.search) const refUrl = params.get('ref') if (refUrl) { let url = refUrl if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'https://' + url } return url } return null } import * as THREE from 'three' export class PortalManager { constructor(scene, camera, socket) { this.scene = scene this.camera = camera this.socket = socket @@ -39,13 +83,7 @@ export class PortalManager { this.playerCheckInterval = null } createPortalMesh(radius = 6, color = 0xff0000, options = {}) { const portal = new THREE.Group() // Create the torus ring @@ -79,37 +117,26 @@ export class PortalManager { const particleColors = new Float32Array(particleCount * 3) for (let i = 0; i < particleCount * 3; i += 3) { const angle = Math.random() * Math.PI * 2 const particleRadius = radius + (Math.random() - 0.5) * (radius * 0.15) particlePositions[i] = Math.cos(angle) * particleRadius particlePositions[i + 1] = Math.sin(angle) * particleRadius particlePositions[i + 2] = (Math.random() - 0.5) * (radius * 0.15) if (color === 0xff0000) { particleColors[i] = 0.8 + Math.random() * 0.2 particleColors[i + 1] = 0 particleColors[i + 2] = 0 } else { particleColors[i] = 0 particleColors[i + 1] = 0.8 + Math.random() * 0.2 particleColors[i + 2] = 0 } } particlesGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3)) particlesGeometry.setAttribute('color', new THREE.BufferAttribute(particleColors, 3)) const particleMaterial = new THREE.PointsMaterial({ size: radius * 0.03, @@ -121,34 +148,24 @@ export class PortalManager { const particles = new THREE.Points(particlesGeometry, particleMaterial) portal.add(particles) portal.userData = { particlesGeometry: particlesGeometry, type: color === 0xff0000 ? 'entrance' : 'exit', } const defaultLabelText = portal.userData.type === 'entrance' ? 'Go back' : 'To Vibeverse' const labelText = options.labelText || defaultLabelText const wrapper = new THREE.Group() if (labelText) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') if (!context) return wrapper canvas.width = 512 canvas.height = 64 const labelColor = options.labelColor || (color === 0xff0000 ? '#ff0000' : '#00ff00') context.fillStyle = labelColor context.font = 'bold 32px Arial' @@ -164,21 +181,32 @@ export class PortalManager { }) const label = new THREE.Mesh(labelGeometry, labelMaterial) label.position.y = radius * 1.5 portal.add(label) } wrapper.add(portal) const bbox = new THREE.Box3().setFromObject(portal) portal.position.y = -bbox.min.y // Position the wrapper const position = options.position || { x: 0, y: 0, z: 0 } wrapper.position.set(position.x, position.y, position.z) // Apply rotation after positioning if (options.lookAt) { wrapper.rotation.setFromEuler(options.lookAt) } else { const targetVector = new THREE.Vector3(0, 0, 0) const direction = targetVector.sub(wrapper.position).normalize() const matrix = new THREE.Matrix4() matrix.lookAt(wrapper.position, targetVector, new THREE.Vector3(0, 1, 0)) wrapper.quaternion.setFromRotationMatrix(matrix) // Rotate 180 degrees around Y axis to face the correct direction wrapper.rotateY(Math.PI) } wrapper.userData = { portal: portal, particlesGeometry: portal.userData.particlesGeometry, @@ -188,166 +216,99 @@ export class PortalManager { return wrapper } createStartPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { const portal = this.createPortalMesh(radius, 0xff0000, { labelText: options.labelText || 'Go back', labelColor: options.labelColor || '#ff0000', lookAt: options.lookAt, position: { x, y, z }, }) this.scene.add(portal) this.startPortalBox = new THREE.Box3().setFromObject(portal) this.startPortal = portal this.animateStartPortal() return portal } createExitPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { const portal = this.createPortalMesh(radius, 0x00ff00, { labelText: options.labelText || 'To Vibeverse', labelColor: options.labelColor || '#00ff00', lookAt: options.lookAt, position: { x, y, z }, }) this.scene.add(portal) this.exitPortalBox = new THREE.Box3().setFromObject(portal) this.exitPortal = portal this.animateExitPortal() return portal } animateStartPortal() { if (!this.startPortal || !this.startPortal.userData) return const particlesGeometry = this.startPortal.userData.particlesGeometry if (!particlesGeometry) return const positions = particlesGeometry.attributes.position.array for (let i = 0; i < positions.length; i += 3) { positions[i + 2] = Math.sin(Date.now() * 0.002 + i) * 0.3 } particlesGeometry.attributes.position.needsUpdate = true requestAnimationFrame(this.animateStartPortal.bind(this)) } animateExitPortal() { if (!this.exitPortal || !this.exitPortal.userData) return const particlesGeometry = this.exitPortal.userData.particlesGeometry if (!particlesGeometry) return const positions = particlesGeometry.attributes.position.array for (let i = 0; i < positions.length; i += 3) { positions[i + 2] = Math.sin(Date.now() * 0.002 + i) * 0.3 } particlesGeometry.attributes.position.needsUpdate = true requestAnimationFrame(this.animateExitPortal.bind(this)) } checkPortalCollisions(player) { if (!player) return const playerPosition = new THREE.Vector3(player.position.x, player.position.y, player.position.z) if (this.startPortalBox && this.startPortal) { this.startPortalBox.setFromObject(this.startPortal) const expandedStartBox = this.startPortalBox.clone().expandByScalar(1.5) if (expandedStartBox.containsPoint(playerPosition)) { this.handleStartPortalEntry() } } if (this.exitPortalBox && this.exitPortal) { this.exitPortalBox.setFromObject(this.exitPortal) const expandedExitBox = this.exitPortalBox.clone().expandByScalar(1.5) if (expandedExitBox.containsPoint(playerPosition)) { this.handleExitPortalEntry() } } } handleStartPortalEntry() { const url = refUrl() if (url) { const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() for (const [key, value] of currentParams) { if (key !== 'ref') { newParams.append(key, value) } } @@ -356,33 +317,27 @@ export class PortalManager { } } handleExitPortalEntry() { const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() newParams.append('portal', 'true') if (this.socket && this.socket.id) { const player = this.socket.id newParams.append('username', player) } newParams.append('color', 'white') for (const [key, value] of currentParams) { if (!newParams.has(key)) { newParams.append(key, value) } } const paramString = newParams.toString() const nextPage = 'https://portal.pieter.com' + (paramString ? '?' + paramString : '') if (!document.getElementById('preloadFrame')) { const iframe = document.createElement('iframe') iframe.id = 'preloadFrame' @@ -391,12 +346,33 @@ export class PortalManager { document.body.appendChild(iframe) } window.location.href = nextPage } createPortals(x = 45, y = 0, z = 45, radius = 6, options = {}) { // Create exit portal with default settings const exitPortal = this.createExitPortal(x, y, z, radius, { labelText: 'To Vibeverse', labelColor: '#00ff00', lookAt: options.lookAt, }) if (isVibeverse()) { // Create entrance portal slightly offset from exit portal const entrancePortal = this.createStartPortal(x - radius * 2.5, y, z, radius, { labelText: 'Go back', labelColor: '#ff0000', lookAt: options.lookAt, }) } return { exitPortal, startPortal: this.startPortal, } } update() { // No need to update collision boxes every frame, we update them in checkPortalCollisions } } -
benallfree renamed this gist
Mar 30, 2025 . 1 changed file with 63 additions and 14 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,34 @@ import * as THREE from 'three' interface PortalOptions { labelText?: string labelColor?: string } interface PortalUserData { particlesGeometry: THREE.BufferGeometry type: 'entrance' | 'exit' } interface PortalWrapper extends THREE.Group { userData: { portal: THREE.Group particlesGeometry: THREE.BufferGeometry type: 'entrance' | 'exit' } } export class PortalManager { private scene: THREE.Scene private camera: THREE.Camera private socket: any // TODO: Replace with proper socket type private startPortal: PortalWrapper | null private exitPortal: PortalWrapper | null private startPortalBox: THREE.Box3 | null private exitPortalBox: THREE.Box3 | null private playerCheckInterval: NodeJS.Timeout | null constructor(scene: THREE.Scene, camera: THREE.Camera, socket: any) { this.scene = scene this.camera = camera this.socket = socket @@ -11,7 +40,11 @@ export class PortalManager { } // Create a portal mesh with proper alignment and origin at bottom createPortalMesh( radius = 6, color = 0xff0000, options: PortalOptions = {} ): PortalWrapper { // Create a single container for the portal elements const portal = new THREE.Group() @@ -101,10 +134,15 @@ export class PortalManager { // Get label text from options or use default const labelText = options.labelText || defaultLabelText // Create a wrapper container with origin at bottom const wrapper = new THREE.Group() as PortalWrapper // Add label if text is provided if (labelText) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') if (!context) return wrapper // Skip label creation if context is null canvas.width = 512 canvas.height = 64 @@ -130,9 +168,6 @@ export class PortalManager { portal.add(label) } // Add portal to wrapper at origin first wrapper.add(portal) @@ -154,7 +189,13 @@ export class PortalManager { } // Create entrance portal at specified coordinates createStartPortal( x = 0, y = 0, z = 0, radius = 6, options: PortalOptions = {} ): PortalWrapper { // Create portal mesh with default or custom options const portal = this.createPortalMesh(radius, 0xff0000, { labelText: options.labelText || 'Go back', @@ -182,7 +223,13 @@ export class PortalManager { } // Create exit portal at specified coordinates createExitPortal( x = 0, y = 0, z = 0, radius = 6, options: PortalOptions = {} ): PortalWrapper { // Create portal mesh with default or custom options const portal = this.createPortalMesh(radius, 0x00ff00, { labelText: options.labelText || 'To Vibeverse', @@ -210,7 +257,7 @@ export class PortalManager { } // Animate entrance portal particles animateStartPortal(): void { if (!this.startPortal || !this.startPortal.userData) return // Get the particles geometry from the userData @@ -229,7 +276,7 @@ export class PortalManager { } // Animate exit portal particles animateExitPortal(): void { if (!this.exitPortal || !this.exitPortal.userData) return // Get the particles geometry from the userData @@ -248,7 +295,9 @@ export class PortalManager { } // Check if player has entered a portal checkPortalCollisions(player: { position: { x: number; y: number; z: number } }): void { if (!player) return // Use player camera position for collision detection @@ -284,7 +333,7 @@ export class PortalManager { } // Handle entrance portal interaction handleStartPortalEntry(): void { // Get ref from URL params const urlParams = new URLSearchParams(window.location.search) const refUrl = urlParams.get('ref') @@ -308,11 +357,11 @@ export class PortalManager { } // Handle exit portal interaction handleExitPortalEntry(): void { // Create parameters for the next page const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() newParams.append('portal', 'true') // Convert boolean to string // If socket available, add username if (this.socket && this.socket.id) { @@ -347,7 +396,7 @@ export class PortalManager { } // Update method to be called each frame update(): void { // No need to update collision boxes every frame, we update them in checkPortalCollisions } } -
benallfree revised this gist
Mar 28, 2025 . 1 changed file with 28 additions and 13 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -94,16 +94,28 @@ export class PortalManager { type: color === 0xff0000 ? 'entrance' : 'exit', } // Set default label text based on portal type const defaultLabelText = portal.userData.type === 'entrance' ? 'Go back' : 'To Vibeverse' // Get label text from options or use default const labelText = options.labelText || defaultLabelText // Add label if text is provided if (labelText) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') canvas.width = 512 canvas.height = 64 // Get label color from options or use portal color const labelColor = options.labelColor || (color === 0xff0000 ? '#ff0000' : '#00ff00') context.fillStyle = labelColor context.font = 'bold 32px Arial' context.textAlign = 'center' context.fillText(labelText, canvas.width / 2, canvas.height / 2) const texture = new THREE.CanvasTexture(canvas) const labelGeometry = new THREE.PlaneGeometry(radius * 1.6, radius * 0.25) @@ -142,13 +154,16 @@ export class PortalManager { } // Create entrance portal at specified coordinates createStartPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { // Create portal mesh with default or custom options const portal = this.createPortalMesh(radius, 0xff0000, { labelText: options.labelText || 'Go back', labelColor: options.labelColor || '#ff0000', }) // Position the portal portal.position.x = x portal.position.y = y portal.position.z = z // Add portal to scene @@ -167,16 +182,16 @@ export class PortalManager { } // Create exit portal at specified coordinates createExitPortal(x = 0, y = 0, z = 0, radius = 6, options = {}) { // Create portal mesh with default or custom options const portal = this.createPortalMesh(radius, 0x00ff00, { labelText: options.labelText || 'To Vibeverse', labelColor: options.labelColor || '#00ff00', }) // Position the portal portal.position.x = x portal.position.y = y portal.position.z = z // Add portal to scene -
benallfree created this gist
Mar 28, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,338 @@ export class PortalManager { constructor(scene, camera, socket) { this.scene = scene this.camera = camera this.socket = socket this.startPortal = null this.exitPortal = null this.startPortalBox = null this.exitPortalBox = null this.playerCheckInterval = null } // Create a portal mesh with proper alignment and origin at bottom createPortalMesh(radius = 6, color = 0xff0000, options = {}) { // Create a single container for the portal elements const portal = new THREE.Group() // Create the torus ring const tubeRadius = radius * 0.1 const ringGeometry = new THREE.TorusGeometry(radius, tubeRadius, 16, 100) const ringMaterial = new THREE.MeshPhongMaterial({ color: color, emissive: color, transparent: true, opacity: 0.8, }) const ring = new THREE.Mesh(ringGeometry, ringMaterial) portal.add(ring) // Create portal inner surface const innerRadius = radius * 0.9 const innerGeometry = new THREE.CircleGeometry(innerRadius, 32) const innerMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5, side: THREE.DoubleSide, }) const inner = new THREE.Mesh(innerGeometry, innerMaterial) portal.add(inner) // Create particle system for portal effect const particleCount = 500 const particlesGeometry = new THREE.BufferGeometry() const particlePositions = new Float32Array(particleCount * 3) const particleColors = new Float32Array(particleCount * 3) for (let i = 0; i < particleCount * 3; i += 3) { // Create particles in a ring around the portal const angle = Math.random() * Math.PI * 2 const particleRadius = radius + (Math.random() - 0.5) * (radius * 0.15) // Position particles in a ring in the same plane as the portal particlePositions[i] = Math.cos(angle) * particleRadius // x particlePositions[i + 1] = Math.sin(angle) * particleRadius // y particlePositions[i + 2] = (Math.random() - 0.5) * (radius * 0.15) // z (small depth variation) // Set color with slight variation if (color === 0xff0000) { // Red portal particleColors[i] = 0.8 + Math.random() * 0.2 particleColors[i + 1] = 0 particleColors[i + 2] = 0 } else { // Green portal particleColors[i] = 0 particleColors[i + 1] = 0.8 + Math.random() * 0.2 particleColors[i + 2] = 0 } } particlesGeometry.setAttribute( 'position', new THREE.BufferAttribute(particlePositions, 3) ) particlesGeometry.setAttribute( 'color', new THREE.BufferAttribute(particleColors, 3) ) const particleMaterial = new THREE.PointsMaterial({ size: radius * 0.03, vertexColors: true, transparent: true, opacity: 0.6, }) const particles = new THREE.Points(particlesGeometry, particleMaterial) portal.add(particles) // Store particles for animation portal.userData = { particlesGeometry: particlesGeometry, type: color === 0xff0000 ? 'entrance' : 'exit', } // Add label if specified if (options.label) { const canvas = document.createElement('canvas') const context = canvas.getContext('2d') canvas.width = 512 canvas.height = 64 context.fillStyle = options.labelColor || '#00ff00' context.font = 'bold 32px Arial' context.textAlign = 'center' context.fillText(options.label, canvas.width / 2, canvas.height / 2) const texture = new THREE.CanvasTexture(canvas) const labelGeometry = new THREE.PlaneGeometry(radius * 1.6, radius * 0.25) const labelMaterial = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide, }) const label = new THREE.Mesh(labelGeometry, labelMaterial) label.position.y = radius * 1.5 // Position label above the portal portal.add(label) } // Create a wrapper container with origin at bottom const wrapper = new THREE.Group() // Add portal to wrapper at origin first wrapper.add(portal) // Compute the portal's bounding box to get actual dimensions const bbox = new THREE.Box3().setFromObject(portal) // Position portal so its bottom is at y=0 of the wrapper // This ensures the bottom of the portal sits exactly on the ground portal.position.y = -bbox.min.y // Offset to make bottom align with y=0 // Store reference to portal for animations wrapper.userData = { portal: portal, particlesGeometry: portal.userData.particlesGeometry, type: portal.userData.type, } return wrapper } // Create entrance portal at specified coordinates createStartPortal(x = 0, y = 0, z = 0, radius = 6) { // Create portal mesh const portal = this.createPortalMesh(radius, 0xff0000) // Position the portal portal.position.x = x portal.position.y = y // Will be at y=radius to sit on ground portal.position.z = z // Add portal to scene this.scene.add(portal) // Create portal collision box this.startPortalBox = new THREE.Box3().setFromObject(portal) // Store portal reference this.startPortal = portal // Start animation this.animateStartPortal() return portal } // Create exit portal at specified coordinates createExitPortal(x = 0, y = 0, z = 0, radius = 6) { // Create portal mesh with label const portal = this.createPortalMesh(radius, 0x00ff00, { label: 'VIBEVERSE PORTAL', labelColor: '#00ff00', }) // Position the portal portal.position.x = x portal.position.y = y // Will be at y=radius to sit on ground portal.position.z = z // Add portal to scene this.scene.add(portal) // Create portal collision box this.exitPortalBox = new THREE.Box3().setFromObject(portal) // Store portal reference this.exitPortal = portal // Start animation this.animateExitPortal() return portal } // Animate entrance portal particles animateStartPortal() { if (!this.startPortal || !this.startPortal.userData) return // Get the particles geometry from the userData const particlesGeometry = this.startPortal.userData.particlesGeometry if (!particlesGeometry) return const positions = particlesGeometry.attributes.position.array for (let i = 0; i < positions.length; i += 3) { // Animate particles moving in/out slightly positions[i + 2] = Math.sin(Date.now() * 0.002 + i) * 0.3 } particlesGeometry.attributes.position.needsUpdate = true requestAnimationFrame(this.animateStartPortal.bind(this)) } // Animate exit portal particles animateExitPortal() { if (!this.exitPortal || !this.exitPortal.userData) return // Get the particles geometry from the userData const particlesGeometry = this.exitPortal.userData.particlesGeometry if (!particlesGeometry) return const positions = particlesGeometry.attributes.position.array for (let i = 0; i < positions.length; i += 3) { // Animate particles moving in/out slightly positions[i + 2] = Math.sin(Date.now() * 0.002 + i) * 0.3 } particlesGeometry.attributes.position.needsUpdate = true requestAnimationFrame(this.animateExitPortal.bind(this)) } // Check if player has entered a portal checkPortalCollisions(player) { if (!player) return // Use player camera position for collision detection const playerPosition = new THREE.Vector3( player.position.x, player.position.y, player.position.z ) // Check entrance portal collision if (this.startPortalBox && this.startPortal) { // Update box to match current position this.startPortalBox.setFromObject(this.startPortal) // Expand the box slightly for better collision detection const expandedStartBox = this.startPortalBox.clone().expandByScalar(1.5) if (expandedStartBox.containsPoint(playerPosition)) { this.handleStartPortalEntry() } } // Check exit portal collision if (this.exitPortalBox && this.exitPortal) { // Update box to match current position this.exitPortalBox.setFromObject(this.exitPortal) // Expand the box slightly for better collision detection const expandedExitBox = this.exitPortalBox.clone().expandByScalar(1.5) if (expandedExitBox.containsPoint(playerPosition)) { this.handleExitPortalEntry() } } } // Handle entrance portal interaction handleStartPortalEntry() { // Get ref from URL params const urlParams = new URLSearchParams(window.location.search) const refUrl = urlParams.get('ref') if (refUrl) { // Add https if not present and include query params let url = refUrl if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'https://' + url } const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() for (const [key, value] of currentParams) { if (key !== 'ref') { // Skip ref param since it's in the base URL newParams.append(key, value) } } const paramString = newParams.toString() window.location.href = url + (paramString ? '?' + paramString : '') } } // Handle exit portal interaction handleExitPortalEntry() { // Create parameters for the next page const currentParams = new URLSearchParams(window.location.search) const newParams = new URLSearchParams() newParams.append('portal', true) // If socket available, add username if (this.socket && this.socket.id) { const player = this.socket.id newParams.append('username', player) } newParams.append('color', 'white') // Copy other params from current URL for (const [key, value] of currentParams) { if (!newParams.has(key)) { newParams.append(key, value) } } const paramString = newParams.toString() const nextPage = 'https://portal.pieter.com' + (paramString ? '?' + paramString : '') // Create hidden iframe to preload next page if (!document.getElementById('preloadFrame')) { const iframe = document.createElement('iframe') iframe.id = 'preloadFrame' iframe.style.display = 'none' iframe.src = nextPage document.body.appendChild(iframe) } // Navigate to the next page window.location.href = nextPage } // Update method to be called each frame update() { // No need to update collision boxes every frame, we update them in checkPortalCollisions } }