ThreeJs+全屏滚动展示(无滚动条)——轻轻滚动滑轮,即可切换全屏页面展示模型(带源码)

223 阅读3分钟

ThreeJs+全屏滚动展示

一、代码

原理:利用上一篇文章的全屏滚动原理加上亿点点ThreeJS的东西,构建出一个简单的全屏滚动展示模型的示例。

1.Html结构

<head>
    <meta charset="UTF-8">
    <title>fullScreen</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
    <script src="https://unpkg.com/three@0.156.1/build/three.module.js"></script>
    <script src="https://unpkg.com/three@0.156.1/examples/jsm/controls/OrbitControls.js"></script>
    <script type="importmap">
        {
      "imports": {
        "three": "https://unpkg.com/three@0.156.1/build/three.module.js",
        "three/addons/": "https://unpkg.com/three@0.156.1/examples/jsm/"
      }
      }
    </script>
</head>
<body>
<div class="page-container" id="page-container">
    <div class="page">
        <div class="page-title">
            <h1>Box</h1>
            <h3>THREE.BoxGeometry</h3>
        </div>
    </div>
    <div class="page-space"></div>
    <div class="page">
        <div class="page-title">
            <h1>Cylinder</h1>
            <h3>THREE.CylinderGeometry</h3>
        </div>
    </div>
    <div class="page-space"></div>
    <div class="page">
        <div class="page-title">
            <h1>Dodecahedron</h1>
            <h3>THREE.DodecahedronGeometry</h3>
        </div>
    </div>
    <div class="page-space"></div>
</div>
</body>

1.CSS结构

<style>
    * {
        margin: 0;
        padding: 0;
    }

    .page-container {
        position: relative;
        background-color: skyblue;
        height: 100vh;
        overflow: hidden;
    }

    canvas {
        position: fixed;
        left: 0;
        top: 0;
    }

    .page {
        height: 30vh;
        display: flex;
        color: #fff;
        flex-direction: column;
        /* justify-content: center; */
        align-items: center;
        position: relative;
        z-index: 10;
        overflow: hidden;
    }

    .page-space {
        height: 70vh;
    }

    .page-title {
        position: relative;
        z-index: 999;

    }

    h1 {
        margin: 20px 0;
        font-size: 40px;
    }

    h3 {
        font-size: 30px;
    }
</style>

1.JS结构

<script type="module">
  import * as THREE from 'three'
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

  window.onload = () => {
    //#region <创建场景>
    // 创建场景
    const scene = new THREE.Scene()
//#endregion
    //#region <灯光>
    // 创建灯光
    const light = new THREE.DirectionalLight(0xffffff, 1)
    // 设置灯光位置
    light.position.set(0, 0, 30)
    // 将灯光添加进场景
    scene.add(light)
    //#endregion
    //#region <相机>
    // 创建相机
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      70,
    )
    // 设置相机位置
    camera.position.set(0, 0, 30)

    scene.add(camera)
    //#endregion
    //#region <创建物体>
    // 位置间距
    let spacing = 200
    //#region <立方体>
    const cubeGeometry = new THREE.BoxGeometry(3, 3, 3)
    const cylinderGeometry = new THREE.CylinderGeometry(2, 2, 3, 32)
    const dodecahedronGeometry = new THREE.DodecahedronGeometry(2, 5)
    const material = new THREE.MeshBasicMaterial({
      wireframe: true,
    })

    /**
     * 创建无数个立方体
     * @param Geometry THREE.BufferGeometry
     * @param Material THREE.MeshBasicMaterial
     * @param translate  { x: number; y: number; z: number } = {
     *         x: 0,
     *         y: 0,
     *         z: 0,
     *       }
     * @returns {{meshArr: Mesh<BufferGeometry, MeshBasicMaterial>[], meshGroup: Group}}
     */
    function createMesh (
      Geometry,
      Material,
      translate = {
        x: 0,
        y: 0,
        z: 0,
      },
    ) {
      // 创建无数立方体
      let meshArr = []
      let meshGroup = new THREE.Group()
      for (let i = 0; i < 5; i++) {
        for (let j = 0; j < 5; j++) {
          for (let z = 0; z < 5; z++) {
            const cube = new THREE.Mesh(Geometry, Material)
            cube.position.set(
              i * 3 - 6 + (translate.x || 0),
              j * 3 - 6 + (translate.y || 0),
              z * 3 - 6 + (translate.z || 0),
            )
            meshGroup.add(cube)
            meshArr.push(cube)
          }
        }
      }
      scene.add(meshGroup)
      return {
        meshArr,
        meshGroup,
      }
    }

    let { meshArr: cubeArr, meshGroup: cubeGroup } = createMesh(
      cubeGeometry,
      material,
    )
    let { meshArr: cylinderArr, meshGroup: cylinderGroup } = createMesh(
      cylinderGeometry,
      material,
      {
        x: 0,
        y: -spacing,
        z: 0,
      },
    )
    let { meshArr: dodecahedronrArr, meshGroup: dodecahedronrGroup } = createMesh(
      dodecahedronGeometry,
      material,
      {
        x: 0,
        y: -2 * spacing,
        z: 0,
      },
    )
    let meshArr = [cubeArr, cylinderArr, dodecahedronrArr]
    let meshGroup = [cubeGroup, cylinderGroup, dodecahedronrGroup]
    //#endregion
    //#region <物体动画>
    meshGroup.forEach((item) => {
      gsap.to(item.rotation, {
        x: 0,
        y: Math.PI * 2,
        duration: 10,
        ease: 'linear',
        // yoyo: true,
        repeat: -1,
      })
    })

    //#endregion
    //#endregion
    //#region <渲染器>
    // 初始化渲染器
    // 渲染器透明
    const renderer = new THREE.WebGLRenderer({ alpha: true })
    // 设置渲染的尺寸大小
    renderer.setSize(window.innerWidth, window.innerHeight)
    // 开启场景中的阴影贴图
    renderer.shadowMap.enabled = true
    renderer.render(scene, camera)
    //#endregion
    //#region <坐标轴辅助器>
    // 创建坐标轴辅助器
    const axesHelper = new THREE.AxesHelper(5)
    // 将坐标轴辅助器添加到场景中
    scene.add(axesHelper)
    //#endregion
    //#region <轨道控制器>
    // 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement)
    // 设置控制器阻尼,让控制器更有弹性
    // 实现真实效果,必须在动画循环里的每一帧调用.update()。
    controls.enableDamping = true
    // 禁止缩放
    controls.enableZoom = false
    //#endregion
    //#region <渲染>
    // 渲染函数
    function render () {
      controls.update()
      renderer.render(scene, camera)
      //   渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render)
    }

    // 监听画面变化,更新渲染画面
    window.addEventListener('resize', () => {
      console.log('画面变化了')

      // 更新摄像头
      camera.aspect = window.innerWidth / window.innerHeight
      //   更新摄像机的投影矩阵
      camera.updateProjectionMatrix()

      //   更新渲染器
      renderer.setSize(window.innerWidth, window.innerHeight)
      //   设置渲染器的像素比
      renderer.setPixelRatio(window.devicePixelRatio)
    })
    //#endregion
    //#region <监听事件双击、滚动事件>
    // 双击事件
    window.addEventListener('dblclick', () => {
      const fullScreenElement = document.fullscreenElement
      if (!fullScreenElement) {
        //   双击控制屏幕进入全屏,退出全屏
        // 让画布对象全屏
        renderer.domElement.requestFullscreen()
      } else {
        //   退出全屏,使用document对象
        document.exitFullscreen()
      }
    })
    // 单击事件
    // 创建投射光线对象
    const raycaster = new THREE.Raycaster()
    // 鼠标的位置对象
    const mouse = new THREE.Vector2()
    // 监听鼠标的位置
    window.addEventListener('click', (event) => {
      // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      mouse.y = -((event.clientY / window.innerHeight) * 2 - 1)
      raycaster.setFromCamera(mouse, camera)
      meshArr.forEach((item) => {
        let intersects = raycaster.intersectObjects(item)
        console.log('intersects ', intersects)
        if (intersects.length === 0) return
          ;
        (intersects[0].object).material =
          new THREE.MeshBasicMaterial({
            // 粉色
            // 随机颜色
            color: getRandomColor(),
          })
      })
    })

    function getRandomColor () {
      const r = Math.floor(Math.random() * 256)
      const g = Math.floor(Math.random() * 256)
      const b = Math.floor(Math.random() * 256)

      const rr = ('0' + r.toString(16)).slice(-2)
      const gg = ('0' + g.toString(16)).slice(-2)
      const bb = ('0' + b.toString(16)).slice(-2)

      return '#' + rr + gg + bb
    }

    function eventListenerHandler (element) {
      // 滚动事件
      element.addEventListener('wheel', (e) => {
        // 页面可视高度
        const innerHeight = window.innerHeight
        // 判断e.deltaY大于0 页面向下滑动 位置坐标向上走
        // y轴移动距离
        let translateY = gsap.utils.clamp(
          -2 * window.innerHeight,
          0,
          +gsap.getProperty('.page', 'y') +
          (-e.deltaY / Math.abs(e.deltaY)) * innerHeight,
        )
        // 更新页面位置
        gsap.to('.page', {
          y: translateY,
          duration: 0.5,
        })
        // 更新物体坐标位置
        let meshTranslateY = gsap.utils.clamp(
          0,
          2 * spacing,
          -(translateY / innerHeight) * spacing,
        )
        meshGroup.forEach((item) => {
          gsap.to(item.position, {
            x: 0,
            y: meshTranslateY,
            z: 0,
            duration: 0.5,
          })
        })
        //  重新设置摄像机位置
        camera.position.set(0, 0, 30)
      })
    }

    //#endregion
    //#region <开始执行>
    // 将webgl渲染的canvas内容添加到body中
    let pageContainer = document.getElementById('page-container') || document.body
    pageContainer?.appendChild(renderer.domElement)
    eventListenerHandler(pageContainer)
    render()
    //#endregion
  }

</script>

二、效果

源代码可在git自取:原生html+css+js构建的一些有意思的模版示例 (github.com)