【征文计划】基于 Rokid JSAR 的 2D 粒子画廊实现:从技术概述到核心代码解析
前言
Rokid JSAR 的核心价值之一是打破纯 2D Web 界面与纯 3D 空间场景的边界,让开发者以 Web 技术快速构建信息 + 交互双优的空间组件。本文以空间化天气助手为案例,详细解析如何利用 JSAR 实现 2D 天气信息展示(温度、预报)+ 3D 动态天气效果(晴 / 雨 / 雪)的融合应用,同时覆盖从项目搭建到落地的全流程,深入核心代码逻辑与实践体验。
JSAR 技术概述

JSAR 是一款可嵌入空间的 Web 运行时,能支持开发者以 Web 类技术开发可嵌入空间场景并独立运行的空间小程序,Web 类技术包括 JavaScript/TypeScript、Web APIs、WebAssembly;其中可嵌入空间指在当前场景划定长宽高的子空间,供空间组件使用,空间组件包括 3D 物体、2D 界面或带 GUI 的 3D 物体
基于 Canvas 的 2D 粒子画廊效果实现
基于 Rokid JSAR 技术开发的 2D 粒子可视化组件,通过 Canvas 2D 渲染动态粒子效果,结合 JSAR 的 3D 空间能力绑定到 3D 球体等元素,在 AR 环境呈现可交互、可配置的粒子流动效果;支持调整粒子参数,具备鼠标交互与颜色切换功能,可作 AR 场景背景装饰或沉浸式模块
入口文件:main.xsml 空间结构

xsml 代码是 2D 粒子画廊项目的入口配置:通过 head 标签设置项目标题并引入业务逻辑文件 lib/main.ts,再以 space 标签作为 3D 空间容器,内部定义 id 为 canvas 的 sphere 球体元素作为后续 Canvas 粒子纹理的承载载体,为 JSAR 运行时解析 3D 空间结构、绑定粒子效果奠定基础
xml
<xsml version="1.0">
<head>
<title>2D Particles</title>
<script type="module" src="lib/main.ts"></script> <!-- 引入业务逻辑 -->
</head>
<space> <!-- 3D空间容器,所有3D元素必须置于此标签内 -->
<sphere id="canvas"></sphere> <!-- 球体:作为Canvas纹理的载体 -->
</space>
</xsml>
JSAR 集成:lib/main.ts 纹理绑定

JSAR 粒子效果的核心集成逻辑:导入 particles.ts 的 init 和 step 函数,通过 JSAR-DOM API 获取 3D 球体元素,为其绑定 512x512 尺寸的 Canvas 纹理并获取 2D 渲染上下文;初始化粒子系统后,以 16ms 每帧的定时器循环调用 step 函数计算粒子运动并渲染,同时调用 canvas.update 同步更新球体纹理,确保 AR 场景中粒子效果正常显示
typescript
import { init, step } from './particles';
const canvasPanel = spatialDocument.getSpatialObjectById('canvas');
const canvas = canvasPanel.attachCanvasTexture(512, 512);
const ctx = canvas.getContext();
init(ctx, 512, 512);
setInterval(() => {
step(ctx);
canvas.update();
}, 16);
粒子核心:lib/particles.ts 运动与渲染

生成粒子网格:init() 创建由 ROWS × COLS 粒子组成的点阵,每个粒子记录初始位置
计算动态交互:step() 中计算每个粒子与目标点(自动移动或鼠标控制)的距离与吸引力
模拟物理行为:粒子受吸引力、阻尼 (DRAG) 和回弹 (EASE) 影响产生流动效果
绘制画面:利用 ctx.createImageData 与 putImageData 将所有粒子以像素点形式渲染到画布上
整体效果:实现一个动态的粒子场动画,粒子随时间或交互呈现聚拢、散开的视觉变化
js
const ROWS = 100;
const COLS = 300;
let NUM_PARTICLES = (ROWS * COLS),
THICKNESS = Math.pow(80, 2),
SPACING = 3,
MARGIN = 100,
COLOR = 220,
DRAG = 0.95,
EASE = 0.25,
particle,
list,
tog,
man,
dx, dy,
mx, my,
d, t, f,
a, b,
n,
p, s,
r, c
;
let w, h;
let i;
particle = {
vx: 0,
vy: 0,
x: 0,
y: 0
};
export function init(ctx, width, height) {
man = false;
tog = true;
list = [];
w = width = COLS * SPACING + MARGIN * 2;
h = height = ROWS * SPACING + MARGIN * 2;
for (i = 0; i < NUM_PARTICLES; i++) {
p = Object.create(particle);
p.x = p.ox = MARGIN + SPACING * (i % COLS);
p.y = p.oy = MARGIN + SPACING * Math.floor(i / COLS);
list[i] = p;
}
}
export function step(ctx) {
if (tog = !tog) {
if (!man) {
t = +new Date() * 0.001;
mx = w * 0.5 + (Math.cos(t * 2.1) * Math.cos(t * 0.9) * w * 0.45);
my = h * 0.5 + (Math.sin(t * 3.2) * Math.tan(Math.sin(t * 0.8)) * h * 0.45);
}
for (i = 0; i < NUM_PARTICLES; i++) {
p = list[i];
d = (dx = mx - p.x) * dx + (dy = my - p.y) * dy;
f = -THICKNESS / d;
if (d < THICKNESS) {
t = Math.atan2(dy, dx);
p.vx += f * Math.cos(t);
p.vy += f * Math.sin(t);
}
p.x += (p.vx *= DRAG) + (p.ox - p.x) * EASE;
p.y += (p.vy *= DRAG) + (p.oy - p.y) * EASE;
}
} else {
b = (a = ctx.createImageData(w, h)).data;
for (i = 0; i < NUM_PARTICLES; i++) {
p = list[i];
b[n = (~~p.x + (~~p.y * w)) * 4] = b[n + 1] = b[n + 2] = COLOR, b[n + 3] = 255;
}
ctx.putImageData(a, 0, 0);
}
}
粒子初始化:init 函数逻辑

定义粒子数据结构与全局配置,通过 init 函数完成粒子初始化:计算基于网格行列数和边距的画布尺寸,按网格规则创建粒子数组,为每个粒子分配均匀分布的初始位置,使当前位置与初始位置一致
typescript
export function init(ctx, width, height) {
man = false;
tog = true;
list = [];
w = width = COLS * SPACING + MARGIN * 2;
h = height = ROWS * SPACING + MARGIN * 2;
for (i = 0; i < NUM_PARTICLES; i++) {
p = Object.create(particle);
p.x = p.ox = MARGIN + SPACING * (i % COLS);
p.y = p.oy = MARGIN + SPACING * Math.floor(i / COLS);
list[i] = p;
}
}
粒子帧更新:step 函数优化

通过 step 函数实现粒子帧更新:定义阻尼、缓动等全局配置,采用双帧分工逻辑,一帧计算粒子运动,另一帧通过 ImageData 直接操作像素数组渲染粒子
js
export function step(ctx) {
if (tog = !tog) {
if (!man) {
t = +new Date() * 0.001;
mx = w * 0.5 + (Math.cos(t * 2.1) * Math.cos(t * 0.9) * w * 0.45);
my = h * 0.5 + (Math.sin(t * 3.2) * Math.tan(Math.sin(t * 0.8)) * h * 0.45);
}
for (i = 0; i < NUM_PARTICLES; i++) {
p = list[i];
d = (dx = mx - p.x) * dx + (dy = my - p.y) * dy;
f = -THICKNESS / d;
if (d < THICKNESS) {
t = Math.atan2(dy, dx);
p.vx += f * Math.cos(t);
p.vy += f * Math.sin(t);
}
p.x += (p.vx *= DRAG) + (p.ox - p.x) * EASE;
p.y += (p.vy *= DRAG) + (p.oy - p.y) * EASE;
}
} else {
b = (a = ctx.createImageData(w, h)).data;
for (i = 0; i < NUM_PARTICLES; i++) {
p = list[i];
b[n = (~~p.x + (~~p.y * w)) * 4] = b[n + 1] = b[n + 2] = COLOR, b[n + 3] = 255;
}
ctx.putImageData(a, 0, 0);
}
}
项目效果验证

借助 glTF Tools 插件可高效验证项目效果:其能解析 3D 资源(如 model/welcome.glb 模型、组件结构),预览 3D 球体形态、Canvas 粒子纹理与球体的绑定适配效果,还可提前排查纹理映射偏差、模型加载兼容性等问题,避免后续 JSAR 运行时出现粒子错位、3D 渲染异常
1、serve 工具启动一个本地服务器
bashserve -p 8080 --cors
2、浏览器看到项目目录即为服务器启动成功
3、浏览器访问如下地址
bashhttps://m-creativelab.github.io/jsar-dom/?url=http://localhost:8080/main.xsml