shikiCommentsGraphLib

Lib для графа комментариев Shikimori

Fra og med 16.10.2025. Se den nyeste version.

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greasyfork.org/scripts/552715/1678737/shikiCommentsGraphLib.js

// ==UserScript==
// @name         shikiCommentsGraphLib
// @namespace    https://shikimori.one/
// @version      3.0
// @description  Lib для графа комментариев Shikimori
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// ==/UserScript==

; (function () {
  'use strict';

  // === Базовая математика (оставляем из оригинала) ===
  class ShikiMath {
    static is_above(x, y, x1, y1, x2, y2) {
      const dx = x2 - x1;
      const dy = y2 - y1;
      return ((((dy * x) - (dx * y)) + (dx * y1)) - (dy * x1)) <= 0;
    }
    static sector(x1, y1, x2, y2, rx, ry) {
      const lb_to_rt = this.is_above(x2, y2, x1 - rx, y1 - ry, x1, y1);
      const lt_to_rb = this.is_above(x2, y2, x1 - rx, y1 + ry, x1, y1);
      if (lb_to_rt && lt_to_rb) return 'top';
      if (!lb_to_rt && lt_to_rb) return 'right';
      if (!lb_to_rt && !lt_to_rb) return 'bottom';
      return 'left';
    }
    static square_cutted_line(x1, y1, x2, y2, rx1, ry1, rx2, ry2) {
      let f_x1, f_x2, f_y1, f_y2;
      const dx = x2 - x1, dy = y2 - y1;
      const y = x => (((dy * x) + (dx * y1)) - (dy * x1)) / dx;
      const x = y => (((dx * y) - (dx * y1)) + (dy * x1)) / dy;
      const target = this.sector(x1, y1, x2, y2, rx1, ry1);
      if (target === 'right') { f_x1 = x1 + rx1; f_y1 = y(f_x1); f_x2 = x2 - rx2; f_y2 = y(f_x2); }
      if (target === 'left') { f_x1 = x1 - rx1; f_y1 = y(f_x1); f_x2 = x2 + rx2; f_y2 = y(f_x2); }
      if (target === 'top') { f_y1 = y1 + ry1; f_x1 = x(f_y1); f_y2 = y2 - ry2; f_x2 = x(f_y2); }
      if (target === 'bottom') { f_y1 = y1 - ry1; f_x1 = x(f_y1); f_y2 = y2 + ry2; f_x2 = x(f_y2); }
      return { x1: f_x1, y1: f_y1, x2: f_x2, y2: f_y2 };
    }
  }

  // === Класс CommentNode ===
  const SELECT_SCALE = 1.5;
  const BORDER_OFFSET = 3;

  class CommentNode {
    constructor(data, size = 64) {
      Object.assign(this, data);
      this.width = this.height = size;
      this.rx = this.ry = size / 2;
      this.selected = false;
    }

    get d3Node() { return this._d3Node ||= d3.select(`.node#${this.id}`); }
    get d3Image() { return this._d3Image ||= this.d3Node.select('image'); }

    deselect(boundX, boundY, tick) {
      this.selected = false;
      this._animate(this.width, this.height, boundX, boundY, tick);
      this._hideTooltip();
    }

    select(boundX, boundY, tick) {
      this.selected = true;
      this._animate(this.width * SELECT_SCALE, this.height * SELECT_SCALE, boundX, boundY, tick);
      this._loadTooltip();
    }

    _animate(newW, newH, boundX, boundY, tick) {
      const iw = d3.interpolate(this.width, newW);
      const ih = d3.interpolate(this.height, newH);
      this.d3Node.transition().duration(300).tween('resize', () => t => {
        this.width = iw(t); this.height = ih(t);
        this.rx = this.width / 2; this.ry = this.height / 2;
        this.d3Node.attr('transform', `translate(${boundX(this) - this.rx}, ${boundY(this) - this.ry})`);
        this.d3Image.attr({ width: this.width, height: this.height });
        tick();
      });
    }

    async _loadTooltip() {
      try {
        const tooltip = document.querySelector('.sticky-tooltip');
        tooltip.style.display = 'block';
        tooltip.querySelector('.inner').innerHTML = '<i>Загрузка...</i>';

        const { data } = await axios.get(`https://shikimori.one/api/comments/${this.id}`);
        const html = data.html_body || '(нет данных)';
        tooltip.querySelector('.inner').innerHTML = html;
      } catch (e) {
        console.error('Tooltip error:', e);
      }
    }

    _hideTooltip() {
      const tooltip = document.querySelector('.sticky-tooltip');
      if (tooltip) tooltip.style.display = 'none';
    }
  }

  // === Граф комментариев ===
  class CommentsGraph extends window.FranchiseGraph {
    constructor(data) {
      super(data);
      this.image_w = this.image_h = 64;
      this.nodes_data = data.nodes.map(node => new CommentNode(node, 64));
    }

    _append_nodes() {
      this.d3_node = this.d3_svg.append('g').selectAll('.node')
        .data(this.nodes_data)
        .enter().append('g')
        .attr({ class: 'node', id: d => d.id })
        .call(this.d3_force.drag())
        .on('click', d => {
          if (d3.event?.defaultPrevented) return;
          this._node_selected(d);
        });

      this.d3_node.append('path').attr({ class: 'border_outer', d: '' });

      this.d3_image_container = this.d3_node.append('g').attr({ class: 'image-container' });
      this.d3_image_container.append('image')
        .attr({
          width: d => d.width,
          height: d => d.height,
          'xlink:href': d => d.avatar
        });
      this.d3_image_container.append('path')
        .attr({ class: 'border_inner', d: d => `M 0,0 ${d.width},0 ${d.width},${d.height} 0,${d.height} 0,0` });
    }
  }

  // === Экспорт ===
  window.ShikiMath = ShikiMath;
  window.CommentNode = CommentNode;
  window.CommentsGraph = CommentsGraph;
})();