Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/assets/components/SimAP.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ body:not(.hideList) .listBtn{color:var(--SimAPTheme);opacity:.7;}
/* 流光背景 */
#background{z-index:-1;inset:0;position:absolute;pointer-events:none;opacity:.2;transition:background .3s;}
#background>div{position:absolute;inset:0;background:linear-gradient(135deg, rgba(255,255,255,.1), rgba(255,255,255,.5));}
#background>canvas{display:block;width:100%;height:100%;}
.disableBackgroundBlur #background>canvas{display:none;}

190 changes: 143 additions & 47 deletions src/frontend/assets/components/SimAP.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,59 +148,155 @@ const switchMusic = (playConfig) => {
// 动态混色控制器
const PlayerBackground = {
init() {
canvas = document.getElementById("backgroundAnimation");
this.ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
this.blobs = [];
this.animate(true);
const canvas = document.getElementById("backgroundAnimation");
const gl = canvas.getContext("webgl2");
if (!gl) return console.error("WebGL2 not supported");
this.canvas = canvas;
this.gl = gl;
this.colors = ["#000000", "#333333", "#666666", "#999999"];
this.pausedAt = 0;
this.offset = 0;
const scale = 0.5;
const resize = () => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
gl.viewport(0, 0, canvas.width, canvas.height);
};
window.addEventListener("resize", resize);
resize();
const vsSource = `#version 300 es
in vec2 aPos;
out vec2 vUv;
void main(){
vUv = (aPos+1.0)*0.5;
gl_Position = vec4(aPos,0.0,1.0);
}`;
const fsSource = `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform float uTime;
uniform vec2 uRes;
uniform vec3 uColors[4];

float fbm(vec2 p){
float v = 0.0;
float a = 0.5;
for(int i=0;i<2;i++){
v += a * sin(p.x*0.8 + float(i)*1.5) * cos(p.y*0.9 + float(i)*1.3);
p *= 1.3;
a *= 0.5;
}
return v*0.5 + 0.5;
}

vec3 palette(float t){
float f = t * 3.0;
int idx = int(floor(f));
float blend = fract(f);
vec3 c1 = uColors[idx];
vec3 c2 = uColors[min(idx+1, 3)];
return mix(c1,c2,blend);
}

void main(){
vec2 uv = vUv * 2.0;
float t = uTime*0.25;

vec2 p = uv;
p += 0.04*vec2(
sin(p.y*1.2 + t),
cos(p.x*1.3 - t)
);

float f = fbm(p + vec2(t*0.3, -t*0.2));
f = smoothstep(0.2,0.8,f);

vec3 col = palette(f);
fragColor = vec4(col,1.0);
}`;
const createShader = (type, source) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
};
const vs = createShader(gl.VERTEX_SHADER, vsSource);
const fs = createShader(gl.FRAGMENT_SHADER, fsSource);
if (!vs || !fs) return;
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return;
}
this.program = program;
this.aPos = gl.getAttribLocation(program, "aPos");
this.uTime = gl.getUniformLocation(program, "uTime");
this.uRes = gl.getUniformLocation(program, "uRes");
this.uColors = gl.getUniformLocation(program, "uColors");
const quad = new Float32Array([-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1]);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
this.vbo = vbo;
this.animate();
},
animate(isInit) {
requestAnimationFrame(() => {PlayerBackground.animate();});
animate() {
requestAnimationFrame(() => { PlayerBackground.animate(); });
if (!config.getItem("backgroundBlur")) return;
if (!document.body.classList.contains("playing") && !isInit) return;
const ctx = PlayerBackground.ctx;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
for (const blob of PlayerBackground.blobs) {
// 位移
blob.x += blob.dx;
blob.y += blob.dy;
if (blob.x - blob.radius < 0 || blob.x + blob.radius > window.innerWidth) blob.dx *= -1;
if (blob.y - blob.radius < 0 || blob.y + blob.radius > window.innerHeight) blob.dy *= -1;
const playing = document.body.classList.contains("playing");
if (!playing) {
if (this.pausedAt === 0) this.pausedAt = performance.now();
return;
}
PlayerBackground.drawBlobs();
},
update(mainColor, subColors) {
document.getElementById("background").style.background = mainColor;
this.mainColor = mainColor;
this.blobs = [];
for (let i = 0; i < 3; i++) {
this.blobs.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * screen.width / 3 + screen.width / 5,
color: subColors[i],
dx: ((Math.random() < 0.5) ? 1 : -1) * (Math.random() * 0.5 + 0.5),
dy: ((Math.random() < 0.5) ? 1 : -1) * (Math.random() * 0.5 + 0.5),
});
if (this.pausedAt !== 0) {
this.offset += performance.now() - this.pausedAt;
this.pausedAt = 0;
}
PlayerBackground.drawBlobs();
this.renderFrame();
},
drawBlobs() {
const ctx = PlayerBackground.ctx;
for (const blob of PlayerBackground.blobs) {
ctx.beginPath();
const gradient = ctx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, blob.radius);
gradient.addColorStop(0, blob.color);
gradient.addColorStop(1, "transparent");
ctx.fillStyle = gradient;
ctx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2);
ctx.fill();
renderFrame() {
if (!this.gl) return;
const gl = this.gl;
const canvas = this.canvas;
const t = (performance.now() - this.offset) * 0.001;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(this.program);
gl.uniform1f(this.uTime, t);
gl.uniform2f(this.uRes, canvas.width, canvas.height);
const colorData = [];
for (let c of this.colors) {
colorData.push(parseInt(c.slice(1, 3), 16) / 255);
colorData.push(parseInt(c.slice(3, 5), 16) / 255);
colorData.push(parseInt(c.slice(5, 7), 16) / 255);
}
gl.uniform3fv(this.uColors, colorData);
gl.enableVertexAttribArray(this.aPos);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
},
rgbToHex(rgb) {
const m = rgb.match(/\d+/g);
if (!m) return "#000000";
return "#" + m.slice(0, 3).map(v => (+v).toString(16).padStart(2, "0")).join("");
},
update(mainColor, subColors) {
document.getElementById("background").style.background = mainColor;
this.colors = [
this.rgbToHex(mainColor),
...subColors.map(c => this.rgbToHex(c))
];
this.renderFrame();
}
}
PlayerBackground.init();
Expand Down