Appearance
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>