canvas动画:点随机运动 距离内自动连接成线 鼠标移动自动吸附附近的点

思路/实现步骤

    1. 创建canvas元素
    1. 获取canvas的上下文ctx
    1. 初始化点的信息(数量、初始坐标、移动方向、移动速度、大小、颜色)
    1. 绘制点
    1. 绘制点之间的连线
    1. 点有规律的动起来
    1. 动画循环
    1. 鼠标移动相关逻辑
    1. 点鼠标之间连线
    1. 鼠标吸附逻辑
    1. 添加配置项
    1. 重绘、重置

示例代码

<template><div class='random-dot-line-canvas'><!-- <div style="margin-bottom: 20px;"><el-button @click="nextDraw">下一帧</el-button></div> --><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><canvasv-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) {// console.log('移入画布')mouseAdsorb()} else if (newConfig.inCanvas === false) {// console.log('移出画布')cancelAnimationFrame(animationMouseNum.value)}},{ immediate: true, deep: true }
)/** 初始化函数 */
const init = async() => {initing.value = falseawait nextTick()let canvas = document.getElementById('random-dot-line-canvas')canvasPositionInPage.value = canvas.getBoundingClientRect()// {"x": 40, "y": 183, "width": 800, "height": 400, "top": 183, "right": 840, "bottom": 583, "left": 40 }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 = truesetNewConfigValue(canvasConfig.value)init()
}
/** 重置 */
const reset = () => {initing.value = truesetNewConfigValue({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.valuecanvasHeight.value = newConfig.canvasHeight || canvasHeight.valueDOT_NUM.value = newConfig.DOT_NUM || DOT_NUM.valueDOT_RADIUS.value = newConfig.DOT_RADIUS || DOT_RADIUS.valueDOT_COLOR.value = newConfig.DOT_COLOR || DOT_COLOR.valueLINE_COLOR.value = newConfig.LINE_COLOR || LINE_COLOR.valueDOT_DRAW_LINE_MAX_DIST.value = newConfig.DOT_DRAW_LINE_MAX_DIST || DOT_DRAW_LINE_MAX_DIST.valueDOT_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 // 移动x轴的速度const move_y = (+(Math.random() * 2 - 1).toFixed(2)) * DOT_MOVE_SPEED.value // 移动y轴的速度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 = 1ctx.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.xlet nextY = dot.ynextX = dot.x + dot.move_xnextY = dot.y + dot.move_yif(nextX > canvasWidth.value - DOT_RADIUS.value) {nextX = canvasWidth.value - DOT_RADIUS.valuedot.move_x = -dot.move_x} else if(nextX < DOT_RADIUS.value) {nextX = DOT_RADIUS.valuedot.move_x = -dot.move_x}if(nextY > canvasHeight.value - DOT_RADIUS.value) {nextY = canvasHeight.value - DOT_RADIUS.valuedot.move_y = -dot.move_y} else if(nextY < DOT_RADIUS.value) {nextY = DOT_RADIUS.valuedot.move_y = -dot.move_y}dot.x = nextXdot.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, // 相对于canvasoffsetY: 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.xlet nextY = dot.yconst 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) // 计算鼠标吸附点xy方向上的速度nextX += mouse_move_xnextY += mouse_move_y} else if(distance <= adsorptionDistance) { // 吸附点dot.isAdsorption = true // 点已经被吸附dot.isAttractive = false} else {dot.isAttractive = false // 点没有被鼠标吸引dot.isAdsorption = false // 点没有被吸附}dot.x = nextXdot.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 = 1ctx.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();
}/** 工具函数 */
// 生成min-max范围内随机整数
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
}
// 十六进制颜色 转 rgb
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 };
}
// 计算点被吸引时 x y 方向上的移动速度
const getAdsorbSpeed = (dot, mouse) => {// const distance = getDistanceByTwoDot(dot, mouse) // 鼠标和点的距离const distanceX = mouse.x - dot.xconst distanceY = mouse.y - dot.yconst speed = DOT_MOVE_SPEED.value * 3const 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>

效果

在这里插入图片描述

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/81136.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年4月AI科技领域周报(4.21-4.27):大模型生态加速演进 通用AI开启产业融合新范式

2025年4月AI科技领域周报&#xff08;4.21-4.27&#xff09;&#xff1a;大模型生态加速演进 通用AI开启产业融合新范式 一、本周热点回顾1. OpenAI发布全球首款通用AI芯片「GPT-NPU」 重塑AI算力架构2. 谷歌DeepMind发布医疗AI系统「Med-Pathfinder」 实现全科室智能诊断3. 微…

【Bash】可以请您解释性地说明一下“2>1”这个语法吗?

Qwen3-235B 当然可以&#xff01;我们一步步拆解 2>&1 的含义&#xff0c;用生活化的例子来理解它&#xff1a; &#x1f9e0; 前提知识&#xff1a;三个“水流通道” 在 Linux/Unix 系统中&#xff0c;程序运行时有三种默认的“水流通道”&#xff1a; 标准输入&…

MAC如何安装多版本jdk(以8,11,17为例)

MAC如何安装多版本jdk(以8,11,17为例&#xff09;_mac安装jdk17-CSDN博客

Nginx核心

一、概述 Nginx一个具有高性能的【HTTP】和【反向代理】的【WEB服务器】&#xff0c;同时也是一个电子邮件代理服务器。正向代理服务的是客户端&#xff08;比如VPN&#xff09;&#xff0c;反向代理服务的是服务端。Nginx是多进程的&#xff0c;有一个Master进程控制多个Worke…

综合开发-手机APP远程控制PLC1500柱灯的亮灭

要通过 ​​Unity3D​​ 开发的手机 App 控制 ​​电气柜上面的柱灯&#xff0c;需要WIFI模块作为桥梁&#xff0c;按照以下步骤实现&#xff1a; ​​1. 硬件准备&#xff08;硬件部分&#xff09;​​ ​​所需材料​​ ​​ESP32开发板​​&#xff08;如ESP32-WROOM-32&a…

五款提效工具

1. 亿可达 核心功能&#xff1a;通过“触发器动作”模式&#xff0c;实现任务自动执行&#xff08;如邮件转发、评论回复、数据同步&#xff09;。 适用场景&#xff1a;自动同步Notion项目到滴答清单生成待办事项 优势&#xff1a;节省重复操作时间&#xff0c;减少人为错误&a…

Docker化HBase排错实录:从Master hflush启动失败到Snappy算法未支持解决

前言 在容器化时代&#xff0c;使用 Docker 部署像 HBase 这样复杂的分布式系统也比较方便。社区也提供了许多方便的 HBase Docker 镜像&#xff0c;没有找到官方的 apache的&#xff0c;但有包含许多大数据工具的 harisekhon/hbase 或用于学习目的的 bigdatauniversity/hbase…

windows远程服务器数据库的搭建和远程访问(Mysql忘记密码通过Navicat连接记录解密密码)

服务器数据库的搭建和远程访问 mysql数据库安装&#xff08;详细&#xff09; window安装mysql详细流程 路程&#xff1a;重设MySQL5密码&#xff0c;发现远程服务器原本有一个MySQL5&#xff0c;尝试在服务器本地建立连接被拒绝&#xff0c;因为不知道密码。 &#xff08;1…

每日c/c++题 备战蓝桥杯(P1093 [NOIP 2007 普及组] 奖学金)

洛谷P1093 [NOIP 2007 普及组] 奖学金 详解题解 题目背景与要求 题目链接&#xff1a;P1093 奖学金 核心任务&#xff1a;根据学生三科总分评选前5名奖学金获得者&#xff0c;需按特定规则排序输出。 排序规则&#xff08;按优先级从高到低&#xff09;&#xff1a; 总分降…

openEuler 22.03 安装 Nginx,支持离线安装

目录 一、环境检查1.1 必要环境检查1.2 在线安装&#xff08;有网络&#xff09;1.3 离线安装&#xff08;无网络&#xff09; 二、下载Nginx2.1 在线下载2.2 离线下载 三、安装Nginx四、开机自启服务五、开放防火墙端口六、常用命令 一、环境检查 1.1 必要环境检查 # 查看 g…

基于深度学习的图像压缩技术(二)

接上篇&#xff1a;基于深度学习的图像压缩技术&#xff08;一&#xff09;-CSDN博客 3 基于生成对抗神经网络的图像压缩技术 生成对抗网络是一种先进的无监督学习算法&#xff0c;由Goodfellow等人在2014 年首次提出&#xff0c;其核心思想源于博弈论。 生成对抗网络在图像压…

TCP和UDP的数据传输+区别

目录 一、数据传输过程 1.1 TCP字节流服务图 1.2 UDP数据报服务图 二、tcp与udp的区别 1.连接方式 2.可靠性 3.传输效率 4.有序性 5.流量控制和拥塞控制 6.应用场景 7.首部长度 三、tcp与udp能不能使用同一个端口号&#xff1f; 四、同一个协议&#xf…

基于ssm的校园旧书交易交换平台(源码+文档)

项目简介 校园旧书交易交换平台的主要使用者分为&#xff1a; 前台功能&#xff1a;用户进入系统可以对首页、书籍信息、校园公告、个人中心、后台管理等功能进行操作&#xff1b; 后台主要是管理员&#xff0c;管理员功能包括主页、个人中心、学生管理、发布人管理、书籍分类…

虚假安全补丁攻击WooCommerce管理员以劫持网站

一场大规模钓鱼攻击正针对WooCommerce用户&#xff0c;通过伪造安全警报诱使他们下载所谓的"关键补丁"&#xff0c;实则为植入WordPress后门的恶意程序。 恶意插件植入 根据Patchstack研究人员发现&#xff0c;上当受骗的用户在下载更新时&#xff0c;实际上安装的…

《冰雪传奇点卡版》:第二大陆介绍!

一、第二大陆&#xff1a;高阶资源与实力验证的核心战场 1. 准入条件与地图分布 进入门槛&#xff1a; 基础要求&#xff1a;角色需达到四转&#xff08;需消耗50万元宝完成转生任务&#xff09;&#xff0c;部分地图需额外满足神魔点数&#xff08;如黑暗之森需神魔全2&#…

信创系统图形界面开发指南:技术选择与实践详解

信创系统图形界面开发指南&#xff1a;技术选择与实践详解 &#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#, Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&…

【人脸去遮挡前沿】三阶段级联引导学习如何突破真实场景遮挡难题?

一、现实痛点:当人脸被遮挡,AI “认脸” 有多难? 你是否遇到过这样的场景? 中考体育测试:2025 年天津泰达街中考考场要求考生 “脸部无遮挡” 才能通过人脸识别入场,戴口罩、帽子的学生需现场调整发型。智能门锁:奇景光电在 CES 2025 推出的 WiseEye 掌静脉模块,通过掌…

c++线程的创建

c 11 线程编程实战 目录 c 11 线程编程实战1&#xff0c;线程的创建1.1 传入无参函数1.2 传入有参函数1.3 传入类内部函数1.4 lambda表达式 1&#xff0c;线程的创建 1.1 传入无参函数 //传入函数&#xff0c;创建线程 void ThreadMain() {//获取线程IDstd::thread::id thi…

人工智能数学基础(六):数理统计

数理统计是人工智能中数据处理和分析的核心工具&#xff0c;它通过收集、分析数据来推断总体特征和规律。本文将系统介绍数理统计的基本概念和方法&#xff0c;并结合 Python 实例&#xff0c;帮助读者更好地理解和应用这些知识。资源绑定附上完整资源供读者参考学习&#xff0…

解决STM32待机模式无法下载程序问题的深度探讨

在现代嵌入式系统开发中&#xff0c;STM32系列微控制器因其高性能、低功耗和丰富的外设资源而广受欢迎。然而&#xff0c;开发者在使用STM32时可能会遇到一个问题&#xff1a;当微控制器进入待机模式后&#xff0c;无法通过调试接口&#xff08;如SWD或JTAG&#xff09;下载程序…