IMP: viz performances
This commit is contained in:
parent
2bd42fc202
commit
baec739b01
1 changed files with 198 additions and 202 deletions
|
|
@ -89,6 +89,18 @@ export class UnknownPleasuresWebGL {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createBuffers() {
|
||||||
|
this.quadBuffer = this.gl.createBuffer();
|
||||||
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadBuffer);
|
||||||
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), this.gl.STATIC_DRAW);
|
||||||
|
|
||||||
|
this.lineBuffer = this.gl.createBuffer();
|
||||||
|
|
||||||
|
// Pre-allocate vertex buffer (max possible size: historySize * dataPoints * 6 vertices * 3 floats)
|
||||||
|
const maxVertices = this.historySize * this.dataPoints * 6; // 6 vertices per segment
|
||||||
|
this.vertexBuffer = new Float32Array(maxVertices * 3); // 3 floats per vertex (x,y,edge)
|
||||||
|
}
|
||||||
|
|
||||||
_initGL(gl, width, height) {
|
_initGL(gl, width, height) {
|
||||||
if (this.lineProgram) return;
|
if (this.lineProgram) return;
|
||||||
this.gl = gl;
|
this.gl = gl;
|
||||||
|
|
@ -275,15 +287,7 @@ export class UnknownPleasuresWebGL {
|
||||||
this.composite_u_isDarkTheme = gl.getUniformLocation(this.compositeProgram, 'u_isDarkTheme');
|
this.composite_u_isDarkTheme = gl.getUniformLocation(this.compositeProgram, 'u_isDarkTheme');
|
||||||
this.composite_u_time = gl.getUniformLocation(this.compositeProgram, 'u_time');
|
this.composite_u_time = gl.getUniformLocation(this.compositeProgram, 'u_time');
|
||||||
|
|
||||||
// === FULLSCREEN QUAD BUFFER ===
|
this._createBuffers(); // Use helper
|
||||||
this.quadBuffer = gl.createBuffer();
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
// === LINE GEOMETRY BUFFER (dynamic) ===
|
|
||||||
this.lineBuffer = gl.createBuffer();
|
|
||||||
|
|
||||||
// === FRAMEBUFFER FOR POST-PROCESSING ===
|
|
||||||
this._createFramebuffer(gl, width, height);
|
this._createFramebuffer(gl, width, height);
|
||||||
|
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
|
|
@ -321,7 +325,7 @@ export class UnknownPleasuresWebGL {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createFramebuffer(gl, width, height) {
|
_createFramebuffer(gl, width, height) {
|
||||||
// Framebuffer 1: Scene (lines)
|
// Framebuffer 1: Scene (lines) - FULL RESOLUTION
|
||||||
this.framebuffer = gl.createFramebuffer();
|
this.framebuffer = gl.createFramebuffer();
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||||
|
|
||||||
|
|
@ -335,27 +339,31 @@ export class UnknownPleasuresWebGL {
|
||||||
|
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.sceneTexture, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.sceneTexture, 0);
|
||||||
|
|
||||||
// Framebuffer 2: Blur intermediate (for horizontal pass)
|
// Blur Resolution (Half size for performance)
|
||||||
|
const blurW = Math.max(1, width >> 1);
|
||||||
|
const blurH = Math.max(1, height >> 1);
|
||||||
|
|
||||||
|
// Framebuffer 2: Blur intermediate
|
||||||
this.blurFramebuffer = gl.createFramebuffer();
|
this.blurFramebuffer = gl.createFramebuffer();
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer);
|
||||||
|
|
||||||
this.blurTexture = gl.createTexture();
|
this.blurTexture = gl.createTexture();
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.blurTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.blurTexture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // LINEAR!
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurTexture, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurTexture, 0);
|
||||||
|
|
||||||
// Framebuffer 3: Blur final (for vertical pass result)
|
// Framebuffer 3: Blur final
|
||||||
this.blurFinalFramebuffer = gl.createFramebuffer();
|
this.blurFinalFramebuffer = gl.createFramebuffer();
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFinalFramebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFinalFramebuffer);
|
||||||
|
|
||||||
this.blurFinalTexture = gl.createTexture();
|
this.blurFinalTexture = gl.createTexture();
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
|
@ -363,21 +371,21 @@ export class UnknownPleasuresWebGL {
|
||||||
|
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurFinalTexture, 0);
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurFinalTexture, 0);
|
||||||
|
|
||||||
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
||||||
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
||||||
console.error('Framebuffer incomplete:', status);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resizeFramebuffer(gl, width, height) {
|
_resizeFramebuffer(gl, width, height) {
|
||||||
|
const blurW = Math.max(1, width >> 1);
|
||||||
|
const blurH = Math.max(1, height >> 1);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.blurTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.blurTexture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.blurFinalTexture);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, blurW, blurH, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildPalette(color) {
|
_buildPalette(color) {
|
||||||
|
|
@ -406,74 +414,99 @@ export class UnknownPleasuresWebGL {
|
||||||
this._paletteColor = color;
|
this._paletteColor = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_generateLineQuads(points, thickness, width, height, outBuffer, offset) {
|
||||||
* Generate quad vertices for a thick line with proper miter joints.
|
if (points.length < 2) return 0;
|
||||||
* Precomputes averaged normals at shared vertices so segments connect seamlessly.
|
|
||||||
*/
|
|
||||||
_generateLineQuads(points, thickness, width, height) {
|
|
||||||
if (points.length < 2) return new Float32Array(0);
|
|
||||||
|
|
||||||
const vertices = [];
|
|
||||||
const toClip = (x, y) => [(x / width) * 2 - 1, 1 - (y / height) * 2];
|
|
||||||
const n = points.length;
|
const n = points.length;
|
||||||
|
let ptr = offset;
|
||||||
|
|
||||||
// Precompute per-segment normals
|
// Precompute normals (reuse internal arrays if possible, but for now stack var is fine)
|
||||||
const segNx = new Float32Array(n - 1);
|
// Optimization: Single pass miter calculation
|
||||||
const segNy = new Float32Array(n - 1);
|
|
||||||
for (let i = 0; i < n - 1; i++) {
|
|
||||||
const dx = points[i + 1].x - points[i].x;
|
|
||||||
const dy = points[i + 1].y - points[i].y;
|
|
||||||
const len = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (len < 0.001) {
|
|
||||||
segNx[i] = 0;
|
|
||||||
segNy[i] = -1;
|
|
||||||
} else {
|
|
||||||
segNx[i] = -dy / len;
|
|
||||||
segNy[i] = dx / len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute miter normals at each point (average of adjacent segment normals)
|
// Helper to clip X,Y
|
||||||
const miterNx = new Float32Array(n);
|
const wInv = 2 / width;
|
||||||
const miterNy = new Float32Array(n);
|
const hInv = 2 / height;
|
||||||
// First point: use first segment normal
|
|
||||||
miterNx[0] = segNx[0];
|
|
||||||
miterNy[0] = segNy[0];
|
|
||||||
// Last point: use last segment normal
|
|
||||||
miterNx[n - 1] = segNx[n - 2];
|
|
||||||
miterNy[n - 1] = segNy[n - 2];
|
|
||||||
// Interior points: average
|
|
||||||
for (let i = 1; i < n - 1; i++) {
|
|
||||||
let mx = segNx[i - 1] + segNx[i];
|
|
||||||
let my = segNy[i - 1] + segNy[i];
|
|
||||||
const ml = Math.sqrt(mx * mx + my * my);
|
|
||||||
if (ml < 0.001) {
|
|
||||||
mx = segNx[i];
|
|
||||||
my = segNy[i];
|
|
||||||
} else {
|
|
||||||
mx /= ml;
|
|
||||||
my /= ml;
|
|
||||||
}
|
|
||||||
miterNx[i] = mx;
|
|
||||||
miterNy[i] = my;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build quads using miter normals
|
|
||||||
for (let i = 0; i < n - 1; i++) {
|
for (let i = 0; i < n - 1; i++) {
|
||||||
const p1 = points[i];
|
const p1 = points[i];
|
||||||
const p2 = points[i + 1];
|
const p2 = points[i + 1];
|
||||||
|
|
||||||
const [x1a, y1a] = toClip(p1.x - miterNx[i] * thickness, p1.y - miterNy[i] * thickness);
|
// Calculate segment normal
|
||||||
const [x1b, y1b] = toClip(p1.x + miterNx[i] * thickness, p1.y + miterNy[i] * thickness);
|
let dx = p2.x - p1.x;
|
||||||
const [x2a, y2a] = toClip(p2.x - miterNx[i + 1] * thickness, p2.y - miterNy[i + 1] * thickness);
|
let dy = p2.y - p1.y;
|
||||||
const [x2b, y2b] = toClip(p2.x + miterNx[i + 1] * thickness, p2.y + miterNy[i + 1] * thickness);
|
let len = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
let nx, ny;
|
||||||
|
|
||||||
// Each vertex: [x, y, edge] where edge = -1 (bottom) or +1 (top)
|
if (len < 0.001) { nx = 0; ny = -1; }
|
||||||
vertices.push(x1a, y1a, -1.0, x1b, y1b, 1.0, x2a, y2a, -1.0);
|
else { nx = -dy / len; ny = dx / len; }
|
||||||
vertices.push(x1b, y1b, 1.0, x2b, y2b, 1.0, x2a, y2a, -1.0);
|
|
||||||
|
// Previous normal (for miter)
|
||||||
|
let prevNx = nx, prevNy = ny;
|
||||||
|
if (i > 0) {
|
||||||
|
const p0 = points[i - 1];
|
||||||
|
const dx0 = p1.x - p0.x;
|
||||||
|
const dy0 = p1.y - p0.y;
|
||||||
|
const len0 = Math.sqrt(dx0 * dx0 + dy0 * dy0);
|
||||||
|
if (len0 >= 0.001) {
|
||||||
|
prevNx = -dy0 / len0;
|
||||||
|
prevNy = dx0 / len0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miter at P1
|
||||||
|
let m1x = nx + prevNx;
|
||||||
|
let m1y = ny + prevNy;
|
||||||
|
let m1l = Math.sqrt(m1x * m1x + m1y * m1y);
|
||||||
|
if (m1l > 0.001) { m1x /= m1l; m1y /= m1l; }
|
||||||
|
|
||||||
|
// Next normal (for P2 miter)
|
||||||
|
let nextNx = nx, nextNy = ny;
|
||||||
|
if (i < n - 2) {
|
||||||
|
const p3 = points[i + 2];
|
||||||
|
const dx2 = p3.x - p2.x;
|
||||||
|
const dy2 = p3.y - p2.y;
|
||||||
|
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||||
|
if (len2 >= 0.001) {
|
||||||
|
nextNx = -dy2 / len2;
|
||||||
|
nextNy = dx2 / len2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miter at P2
|
||||||
|
let m2x = nx + nextNx;
|
||||||
|
let m2y = ny + nextNy;
|
||||||
|
let m2l = Math.sqrt(m2x * m2x + m2y * m2y);
|
||||||
|
if (m2l > 0.001) { m2x /= m2l; m2y /= m2l; }
|
||||||
|
|
||||||
|
// Generate vertices
|
||||||
|
// P1 Top
|
||||||
|
const x1a = (p1.x - m1x * thickness) * wInv - 1;
|
||||||
|
const y1a = 1 - (p1.y - m1y * thickness) * hInv;
|
||||||
|
|
||||||
|
// P1 Bottom
|
||||||
|
const x1b = (p1.x + m1x * thickness) * wInv - 1;
|
||||||
|
const y1b = 1 - (p1.y + m1y * thickness) * hInv;
|
||||||
|
|
||||||
|
// P2 Top
|
||||||
|
const x2a = (p2.x - m2x * thickness) * wInv - 1;
|
||||||
|
const y2a = 1 - (p2.y - m2y * thickness) * hInv;
|
||||||
|
|
||||||
|
// P2 Bottom
|
||||||
|
const x2b = (p2.x + m2x * thickness) * wInv - 1;
|
||||||
|
const y2b = 1 - (p2.y + m2y * thickness) * hInv;
|
||||||
|
|
||||||
|
// Triangle 1
|
||||||
|
outBuffer[ptr++] = x1a; outBuffer[ptr++] = y1a; outBuffer[ptr++] = -1.0;
|
||||||
|
outBuffer[ptr++] = x1b; outBuffer[ptr++] = y1b; outBuffer[ptr++] = 1.0;
|
||||||
|
outBuffer[ptr++] = x2a; outBuffer[ptr++] = y2a; outBuffer[ptr++] = -1.0;
|
||||||
|
|
||||||
|
// Triangle 2
|
||||||
|
outBuffer[ptr++] = x1b; outBuffer[ptr++] = y1b; outBuffer[ptr++] = 1.0;
|
||||||
|
outBuffer[ptr++] = x2b; outBuffer[ptr++] = y2b; outBuffer[ptr++] = 1.0;
|
||||||
|
outBuffer[ptr++] = x2a; outBuffer[ptr++] = y2a; outBuffer[ptr++] = -1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Float32Array(vertices);
|
return ptr - offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx, canvas, analyser, dataArray, params) {
|
draw(ctx, canvas, analyser, dataArray, params) {
|
||||||
|
|
@ -481,84 +514,75 @@ export class UnknownPleasuresWebGL {
|
||||||
const { width, height } = canvas;
|
const { width, height } = canvas;
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') !== 'white';
|
const isDark = document.documentElement.getAttribute('data-theme') !== 'white';
|
||||||
|
|
||||||
// FORCE Normal blending as requested - no more screen blend tricks
|
|
||||||
canvas.style.mixBlendMode = 'normal';
|
canvas.style.mixBlendMode = 'normal';
|
||||||
|
|
||||||
// Initialize WebGL on first draw
|
|
||||||
if (!this.lineProgram) {
|
if (!this.lineProgram) {
|
||||||
this._initGL(gl, width, height);
|
this._initGL(gl, width, height);
|
||||||
if (!this.lineProgram) {
|
|
||||||
console.error('WebGL init failed');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset if needed
|
|
||||||
if (this.history.length === 0) {
|
if (this.history.length === 0) {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update history with propagation speed control
|
if (!params.paused) {
|
||||||
// Higher PROPAGATION_SPEED = faster wave propagation
|
this._propagationAccum += UnknownPleasuresWebGL.PROPAGATION_SPEED;
|
||||||
this._propagationAccum += UnknownPleasuresWebGL.PROPAGATION_SPEED;
|
const pts = this.dataPoints;
|
||||||
const pts = this.dataPoints;
|
|
||||||
|
|
||||||
if (this._propagationAccum >= 1.0) {
|
if (this._propagationAccum >= 1.0) {
|
||||||
this._propagationAccum -= 1.0;
|
this._propagationAccum -= 1.0;
|
||||||
|
|
||||||
const sampleRate = analyser.context.sampleRate;
|
const sampleRate = analyser.context.sampleRate;
|
||||||
const nyquist = sampleRate / 2;
|
const nyquist = sampleRate / 2;
|
||||||
const targetFreq = 22000; // Visualizing up to 22kHz
|
const targetFreq = 22000;
|
||||||
const scale = Math.min(1.0, targetFreq / nyquist);
|
const scale = Math.min(1.0, targetFreq / nyquist);
|
||||||
const len = Math.floor(dataArray.length * scale);
|
const len = Math.floor(dataArray.length * scale);
|
||||||
|
|
||||||
const line = this.history[this.writeIndex];
|
const line = this.history[this.writeIndex];
|
||||||
if (line) {
|
if (line) {
|
||||||
for (let i = 0; i < pts; i++) {
|
for (let i = 0; i < pts; i++) {
|
||||||
line[i] = (dataArray[(this.xLookup[i] * len) | 0] / 255) * this.pLookup[i];
|
line[i] = (dataArray[(this.xLookup[i] * len) | 0] / 255) * this.pLookup[i];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.writeIndex = (this.writeIndex + 1) % this.historySize;
|
||||||
}
|
}
|
||||||
this.writeIndex = (this.writeIndex + 1) % this.historySize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update palette if color changed
|
|
||||||
if (this._paletteColor !== params.primaryColor) {
|
if (this._paletteColor !== params.primaryColor) {
|
||||||
this._buildPalette(params.primaryColor);
|
this._buildPalette(params.primaryColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute size for rotated bounding box
|
|
||||||
const rotatedW = Math.abs(width * this._cos) + Math.abs(height * this._sin);
|
|
||||||
const rotatedH = Math.abs(width * this._sin) + Math.abs(height * this._cos);
|
|
||||||
const size = Math.max(rotatedW, rotatedH) * 1.15;
|
|
||||||
|
|
||||||
// === PASS 1: Scene ===
|
// === PASS 1: Scene ===
|
||||||
// We render lines to a transparent texture so we can composite them properly later
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
||||||
gl.viewport(0, 0, width, height);
|
gl.viewport(0, 0, width, height);
|
||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
// Perspective constants - extended for better corner coverage
|
// Constants
|
||||||
const horizonY = size * 0.05; // Further back (was 0.1)
|
const size = Math.max(Math.abs(width * this._cos) + Math.abs(height * this._sin), Math.abs(width * this._sin) + Math.abs(height * this._cos)) * 1.15;
|
||||||
const frontY = size * 0.9; // Closer to edge (was 0.8)
|
const horizonY = size * 0.05;
|
||||||
|
const frontY = size * 0.9;
|
||||||
const depth = 2.0;
|
const depth = 2.0;
|
||||||
const totalH = frontY - horizonY;
|
const totalH = frontY - horizonY;
|
||||||
const B = totalH / (1 - 1 / (1 + depth));
|
const B = totalH / (1 - 1 / (1 + depth));
|
||||||
const A = frontY - B;
|
const A = frontY - B;
|
||||||
|
|
||||||
// Lines output premultiplied alpha (color * aa, aa).
|
// --- BATCH GEOMETRY GENERATION ---
|
||||||
gl.enable(gl.BLEND);
|
// Fill the vertex buffer with ALL lines for this frame
|
||||||
if (isDark) {
|
let bufferOffset = 0;
|
||||||
// Additive premultiplied
|
// Store draw commands to execute later: { start, count, colorIndex }
|
||||||
gl.blendFunc(gl.ONE, gl.ONE);
|
const drawCommands = [];
|
||||||
} else {
|
|
||||||
// Standard premultiplied
|
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
gl.useProgram(this.lineProgram);
|
// Reuse temporary points array
|
||||||
|
if (!this._tempPoints) this._tempPoints = [];
|
||||||
|
const points = this._tempPoints;
|
||||||
|
const pts = this.dataPoints;
|
||||||
|
const cx = width / 2;
|
||||||
|
const cy = height / 2;
|
||||||
|
const cosR = this._cos;
|
||||||
|
const sinR = this._sin;
|
||||||
|
const offsetX = -size / 2;
|
||||||
|
const offsetY = -size / 2;
|
||||||
|
|
||||||
// Draw each line (back to front)
|
|
||||||
for (let i = this.historySize - 1; i >= 0; i--) {
|
for (let i = this.historySize - 1; i >= 0; i--) {
|
||||||
const idx = (this.writeIndex + i) % this.historySize;
|
const idx = (this.writeIndex + i) % this.historySize;
|
||||||
const historyLine = this.history[idx];
|
const historyLine = this.history[idx];
|
||||||
|
|
@ -573,63 +597,66 @@ export class UnknownPleasuresWebGL {
|
||||||
const amp = 200 * scale;
|
const amp = 200 * scale;
|
||||||
const lineWidth = Math.max(1, 8 * scale + params.kick * 3);
|
const lineWidth = Math.max(1, 8 * scale + params.kick * 3);
|
||||||
|
|
||||||
// Generate line points (in rotated space, then transform to screen)
|
// Generate points
|
||||||
const points = [];
|
points.length = 0;
|
||||||
const cx = width / 2;
|
|
||||||
const cy = height / 2;
|
|
||||||
const cosR = this._cos;
|
|
||||||
const sinR = this._sin;
|
|
||||||
const offsetX = -size / 2;
|
|
||||||
const offsetY = -size / 2;
|
|
||||||
|
|
||||||
for (let j = 0; j < pts; j++) {
|
for (let j = 0; j < pts; j++) {
|
||||||
// Position in rotated coordinate system
|
|
||||||
const rx = margin + this.xLookup[j] * lw;
|
const rx = margin + this.xLookup[j] * lw;
|
||||||
const ry = y - historyLine[j] * amp;
|
const ry = y - historyLine[j] * amp;
|
||||||
|
|
||||||
// Apply rotation and translate to screen
|
|
||||||
const dx = rx + offsetX;
|
const dx = rx + offsetX;
|
||||||
const dy = ry + offsetY;
|
const dy = ry + offsetY;
|
||||||
const screenX = dx * cosR - dy * sinR + cx;
|
points.push({ x: dx * cosR - dy * sinR + cx, y: dx * sinR + dy * cosR + cy });
|
||||||
const screenY = dx * sinR + dy * cosR + cy;
|
|
||||||
|
|
||||||
points.push({ x: screenX, y: screenY });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate quad geometry for thick line
|
// Write to buffer
|
||||||
const vertices = this._generateLineQuads(points, lineWidth / 2, width, height);
|
const vertexCount = this._generateLineQuads(points, lineWidth / 2, width, height, this.vertexBuffer, bufferOffset);
|
||||||
if (vertices.length === 0) continue;
|
|
||||||
|
|
||||||
// Upload vertices
|
if (vertexCount > 0) {
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.lineBuffer);
|
drawCommands.push({
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.DYNAMIC_DRAW);
|
start: bufferOffset / 3, // Start vertex index
|
||||||
|
count: vertexCount / 3, // Number of vertices
|
||||||
gl.enableVertexAttribArray(this.line_a_posEdge);
|
colorIndex: i
|
||||||
gl.vertexAttribPointer(this.line_a_posEdge, 3, gl.FLOAT, false, 0, 0);
|
});
|
||||||
|
bufferOffset += vertexCount; // Advance by number of floats
|
||||||
// Set raw palette color
|
}
|
||||||
const color = this._paletteRGB[i] || [1, 1, 1];
|
}
|
||||||
gl.uniform3f(this.line_u_color, color[0], color[1], color[2]);
|
|
||||||
|
// --- UPLOAD ONCE ---
|
||||||
// Draw (vertices.length / 3 because each vertex is [x, y, edge])
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.lineBuffer);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 3);
|
// Upload only the used portion of the pre-allocated buffer
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.vertexBuffer.subarray(0, bufferOffset), gl.DYNAMIC_DRAW);
|
||||||
|
gl.enableVertexAttribArray(this.line_a_posEdge);
|
||||||
|
gl.vertexAttribPointer(this.line_a_posEdge, 3, gl.FLOAT, false, 0, 0);
|
||||||
|
|
||||||
|
// --- DRAW BATCH ---
|
||||||
|
gl.useProgram(this.lineProgram);
|
||||||
|
gl.enable(gl.BLEND);
|
||||||
|
if (isDark) {
|
||||||
|
gl.blendFunc(gl.ONE, gl.ONE);
|
||||||
|
} else {
|
||||||
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cmd of drawCommands) {
|
||||||
|
const color = this._paletteRGB[cmd.colorIndex] || [1, 1, 1];
|
||||||
|
gl.uniform3f(this.line_u_color, color[0], color[1], color[2]);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, cmd.start, cmd.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MUST DISABLE BLEND for post-processing passes so we strictly overwrite FBO contents!
|
|
||||||
gl.disable(gl.BLEND);
|
gl.disable(gl.BLEND);
|
||||||
|
|
||||||
// === PASS 2: Bloom ===
|
// === PASS 2: Bloom (Half Res) ===
|
||||||
|
const blurW = Math.max(1, width >> 1);
|
||||||
|
const blurH = Math.max(1, height >> 1);
|
||||||
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFramebuffer);
|
||||||
gl.viewport(0, 0, width, height);
|
gl.viewport(0, 0, blurW, blurH);
|
||||||
gl.clearColor(0, 0, 0, 0);
|
gl.clearColor(0, 0, 0, 0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
gl.useProgram(this.brightnessProgram);
|
gl.useProgram(this.brightnessProgram);
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
||||||
gl.uniform1i(this.brightness_u_texture, 0);
|
gl.uniform1i(this.brightness_u_texture, 0);
|
||||||
// NO THRESHOLD - EVERYTHING GLOWS (Thread Ripper Style)
|
|
||||||
gl.uniform1f(this.brightness_u_threshold, 0.0);
|
gl.uniform1f(this.brightness_u_threshold, 0.0);
|
||||||
gl.uniform1f(this.brightness_u_isDarkTheme, isDark ? 1.0 : 0.0);
|
gl.uniform1f(this.brightness_u_isDarkTheme, isDark ? 1.0 : 0.0);
|
||||||
|
|
||||||
|
|
@ -641,88 +668,57 @@ export class UnknownPleasuresWebGL {
|
||||||
// === PASS 3: Gaussian Blur (Ping Pong) ===
|
// === PASS 3: Gaussian Blur (Ping Pong) ===
|
||||||
gl.useProgram(this.blurProgram);
|
gl.useProgram(this.blurProgram);
|
||||||
|
|
||||||
// More iterations for wider, smoother glow (Thread Ripper uses 8 * 2 passes)
|
const iterations = 4;
|
||||||
// We have 2 framebuffers: blurFramebuffer (holds brightness extract), blurFinalFramebuffer (temp)
|
|
||||||
// thread_ripper uses ping-pong. Let's adapt.
|
|
||||||
|
|
||||||
// We start with 'blurFramebuffer' having the bright pixels.
|
|
||||||
// We want to ping-pong between blurFramebuffer and blurFinalFramebuffer.
|
|
||||||
|
|
||||||
const iterations = 8;
|
|
||||||
let horizontal = true;
|
let horizontal = true;
|
||||||
|
|
||||||
for (let i = 0; i < iterations * 2; i++) {
|
for (let i = 0; i < iterations * 2; i++) {
|
||||||
// Thread Ripper ping-pong: horizontal toggles each iteration
|
|
||||||
const destFBO = horizontal ? this.blurFinalFramebuffer : this.blurFramebuffer;
|
const destFBO = horizontal ? this.blurFinalFramebuffer : this.blurFramebuffer;
|
||||||
const srcTex = horizontal ? this.blurTexture : this.blurFinalTexture;
|
const srcTex = horizontal ? this.blurTexture : this.blurFinalTexture;
|
||||||
|
const spread = 1.0 + i * 0.75;
|
||||||
// Thread Ripper spread: grows linearly with i (not i/2)
|
|
||||||
// Increased by 50% from 0.375 to 0.5625 for wider glow
|
|
||||||
const spread = 1.0 + i * 0.5625;
|
|
||||||
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, destFBO);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, destFBO);
|
||||||
gl.viewport(0, 0, width, height);
|
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, srcTex);
|
gl.bindTexture(gl.TEXTURE_2D, srcTex);
|
||||||
gl.uniform1i(this.blur_u_texture, 0);
|
gl.uniform1i(this.blur_u_texture, 0);
|
||||||
gl.uniform2f(this.blur_u_resolution, width, height);
|
gl.uniform2f(this.blur_u_resolution, blurW, blurH);
|
||||||
|
|
||||||
gl.uniform2f(this.blur_u_direction, horizontal ? 1.0 : 0.0, horizontal ? 0.0 : 1.0);
|
gl.uniform2f(this.blur_u_direction, horizontal ? 1.0 : 0.0, horizontal ? 0.0 : 1.0);
|
||||||
gl.uniform1f(this.blur_u_spread, spread);
|
gl.uniform1f(this.blur_u_spread, spread);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
|
|
||||||
gl.enableVertexAttribArray(this.blur_a_position);
|
|
||||||
gl.vertexAttribPointer(this.blur_a_position, 2, gl.FLOAT, false, 0, 0);
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
horizontal = !horizontal;
|
horizontal = !horizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final result is in the LAST written framebuffer.
|
|
||||||
// iter 0 -> writes Final
|
|
||||||
// iter 1 -> writes Blur
|
|
||||||
// ...
|
|
||||||
// iter 15 -> writes Blur
|
|
||||||
|
|
||||||
// So 'blurTexture' holds the final blurred result.
|
|
||||||
|
|
||||||
// === PASS 4: Composite ===
|
// === PASS 4: Composite ===
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
gl.viewport(0, 0, width, height);
|
gl.viewport(0, 0, width, height);
|
||||||
|
|
||||||
// Clear color for MAIN canvas
|
|
||||||
if (params.mode !== 'blended') {
|
if (params.mode !== 'blended') {
|
||||||
const bg = isDark ? [0.02, 0.02, 0.02, 1] : [0.9, 0.9, 0.9, 1];
|
const bg = isDark ? [0.02, 0.02, 0.02, 1] : [0.9, 0.9, 0.9, 1];
|
||||||
gl.clearColor(bg[0], bg[1], bg[2], bg[3]);
|
gl.clearColor(bg[0], bg[1], bg[2], bg[3]);
|
||||||
} else if (isDark) {
|
} else if (isDark) {
|
||||||
gl.clearColor(0, 0, 0, 0.4); // Dark blended
|
gl.clearColor(0, 0, 0, 0.4);
|
||||||
} else {
|
} else {
|
||||||
gl.clearColor(0.95, 0.95, 0.95, 0.4); // Light frosted
|
gl.clearColor(0.95, 0.95, 0.95, 0.4);
|
||||||
}
|
}
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
// Classic normal blending for the final composite quad over the canvas background!
|
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
gl.useProgram(this.compositeProgram);
|
gl.useProgram(this.compositeProgram);
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
gl.activeTexture(gl.TEXTURE0);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
gl.bindTexture(gl.TEXTURE_2D, this.sceneTexture);
|
||||||
gl.uniform1i(this.composite_u_scene, 0);
|
gl.uniform1i(this.composite_u_scene, 0);
|
||||||
|
|
||||||
gl.activeTexture(gl.TEXTURE1);
|
gl.activeTexture(gl.TEXTURE1);
|
||||||
// Use last output: horizontal toggles, so pick the right texture (Thread Ripper pattern)
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, horizontal ? this.blurTexture : this.blurFinalTexture);
|
gl.bindTexture(gl.TEXTURE_2D, horizontal ? this.blurTexture : this.blurFinalTexture);
|
||||||
gl.uniform1i(this.composite_u_blur, 1);
|
gl.uniform1i(this.composite_u_blur, 1);
|
||||||
|
|
||||||
// Glow strength - EXACT Thread Ripper formula
|
const glowBoost = 1.0 + params.kick;
|
||||||
const glowBoost = 1.0 + params.kick; // Pulse with kick
|
gl.uniform1f(this.composite_u_glowStrength, UnknownPleasuresWebGL.GLOW_INTENSITY * glowBoost);
|
||||||
const glowStrength = UnknownPleasuresWebGL.GLOW_INTENSITY * glowBoost;
|
|
||||||
|
|
||||||
gl.uniform1f(this.composite_u_glowStrength, glowStrength);
|
|
||||||
gl.uniform1f(this.composite_u_noiseStrength, UnknownPleasuresWebGL.NOISE_STRENGTH);
|
gl.uniform1f(this.composite_u_noiseStrength, UnknownPleasuresWebGL.NOISE_STRENGTH);
|
||||||
gl.uniform1f(this.composite_u_isDarkTheme, isDark ? 1.0 : 0.0);
|
gl.uniform1f(this.composite_u_isDarkTheme, isDark ? 1.0 : 0.0);
|
||||||
|
gl.uniform1f(this.composite_u_time, performance.now() / 1000.0);
|
||||||
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
|
||||||
gl.enableVertexAttribArray(this.composite_a_position);
|
gl.enableVertexAttribArray(this.composite_a_position);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue