新闻中心
J*aScript Canvas实现可旋转多等分圆形的频闪效应模拟

本教程旨在指导如何修改现有J*aScript Canvas代码,以实现将圆形等分为多份并进行旋转,从而更准确地模拟频闪效应。文章将详细解释如何从圆心绘制多条分割线来替代原始的直径绘制方式,并提供修改后的代码示例,帮助开发者解决特定采样频率下180度视觉偏差的问题,并为实现更多等分和自定义颜色提供基础。
引言:频闪效应与圆形分割
频闪效应(Stroboscopic Effect)是一种常见的视觉现象,当观察快速旋转的物体在间歇性光照或采样下时,物体可能看起来静止、反向旋转或以不同速度旋转。在Web前端,我们可以利用J*aScript的Canvas API来模拟这一效果。
原始的代码示例旨在展示频闪效应,但其默认的圆形分割方式是绘制一条穿过圆心的直径(即180度分割)。这种方式在某些特定采样频率下,尤其当输入频率与采样频率接近时,可能无法清晰地展示出物体在180度相位上变化的频闪特性,导致视觉上的混淆或无法观察到预期的效果。为了更灵活、更准确地观察和控制频闪效应,我们需要将圆形划分为更多等份,例如3份或更多,并确保这些分割线能够正确旋转。
核心问题分析:多等分与180°采样问题
用户提出的核心需求是:
- 将圆形划分为3个或更多等份。
- 其中一半分割线能够显示不同的颜色(尽管提供的解决方案主要侧重于分割逻辑,而非复杂的颜色区分)。
- 解决在输入频率与采样频率相同时,采样可能出现180度偏差导致视觉效果不准确的问题。
原始代码通过绘制一个从圆周一点到其对角点的线来表示旋转物体,这实际上是将其分成了两部分。要实现多等分,并正确显示每个部分的旋转,我们需要改变绘制策略。关键在于,当绘制多条分割线时,每条线都应该从圆心出发,延伸到圆周上的一个点。
解决方案:从中心绘制多条分割线
为了将圆形分割成N个等份,并让这些分割线随着圆的旋转而移动,我们需要对 render() 函数中的绘制逻辑进行修改。原始代码的绘制方式是 moveTo 到圆周上的一点,然后 lineTo 到其对角点,这本质上是绘制一条直径。当我们需要多条分割线时,这种方法不再适用。正确的做法是:
- 确定圆心坐标。
- 对于每一个分割点,从圆心 moveTo 到圆心本身。
- 然后从圆心 lineTo 到圆周上该分割点对应的位置。
这样,每条分割线都将是半径,并且它们会以圆心为轴心旋转。对于3等分,每条分割线之间的角度差为 360 / 3 = 120 度。
代码实现细节:render 函数的修改
以下是针对 stroboscopic_effect.js 文件中 render() 函数的关键修改部分。
Visla
AI视频生成器,快速轻松地将您的想法转化为视觉上令人惊叹的视频。
100
查看详情
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设置保持不变)
}修改说明:
- 定义圆心: 首先,我们为左右两个圆分别定义了圆心坐标 (x1, y1) 和 (x2, y2)。
- context.moveTo(x, y) 到圆心: 在绘制每条分割线之前,都使用 context.moveTo(x, y) 将画笔移动到当前圆的圆心。这是实现从中心向外绘制的关键。
-
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() 接受弧度作为参数。
- 重复绘制: 对于每个分割线,都需要重复 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)));
}通过这种循环结构,你可以轻松地将圆分割成任意数量的等份,使其更具通用性。
颜色定制
如果需要实现“一半分割线是不同颜色”的需求,则需要更精细的绘制控制。例如,如果想让某个扇形区域或某条分割线具有不同颜色,可以这样做:
- 绘制不同颜色的分割线: 在循环中,根据 i 的值判断是否更改 context.strokeStyle。
- 绘制不同颜色的扇形区域: 如果目标是让分割出的“扇形”区域具有不同颜色,则需要使用 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(); // 其他扇形或线条继续绘制
完整代码示例
为了提供一个完整的可运行示例,下面将整合修改后的J*aScript代码。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 (vari
able == '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) {以上就是J*aScript Canvas实现可旋转多等分圆形的频闪效应模拟的详细内容,更多请关注其它相关文章!
# 每条
# 新推广渠道网络营销策略
# 开平租房网站建设
# 抖音推广营销怎么做
# 网站建设制作880元
# 幼儿园营销推广计划书
# 宁波关键词排名有限公司
# 网站建设和营运管理
# 兴宁网站优化价格
# 国内公司网站建设工具
# 动森岛建设计图网站
# 划分为
# 表单
# 鼠标
# 多条
# javascript
# 第三条
# 第二条
# 第一条
# 多等
# 分割线
# red
# canva
# cos
# github
# git
# 前端
# js
# html
# java
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
怎么在mac上运行html代码_mac运行html代码方法【指南】
J*aScript设计模式实践_j*ascript代码优化
解决J*aScript中重复选择项的确认对话框显示问题
Yandex搜索引擎官方地址 俄罗斯网络世界的主要入口
如何在复杂的电商平台中优雅地管理共享资源并确保正确重定向,使用spryker-shop/resource-share-page模块助你一臂之力
一加 14R 快充无反应_一加 14R 充电优化
React/Next.js中实现列表项的动态移动与状态管理:兼论唯一键的重要性
PrimeNG Sidebar背景色自定义指南:CSS覆盖与主题化实践
fishbowl官网免费版 fishbowl养鱼网站入口
怎样更改Windows系统的默认安装路径_避免C盘爆满的终极设置【技巧】
蛙漫2台版漫画地址 Manwa2正版网页版链接
汽水音乐在线解析 汽水音乐在线解析入口
Kafka Streams中基于消息头条件过滤消息的实现指南
Lar*el表单中优雅地处理“返回”按钮以规避验证:最佳实践指南
拼多多视频播放卡顿如何处理 拼多多视频播放优化技巧
利用5118提升短视频内容效果_5118短视频关键词优化方法
QQ邮箱在线使用入口 QQ邮箱个人账号网页版登录
c++如何使用Meson构建系统_c++比CMake更快的构建工具
Android Studio计算器C键功能异常排查与修复教程
126邮箱手机版登录官网2026_126手机邮箱免费入口最新
C++ string find函数返回值npos详解_C++字符串查找失败的判断条件
Win11如何开启讲述人功能 Win11屏幕阅读器(讲述人)开启与关闭【教程】
黑猫投诉统一入口官网 消费者权益保护投诉平台
Eclipse怎么运行工程_Eclipse工程运行配置说明
192.168.1.1管理中心入口 192.168.1.1路由器网页设置平台
铃兰之剑为这和平的世界希里技能组及加点推荐
windows10怎么查看本机ip_windows10命令提示符ipconfig使用
现代化 SciPy 一维插值:interp1d 的替代方案与最佳实践
豆包手机助手发布技术预览版:直接嵌入手机系统!努比亚样机发售
Python实时数据流中的动态最值查找策略
Go RPC HTTP服务正确实现与常见陷阱解析
学习通网页版快速入口 学习通官网网页版直接打开
Python中如何避免重复条件判断:利用数据结构实现动态逻辑
怎样使用“本地安全策略”提升Windows安全性_Secpol.msc配置指南【高手】
c++如何使用chrono库处理时间_c++标准库时间与日期操作
Lar*el如何生成PDF或Excel文件_Lar*el文档导出工具与使用教程
如何使用CaptainHook和Composer管理Git钩子_在提交前自动运行代码检查的Composer配置
Typer应用中动态命令行参数的解析与处理
TikTok搜索结果不显示如何解决 TikTok搜索刷新优化方法
菜鸟取件码是什么怎么查 最全查询渠道汇总
天猫双十一预售商品怎么退款_天猫双十一预售退款操作指南
响应式图片在网页设计中的正确实现方法
为什么我的微信朋友圈看不到别人的更新_微信朋友圈更新显示异常解决方法
高德地图沿途添加点失败如何解决 高德多点规划方法
知音漫客官网漫画下载_知音漫客网页版阅读记录
J*a里如何使用forEach遍历Map_Map遍历方法说明
Tailwind CSS line-clamp 布局问题解析与修复指南
Django AJAX 文件上传教程:解决图片无法保存到模型的常见问题
BetterDiscord插件中安全更新用户简介的实践指南
C++如何生成随机数_C++ random库使用方法与范围设置


2025-11-15
浏览次数:次
返回列表
able == '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) {