跳转到内容

Web Player API

Cavalry Web Player 和 API 目前处于测试阶段,因此可能会发生变化。

Cavalry Web Player 可用于在网页浏览器中播放和交互 Cavalry 场景。本文档提供了完整的集成指南和 API 参考。

Web Player 由两个主要部分组成。

  1. WebAssembly 运行时
  2. JavaScript 模块

JavaScript 模块用于实例化 Web Player。Web Player 包含 4 个方面。

  1. Module — 导入的 CavalryWasm 模块的实例
  2. Player — 控制播放、资源管理等功能的 Cavalry 播放器(类似于 Cavalry 的脚本 API
  3. Canvas — 用作视图的 canvas 元素
  4. Surface — 将场景渲染到 canvas 的 WebGL 表面
// Import the Cavalry moduleconst wasm = await import("../wasm-lib/CavalryWasm.js")// Initialise the moduleconst Module = await wasm.default({ // `locateFile` is required for the module to load the wasm files locateFile: (path) => `../wasm-lib/${path}`, print: (text) => console.log(text), printErr: (text) => console.error(text),})// Load a scene fileconst response = await fetch("scene.cv")const sceneData = await response.arrayBuffer()Module.FS.writeFile("scene.cv", new Uint8Array(sceneData))// Create a player instanceconst player = Module.Cavalry.MakeWithPath("scene.cv")// Read the scene resolutionconst scene = player.getSceneResolution()const canvas = document.getElementById("canvas")canvas.width = scene.widthcanvas.height = scene.height// Set up a rendering surfaceconst surface = Module.makeWebGLSurfaceFromElement( canvas, scene.width, scene.height,)// Render the current frameplayer.render(surface)// Use `requestAnimationFrame` for efficient playbacklet animationFrameId = 0const runPlaybackLoop = () => { const tick = (timestamp) => { // `tick` advances the frame based on the elapsed time const status = player.tick(surface, timestamp); // Optionally update the UI when a frame changes if (status.frameChanged) { console.log("Frame", status.currentFrame); } animationFrameId = requestAnimationFrame(tick) } animationFrameId = requestAnimationFrame(tick)}runPlaybackLoop()player.play()// To stop playback// player.stop();// cancelAnimationFrame(animationFrameId);

使用以下方式导入模块:

// Import the Cavalry moduleconst wasm = await import("../wasm-lib/CavalryWasm.js")// Initialise the moduleconst Module = await wasm.default({ // `locateFile` is required for the module to load the wasm files locateFile: (path) => `../wasm-lib/${path}`, // Logs diagnostic data to the console print: (text) => console.log(text), // Logs errors to the console printErr: (text) => console.error(text),})

makeWebGLSurface(canvasId:string, width:number, height:number) → SkSurface

标题为“makeWebGLSurface(canvasId:string, width:number, height:number) → SkSurface”的章节

使用指定的 canvas 元素初始化 WebGL2 上下文和 Skia 表面。

参数:

  • canvasId: 画布 HTML 元素的 ID。
  • width: 渲染表面的宽度。
  • height: 渲染表面的高度。

返回值: 绑定到 WebGL 上下文的 Skia SkSurface

const surface = Module.makeWebGLSurface("canvas", 800, 600);

makeWebGLSurfaceFromElement(canvas:emscripten::val, width:number, height:number) → SkSurface

标题为“makeWebGLSurfaceFromElement(canvas:emscripten::val, width:number, height:number) → SkSurface”的章节

使用指定的 canvas 元素初始化 WebGL2 上下文和 Skia 表面。

参数:

  • canvas: 画布 HTML 元素。
  • width: 渲染表面的宽度。
  • height: 渲染表面的高度。

返回值: 绑定到 WebGL 上下文的 Skia SkSurface

const surface = Module.makeWebGLSurfaceFromElement(canvas, canvas.width, canvas.height);

将字体从虚拟文件系统加载到运行时字体注册表中。

参数:

  • path: 虚拟文件系统中字体文件的路径(例如 .ttf.otf.woff.woff2)。
  • alias(可选): 注册字体时使用的自定义名称。如果提供,字体将使用此别名而不是其内部族名进行注册。这对于匹配外部命名系统(例如 CSS font-family 名称)非常有用。

返回值: 注册的族名(如果提供了别名则为别名,否则为字体的内部族名)。

// Load font using its internal family nameModule.FS.writeFile("Montserrat.ttf", fontBytes);const familyName = Module.loadFont("Montserrat.ttf", "");// familyName = "Montserrat" (from font's internal name)// Load font with a custom aliasModule.FS.writeFile("AC-Compacta.woff", fontBytes);const familyName = Module.loadFont("AC-Compacta.woff", "my-custom-id");// familyName = "my-custom-id"// Use the alias with other font APIsconst styles = player.getFontStyles("my-custom-id");player.setAttributeFont("textShape#1", "font", { font: "my-custom-id", style: "Regular" });

获取已加载字体的详细元数据,包括所有可用样式及其 CSS 兼容值。

参数:

  • familyName: 注册的族名(来自 loadFont()queryFonts())。

返回值: 包含字体名称变体和样式信息的 FontMetadata 对象。

// Load a fontconst familyName = Module.loadFont("Inter.ttf", "");// Get metadataconst metadata = Module.getFontMetadata(familyName);console.log("Family:", metadata.registeredFamily);console.log("Styles:", metadata.styles);// Use style info for CSS mappingmetadata.styles.forEach(style => { console.log(`${style.name}: weight=${style.weight}, slant=${style.slant}`);});// Output: "Regular: weight=400, slant=normal"// Output: "Bold: weight=700, slant=normal"// Output: "Italic: weight=400, slant=italic"

从给定的 URL 加载字体数据并将其加载到运行时字体注册表中。

参数:

  • url(字符串): 获取字体数据的 URL。
// Load a font from a URLModule.loadFontData("/url/to/font.ttf");

使用以下方式创建实例:

const player = Module.Cavalry.Make("scene.cv");

当通过 MakeWithPath 加载场景时,其资源将位于 pendingAssets 数组中。然后可以通过遍历该数组来加载资源。

有关实现示例,请参见加载资源

const player = Module.Cavalry.MakeWithPath("scene.cv");console.log(Module.pendingAssets)

向此对象添加 canvas 元素允许 CavalryWasm 模块在 canvas 位于 Shadow DOM 中时找到它。

请参见 Shadow DOM

const id = "my-canvas"const canvas = document.createElement("canvas")canvas.id = idModule.specialHTMLTargets[id] = canvas

构造并返回一个新的 Cavalry 类实例。

参数:

  • scenePath(可选): 虚拟文件系统中场景文件的路径。默认为 "scene.cv" 以保持向后兼容。
// Load default scene.cvconst player = Module.Cavalry.Make();// Load a specific scene fileconst player = Module.Cavalry.Make("myAnimation.cv");

构造并返回一个新的 Cavalry 类实例。

这是 Make 的替代方法。

参数:

  • scenePath: 虚拟文件系统中场景文件的路径。
// Load a specific scene fileconst player = Module.Cavalry.MakeWithPath("scene.cv");

将当前帧渲染到指定的 WebGL Skia 表面。

player.render(surface);

获取当前合成画面的分辨率。

返回值: Resolution 类型的对象。

const resolution = player.getSceneResolution();console.log(resolution.width, resolution.height);

replaceImageAsset(filePath:string, assetId:string)

标题为“replaceImageAsset(filePath:string, assetId:string)”的章节

assetId 处的图像资源替换为给定路径的图像。

Module.FS.writeFile("image.png", imageBytes);player.replaceImageAsset("image.png", "asset#2");player.render(surface);

replaceImageAssetData(assetId:string, image:string)

标题为“replaceImageAssetData(assetId:string, image:string)”的章节

assetId 处的图像资源替换为虚拟文件系统中的图像。

player.replaceImageAssetData("asset#2", "test.png");player.render(surface);

replaceFontAsset(filePath:string, assetId:string)

标题为“replaceFontAsset(filePath:string, assetId:string)”的章节

assetId 处的字体资源替换为虚拟文件系统中的字体。

player.replaceFontAsset("font.ttf", "asset#2");player.render(surface);

replaceCSVAsset(filePath:string, assetId:string)

标题为“replaceCSVAsset(filePath:string, assetId:string)”的章节

assetId 处的 CSV 资源替换为给定路径的 CSV。

轻量快速的 Web CSV 解析器与 Cavalry 自带的大型全功能解析器相比功能较为基础。目前不支持自定义分隔符和引号。如果您在使用 CSV 文件时遇到问题,请告知我们。

Module.FS.writeFile("test.csv", csvBytes);player.replaceCSVAsset("text.csv", "asset#2");player.render(surface);

replaceCSVAssetData(assetId:string, csvData:string)

标题为“replaceCSVAssetData(assetId:string, csvData:string)”的章节

assetId 处的 CSV 资源替换为 CSV 数据。

请注意 — 轻量快速的 Web CSV 解析器与 Cavalry 自带的大型全功能解析器相比功能较为基础。目前不支持自定义分隔符和引号。如果您在使用 CSV 文件时遇到问题,请告知我们。

player.replaceCSVAssetData("asset#2", "test.csv");player.render(surface);

replaceExcelAsset(filePath:string, assetId:string)

标题为“replaceExcelAsset(filePath:string, assetId:string)”的章节

assetId 处的 Excel 资源替换为虚拟文件系统中的 Excel 资源。

player.replaceExcelAsset("sheet.xlsx", "asset#2");player.render(surface);

replaceExcelAssetData(assetId:string, csvData:string)

标题为“replaceExcelAssetData(assetId:string, csvData:string)”的章节

assetId 处的 CSV 资源替换为 CSV 数据。

player.replaceExcelAssetData("asset#2", "test.csv");player.render(surface);

replaceSVGAsset(filePath:string, assetId:string)

标题为“replaceSVGAsset(filePath:string, assetId:string)”的章节

assetId 处的 SVG 资源替换为虚拟文件系统中的 SVG 资源。

player.replaceExcelAsset("image.svg", "asset#2");player.render(surface);

replaceSVGAssetData(assetId:string, svgData:string)

标题为“replaceSVGAssetData(assetId:string, svgData:string)”的章节

assetId 处的 SVG 资源替换为 SVG 数据。

player.replaceExcelAssetData("asset#2", "image.svg");player.render(surface);

replaceGoogleSheet(sheetUrl:string, assetId:string)

标题为“replaceGoogleSheet(sheetUrl:string, assetId:string)”的章节

Google Sheets 会自动加载。使用 replaceGoogleSheet 在运行时替换 Google Sheet 资源。

将 Google Sheet 的共享权限设置为”知道链接的任何人”以避免错误。

const sheetId = "UNIQUE_ID_OF_GOOGLE_SHEET"const url = `https://docs.google.com/spreadsheets/d/${sheetId}`player.replaceGoogleSheet(url, assetId);

返回当前活动合成画面的 ID。

const compId = player.getActiveComp();

getInConnection(layerId:string, attrId:string) → string

标题为“getInConnection(layerId:string, attrId:string) → string”的章节

返回属性的输入连接。如果该属性没有输入,则返回空字符串。

const path = player.getInConnection("basicShape#2", "scale");

disconnect(fromLayerId:string, fromAttrId:string, toLayerId:string, toAttrId:string)

标题为“disconnect(fromLayerId:string, fromAttrId:string, toLayerId:string, toAttrId:string)”的章节

移除属性之间的连接。当连接被移除时返回空字符串。

player.disconnect("basicShape#2", "position", "basicShape#2", "scale");const path = player.getInConnection("basicShape#2", "scale"); console.log("Connection for Scale Attribute after disconnect is", path);

设置合成画面的时间倍率。将其设置为 2.0 意味着合成画面将以两倍速度播放。

player.setCompTimeMultiplier(0.5);

通过 ID 设置活动合成画面。如果找不到该合成画面,则会记录警告。

setAttributeViaStringify(layerId:string, jsonString:string)

标题为“setAttributeViaStringify(layerId:string, jsonString:string)”的章节

通过传递 JSON 字符串来设置图层上的一个或多个属性。

参数:

  • layerId: 要修改的图层的 ID。
  • jsonString: 将属性名称映射到值的 JSON 字符串。
const color = JSON.stringify({ backgroundColor: { r: 255, g: 0, b: 128 } });player.setAttributeViaStringify(player.getActiveComp(), color);player.render(surface);const playbackStep = JSON.stringify({ playbackStep: 2 });player.setAttributeViaStringify(player.getActiveComp(), playbackStep);

setAttribute(layerId:string, attrId:string, value:any)

标题为“setAttribute(layerId:string, attrId:string, value:any)”的章节

设置图层上的属性。值类型会自动检测并转换为适当的 C++ 类型。

参数:

  • layerId: 要修改的图层的 ID。
  • attrId: 要修改的属性。
  • value: 要设置的值。支持的类型:
    • 数字: 根据属性类型自动检测为 doubleintenum
    • 字符串: 用于文本和富文本属性
    • 布尔值: 用于布尔属性
    • 数组: [x, y] 用于 2D 向量,[x, y, z] 用于 3D 向量,[r, g, b][r, g, b, a] 用于颜色
    • 对象: {x, y}{x, y, z} 用于向量,{r, g, b}{r, g, b, a} 用于颜色
// Numbers (auto-detected as double, int, or enum based on attribute type)player.setAttribute("textShape#1", "fontSize", 24.5);player.setAttribute("duplicator#1", "count", 10);player.setAttribute("dropdown#1", "alignment", 2);// Stringsplayer.setAttribute("textShape#1", "text", "Hello World!");// Booleansplayer.setAttribute("shape#1", "visible", true);// 2D vectors (array or object syntax)player.setAttribute("shape#1", "position", [100, 200]);player.setAttribute("shape#1", "scale", {x: 1.5, y: 2.0});// 3D vectors (array or object syntax)player.setAttribute("camera#1", "position", [10, 20, 30]);player.setAttribute("transform#1", "rotation", {x: 0, y: 45, z: 0});// Colours (array or object syntax, values 0-255)player.setAttribute("shape#1", "fillColor", [255, 0, 0, 255]);player.setAttribute("shape#1", "material.materialColor", {r: 128, g: 64, b: 255, a: 255});// Composition background colourplayer.setAttribute(player.getActiveComp(), "backgroundColor", {r: 30, g: 30, b: 30});

getAttribute(layerId:string, attrId:string) → any

标题为“getAttribute(layerId:string, attrId:string) → any”的章节

从图层获取属性值。返回类型取决于属性类型。

参数:

  • layerId: 要查询的图层的 ID。
  • attrId: 要获取的属性。

返回值: 属性值。返回类型:

  • double/int/enum: number
  • bool: boolean
  • string/richText: string
  • double2/int2: [x, y] 数组
  • double3/int3: [x, y, z] 数组
  • color: {r, g, b, a} 对象,值范围为 0-255
// Numbersconst fontSize = player.getAttribute("textShape#1", "fontSize");const count = player.getAttribute("duplicator#1", "count");// Stringsconst text = player.getAttribute("textShape#1", "text");// Booleansconst isVisible = player.getAttribute("shape#1", "visible");// 2D vectors (returns array)const position = player.getAttribute("shape#1", "position");console.log(`Position: ${position[0]}, ${position[1]}`);// 3D vectors (returns array)const rotation = player.getAttribute("transform#1", "rotation");console.log(`Rotation: ${rotation[0]}, ${rotation[1]}, ${rotation[2]}`);// Colours (returns object)const color = player.getAttribute("shape#1", "material.materialColor");console.log(`Color: R=${color.r}, G=${color.g}, B=${color.b}, A=${color.a}`);// Convert colour to CSS hexconst hex = "#" + [color.r, color.g, color.b] .map(x => x.toString(16).padStart(2, "0")) .join("");

getAttributeViaStringify(layerId:string, attrId:string) → string

标题为“getAttributeViaStringify(layerId:string, attrId:string) → string”的章节

将任何属性值作为 JSON 字符串获取。这是没有特定 getter 的复杂属性类型的备用方法。

参数:

  • layerId: 要查询的图层的 ID。
  • attrId: 要获取的属性。

返回值: 属性值的 JSON 字符串表示。

const jsonString = player.getAttributeViaStringify("basicShape#1", "customData");const data = JSON.parse(jsonString);

getAttributeName(layerId:string, attrId:string) → string

标题为“getAttributeName(layerId:string, attrId:string) → string”的章节

获取属性名称。可以在 Cavalry 中通过在Attribute Editor中右键点击属性并选择”Rename…”来设置属性名称。

参数:

  • layerId: 要查询的图层的 ID。
  • attrId: 要获取的属性。

返回值: 属性名称或属性 ID。

const name = player.getAttributeName("basicShape#1", "position");

getAttributeDefinition(layerId:string, attrId:string) → AttributeDefinition

标题为“getAttributeDefinition(layerId:string, attrId:string) → AttributeDefinition”的章节

获取有关属性的详细元数据,包括其类型、约束、默认值和结构。这对于构建需要了解属性特征的动态用户界面特别有用。

参数:

  • layerId: 要查询的图层的 ID。
  • attrId: 要检查的属性。

返回值: 包含全面属性元数据的 AttributeDefinition 对象。

// Get definition for a numeric attribute with constraintsconst sizeDef = player.getAttributeDefinition("pixelateFilter#1", "size");console.log(`Type: ${sizeDef.type}`);console.log(`Default: ${sizeDef.defaultValue}`);console.log(`Min: ${sizeDef.numericInfo.hardMin}, Max: ${sizeDef.numericInfo.hardMax}`);// Use definition to create appropriate UIif (sizeDef.type === "int" && sizeDef.numericInfo.hasHardMin && sizeDef.numericInfo.hasHardMax) { // Create slider with proper min/max values const slider = document.createElement("input"); slider.type = "range"; slider.min = sizeDef.numericInfo.hardMin; slider.max = sizeDef.numericInfo.hardMax; slider.value = JSON.parse(sizeDef.defaultValue);}

AttributeDefinition 属性:

interface AttributeDefinition { attrId: string; // Attribute id type: string; // Data type (int, double, bool, string, Color, etc.) prefix: string; // UI prefix text (e.g X, or R) placeholder: string; // UI placeholder text for string attributes defaultValue: string; // JSON-serialized default value isArray: boolean; // Whether this is an array attribute isCompound: boolean; // Whether this has child attributes isDynamic: boolean; // Whether this is dynamically created isAttrReadOnly: boolean; // Whether this is read-only allowsConstrainProportions: boolean; // Whether proportions can be constrained multiline: boolean; // Whether text should be multiline numericInfo: NumericInfo; // Numeric constraint information enumValues: number[]; // Enum option values children: AttributeDefinition[]; // Child attribute definitions}interface NumericInfo { hardMin: number; // Hard minimum constraint hardMax: number; // Hard maximum constraint softMin: number; // Soft minimum constraint softMax: number; // Soft maximum constraint step: number; // Step increment hasHardMin: boolean; // Whether hardMin is set hasHardMax: boolean; // Whether hardMax is set hasSoftMin: boolean; // Whether softMin is set hasSoftMax: boolean; // Whether softMax is set hasStep: boolean; // Whether step is set}

getControlCentreAttributes(compId:string) → [string]

标题为“getControlCentreAttributes(compId:string) → [string]”的章节

获取给定合成画面的所有控制中心属性路径列表。

参数:

  • compId: 要查询的合成画面的 ID(通常从 getActiveComp() 获取)。

返回值: 表示属性路径的字符串数组,格式为 "layerId.attributeId"

// Get active composition and its control centre attributesconst activeComp = player.getActiveComp();const attributes = player.getControlCentreAttributes(activeComp);console.log("Control Centre Attributes:", attributes);// Output example: [ "pixelateFilter#1.size", "pixelateFilter#1.outline", "pixelateFilter#1.borderColor" ]// Generate dynamic UI for each Attributeattributes.forEach((attrPath) => { const [layerId, attrId] = attrPath.split("."); // Get attribute definition to determine UI type const definition = player.getAttributeDefinition(layerId, attrId); // Create appropriate control based on type switch (definition.type) { case "int": // Create integer input or slider break; case "bool": // Create checkbox break; case "color": // Create color picker break; // ... handle other types }});

获取给定字体的可变字体轴信息。可变字体包含可调节的轴(如宽度、字重、倾斜度),允许对字体的外观进行精细控制。此方法返回有关每个可用轴的元数据,包括其名称、标签、范围和当前值。

参数:

  • fontName: 字体族名称(例如 “Roboto”、“Inter”)。
  • fontStyle: 字体样式(例如 “Regular”、“Bold”)。

返回值: FontAxisInfo 对象数组,每个可变字体轴对应一个。如果字体不是可变字体或没有轴,则返回空数组。

// Query available fonts and their stylesconst fonts = player.queryFonts();const styles = player.getFontStyles(fonts[0]);// Get axis information for a variable fontconst axisInfo = player.getFontAxisInfo("Roboto Flex", "Regular");if (!axesInfo.length) { console.warn("This font has no variable axes (not a variable font)");} else { console.log(`Font has ${axisInfo.length} variable axes`); console.log(axisInfo) // Create sliders for each axis axisInfo.forEach(axis => { const slider = document.createElement("input"); slider.type = "range"; slider.min = axis.minValue; slider.max = axis.maxValue; slider.value = axis.currentValue; slider.addEventListener("input", (e) => { // Update font axis value when slider changes console.log(`${axis.axisName} set to ${e.target.value}`); }) })}

fetchAndProcess(zipUrl:string) → Promise<void>

标题为“fetchAndProcess(zipUrl:string) → Promise<void>”的章节

获取并处理 ZIP 文件,解压其内容并在播放器中创建图像序列资源。

参数:

  • zipUrl(字符串): 要获取和处理的 ZIP 文件的 URL。
await player.fetchAndProcess("/url/to/ImageSequence.zip");

Web Player 提供了一个原生播放管理器来处理帧计时和渲染。

提供两种播放模式:

  • 精确模式(默认): 基于墙上时钟的计时,跳过帧以保持正确的时序。最适合音频同步和实时播放。
  • 平滑模式: 按顺序显示每一帧,但在复杂场景上可能比实时播放慢。最适合逐帧精确预览。
// Start playbackplayer.play();// Run the playback loopfunction runPlaybackLoop() { const tick = (timestamp) => { if (!player.isPlaying()) { return; } const status = player.tick(surface, timestamp); // Update UI if needed if (status.frameChanged) { console.log(status) } requestAnimationFrame(tick); }; requestAnimationFrame(tick);}runPlaybackLoop();// Stop playbackplayer.stop();

从当前帧位置开始播放。在启动 requestAnimationFrame 循环之前调用此方法。

player.play();runPlaybackLoop();

停止播放。当前帧位置被保留,因此再次调用 play() 将从停止的位置恢复播放。

player.stop();cancelAnimationFrame(animationFrameId);

切换播放状态。如果正在播放,则停止播放。如果已停止,则开始播放。

player.toggle();console.log(player.isPlaying() ? "Stop" : "Play");

返回当前是否正在播放。

console.log("Playback is running?", player.isPlaying());

根据经过的时间推进播放并渲染当前帧。这应该从 requestAnimationFrame 回调中调用。

参数:

  • surface: 要渲染到的 Skia 表面。
  • timestampMs: 来自 requestAnimationFrame 的时间戳(DOMHighResTimeStamp)。

返回值: 包含当前播放状态的 PlaybackStatus 对象。

const tick = (timestamp) => { if (!player.isPlaying()) { return; } const status = player.tick(surface, timestamp); if (status.frameChanged) { console.log("Frame", status.currentFrame); } requestAnimationFrame(tick);}

设置当前帧。帧号会被限制在有效的播放范围内(getStartFrame()getEndFrame() 之间)。

注意: 如果正在播放,请在设置帧之前停止播放,然后可以选择重新启动。

// Seek to a specific frameconst wasPlaying = player.isPlaying();player.stop();player.setFrame(50);player.render(surface);if (wasPlaying) { player.play(); runPlaybackLoop();}

返回当前帧号。

const currentFrame = player.getFrame();console.log("Current frame", currentFrame);

返回当前合成画面播放范围的起始帧。

const startFrame = player.getStartFrame();console.log(startFrame);

返回当前合成画面播放范围的结束帧。

const endFrame = player.getEndFrame();console.log(endFrame);

返回当前合成画面的每秒帧数(FPS)。

const fps = player.getFPS();console.log(`Scene runs at ${fps} FPS`);

返回活动播放期间测量的播放 FPS。这是在 1 秒窗口内计算的,可用于监控播放性能。

const actualFps = player.getActualFPS();console.log(`${actualFps.toFixed(1)} FPS`);

设置播放模式。

参数:

  • mode: 0 为精确模式(默认),1 为平滑模式。
// Use Smooth mode for frame-accurate previewplayer.setPlaybackMode(1);// Use Accurate mode for real-time playbackplayer.setPlaybackMode(0);

返回当前播放模式。0 = 精确模式,1 = 平滑模式。

const mode = player.getPlaybackMode();console.log(mode === 0 ? "Accurate" : "Smooth");

设置播放到达结束帧时是否应循环播放。

参数:

  • loop: true 启用循环(默认),false 在结束时停止。
player.setLoop(false);

返回是否启用了播放循环。

console.log("Playback will loop?", player.isLooping());

手动推进到下一帧。这是一个用于手动帧控制的低级方法 — 对于大多数用例,请使用 tick() 代替,它会自动处理计时。

player.incrementFrame();player.render(surface);
interface Resolution { width: number; height: number;}

表示场景或画布分辨率。

interface ColorRGBA { r: number; // Red component (0-255) g: number; // Green component (0-255) b: number; // Blue component (0-255) a: number; // Alpha component (0-255)}

表示 RGBA 颜色值,各分量范围为 0-255。通过 getAttribute() 获取颜色属性时返回。

const color = player.getAttribute("shape#1", "fillColor");// Convert to CSS hex colourconst hex = "#" + [color.r, color.g, color.b] .map(x => x.toString(16).padStart(2, "0")) .join("");// Set a colour using an objectplayer.setAttribute("shape#1", "fillColor", {r: 255, g: 128, b: 0, a: 255});
interface Double2 { x: number; // X component y: number; // Y component}

表示双精度 2D 向量。请注意,getAttribute() 为了方便,将 2D 向量作为数组 [x, y] 返回。

// getAttribute returns an array [x, y]const position = player.getAttribute("shape#1", "position");console.log(`Position: (${position[0]}, ${position[1]})`);// Set a new position using array syntaxplayer.setAttribute("shape#1", "position", [position[0] + 10, position[1] + 20]);// Or use object syntaxplayer.setAttribute("shape#1", "position", {x: 100, y: 200});
interface Double3 { x: number; // X component y: number; // Y component z: number; // Z component}

表示双精度 3D 向量。请注意,getAttribute() 为了方便,将 3D 向量作为数组 [x, y, z] 返回。

// getAttribute returns an array [x, y, z]const cameraPos = player.getAttribute("camera#1", "position");console.log(`Camera at: (${cameraPos[0]}, ${cameraPos[1]}, ${cameraPos[2]})`);// Set camera position using array syntaxplayer.setAttribute("camera#1", "position", [ cameraPos[0], cameraPos[1], cameraPos[2] + 100]);// Or use object syntaxplayer.setAttribute("camera#1", "position", { x: 10, y: 20, z: 30 });
interface FontMetadata { registeredFamily: string; // The name used for registration (use with getFontStyles) registeredStyle: string; // Default style name fullName: string; // Full font name from the font file preferredFamily: string; // Preferred family name (may differ from registeredFamily) preferredSubfamily: string; // Preferred subfamily name postscriptName: string; // PostScript name wwsFamilyName: string; // WWS (Weight Width Slope) family name styles: FontStyleInfo[]; // All available styles with CSS-compatible values}

包含字体元数据,包括字体名称表中的名称变体和所有可用样式。由 getFontMetadata() 返回。

const metadata = Module.getFontMetadata("Inter");console.log(metadata.registeredFamily); // "Inter"console.log(metadata.styles); // Array of FontStyleInfo objects
interface FontStyleInfo { name: string; // Style name (e.g., "Regular", "Bold Italic") weight: number; // CSS font-weight: 100-900 (400=normal, 700=bold) slant: string; // CSS font-style: "normal", "italic", or "oblique" width: number; // CSS font-stretch: 1-9 (5=normal, 3=condensed, 7=expanded)}

表示具有 CSS 兼容值的字体样式。使用这些值将 Cavalry 字体样式映射到 CSS 属性。

const metadata = Module.getFontMetadata("Roboto");metadata.styles.forEach(style => { // Create CSS font-face rules or match styles console.log(`@font-face { font-family: "${style.name}"; font-weight: ${style.weight}; font-style: ${style.slant}; }`);});
interface FontAxisInfo { axisName: string; // Human-readable name (e.g., "Weight", "Width") axisTag: string; // Four-character axis tag (e.g., "wght", "wdth") minValue: number; // Minimum allowed value for this axis maxValue: number; // Maximum allowed value for this axis defaultValue: number; // Default value for this axis currentValue: number; // Current value for this axis}

表示可变字体轴的信息。可变字体可以有多个轴,允许对字体的外观进行精细控制。常见的轴包括字重(wght)、宽度(wdth)、倾斜度(slnt)和光学尺寸(opsz)。

// Get variable font axis informationconst axisInfo = player.getFontAxisInfo("Inter", "Regular");console.log(axisInfo)
interface PlaybackStatus { isPlaying: boolean; // Whether playback is currently active currentFrame: number; // The current frame number frameChanged: boolean; // Whether the frame changed since the last tick actualFPS: number; // Measured playback FPS}

tick() 返回,提供有关当前播放状态的信息。使用 frameChanged 来确定何时更新 UI 元素。

const status = player.tick(surface, timestamp);if (status.frameChanged) { console.log(status.currentFrame); console.log("Frame", status.currentFrame);}if (status.actualFPS > 0) { console.log(`${status.actualFPS.toFixed(1)} FPS`)}
enum PlaybackMode { Accurate = 0, // Wall-clock based, skips frames to maintain timing Smooth = 1 // Shows every frame, may run slow}

定义播放管理器如何推进帧:

  • 精确模式(0): 根据经过的墙上时钟时间计算正确的帧。如果渲染较慢,将跳过帧以保持正确的时序。最适合音频同步。
  • 平滑模式(1): 当足够的时间过去时,一次精确推进一帧。从不跳帧,但在复杂场景上播放可能比实时更慢。
// Check current modeconsole.log(player.getPlaybackMode());// Set to Smooth mode for frame-by-frame previewplayer.setPlaybackMode(1);// Set to Accurate mode for real-time playbackplayer.setPlaybackMode(0);

Cavalry Web Player 会自动为某些与资源相关的操作派发事件。这些事件允许自动加载资源和其他集成场景。

事件类型: CustomEvent

当 Cavalry Web Player 检测到场景文件中需要从 Web 服务器加载到虚拟文件系统的图像或字体资源时,自动派发。

事件详情:

interface CavalryAutoLoadAssetDetail { type: "image" | "font" | "csv" | "svg" | "excel" | "googlesheet"; assetId: string; // Asset ID from the scene file (e.g., "asset#2") filename: string; // Extracted filename (e.g., "3.jpg" or "font.ttf")}
// Listen for automatic asset loading eventswindow.addEventListener("cavalryAutoLoadAsset", async (event) => { console.log("Auto-loading asset:", event.detail); const path = "/path/to/assets/" if (event.detail.type === "image") { const { assetId, filename } = event.detail; const response = await fetch(path + filename); if (!response.ok) { throw new Error(`Could not find image ${filename}`); } const imageData = await response.arrayBuffer(); // Write to virtual filesystem Module.FS.writeFile(filename, new Uint8Array(imageData)); // Load the asset player.replaceImageAsset(filename, assetId); // Re-render to show the image player.render(surface); console.log(`Auto-loaded image: ${filename} for ${assetId}`); } if (event.detail.type === "font") { const { assetId, filename } = event.detail; const response = await fetch(path + filename); if (!response.ok) { throw new Error(`Could not find font ${filename}`); } const fontData = await response.arrayBuffer(); // Write to virtual filesystem Module.FS.writeFile(filename, new Uint8Array(fontData)); // Load the font into the font registry Module.loadFont(filename); // Re-render to show font changes player.render(surface); console.log(`Auto-loaded font: ${filename} for ${assetId}`); }});

工作原理:

  1. 当加载 Cavalry 场景(.cv 文件)时,Web Player 会扫描资源定义
  2. 具有 "type": "file" 和受支持扩展名的文件资源会触发此事件:
    • 图像文件: png、jpg、jpeg、exr、webp
    • 字体文件: ttf、otf、woff、woff2
  3. 事件提供资源 ID 和提取的文件名供应用程序处理
  4. 事件处理程序应从 Web 服务器获取文件并将其写入虚拟文件系统
  5. 然后调用适当的 API 加载资源:
    • 图像: replaceImageAsset(filename, assetId)
    • 字体: Module.loadFont(filename)

自动加载:

  • Google Sheets 资源 会自动处理,不需要此事件
  • 图像和字体资源 需要 JavaScript 处理,因为它们需要从 Web 服务器获取

文件路径解析: Web Player 从 Cavalry 的内部路径中提取文件名,例如:

  • "@assets/image.jpg""image.jpg"
  • "@assets/Changa_One/ChangaOne-Regular.ttf""ChangaOne-Regular.ttf"
  • "subfolder/font.ttf""font.ttf"

事件处理程序应尝试公共位置,如根目录(./filename)和 Assets 文件夹(./Assets/filename)。

示例实现:

  • 图像加载: 请参见 src/wasm/cavalry-web-player/image-asset/ 中的图像资源演示
  • 字体加载: 请参见 src/wasm/cavalry-web-player/custom-font/ 中的自定义字体演示