canvas动画:点随机活动 距离内主动毗连成线 鼠标移动主动吸附附近的点

[复制链接]
发表于 2025-9-19 05:03:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
思绪/实现步调



    • 创建canvas元素


    • 获取canvas的上下文ctx


    • 初始化点的信息(数目、初始坐标、移动方向、移动速率、巨细、颜色)


    • 绘制点


    • 绘制点之间的连线


    • 点有规律的动起来


    • 动画循环


    • 鼠标移动相干逻辑


    • 点鼠标之间连线


    • 鼠标吸附逻辑



    • 重绘、重置

示例代码
  1. <template>
  2.         <div class='random-dot-line-canvas'>
  3.                 <!-- <div style="margin-bottom: 20px;"><el-button @click="nextDraw">下一帧</el-button></div> -->
  4.                 <div class="config">
  5.                         <el-form :inline="true" :model="canvasConfig">
  6.                                 <el-form-item label="画布宽度" prop="canvasWidth">
  7.                                         <el-input-number v-model="canvasConfig.canvasWidth" :min="800" :max="1920" :step="50"></el-input-number>
  8.                                 </el-form-item>
  9.                                 <el-form-item label="画布高度" prop="canvasHeight">
  10.                                         <el-input-number v-model="canvasConfig.canvasHeight" :min="200" :max="900" :step="50"></el-input-number>
  11.                                 </el-form-item>
  12.                                 <el-form-item label="点数量" prop="DOT_NUM">
  13.                                         <el-input-number v-model="canvasConfig.DOT_NUM" :min="10" :max="200" :step="5"></el-input-number>
  14.                                 </el-form-item>
  15.                                 <el-form-item label="点半径" prop="DOT_RADIUS">
  16.                                         <el-input-number v-model="canvasConfig.DOT_RADIUS" :min="1" :max="20" :step="1"></el-input-number>
  17.                                 </el-form-item>
  18.                                 <el-form-item label="点颜色" prop="DOT_COLOR">
  19.                                         <el-color-picker v-model="canvasConfig.DOT_COLOR" />
  20.                                 </el-form-item>
  21.                                 <el-form-item label="线颜色" prop="LINE_COLOR">
  22.                                         <el-color-picker v-model="canvasConfig.LINE_COLOR" />
  23.                                 </el-form-item>
  24.                                 <el-form-item label="连线最大距离" prop="DOT_DRAW_LINE_MAX_DIST">
  25.                                         <el-input-number v-model="canvasConfig.DOT_DRAW_LINE_MAX_DIST" :min="50" :max="400" :step="20"></el-input-number>
  26.                                 </el-form-item>
  27.                                 <el-form-item label="点移动速度" prop="DOT_MOVE_SPEED">
  28.                                         <el-input-number v-model="canvasConfig.DOT_MOVE_SPEED" :min="1" :max="20" :step="1"></el-input-number>
  29.                                 </el-form-item>
  30.                                 <el-form-item>
  31.                                         <el-button type="primary" @click="redraw">重绘</el-button>
  32.                                         <el-button type="primary" @click="reset">重置</el-button>
  33.                                 </el-form-item>
  34.                         </el-form>
  35.                 </div>
  36.                 <canvas
  37.                         v-if="!initing"
  38.                         id="random-dot-line-canvas"
  39.       :width="canvasWidth"
  40.       :height="canvasHeight"
  41.                         style="background-color: black;"
  42.                         color-space="srgb"
  43.                 >
  44.       Your browser does not support the HTML5 canvas tag.
  45.                 </canvas>
  46.         </div>
  47. </template>
  48. <script setup name='random-dot-line-canvas'>
  49. import { ref, onMounted, nextTick, watch } from 'vue'
  50. import store from '../../../../store';
  51. const themeColor = ref(store.state.setting.themeColor || '#ffffff') // 主题色
  52. const ctx = ref()
  53. const initing = ref(true)
  54. const canvasWidth = ref(800) // 画布宽度
  55. const canvasHeight = ref(400) // 画布高度
  56. const dotList = ref([]) // 点的坐标、移动方向/移动速度
  57. const DOT_NUM = ref(40) // 点的数量
  58. const DOT_RADIUS = ref(2.5) // 点的半径
  59. const DOT_COLOR = ref(themeColor) // 点的颜色
  60. const LINE_COLOR = ref(themeColor) // 连线颜色
  61. const DOT_DRAW_LINE_MAX_DIST = ref(100) // 两点之间要连线的最大距离
  62. const DOT_MOVE_SPEED = ref(1) // 点移动速度
  63. const canvasConfig = ref({ // 表单配置项
  64.         canvasWidth: canvasWidth.value,
  65.         canvasHeight: canvasHeight.value,
  66.         DOT_NUM: DOT_NUM.value,
  67.         DOT_RADIUS: DOT_RADIUS.value,
  68.         DOT_COLOR: DOT_COLOR.value,
  69.         LINE_COLOR: LINE_COLOR.value,
  70.         DOT_DRAW_LINE_MAX_DIST: DOT_DRAW_LINE_MAX_DIST.value,
  71.         DOT_MOVE_SPEED: DOT_MOVE_SPEED.value,
  72. })
  73. const animationNum = ref(null)
  74. const animationMouseNum = ref(null)
  75. const canvasPositionInPage = ref() // 画布位置
  76. const mousePosition = ref({}) // 鼠标位置
  77. onMounted(()=>{
  78.         init()
  79. })
  80. watch(
  81.         () => mousePosition.value,
  82.         (newConfig, oldConfig = {}) => {
  83.                 if(newConfig.inCanvas && !oldConfig.inCanvas) {
  84.                         // console.log('移入画布')
  85.                         mouseAdsorb()
  86.                 } else if (newConfig.inCanvas === false) {
  87.                         // console.log('移出画布')
  88.                         cancelAnimationFrame(animationMouseNum.value)
  89.                 }
  90.         },
  91.         { immediate: true, deep: true }
  92. )
  93. /** 初始化函数 */
  94. const init = async() => {
  95.         initing.value = false
  96.         await nextTick()
  97.         let canvas = document.getElementById('random-dot-line-canvas')
  98.         canvasPositionInPage.value = canvas.getBoundingClientRect()
  99.         // {"x": 40, "y": 183, "width": 800, "height": 400, "top": 183, "right": 840, "bottom": 583, "left": 40 }
  100.         try {
  101.                 ctx.value = canvas.getContext('2d')
  102.         } catch (error) {
  103.                 console.log('canvas,ctx,getContext', error)
  104.         }
  105.         createdDot() // 创建点坐标
  106.         draw() // 绘制
  107.         mouseMove() // 鼠标移动相关逻辑
  108.         dotMove() // 点动起来
  109. }
  110. /** 重绘 */
  111. const redraw = () => {
  112.         cancelAnimationFrame(animationNum.value)
  113.         initing.value = true
  114.         setNewConfigValue(canvasConfig.value)
  115.         init()
  116. }
  117. /** 重置 */
  118. const reset = () => {
  119.         initing.value = true
  120.         setNewConfigValue({
  121.                 canvasWidth: 800,
  122.                 canvasHeight: 400,
  123.                 DOT_NUM: 40,
  124.                 DOT_RADIUS: 2.5,
  125.                 DOT_DRAW_LINE_MAX_DIST: 100,
  126.                 DOT_MOVE_SPEED: 1,
  127.         })
  128.         init()
  129. }
  130. /** 设置配置项值 */
  131. const setNewConfigValue = (newConfig) => {
  132.         canvasWidth.value = newConfig.canvasWidth || canvasWidth.value
  133.         canvasHeight.value = newConfig.canvasHeight || canvasHeight.value
  134.         DOT_NUM.value = newConfig.DOT_NUM || DOT_NUM.value
  135.         DOT_RADIUS.value = newConfig.DOT_RADIUS || DOT_RADIUS.value
  136.         DOT_COLOR.value = newConfig.DOT_COLOR || DOT_COLOR.value
  137.         LINE_COLOR.value = newConfig.LINE_COLOR || LINE_COLOR.value
  138.         DOT_DRAW_LINE_MAX_DIST.value = newConfig.DOT_DRAW_LINE_MAX_DIST || DOT_DRAW_LINE_MAX_DIST.value
  139.         DOT_MOVE_SPEED.value = newConfig.DOT_MOVE_SPEED || DOT_MOVE_SPEED.value
  140. }
  141. /** 绘制函数 */
  142. const draw = () => {
  143.         ctx.value.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
  144.         drawDot() // 绘制点
  145.         drawLine() // 连线
  146.         if(mousePosition.value.inCanvas) { // 鼠标在画布内 执行吸附逻辑
  147.                 drawLineByMouse() // 连线到鼠标
  148.         }
  149. }
  150. /** 下一帧-调试用 */
  151. const nextDraw = () => {
  152.         dotMove()
  153. }
  154. /** 创建点坐标 */
  155. const createdDot =() => {
  156.         dotList.value = []
  157.         for (let num = 1; num <= DOT_NUM.value; num++) {
  158.                         const x = getRandomInteger(0, canvasWidth.value)
  159.                         const y = getRandomInteger(0, canvasHeight.value)
  160.                         const move_x = (+(Math.random() * 2 - 1).toFixed(2)) * DOT_MOVE_SPEED.value // 移动x轴的速度
  161.                         const move_y = (+(Math.random() * 2 - 1).toFixed(2)) * DOT_MOVE_SPEED.value // 移动y轴的速度
  162.                         dotList.value.push({x,y,move_x,move_y})
  163.         }
  164. }
  165. /** 绘制点 */
  166. const drawDot = () => {
  167.         ctx.value.save();
  168.         ctx.value.translate(0.5, 0.5);
  169.         dotList.value.forEach((dot, index)=>{
  170.                 ctx.value.fillStyle = index === 0 ? 'red' : DOT_COLOR.value || '#fff'
  171.                 ctx.value.beginPath();
  172.                 ctx.value.arc(dot.x, dot.y, DOT_RADIUS.value, 0, 2*Math.PI)
  173.                 ctx.value.fill()
  174.                 ctx.value.closePath();
  175.         })
  176.         ctx.value.restore();
  177. }
  178. /** 点之间连线 */
  179. const drawLine = () => {
  180.         ctx.value.save();
  181.         ctx.value.translate(0.5, 0.5);
  182.         const lineColorRgb = hexToRgb(LINE_COLOR.value)
  183.         dotList.value.forEach((dot1, index1)=>{
  184.                 dotList.value.forEach((dot2, index2)=>{
  185.                         const s = getDistanceByTwoDot(dot1, dot2) // 两点之间的距离
  186.                         if(index1 !== index2 && s <= DOT_DRAW_LINE_MAX_DIST.value) {
  187.                                 ctx.value.lineWidth = 1
  188.                                 ctx.value.strokeStyle = `rgba(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b}, ${((DOT_DRAW_LINE_MAX_DIST.value-s) / DOT_DRAW_LINE_MAX_DIST.value)})`
  189.                                 ctx.value.beginPath();
  190.                                 ctx.value.moveTo(dot1.x, dot1.y)
  191.                                 ctx.value.lineTo(dot2.x, dot2.y)
  192.                                 ctx.value.stroke()
  193.                                 ctx.value.closePath();
  194.                         }
  195.                 })
  196.         })
  197.         ctx.value.restore();
  198. }
  199. /** 点有规律的动起来 */
  200. const dotMove = () => {
  201.         dotList.value.forEach((dot, index) => {
  202.                 let nextX = dot.x
  203.                 let nextY = dot.y
  204.                 nextX = dot.x + dot.move_x
  205.                 nextY = dot.y + dot.move_y
  206.                 if(nextX > canvasWidth.value - DOT_RADIUS.value) {
  207.                         nextX = canvasWidth.value - DOT_RADIUS.value
  208.                         dot.move_x = -dot.move_x
  209.                 } else if(nextX < DOT_RADIUS.value) {
  210.                         nextX = DOT_RADIUS.value
  211.                         dot.move_x = -dot.move_x
  212.                 }
  213.                 if(nextY > canvasHeight.value - DOT_RADIUS.value) {
  214.                         nextY = canvasHeight.value - DOT_RADIUS.value
  215.                         dot.move_y = -dot.move_y
  216.                 } else if(nextY < DOT_RADIUS.value) {
  217.                         nextY = DOT_RADIUS.value
  218.                         dot.move_y = -dot.move_y
  219.                 }
  220.                 dot.x = nextX
  221.                 dot.y = nextY
  222.         })
  223.         draw() // 绘制
  224.         animationNum.value = requestAnimationFrame(dotMove)
  225. }
  226. /** 鼠标移动相关逻辑 */
  227. const mouseMove = () => {
  228.         const canvas = document.getElementById('random-dot-line-canvas')
  229.         canvas.addEventListener('mousemove', (e) => {
  230.                 mousePosition.value = {
  231.                         inCanvas: true,
  232.                         x: e.offsetX,
  233.                         y: e.offsetY,
  234.                         offsetX: e.offsetX, // 相对于canvas
  235.                         offsetY: e.offsetY,
  236.                         pageX: e.pageX, // 相对于页面
  237.                         pageY: e.pageY,
  238.                 }
  239.         })
  240.         canvas.addEventListener('mouseleave', (e) => {
  241.                 mousePosition.value.inCanvas = false
  242.         })
  243. }
  244. /** 点移动兼容鼠标位置的戏份逻辑 */
  245. const mouseAdsorb = () => {
  246.         dotList.value.forEach((dot) => {
  247.                 let nextX = dot.x
  248.                 let nextY = dot.y
  249.                 const attractiveRange = DOT_DRAW_LINE_MAX_DIST.value + 50 // 吸引力作用范围
  250.                 const adsorptionDistance = DOT_DRAW_LINE_MAX_DIST.value // 吸附距离
  251.                 const distance = getDistanceByTwoDot(mousePosition.value, dot) // 鼠标和点的距离
  252.                 if(distance < attractiveRange && distance > adsorptionDistance) {
  253.                         dot.isAttractive = true // 点正在被鼠标吸引
  254.                         const { mouse_move_x, mouse_move_y } = getAdsorbSpeed(dot, mousePosition.value) // 计算鼠标吸附点xy方向上的速度
  255.                         nextX += mouse_move_x
  256.                         nextY += mouse_move_y
  257.                 } else if(distance <= adsorptionDistance) { // 吸附点
  258.                         dot.isAdsorption = true // 点已经被吸附
  259.                         dot.isAttractive = false
  260.                 } else {
  261.                         dot.isAttractive = false // 点没有被鼠标吸引
  262.                         dot.isAdsorption = false // 点没有被吸附
  263.                 }
  264.                 dot.x = nextX
  265.                 dot.y = nextY
  266.         })
  267.         draw() // 绘制
  268.         animationMouseNum.value = requestAnimationFrame(mouseAdsorb)
  269. }
  270. /** 点鼠标之间连线 */
  271. const drawLineByMouse = () => {
  272.         ctx.value.save();
  273.         ctx.value.translate(0.5, 0.5);
  274.         const lineColorRgb = hexToRgb(LINE_COLOR.value)
  275.         dotList.value.forEach((dot)=>{
  276.                 if(dot.isAdsorption || dot.isAttractive) { // 被吸引 或 被吸附的点 与鼠标位置连线
  277.                         ctx.value.lineWidth = 1
  278.                         ctx.value.strokeStyle = `rgba(${lineColorRgb.r}, ${lineColorRgb.g}, ${lineColorRgb.b}, ${0.3})`
  279.                         ctx.value.beginPath();
  280.                         ctx.value.moveTo(dot.x, dot.y)
  281.                         ctx.value.lineTo(mousePosition.value.x, mousePosition.value.y)
  282.                         ctx.value.stroke()
  283.                         ctx.value.closePath();
  284.                 }
  285.         })
  286.         ctx.value.restore();
  287. }
  288. /** 工具函数 */
  289. // 生成min-max范围内随机整数
  290. const getRandomInteger = (min, max) => {
  291.   return Math.floor(Math.random() * (max - min + 1)) + min;
  292. }
  293. // 计算两点之间的距离
  294. const getDistanceByTwoDot = (dot1, dot2) => {
  295.         const w = Math.abs(dot1.x - dot2.x)
  296.         const h = Math.abs(dot1.y - dot2.y)
  297.         const s = Math.sqrt(w**2 + h**2)
  298.         return s
  299. }
  300. // 十六进制颜色 转 rgb
  301. function hexToRgb(hex) {
  302.         if(hex.includes('#')) {
  303.                 hex = hex.replace('#', '')
  304.         }
  305.   // 解析红、绿、蓝分量
  306.   let r = parseInt(hex.substring(0, 2), 16);
  307.   let g = parseInt(hex.substring(2, 4), 16);
  308.   let b = parseInt(hex.substring(4, 6), 16) || 0;
  309.   return { r, g, b };
  310. }
  311. // 计算点被吸引时 x y 方向上的移动速度
  312. const getAdsorbSpeed = (dot, mouse) => {
  313.         // const distance = getDistanceByTwoDot(dot, mouse) // 鼠标和点的距离
  314.         const distanceX = mouse.x - dot.x
  315.         const distanceY = mouse.y - dot.y
  316.         const speed = DOT_MOVE_SPEED.value * 3
  317.         const mouse_move_x =  +(speed * (distanceX < 0 ? -1 : 1)).toFixed(2)
  318.         const mouse_move_y =        +(speed * (distanceY < 0 ? -1 : 1) * Math.abs((distanceY / distanceX))).toFixed(2)
  319.         return { mouse_move_x, mouse_move_y }
  320. }
  321. </script>
  322. <style lang='scss' scoped>
  323. #random-dot-line-canvas {
  324.         cursor: pointer;
  325. }
  326. </style>
复制代码
结果



回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表