Appearance
烟花
鼠标点击生成烟花
点击运行
<template>
<div id="fireWork-box">
<div>鼠标点击生成烟花</div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="fireWork" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const PI2 = Math.PI * 2
const random = (min, max) => Math.random() * (max - min + 1) + min | 0
const timestamp = () => new Date().getTime()
let box
let scene
class Scene {
fireworks: any[] = []
counter = 0
width = 0
height = 0
center = 0
spawnA = 0
spawnB = 0
spawnC = 0
spawnD = 0
ctx: any = null
constructor(el) {
const canvas = document.getElementById(el) as any
this.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
this.width = width
this.height = height
this.center = this.width / 2
this.spawnA = this.center - this.center / 4 | 0
this.spawnB = this.center + this.center / 4 | 0
this.spawnC = this.height * 0.1
this.spawnD = this.height * 0.5
}
onClick(e) {
const x = e.offsetX
const y = e.offsetY
const count = random(3, 5) // 生成多个烟花
for (let i = 0; i< count; i++) {
const obj = new FireWork(this.fireworks, this.ctx, random(this.spawnA, this.spawnB), this.height, x, y, random(0, 260), random(30, 110))
this.fireworks.push(obj)
this.counter = -1
}
}
update(delta) {
this.ctx.globalCompositeOperation = 'hard-light'
this.ctx.fillStyle = `rgba(20,20,20,${ 7 * delta })`
this.ctx.fillRect(0, 0, this.width, this.height)
this.ctx.globalCompositeOperation = 'lighter'
for (let firework of this.fireworks) firework.update(delta)
this.counter += delta * 3
if (this.counter >= 1) {
this.fireworks.push(
new FireWork(
this.fireworks,
this.ctx,
random(this.spawnA, this.spawnB),
this.height,
random(0, this.width),
random(this.spawnC, this.spawnD),
random(0, 360),
random(30, 110)
)
)
this.counter = 0
}
if (this.fireworks.length > 1000) this.fireworks = this.fireworks.filter(firework => !firework.dead)
}
}
class FireWork {
dead= false
madeChildren = false
x = 0
y = 0
targetX = 0
targetY = 0
shade = 0
offsprings = 0
history: any[] = []
fireworks: any[] = []
ctx:any = null
constructor(fireworks, ctx, x, y, targetX, targetY, shade, offsprings) {
this.fireworks = fireworks
this.ctx = ctx
this.dead = false
this.offsprings = offsprings
this.x = x
this.y = y
this.targetX = targetX
this.targetY = targetY
this.shade = shade
this.history = []
}
update(delta) {
if (this.dead) return
const xDiff = this.targetX - this.x
const yDiff = this.targetY - this.y
if (Math.abs(xDiff) > 3 || Math.abs(yDiff) > 3) {
this.x += xDiff * 2 * delta
this.y += yDiff * 2 * delta
this.history.push({
x: this.x,
y: this.y
})
if (this.history.length > 20) {
this.history.shift()
}
} else {
if (this.offsprings && !this.madeChildren) {
// 生成子烟花
const babies = this.offsprings / 2
for (let i = 0; i < babies; i++) {
const targetX = this.x + this.offsprings * Math.cos(PI2 * i / babies) | 0
const targetY = this.y + this.offsprings * Math.sin(PI2 * i / babies) | 0
this.fireworks.push(new FireWork(this.fireworks, this.ctx, this.x, this.y, targetX, targetY, this.shade, 0))
}
}
this.madeChildren = true
this.history.shift()
}
if (!this.history.length) {
this.dead = true
} else if (this.offsprings) {
for (let i = 0; this.history.length > i; i++) {
const point = this.history[i]
this.ctx.beginPath()
this.ctx.fillStyle = 'hsl(' + this.shade + ',100%,' + i + '%)'
this.ctx.arc(point.x, point.y, 1, 0, PI2, false)
this.ctx.fill()
}
} else {
this.ctx.beginPath()
this.ctx.fillStyle = 'hsl(' + this.shade + ',100%,50%)'
this.ctx.arc(this.x, this.y, 1, 0, PI2, false)
this.ctx.fill()
}
}
}
const onAddEvent = (evt) => {
scene.onClick(evt)
}
const onRunning = async () => {
await nextTick()
box = document.getElementById('fireWork-box') as any
let then = timestamp()
scene = new Scene('fireWork')
box.addEventListener('click', onAddEvent)
function loop() {
const now = timestamp()
const delta = now - then
then = now
// 不加这个if,可能会导致烟花回流
if (delta < 50) {
scene.update(delta / 1000)
}
requestID.value = window.requestAnimationFrame(loop)
}
loop()
}
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
}
}
const destroy = () => {
box?.removeEventListener('click', onAddEvent)
cancelAnimationFrame(requestID.value)
scene = null
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
傅里叶轨迹
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="fourier" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
const controls = ref(4)
const requestID = ref<any>()
const isRunning = ref(false)
let canvas
let ctx
let running
let draw
const onRunning = async () => {
await nextTick()
canvas = document.getElementById('fourier')
ctx = canvas.getContext('2d')
canvas?.addEventListener('click', onAddEvent)
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 arr: any = []
const colorArr = ['#00bcd4', '#dd5866', '#335678', '#28a745']
let time = 0
const createDisc = (r, color, x?, y?) => {
ctx.beginPath()
ctx.arc(x || 0, y || 0, r, 0, Math.PI * 2, true)
ctx.strokeStyle = color || '#00bcd4'
ctx.stroke()
}
const createLine = (d, rotate) => {
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(d, 0)
ctx.stroke()
}
draw = () => {
time -= Math.PI / 180
// 设置canvas大小
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制左侧的圆
ctx.save() // 将第一个状态推入到栈中
ctx.translate(150, 150)
let lastR = 50;
for (let i = 0; i < controls.value; i++) {
if (i === 0) {
ctx.rotate(time)
} else {
ctx.translate(50 - (i - 1) * 15, 0)
ctx.rotate(time * 2)
lastR = 50 - i * 15
}
createDisc(50 - i * 15, colorArr[i])
createLine(50 - i * 15, 2 * time)
}
// 绘制圆上的线
ctx.translate(lastR, 0)
ctx.rotate(-(controls.value * 2 - 1) * time)
ctx.beginPath()
ctx.moveTo(0, 0)
let x = 150
for (let i = 0; i < controls.value; i++) {
x -= Math.cos((i * 2 + 1) * time) * (50 - i * 15)
}
ctx.lineTo(x, 0)
ctx.stroke()
// 绘制箭头
ctx.translate(x, 0)
ctx.save() // 将第二个状态推入到栈中
ctx.rotate(-2.5)
ctx.lineTo(5, 0)
ctx.stroke()
ctx.restore() // 取出堆栈 (第二个状态)
ctx.save() // 将第三个状态推入到栈中
ctx.moveTo(0, 0)
ctx.rotate(2.5)
ctx.lineTo(5, 0)
ctx.stroke()
ctx.restore() // 取出堆栈 (第三个状态)
// 记录频域
let arrY = 150
for (let i = 0; i < controls.value; i++) {
arrY += Math.sin((i * 2 + 1) * time) * (50 - i * 15)
}
arr.unshift(arrY)
if (arr.length > 500) arr.pop()
ctx.restore() // 取出堆栈 (第一个状态)
ctx.strokeStyle = 'red'
ctx.beginPath()
ctx.moveTo(300, arrY)
let arrX = 0;
arr.forEach(v => {
ctx.lineTo(300 + arrX++, v)
})
ctx.stroke()
requestID.value = window.requestAnimationFrame(draw)
}
draw()
}
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
}
}
const onAddEvent = () => {
if (isRunning.value) {
running = !running
running ? cancelAnimationFrame(requestID.value) : requestID.value = requestAnimationFrame(draw)
}
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
canvas?.removeEventListener('click', onAddEvent)
if (draw) draw = null
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
太阳系
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="solarSystem" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const onRunning = async () => {
const canvas: any = document.getElementById('solarSystem')
const ctx = canvas.getContext('2d')
const wrapDomStyle = getComputedStyle(canvas)
const width = parseInt(wrapDomStyle.width, 10)
const height = parseInt(wrapDomStyle.height, 10)
// 需要设置canvas的width和height
canvas.width = width
canvas.height = height
const drawLine = () => {
for (let i = 0; i < 8; i++) {
ctx.beginPath()
ctx.arc(width / 2, height / 2, (i + 1) * 20, 0, 360, false)
ctx.closePath()
ctx.strokeStyle = '#00f77c'
ctx.stroke()
}
}
class Star {
ctx
x
y
radius
cycle
sColor
eColor
color
time
constructor(ctx, x, y, radius, cycle, sColor, eColor) {
this.ctx = ctx
this.x = x
this.y = y
this.radius = radius
this.cycle = cycle
this.sColor = sColor
this.eColor = eColor
this.color = null
//设置一个计时器
this.time = 0
}
draw() {
this.ctx.save() // 保存之前的内容
this.ctx.translate(width / 2, height / 2) // 重置0,0坐标
this.ctx.rotate(this.time * (360 / this.cycle) * Math.PI / 180) //旋转角度
// 画星球
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 360, false)
this.ctx.closePath()
// //设置星球的填充颜色
this.color = this.ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius)
this.color.addColorStop(0, this.sColor)
this.color.addColorStop(1, this.eColor)
this.ctx.fillStyle = this.color
this.ctx.fill()
//恢复之前画布的内容
this.ctx.restore()
this.time += 1
}
}
const sun = new Star(ctx, 0, 0, 10, 0, '#FFFF00', '#FF9900')
const mercury = new Star(ctx, 0, -20, 5, 87.70, '#A69697', '#5C3E40')
const venus = new Star(ctx, 0, -40, 5, 224.701, '#C4BBAC', '#1F1315')
const earth = new Star(ctx, 0, -60, 5, 365.2422, '#78B1E8', '#050C12')
const mars = new Star(ctx, 0, -80, 5, 686.98, '#CEC9B6', '#76422D')
const jupiter = new Star(ctx, 0, -100, 5, 4332.589, '#C0A48E', '#322222')
const saturn = new Star(ctx, 0, -120, 5, 10759.5, '#F7F9E3', '#5C4533')
const uranus = new Star(ctx, 0, -140, 5, 30799.095, '#A7E1E5', '#19243A')
const neptune = new Star(ctx, 0, -160, 5, 60152, '#0661B2', '#1E3B73')
const runAnimate = () => {
ctx.clearRect(0, 0, width, height)
drawLine()
sun.draw()
mercury.draw()
venus.draw()
earth.draw()
mars.draw()
jupiter.draw()
saturn.draw()
uranus.draw()
neptune.draw()
requestID.value = requestAnimationFrame(runAnimate)
}
runAnimate()
}
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
}
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
自动移动
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="autoMove1" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const onRunning = async() => {
await nextTick()
const canvas: any = document.getElementById('autoMove1')
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 start = { x: 50, y: 50 }
const middle = { x: 20, y: 360 }
const end = { x: 600, y: 50 }
let prevPos
let nextPos
let add = 0
// 获取二次贝塞尔曲线上的x和y
// 贝塞尔曲线方程B(t)=(1−t)^2 * P0 + 2 * t * (1−t) * P1 + t^2 * P2
const getBezierPoints = (p0, p1, p2, pointNum) => {
const points: any = []
for (let i = 0; i < pointNum; i++) {
const t = i / pointNum
const x = (1 - t) ** 2 * p0.x + 2 * t * (1 - t) * p1.x + t ** 2 * p2.x
const y = (1 - t) ** 2 * p0.y + 2 * t * (1 - t) * p1.y + t ** 2 * p2.y
points.push({ x, y })
}
return points
}
// 计算两点之间的角度
const calculateAngle = (x1, y1, x2, y2) => {
const dx = x2 - x1
const dy = y2 - y1
const angle = Math.atan2(dy, dx)
return angle
}
const drawWay = (p0, p1, p2) => {
ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y)
ctx.strokeStyle = 'blue'
ctx.lineWidth = 2
ctx.stroke()
}
const drawLine = (arr) => {
const start = arr.shift()
const end = arr.pop()
ctx.beginPath()
ctx.moveTo(start.x + 10, start.y + 10)
for (let i=0;i<arr.length;i++){
ctx.lineTo(arr[i].x + 10, arr[i].y + 10)
}
ctx.lineTo(end.x + 10, end.y + 10)
ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()
}
const drawCar = (prevPos, nextPos) => {
// 保存当前的绘图状态
ctx.save()
// 移动绘图原点到矩形的中心
ctx.translate(nextPos.x, nextPos.y)
if(prevPos) {
// 旋转绘图上下文
const angle = calculateAngle(prevPos.x, prevPos.y, nextPos.x, nextPos.y)
ctx.rotate(angle)
}
// 移动回矩形左上角的位置
ctx.translate(-nextPos.x, -nextPos.y)
ctx.beginPath()
ctx.rect(nextPos.x - 10, nextPos.y - 5, 20, 10)
ctx.fillStyle = 'orange'
ctx.fill()
// 恢复之前的绘图状态
ctx.restore()
}
const dealPoints = (pointList) => {
const newPointList: any = []
for (let i = 0; i < pointList.length; i += 10) {
newPointList.push(pointList[i])
}
return newPointList
}
const clearCanvas = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
const pointList = getBezierPoints(start, middle, end, 300)
drawLine([...pointList])
drawWay(start, middle, end)
const newPointList = dealPoints(pointList)
const firstPoint = newPointList.shift()
nextPos = firstPoint
requestID.value = setInterval(() => {
clearCanvas()
drawLine([...pointList])
drawWay(start, middle, end)
if(add >= newPointList.length) {
add = 0
prevPos = null
nextPos = start
}
drawCar(prevPos, nextPos)
prevPos = nextPos
nextPos = newPointList[add]
add += 1
}, 1000 / 4)
}
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
}
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>
金属字体
点击运行
<template>
<div>
<div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
<canvas v-if="isRunning" id="heavyMetal" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'
const requestID = ref<any>()
const isRunning = ref(false)
const onRunning = async() => {
await nextTick()
const canvas: any = document.getElementById('heavyMetal')
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 text = '天元突破'
const fontSize = 120
ctx.textAlign = 'left'
ctx.textBaseline = 'top'
ctx.font = 'italic bold 120px arial'
const widthText = ctx.measureText(text).width
ctx.translate(width / 2 - widthText / 2, height / 2 - fontSize / 2)
ctx.save()
ctx.miterLimit = 4
ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'
ctx.shadowOffsetY = 5
ctx.shadowBlur = 3
const gradient1 = ctx.createLinearGradient(0, 0, 0, fontSize)
gradient1.addColorStop(0, '#999')
gradient1.addColorStop(0.2, '#333')
gradient1.addColorStop(1, '#696970')
ctx.strokeStyle = gradient1
ctx.lineWidth = 10
ctx.strokeText(text, 0, 0)
ctx.restore()
ctx.save()
ctx.lineJoin = 'bevel'
const gradient2 = ctx.createLinearGradient(0, 0, 0, fontSize)
gradient2.addColorStop(0, '#868487')
gradient2.addColorStop(0.3, '#3d3b3e')
gradient2.addColorStop(1, '#fff')
ctx.strokeStyle = gradient2
ctx.lineWidth = 5
ctx.strokeText(text, 0, 0)
ctx.restore()
ctx.save()
const gradient3 = ctx.createLinearGradient(0, 0, 0, fontSize)
gradient3.addColorStop(0, '#9d9d9d')
gradient3.addColorStop(0.3, '#ffffff')
gradient3.addColorStop(0.6, '#3e3f41')
gradient3.addColorStop(1, '#67686b')
ctx.fillStyle = gradient3
ctx.fillText(text, 0, 0)
ctx.restore()
}
const onTrigger = async () => {
if (!isRunning.value) {
isRunning.value = true
await nextTick()
onRunning()
} else {
isRunning.value = false
destroy()
}
}
const destroy = () => {
cancelAnimationFrame(requestID.value)
}
onMounted(async() => {
await nextTick()
})
onUnmounted(() => {
destroy()
})
</script>