思路/实现步骤
-
- 创建canvas元素
-
- 获取canvas的上下文ctx
-
- 初始化点的信息(数量、初始坐标、移动方向、移动速度、大小、颜色)
-
- 绘制点
-
- 绘制点之间的连线
-
- 点有规律的动起来
-
- 动画循环
-
- 鼠标移动相关逻辑
-
- 点鼠标之间连线
-
- 鼠标吸附逻辑
-
- 添加配置项
-
- 重绘、重置
示例代码
<template>
<div class='random-dot-line-canvas'>
<div class="config">
<el-form :inline="true" :model="canvasConfig">
<el-form-item label="画布宽度" prop="canvasWidth">
<el-input-number v-model="canvasConfig.canvasWidth" :min="800" :max="1920" :step="50"></el-input-number>
</el-form-item>
<el-form-item label="画布高度" prop="canvasHeight">
<el-input-number v-model="canvasConfig.canvasHeight" :min="200" :max="900" :step="50"></el-input-number>
</el-form-item>
<el-form-item label="点数量" prop="DOT_NUM">
<el-input-number v-model="canvasConfig.DOT_NUM" :min="10" :max="200" :step="5"></el-input-number>
</el-form-item>
<el-form-item label="点半径" prop="DOT_RADIUS">
<el-input-number v-model="canvasConfig.DOT_RADIUS" :min="1" :max="20" :step="1"></el-input-number>
</el-form-item>
<el-form-item label="点颜色" prop="DOT_COLOR">
<el-color-picker v-model="canvasConfig.DOT_COLOR" />
</el-form-item>
<el-form-item label="线颜色" prop="LINE_COLOR">
<el-color-picker v-model="canvasConfig.LINE_COLOR" />
</el-form-item>
<el-form-item label="连线最大距离" prop="DOT_DRAW_LINE_MAX_DIST">
<el-input-number v-model="canvasConfig.DOT_DRAW_LINE_MAX_DIST" :min="50" :max="400" :step="20"></el-input-number>
</el-form-item>
<el-form-item label="点移动速度" prop="DOT_MOVE_SPEED">
<el-input-number v-model="canvasConfig.DOT_MOVE_SPEED" :min="1" :max="20" :step="1"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="redraw">重绘</el-button>
<el-button type="primary" @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<canvas
v-if="!initing"
id="random-dot-line-canvas"
:width="canvasWidth"
:height="canvasHeight"
style="background-color: black;"
color-space="srgb"
>
Your browser does not support the HTML5 canvas tag.
</canvas>
</div>
</template>
<script setup name='random-dot-line-canvas'>
import { ref, onMounted, nextTick, watch } from 'vue'
import store from '../../../../store';
const themeColor = ref(store.state.setting.themeColor || '#ffffff')
const ctx = ref()
const initing = ref(true)
const canvasWidth = ref(800)
const canvasHeight = ref(400)
const dotList = ref([])
const DOT_NUM = ref(40)
const DOT_RADIUS = ref(2.5)
const DOT_COLOR = ref(themeColor)
const LINE_COLOR = ref(themeColor)
const DOT_DRAW_LINE_MAX_DIST = ref(100)
const DOT_MOVE_SPEED = ref(1)
const canvasConfig = ref({
canvasWidth: canvasWidth.value,
canvasHeight: canvasHeight.value,
DOT_NUM: DOT_NUM.value,
DOT_RADIUS: DOT_RADIUS.value,
DOT_COLOR: DOT_COLOR.value,
LINE_COLOR: LINE_COLOR.value,
DOT_DRAW_LINE_MAX_DIST: DOT_DRAW_LINE_MAX_DIST.value,
DOT_MOVE_SPEED: DOT_MOVE_SPEED.value,
})
const animationNum = ref(null)
const animationMouseNum = ref(null)
const canvasPositionInPage = ref()
const mousePosition = ref({})
onMounted(()=>{
init()
})
watch(
() => mousePosition.value,
(newConfig, oldConfig = {}) => {
if(newConfig.inCanvas && !oldConfig.inCanvas) {
mouseAdsorb()
} else if (newConfig.inCanvas === false) {
cancelAnimationFrame(animationMouseNum.value)
}
},
{ immediate: true, deep: true }
)
const init = async() => {
initing.value = false
await nextTick()
let canvas = document.getElementById('random-dot-line-canvas')
canvasPositionInPage.value = canvas.getBoundingClientRect()
try {
ctx.value = canvas.getContext('2d')
} catch (error) {
console.log('canvas,ctx,getContext', error)
}
createdDot()
draw()
mouseMove()
dotMove()
}
const redraw = () => {
cancelAnimationFrame(animationNum.value)
initing.value = true
setNewConfigValue(canvasConfig.value)
init()
}
const reset = () => {
initing.value = true
setNewConfigValue({
canvasWidth: 800,
canvasHeight: 400,
DOT_NUM: 40,
DOT_RADIUS: 2.5,
DOT_DRAW_LINE_MAX_DIST: 100,
DOT_MOVE_SPEED: 1,
})
init()
}
const setNewConfigValue = (newConfig) => {
canvasWidth.value = newConfig.canvasWidth || canvasWidth.value
canvasHeight.value = newConfig.canvasHeight || canvasHeight.value
DOT_NUM.value = newConfig.DOT_NUM || DOT_NUM.value
DOT_RADIUS.value = newConfig.DOT_RADIUS || DOT_RADIUS.value
DOT_COLOR.value = newConfig.DOT_COLOR || DOT_COLOR.value
LINE_COLOR.value = newConfig.LINE_COLOR || LINE_COLOR.value
DOT_DRAW_LINE_MAX_DIST.value = newConfig.DOT_DRAW_LINE_MAX_DIST || DOT_DRAW_LINE_MAX_DIST.value
DOT_MOVE_SPEED.value = newConfig.DOT_MOVE_SPEED || DOT_MOVE_SPEED.value
}
const draw = () => {
ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
drawDot()
drawLine()
if(mousePosition.value.inCanvas) {
drawLineByMouse()
}
}
const nextDraw = () => {
dotMove()
}
const createdDot =() => {
dotList.value = []
for (let num = 1; num <= DOT_NUM.value; num++) {
const x = getRandomInteger(0, canvasWidth.value)
const y = getRandomInteger(0, canvasHeight.value)
const move_x = (+(Math.random() * 2 - 1).toFixed(2)) * DOT_MOVE_SPEED.value
const move_y = (+(Math.random() * 2 - 1).toFixed(2)) * DOT_MOVE_SPEED.value
dotList.value.push({x,y,move_x,move_y})
}
}
const drawDot = () => {
ctx.value.save();
ctx.value.translate(0.5, 0.5);
dotList.value.forEach((dot, index)=>{
ctx.value.fillStyle = index === 0 ? 'red' : DOT_COLOR.value || '#fff'
ctx.value.beginPath();
ctx.value.arc(dot.x, dot.y, DOT_RADIUS.value, 0, 2*Math.PI)
ctx.value.fill()
ctx.value.closePath();
})
ctx.value.restore();
}
const drawLine = () => {
ctx.value.save();
ctx.value.translate(0.5, 0.5);
const lineColorRgb = hexToRgb(LINE_COLOR.value)
dotList.value.forEach((dot1, index1)=>{
dotList.value.forEach((dot2, index2)=>{
const s = getDistanceByTwoDot(dot1, dot2)
if(index1 !== index2 && s <= DOT_DRAW_LINE_MAX_DIST.value) {
ctx.value.lineWidth = 1
ctx.value.strokeStyle = `rgba(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b}, ${((DOT_DRAW_LINE_MAX_DIST.value-s) / DOT_DRAW_LINE_MAX_DIST.value)})`
ctx.value.beginPath();
ctx.value.moveTo(dot1.x, dot1.y)
ctx.value.lineTo(dot2.x, dot2.y)
ctx.value.stroke()
ctx.value.closePath();
}
})
})
ctx.value.restore();
}
const dotMove = () => {
dotList.value.forEach((dot, index) => {
let nextX = dot.x
let nextY = dot.y
nextX = dot.x + dot.move_x
nextY = dot.y + dot.move_y
if(nextX > canvasWidth.value - DOT_RADIUS.value) {
nextX = canvasWidth.value - DOT_RADIUS.value
dot.move_x = -dot.move_x
} else if(nextX < DOT_RADIUS.value) {
nextX = DOT_RADIUS.value
dot.move_x = -dot.move_x
}
if(nextY > canvasHeight.value - DOT_RADIUS.value) {
nextY = canvasHeight.value - DOT_RADIUS.value
dot.move_y = -dot.move_y
} else if(nextY < DOT_RADIUS.value) {
nextY = DOT_RADIUS.value
dot.move_y = -dot.move_y
}
dot.x = nextX
dot.y = nextY
})
draw()
animationNum.value = requestAnimationFrame(dotMove)
}
const mouseMove = () => {
const canvas = document.getElementById('random-dot-line-canvas')
canvas.addEventListener('mousemove', (e) => {
mousePosition.value = {
inCanvas: true,
x: e.offsetX,
y: e.offsetY,
offsetX: e.offsetX,
offsetY: e.offsetY,
pageX: e.pageX,
pageY: e.pageY,
}
})
canvas.addEventListener('mouseleave', (e) => {
mousePosition.value.inCanvas = false
})
}
const mouseAdsorb = () => {
dotList.value.forEach((dot) => {
let nextX = dot.x
let nextY = dot.y
const attractiveRange = DOT_DRAW_LINE_MAX_DIST.value + 50
const adsorptionDistance = DOT_DRAW_LINE_MAX_DIST.value
const distance = getDistanceByTwoDot(mousePosition.value, dot)
if(distance < attractiveRange && distance > adsorptionDistance) {
dot.isAttractive = true
const { mouse_move_x, mouse_move_y } = getAdsorbSpeed(dot, mousePosition.value)
nextX += mouse_move_x
nextY += mouse_move_y
} else if(distance <= adsorptionDistance) {
dot.isAdsorption = true
dot.isAttractive = false
} else {
dot.isAttractive = false
dot.isAdsorption = false
}
dot.x = nextX
dot.y = nextY
})
draw()
animationMouseNum.value = requestAnimationFrame(mouseAdsorb)
}
const drawLineByMouse = () => {
ctx.value.save();
ctx.value.translate(0.5, 0.5);
const lineColorRgb = hexToRgb(LINE_COLOR.value)
dotList.value.forEach((dot)=>{
if(dot.isAdsorption || dot.isAttractive) {
ctx.value.lineWidth = 1
ctx.value.strokeStyle = `rgba(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b}, ${0.3})`
ctx.value.beginPath();
ctx.value.moveTo(dot.x, dot.y)
ctx.value.lineTo(mousePosition.value.x, mousePosition.value.y)
ctx.value.stroke()
ctx.value.closePath();
}
})
ctx.value.restore();
}
const getRandomInteger = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const getDistanceByTwoDot = (dot1, dot2) => {
const w = Math.abs(dot1.x - dot2.x)
const h = Math.abs(dot1.y - dot2.y)
const s = Math.sqrt(w**2 + h**2)
return s
}
function hexToRgb(hex) {
if(hex.includes('#')) {
hex = hex.replace('#', '')
}
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16) || 0;
return { r, g, b };
}
const getAdsorbSpeed = (dot, mouse) => {
const distanceX = mouse.x - dot.x
const distanceY = mouse.y - dot.y
const speed = DOT_MOVE_SPEED.value * 3
const mouse_move_x = +(speed * (distanceX < 0 ? -1 : 1)).toFixed(2)
const mouse_move_y = +(speed * (distanceY < 0 ? -1 : 1) * Math.abs((distanceY / distanceX))).toFixed(2)
return { mouse_move_x, mouse_move_y }
}
</script>
<style lang='scss' scoped>
#random-dot-line-canvas {
cursor: pointer;
}
</style>
效果

