一份基础的 DSL
在前面的章节中,我们已经大致了解了如何快速绘制制作一个 VStory 作品。本教程将以一个简单的仪表盘为例,细致介绍 VStory 的一份基础的 dsl 组成。一份基础的 dsl 需包含以下部分:
character作品中会使用到的角色acts角色在不同时刻的不同行为
教程最终,我们将会实现如下图片中的效果

1. 物料准备
一个仪表盘会包含多种图表、以及标题、表格等模块、这些模块一部分可以使用 VStory 中提供的特定 character 实现,还有一些可以通过 VChart 自行去配置。在本教程中,我们将简化物料准备过程,直接给到所有用到的图表 spec。
- 一个基于
VChart的简单的柱状图
- 一个基于
VChart的简单的面积图
- 一个基于
VChart的简单的雷达图
- 一个基于
VChart的简单的玫瑰图
- 一个基于
VChart的简单的仪表盘图
- 使用一个
VStory的Text类型作为标题
- 使用一个
VStory的WaveScatter图表类型
2. 拼接
接下来,我们将这些素材拼接到VStory的大画布中,形成一个完整的作品,我们使用 1920 * 1080 作为画布的完整尺寸,图表之间的margin为 30px,距离左右边界的margin也是 30px。具体的布局如下图所示

完成了布局的设计之后,接下来我们开始 DSL 的编写,来实现上图中的效果,DSL 核心包括一个character数组和一个acts数组,character数组包含了作品中的所有角色(元素),acts数组包含了作品中的各种角色的各种动作(动画),具体的接口定义如下:
interface IStoryDSL { acts: IActSpec[]; // 作品的章节 characters: ICharacterConfig[]; // 作品中的元素 } /* character定义 */ type ICharacterConfig = { id: string; type: string; // 类型 position: IWidgetData; // 定位描述 zIndex: number; extra?: any; // 带着的额外信息 options?: any; // 具体的配置信息 }; /* act定义 */ interface IActSpec { id: string; // 这一幕的id scenes: ISceneSpec[]; // 这一幕包含的场景 } interface ISceneSpec { id: string; // 这个场景的id delay?: number; // 场景的入场延迟,可以是正数或者负数 actions: IActions[]; // 这个场景包含的动作 } interface IActions { // 行为定义,角色和行为都可以配数组,可以定义多个角色执行多个行为 characterId: string | string[]; // 执行行为的角色id characterActions: IActionSpec[]; // 执行的具体行为 } // 具体的行为定义 interface IAction { action: string; // 行为名称 startTime?: number; // 开始时间 payload?: { // 行为的参数 animation?: IAnimationParams; selector?: string; [key?: string]: any; }; }
2.1 character 数组配置
根据我们提供的每个character的配置,以及接口定义,我们可以组装我们的character数组。
const characters = [ { type: 'Text', id: 'Title', zIndex: 3, position: { top: 100, left: 1920 / 2, width: 1920, height: 90 }, options: { graphic: { fontSize: 70, wordBreak: 'break-word', textAlign: 'center', textBaseline: 'bottom', fill: 'black', fontWeight: 200, text: 'VStory简易仪表盘' } } }, { type: 'WaveScatter', id: 'wave-scatter', zIndex: 1, position: { top: 130, left: 30, width: 600, height: 630 }, options: { data: { values: mockData.filter(item => item.type === 'A') }, categoryField: 'month', valueField: 'value', /* 水波动画的配置 */ waveDuration: 2000, waveRatio: 0.00525, waveColor: '#0099ff', background: 'linear-gradient(180deg, #0099ff11 100%, #0099ff33 0%)', amplitude: 10, frequency: 2, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 8, clip: true } } }, { type: 'VChart', id: 'radar1', zIndex: 3, position: { top: 130, left: 660, width: 600, height: 630 }, options: { spec: radar1, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 20 } } }, { type: 'VChart', id: 'rose1', zIndex: 3, position: { top: 130, left: 1290, width: 600, height: 630 }, options: { spec: rose1, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 20 } } }, { type: 'VChart', id: 'gauge1', zIndex: 3, position: { top: 790, left: 30, width: 600, height: 260 }, options: { spec: gauge1, padding: { left: 0, right: 0, top: 0, bottom: 0 }, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 20 } } }, { type: 'VChart', id: 'bar1', zIndex: 3, position: { top: 790, left: 660, width: 600, height: 260 }, options: { spec: bar1, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 20 } } }, { type: 'VChart', id: 'area1', zIndex: 3, position: { top: 790, left: 1290, width: 600, height: 260 }, options: { spec: area1, panel: { fill: '#ffffff', shadowColor: 'rgba(0, 0, 0, 0.05)', shadowBlur: 10, shadowOffsetX: 4, shadowOffsetY: 4, cornerRadius: 20 } } } ];
2.2 acts 数组配置
characters数组中只是定义了作品中有这些元素可用,具体的动作还没有定义,如果不定义动作的话,元素将不会展示,所以接下来我们开始定义acts数组。我们期望作品中的元素有如下动作
- 柱状图和玫瑰图会有
oneByOne(图元一个接着一个)的appear(入场)动画效果,其他图表都是默认的appear(入场)的动画效果 - 包含图表本身的面板也要有一个
bounce(弹跳)的appear(入场)的动画效果
由于行为都很简单,所以只需要一幕,一个场景就能完成。
const acts = [ { id: 'page1', // 这一幕的id scenes: [ { id: 'singleScene', // 这一幕包含的场景 actions: [ // 除了柱状图和玫瑰图以外,其他character都做默认的appear的动画效果 { characterId: ['Title', 'area1', 'radar1', 'gauge1', 'wave-scatter'], characterActions: [ { action: 'appear', startTime: 0, payload: { animation: { duration: 2000 } } } ] }, // 柱状图和玫瑰图做oneByOne的appear动画效果 { characterId: ['bar1', 'rose1'], characterActions: [ { action: 'appear', startTime: 0, payload: { animation: { duration: 3000, oneByOne: true, dimensionCount: mockData.length } } } ] }, // 包含图表本身的面板做bounce的appear动画效果 { characterId: ['area1', 'radar1', 'bar1', 'rose1', 'gauge1', 'wave-scatter'], characterActions: [ { action: 'bounce', payload: { animation: { duration: 2000, easing: 'quadOut' }, type: 'bounce4', flipY: true } } ] } ] } ] } ];
3. 播放
至此,我们已经完成了一个简易的仪表盘的制作步骤,接下来,我们将character和acts数组拼起来合成一个 DSL,然后使用 VStory 进行播放。
// 注册所有需要的内容
VStory.registerAll();
// 创建一个VStory实例,将DSL传入
const story = new VStory.Story(dsl, { dom: CONTAINER_ID, background: '#ebecf0' });
// 创建一个player实例,用于播放这个Story
const player = new VStory.Player(story);
story.init(player);
// 开始播放,传入1表示循环播放
// 传入0表示单次播放,播放到结束时间就停止
// 传入-1表示单次播放,但是播放结束后,时间会继续往后走,不会停止
// 我们这里因为有一个永久在播放的波浪动画(wave),所以这里传入-1,不循环,但是时间不停止
player.play(-1);