• 前端练习小项目 —— 养一只电子蜘蛛


            前言:在学习完JavaScript之后,我们就可以使用JavaScript来实现一下好玩的效果了,本篇文章讲解的是如何纯使用JavaScript来实现一个网页中的电子蜘蛛。


    ✨✨✨这里是秋刀鱼不做梦的BLOG

    ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客

    在开始学习如何编写一个网页蜘蛛之前,先让我们看一下这个电子蜘蛛长什么样:

            ——我们可以看到,其会跟随着我们的鼠标进行移动,那么我们如何实现这样的效果呢?接下来让我们开始讲解。

    1.HTML代码

            我们的html代码十分的简单,就是创建一个画布,而我们接下来的操作,都是在此上边进行操作的:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>秋刀鱼不做梦title>
    8. <script src="./test.js">script>
    9. <style>
    10. /* 移除body的默认外边距和内边距 */
    11. body {
    12. margin: 0px;
    13. padding: 0px;
    14. position: fixed;
    15. /* 设置网页背景颜色为黑色 */
    16. background: rgb(0, 0, 0);
    17. }
    18. style>
    19. head>
    20. <body>
    21. <canvas id="canvas">canvas>
    22. body>
    23. html>

            可以看到我们的HTML代码非常的简单,接下来让我们开始在其上边进行操作!

    2.JavaScript代码

            在开始编写JavaScript代码之前,先让我们理清一下思路:

    总体流程

    1. 页面加载时,canvas 元素和绘图上下文初始化

    2. 定义触手对象,每条触手由多个段组成。

    3. 监听鼠标移动事件,实时更新鼠标的位置。

    4. 通过动画循环绘制触手,触手根据鼠标的位置动态变化,形成流畅的动画效果。

            大致的流程就是上边的步骤,但是我相信读者在没用自己完成此代码的编写之前,可能不能理解上边的流程,不过没关系,现在让我们开始我们的网页小蜘蛛的编写:

           

            写在前面:为了让读者可以更好的理解代码的逻辑,我们给没一句代码都加上了注释,希望读者可以根据注释的帮助一点一点的理解代码:

    JavaScript代码:

    1. // 定义requestAnimFrame函数
    2. window.requestAnimFrame = function () {
    3. // 检查浏览器是否支持requestAnimFrame函数
    4. return (
    5. window.requestAnimationFrame ||
    6. window.webkitRequestAnimationFrame ||
    7. window.mozRequestAnimationFrame ||
    8. window.oRequestAnimationFrame ||
    9. window.msRequestAnimationFrame ||
    10. // 如果所有这些选项都不可用,使用设置超时来调用回调函数
    11. function (callback) {
    12. window.setTimeout(callback)
    13. }
    14. )
    15. }
    16. // 初始化函数,用于获取canvas元素并返回相关信息
    17. function init(elemid) {
    18. // 获取canvas元素
    19. let canvas = document.getElementById(elemid)
    20. // 获取2d绘图上下文,这里d是小写的
    21. c = canvas.getContext('2d')
    22. // 设置canvas的宽度为窗口内宽度,高度为窗口内高度
    23. w = (canvas.width = window.innerWidth)
    24. h = (canvas.height = window.innerHeight)
    25. // 设置填充样式为半透明黑
    26. c.fillStyle = "rgba(30,30,30,1)"
    27. // 使用填充样式填充整个canvas
    28. c.fillRect(0, 0, w, h)
    29. // 返回绘图上下文和canvas元素
    30. return { c: c, canvas: canvas }
    31. }
    32. // 等待页面加载完成后执行函数
    33. window.onload = function () {
    34. // 获取绘图上下文和canvas元素
    35. let c = init("canvas").c,
    36. canvas = init("canvas").canvas,
    37. // 设置canvas的宽度为窗口内宽度,高度为窗口内高度
    38. w = (canvas.width = window.innerWidth),
    39. h = (canvas.height = window.innerHeight),
    40. // 初始化鼠标对象
    41. mouse = { x: false, y: false },
    42. last_mouse = {}
    43. // 定义计算两点距离的函数
    44. function dist(p1x, p1y, p2x, p2y) {
    45. return Math.sqrt(Math.pow(p2x - p1x, 2) + Math.pow(p2y - p1y, 2))
    46. }
    47. // 定义 segment 类
    48. class segment {
    49. // 构造函数,用于初始化 segment 对象
    50. constructor(parent, l, a, first) {
    51. // 如果是第一条触手段,则位置坐标为触手顶部位置
    52. // 否则位置坐标为上一个segment对象的nextPos坐标
    53. this.first = first
    54. if (first) {
    55. this.pos = {
    56. x: parent.x,
    57. y: parent.y,
    58. }
    59. } else {
    60. this.pos = {
    61. x: parent.nextPos.x,
    62. y: parent.nextPos.y,
    63. }
    64. }
    65. // 设置segment的长度和角度
    66. this.l = l
    67. this.ang = a
    68. // 计算下一个segment的坐标位置
    69. this.nextPos = {
    70. x: this.pos.x + this.l * Math.cos(this.ang),
    71. y: this.pos.y + this.l * Math.sin(this.ang),
    72. }
    73. }
    74. // 更新segment位置的方法
    75. update(t) {
    76. // 计算segment与目标点的角度
    77. this.ang = Math.atan2(t.y - this.pos.y, t.x - this.pos.x)
    78. // 根据目标点和角度更新位置坐标
    79. this.pos.x = t.x + this.l * Math.cos(this.ang - Math.PI)
    80. this.pos.y = t.y + this.l * Math.sin(this.ang - Math.PI)
    81. // 根据新的位置坐标更新nextPos坐标
    82. this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang)
    83. this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang)
    84. }
    85. // 将 segment 回执回初始位置的方法
    86. fallback(t) {
    87. // 将位置坐标设置为目标点坐标
    88. this.pos.x = t.x
    89. this.pos.y = t.y
    90. this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang)
    91. this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang)
    92. }
    93. show() {
    94. c.lineTo(this.nextPos.x, this.nextPos.y)
    95. }
    96. }
    97. // 定义 tentacle 类
    98. class tentacle {
    99. // 构造函数,用于初始化 tentacle 对象
    100. constructor(x, y, l, n, a) {
    101. // 设置触手的顶部位置坐标
    102. this.x = x
    103. this.y = y
    104. // 设置触手的长度
    105. this.l = l
    106. // 设置触手的段数
    107. this.n = n
    108. // 初始化触手的目标点对象
    109. this.t = {}
    110. // 设置触手的随机移动参数
    111. this.rand = Math.random()
    112. // 创建触手的第一条段
    113. this.segments = [new segment(this, this.l / this.n, 0, true)]
    114. // 创建其他的段
    115. for (let i = 1; i < this.n; i++) {
    116. this.segments.push(
    117. new segment(this.segments[i - 1], this.l / this.n, 0, false)
    118. )
    119. }
    120. }
    121. // 移动触手到目标点的方法
    122. move(last_target, target) {
    123. // 计算触手顶部与目标点的角度
    124. this.angle = Math.atan2(target.y - this.y, target.x - this.x)
    125. // 计算触手的距离参数
    126. this.dt = dist(last_target.x, last_target.y, target.x, target.y)
    127. // 计算触手的目标点坐标
    128. this.t = {
    129. x: target.x - 0.8 * this.dt * Math.cos(this.angle),
    130. y: target.y - 0.8 * this.dt * Math.sin(this.angle)
    131. }
    132. // 如果计算出了目标点,则更新最后一个segment对象的位置坐标
    133. // 否则,更新最后一个segment对象的位置坐标为目标点坐标
    134. if (this.t.x) {
    135. this.segments[this.n - 1].update(this.t)
    136. } else {
    137. this.segments[this.n - 1].update(target)
    138. }
    139. // 遍历所有segment对象,更新它们的位置坐标
    140. for (let i = this.n - 2; i >= 0; i--) {
    141. this.segments[i].update(this.segments[i + 1].pos)
    142. }
    143. if (
    144. dist(this.x, this.y, target.x, target.y) <=
    145. this.l + dist(last_target.x, last_target.y, target.x, target.y)
    146. ) {
    147. this.segments[0].fallback({ x: this.x, y: this.y })
    148. for (let i = 1; i < this.n; i++) {
    149. this.segments[i].fallback(this.segments[i - 1].nextPos)
    150. }
    151. }
    152. }
    153. show(target) {
    154. // 如果触手与目标点的距离小于触手的长度,则回执触手
    155. if (dist(this.x, this.y, target.x, target.y) <= this.l) {
    156. // 设置全局合成操作为lighter
    157. c.globalCompositeOperation = "lighter"
    158. // 开始新路径
    159. c.beginPath()
    160. // 从触手起始位置开始绘制线条
    161. c.moveTo(this.x, this.y)
    162. // 遍历所有的segment对象,并使用他们的show方法回执线条
    163. for (let i = 0; i < this.n; i++) {
    164. this.segments[i].show()
    165. }
    166. // 设置线条样式
    167. c.strokeStyle = "hsl(" + (this.rand * 60 + 180) +
    168. ",100%," + (this.rand * 60 + 25) + "%)"
    169. // 设置线条宽度
    170. c.lineWidth = this.rand * 2
    171. // 设置线条端点样式
    172. c.lineCap = "round"
    173. // 设置线条连接处样式
    174. c.lineJoin = "round"
    175. // 绘制线条
    176. c.stroke()
    177. // 设置全局合成操作为“source-over”
    178. c.globalCompositeOperation = "source-over"
    179. }
    180. }
    181. // 绘制触手的圆形头的方法
    182. show2(target) {
    183. // 开始新路径
    184. c.beginPath()
    185. // 如果触手与目标点的距离小于触手的长度,则回执白色的圆形
    186. // 否则绘制青色的圆形
    187. if (dist(this.x, this.y, target.x, target.y) <= this.l) {
    188. c.arc(this.x, this.y, 2 * this.rand + 1, 0, 2 * Math.PI)
    189. c.fillStyle = "whith"
    190. } else {
    191. c.arc(this.x, this.y, this.rand * 2, 0, 2 * Math.PI)
    192. c.fillStyle = "darkcyan"
    193. }
    194. // 填充圆形
    195. c.fill()
    196. }
    197. }
    198. // 初始化变量
    199. let maxl = 400,//触手的最大长度
    200. minl = 50,//触手的最小长度
    201. n = 30,//触手的段数
    202. numt = 600,//触手的数量
    203. tent = [],//触手的数组
    204. clicked = false,//鼠标是否被按下
    205. target = { x: 0, y: 0 }, //触手的目标点
    206. last_target = {},//上一个触手的目标点
    207. t = 0,//当前时间
    208. q = 10;//触手每次移动的步长
    209. // 创建触手对象
    210. for (let i = 0; i < numt; i++) {
    211. tent.push(
    212. new tentacle(
    213. Math.random() * w,//触手的横坐标
    214. Math.random() * h,//触手的纵坐标
    215. Math.random() * (maxl - minl) + minl,//触手的长度
    216. n,//触手的段数
    217. Math.random() * 2 * Math.PI,//触手的角度
    218. )
    219. )
    220. }
    221. // 绘制图像的方法
    222. function draw() {
    223. // 如果鼠标移动,则计算触手的目标点与当前点的偏差
    224. if (mouse.x) {
    225. target.errx = mouse.x - target.x
    226. target.erry = mouse.y - target.y
    227. } else {
    228. // 否则,计算触手的目标点的横坐标
    229. target.errx =
    230. w / 2 +
    231. ((h / 2 - q) * Math.sqrt(2) * Math.cos(t)) /
    232. (Math.pow(Math.sin(t), 2) + 1) -
    233. target.x;
    234. target.erry =
    235. h / 2 +
    236. ((h / 2 - q) * Math.sqrt(2) * Math.cos(t) * Math.sin(t)) /
    237. (Math.pow(Math.sin(t), 2) + 1) -
    238. target.y;
    239. }
    240. // 更新触手的目标点坐标
    241. target.x += target.errx / 10
    242. target.y += target.erry / 10
    243. // 更新时间
    244. t += 0.01;
    245. // 绘制触手的目标点
    246. c.beginPath();
    247. c.arc(
    248. target.x,
    249. target.y,
    250. dist(last_target.x, last_target.y, target.x, target.y) + 5,
    251. 0,
    252. 2 * Math.PI
    253. );
    254. c.fillStyle = "hsl(210,100%,80%)"
    255. c.fill();
    256. // 绘制所有触手的中心点
    257. for (i = 0; i < numt; i++) {
    258. tent[i].move(last_target, target)
    259. tent[i].show2(target)
    260. }
    261. // 绘制所有触手
    262. for (i = 0; i < numt; i++) {
    263. tent[i].show(target)
    264. }
    265. // 更新上一个触手的目标点坐标
    266. last_target.x = target.x
    267. last_target.y = target.y
    268. }
    269. // 循环执行绘制动画的函数
    270. function loop() {
    271. // 使用requestAnimFrame函数循环执行
    272. window.requestAnimFrame(loop)
    273. // 清空canvas
    274. c.clearRect(0, 0, w, h)
    275. // 绘制动画
    276. draw()
    277. }
    278. // 监听窗口大小改变事件
    279. window.addEventListener("resize", function () {
    280. // 重置canvas的大小
    281. w = canvas.width = window.innerWidth
    282. w = canvas.height = window.innerHeight
    283. // 循环执行回执动画的函数
    284. loop()
    285. })
    286. // 循环执行回执动画的函数
    287. loop()
    288. // 使用setInterval函数循环
    289. setInterval(loop, 1000 / 60)
    290. // 监听鼠标移动事件
    291. canvas.addEventListener("mousemove", function (e) {
    292. // 记录上一次的鼠标位置
    293. last_mouse.x = mouse.x
    294. last_mouse.y = mouse.y
    295. // 更新点前的鼠标位置
    296. mouse.x = e.pageX - this.offsetLeft
    297. mouse.y = e.pageY - this.offsetTop
    298. }, false)
    299. // 监听鼠标离开事件
    300. canvas.addEventListener("mouseleave", function (e) {
    301. // 将mouse设为false
    302. mouse.x = false
    303. mouse.y = false
    304. })
    305. }

    这里我们在大致的梳理一下上述代码的流程:

    1. 初始化阶段

    • init 函数:当页面加载时,init 函数被调用,获取 canvas 元素并设置其宽高为窗口的大小。获取到的 2D 绘图上下文(context)用于后续绘制。
    • window.onload:页面加载完成后,初始化 canvascontext,并设置鼠标初始状态。

    2. 触手对象的定义

    • segment:这是触手的一段,每个段有起始点(pos)、长度(l)、角度(ang),并通过角度计算出下一段的位置(nextPos)。
    • tentacle:代表完整的触手,由若干个 segment 组成。触手的起始点在屏幕中心,并且每个触手包含多个段。tentacle 的主要方法有:
      • move:根据鼠标位置更新每一段的位置。
      • show:绘制触手的路径。

    3. 事件监听

    • canvas.addEventListener("mousemove", ...):当鼠标移动时,捕捉鼠标的位置并存储在 mouse 变量中。每次鼠标移动会更新 mouselast_mouse 的坐标,用于后续的动画。

    4. 动画循环

    • draw 函数:这是一个递归的函数,用于创建动画效果。
      • 首先,它会在每一帧中为画布填充半透明背景,使得之前绘制的内容逐渐消失,产生拖影效果。
      • 然后,遍历所有触手(tentacles),调用它们的 moveshow 方法,更新位置并绘制每一帧。
      • 最后,使用 requestAnimFrame(draw) 不断递归调用 draw,形成一个动画循环。

    5. 触手的行为

    • 触手的运动是通过 move 函数实现的,触手的最后一个段首先更新位置,然后其他段依次跟随。
    • 触手的绘制通过 show 函数,遍历所有段并绘制线条,最后显示在屏幕上。

            ——这样我们就完成了电子小蜘蛛的制作了!!!

    最后,在让我们看一下最终效果:


    以上就是本篇文章的全部内容了!!

  • 相关阅读:
    “升职加薪”必经路,深入详解Spring,读懂源码So easy
    笔试强训Day12
    初识结构体
    提高网申通过率的秘籍,校园招聘之春招秋招都有效
    指针进阶(一)
    c盘文件误删怎么恢复?这里介绍四种方法,赶紧看过来
    JFrame中有关于DefaultCloseOperation的使用及参数说明(含源码阅读)
    SSM毕设项目超市零售管理系统mq344(java+VUE+Mybatis+Maven+Mysql)
    吃鸡游戏出现msvcp140.dll缺失的四个解决方法
    B+tree - B+树深度解析+C语言实现+opencv绘图助解
  • 原文地址:https://blog.csdn.net/2302_80198073/article/details/142066354