WebGL 密码盒:解锁 soul 星球效果的创意密钥

3D soul星球效果 )这个 HTML 文件展示了如何使用 Three.js 创建一个 3D 场景,其中包括一个半透明的球体和多个可交互的小球体,每个小球体上都有一个随机生成的昵称。用户可以通过鼠标或触摸与这些小球体进行交互,查看它们的标签。这是一个展示 Three.js 基本功能和交互性的示例。

html 复制代码
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8" />
    <title>soul星球</title>
    <style>
        body {
            margin: 0;
            background-color: black;
            touch-action: none;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <script type="module">
        import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js'

        // 创建场景
        const scene = new THREE.Scene()

        // 创建相机
        const camera = new THREE.PerspectiveCamera(
            75,
            window.innerWidth / window.innerHeight,
            0.1,
            1000
        )
        camera.position.set(0, 0, 14)
        camera.lookAt(0, 0, 0)

        // 创建渲染器
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
        renderer.setSize(window.innerWidth, window.innerHeight)
        renderer.setPixelRatio(window.devicePixelRatio)
        renderer.setClearColor(0x000000, 0)
        document.body.appendChild(renderer.domElement)

        // 创建半透明球体
        const sphereGeometry = new THREE.SphereGeometry(4.85, 16, 16)
        const sphereMaterial = new THREE.ShaderMaterial({
            uniforms: {
                color: { value: new THREE.Color(0x000000) },
                opacity: { value: 0.8 },
            },
            vertexShader: `
          varying vec3 vNormal;
          void main() {
              vNormal = normalize(normalMatrix * normal);
              gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
          }
        `,
            fragmentShader: `
          uniform vec3 color;
          uniform float opacity;
          varying vec3 vNormal;
          void main() {
              float alpha = opacity * smoothstep(0.5, 1.0, vNormal.z);
              gl_FragColor = vec4(color, alpha);
          }
        `,
            transparent: true,
            side: THREE.FrontSide,
            depthWrite: false,
        })

        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
        scene.add(sphere)

        // 创建小球体和标签数组
        const smallBallGeometry = new THREE.SphereGeometry(0.15, 16, 16)
        const smallBalls = []
        const labelSprites = []

        const radius = 5
        const numPoints = 88
        const goldenRatio = (1 + Math.sqrt(5)) / 2
        const maxWidth = 160
        const textSpeed = 0.002

        // 创建射线投射器
        const raycaster = new THREE.Raycaster()
        const mouse = new THREE.Vector2()

        function createTextTexture(text, parameters = {}) {
            const {
                fontSize = 24,
                fontFace = 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
                textColor = 'white',
                backgroundColor = 'rgba(0,0,0,0)',
                maxWidth = 160,
            } = parameters

            const canvas = document.createElement('canvas')
            const context = canvas.getContext('2d')
            context.font = `${fontSize}px ${fontFace}`

            const textMetrics = context.measureText(text)
            const textWidth = Math.ceil(textMetrics.width)
            const textHeight = fontSize * 1.2

            const needMarquee = textWidth > maxWidth

            let canvasWidth = maxWidth
            if (needMarquee) {
                canvasWidth = textWidth + 60
            }

            canvas.width = canvasWidth
            canvas.height = textHeight
            context.font = `${fontSize}px ${fontFace}`
            context.clearRect(0, 0, canvas.width, canvas.height)

            context.fillStyle = backgroundColor
            context.fillRect(0, 0, canvas.width, canvas.height)

            context.fillStyle = textColor
            context.textAlign = needMarquee ? 'left' : 'center'
            context.textBaseline = 'middle'

            if (needMarquee) {
                context.fillText(text, 0, canvas.height / 2)
            } else {
                context.fillText(text, maxWidth / 2, canvas.height / 2)
            }

            const texture = new THREE.CanvasTexture(canvas)
            texture.needsUpdate = true

            if (needMarquee) {
                texture.wrapS = THREE.RepeatWrapping
                texture.wrapT = THREE.ClampToEdgeWrapping
                texture.repeat.x = maxWidth / canvas.width
            } else {
                texture.wrapS = THREE.ClampToEdgeWrapping
                texture.wrapT = THREE.ClampToEdgeWrapping
            }

            texture.minFilter = THREE.LinearFilter
            texture.magFilter = THREE.LinearFilter
            texture.generateMipmaps = false
            return { texture, needMarquee, HWRate: textHeight / maxWidth }
        }

        for (let i = 0; i < numPoints; i++) {
            const y = 1 - (i / (numPoints - 1)) * 2
            const radiusAtY = Math.sqrt(1 - y * y)

            const theta = (2 * Math.PI * i) / goldenRatio

            const x = Math.cos(theta) * radiusAtY
            const z = Math.sin(theta) * radiusAtY
            const smallBallMaterial = new THREE.MeshBasicMaterial({
                color: getRandomBrightColor(),
                depthWrite: true,
                depthTest: true,
                side: THREE.FrontSide,
            })
            const smallBall = new THREE.Mesh(smallBallGeometry, smallBallMaterial)
            smallBall.position.set(x * radius, y * radius, z * radius)
            sphere.add(smallBall)
            smallBalls.push(smallBall)

            const labelText = getRandomNickname()
            const { texture, needMarquee, HWRate } = createTextTexture(labelText, {
                fontSize: 38,
                fontFace: 'PingFang SC, Microsoft YaHei, Noto Sans, Arial, sans-serif',
                textColor: '#FFFFFF',
                maxWidth: maxWidth,
            })

            const spriteMaterial = new THREE.SpriteMaterial({
                map: texture,
                transparent: true,
                depthWrite: true,
                depthTest: true,
                blending: THREE.NormalBlending,
            })

            const sprite = new THREE.Sprite(spriteMaterial)
            sprite.scale.set(1, HWRate, 1)
            labelSprites.push({ sprite, smallBall, texture, needMarquee, labelText })
            scene.add(sprite)
        }

        // 添加灯光
        const light = new THREE.AmbientLight(0xffffff, 0.5)
        scene.add(light)
        const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
        directionalLight.position.set(5, 5, 5)
        scene.add(directionalLight)

        // 定义自动旋转速度和轴
        const autoRotationSpeed = 0.0005
        let autoRotationAxis = new THREE.Vector3(0, 1, 0).normalize()
        let currentAngularVelocity = autoRotationAxis.clone().multiplyScalar(autoRotationSpeed)

        let isDragging = false
        let previousMousePosition = { x: 0, y: 0 }
        let lastDragDelta = { x: 0, y: 0 }

        const decayRate = 0.92
        const increaseRate = 1.02

        // 鼠标事件处理
        const onMouseDown = (event) => {
            isDragging = true
            previousMousePosition = {
                x: event.clientX,
                y: event.clientY,
            }
        }

        const onMouseMove = (event) => {
            if (isDragging) {
                const deltaX = event.clientX - previousMousePosition.x
                const deltaY = event.clientY - previousMousePosition.y

                lastDragDelta = { x: deltaX, y: deltaY }

                const rotationFactor = 0.005

                const angleY = deltaX * rotationFactor
                const angleX = deltaY * rotationFactor

                const quaternionY = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(0, 1, 0),
                    angleY
                )
                const quaternionX = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(1, 0, 0),
                    angleX
                )

                const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)

                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor

                if (dragRotationAxis.length() > 0) {
                    currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
                }

                previousMousePosition = {
                    x: event.clientX,
                    y: event.clientY,
                }
            }
        }

        const onMouseUp = () => {
            if (isDragging) {
                isDragging = false

                const deltaX = lastDragDelta.x
                const deltaY = lastDragDelta.y

                if (deltaX !== 0 || deltaY !== 0) {
                    const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                    if (newAxis.length() > 0) {
                        autoRotationAxis.copy(newAxis)
                    }

                    const dragSpeed = currentAngularVelocity.length()
                    if (dragSpeed > autoRotationSpeed) {
                        // 维持当前旋转速度
                    } else {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                }
            }
        }

        // 触摸事件处理
        const onTouchStart = (event) => {
            isDragging = true
            const touch = event.touches[0]
            previousMousePosition = {
                x: touch.clientX,
                y: touch.clientY,
            }
        }

        const onTouchMove = (event) => {
            event.preventDefault()
            if (isDragging) {
                const touch = event.touches[0]
                const deltaX = touch.clientX - previousMousePosition.x
                const deltaY = touch.clientY - previousMousePosition.y

                lastDragDelta = { x: deltaX, y: deltaY }

                const rotationFactor = 0.002

                const angleY = deltaX * rotationFactor
                const angleX = deltaY * rotationFactor

                const quaternionY = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(0, 1, 0),
                    angleY
                )
                const quaternionX = new THREE.Quaternion().setFromAxisAngle(
                    new THREE.Vector3(1, 0, 0),
                    angleX
                )

                const deltaQuat = new THREE.Quaternion().multiplyQuaternions(quaternionY, quaternionX)

                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const dragRotationAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                const dragRotationSpeed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * rotationFactor

                if (dragRotationAxis.length() > 0) {
                    currentAngularVelocity.copy(dragRotationAxis).multiplyScalar(dragRotationSpeed)
                }

                previousMousePosition = {
                    x: touch.clientX,
                    y: touch.clientY,
                }
            }
        }

        const onTouchEnd = (event) => {
            if (isDragging) {
                isDragging = false

                const deltaX = lastDragDelta.x
                const deltaY = lastDragDelta.y

                if (deltaX !== 0 || deltaY !== 0) {
                    const newAxis = new THREE.Vector3(deltaY, deltaX, 0).normalize()
                    if (newAxis.length() > 0) {
                        autoRotationAxis.copy(newAxis)
                    }

                    const dragSpeed = currentAngularVelocity.length()
                    if (dragSpeed > autoRotationSpeed) {
                        // 维持当前旋转速度
                    } else {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                }
            }

            // 检查点击事件
            if (event.changedTouches.length > 0) {
                const touch = event.changedTouches[0]
                mouse.x = (touch.clientX / window.innerWidth) * 2 - 1
                mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1
                checkIntersection()
            }
        }

        // 事件监听
        window.addEventListener('mousedown', onMouseDown)
        window.addEventListener('mousemove', onMouseMove)
        window.addEventListener('mouseup', onMouseUp)
        window.addEventListener('touchstart', onTouchStart)
        window.addEventListener('touchmove', onTouchMove)
        window.addEventListener('touchend', onTouchEnd)
        document.addEventListener('gesturestart', function (e) {
            e.preventDefault()
        })

        // 添加点击事件监听
        window.addEventListener('click', onMouseClick)

        // 处理窗口大小调整
        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight
            camera.updateProjectionMatrix()
            renderer.setSize(window.innerWidth, window.innerHeight)
        })

        function onMouseClick(event) {
            event.preventDefault()
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
            console.log(event.clientX, mouse.x, mouse.y)

            checkIntersection()
        }

        function checkIntersection() {
            raycaster.setFromCamera(mouse, camera)
            const intersects = raycaster.intersectObjects(smallBalls)

            if (intersects.length > 0) {
                const intersectedBall = intersects[0].object
                const index = smallBalls.indexOf(intersectedBall)
                if (index !== -1) {
                    const labelInfo = labelSprites[index]
                    showLabelInfo(labelInfo)
                }
            }
        }

        function showLabelInfo(labelInfo) {
            alert(`点击的小球标签:${labelInfo.labelText}`)
        }

        // 动画循环
        function animate() {
            requestAnimationFrame(animate)

            if (!isDragging) {
                const deltaQuat = new THREE.Quaternion().setFromEuler(
                    new THREE.Euler(
                        currentAngularVelocity.x,
                        currentAngularVelocity.y,
                        currentAngularVelocity.z,
                        'XYZ'
                    )
                )
                sphere.quaternion.multiplyQuaternions(deltaQuat, sphere.quaternion)

                const currentSpeed = currentAngularVelocity.length()

                if (currentSpeed > autoRotationSpeed) {
                    currentAngularVelocity.multiplyScalar(decayRate)

                    if (currentAngularVelocity.length() < autoRotationSpeed) {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                } else if (currentSpeed < autoRotationSpeed) {
                    currentAngularVelocity.multiplyScalar(increaseRate)

                    if (currentAngularVelocity.length() > autoRotationSpeed) {
                        currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                    }
                } else {
                    currentAngularVelocity.copy(autoRotationAxis).multiplyScalar(autoRotationSpeed)
                }
            }

            // 更新标签的位置和跑马灯效果
            labelSprites.forEach(({ sprite, smallBall, texture, needMarquee }) => {
                smallBall.updateMatrixWorld()
                const smallBallWorldPos = new THREE.Vector3()
                smallBall.getWorldPosition(smallBallWorldPos)

                const upOffset = new THREE.Vector3(0, 0.3, 0)

                sprite.position.copy(smallBallWorldPos).add(upOffset)

                if (needMarquee) {
                    texture.offset.x += textSpeed

                    if (texture.offset.x > 1) {
                        texture.offset.x = 0
                    }
                }
            })

            renderer.render(scene, camera)
        }

        animate()

        function getRandomBrightColor() {
            const hue = Math.floor(Math.random() * 360)
            const saturation = Math.floor(Math.random() * 40 + 10)
            const lightness = Math.floor(Math.random() * 40 + 40)

            const rgb = hslToRgb(hue, saturation, lightness)

            return (rgb.r << 16) | (rgb.g << 8) | rgb.b
        }

        function hslToRgb(h, s, l) {
            s /= 100
            l /= 100

            const c = (1 - Math.abs(2 * l - 1)) * s
            const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
            const m = l - c / 2

            let r, g, b
            if (h >= 0 && h < 60) {
                r = c
                g = x
                b = 0
            } else if (h >= 60 && h < 120) {
                r = x
                g = c
                b = 0
            } else if (h >= 120 && h < 180) {
                r = 0
                g = c
                b = x
            } else if (h >= 180 && h < 240) {
                r = 0
                g = x
                b = c
            } else if (h >= 240 && h < 300) {
                r = x
                g = 0
                b = c
            } else {
                r = c
                g = 0
                b = x
            }

            return {
                r: Math.round((r + m) * 255),
                g: Math.round((g + m) * 255),
                b: Math.round((b + m) * 255),
            }
        }

        function getRandomNickname() {
            const adjectives = [
                '孤狼', '清风', '梦蝶', '墨染', '紫电',
                '流云', '星河', '竹影', '晨曦', '飞絮'
            ]
            const nouns = [
                '漫步', '飞翔', '追梦', '墨舞', '紫梦',
                '流光', '星辰', '竹林', '晨光', '飘雪'
            ]

            const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]
            const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]

            const nickname = `${randomAdjective} ${randomNoun}`

            if (nickname.length < 2) {
                return getRandomNickname()
            } else if (nickname.length > 22) {
                return nickname.slice(0, 22)
            }

            return nickname
        }
    </script>
</body>

</html>
  • JavaScript 部分 type="module: 使用 ES6 模块导入 Three.js 库。
  • import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js';从 CDN 导入 Three.js 库。
  • 创建场景、相机和渲染器:创建一个场景 scene,相机 camera 和渲染器 renderer,并设置相机和渲染器的基本属性。
  • 创建半透明球体:使用 THREE.SphereGeometry 创建球体几何体,THREE.ShaderMaterial 创建材质,并设置为半透明。
  • 创建小球体和标签数组:创建小球体的几何体 smallBallGeometry,以及用于存储小球体和标签的数组 smallBalls 和 labelSprites。
  • 射线投射器和鼠标位置:创建射线投射器 raycaster 和鼠标位置向量 mouse。
  • 创建文本纹理:createTextTexture 函数用于创建文本的纹理,可以用于小球体上的标签。
  • 动画循环:animate 函数用于更新场景中的物体位置和渲染场景。
  • 事件监听:添加鼠标和触摸事件监听,实现用户与 3D 场景的交互。
  • 随机昵称生成:getRandomNickname 函数用于生成随机昵称。
相关推荐
文阿花11 分钟前
Echarts实现自定旋转3D饼状图
javascript·3d·echarts·饼状图
meilindehuzi_a1 小时前
深入理解 JavaScript 的同步与异步机制:从单线程设计到 Promise 核心应用
开发语言·javascript·ecmascript
如烟花的信页1 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
永远的WEB小白1 小时前
css改变svg图标的颜色
前端·javascript·css
ikoala1 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
赵庆明老师2 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love2 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年2 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
moMo3 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript
Xzh04233 小时前
Web 前端开发 — 期末复习指南(Html、Css、Js)
css·html5·web·js·期末