Files
public_js/background.js
2026-05-31 15:58:07 +00:00

470 lines
16 KiB
JavaScript

(() => {
const vertexShaderSource = `#version 300 es
in vec2 a_position;
out vec2 v_position;
void main() {
v_position = a_position;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const fragmentShaderSource = `#version 300 es
precision highp float;
uniform vec2 u_viewportSize;
uniform vec3 u_color1;
uniform vec3 u_color2;
uniform vec3 u_color3;
uniform vec3 u_color4;
uniform float u_colorSize;
uniform float u_colorSpacing;
uniform float u_colorRotation;
uniform float u_colorSpread;
uniform vec2 u_colorOffset;
uniform float u_displacement;
uniform float u_zoom;
uniform float u_spacing;
uniform float u_seed;
uniform vec2 u_transformPosition;
uniform float u_time;
in vec2 v_position;
out vec4 outColor;
vec4 permute(vec4 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
vec4 taylorInvSqrt(vec4 r) {
return 1.79284291400159 - 0.85373472095314 * r;
}
float snoise(vec3 v) {
const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod(i, 289.0);
vec4 p = permute(permute(permute(
i.z + vec4(0.0, i1.z, i2.z, 1.0))
+ i.y + vec4(0.0, i1.y, i2.y, 1.0))
+ i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(
dot(p0, p0),
dot(p1, p1),
dot(p2, p2),
dot(p3, p3)
));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
vec4 m = max(0.6 - vec4(
dot(x0, x0),
dot(x1, x1),
dot(x2, x2),
dot(x3, x3)
), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(
dot(p0, x0),
dot(p1, x1),
dot(p2, x2),
dot(p3, x3)
));
}
vec3 noiseDerivatives(vec3 p) {
float e = 0.035;
float n = snoise(p);
float dx = snoise(p + vec3(e, 0.0, 0.0)) - n;
float dy = snoise(p + vec3(0.0, e, 0.0)) - n;
float dz = snoise(p + vec3(0.0, 0.0, e)) - n;
return vec3(dx, dy, dz) / e;
}
float grain(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
mat2 rotate2d(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(c, -s, s, c);
}
void main() {
vec2 position = v_position;
position.x *= min(1.0, u_viewportSize.x / u_viewportSize.y);
position.y *= min(1.0, u_viewportSize.y / u_viewportSize.x);
position /= u_zoom;
position += u_transformPosition;
vec2 noisePosition = position * 0.5 + 0.5;
vec3 displacementNoise = noiseDerivatives(vec3(noisePosition, u_seed + u_time * 0.015));
position += displacementNoise.xz * u_displacement * 0.12;
vec2 offsetPosition = position;
offsetPosition -= u_colorOffset;
offsetPosition = mod(offsetPosition - u_spacing, vec2(u_spacing * 2.0)) - u_spacing;
offsetPosition = rotate2d(offsetPosition.x * 0.04 - u_colorRotation) * offsetPosition;
offsetPosition /= vec2(max(u_colorSize, 0.001));
offsetPosition *= vec2(1.0 / max(u_colorSpread, 0.001), 1.0);
vec3 color = vec3(0.0);
color = mix(u_color1, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, u_colorSpacing * 1.5))));
color = mix(u_color2, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, u_colorSpacing * 0.5))));
color = mix(u_color3, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, -u_colorSpacing * 0.5))));
color = mix(u_color4, color, smoothstep(0.0, 1.0, distance(offsetPosition, vec2(0.0, -u_colorSpacing * 1.5))));
float textureNoise = grain(v_position * u_viewportSize * 0.5 + u_time * 12.0);
float vignette = smoothstep(1.1, 0.16, length(v_position * vec2(0.82, 1.0)));
color += (textureNoise - 0.5) * 0.055;
color *= 0.74 + vignette * 0.42;
color = clamp(color, 0.0, 1.0);
outColor = vec4(color, 1.0);
}
`;
const defaultOptions = {
color1: "#16254b",
color2: "#23418a",
color3: "#aadfd9",
color4: "#e64f0f",
colorSize: 0.75,
colorSpacing: 0.52,
colorRotation: -0.381592653589793,
colorSpread: 4.52,
colorOffset: [-0.7741174697875977, -0.20644775390624992],
displacement: 5,
seed: 0.18,
position: [-0.2816110610961914, -0.43914794921875],
zoom: 0.72,
spacing: 4.27
};
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const message = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error(message || "Shader compile failed");
}
return shader;
}
function createProgram(gl) {
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program);
gl.deleteProgram(program);
throw new Error(message || "Program link failed");
}
return program;
}
function parseHexColor(value) {
const hex = String(value || "").trim().replace("#", "");
const normalized = hex.length === 3
? hex.split("").map((char) => char + char).join("")
: hex.padEnd(6, "0").slice(0, 6);
const int = Number.parseInt(normalized, 16);
if (!Number.isFinite(int)) return [0, 0, 0];
return [
((int >> 16) & 255) / 255,
((int >> 8) & 255) / 255,
(int & 255) / 255
];
}
function parseNumber(value, fallback) {
const number = Number.parseFloat(value);
return Number.isFinite(number) ? number : fallback;
}
function parseVector(value, fallback) {
if (!value) return fallback;
const parts = String(value).split(",").map((part) => Number.parseFloat(part.trim()));
return parts.length >= 2 && parts.every(Number.isFinite) ? [parts[0], parts[1]] : fallback;
}
class FluidGradient extends HTMLElement {
static get observedAttributes() {
return [
"color1",
"color2",
"color3",
"color4",
"colorsize",
"colorspacing",
"colorrotation",
"colorspread",
"coloroffset",
"displacement",
"seed",
"position",
"zoom",
"spacing"
];
}
constructor() {
super();
this.canvas = document.createElement("canvas");
this.shadow = this.attachShadow({ mode: "open" });
this.shadow.innerHTML = `
<style>
:host {
display: block;
position: relative;
overflow: hidden;
contain: content;
touch-action: none;
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
</style>
`;
this.shadow.append(this.canvas);
this.options = structuredClone(defaultOptions);
this.pointer = {
x: 0.5,
y: 0.5,
targetX: 0.5,
targetY: 0.5,
displacement: defaultOptions.displacement,
seed: defaultOptions.seed
};
this.bounds = { width: 1, height: 1, dpr: 1 };
this.frame = 0;
this.start = performance.now();
this.resizeObserver = new ResizeObserver(() => this.resize());
this.handlePointerMove = this.handlePointerMove.bind(this);
this.render = this.render.bind(this);
}
connectedCallback() {
this.gl = this.canvas.getContext("webgl2", {
alpha: false,
antialias: true,
depth: false,
preserveDrawingBuffer: false
});
if (!this.gl) {
this.style.background = "radial-gradient(circle at 30% 15%, #aadfd9, transparent 28%), radial-gradient(circle at 24% 78%, #e64f0f, transparent 38%), radial-gradient(circle at 70% 60%, #23418a, transparent 42%), #05090a";
return;
}
this.program = createProgram(this.gl);
this.createGeometry();
this.collectUniforms();
this.readAttributes();
this.resizeObserver.observe(this);
this.addEventListener("pointermove", this.handlePointerMove, { passive: true });
this.resize();
this.play();
}
disconnectedCallback() {
cancelAnimationFrame(this.frame);
this.resizeObserver.disconnect();
this.removeEventListener("pointermove", this.handlePointerMove);
}
attributeChangedCallback() {
this.readAttributes();
}
createGeometry() {
const gl = this.gl;
const positions = new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
]);
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(this.program, "a_position");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
}
collectUniforms() {
const gl = this.gl;
this.uniforms = {
viewportSize: gl.getUniformLocation(this.program, "u_viewportSize"),
color1: gl.getUniformLocation(this.program, "u_color1"),
color2: gl.getUniformLocation(this.program, "u_color2"),
color3: gl.getUniformLocation(this.program, "u_color3"),
color4: gl.getUniformLocation(this.program, "u_color4"),
colorSize: gl.getUniformLocation(this.program, "u_colorSize"),
colorSpacing: gl.getUniformLocation(this.program, "u_colorSpacing"),
colorRotation: gl.getUniformLocation(this.program, "u_colorRotation"),
colorSpread: gl.getUniformLocation(this.program, "u_colorSpread"),
colorOffset: gl.getUniformLocation(this.program, "u_colorOffset"),
displacement: gl.getUniformLocation(this.program, "u_displacement"),
seed: gl.getUniformLocation(this.program, "u_seed"),
transformPosition: gl.getUniformLocation(this.program, "u_transformPosition"),
zoom: gl.getUniformLocation(this.program, "u_zoom"),
spacing: gl.getUniformLocation(this.program, "u_spacing"),
time: gl.getUniformLocation(this.program, "u_time")
};
}
readAttributes() {
this.options.color1 = parseHexColor(this.getAttribute("color1") || defaultOptions.color1);
this.options.color2 = parseHexColor(this.getAttribute("color2") || defaultOptions.color2);
this.options.color3 = parseHexColor(this.getAttribute("color3") || defaultOptions.color3);
this.options.color4 = parseHexColor(this.getAttribute("color4") || defaultOptions.color4);
this.options.colorSize = parseNumber(this.getAttribute("colorsize"), defaultOptions.colorSize);
this.options.colorSpacing = parseNumber(this.getAttribute("colorspacing"), defaultOptions.colorSpacing);
this.options.colorRotation = parseNumber(this.getAttribute("colorrotation"), defaultOptions.colorRotation);
this.options.colorSpread = parseNumber(this.getAttribute("colorspread"), defaultOptions.colorSpread);
this.options.colorOffset = parseVector(this.getAttribute("coloroffset"), defaultOptions.colorOffset);
this.options.displacement = parseNumber(this.getAttribute("displacement"), defaultOptions.displacement);
this.options.seed = parseNumber(this.getAttribute("seed"), defaultOptions.seed);
this.options.position = parseVector(this.getAttribute("position"), defaultOptions.position);
this.options.zoom = parseNumber(this.getAttribute("zoom"), defaultOptions.zoom);
this.options.spacing = parseNumber(this.getAttribute("spacing"), defaultOptions.spacing);
}
resize() {
if (!this.gl) return;
const rect = this.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.max(1, Math.floor(rect.width * dpr));
const height = Math.max(1, Math.floor(rect.height * dpr));
if (this.canvas.width !== width || this.canvas.height !== height) {
this.canvas.width = width;
this.canvas.height = height;
this.bounds = { width, height, dpr };
this.gl.viewport(0, 0, width, height);
}
}
handlePointerMove(event) {
const rect = this.getBoundingClientRect();
const x = rect.width > 0 ? (event.clientX - rect.left) / rect.width : 0.5;
const y = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
this.pointer.targetX = Math.min(1, Math.max(0, x));
this.pointer.targetY = Math.min(1, Math.max(0, y));
}
play() {
if (this.frame) return;
this.frame = requestAnimationFrame(this.render);
}
render(now) {
const gl = this.gl;
const pointer = this.pointer;
const options = this.options;
pointer.x += (pointer.targetX - pointer.x) * 0.1;
pointer.y += (pointer.targetY - pointer.y) * 0.1;
pointer.displacement += ((pointer.x * 5) - pointer.displacement) * 0.1;
pointer.seed += (((pointer.y * 2) - 1) - pointer.seed) * 0.1;
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(this.program);
gl.uniform2f(this.uniforms.viewportSize, this.bounds.width, this.bounds.height);
gl.uniform3fv(this.uniforms.color1, options.color1);
gl.uniform3fv(this.uniforms.color2, options.color2);
gl.uniform3fv(this.uniforms.color3, options.color3);
gl.uniform3fv(this.uniforms.color4, options.color4);
gl.uniform1f(this.uniforms.colorSize, options.colorSize);
gl.uniform1f(this.uniforms.colorSpacing, options.colorSpacing);
gl.uniform1f(this.uniforms.colorRotation, options.colorRotation);
gl.uniform1f(this.uniforms.colorSpread, options.colorSpread);
gl.uniform2fv(this.uniforms.colorOffset, options.colorOffset);
gl.uniform1f(this.uniforms.displacement, pointer.displacement);
gl.uniform1f(this.uniforms.seed, pointer.seed);
gl.uniform2fv(this.uniforms.transformPosition, options.position);
gl.uniform1f(this.uniforms.zoom, options.zoom);
gl.uniform1f(this.uniforms.spacing, options.spacing);
gl.uniform1f(this.uniforms.time, (now - this.start) / 1000);
gl.drawArrays(gl.TRIANGLES, 0, 6);
this.frame = requestAnimationFrame(this.render);
}
}
customElements.define("fluid-gradient", FluidGradient);
})();