Skip to content

3D 旋转 -1

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div id="rotate1Box">
      <canvas v-if="isRunning" id="rotate1" class="stage"></canvas>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)
const mouseInfo = ref({ x: 0, y: 0 })

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

function parseColor (color, toNumber) {
  if (toNumber === true) {
    if (typeof color === 'number') {
      return (color | 0) // chop off decimal
    }
    if (typeof color === 'string' && color[0] === '#') {
      color = color.slice(1)
    }
    return window.parseInt(color, 16)
  } else {
    if (typeof color === 'number') {
      color = '#' + ('00000' + (color | 0).toString(16)).substr(-6)
    }
    return color
  }
}

function Ball3d (radius, color) {
  if (radius === undefined) { radius = 40 }
  if (color === undefined) { color = '#00ff00' }
  this.x = 0
  this.y = 0
  this.xpos = 0
  this.ypos = 0
  this.zpos = 0
  this.vz = 0
  this.vx = 0
  this.vy = 0
  this.radius = radius
  this.rotation = 0
  this.mass = 1
  this.scaleX = 1
  this.scaleY = 1
  this.name = ""
  this.color = parseColor(color, false)
  this.lineWidth = 1
  this.visible = true
}

Ball3d.prototype.draw = function (context) {
  context.save()
  context.translate(this.x, this.y)
  context.rotate(this.rotation)
  context.scale(this.scaleX, this.scaleY)
  context.lineWidth = this.lineWidth
  context.fillStyle = this.color
  context.strokeStyle = this.color
  context.beginPath()
  context.arc(0, 0, this.radius, 0, Math.PI * 2, false)
  context.closePath()
  context.fill()
  context.stroke()
  context.restore()
}

const rotateMouseMove = (event) => {
  mouseInfo.value = {
    x: event.offsetX < 0 ? 0 : event.offsetX,
    y: event.offsetY < 0 ? 0 : event.offsetY   
  }
}

const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('rotate1')
  const ctx = canvas.getContext('2d')

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height

  const parent = document.getElementById('rotate1Box') as any
  parent.addEventListener('mousemove', rotateMouseMove)

  const balls: any = []
  const numBalls = 250
  const fl = 250
  const vpX = width / 2
  const vpY = height / 2
  let angleY // 绕Y轴旋转的角度
  let angleX // 绕X轴旋转的角度


  for (let i = 0; i < numBalls; i++) {
    const size = Math.random() * 10 + 5
    const color = Math.random() * (0xffffff)
    const ball = new Ball3d(size, color)
    
    ball.xpos = Math.random() * 300 - 150
    ball.ypos = Math.random() * 300 - 150
    ball.zpos = Math.random() * 300 - 150
    
    balls.push(ball)
  }

  const rotateY = (ball, angle) => {
    const cos = Math.cos(angle)
    const sin = Math.sin(angle)
    const x1 = ball.xpos * cos - ball.zpos * sin
    const z1 = ball.zpos * cos + ball.xpos * sin
    
    ball.xpos = x1
    ball.zpos = z1
  }

  const rotateX = (ball, angle) => {
    const cos = Math.cos(angle)
    const sin = Math.sin(angle)
    const y1 = ball.ypos * cos - ball.zpos * sin
    const z1 = ball.zpos * cos + ball.ypos * sin
    
    ball.ypos = y1
    ball.zpos = z1
  }

  const setPerspective = (ball) => {
    if (ball.zpos > -fl) {
      const scale = fl / (fl + ball.zpos)
      ball.scaleX = scale
      ball.scaleY = scale
      ball.x = vpX + ball.xpos * scale
      ball.y = vpY + ball.ypos * scale
      ball.visible = true
    } else {
      ball.visible = false
    }
  }

  const move = (ball) => {
    rotateY(ball, angleY)
    rotateX(ball, angleX)
    setPerspective(ball)
  }
  
  const zSort = (a, b) => {
    return (b.zpos - a.zpos)
  }
  
  const draw = (ball) => {
    if (ball.visible) {
      ball.draw(ctx)
    }
  }

  const runAnimate = () => {
    ctx.clearRect(0, 0, width, height)

    requestID.value = requestAnimationFrame(runAnimate)

    angleY = (mouseInfo.value.x - vpX) * 0.0001
    angleX = (mouseInfo.value.y - vpY) * 0.0001

    balls.forEach(move)
    balls.sort(zSort)
    balls.forEach(draw)
  }

  runAnimate()
}


const destroy = () => {
  cancelAnimationFrame(requestID.value)
  const parent = document.getElementById('rotate1Box') as any
  if (parent) parent.removeEventListener('mousemove', rotateMouseMove)
}

onUnmounted(() => {
  destroy()
})
</script>

3D 生成树 -1(由于递归原因,卡顿,后续使用 offsetCanvas + webWorker 优化)

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="tree1" class="stage bg-white"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)
const vz = ref(0)
const trees = ref<any>([]) // 树的数组
const treeNum = 5 // 树的数量
const vpX = 648 / 2 // 视点
const vpY = 360 / 2 // 视点
const floor = 200 // 地面高度
const fl = 250 // 焦距

const onTrigger = async () => {
  trees.value = []
  vz.value = 0
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

function ranColor () {
	return '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1,6)
}

function NatureTree (ctx) {
  this.ctx = ctx
  this.x = 0 // 位置
  this.y = 0 // 位置
  this.xpos = 0 // 位置
  this.ypos = 0 // 位置
  this.zpos = 0 // 位置
  this.scaleX = 0.85 // 缩放
  this.scaleY = 0.85 // 缩放
  this.alpha = 1 // 透明度
  this.spread = 0.6 // 树枝分叉状态
  this.drawLeaves = true // 是否绘制树叶
  this.leavesColor = ranColor() // 树叶颜色
  this.max_branch_width = 20 // 树枝最大宽度
  this.max_branch_height = 60 // 树枝最大高度

  this.small_leaves = 10 // 小叶子
  this.medium_leaves = 200 // 中叶子
  this.big_leaves = 500 // 大叶子
  this.thin_leaves = 900 // 细叶子
  
  this.leaveType = this.medium_leaves // 树叶类型
}

NatureTree.prototype.draw = function (spread, leaves, leaveType) {
  // 设置树杈分多少枝
  if (spread >= 0.3 && spread <= 1) {
    this.spread = spread
  } else {
    this.spread = 0.6
  }

  // 是否绘制树叶
  if (leaves === true || leaves === false) {
    this.drawLeaves = leaves
  } else {
    this.drawLeaves = true
  }

  if (leaveType === this.small_leaves ||
      leaveType === this.medium_leaves ||
      leaveType === this.big_leaves ||
      leaveType === this.thin_leaves) {
    this.leaveType = leaveType
  } else {
    this.leaveType = this.medium_leaves
  }

  this.ctx.save()
  this.ctx.lineWidth = 1 + Math.random() * this.max_branch_width
  this.ctx.lineCap = 'round'
  this.ctx.lineJoin = 'round'
  this.ctx.translate(this.x, this.y)
  this.ctx.scale(this.scaleX, this.scaleY)
  this.branchAndLeaves(0)
  this.ctx.restore()
}

// 当前递归的层级(代数)是gen,当gen小于12时,继续递归,画树枝,否则画树叶
// 使用 rotate 和 scale 方法模拟树枝的分支
NatureTree.prototype.branchAndLeaves = function (gen) {
  if (gen < 12) {
    this.ctx.save()

    this.ctx.beginPath()
    this.ctx.moveTo(0, 0)
    this.ctx.lineTo(0, -this.max_branch_height)
    this.ctx.stroke()

    this.ctx.translate(0, -this.max_branch_height)

    const randomN = -(Math.random() * 0.1) + 0.1
    this.ctx.rotate(randomN)

    // 画树枝
    if ((Math.random() * 1) < this.spread) {
      // 画左侧树枝
      this.ctx.rotate(-0.35)
      this.ctx.scale(0.7, 0.7)
      this.ctx.save()
      this.branchAndLeaves(gen + 1) // 递归绘制左侧分支
      this.ctx.restore()
    		
      // 画右侧树枝
      this.ctx.rotate(0.6)
      this.ctx.save()
      this.branchAndLeaves(gen + 1) // 递归绘制右侧分支
      this.ctx.restore()
    } else {
      this.branchAndLeaves(gen)
    }

    this.ctx.restore()

  } else {
    // 枝条画完画树叶
    if (this.drawLeaves) {
      let lengthFactor = 200
      if (this.leaveType === this.thin_leaves) {
        lengthFactor = 10
      }
      this.ctx.save()
      this.ctx.fillStyle = this.leavesColor
      this.ctx.fillRect(0, 0, this.leaveType, lengthFactor)
      this.ctx.restore()
    }
  }
}

const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('tree1')
  const ctx = canvas.getContext('2d')

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height

  for (let i = 0; i < treeNum; i++) {
    const tree = new NatureTree(ctx)
    tree.xpos = Math.random() * 1000 - 500
    tree.ypos = floor
    tree.zpos = Math.random() * 1000
    trees.value.push(tree)
  }

  const move = (tree) => {
    tree.zpos += vz.value
    if (tree.zpos < -fl) {
      tree.zpos += 1000
    }
    if (tree.zpos > 1000 - fl) {
      tree.zpos -= 1000
    }
    const scale = fl / (fl + tree.zpos)
    tree.scaleX =scale
    tree.scaleY = scale
    tree.x = vpX + tree.xpos * scale
    tree.y = vpY + tree.ypos * scale
    tree.alpha = scale
  }

  const zSort = (a, b) => {
    return (b.zpos - a.zpos)
  }
      
  const draw = (tree) => {
    tree.draw(0.6, true, tree.medium_leaves)
  }

  
  const runAnimate = () => {
    ctx.clearRect(0, 0, width, height)
    trees.value.forEach(move)
    trees.value.sort(zSort)
    trees.value.forEach(draw)

    // requestID.value = requestAnimationFrame(runAnimate)
  }
  runAnimate()
}


const destroy = () => {
  cancelAnimationFrame(requestID.value)
}

onUnmounted(() => {
  destroy()
})
</script>

3D 生成树 -2(w、a、s、d)

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="tree2" class="stage bg-black"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)
const eventListeners = ref<any>([])

const onTrigger = async () => {
  if (!isRunning.value) {
    eventListeners.value = []
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
    eventListeners.value = []
  }
}


function parseColor (color, toNumber) {
  if (toNumber === true) {
    if (typeof color === 'number') {
      return (color | 0) // chop off decimal
    }
    if (typeof color === 'string' && color[0] === '#') {
      color = color.slice(1)
    }
    return window.parseInt(color, 16)
  } else {
    if (typeof color === 'number') {
      color = '#' + ('00000' + (color | 0).toString(16)).substr(-6)
    }
    return color
  }
}

function colorToRGB (color, alpha) {
  // 如果是字符串格式,转换为数字
  if (typeof color === "string" && color[0] === "#") {
    // parseInt(('#ffffff').slice(1),16) 为 16777215
    color = window.parseInt(color.slice(1), 16)
  }
  alpha = (alpha === undefined) ? 1 : alpha
    
  // 将color转换成r,g,b值,>>右移  <<左移
  const r = color >> 16 & 0xff // 例如:16777215 >> 16 变成 255, 255 & 0xff为255
  const g = color >> 8 & 0xff
  const b = color & 0xff
  const a = (alpha < 0) ? 0 : ((alpha > 1) ? 1 : alpha)
    
  return `rgb(${r}, ${g}, ${b}, ${a})`
}

function Tree (color) {
  this.x = 0
  this.y = 0
  this.xpos = 0
  this.ypos = 0
  this.zpos = 0
  this.scaleX = 1
  this.scaleY = 1
  this.color = parseColor(color, false)
  this.alpha = 1
  this.lineWidth = 1
  this.branch = []
  
  // 生成一些随机分支位置
  this.branch[0] = -140 - Math.random() * 20
  this.branch[1] = -30 - Math.random() * 30
  this.branch[2] = Math.random() * 80 - 40
  this.branch[3] = -100 - Math.random() * 40
  this.branch[4] = -60 - Math.random() * 40
  this.branch[5] = Math.random() * 60 - 30
  this.branch[6] = -110 - Math.random() * 20
}

Tree.prototype.draw = function (context) {
  context.save()
  context.translate(this.x, this.y)
  context.scale(this.scaleX, this.scaleY)
  
  context.lineWidth = this.lineWidth
  context.strokeStyle = colorToRGB(this.color, this.alpha)
  context.beginPath()
  context.moveTo(0, 0)
  context.lineTo(0, this.branch[0])
  context.moveTo(0, this.branch[1])
  context.lineTo(this.branch[2], this.branch[3])
  context.moveTo(0, this.branch[4])
  context.lineTo(this.branch[5], this.branch[6])
  context.stroke()
  context.restore()
}


const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('tree2')
  const ctx = canvas.getContext('2d')

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height

  const trees: any = []
  const treeNum = 1000 // 树的数量
  const fl = 250 // 焦距
  const vpX = width / 2 // 视点中心x坐标
  const vpY = height / 2  // 视点中心y坐标
  const floor = 50 // 地板位置
  const gravity = 0.3 // 重力
  const friction = 0.98 // 摩擦力
  let ax = 0
  let ay = 0
  let az = 0
  let vx = 0
  let vy = 0
  let vz = 0

  for (let i = 0; i < treeNum; i++) {
    const color = Math.random() * (0xffffff)
    const tree = new Tree(color)
    tree.xpos = Math.random() * 2000 - 1000
    tree.ypos = floor
    tree.zpos = Math.random() * 10000
    trees.push(tree)
  }

  const move = (tree) => {
    tree.xpos += vx
    tree.ypos += vy
    tree.zpos += vz
      
    if (tree.ypos < floor) {
        tree.ypos = floor
    }
    
    if (tree.zpos < -fl) {
      tree.zpos += 10000
    }

    if (tree.zpos > 10000 - fl) {
      tree.zpos -= 10000
    }

    const scale = fl / (fl + tree.zpos)
    tree.scaleX = scale
    tree.scaleY = scale
    tree.x = vpX + tree.xpos * scale
    tree.y = vpY + tree.ypos * scale
    tree.alpha = scale
  }

  const zSort = (a, b) => {
    return (b.zpos - a.zpos)
  }
      
  const draw = (tree) => {
    tree.draw(ctx)
  }


  const onKeyDown = (event) => {
    console.log(event.keyCode)
    switch (event.keyCode) {
      case 87: // w
        az = -1
        break
      case 83: // s
        az = 1
        break
      case 65: // a
        ax = 1
        break
      case 68: // d
        ax = -1
        break
      case 32: // space
        ay = 1
        break
    }
  }

  const onKeyUp = (event) => {
    switch (event.keyCode) {
      case 87: // w
      case 83: // s
        az = 0
        break
      case 65: // a
      case 68: // d
        ax = 0
        break
      case 32: // space
        ay = 0
        break
    }
  }

  eventListeners.value.push(onKeyDown, onKeyUp)

  window.addEventListener('keydown', onKeyDown)
  window.addEventListener('keyup', onKeyUp)
  
  const runAnimate = () => {
    ctx.clearRect(0, 0, width, height)

    requestID.value = requestAnimationFrame(runAnimate)

    vx += ax
    vy += ay
    vz += az
    vy -= gravity
    trees.forEach(move)
    vx *= friction
    vy *= friction
    vz *= friction
    trees.sort(zSort)
    trees.forEach(draw)
  }
  runAnimate()
}


const destroy = () => {
  cancelAnimationFrame(requestID.value)
  eventListeners.value.forEach(listener => {
    window.removeEventListener('keydown', listener)
    window.removeEventListener('keyup', listener)
  })
}

onUnmounted(() => {
  destroy()
})
</script>