Newer
Older
rpg_game / panel / static / graph.js
// Дерево сюжета: акты — колонки (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();