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.pswp.cn/diannao/81136.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81136.shtml
英文地址,請注明出處:http://en.pswp.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;下載程序…