技术拆解-叙事流程图

设计亮点

数据驱动

抽象出了FlowchartStore这个 store 类,提供数据结构和响应的数据处理方法。

currentNarrationNode(state)这种方法就很贴合叙事可视化。

数据的存取也很不错:saveToLocalStorage()resumeFromLocalStorage()clearLocalStorageAndReload()

组件化

在 components 下面设计了一个 TheFlowchart 组件。

渲染分离

TheFlowchart 组件中通过updateAppearance()做到了数据和渲染分离。

且渲染的时候通过 CSS 的正则匹配选择器选择元素,很不错:

1
2
3
4
5
this.flowchartElement
.querySelectorAll('[id^=n-], [id^=e-]')
.forEach((element) => {
element.classList.remove('replaced-out', 'replaced-in');
});

状态渲染

设计了不同的状态(teased, revealed, next, current),进行不同样式的渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
this.flowchartStore.teasedItems.forEach((id) => {
document.getElementById(id).setAttribute('data-state', 'teased');
});

this.flowchartStore.revealedItems.forEach((id) => {
document.getElementById(id).setAttribute('data-state', 'revealed');
});

this.flowchartStore.currentNode.outgoing.forEach((item) => {
item.edge.setAttribute('data-state', 'next');
item.node.element.setAttribute('data-state', 'next');
});

this.flowchartStore.currentNode.element.setAttribute('data-state', 'current');
this.markItemAsRevealed(this.flowchartStore.currentNode.element);

// replace primary elements with alternate state variants if those exist
['teased', 'revealed', 'next', 'current'].forEach((state) => {
this.flowchartElement
.querySelectorAll('[data-state=' + state + ']')
.forEach((element) => {
const replacementElement = this.findReplacementElement(element, state);

if (replacementElement) {
element.classList.add('replaced-out');
replacementElement.classList.add('replaced-in');
}
});
});

事件驱动的交互

给 TheFlowchart 组件设计了事件 API,用于交互功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
name: 'TheFlowchart',

components: {
InlineSvg,
},
// 事件API
emits: [
'setCurrentNodeId',
'jumpNarrationToNode',
'startPlayback',
'startExplorationDuringPlayback',
'stopExplorationDuringPlayback',
'toggleIntroPanel',
],
// ......
};

动画

动画的实现很简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// smooth scroll to coordinate using custom duration and easing
smoothScroll(xEnd, yEnd, duration) {
const time = Date.now();
const xStart = this.flowchartContainer.scrollLeft;
const yStart = this.flowchartContainer.scrollTop;

const step = () => {
const elapsed = Date.now() - time;
const scrolling = elapsed < duration;
const x = scrolling ? xStart + (xEnd - xStart) * easeExpOut(elapsed / duration) : xEnd;
const y = scrolling ? yStart + (yEnd - yStart) * easeExpOut(elapsed / duration) : yEnd;

if (scrolling) {
requestAnimationFrame(step);
}

this.flowchartContainer.scrollTo({
left: x,
top: y,
behavior: 'instant'
});
}

step();
},

资料

代码:

GitHub - uclab-potsdam/interactive-flowchart: Navigating and narrating complexity through exploration and storytelling