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() 方法中会创建很多种效果。

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】

在这里插入图片描述


  • 41
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
Vue3 + TypeScript 是一种常用的前端开发技术组合,它结合了Vue框架的便捷性和TypeScript的类型检查能力,可以提高项目的可维护性和开发效率。下面是一些Vue3 + TypeScript项目经验的介绍: 1. 项目初始化:使用Vue CLI创建一个Vue3项目,并选择TypeScript作为项目的语言。这样可以自动生成一些基本的配置和文件结构,方便快速开始开发。 2. 类型定义:在Vue3 + TypeScript项目中,可以使用TypeScript的类型系统来定义组件的props、data、computed等属性的类型,以及事件的参数类型等。这样可以在编码过程中及早发现潜在的类型错误,提高代码的健壮性。 3. 组件开发:在Vue3 + TypeScript项目中,可以使用装饰器(decorators)来定义组件选项,例如使用@Prop来定义props属性的类型,使用@Emit来定义事件的参数类型。这样可以更清晰地描述组件的接口和行为。 4. 路由管理:在Vue3 + TypeScript项目中,可以使用Vue Router进行路由管理。通过定义路由的类型和参数类型,可以在编码过程中获得更好的类型提示和错误检查。 5. 状态管理:在Vue3 + TypeScript项目中,可以使用Vuex进行状态管理。通过定义状态的类型和操作的参数类型,可以提高代码的可读性和可维护性。 6. 第三方库的使用:在Vue3 + TypeScript项目中,可以使用第三方库,并为其编写类型声明文件,以获得更好的类型检查和代码提示。例如,可以使用axios发送HTTP请求,并为其编写类型声明文件,以确保请求参数和响应数据的类型正确。 7. 单元测试:在Vue3 + TypeScript项目中,可以使用Jest等测试框架进行单元测试。通过编写类型安全的测试用例,可以提高代码的质量和可靠性。 8. 构建和部署:在Vue3 + TypeScript项目中,可以使用Webpack等构建工具进行项目的打包和优化。通过配置合适的TypeScript编译选项和Webpack插件,可以生成高效的生产环境代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端杂货铺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值