shikiCommentsGraphLib

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

Tính đến 16-10-2025. Xem phiên bản mới nhất.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @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;
})();