设计亮点
数据驱动
抽象出了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);
['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, }, 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
| 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