// Дерево сюжета: акты — колонки (compound-узлы), звенья — узлы внутри.
const NODE_STYLE = {
lane: {
'background-color': '#171d27',
'border-width': 0,
'shape': 'round-rectangle',
'label': 'data(label)',
'color': '#e0a458',
'font-size': 14,
'text-wrap': 'wrap',
'text-max-width': 460,
'text-valign': 'top',
'text-halign': 'left',
'padding-left': 12,
'padding-top': 12,
'min-width': 500,
'min-height': 200,
},
quest: {
'shape': 'round-rectangle',
'background-color': '#3d6db5',
'color': '#fff',
'label': 'data(label)',
'text-wrap': 'wrap',
'text-max-width': 200,
'text-valign': 'center',
'text-halign': 'center',
'font-size': 12,
'padding': 10,
'width': 'data(w)',
'height': 'data(h)',
},
event: {
'shape': 'round-rectangle',
'background-color': '#a63d40',
'color': '#fff',
'label': 'data(label)',
'text-wrap': 'wrap',
'text-max-width': 200,
'text-valign': 'center',
'text-halign': 'center',
'font-size': 12,
'padding': 10,
'width': 'data(w)',
'height': 'data(h)',
},
ghost: {
'shape': 'round-rectangle',
'background-color': '#20242e',
'color': '#4c5566',
'border-style': 'dashed',
'border-color': '#333a48',
'label': 'data(label)',
'text-wrap': 'wrap',
'text-max-width': 190,
'text-valign': 'center',
'text-halign': 'center',
'font-size': 12,
'font-style': 'italic',
'padding': 10,
'width': 'data(w)',
'height': 'data(h)',
},
};
async function buildCy() {
const container = document.getElementById('cy');
const elements = await fetch('/api/graph').then(r => r.json());
// сохранить позицию/зум между live-reload'ами
const saved = sessionStorage.getItem('cy-view');
const view = saved ? JSON.parse(saved) : null;
if (window.cyInst) window.cyInst.destroy();
const cy = cytoscape({
container,
elements,
wheelSensitivity: 0.2,
layout: { name: 'preset' },
style: [
{ selector: 'node[kind = "lane"]', style: NODE_STYLE.lane },
{ selector: 'node[kind = "quest"]', style: NODE_STYLE.quest },
{ selector: 'node[kind = "event"]', style: NODE_STYLE.event },
{ selector: 'node[kind = "ghost"]', style: NODE_STYLE.ghost },
{ selector: 'node[side = true]',
style: { 'border-width': 2, 'border-style': 'dashed', 'border-color': '#9ec99e' } },
{ selector: 'edge[kind = "main"]',
style: { 'width': 2, 'line-color': '#5c6a82',
'target-arrow-shape': 'triangle', 'target-arrow-color': '#5c6a82',
'curve-style': 'bezier' } },
{ selector: 'edge[kind = "sideedge"]',
style: { 'width': 2, 'line-color': '#6b8f6b', 'line-style': 'dashed',
'target-arrow-shape': 'triangle', 'target-arrow-color': '#6b8f6b',
'curve-style': 'bezier' } },
],
});
window.cyInst = cy;
cy.on('tap', 'node', (e) => {
const d = e.target.data();
if (d.kind === 'lane' || d.kind === 'ghost') return;
location.href = '/node/' + d.id;
});
const persist = () => sessionStorage.setItem('cy-view',
JSON.stringify({ zoom: cy.zoom(), pan: cy.pan() }));
cy.on('viewport', persist);
if (view) {
cy.zoom(view.zoom);
cy.pan(view.pan);
} else {
cy.fit(60);
}
}
buildCy();