0

0

JavaScript Canvas实现可旋转多等分圆形的频闪效应模拟

花韻仙語

花韻仙語

发布时间:2025-11-15 11:28:02

|

670人浏览过

|

来源于php中文网

原创

javascript canvas实现可旋转多等分圆形的频闪效应模拟

本教程旨在指导如何修改现有JavaScript Canvas代码,以实现将圆形等分为多份并进行旋转,从而更准确地模拟频闪效应。文章将详细解释如何从圆心绘制多条分割线来替代原始的直径绘制方式,并提供修改后的代码示例,帮助开发者解决特定采样频率下180度视觉偏差的问题,并为实现更多等分和自定义颜色提供基础。

引言:频闪效应与圆形分割

频闪效应(Stroboscopic Effect)是一种常见的视觉现象,当观察快速旋转的物体在间歇性光照或采样下时,物体可能看起来静止、反向旋转或以不同速度旋转。在Web前端,我们可以利用JavaScript的Canvas API来模拟这一效果。

原始的代码示例旨在展示频闪效应,但其默认的圆形分割方式是绘制一条穿过圆心的直径(即180度分割)。这种方式在某些特定采样频率下,尤其当输入频率与采样频率接近时,可能无法清晰地展示出物体在180度相位上变化的频闪特性,导致视觉上的混淆或无法观察到预期的效果。为了更灵活、更准确地观察和控制频闪效应,我们需要将圆形划分为更多等份,例如3份或更多,并确保这些分割线能够正确旋转。

核心问题分析:多等分与180°采样问题

用户提出的核心需求是:

立即学习Java免费学习笔记(深入)”;

  1. 将圆形划分为3个或更多等份。
  2. 其中一半分割线能够显示不同的颜色(尽管提供的解决方案主要侧重于分割逻辑,而非复杂的颜色区分)。
  3. 解决在输入频率与采样频率相同时,采样可能出现180度偏差导致视觉效果不准确的问题。

原始代码通过绘制一个从圆周一点到其对角点的线来表示旋转物体,这实际上是将其分成了两部分。要实现多等分,并正确显示每个部分的旋转,我们需要改变绘制策略。关键在于,当绘制多条分割线时,每条线都应该从圆心出发,延伸到圆周上的一个点。

解决方案:从中心绘制多条分割线

为了将圆形分割成N个等份,并让这些分割线随着圆的旋转而移动,我们需要对 render() 函数中的绘制逻辑进行修改。原始代码的绘制方式是 moveTo 到圆周上的一点,然后 lineTo 到其对角点,这本质上是绘制一条直径。当我们需要多条分割线时,这种方法不再适用。正确的做法是:

  1. 确定圆心坐标。
  2. 对于每一个分割点,从圆心 moveTo 到圆心本身。
  3. 然后从圆心 lineTo 到圆周上该分割点对应的位置。

这样,每条分割线都将是半径,并且它们会以圆心为轴心旋转。对于3等分,每条分割线之间的角度差为 360 / 3 = 120 度。

Cutout.Pro
Cutout.Pro

AI驱动的视觉设计平台

下载

代码实现细节:render 函数的修改

以下是针对 stroboscopic_effect.js 文件中 render() 函数的关键修改部分。

function render() {
    context.fillStyle = "#000000";
    context.fillRect(0, 0, canvas_width, canvas_height);

    context.strokeStyle = "#ffffff";
    context.beginPath();
    context.moveTo(canvas_width / 2, 0);
    context.lineTo(canvas_width / 2, canvas_height);
    context.stroke();

    context.strokeStyle = "#ff51ff";
    context.beginPath();

    /* 左侧旋转圆的绘制逻辑修改 */
    const x1 = canvas_width / 4, y1 = canvas_height / 2 + y_offset; // 左侧圆的圆心
    context.moveTo(x1, y1); // 移动到圆心
    // 绘制第一条分割线
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle)));
    context.moveTo(x1, y1); // 移动回圆心
    // 绘制第二条分割线 (120度偏移)
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle + 120)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle + 120)));
    context.moveTo(x1, y1); // 移动回圆心
    // 绘制第三条分割线 (240度偏移)
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle + 240)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle + 240)));

    context.stroke(); // 提交绘制

    context.beginPath();
    context.arc(canvas_width / 4, canvas_height / 2 + y_offset, wheel_radius, 0, 2 * Math.PI);
    context.stroke();

    /* 右侧采样圆的绘制逻辑修改 */
    context.strokeStyle = "#00ff00";
    context.beginPath();
    const x2 = 3 * canvas_width / 4, y2 = y1; // 右侧圆的圆心,与左侧圆y坐标相同
    context.moveTo(x2, y2); // 移动到圆心
    // 绘制第一条分割线
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle)), y1 - wheel_radius * Math.sin(toRadian(camera_angle)));
    context.moveTo(x2, y2); // 移动回圆心
    // 绘制第二条分割线 (120度偏移)
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle + 120)), y2 - wheel_radius * Math.sin(toRadian(camera_angle + 120)));
    context.moveTo(x2, y2); // 移动回圆心
    // 绘制第三条分割线 (240度偏移)
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle + 240)), y2 - wheel_radius * Math.sin(toRadian(camera_angle + 240)));

    context.stroke(); // 提交绘制

    context.beginPath();
    context.arc(3 * canvas_width / 4, canvas_height / 2 + y_offset, wheel_radius, 0, 2 * Math.PI);
    context.stroke();

    // ... (其他文本和Canvas设置保持不变)
}

修改说明:

  1. 定义圆心: 首先,我们为左右两个圆分别定义了圆心坐标 (x1, y1) 和 (x2, y2)。
  2. context.moveTo(x, y) 到圆心: 在绘制每条分割线之前,都使用 context.moveTo(x, y) 将画笔移动到当前圆的圆心。这是实现从中心向外绘制的关键。
  3. context.lineTo(x, y) 到圆周: 接着,使用 context.lineTo(x, y) 绘制一条从圆心到圆周上特定角度点的线。
    • wheel_angle 或 camera_angle 代表当前旋转角度。
    • wheel_angle + 120 和 wheel_angle + 240 分别表示相对于当前角度偏移120度和240度的位置,从而实现3等分。
    • toRadian() 函数将角度转换为弧度,因为 Math.cos() 和 Math.sin() 接受弧度作为参数。
  4. 重复绘制: 对于每个分割线,都需要重复 moveTo 到圆心,然后 lineTo 到新的圆周点。

通用化与扩展:N等分与颜色定制

N等分

要将圆形分割成 N 个等份,只需将 120 和 240 等固定角度替换为基于 N 的计算:

// 假设 divisions 是你想要分割的份数 (例如 3, 4, 6 等)
const divisions = 3; // 可以是任何大于2的整数
const angleIncrement = 360 / divisions;

for (let i = 0; i < divisions; i++) {
    const currentAngle = wheel_angle + i * angleIncrement;
    context.moveTo(x1, y1);
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(currentAngle)), y1 - wheel_radius * Math.sin(toRadian(currentAngle)));
}

通过这种循环结构,你可以轻松地将圆分割成任意数量的等份,使其更具通用性。

颜色定制

如果需要实现“一半分割线是不同颜色”的需求,则需要更精细的绘制控制。例如,如果想让某个扇形区域或某条分割线具有不同颜色,可以这样做:

  1. 绘制不同颜色的分割线: 在循环中,根据 i 的值判断是否更改 context.strokeStyle。
  2. 绘制不同颜色的扇形区域: 如果目标是让分割出的“扇形”区域具有不同颜色,则需要使用 context.arc() 结合 context.lineTo() 和 context.fill() 来绘制和填充扇形,而不是简单地绘制线。这会比仅仅绘制分割线复杂一些,需要计算每个扇形的起始和结束角度。

例如,绘制一个特定颜色的扇形:

// 假设要绘制第一个120度扇形为红色
context.fillStyle = "red";
context.beginPath();
context.moveTo(x1, y1);
context.arc(x1, y1, wheel_radius, toRadian(wheel_angle), toRadian(wheel_angle + angleIncrement));
context.closePath();
context.fill();

// 其他扇形或线条继续绘制

完整代码示例

为了提供一个完整的可运行示例,下面将整合修改后的JavaScript代码。HTML部分保持不变,因为它主要负责设置Canvas元素和用户交互控件。

stroboscopic_effect.js (修改后)

let wheel_angle, camera_angle;
let wheel_rpm, angular_speed;
let wheel_radius;
let frames_skip, cooldown, sampling_rate, frame_no;
let is_paused, direction;
/* original code from  */
/* https://visualize-it.github.io/stroboscopic_effect/simulation.html */

function update() {
    wheel_angle += direction * angular_speed;
    if (wheel_angle > 360) {
        wheel_angle -= 360;
    }
    else if (wheel_angle < 0) {
        wheel_angle += 360;
    }

    if (frame_no == 0) {
        camera_angle = wheel_angle;
        frame_no = frames_skip;
    }
    else {
        frame_no -= 1;
    }
}

function render() {
    context.fillStyle = "#000000";
    context.fillRect(0, 0, canvas_width, canvas_height);

    context.strokeStyle = "#ffffff";
    context.beginPath();
    context.moveTo(canvas_width / 2, 0);
    context.lineTo(canvas_width / 2, canvas_height);
    context.stroke();

    context.strokeStyle = "#ff51ff";
    context.beginPath();

    /* 左侧旋转圆的绘制逻辑修改 */
    const x1 = canvas_width / 4, y1 = canvas_height / 2 + y_offset; // 左侧圆的圆心
    context.moveTo(x1, y1); // 移动到圆心
    // 绘制第一条分割线
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle)));
    context.moveTo(x1, y1); // 移动回圆心
    // 绘制第二条分割线 (120度偏移)
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle + 120)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle + 120)));
    context.moveTo(x1, y1); // 移动回圆心
    // 绘制第三条分割线 (240度偏移)
    context.lineTo(x1 + wheel_radius * Math.cos(toRadian(wheel_angle + 240)), y1 - wheel_radius * Math.sin(toRadian(wheel_angle + 240)));

    context.stroke(); // 提交绘制

    context.beginPath();
    context.arc(canvas_width / 4, canvas_height / 2 + y_offset, wheel_radius, 0, 2 * Math.PI);
    context.stroke();

    /* 右侧采样圆的绘制逻辑修改 */
    context.strokeStyle = "#00ff00";
    context.beginPath();
    const x2 = 3 * canvas_width / 4, y2 = y1; // 右侧圆的圆心,与左侧圆y坐标相同
    context.moveTo(x2, y2); // 移动到圆心
    // 绘制第一条分割线
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle)), y1 - wheel_radius * Math.sin(toRadian(camera_angle)));
    context.moveTo(x2, y2); // 移动回圆心
    // 绘制第二条分割线 (120度偏移)
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle + 120)), y2 - wheel_radius * Math.sin(toRadian(camera_angle + 120)));
    context.moveTo(x2, y2); // 移动回圆心
    // 绘制第三条分割线 (240度偏移)
    context.lineTo(x2 + wheel_radius * Math.cos(toRadian(camera_angle + 240)), y2 - wheel_radius * Math.sin(toRadian(camera_angle + 240)));

    context.stroke(); // 提交绘制

    context.beginPath();
    context.arc(3 * canvas_width / 4, canvas_height / 2 + y_offset, wheel_radius, 0, 2 * Math.PI);
    context.stroke();

    if (mobile) {
        context.font = "15px Arial";
    }
    else {
        context.font = "30px Arial";
    }
    context.textAlign = "center";
    context.fillStyle = "#ffffff";
    context.fillText("Base", canvas_width / 4, 30);
    context.fillText("Teste Aliasing", 3 * canvas_width / 4, 30);
}

function initParams() {
    wheel_angle = Math.random() * 360;
    wheel_rpm = 60;
    rpm_slider.value = wheel_rpm;
    calcSpeed();
    //rpm_display.innerHTML = `Wheel speed: ${wheel_rpm} RPM or ${(angular_speed * fps / 360).toFixed(2)} revolution(s) per second`;
    rpm_display.innerHTML = `Frequência do cículo base: ${(angular_speed * fps / 360).toFixed(2)} Hz (Hertz)`;

    frames_skip = 26;
    calcCooldown();
    fps_slider.value = 60 - frames_skip;
    //fps_display.innerHTML = `Sampling rate: ${sampling_rate.toFixed(2)} Hz; Sampling Time: ${cooldown.toFixed(2)} seconds`;
    fps_display.innerHTML = `Frequência de Amostragem: ${sampling_rate.toFixed(2)} Hz;`;

    wheel_radius = (canvas_width / 4) - 20;
    frame_no = 0;
    paused = false;
    direction = -1;
}

function updateParams(variable) {
    if (variable == 'rpm') {
        wheel_rpm = rpm_slider.value;
        calcSpeed();
        //rpm_display.innerHTML = `Wheel speed: ${wheel_rpm} RPM or ${(angular_speed * fps / 360).toFixed(2)} revolution(s) per second`;
        rpm_display.innerHTML = `Frequência do cículo base: ${(angular_speed * fps / 360).toFixed(2)} Hz (Hertz)`;
    }
    else if (variable == 'fps') {
        frames_skip = 60 - fps_slider.value;
        calcCooldown();
        //fps_display.innerHTML = `Sampling rate: ${sampling_rate.toFixed(2)} Hz; Sampling Time: ${cooldown.toFixed(2)} seconds`;
        fps_display.innerHTML = `Frequência de Amostragem: ${sampling_rate.toFixed(2)} Hz;`;
    }
    else if (variable == 'pause') {
        if (is_paused) {
            is_paused = false;
            pause_button.innerHTML = "Parar";
        }
        else {
            is_paused = true;
            pause_button.innerHTML = "Continuar";
        }
    }
    else if (variable == 'dir') {
        direction *= (-1);
    }
}

function simulate(number) {
    if (number == 1) {
        rpm_slider.value = 80;
        fps_slider.value = 59;
    }
    else if (number == 2) {

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

531

2023.06.20

js获取当前时间
js获取当前时间

JS全称JavaScript,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言;它是一种属于网络的高级脚本语言,主要用于Web,常用来为网页添加各式各样的动态功能。js怎么获取当前时间呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

576

2023.07.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

761

2023.08.03

js是什么意思
js是什么意思

JS是JavaScript的缩写,它是一种广泛应用于网页开发的脚本语言。JavaScript是一种解释性的、基于对象和事件驱动的编程语言,通常用于为网页增加交互性和动态性。它可以在网页上实现复杂的功能和效果,如表单验证、页面元素操作、动画效果、数据交互等。

6283

2023.08.17

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

494

2023.09.01

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

Js中concat和push的区别
Js中concat和push的区别

Js中concat和push的区别:1、concat用于将两个或多个数组合并成一个新数组,并返回这个新数组,而push用于向数组的末尾添加一个或多个元素,并返回修改后的数组的新长度;2、concat不会修改原始数组,是创建新的数组,而push会修改原数组,将新元素添加到原数组的末尾等等。本专题为大家提供concat和push相关的文章、下载、课程内容,供大家免费下载体验。

240

2023.09.14

js截取字符串的方法介绍
js截取字符串的方法介绍

JavaScript字符串截取方法,包括substring、slice、substr、charAt和split方法。这些方法可以根据具体需求,灵活地截取字符串的不同部分。在实际开发中,根据具体情况选择合适的方法进行字符串截取,能够提高代码的效率和可读性 。

303

2023.09.21

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

49

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6.1万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 3.5万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号