Bangumi 打上贴贴

Bangumi 打上贴贴,让贴贴有趣起来

// ==UserScript==
// @name         Bangumi 打上贴贴
// @namespace    b38.dev
// @version      1.0.0
// @author       神戸小鳥 @vickscarlet
// @description  Bangumi 打上贴贴,让贴贴有趣起来
// @license      MIT
// @icon         https://bgm.tv/img/favicon.ico
// @homepage     https://github.com/bangumi/scripts/blob/master/vickscarlet/scripts/likes_firework
// @match        *://bgm.tv/*
// @match        *://chii.in/*
// @match        *://bangumi.tv/*
// ==/UserScript==

(function () {
  'use strict';

  (() => {
    chiiLib.ukagaka.addGeneralConfig({
      title: "打上贴贴",
      name: "myCustomSetting",
      type: "radio",
      defaultValue: "default",
      getCurrentValue: () => localStorage.getItem("likes-firework-autoplay") || "no",
      onChange: (value) => localStorage.setItem("likes-firework-autoplay", value),
      options: [
        { value: "yes", label: "自动播放" },
        { value: "no", label: "不自动播放" }
      ]
    });
    const likes = Array.from(
      document.querySelectorAll("#columnInSubjectA > .clearit .likes_grid a.item")
    ).map((e) => {
      const url = e.querySelector(".emoji").style.backgroundImage.split('"')[1];
      const count = Number(e.querySelector(".num").innerText);
      return [url, count];
    });
    if (!likes.length) return;
    const rand = (min, max) => ~~(Math.random() * (max - min + 1) + min);
    const displacement = (v0, a, t) => v0 * t + a * t * t / 2;
    class Launch {
      start;
      target;
      current;
      hue;
      end;
      xy;
      total;
      hitX = false;
      hitY = false;
      speed = 2;
      lineWidth = 1;
      brightness = rand(50, 80);
      alpha = rand(50, 100) / 100;
      targetRadius = 1;
      acceleration = 4 / 100;
      time = Date.now();
      constructor({ start, target, hue, end }) {
        this.start = Array.from(start);
        this.target = Array.from(target);
        this.current = Array.from(this.start);
        this.hue = hue;
        this.end = end;
        const angle = Math.atan2(target[1] - start[1], target[0] - start[0]);
        this.xy = [Math.cos(angle), Math.sin(angle)];
        this.total = Math.sqrt((target[0] - start[0]) ** 2 + (target[1] - start[1]) ** 2);
      }
      update(ctx, _) {
        const dt = (Date.now() - this.time) / 4;
        const s = displacement(this.speed, this.acceleration, dt);
        if (s > this.total) return this.end();
        const last = this.current;
        this.current = [this.start[0] + s * this.xy[0], this.start[1] + s * this.xy[1]];
        ctx.beginPath();
        ctx.moveTo(Math.round(last[0]), Math.round(last[1]));
        ctx.lineTo(Math.round(this.current[0]), Math.round(this.current[1]));
        ctx.closePath();
        ctx.strokeStyle = "hsla(" + this.hue + ", 100%, " + this.brightness + "%, " + this.alpha + ")";
        ctx.stroke();
      }
    }
    class Explosion {
      center;
      hue;
      url;
      endCallback;
      particles = /* @__PURE__ */ new Set();
      like;
      total = rand(30, 45);
      _done = false;
      _dp = false;
      _dl = false;
      constructor({ center, hue, url, end }) {
        this.center = Array.from(center);
        this.hue = hue;
        this.url = url;
        this.endCallback = end;
        let count = this.total;
        while (count--) {
          const particle = new Particle({
            center,
            hue,
            end: () => {
              this.particles.delete(particle);
              if (!this.particles.size) {
                this._dp = true;
                if (this._dl) this.end();
              }
            }
          });
          this.particles.add(particle);
        }
        this.like = new Like({
          url: this.url,
          center: this.center,
          end: () => {
            this._dl = true;
            if (this._dp) this.end();
          }
        });
      }
      update(ctx, dt) {
        if (this._done) return this;
        for (const particle of this.particles) {
          particle.update(ctx, dt);
        }
        this.like.update(ctx, dt);
      }
      end() {
        this._done = true;
        this.endCallback();
      }
    }
    class Like {
      center;
      url;
      end;
      size = 21;
      pixel = 0.5;
      gap = 1;
      alpha = 0.5;
      constructor({ center, url, end }) {
        this.center = Array.from(center);
        this.url = url;
        this.end = end;
      }
      update(ctx, dt) {
        this.gap += dt / 10;
        this.pixel += dt / 40;
        this.alpha -= 0.01;
        if (this.alpha < 0.05 || this.gap > 3) {
          return this.end();
        }
        const data = Like.get(this.url);
        const [cx, cy] = this.center;
        for (let h = 0; h < this.size; h++) {
          for (let w = 0; w < this.size; w++) {
            ctx.beginPath();
            ctx.arc(
              cx + (w - 11) * this.gap,
              cy + (h - 11) * this.gap,
              this.pixel,
              0,
              Math.PI * 2,
              false
            );
            const [r, g, b, a] = data[h * this.size + w];
            const alpha = a * this.alpha;
            ctx.closePath();
            ctx.fillStyle = "rgba(" + r + "," + g + "," + b + "," + alpha + ")";
            ctx.fill();
          }
        }
      }
      static data = /* @__PURE__ */ new Map();
      static async init(urls) {
        for (const url of urls) {
          if (this.data.has(url)) continue;
          const data = await this.loading(url);
          this.data.set(url, data);
        }
      }
      static async loading(url) {
        return new Promise((reslove) => {
          const img = new Image();
          img.src = url;
          img.onload = function() {
            let imgWidth = 21;
            let imgHeight = 21;
            const c = document.createElement("canvas");
            c.width = 21;
            c.height = 21;
            const ctx = c.getContext("2d");
            ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
            let imgData = ctx.getImageData(0, 0, imgWidth, imgHeight);
            const datas = [];
            for (let h = 0; h < imgHeight; h += 1) {
              for (let w = 0; w < imgWidth; w += 1) {
                let position = (imgWidth * h + w) * 4;
                let r = imgData.data[position], g = imgData.data[position + 1], b = imgData.data[position + 2], a = imgData.data[position + 3];
                datas.push([r, g, b, a]);
              }
            }
            reslove(datas);
          };
        });
      }
      static get(url) {
        return this.data.get(url);
      }
    }
    class Particle {
      x;
      y;
      hue;
      end;
      angle = rand(0, 360);
      speed = rand(1, 15);
      brightness = rand(50, 80);
      alpha = rand(40, 100) / 100;
      decay = rand(10, 50) / 1e3;
      wind = rand(-100, 100) / 100;
      partSpeedVariance = 10;
      gravity = 1 / 2;
      friction = 1 - 5 / 100;
      hueVariance = 30;
      lineWidth = 1;
      flickerDensity = 20;
      constructor({ center, hue, end }) {
        [this.x, this.y] = center;
        this.hue = rand(hue - this.hueVariance, hue + this.hueVariance);
        this.end = end;
      }
      update(ctx, dt) {
        let radians = this.angle * Math.PI / 180;
        let vx = Math.cos(radians) * this.speed;
        let vy = Math.sin(radians) * this.speed + this.gravity;
        this.speed *= this.friction;
        const { x, y } = this;
        this.x += vx * dt;
        this.y += vy * dt;
        this.angle += this.wind;
        this.alpha -= this.decay;
        if (this.alpha < 0.05) {
          this.end();
          return;
        }
        ctx.beginPath();
        ctx.moveTo(Math.round(x), Math.round(y));
        ctx.lineTo(Math.round(this.x), Math.round(this.y));
        ctx.closePath();
        ctx.strokeStyle = "hsla(" + this.hue + ", 100%, " + this.brightness + "%, " + this.alpha + ")";
        ctx.stroke();
        let inverseDensity = 50 - this.flickerDensity;
        if (rand(0, inverseDensity) === inverseDensity) {
          ctx.beginPath();
          ctx.arc(
            Math.round(this.x),
            Math.round(this.y),
            rand(this.lineWidth, this.lineWidth + 3) / 2,
            0,
            Math.PI * 2,
            false
          );
          ctx.closePath();
          let randAlpha = rand(50, 100) / 100;
          ctx.fillStyle = "hsla(" + this.hue + ", 100%, " + this.brightness + "%, " + randAlpha + ")";
          ctx.fill();
        }
      }
    }
    class Firework {
      url;
      start;
      target;
      endCallback;
      state = { type: "idle" };
      hue = rand(0, 360);
      constructor({ url, start, target, end }) {
        this.url = url;
        this.start = Array.from(start);
        this.target = Array.from(target);
        this.endCallback = end;
      }
      update(ctx, dt) {
        switch (this.state.type) {
          case "launch":
          case "explosion":
            this.state.item.update(ctx, dt);
            break;
        }
        return this;
      }
      launch() {
        this.state = {
          type: "launch",
          item: new Launch({
            start: this.start,
            target: this.target,
            hue: this.hue,
            end: () => this.explosion()
          })
        };
        return this;
      }
      explosion() {
        this.state = {
          type: "explosion",
          item: new Explosion({
            url: this.url,
            center: this.target,
            hue: this.hue,
            end: () => this.end()
          })
        };
        return this;
      }
      end() {
        this.state = { type: "end" };
        this.endCallback();
        return this;
      }
    }
    class Fireworks {
      cw = window.innerWidth;
      ch = window.innerHeight;
      canvas = document.createElement("canvas");
      ctx = this.canvas.getContext("2d");
      fireworks = /* @__PURE__ */ new Set();
      _loop = false;
      time = Date.now();
      timeout = 0;
      constructor() {
        const actions = document.querySelector(
          "#columnInSubjectA > .clearit .topic_actions .post_actions"
        );
        const btn = document.createElement("div");
        btn.classList.add("action");
        const a = document.createElement("a");
        a.classList.add("icon");
        a.href = "javascript:void(0)";
        const span = document.createElement("span");
        span.classList.add("title");
        span.appendChild(document.createTextNode("🎇 打上贴贴"));
        a.append(span);
        btn.append(a);
        actions?.prepend(btn);
        btn.addEventListener("click", () => this.init(likes));
        this.canvas.style.position = "fixed";
        this.canvas.style.top = "0";
        this.canvas.style.left = "0";
        this.canvas.style.width = "100vw";
        this.canvas.style.height = "100vh";
        this.canvas.style.pointerEvents = "none";
        this.canvas.style.zIndex = "999999";
        this.canvas.width = this.cw;
        this.canvas.height = this.ch;
        this.ctx.lineCap = "round";
        this.ctx.lineJoin = "round";
        document.body.append(this.canvas);
      }
      async init(likes2) {
        await Like.init(likes2.map(([url]) => url));
        likes2.map(([url, count]) => new Array(count).fill(url)).flat().sort(() => rand(-10, 10)).forEach((url, i) => {
          setTimeout(() => {
            this.launch(url);
          }, Math.min(rand(20, 100) * i, 15e3));
        });
      }
      launch(url) {
        const target = [rand(50, this.cw - 50), rand(50, this.ch / 2) - 50];
        const dx = rand(30, 200);
        const firework = new Firework({
          url,
          start: [target[0] > this.cw / 2 ? target[0] - dx : target[0] + dx, this.ch],
          target,
          end: () => {
            this.fireworks.delete(firework);
            if (!this.fireworks.size) {
              clearTimeout(this.timeout);
              this.timeout = setTimeout(() => {
                this.canvas.style.display = "none";
                this._loop = false;
              }, 5e3);
            }
          }
        });
        this.fireworks.add(firework);
        firework.launch();
        if (this.fireworks.size == 1) {
          this.canvas.style.display = "block";
          this._loop = true;
          this.loop();
        }
      }
      loop() {
        if (!this._loop) return;
        requestAnimationFrame(() => this.loop());
        this.ctx.globalCompositeOperation = "destination-out";
        this.ctx.fillStyle = "rgba(0,0,0,.25)";
        this.ctx.fillRect(0, 0, this.cw, this.ch);
        this.ctx.globalCompositeOperation = "lighter";
        let now = Date.now();
        let dt = (now - this.time) / 16;
        dt = dt > 5 ? 5 : dt;
        this.time = now;
        for (const firework of this.fireworks) firework.update(this.ctx, dt);
      }
    }
    const fireworks = new Fireworks();
    if (localStorage.getItem("likes-firework-autoplay") != "yes") return;
    fireworks.init(likes);
  })();

})();