技术拆解-叙事流程图
设计亮点
数据驱动
抽象出了FlowchartStore这个 store 类,提供数据结构和响应的数据处理方法。
像currentNarrationNode(state)这种方法就很贴合叙事可视化。
数据的存取也很不错:saveToLocalStorage()、resumeFromLocalStorage()、clearLocalStorageAndReload()
组件化
在 components 下面设计了一个 TheFlowchart 组件。
渲染分离
TheFlowchart 组件中通过updateAppearance()做到了数据和渲染分离。
且渲染的时候通过 CSS 的正则匹配选择器选择元素,很不错:
this.flowchartElement
.querySelectorAll('[id^=n-], [id^=e-]')
.forEach((element) => {
element.classList.remove('replaced-out', 'replaced-in');
});
状态渲染
设计了不同的状态(teased, revealed, next, current),进行不同样式的渲染:
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,用于交互功能:
export default {
name: 'TheFlowchart',
components: {
InlineSvg,
},
// 事件API
emits: [
'setCurrentNodeId',
'jumpNarrationToNode',
'startPlayback',
'startExplorationDuringPlayback',
'stopExplorationDuringPlayback',
'toggleIntroPanel',
],
// ......
};
动画
动画的实现很简洁:
// 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();
},
资料
代码: