fork(fix): Clone AMLL 并修复 BUG

- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
lqtmcstudio
2026-06-07 00:02:14 +08:00
parent 783d2c3dee
commit 72f4510dc8
458 changed files with 86075 additions and 1665 deletions

View File

@@ -0,0 +1,154 @@
import type { Disposable, HasElement } from "../interfaces.ts";
export abstract class AbstractBaseRenderer implements Disposable, HasElement {
/**
* 修改背景的流动速度,数字越大越快,默认为 8
* @param speed 背景的流动速度,默认为 8
*/
abstract setFlowSpeed(speed: number): void;
/**
* 修改背景的渲染比例,默认是 0.5
*
* 一般情况下这个程度既没有明显瑕疵也不会特别吃性能
* @param scale 背景的渲染比例
*/
abstract setRenderScale(scale: number): void;
/**
* 是否启用静态模式,即图片在更换后就会保持静止状态并禁用更新,以节省性能
* @param enable 是否启用静态模式
*/
abstract setStaticMode(enable: boolean): void;
/**
* 修改背景动画帧率,默认是 30 FPS
*
* 如果设置成 0 则会停止动画
* @param fps 目标帧率,默认 30 FPS
*/
abstract setFPS(fps: number): void;
/**
* 暂停背景动画,画面即便是更新了图片也不会发生变化
*/
abstract pause(): void;
/**
* 恢复播放背景动画
*/
abstract resume(): void;
/**
* 设置背景专辑资源,纹理加载并设置完成后会返回
* @param albumSource 专辑的资源链接,可以是图片或视频链接,抑或是任意 img/video 元素,如果提供字符串链接且为视频则需要指定第二个参数
*/
abstract setAlbum(
albumSource: string | HTMLImageElement | HTMLVideoElement,
isVideo?: boolean,
): Promise<void>;
/**
* 设置低频的音量大小,范围在 80hz-120hz 之间为宜,取值范围在 [0.0-1.0] 之间
*
* 部分渲染器会根据音量大小调整背景效果(例如根据鼓点跳动)
*
* 如果无法获取到类似的数据,请传入 1.0 作为默认值,或不做任何处理(默认值即 1.0
* @param volume 低频的音量大小,范围在 50hz-120hz 之间为宜,取值范围在 [0.0-1.0] 之间
*/
abstract setLowFreqVolume(volume: number): void;
/**
* 设置背景是否根据“是否有歌词”这个特征调整自身效果,例如有歌词时会变得更加活跃
*
* 部分渲染器会根据这个特征调整自身效果
*
* 如果不确定是否需要赋值或无法知晓是否包含歌词,请传入 true 或不做任何处理(默认值为 true
*
* @param hasLyric 是否有歌词,如不确定是否需要赋值,请传入 true 或不做任何处理(默认值为 true
*/
abstract setHasLyric(hasLyric: boolean): void;
abstract dispose(): void;
abstract getElement(): HTMLElement;
}
function clamp1(x: number): number {
return Math.max(1, x);
}
export abstract class BaseRenderer extends AbstractBaseRenderer {
private observer: ResizeObserver;
protected flowSpeed = 1;
protected currerntRenderScale = 0.75;
constructor(protected canvas: HTMLCanvasElement) {
super();
this.observer = new ResizeObserver(() => {
const width = clamp1(
canvas.clientWidth * window.devicePixelRatio * this.currerntRenderScale,
);
const height = clamp1(
canvas.clientHeight *
window.devicePixelRatio *
this.currerntRenderScale,
);
this.onResize(width, height);
});
this.observer.observe(canvas);
}
setRenderScale(scale: number): void {
this.currerntRenderScale = scale;
this.onResize(
this.canvas.clientWidth *
window.devicePixelRatio *
this.currerntRenderScale,
this.canvas.clientHeight *
window.devicePixelRatio *
this.currerntRenderScale,
);
}
/**
* 当画板元素大小发生变化时此函数会被调用
* 可以在此处重设和渲染器相关的尺寸设置
* 考虑到初始化的时候元素不一定在文档中或出于某些特殊样式状态,尺寸长宽有可能会为 0请注意进行特判处理
* @param width 画板元素实际的物理像素宽度,有可能为 0
* @param height 画板元素实际的物理像素高度,有可能为 0
*/
protected onResize(width: number, height: number): void {
this.canvas.width = width;
this.canvas.height = height;
}
/**
* 修改背景的流动速度,数字越大越快,默认为 1
* @param speed 背景的流动速度,默认为 1
*/
setFlowSpeed(speed: number): void {
this.flowSpeed = speed;
}
/**
* 是否启用静态模式,即图片在更换后就会保持静止状态并禁用更新,以节省性能
* @param enable 是否启用静态模式
*/
abstract override setStaticMode(enable: boolean): void;
/**
* 修改背景动画帧率,默认是 30 FPS
*
* 如果设置成 0 则会停止动画
* @param fps 目标帧率,默认 30 FPS
*/
abstract override setFPS(fps: number): void;
/**
* 暂停背景动画,画面即便是更新了图片也不会发生变化
*/
abstract override pause(): void;
/**
* 恢复播放背景动画
*/
abstract override resume(): void;
/**
* 设置背景专辑资源,纹理加载并设置完成后会返回
* @param albumSource 专辑的资源链接,可以是图片或视频链接,抑或是任意 img/video 元素,如果提供字符串链接且为视频则需要指定第二个参数
*/
abstract override setAlbum(
albumSource: string | HTMLImageElement | HTMLVideoElement,
isVideo?: boolean,
): Promise<void>;
dispose(): void {
this.observer.disconnect();
this.canvas.remove();
}
override getElement(): HTMLElement {
return this.canvas;
}
}

View File

@@ -0,0 +1,168 @@
export function blurImage(
imageData: ImageData,
radius: number,
quality: number,
): void {
const pixels = imageData.data;
const width = imageData.width;
const height = imageData.height;
let rsum: number;
let gsum: number;
let bsum: number;
let asum: number;
let x: number;
let y: number;
let i: number;
let p: number;
let p1: number;
let p2: number;
let yp: number;
let yi: number;
let yw: number;
const wm = width - 1;
const hm = height - 1;
const rad1x = radius + 1;
const divx = radius + rad1x;
const rad1y = radius + 1;
const divy = radius + rad1y;
const div2 = 1 / (divx * divy);
const r: number[] = [];
const g: number[] = [];
const b: number[] = [];
const a: number[] = [];
const vmin: number[] = [];
const vmax: number[] = [];
while (quality-- > 0) {
yw = yi = 0;
for (y = 0; y < height; y++) {
rsum = pixels[yw] * rad1x;
gsum = pixels[yw + 1] * rad1x;
bsum = pixels[yw + 2] * rad1x;
asum = pixels[yw + 3] * rad1x;
for (i = 1; i <= radius; i++) {
p = yw + ((i > wm ? wm : i) << 2);
rsum += pixels[p++];
gsum += pixels[p++];
bsum += pixels[p++];
asum += pixels[p];
}
for (x = 0; x < width; x++) {
r[yi] = rsum;
g[yi] = gsum;
b[yi] = bsum;
a[yi] = asum;
if (y === 0) {
vmin[x] = Math.min(x + rad1x, wm) << 2;
vmax[x] = Math.max(x - radius, 0) << 2;
}
p1 = yw + vmin[x];
p2 = yw + vmax[x];
rsum += pixels[p1++] - pixels[p2++];
gsum += pixels[p1++] - pixels[p2++];
bsum += pixels[p1++] - pixels[p2++];
asum += pixels[p1] - pixels[p2];
yi++;
}
yw += width << 2;
}
for (x = 0; x < width; x++) {
yp = x;
rsum = r[yp] * rad1y;
gsum = g[yp] * rad1y;
bsum = b[yp] * rad1y;
asum = a[yp] * rad1y;
for (i = 1; i <= radius; i++) {
yp += i > hm ? 0 : width;
rsum += r[yp];
gsum += g[yp];
bsum += b[yp];
asum += a[yp];
}
yi = x << 2;
for (y = 0; y < height; y++) {
pixels[yi] = (rsum * div2 + 0.5) | 0;
pixels[yi + 1] = (gsum * div2 + 0.5) | 0;
pixels[yi + 2] = (bsum * div2 + 0.5) | 0;
pixels[yi + 3] = (asum * div2 + 0.5) | 0;
if (x === 0) {
vmin[y] = Math.min(y + rad1y, hm) * width;
vmax[y] = Math.max(y - radius, 0) * width;
}
p1 = x + vmin[y];
p2 = x + vmax[y];
rsum += r[p1] - r[p2];
gsum += g[p1] - g[p2];
bsum += b[p1] - b[p2];
asum += a[p1] - a[p2];
yi += width << 2;
}
}
}
}
export function saturateImage(imageData: ImageData, saturation: number): void {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
const gray = r * 0.3 + g * 0.59 + b * 0.11;
pixels[i] = gray * (1 - saturation) + r * saturation;
pixels[i + 1] = gray * (1 - saturation) + g * saturation;
pixels[i + 2] = gray * (1 - saturation) + b * saturation;
pixels[i + 3] = a;
}
}
export function brightnessImage(
imageData: ImageData,
brightness: number,
): void {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
pixels[i] = r * brightness;
pixels[i + 1] = g * brightness;
pixels[i + 2] = b * brightness;
pixels[i + 3] = a;
}
}
export function contrastImage(imageData: ImageData, contrast: number): void {
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const a = pixels[i + 3];
pixels[i] = (r - 128) * contrast + 128;
pixels[i + 1] = (g - 128) * contrast + 128;
pixels[i + 2] = (b - 128) * contrast + 128;
pixels[i + 3] = a;
}
}

View File

@@ -0,0 +1,71 @@
/**
* @fileoverview
* 一个播放歌词的组件
* @author SteveXMH
*/
export { AbstractBaseRenderer, BaseRenderer } from "./base.ts";
export { MeshGradientRenderer } from "./mesh-renderer/index.ts";
export { PixiRenderer } from "./pixi-renderer.ts";
import type { AbstractBaseRenderer, BaseRenderer } from "./base.ts";
export class BackgroundRender<Renderer extends BaseRenderer>
implements AbstractBaseRenderer
{
private element: HTMLCanvasElement;
private renderer: Renderer;
constructor(renderer: Renderer, canvas: HTMLCanvasElement) {
this.renderer = renderer;
this.element = canvas;
canvas.style.pointerEvents = "none";
canvas.style.zIndex = "-1";
canvas.style.contain = "strict";
}
static new<Renderer extends BaseRenderer>(type: {
new (canvas: HTMLCanvasElement): Renderer;
}): BackgroundRender<Renderer> {
const newCanvas = document.createElement("canvas");
return new BackgroundRender(new type(newCanvas), newCanvas);
}
setRenderScale(scale: number): void {
this.renderer.setRenderScale(scale);
}
setFlowSpeed(speed: number): void {
this.renderer.setFlowSpeed(speed);
}
setStaticMode(enable: boolean): void {
this.renderer.setStaticMode(enable);
}
setFPS(fps: number): void {
this.renderer.setFPS(fps);
}
pause(): void {
this.renderer.pause();
}
resume(): void {
this.renderer.resume();
}
setLowFreqVolume(volume: number): void {
this.renderer.setLowFreqVolume(volume);
}
setHasLyric(hasLyric: boolean): void {
this.renderer.setHasLyric(hasLyric);
}
setAlbum(
albumSource: string | HTMLImageElement | HTMLVideoElement,
isVideo?: boolean,
): Promise<void> {
return this.renderer.setAlbum(albumSource, isVideo);
}
getElement(): HTMLCanvasElement {
return this.element;
}
dispose(): void {
this.renderer.dispose();
this.element.remove();
}
}

View File

@@ -0,0 +1,211 @@
/**
* @fileoverview
* 实验性的随机控制点生成函数算法
* 目的是取代原先大量的预设控制点代码
*/
import { clamp01 } from "#utils/clamp.ts";
import {
type ControlPointConf,
type ControlPointPreset,
p,
preset,
} from "./cp-presets.ts";
const randomRange = (min: number, max: number): number =>
Math.random() * (max - min) + min;
function smoothstep(edge0: number, edge1: number, x: number): number {
const t = clamp01((x - edge0) / (edge1 - edge0));
return t * t * (3 - 2 * t);
}
function smoothifyControlPoints(
conf: ControlPointConf[],
w: number,
h: number,
iterations = 2,
factor = 0.5,
factorIterationModifier = 0.1,
): void {
let grid: ControlPointConf[][] = [];
let f = factor;
for (let j = 0; j < h; j++) {
grid[j] = [];
for (let i = 0; i < w; i++) {
grid[j][i] = conf[j * w + i];
}
}
const kernel = [
[1, 2, 1],
[2, 4, 2],
[1, 2, 1],
];
const kernelSum = 16;
for (let iter = 0; iter < iterations; iter++) {
const newGrid: ControlPointConf[][] = [];
for (let j = 0; j < h; j++) {
newGrid[j] = [];
for (let i = 0; i < w; i++) {
if (i === 0 || i === w - 1 || j === 0 || j === h - 1) {
newGrid[j][i] = grid[j][i];
continue;
}
let sumX = 0;
let sumY = 0;
let sumUR = 0;
let sumVR = 0;
let sumUP = 0;
let sumVP = 0;
for (let dj = -1; dj <= 1; dj++) {
for (let di = -1; di <= 1; di++) {
const weight = kernel[dj + 1][di + 1];
const nb = grid[j + dj][i + di];
sumX += nb.x * weight;
sumY += nb.y * weight;
sumUR += nb.ur * weight;
sumVR += nb.vr * weight;
sumUP += nb.up * weight;
sumVP += nb.vp * weight;
}
}
const avgX = sumX / kernelSum;
const avgY = sumY / kernelSum;
const avgUR = sumUR / kernelSum;
const avgVR = sumVR / kernelSum;
const avgUP = sumUP / kernelSum;
const avgVP = sumVP / kernelSum;
const cur = grid[j][i];
const newX = cur.x * (1 - f) + avgX * f;
const newY = cur.y * (1 - f) + avgY * f;
const newUR = cur.ur * (1 - f) + avgUR * f;
const newVR = cur.vr * (1 - f) + avgVR * f;
const newUP = cur.up * (1 - f) + avgUP * f;
const newVP = cur.vp * (1 - f) + avgVP * f;
newGrid[j][i] = p(i, j, newX, newY, newUR, newVR, newUP, newVP);
}
}
grid = newGrid;
f = clamp01(f + factorIterationModifier);
}
for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) {
conf[j * w + i] = grid[j][i];
}
}
}
function noise(x: number, y: number): number {
return fract(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453);
}
function fract(x: number): number {
return x - Math.floor(x);
}
function smoothNoise(x: number, y: number): number {
const x0 = Math.floor(x);
const y0 = Math.floor(y);
const x1 = x0 + 1;
const y1 = y0 + 1;
const xf = x - x0;
const yf = y - y0;
const u = xf * xf * (3 - 2 * xf);
const v = yf * yf * (3 - 2 * yf);
const n00 = noise(x0, y0);
const n10 = noise(x1, y0);
const n01 = noise(x0, y1);
const n11 = noise(x1, y1);
const nx0 = n00 * (1 - u) + n10 * u;
const nx1 = n01 * (1 - u) + n11 * u;
return nx0 * (1 - v) + nx1 * v;
}
function computeNoiseGradient(
perlinFn: (x: number, y: number) => number,
x: number,
y: number,
epsilon = 0.001,
): [number, number] {
const n1 = perlinFn(x + epsilon, y);
const n2 = perlinFn(x - epsilon, y);
const n3 = perlinFn(x, y + epsilon);
const n4 = perlinFn(x, y - epsilon);
const dx = (n1 - n2) / (2 * epsilon);
const dy = (n3 - n4) / (2 * epsilon);
const len = Math.sqrt(dx * dx + dy * dy) || 1;
return [dx / len, dy / len];
}
export function generateControlPoints(
width: number,
height: number,
variationFraction: number = randomRange(0.4, 0.6), // = 0.2,
normalOffset: number = randomRange(0.3, 0.6), // = 0.3,
blendFactor = 0.8,
smoothIters: number = Math.floor(randomRange(3, 5)), // = 3,
smoothFactor: number = randomRange(0.2, 0.3), // = 0.3,
smoothModifier: number = randomRange(-0.1, -0.05), // = -0.05,
): ControlPointPreset {
const w = width ?? Math.floor(randomRange(3, 6));
const h = height ?? Math.floor(randomRange(3, 6));
const conf: ControlPointConf[] = [];
const dx = w === 1 ? 0 : 2 / (w - 1);
const dy = h === 1 ? 0 : 2 / (h - 1);
for (let j = 0; j < h; j++) {
for (let i = 0; i < w; i++) {
const baseX = (w === 1 ? 0 : i / (w - 1)) * 2 - 1;
const baseY = (h === 1 ? 0 : j / (h - 1)) * 2 - 1;
const isBorder = i === 0 || i === w - 1 || j === 0 || j === h - 1;
const pertX = isBorder
? 0
: randomRange(-variationFraction * dx, variationFraction * dx);
const pertY = isBorder
? 0
: randomRange(-variationFraction * dy, variationFraction * dy);
let x = baseX + pertX;
let y = baseY + pertY;
const ur = isBorder ? 0 : randomRange(-60, 60);
const vr = isBorder ? 0 : randomRange(-60, 60);
const up = isBorder ? 1 : randomRange(0.8, 1.2);
const vp = isBorder ? 1 : randomRange(0.8, 1.2);
if (!isBorder) {
const uNorm = (baseX + 1) / 2;
const vNorm = (baseY + 1) / 2;
const [nx, ny] = computeNoiseGradient(smoothNoise, uNorm, vNorm, 0.001);
let offsetX = nx * normalOffset;
let offsetY = ny * normalOffset;
const distToBorder = Math.min(uNorm, 1 - uNorm, vNorm, 1 - vNorm); // in [0,0.5]
const weight = smoothstep(0, 1.0, distToBorder);
offsetX *= weight;
offsetY *= weight;
x = x * (1 - blendFactor) + (x + offsetX) * blendFactor;
y = y * (1 - blendFactor) + (y + offsetY) * blendFactor;
}
conf.push(p(i, j, x, y, ur, vr, up, vp));
}
}
smoothifyControlPoints(conf, w, h, smoothIters, smoothFactor, smoothModifier);
return preset(w, h, conf);
}

View File

@@ -0,0 +1,185 @@
/** @internal */
export interface ControlPointConf {
cx: number;
cy: number;
x: number;
y: number;
ur: number;
vr: number;
up: number;
vp: number;
}
/** @internal */
export interface ControlPointPreset {
width: number;
height: number;
conf: ControlPointConf[];
}
/** @internal */
export const p = (
cx: number,
cy: number,
x: number,
y: number,
ur = 0,
vr = 0,
up = 1,
vp = 1,
) => Object.freeze({ cx, cy, x, y, ur, vr, up, vp }) as ControlPointConf;
/** @internal */
export const preset = (
width: number,
height: number,
conf: ControlPointConf[],
) => Object.freeze({ width, height, conf }) as ControlPointPreset;
export const CONTROL_POINT_PRESETS: ControlPointPreset[] = [
// TODO: 竖屏推荐
preset(5, 5, [
p(0, 0, -1, -1, 0, 0, 1, 1),
p(1, 0, -0.5, -1, 0, 0, 1, 1),
p(2, 0, 0, -1, 0, 0, 1, 1),
p(3, 0, 0.5, -1, 0, 0, 1, 1),
p(4, 0, 1, -1, 0, 0, 1, 1),
p(0, 1, -1, -0.5, 0, 0, 1, 1),
p(1, 1, -0.5, -0.5, 0, 0, 1, 1),
p(2, 1, -0.0052029684413368305, -0.6131420587090777, 0, 0, 1, 1),
p(3, 1, 0.5884227308309977, -0.3990805107556692, 0, 0, 1, 1),
p(4, 1, 1, -0.5, 0, 0, 1, 1),
p(0, 2, -1, 0, 0, 0, 1, 1),
p(1, 2, -0.4210024670505933, -0.11895058380429502, 0, 0, 1, 1),
p(2, 2, -0.1019613423315412, -0.023812118047224606, 0, -47, 0.629, 0.849),
p(3, 2, 0.40275125660925437, -0.06345314544600389, 0, 0, 1, 1),
p(4, 2, 1, 0, 0, 0, 1, 1),
p(0, 3, -1, 0.5, 0, 0, 1, 1),
p(1, 3, 0.06801958477287173, 0.5205913248960121, -31, -45, 1, 1),
p(2, 3, 0.21446469120128908, 0.29331610114301043, 6, -56, 0.566, 1.321),
p(3, 3, 0.5, 0.5, 0, 0, 1, 1),
p(4, 3, 1, 0.5, 0, 0, 1, 1),
p(0, 4, -1, 1, 0, 0, 1, 1),
p(1, 4, -0.31378372841550195, 1, 0, 0, 1, 1),
p(2, 4, 0.26153633255328046, 1, 0, 0, 1, 1),
p(3, 4, 0.5, 1, 0, 0, 1, 1),
p(4, 4, 1, 1, 0, 0, 1, 1),
]),
// TODO: 横屏推荐
preset(4, 4, [
p(0, 0, -1, -1, 0, 0, 1, 1),
p(1, 0, -0.33333333333333337, -1, 0, 0, 1, 1),
p(2, 0, 0.33333333333333326, -1, 0, 0, 1, 1),
p(3, 0, 1, -1, 0, 0, 1, 1),
p(0, 1, -1, -0.04495399932657351, 0, 0, 1, 1),
p(1, 1, -0.24056117520129328, -0.22465999020104, 0, 0, 1, 1),
p(2, 1, 0.334758885767489, -0.00531297192779423, 0, 0, 1, 1),
p(3, 1, 0.9989920470678106, -0.3382976020775408, 8, 0, 0.566, 1.792),
p(0, 2, -1, 0.33333333333333326, 0, 0, 1, 1),
p(1, 2, -0.3425497314639411, -0.000027501607956947893, 0, 0, 1, 1),
p(2, 2, 0.3321437945812673, 0.1981776353859399, 0, 0, 1, 1),
p(3, 2, 1, 0.0766118180296832, 0, 0, 1, 1),
p(0, 3, -1, 1, 0, 0, 1, 1),
p(1, 3, -0.33333333333333337, 1, 0, 0, 1, 1),
p(2, 3, 0.33333333333333326, 1, 0, 0, 1, 1),
p(3, 3, 1, 1, 0, 0, 1, 1),
]),
preset(4, 4, [
p(0, 0, -1, -1, 0, 0, 1, 2.075),
p(1, 0, -0.33333333333333337, -1, 0, 0, 1, 1),
p(2, 0, 0.33333333333333326, -1, 0, 0, 1, 1),
p(3, 0, 1, -1, 0, 0, 1, 1),
p(0, 1, -1, -0.4545779491139603, 0, 0, 1, 1),
p(1, 1, -0.33333333333333337, -0.33333333333333337, 0, 0, 1, 1),
p(2, 1, 0.0889403142626457, -0.6025711180694033, -32, 45, 1, 1),
p(3, 1, 1, -0.33333333333333337, 0, 0, 1, 1),
p(0, 2, -1, -0.07402408608567845, 1, 0, 1, 0.094),
p(1, 2, -0.2719422694359541, 0.09775369930903222, 25, -18, 1.321, 0),
p(2, 2, 0.19877414408395877, 0.4307383294587789, 48, -40, 0.755, 0.975),
p(3, 2, 1, 0.33333333333333326, -37, 0, 1, 1),
p(0, 3, -1, 1, 0, 0, 1, 1),
p(1, 3, -0.33333333333333337, 1, 0, 0, 1, 1),
p(2, 3, 0.5125850864305672, 1, -20, -18, 0, 1.604),
p(3, 3, 1, 1, 0, 0, 1, 1),
]),
preset(5, 5, [
p(0, 0, -1, -1, 0, 0, 1, 1),
p(1, 0, -0.4501953125, -1, 0, 55, 1, 2.075),
p(2, 0, 0.1953125, -1, 0, 0, 1, 1),
p(3, 0, 0.4580078125, -1, 0, -25, 1, 1),
p(4, 0, 1, -1, 0, 0, 1, 1),
p(0, 1, -1, -0.2514475377525607, -16, 0, 2.327, 0.943),
p(1, 1, -0.55859375, -0.6609325945787148, 47, 0, 2.358, 0.377),
p(2, 1, 0.232421875, -0.5244375756366635, -66, -25, 1.855, 1.164),
p(3, 1, 0.685546875, -0.3753706470552125, 0, 0, 1, 1),
p(4, 1, 1, -0.6699125300354287, 0, 0, 1, 1),
p(0, 2, -1, 0.035910396862284255, 0, 0, 1, 1),
p(1, 2, -0.4921875, 0.005378616309457018, 90, 23, 1, 1.981),
p(2, 2, 0.021484375, -0.1365043639066228, 0, 42, 1, 1),
p(3, 2, 0.4765625, 0.05925822904974043, -30, 0, 1.95, 0.44),
p(4, 2, 1, 0.251428847823418, 0, 0, 1, 1),
p(0, 3, -1, 0.6968336464764276, -68, 0, 1, 0.786),
p(1, 3, -0.6904296875, 0.5890744209958608, -68, 0, 1, 1),
p(2, 3, 0.1845703125, 0.3879238667654693, 61, 0, 1, 1),
p(3, 3, 0.60546875, 0.4633553246018661, -47, -59, 0.849, 1.73),
p(4, 3, 1, 0.6214021886400309, -33, 0, 0.377, 1.604),
p(0, 4, -1, 1, 0, 0, 1, 1),
p(1, 4, -0.5, 1, 0, -73, 1, 1),
p(2, 4, -0.3271484375, 1, 0, -24, 0.314, 2.704),
p(3, 4, 0.5, 1, 0, 0, 1, 1),
p(4, 4, 1, 1, 0, 0, 1, 1),
]),
preset(5, 5, [
p(0, 0, -1, -1),
p(1, 0, -0.6393, -1, 0, 0, 1, 2.3884),
p(2, 0, 0, -1),
p(3, 0, 0.5, -1),
p(4, 0, 1, -1),
p(0, 1, -1, -0.2301),
p(1, 1, -0.6934, -0.331, 0, -0.7188, 1, 1.063),
p(2, 1, -0.0082, -0.6814, -0.2583, 0, 1.0964, 1),
p(3, 1, 0.5836, -0.531, 0.7029, 0, 1.5466, 1),
p(4, 1, 1, -0.6407),
p(0, 2, -1, 0.2973, 0, 0, 1.8352, 1),
p(1, 2, -0.4082, 0.0602),
p(2, 2, -0.1803, -0.3646, -0.2998, 0, 1.1513, 1),
p(3, 2, 0.477, -0.1027, 0.8903, -0.1882, 1.0807, 0.8551),
p(4, 2, 1, -0.2973),
p(0, 3, -1, 0.7628, 0, 0, 2.3868, 1),
p(1, 3, -0.2525, 0.4814, -0.8406, -1.6199, 1.4093, 1.2215),
p(2, 3, 0.3607, 0.2814, -1.0713, -0.0529, 1.0025, 0.7611),
p(3, 3, 0.4885, 0.623, 0, 0.8184, 1, 1.2876),
p(4, 3, 1, 0.5),
p(0, 4, -1, 1),
p(1, 4, -0.4033, 1),
p(2, 4, 0.2672, 1),
p(3, 4, 0.5967, 1),
p(4, 4, 1, 1),
]),
preset(5, 5, [
p(0, 0, -1, -1),
p(1, 0, -0.2197, -1),
p(2, 0, 0.0197, -1),
p(3, 0, 0.8033, -1),
p(4, 0, 1, -1),
p(0, 1, -1, -0.5451),
p(1, 1, -0.4885, -0.4035, -1.0246, -0.2268, 1.1936, 0.8005),
p(2, 1, -0.1213, -0.2867, 0, -0.6981, 1, 0.809),
p(3, 1, 0.3246, -0.5628, 0, -1.2188, 1, 1.044),
p(4, 1, 1, -0.3292),
p(0, 2, -1, 0.1416),
p(1, 2, -0.341, -0.0142, 0, -0.4004, 1, 1.1293),
p(2, 2, -0.0393, -0.023, 0.2915, -0.373, 1.044, 0.9879),
p(3, 2, 0.3148, -0.0673, -0.7853, -0.8962, 1.4709, 1.0247),
p(4, 2, 1, 0.1912),
p(0, 3, -1, 0.5),
p(1, 3, -0.2689, 0.2743, 0.3404, -0.5248, 1.0184, 0.4391),
p(2, 3, 0.0721, 0.269, 0.5302, 0.1244, 0.6723, 0.3225),
p(3, 3, 0.4148, 0.3894, -0.6977, -0.6783, 0.8094, 0.9247),
p(4, 3, 1, 0.446),
p(0, 4, -1, 1),
p(1, 4, -0.7311, 1),
p(2, 4, 0.323, 1),
p(3, 4, 0.6393, 1),
p(4, 4, 1, 1),
]),
] as const;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
precision highp float;
varying vec3 v_color;
varying vec2 v_uv;
uniform sampler2D u_texture;
uniform float u_time;
uniform float u_volume;
uniform float u_alpha;
// 预计算常量
const float INV_255 = 1.0 / 255.0;
const float HALF_INV_255 = 0.5 / 255.0;
const float GRADIENT_NOISE_A = 52.9829189;
const vec2 GRADIENT_NOISE_B = vec2(0.06711056, 0.00583715);
/* Gradient noise from Jorge Jimenez's presentation: */
/* http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare */
float gradientNoise(in vec2 uv) {
return fract(GRADIENT_NOISE_A * fract(dot(uv, GRADIENT_NOISE_B)));
}
// 优化的旋转函数避免重复计算sin/cos
vec2 rot(vec2 v, float angle) {
float s = sin(angle);
float c = cos(angle);
return vec2(c * v.x - s * v.y, s * v.x + c * v.y);
}
void main() {
// 合并计算以减少指令数
float volumeEffect = u_volume * 2.0;
float timeVolume = u_time + u_volume;
float dither = INV_255 * gradientNoise(gl_FragCoord.xy) - HALF_INV_255;
vec2 centeredUV = v_uv - vec2(0.2);
vec2 rotatedUV = rot(centeredUV, timeVolume * 2.0);
vec2 finalUV = rotatedUV * max(0.001, 1.0 - volumeEffect) + vec2(0.5);
vec4 result = texture2D(u_texture, finalUV);
float alphaVolumeFactor = u_alpha * max(0.5, 1.0 - u_volume * 0.5);
result.rgb *= v_color * alphaVolumeFactor;
result.a *= alphaVolumeFactor;
result.rgb += vec3(dither);
float dist = distance(v_uv, vec2(0.5));
float vignette = smoothstep(0.8, 0.3, dist);
float mask = 0.6 + vignette * 0.4;
result.rgb *= mask;
gl_FragColor = result;
}

View File

@@ -0,0 +1,21 @@
precision highp float;
attribute vec2 a_pos;
attribute vec3 a_color;
attribute vec2 a_uv;
varying vec3 v_color;
varying vec2 v_uv;
uniform float u_aspect;
void main() {
v_color = a_color;
v_uv = a_uv;
vec2 pos = a_pos;
if (u_aspect > 1.0) {
pos.y *= u_aspect;
} else {
pos.x /= u_aspect;
}
gl_Position = vec4(pos, 0.0, 1.0);
}

View File

@@ -0,0 +1,264 @@
import { Application } from "@pixi/app";
import { Texture } from "@pixi/core";
import { Container } from "@pixi/display";
import { BlurFilter } from "@pixi/filter-blur";
import { BulgePinchFilter } from "@pixi/filter-bulge-pinch";
import { ColorMatrixFilter } from "@pixi/filter-color-matrix";
import { Sprite } from "@pixi/sprite";
import {
loadResourceFromElement,
loadResourceFromUrl,
} from "../utils/resource";
import { BaseRenderer } from "./base";
import { clampPositive } from "#utils/clamp.ts";
class TimedContainer extends Container {
public time = 0;
}
export class PixiRenderer extends BaseRenderer {
private app: Application;
private curContainer?: TimedContainer;
private staticMode = false;
private lastContainer: Set<TimedContainer> = new Set();
private onTick = (delta: number): void => {
for (const lastContainer of this.lastContainer) {
lastContainer.alpha = clampPositive(lastContainer.alpha - delta / 60);
if (lastContainer.alpha <= 0) {
this.app.stage.removeChild(lastContainer);
this.lastContainer.delete(lastContainer);
lastContainer.destroy(true);
}
}
if (this.curContainer) {
this.curContainer.alpha = Math.min(
1,
this.curContainer.alpha + delta / 60,
);
const [s1, s2, s3, s4] = this.curContainer.children as Sprite[];
const maxSize = Math.max(this.app.screen.width, this.app.screen.height);
s1.position.set(this.app.screen.width / 2, this.app.screen.height / 2);
s2.position.set(
this.app.screen.width / 2.5,
this.app.screen.height / 2.5,
);
s3.position.set(this.app.screen.width / 2, this.app.screen.height / 2);
s4.position.set(this.app.screen.width / 2, this.app.screen.height / 2);
s1.width = maxSize * Math.sqrt(2);
s1.height = s1.width;
s2.width = maxSize * 0.8;
s2.height = s2.width;
s3.width = maxSize * 0.5;
s3.height = s3.width;
s4.width = maxSize * 0.25;
s4.height = s4.width;
this.curContainer.time += delta * this.flowSpeed;
s1.rotation += (delta / 1000) * this.flowSpeed;
s2.rotation -= (delta / 500) * this.flowSpeed;
s3.rotation += (delta / 1000) * this.flowSpeed;
s4.rotation -= (delta / 750) * this.flowSpeed;
s3.x =
this.app.screen.width / 2 +
(this.app.screen.width / 4) *
Math.cos((this.curContainer.time / 1000) * 0.75);
s3.y =
this.app.screen.height / 2 +
(this.app.screen.width / 4) *
Math.cos((this.curContainer.time / 1000) * 0.75);
s4.x =
this.app.screen.width / 2 +
(this.app.screen.width / 4) * 0.1 +
Math.cos(this.curContainer.time * 0.006 * 0.75);
s4.y =
this.app.screen.height / 2 +
(this.app.screen.width / 4) * 0.1 +
Math.cos(this.curContainer.time * 0.006 * 0.75);
if (
this.curContainer.alpha >= 1 &&
this.lastContainer.size === 0 &&
this.staticMode
) {
this.app.ticker.stop();
}
}
};
constructor(protected override canvas: HTMLCanvasElement) {
super(canvas);
this.app = new Application({
view: canvas,
resizeTo: this.canvas,
powerPreference: "low-power",
backgroundAlpha: 1,
});
this.rebuildFilters();
this.app.ticker.maxFPS = 30;
this.app.ticker.add(this.onTick);
this.app.ticker.start();
}
protected override onResize(width: number, height: number): void {
super.onResize(width, height);
this.app.resize();
this.rebuildFilters();
}
override setRenderScale(scale: number): void {
super.setRenderScale(scale);
this.rebuildFilters();
}
private rebuildFilters() {
const minBorder = Math.min(this.canvas.width, this.canvas.height);
const maxBorder = Math.max(this.canvas.width, this.canvas.height);
const c0 = new ColorMatrixFilter();
c0.saturate(1.2, false);
const c1 = new ColorMatrixFilter();
c1.brightness(0.6, false);
const c2 = new ColorMatrixFilter();
c2.contrast(0.3, true);
for (const filter of this.app.stage.filters ?? []) {
filter.destroy();
}
this.app.stage.filters = [];
this.app.stage.filters.push(new BlurFilter(5, 1));
this.app.stage.filters.push(new BlurFilter(10, 1));
this.app.stage.filters.push(new BlurFilter(20, 2));
this.app.stage.filters.push(new BlurFilter(40, 2));
this.app.stage.filters.push(new BlurFilter(80, 2));
if (minBorder > 768) this.app.stage.filters.push(new BlurFilter(160, 4));
if (minBorder > 768 * 2)
this.app.stage.filters.push(new BlurFilter(320, 4));
this.app.stage.filters.push(c0, c1, c2);
this.app.stage.filters.push(new BlurFilter(5, 1));
if (Math.random() > 0.5) {
this.app.stage.filters.push(
new BulgePinchFilter({
radius: (maxBorder + minBorder) / 2,
strength: 1,
center: [0.25, 1],
}),
);
this.app.stage.filters.push(
new BulgePinchFilter({
radius: (maxBorder + minBorder) / 2,
strength: 1,
center: [0.75, 0],
}),
);
} else {
this.app.stage.filters.push(
new BulgePinchFilter({
radius: (maxBorder + minBorder) / 2,
strength: 1,
center: [0.75, 1],
}),
);
this.app.stage.filters.push(
new BulgePinchFilter({
radius: (maxBorder + minBorder) / 2,
strength: 1,
center: [0.25, 0],
}),
);
}
}
override setStaticMode(enable = false): void {
this.staticMode = enable;
this.app.ticker.start();
}
override setFPS(fps: number): void {
this.app.ticker.maxFPS = fps;
}
override pause(): void {
this.app.ticker.stop();
this.app.render();
}
override resume(): void {
this.app.ticker.start();
}
override setLowFreqVolume(_volume: number): void {
// NOOP
}
override setHasLyric(_hasLyric: boolean): void {
// NOOP
}
override async setAlbum(
albumSource?: string | HTMLImageElement | HTMLVideoElement,
isVideo?: boolean,
): Promise<void> {
if (
!albumSource ||
(typeof albumSource === "string" && albumSource.trim().length === 0)
)
return;
let res: HTMLImageElement | HTMLVideoElement | null = null;
let remainRetryTimes = 5;
let tex: Texture | null = null;
while (!tex?.baseTexture?.resource?.valid && remainRetryTimes > 0) {
try {
if (typeof albumSource === "string") {
res = await loadResourceFromUrl(albumSource, isVideo);
} else {
res = await loadResourceFromElement(albumSource);
}
tex = Texture.from(res, {
resourceOptions: {
autoLoad: false,
},
});
await tex.baseTexture.resource.load();
} catch (error) {
console.warn(
`failed on loading album image, retrying (${remainRetryTimes})`,
albumSource,
error,
);
tex = null;
remainRetryTimes--;
}
}
if (!tex) return;
const container = new TimedContainer();
const s1 = new Sprite(tex);
const s2 = new Sprite(tex);
const s3 = new Sprite(tex);
const s4 = new Sprite(tex);
s1.anchor.set(0.5, 0.5);
s2.anchor.set(0.5, 0.5);
s3.anchor.set(0.5, 0.5);
s4.anchor.set(0.5, 0.5);
s1.rotation = Math.random() * Math.PI * 2;
s2.rotation = Math.random() * Math.PI * 2;
s3.rotation = Math.random() * Math.PI * 2;
s4.rotation = Math.random() * Math.PI * 2;
container.addChild(s1, s2, s3, s4);
if (this.curContainer) this.lastContainer.add(this.curContainer);
this.curContainer = container;
this.app.stage.addChild(container);
this.curContainer.alpha = 0;
this.app.ticker.start();
}
override dispose(): void {
super.dispose();
this.app.ticker.remove(this.onTick);
this.app.destroy(true);
}
override getElement(): HTMLElement {
return this.canvas;
}
}

View File

@@ -0,0 +1,11 @@
#version 300 es
precision highp float;
uniform sampler2D src;
in vec2 f_v_coord;
out vec4 fragColor;
void main() {
vec2 coord = f_v_coord;
fragColor = texture(src, coord);
}

View File

@@ -0,0 +1,10 @@
#version 300 es
precision highp float;
in vec2 v_coord;
out vec2 f_v_coord;
void main() {
gl_Position = vec4(v_coord, 0.0f, 1.0f);
f_v_coord = (vec2(v_coord.x, v_coord.y) + vec2(1.0f, 1.0f)) / 2.0f;
}

View File

@@ -0,0 +1,17 @@
#version 300 es
precision highp float;
uniform sampler2D src;
uniform float lerp;
uniform float scale;
in vec2 f_v_coord;
out vec4 fragColor;
void main() {
vec2 tex_coord = f_v_coord;
if(scale < 1.0f) {
tex_coord /= scale;
}
vec4 srcColor = texture(src, tex_coord);
fragColor = mix(vec4(srcColor.xyz, 0.0f), srcColor, lerp);
}

View File

@@ -0,0 +1,20 @@
#version 300 es
precision highp float;
uniform sampler2D src;
in vec2 f_v_coord;
out vec4 fragColor;
/* Gradient noise from Jorge Jimenez's presentation: */
/* http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare */
float gradientNoise(in vec2 uv) {
return fract(52.9829189f * fract(dot(uv, vec2(0.06711056f, 0.00583715f))));
}
void main() {
vec2 tex_coord = f_v_coord;
float dither = (1.0f / 255.0f) * gradientNoise(gl_FragCoord.xy) - (0.5f / 255.0f);
vec4 color = texture(src, tex_coord);
color += dither;
fragColor = color;
}

View File

@@ -0,0 +1,42 @@
#version 300 es
precision highp float;
uniform sampler2D src;
uniform sampler2D historyFrame0;
uniform vec2 texSize;
in vec2 f_v_coord;
out vec4 fragColor;
void main() {
fragColor = texture(src, f_v_coord);
return;
// get the neighborhood min / max from this frame's render
vec3 center = texture(src, f_v_coord).rgb;
vec3 minColor = center;
vec3 maxColor = center;
for (int iy = -1; iy <= 1; ++iy)
{
for (int ix = -1; ix <= 1; ++ix)
{
if (ix == 0 && iy == 0)
continue;
vec2 offsetUV = ((f_v_coord * texSize + vec2(ix, iy))) / texSize;
vec3 color = texture(src, offsetUV).rgb;
minColor = min(minColor, color);
maxColor = max(maxColor, color);
}
}
// get last frame's pixel and clamp it to the neighborhood of this frame
vec3 old = texture(historyFrame0, f_v_coord).rgb;
old = max(minColor, old);
old = min(maxColor, old);
// interpolate from the clamped old color to the new color.
// Reject all history when the mouse moves.
float lerpAmount = 0.1f;
vec3 pixelColor = mix(old, center, lerpAmount);
fragColor = vec4(pixelColor, 1.0f);
}