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

news2024/12/23 18:04:06

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


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

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

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

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

1.HTML代码

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

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>秋刀鱼不做梦</title>
    <!-- 引入外部的JavaScript文件 -->
    <script src="./test.js"></script>
    <style>
        /* 移除body的默认外边距和内边距 */
        body {
            margin: 0px;
            padding: 0px;
            position: fixed;
            /* 设置网页背景颜色为黑色 */
            background: rgb(0, 0, 0);
        }
    </style>
</head>

<body>
    <!-- 创建一个画布用于图形绘制 -->
    <canvas id="canvas"></canvas>
</body>

</html>

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

2.JavaScript代码

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

总体流程

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

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

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

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

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

       

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

JavaScript代码:

// 定义requestAnimFrame函数
window.requestAnimFrame = function () {
    // 检查浏览器是否支持requestAnimFrame函数
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        // 如果所有这些选项都不可用,使用设置超时来调用回调函数
        function (callback) {
            window.setTimeout(callback)
        }
    )
}

// 初始化函数,用于获取canvas元素并返回相关信息
function init(elemid) {
    // 获取canvas元素
    let canvas = document.getElementById(elemid)
    // 获取2d绘图上下文,这里d是小写的
    c = canvas.getContext('2d')
    // 设置canvas的宽度为窗口内宽度,高度为窗口内高度
    w = (canvas.width = window.innerWidth)
    h = (canvas.height = window.innerHeight)
    // 设置填充样式为半透明黑
    c.fillStyle = "rgba(30,30,30,1)"
    // 使用填充样式填充整个canvas
    c.fillRect(0, 0, w, h)
    // 返回绘图上下文和canvas元素
    return { c: c, canvas: canvas }
}

// 等待页面加载完成后执行函数
window.onload = function () {
    // 获取绘图上下文和canvas元素
    let c = init("canvas").c,
        canvas = init("canvas").canvas,
        // 设置canvas的宽度为窗口内宽度,高度为窗口内高度
        w = (canvas.width = window.innerWidth),
        h = (canvas.height = window.innerHeight),
        // 初始化鼠标对象
        mouse = { x: false, y: false },
        last_mouse = {}

    // 定义计算两点距离的函数
    function dist(p1x, p1y, p2x, p2y) {
        return Math.sqrt(Math.pow(p2x - p1x, 2) + Math.pow(p2y - p1y, 2))
    }

    // 定义 segment 类
    class segment {
        // 构造函数,用于初始化 segment 对象
        constructor(parent, l, a, first) {
            // 如果是第一条触手段,则位置坐标为触手顶部位置
            // 否则位置坐标为上一个segment对象的nextPos坐标
            this.first = first
            if (first) {
                this.pos = {
                    x: parent.x,
                    y: parent.y,
                }
            } else {
                this.pos = {
                    x: parent.nextPos.x,
                    y: parent.nextPos.y,
                }
            }
            // 设置segment的长度和角度
            this.l = l
            this.ang = a
            // 计算下一个segment的坐标位置
            this.nextPos = {
                x: this.pos.x + this.l * Math.cos(this.ang),
                y: this.pos.y + this.l * Math.sin(this.ang),
            }
        }
        // 更新segment位置的方法
        update(t) {
            // 计算segment与目标点的角度
            this.ang = Math.atan2(t.y - this.pos.y, t.x - this.pos.x)
            // 根据目标点和角度更新位置坐标
            this.pos.x = t.x + this.l * Math.cos(this.ang - Math.PI)
            this.pos.y = t.y + this.l * Math.sin(this.ang - Math.PI)
            // 根据新的位置坐标更新nextPos坐标
            this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang)
            this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang)
        }
        // 将 segment 回执回初始位置的方法
        fallback(t) {
            // 将位置坐标设置为目标点坐标
            this.pos.x = t.x
            this.pos.y = t.y
            this.nextPos.x = this.pos.x + this.l * Math.cos(this.ang)
            this.nextPos.y = this.pos.y + this.l * Math.sin(this.ang)
        }
        show() {
            c.lineTo(this.nextPos.x, this.nextPos.y)
        }
    }

    // 定义 tentacle 类
    class tentacle {
        // 构造函数,用于初始化 tentacle 对象
        constructor(x, y, l, n, a) {
            // 设置触手的顶部位置坐标
            this.x = x
            this.y = y
            // 设置触手的长度
            this.l = l
            // 设置触手的段数
            this.n = n
            // 初始化触手的目标点对象
            this.t = {}
            // 设置触手的随机移动参数
            this.rand = Math.random()
            // 创建触手的第一条段
            this.segments = [new segment(this, this.l / this.n, 0, true)]
            // 创建其他的段
            for (let i = 1; i < this.n; i++) {
                this.segments.push(
                    new segment(this.segments[i - 1], this.l / this.n, 0, false)
                )
            }
        }
        // 移动触手到目标点的方法
        move(last_target, target) {
            // 计算触手顶部与目标点的角度
            this.angle = Math.atan2(target.y - this.y, target.x - this.x)
            // 计算触手的距离参数
            this.dt = dist(last_target.x, last_target.y, target.x, target.y)
            // 计算触手的目标点坐标
            this.t = {
                x: target.x - 0.8 * this.dt * Math.cos(this.angle),
                y: target.y - 0.8 * this.dt * Math.sin(this.angle)
            }
            // 如果计算出了目标点,则更新最后一个segment对象的位置坐标
            // 否则,更新最后一个segment对象的位置坐标为目标点坐标
            if (this.t.x) {
                this.segments[this.n - 1].update(this.t)
            } else {
                this.segments[this.n - 1].update(target)
            }
            // 遍历所有segment对象,更新它们的位置坐标
            for (let i = this.n - 2; i >= 0; i--) {
                this.segments[i].update(this.segments[i + 1].pos)
            }
            if (
                dist(this.x, this.y, target.x, target.y) <=
                this.l + dist(last_target.x, last_target.y, target.x, target.y)
            ) {
                this.segments[0].fallback({ x: this.x, y: this.y })
                for (let i = 1; i < this.n; i++) {
                    this.segments[i].fallback(this.segments[i - 1].nextPos)
                }
            }
        }
        show(target) {
            // 如果触手与目标点的距离小于触手的长度,则回执触手
            if (dist(this.x, this.y, target.x, target.y) <= this.l) {
                // 设置全局合成操作为lighter
                c.globalCompositeOperation = "lighter"
                // 开始新路径
                c.beginPath()
                // 从触手起始位置开始绘制线条
                c.moveTo(this.x, this.y)
                // 遍历所有的segment对象,并使用他们的show方法回执线条
                for (let i = 0; i < this.n; i++) {
                    this.segments[i].show()
                }
                // 设置线条样式
                c.strokeStyle = "hsl(" + (this.rand * 60 + 180) +
                    ",100%," + (this.rand * 60 + 25) + "%)"
                // 设置线条宽度
                c.lineWidth = this.rand * 2
                // 设置线条端点样式
                c.lineCap = "round"
                // 设置线条连接处样式
                c.lineJoin = "round"
                // 绘制线条
                c.stroke()
                // 设置全局合成操作为“source-over”
                c.globalCompositeOperation = "source-over"
            }
        }
        // 绘制触手的圆形头的方法
        show2(target) {
            // 开始新路径
            c.beginPath()
            // 如果触手与目标点的距离小于触手的长度,则回执白色的圆形
            // 否则绘制青色的圆形
            if (dist(this.x, this.y, target.x, target.y) <= this.l) {
                c.arc(this.x, this.y, 2 * this.rand + 1, 0, 2 * Math.PI)
                c.fillStyle = "whith"
            } else {
                c.arc(this.x, this.y, this.rand * 2, 0, 2 * Math.PI)
                c.fillStyle = "darkcyan"
            }
            // 填充圆形
            c.fill()
        }
    }
    // 初始化变量
    let maxl = 400,//触手的最大长度
        minl = 50,//触手的最小长度
        n = 30,//触手的段数
        numt = 600,//触手的数量
        tent = [],//触手的数组
        clicked = false,//鼠标是否被按下
        target = { x: 0, y: 0 }, //触手的目标点
        last_target = {},//上一个触手的目标点
        t = 0,//当前时间
        q = 10;//触手每次移动的步长

    // 创建触手对象
    for (let i = 0; i < numt; i++) {
        tent.push(
            new tentacle(
                Math.random() * w,//触手的横坐标
                Math.random() * h,//触手的纵坐标
                Math.random() * (maxl - minl) + minl,//触手的长度
                n,//触手的段数
                Math.random() * 2 * Math.PI,//触手的角度
            )
        )
    }
    // 绘制图像的方法
    function draw() {
        // 如果鼠标移动,则计算触手的目标点与当前点的偏差
        if (mouse.x) {
            target.errx = mouse.x - target.x
            target.erry = mouse.y - target.y
        } else {
            // 否则,计算触手的目标点的横坐标
            target.errx =
                w / 2 +
                ((h / 2 - q) * Math.sqrt(2) * Math.cos(t)) /
                (Math.pow(Math.sin(t), 2) + 1) -
                target.x;
            target.erry =
                h / 2 +
                ((h / 2 - q) * Math.sqrt(2) * Math.cos(t) * Math.sin(t)) /
                (Math.pow(Math.sin(t), 2) + 1) -
                target.y;
        }

        // 更新触手的目标点坐标
        target.x += target.errx / 10
        target.y += target.erry / 10

        // 更新时间
        t += 0.01;

        // 绘制触手的目标点
        c.beginPath();
        c.arc(
            target.x,
            target.y,
            dist(last_target.x, last_target.y, target.x, target.y) + 5,
            0,
            2 * Math.PI
        );
        c.fillStyle = "hsl(210,100%,80%)"
        c.fill();

        // 绘制所有触手的中心点
        for (i = 0; i < numt; i++) {
            tent[i].move(last_target, target)
            tent[i].show2(target)
        }
        // 绘制所有触手
        for (i = 0; i < numt; i++) {
            tent[i].show(target)
        }
        // 更新上一个触手的目标点坐标
        last_target.x = target.x
        last_target.y = target.y
    }
    // 循环执行绘制动画的函数
    function loop() {
        // 使用requestAnimFrame函数循环执行
        window.requestAnimFrame(loop)

        // 清空canvas
        c.clearRect(0, 0, w, h)

        // 绘制动画
        draw()
    }

    // 监听窗口大小改变事件
    window.addEventListener("resize", function () {
        // 重置canvas的大小
        w = canvas.width = window.innerWidth
        w = canvas.height = window.innerHeight

        // 循环执行回执动画的函数
        loop()
    })

    // 循环执行回执动画的函数
    loop()
    // 使用setInterval函数循环
    setInterval(loop, 1000 / 60)

    // 监听鼠标移动事件
    canvas.addEventListener("mousemove", function (e) {
        // 记录上一次的鼠标位置
        last_mouse.x = mouse.x
        last_mouse.y = mouse.y

        // 更新点前的鼠标位置
        mouse.x = e.pageX - this.offsetLeft
        mouse.y = e.pageY - this.offsetTop
    }, false)

    // 监听鼠标离开事件
    canvas.addEventListener("mouseleave", function (e) {
        // 将mouse设为false
        mouse.x = false
        mouse.y = false
    })
}

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

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 函数,遍历所有段并绘制线条,最后显示在屏幕上。

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

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


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

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

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

相关文章

2.3.1 协程设计原理与汇编实现coroutine

LINUX 精通 7 day23 20240908 晚19&#xff1a;25 - 21:30 课程链接地址 2.3.1 协程设计原理与汇编实现coroutine 目的 协程不是某种语言特有的&#xff0c;lua&#xff0c;go都有 ntyco 是king老师自己写的 原语操作&#xff1a;“原语操作”通常指的是在编程或计算机科学中…

Android 12系统源码_窗口管理(八)WindowConfiguration的作用

前言 在Android系统中WindowConfiguration这个类用于管理与窗口相关的设置&#xff0c;该类存储了当前窗口的显示区域、屏幕的旋转方向、窗口模式等参数&#xff0c;应用程序通过该类提供的信息可以更好的适配不同的屏幕布局和窗口环境&#xff0c;以提高用户体验。 一、类定…

性能测试的复习2-jmeter的搭建、使用、参数化

通过网盘分享的文件&#xff1a;性能测试共享文件 链接: https://pan.baidu.com/s/1A4Nc8C5Xp6qxQ5QFtecK8g?pwds73c 提取码: s73c 1、性能测试工具 2、jmeter环境搭建 3、jmeter的基本使用 4、jmeter的参数化

strncpy函数的使用和模拟实现

目录 1.头文件 2.strncpy函数功能 2.1情况二&#xff1a; 3.strncpy函数&#xff08;模拟实现&#xff09; 方源一把抓住VS2022&#xff0c;催动春秋产的气息&#xff0c;顷刻炼化&#xff01; 1.头文件 strncpy函数的使用需要包括头文件<string.h> #include<string…

Windows系统好用软件推荐

uTools uTools官网&#xff1a;https://u.tools/download/ 功能介绍&#xff1a; 内置许多有用的插件、快速打开应用、复制图片保存等

4457E/4457F/4457G/4457K数字示波器

KEYSIGHT是德 4457E/4457F/4457G/4457K数字示波器 4457系列数字示波器共4个产品型号&#xff0c;产品带宽从1GHz到4GHz&#xff0c;采样率10GSa/s、20GSa/s&#xff0c;垂直分辨率8bit&#xff0c;存储深度2Gpts&#xff0c;最快波形捕获率120万个波形/秒&#xff0c;独创的An…

LIN帧显隐性电平和字节传输顺序理解

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

干耳屎硬掏不出来怎么办?质量最好的可视挖耳勺推荐

干耳朵的朋友都会有这样子的困扰&#xff0c;耳朵中的耳屎太硬挖不出来。用铁耳勺去挖会疼&#xff0c;还容易因为操作不当弄伤耳膜。而用棉签掏耳&#xff0c;只有越推越进去。所以当耳屎弄不出来是可以用专业的工具来挖取。市面上的可视挖耳勺通过内窥镜观察耳道中的情况。但…

电脑文件被删如何找回?选它,文件恢复又快又全面!

在我们的日常工作和生活中&#xff0c;文件是无比重要的存在。它可能是您精心撰写的报告&#xff0c;可能是珍贵的照片回忆&#xff0c;也可能是多年积累的工作资料。然而&#xff0c;有时一个不小心&#xff0c;文件可能就被我们删除了&#xff0c;那种焦急和无奈想必您也曾体…

解锁阿尔茨海默病(AD)靶点密码,开启靶向治疗新篇章

前 言&#xff1a; 阿尔茨海默病&#xff08;AD&#xff09;是一种严重的神经退行性疾病&#xff0c;多发于高龄人群&#xff0c;主要表现为记忆、思维、分析判断、视空间辨认、情绪等障碍。从实验室到临床应用的过程充满挑战。阿尔茨海默症新型疗法的开发主要聚焦于靶向Aβ、…

Percona Toolkit 神器全攻略(性能类)

Percona Toolkit 神器全攻略&#xff08;性能类&#xff09; Percona Toolkit 神器全攻略系列共八篇&#xff0c;前文回顾&#xff1a; 前文回顾Percona Toolkit 神器全攻略Percona Toolkit 神器全攻略&#xff08;实用类&#xff09;Percona Toolkit 神器全攻略&#xff08;配…

STM32+FATFS+SD卡+RTC(生成.CSV格式文件)

一、简介 实验目的&#xff1a;在SD卡上挂载文件系统&#xff0c;实时记录压力传感器采集到的数据&#xff1b;且在表格第一排记录采集时间&#xff1b; 因为前面文章包含了除RTC之外的所有的代码&#xff0c;此文章只放RTC代码。 二、工程源码 RTC.c #include "sys.h…

人工智能|集成学习——混合专家模型 (MoE)

随着 Mixtral 8x7B (announcement, model card) 的推出&#xff0c;一种称为混合专家模型 (Mixed Expert Models&#xff0c;简称 MoEs) 的 Transformer 模型在开源人工智能社区引起了广泛关注。在本篇博文中&#xff0c;我们将深入探讨 MoEs 的核心组件、训练方法&#xff0c;…

arcgisPro绘制平行线、垂直线段

1、绘制一条线 2、点击【创建要素】按钮&#xff0c;选择线&#xff0c;点一个点后&#xff0c;将鼠标移至需要对其的线上&#xff0c;并右击&#xff0c;选择【平行】 3、移动一段距离后&#xff0c;完成绘制&#xff0c;可得到一条平行线 4、得到平行线 5、绘制垂直线&#x…

一文读懂:区块链的原理、技术、应用领域

引言 在当今数字化时代&#xff0c;区块链技术已经成为全球范围内备受瞩目的话题。从金融到供应链&#xff0c;从物联网到数字身份&#xff0c;区块链正在以惊人的速度渗透到各个行业&#xff0c;并在重塑着我们的社会和经济格局。 区块链最初因其作为比特币背后技术的而引起…

OrionX vGPU研发测试场景下最佳实践之SSH模式

开发机场景概述 目前很多企业在做AI开发的场景时&#xff0c;对GPU资源的管理都是非常简单粗暴的。他们大多都是以开发小组为管理单位、由运维以台为单位分配给开发工程师使用。而在AI开发中涉及开发的场景和测试的场景&#xff0c;很多是将开发测试甚至训练任务都放在一起来使…

<<编码>> 第 11 章 逻辑门电路--门电路 示例电路

作为门电路的继电器开关电路 info::操作说明 鼠标单击开关切换开合状态 primary::在线交互操作链接 https://cc.xiaogd.net/?startCircuitLinkhttps://book.xiaogd.net/code-hlchs-examples/assets/circuit/code-hlchs-ch11-06-relay-as-gate.txt 作为反相器的继电器开关电路 …

EPON光模块介绍

EPON光模块在依靠光纤网络实现快速可靠的数据传输、增强带宽能力和提高网络效率的过程中发挥着至关重要的作用。在这篇文章中&#xff0c;我们将深入研究EPON光模块的基本概念、各种类型、优点和局限性&#xff0c;全面了解它们在现代电信中的重要性。 EPON光模块的定义 EPON…

ZYNQ 入门笔记(二):动态时钟

文章目录 1 概述1.1 DRP1.2 AXI4-Lite 2 示例2.1 单时钟输出2.2 多时钟输出 3 参考文档 1 概述 Clocking Wizard 可通过配置内部寄存器动态调整输出频率&#xff0c;配置接口可选 DRP 或 AXI4-Lite&#xff0c;其中 AXI4-Lite 实际上是对 DRP 接口的封装 1.1 DRP 通过 DRP 接…

Python文件操作:上下文管理器(with语句)②

文章目录 1. 上下文管理器概述1.1 什么是上下文管理器&#xff1f;1.2 为什么使用上下文管理器&#xff1f;1.3 with语句的基本语法 2. 文件操作中的上下文管理器2.1 使用with语句打开文件2.2 读取文件2.2.1 读取整个文件内容2.2.2 逐行读取文件 2.3 写入文件2.3.1 覆盖写入2.3…