Vue3+TypeScript项目实战——打造雨雪交加的智慧城市

个人简介

👀个人主页: 前端杂货铺

开源项目: rich-vue3 (基于 Vue3 + TS + Pinia + Element Plus + Spring全家桶 + MySQL)

🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展

📃个人状态: 研发工程师,现效力于中国工业软件事业

🚀人生格言: 积跬步至千里,积小流成江海

🥇推荐学习:🍖开源 rich-vue3 🍍前端面试宝典 🍉Vue2 🍋Vue3 🍓Vue2/3项目实战 🥝Node.js实战 🍒Three.js

🌕个人推广:每篇文章最下方都有加入方式,旨在交流学习&资源分享,快加入进来吧

内容 参考链接
THREE.JS 专栏 Three.js 入门

文章目录

前言

大家好,这里是前端杂货铺。

这篇文章我们使用 Vue3+TypeScript+Three.js 等主流前端技术,打造 雨雪交加的智慧城市 项目。

three.js-雨雪交加的智慧城市

项目源码 => 请点击此处自行获取 [github] rich-vue3

如果此项目对你有些帮助,欢迎给个免费的 Star !!!(Thanks♪(・ω・)ノ)


项目概况

源码路径

该项目已被托管到 rich-vue3 中,具体源码在 rich-vue3 项目的 rich-vue3-webapp/src/views/city-three 路径。

文件结构与职责

下面是该项目涉及到文件的基本结构:

  • base/index.css:页面的基础样式
  • config/index.ts:存储项目中需要使用的颜色
  • effect/...: 各种效果及特效,包括 天空盒子、扩散半球、扩散圆、旋转四棱锥、飞线、文字、雷达、雨、路径运动、烟雾、雪、建筑物外围线条、透明墙等
  • enter/initCity.ts:初始化场景、 创建城市实例、监听浏览器变化、动画
  • enter/city.ts:城市类,加载城市模型、初始化各种效果、点击聚焦和滑动滑轮缩放
  • utils/index.ts:封装加载城市模型的方法
  • index.vue:基本 UI,初始化项目的入口

城市类代码如下,在 initEffect() 方法中会创建很多种效果。

javascript 复制代码
import { loadFBX } from "../utils"
import * as THREE from "three"
import * as TWEEN from "@tweenjs/tween.js"
import { SurroundLine } from "@/views/city-three/effect/surroundLine"
import { Background } from "@/views/city-three/effect/background"
import { Radar } from "../effect/radar"
import { Wall } from "../effect/wall"
import { Circle } from "@/views/city-three/effect/circle"
import { Ball } from "@/views/city-three/effect/ball"
import { Cone } from "@/views/city-three/effect/cone"
import { Fly } from "@/views/city-three/effect/fly"
import { Road } from "@/views/city-three/effect/road"
import { Font } from "@/views/city-three/effect/font"
import { Snow } from "@/views/city-three/effect/snow"
import { Rain } from "@/views/city-three/effect/rain"
import { Smoke } from "@/views/city-three/effect/smoke";

export class City {
  private readonly scene: any
  private readonly camera: any
  private readonly controls: any
  private tweenPosition: any
  private tweenRotation: any
  private flag: boolean
  private readonly height: { value: number }
  private readonly time: { value: number }
  private readonly top: { value: number }
  private readonly effect: {
    snow: any
    rain: any
    smoke: any
  }
  constructor(scene: object, camera: object, controls: any) {
    this.scene = scene
    this.camera = camera
    this.controls = controls
    this.flag = false
    this.tweenPosition = null
    this.tweenRotation = null

    this.height = {
      value: 5
    }

    this.time = {
      value: 0
    }

    this.top = {
      value: 0
    }

    // 雪、雨、烟雾
    this.effect = {
      snow: null,
      rain: null,
      smoke: null
    }

    this.loadCity()
  }

  loadCity() {
    // 加载城市模型,并且渲染到画布
    loadFBX("model/beijing.fbx").then((object: any) => {
      object.traverse((child: any) => {
        if (child.isMesh) {
          new SurroundLine(this.scene, child, this.height, this.time)
        }
      })
      this.initEffect()
    })
  }

  // 初始化效果,各个功能点都放在了这里
  initEffect() {
    new Background(this.scene)

    new Radar(this.scene, this.time)

    new Wall(this.scene, this.time)

    new Circle(this.scene, this.time)

    new Ball(this.scene, this.time)

    new Cone(this.scene, this.top, this.height)

    new Fly(this.scene, this.time)

    new Road(this.scene, this.time)

    new Font(this.scene)

    this.effect.snow = new Snow(this.scene)

    this.effect.rain = new Rain(this.scene)

    this.effect.smoke = new Smoke(this.scene)

    // 点击选择
    this.addClick()

    this.addWheel()
  }

  addClick() {
    let flag = true
    document.onmousedown = () => {
      flag = true
      document.onmousemove = () => {
        flag = false
      }
    }
    document.onmouseup = (event) => {
      if (flag) {
        this.clickEvent(event)
      }
      document.onmousemove = null
    }
  }

  // 场景跟随鼠标坐标缩放
  addWheel() {
    const body: HTMLElement = document.body
    // @ts-ignore
    body.onmousewheel = (event: MouseEvent) => {
      // 鼠标当前的坐标
      const x = (event.clientX / window.innerWidth) * 2 - 1
      const y = -(event.clientY / window.innerHeight) * 2 + 1

      const value = 30

      const vector = new THREE.Vector3(x, y, 0.5)
      vector.unproject(this.camera)
      vector.sub(this.camera.position).normalize()

      // @ts-ignore
      if (event.wheelDelta > 0) {
        this.camera.position.x += vector.x * value
        this.camera.position.y += vector.y * value
        this.camera.position.z += vector.z * value

        this.controls.target.x += vector.x * value
        this.controls.target.y += vector.y * value
        this.controls.target.z += vector.z * value
      } else {
        this.camera.position.x -= vector.x * value
        this.camera.position.y -= vector.y * value
        this.camera.position.z -= vector.z * value

        this.controls.target.x -= vector.x * value
        this.controls.target.y -= vector.y * value
        this.controls.target.z -= vector.z * value
      }
    }
  }

  // 点击聚焦
  clickEvent(event: MouseEvent) {
    // 归一化坐标(将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1))
    const x = (event.clientX / window.innerWidth) * 2 - 1
    const y = -(event.clientY / window.innerHeight) * 2 + 1

    // 创建设备坐标(三维)
    const standardVector = new THREE.Vector3(x, y, 0.5)
    // 转化为世界坐标 (将此向量 (坐标) 从相机的标准化设备坐标 (NDC) 空间投影到世界空间)
    const worldVector = standardVector.unproject(this.camera)
    // 做序列化
    const ray = worldVector.sub(this.camera.position).normalize()

    // 实现点击选中
    // 创建一个射线发射器,用来发射一条射线
    const raycaster = new THREE.Raycaster(this.camera.position, ray)
    // 返回射线碰撞到的物体
    const intersects = raycaster.intersectObjects(this.scene.children, true)

    let point3d = null
    if (intersects.length) {
      point3d = intersects[0]
    }

    if (point3d) {
      const proportion = 3
      // 开始动画修改观察点
      const time = 1000

      this.tweenPosition = new TWEEN.Tween(this.camera.position)
        .to({ x: point3d.point.x * proportion, y: point3d.point.y * proportion, z: point3d.point.y * proportion }, time)
        .start()

      this.tweenRotation = new TWEEN.Tween(this.camera.rotation)
        .to({ x: this.camera.rotation.x, y: this.camera.rotation.y, z: this.camera.rotation.z }, time)
        .start()
    }
  }

  start(delta: number) {
    for (const key in this.effect) {
      // @ts-ignore
      this.effect[key] && this.effect[key].animation()
    }

    if (this.tweenPosition && this.tweenRotation) {
      this.tweenPosition.update()
      this.tweenRotation.update()
    }

    this.height.value += 0.4
    if (this.height.value > 160) {
      this.height.value = 5
    }

    this.time.value += delta

    if (this.top.value > 15 || this.top.value < 0) {
      this.flag = !this.flag
    }

    this.top.value += this.flag ? -0.8 : 0.8
  }
}

写在最后

由于本项目涉及到的代码较多,在本篇文章中就不一一讲解了,感兴趣的同学可以去下载项目源码自行学习,有问题的话可以评论区一起讨论交流~

好啦,本篇文章到这里就要和大家说再见啦,祝你这篇文章阅读愉快,你下篇文章的阅读愉快留着我下篇文章再祝!


参考资料:

  1. Three.js 官方文档
  2. WebGL+Three.js 入门与实战【作者:慕课网_yancy】


相关推荐
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
Web阿成5 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
噢,我明白了5 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__5 小时前
APIs-day2
javascript·css·css3
关你西红柿子6 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根6 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.6 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia3117 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
m0_748256567 小时前
Vue - axios的使用
前端·javascript·vue.js
m0_748256347 小时前
QWebChannel实现与JS的交互
java·javascript·交互