【征文计划】基于 Rokid JSAR 的 2D 粒子画廊实现:从技术概述到核心代码解析

【征文计划】基于 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 工具启动一个本地服务器

bash 复制代码
serve -p 8080 --cors

2、浏览器看到项目目录即为服务器启动成功

3、浏览器访问如下地址

bash 复制代码
https://m-creativelab.github.io/jsar-dom/?url=http://localhost:8080/main.xsml
相关推荐
whysqwhw4 小时前
KuiklyUI core-ksp 模块分析
github
whysqwhw4 小时前
KuiklyUI 架构文档
github
HelloGitHub5 小时前
求求了,别再让你的 GPU 公开“摸鱼”了!
开源·github·gpu
摆烂且佛系17 小时前
IDEA Maven 仓库配置优先级
github·maven·intellij-idea
foundbug99920 小时前
查看nginx日志文件
linux·nginx·github
whysqwhw21 小时前
使用Wire 基于 KMP实现imdk
github
whysqwhw21 小时前
wire 库介绍
github
绝无仅有21 小时前
某大厂跳动Java面试真题之问题与解答总结(五)
后端·面试·github
绝无仅有21 小时前
某大厂跳动Java面试真题之问题与解答总结(四)
后端·面试·github