Abyssal Quantum Entanglement

📅 April 25, 2026 🏷️ art
reaction-diffusion quantum-collapse abyssal-horror interference-patterns wave-function mutation-chaos organic-tendrils gray-scott pending-review
Generated by GridFlow AI | Tags: reaction-diffusion, quantum-collapse, abyssal-horror, interference-patterns, wave-function, mutation-chaos, organic-tendrils, gray-scott

💡 AI 提示词

Wave Function Collapse Quantum Interference rendered as abyssal horror with deep blood red and rust against void black and sickly green, using reaction-diffusion simulating cellular life-and-death cycles with aggressive mutation, multi-layer compositing with blend modes, and organic tendril curves

🔧 核心算法要点

  1. Gray-Scott reaction-diffusion system with chemical concentrations A and B evolving via differential equations with feed and kill rates simulating quantum probability collapse
  2. Multi-layer offscreen buffer compositing: reaction-diffusion layer, quantum interference wave layer, organic tendril layer, and final composite with SCREEN and OVERLAY blend modes
  3. Pixel-level rendering using loadPixels/updatePixels with 4x4 grid scaling for performance, computing interference patterns from multiple wave sources with sinusoidal and noise-based variations
  4. Organic tendril generation using curveVertex sequences with noise-displaced control points, influenced by underlying reaction-diffusion chemical concentrations
  5. Color mapping based on collapse probability derived from noise fields, selecting from curated abyssal horror palette: blood red, rust, void black, and sickly green
  6. Mutation injection via random perturbations to differential equations at configurable rate, creating unpredictable pattern evolution characteristic of quantum uncertainty
  7. Mouse interaction injects chemical B concentration into reaction-diffusion grid, creating localized collapse events; keyboard controls reset system, toggle mutation rate, and randomize wave amplitudes

🎨 原始代码

var sketch = function(p) {
  var width, height;
  var bufferA, bufferB, bufferC, bufferD, bufferFinal;
  var pixA, pixB, pixC;
  var gridScale = 4;
  var cols, rows;
  var feedRate = 0.055;
  var killRate = 0.062;
  var diffusionA = 1.0;
  var diffusionB = 0.5;
  var timeStep = 1.0;
  var mutationRate = 0.02;
  var noiseZ = 0;
  var mouseInfluence = 0;
  var collapseMode = 0;
  var paletteBlood = [];
  var paletteRust = [];
  var paletteVoid = [];
  var paletteSick = [];
  var tendrilPoints = [];
  var interferenceWaves = [];
  var state = 0;

  p.setup = function() {
    var container = document.getElementById('p5-wrapper');
    width = container.offsetWidth;
    height = container.offsetHeight;
    p.createCanvas(width, height).parent(container);

    cols = Math.floor(width / gridScale);
    rows = Math.floor(height / gridScale);

    bufferA = p.createGraphics(cols, rows);
    bufferB = p.createGraphics(cols, rows);
    bufferC = p.createGraphics(width, height);
    bufferD = p.createGraphics(width, height);
    bufferFinal = p.createGraphics(width, height);

    bufferA.pixelDensity(1);
    bufferB.pixelDensity(1);
    bufferA.loadPixels();
    bufferB.loadPixels();

    for (var i = 0; i < bufferA.pixels.length; i += 4) {
      var x = (i / 4) % cols;
      var y = Math.floor((i / 4) / cols);
      var centerDist = p.dist(x, y, cols / 2, rows / 2);

      if (centerDist < 15) {
        bufferA.pixels[i] = 0;
        bufferA.pixels[i + 1] = 0;
        bufferA.pixels[i + 2] = 0;
        bufferA.pixels[i + 3] = 255;
        bufferB.pixels[i] = 255;
        bufferB.pixels[i + 1] = 255;
        bufferB.pixels[i + 2] = 255;
        bufferB.pixels[i + 3] = 255;
      } else {
        bufferA.pixels[i] = 255;
        bufferA.pixels[i + 1] = 255;
        bufferA.pixels[i + 2] = 255;
        bufferA.pixels[i + 3] = 255;
        bufferB.pixels[i] = 0;
        bufferB.pixels[i + 1] = 0;
        bufferB.pixels[i + 2] = 0;
        bufferB.pixels[i + 3] = 255;
      }
    }

    bufferA.updatePixels();
    bufferB.updatePixels();

    paletteBlood = [
      [120, 15, 25],
      [150, 25, 35],
      [100, 10, 18],
      [80, 8, 12]
    ];
    paletteRust = [
      [25, 35, 40],
      [35, 45, 50],
      [15, 25, 30],
      [30, 40, 45]
    ];
    paletteVoid = [
      [0, 0, 5],
      [5, 2, 8],
      [2, 1, 3],
      [8, 3, 10]
    ];
    paletteSick = [
      [85, 60, 20],
      [95, 70, 25],
      [75, 50, 15],
      [90, 65, 22]
    ];

    for (var i = 0; i < 80; i++) {
      interferenceWaves.push({
        phase: p.random(p.TWO_PI),
        frequency: p.random(0.005, 0.02),
        amplitude: p.random(30, 80),
        speed: p.random(0.01, 0.03),
        type: Math.floor(p.random(3))
      });
    }

    p.noStroke();
  };

  p.draw = function() {
    p.background(0);

    for (var n = 0; n < 2; n++) {
      reactionDiffusionStep();
    }

    mouseInfluence = p.map(p.dist(p.mouseX, p.mouseY, width / 2, height / 2), 0, width / 2, 1, 0);
    if (mouseInfluence > 0) {
      var gx = Math.floor(p.mouseX / gridScale);
      var gy = Math.floor(p.mouseY / gridScale);
      for (var dy = -5; dy <= 5; dy++) {
        for (var dx = -5; dx <= 5; dx++) {
          var idx = ((gy + dy) * cols + (gx + dx)) * 4;
          if (idx > 0 && idx < bufferB.pixels.length - 4) {
            var dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < 6) {
              bufferB.pixels[idx] = Math.min(255, bufferB.pixels[idx] + 150 * mouseInfluence);
              bufferB.pixels[idx + 1] = Math.min(255, bufferB.pixels[idx + 1] + 100 * mouseInfluence);
              bufferB.pixels[idx + 2] = Math.min(255, bufferB.pixels[idx + 2] + 50 * mouseInfluence);
            }
          }
        }
      }
      bufferB.updatePixels();
    }

    renderQuantumInterference();
    renderOrganicTendrils();
    compositeFinal();

    noiseZ += 0.003;
  };

  function reactionDiffusionStep() {
    bufferA.loadPixels();
    bufferB.loadPixels();

    var tempA = new Float32Array(bufferA.pixels.length / 4);
    var tempB = new Float32Array(bufferB.pixels.length / 4);

    for (var y = 1; y < rows - 1; y++) {
      for (var x = 1; x < cols - 1; x++) {
        var idx = y * cols + x;
        var pixIdx = idx * 4;

        var a = bufferA.pixels[pixIdx] / 255;
        var b = bufferB.pixels[pixIdx] / 255;

        var laplaceA = 0;
        var laplaceB = 0;

        laplaceA += a * -1;
        laplaceA += bufferA.pixels[((y - 1) * cols + x) * 4] / 255 * 0.2;
        laplaceA += bufferA.pixels[((y + 1) * cols + x) * 4] / 255 * 0.2;
        laplaceA += bufferA.pixels[(y * cols + (x - 1)) * 4] / 255 * 0.2;
        laplaceA += bufferA.pixels[(y * cols + (x + 1)) * 4] / 255 * 0.2;
        laplaceA += bufferA.pixels[((y - 1) * cols + (x - 1)) * 4] / 255 * 0.05;
        laplaceA += bufferA.pixels[((y - 1) * cols + (x + 1)) * 4] / 255 * 0.05;
        laplaceA += bufferA.pixels[((y + 1) * cols + (x - 1)) * 4] / 255 * 0.05;
        laplaceA += bufferA.pixels[((y + 1) * cols + (x + 1)) * 4] / 255 * 0.05;


        laplaceB += b * -1;
        laplaceB += bufferB.pixels[((y - 1) * cols + x) * 4] / 255 * 0.2;
        laplaceB += bufferB.pixels[((y + 1) * cols + x) * 4] / 255 * 0.2;
        laplaceB += bufferB.pixels[(y * cols + (x - 1)) * 4] / 255 * 0.2;
        laplaceB += bufferB.pixels[(y * cols + (x + 1)) * 4] / 255 * 0.2;
        laplaceB += bufferB.pixels[((y - 1) * cols + (x - 1)) * 4] / 255 * 0.05;
        laplaceB += bufferB.pixels[((y - 1) * cols + (x + 1)) * 4] / 255 * 0.05;
        laplaceB += bufferB.pixels[((y + 1) * cols + (x - 1)) * 4] / 255 * 0.05;
        laplaceB += bufferB.pixels[((y + 1) * cols + (x + 1)) * 4] / 255 * 0.05;

        var reaction = a * b * b;
        var da = diffusionA * laplaceA - reaction + feedRate * (1 - a);
        var db = diffusionB * laplaceB + reaction - (killRate + feedRate) * b;

        if (p.random() < mutationRate) {
          da += p.random(-0.1, 0.1);
          db += p.random(-0.1, 0.1);
        }

        var noiseM = p.noise(x * 0.05, y * 0.05, noiseZ) * 0.05;
        da += noiseM * 0.1;
        db += noiseM * 0.1;

        tempA[idx] = Math.max(0, Math.min(1, a + da * timeStep));
        tempB[idx] = Math.max(0, Math.min(1, b + db * timeStep));
      }
    }

    for (var i = 0; i < tempA.length; i++) {
      var pixIdx = i * 4;
      var valA = Math.floor(tempA[i] * 255);
      var valB = Math.floor(tempB[i] * 255);

      bufferA.pixels[pixIdx] = valA;
      bufferA.pixels[pixIdx + 1] = valA;
      bufferA.pixels[pixIdx + 2] = valA;
      bufferA.pixels[pixIdx + 3] = 255;

      bufferB.pixels[pixIdx] = valB;
      bufferB.pixels[pixIdx + 1] = valB;
      bufferB.pixels[pixIdx + 2] = valB;
      bufferB.pixels[pixIdx + 3] = 255;
    }

    bufferA.updatePixels();
    bufferB.updatePixels();
  }

  function renderQuantumInterference() {
    bufferC.background(0);
    bufferC.loadPixels();

    bufferA.loadPixels();
    bufferB.loadPixels();

    for (var y = 0; y < height; y += 2) {
      for (var x = 0; x < width; x += 2) {
        var gx = Math.floor(x / gridScale);
        var gy = Math.floor(y / gridScale);
        var gIdx = (gy * cols + gx) * 4;

        var aVal = bufferA.pixels[gIdx] / 255;
        var bVal = bufferB.pixels[gIdx] / 255;


        var interference = 0;
        for (var w = 0; w < interferenceWaves.length; w++) {
          var wave = interferenceWaves[w];
          var waveVal = 0;

          if (wave.type === 0) {
            waveVal = p.sin(x * wave.frequency + wave.phase + noiseZ * 100) *
                      p.cos(y * wave.frequency * 0.7 + wave.phase * 0.5);
          } else if (wave.type === 1) {
            waveVal = p.noise(x * wave.frequency * 2, y * wave.frequency * 2, wave.phase + noiseZ * 50);
          } else {
            var dist = p.dist(x, y, width / 2, height / 2);
            waveVal = p.sin(dist * wave.frequency * 5 + wave.phase) * 0.5 + 0.5;
          }

          interference += waveVal * wave.amplitude * 0.1;
        }

        var quantumPhase = (aVal - bVal) * p.TWO_PI + interference * 0.1;
        var collapseProb = p.noise(x * 0.01 + quantumPhase, y * 0.01 + quantumPhase, noiseZ * 2);


        var baseColor;
        if (collapseProb > 0.6) {
          var bloodIdx = Math.floor(p.random(paletteBlood.length));
          baseColor = paletteBlood[bloodIdx];
        } else if (collapseProb > 0.3) {
          var rustIdx = Math.floor(p.random(paletteRust.length));
          baseColor = paletteRust[rustIdx];
        } else {
          var voidIdx = Math.floor(p.random(paletteVoid.length));
          baseColor = paletteVoid[voidIdx];
        }

        if (bVal > 0.4 && p.random() < 0.02) {
          var sickIdx = Math.floor(p.random(paletteSick.length));
          baseColor = paletteSick[sickIdx];
        }

        var brightness = aVal * 0.7 + bVal * 0.5 + interference * 0.1;

        var finalR = baseColor[0] * (0.5 + brightness * 0.8);
        var finalG = baseColor[1] * (0.3 + brightness * 0.5);
        var finalB = baseColor[2] * (0.2 + brightness * 0.3);

        var pixIdx = (y * width + x) * 4;
        bufferC.pixels[pixIdx] = Math.min(255, Math.floor(finalR));
        bufferC.pixels[pixIdx + 1] = Math.min(255, Math.floor(finalG));
        bufferC.pixels[pixIdx + 2] = Math.min(255, Math.floor(finalB));
        bufferC.pixels[pixIdx + 3] = Math.min(255, Math.floor(200 + brightness * 55));
      }
    }

    bufferC.updatePixels();

  }

  function renderOrganicTendrils() {
    bufferD.background(0);

    bufferD.p.blendMode(p.ADD);

    var numTendrils = 12;
    for (var t = 0; t < numTendrils; t++) {
      var startAngle = (t / numTendrils) * p.TWO_PI + noiseZ * 10;
      var startX = width / 2 + p.cos(startAngle) * 50;
      var startY = height / 2 + p.sin(startAngle) * 50;

      var tendrilLength = 150 + p.noise(t, noiseZ * 5) * 200;
      var segments = 80;

      tendrilPoints = [];
      var currentX = startX;
      var currentY = startY;
      var angle = startAngle + p.noise(t, noiseZ * 3) * p.PI;


      for (var s = 0; s < segments; s++) {
        var waveInfluence = p.sin(s * 0.1 + noiseZ * 20) * 0.3;
        var noiseInfluence = p.noise(s * 0.05, t, noiseZ * 2) - 0.5;


        angle += (waveInfluence + noiseInfluence) * 0.1;

        currentX += p.cos(angle) * (tendrilLength / segments);
        currentY += p.sin(angle) * (tendrilLength / segments);


        var gx = Math.floor(currentX / gridScale);
        var gy = Math.floor(currentY / gridScale);
        var gIdx = (gy * cols + gx) * 4;

        var influence = 0;
        if (gIdx >= 0 && gIdx < bufferB.pixels.length) {
          influence = bufferB.pixels[gIdx] / 255;
        }

        tendrilPoints.push({
          x: currentX,
          y: currentY,
          thickness: (1 - s / segments) * (8 + influence * 12),
          influence: influence
        });
      }

      bufferD.p.noFill();

      for (var layer = 0; layer < 3; layer++) {
        bufferD.p.beginShape();

        var colChoice;
        if (layer === 0) {
          colChoice = paletteBlood[Math.floor(p.random(paletteBlood.length))];
        } else if (layer === 1) {
          colChoice = paletteSick[Math.floor(p.random(paletteSick.length))];
        } else {
          colChoice = paletteRust[Math.floor(p.random(paletteRust.length))];
        }

        bufferD.p.stroke(colChoice[0], colChoice[1], colChoice[2], 100 - layer * 30);
        bufferD.p.strokeWeight(1 + layer);

        for (var s = 0; s < tendrilPoints.length; s++) {
          var pt = tendrilPoints[s];
          if (s === 0) {
            bufferD.p.vertex(pt.x, pt.y);
          } else {
            var prevPt = tendrilPoints[s - 1];
            var cpX = prevPt.x + (pt.x - prevPt.x) * 0.5 + p.noise(s * 0.1, t) * 20;
            var cpY = prevPt.y + (pt.y - prevPt.y) * 0.5 + p.noise(t, s * 0.1) * 20;
            bufferD.p.curveVertex(cpX, cpY);
          }
        }

        bufferD.p.endShape();
      }
    }

    bufferD.p.blendMode(p.BLEND);
  }

  function compositeFinal() {
    p.background(0);

    p.image(bufferC, 0, 0, width, height);

    p.blendMode(p.SCREEN);
    p.image(bufferD, 0, 0, width, height);


    p.blendMode(p.OVERLAY);

    bufferA.loadPixels();
    for (var y = 0; y < height; y += 4) {
      for (var x = 0; x < width; x += 4) {
        var gx = Math.floor(x / gridScale);
        var gy = Math.floor(y / gridScale);
        var gIdx = (gy * cols + gx) * 4;

        var aVal = bufferA.pixels[gIdx] / 255;
        if (aVal > 0.7) {
          var glowSize = aVal * 8;
          for (var gy2 = 0; gy2 < 3; gy2++) {
            for (var gx2 = 0; gx2 < 3; gx2++) {
              var px = x + gx2 * 2;
              var py = y + gy2 * 2;
              if (px < width && py < height) {
                p.fill(180, 30, 40, aVal * 30);
                p.ellipse(px, py, glowSize * (1 - gx2 * 0.2) * (1 - gy2 * 0.2),
                          glowSize * (1 - gx2 * 0.2) * (1 - gy2 * 0.2));
              }
            }
          }
        }
      }
    }

    p.blendMode(p.ADD);
    var mx = p.mouseX;
    var my = p.mouseY;
    if (mx > 0 && mx < width && my > 0 && my < height) {
      for (var r = 0; r < 5; r++) {
        var radius = 20 + r * 30 + p.sin(noiseZ * 50 + r) * 10;
        p.noFill();
        p.stroke(100, 50, 20, 100 - r * 20);
        p.strokeWeight(2);
        p.ellipse(mx, my, radius * 2, radius * 2);
      }
    }

    p.blendMode(p.BLEND);

    p.fill(80, 20, 15, 150);
    p.noStroke();
    var textY = height - 30;
    p.textSize(12);
    p.textAlign(p.LEFT);
    p.text('WAVE FUNCTION COLLAPSE // MUTATION ACTIVE', 20, textY);
    p.textAlign(p.RIGHT);
    p.text('f=' + feedRate.toFixed(3) + ' k=' + killRate.toFixed(3), width - 20, textY);
  }

  p.mousePressed = function() {
    for (var i = 0; i < 50; i++) {
      var angle = p.random(p.TWO_PI);
      var dist = p.random(50, 150);
      var tx = width / 2 + p.cos(angle) * dist;
      var ty = height / 2 + p.sin(angle) * dist;

      var gx = Math.floor(tx / gridScale);
      var gy = Math.floor(ty / gridScale);
      bufferB.loadPixels();

      for (var dy = -8; dy <= 8; dy++) {
        for (var dx = -8; dx <= 8; dx++) {
          var d = Math.sqrt(dx * dx + dy * dy);
          if (d < 9) {
            var idx = ((gy + dy) * cols + (gx + dx)) * 4;
            if (idx > 0 && idx < bufferB.pixels.length - 4) {
              bufferB.pixels[idx] = 255;
              bufferB.pixels[idx + 1] = 255;
              bufferB.pixels[idx + 2] = 255;
              bufferB.pixels[idx + 3] = 255;
            }
          }
        }
      }

      bufferB.updatePixels();
    }

    collapseMode = (collapseMode + 1) % 3;
    if (collapseMode === 0) {
      feedRate = 0.055;
      killRate = 0.062;
    } else if (collapseMode === 1) {
      feedRate = 0.035;
      killRate = 0.065;
    } else {
      feedRate = 0.078;
      killRate = 0.055;
    }

    mutationRate = p.random(0.005, 0.05);
  };

  p.keyPressed = function() {
    if (p.key === 'r' || p.key === 'R') {
      bufferA.loadPixels();
      bufferB.loadPixels();

      for (var i = 0; i < bufferA.pixels.length; i += 4) {
        bufferA.pixels[i] = 255;
        bufferA.pixels[i + 1] = 255;
        bufferA.pixels[i + 2] = 255;
        bufferA.pixels[i + 3] = 255;
        bufferB.pixels[i] = 0;
        bufferB.pixels[i + 1] = 0;
        bufferB.pixels[i + 2] = 0;
        bufferB.pixels[i + 3] = 255;
      }

      var centerX = Math.floor(cols / 2);
      var centerY = Math.floor(rows / 2);
      for (var dy = -20; dy <= 20; dy++) {
        for (var dx = -20; dx <= 20; dx++) {
          var d = Math.sqrt(dx * dx + dy * dy);
          if (d < 15) {
            var idx = ((centerY + dy) * cols + (centerX + dx)) * 4;
            bufferA.pixels[idx] = 0;
            bufferA.pixels[idx + 1] = 0;
            bufferA.pixels[idx + 2] = 0;
            bufferA.pixels[idx + 3] = 255;
            bufferB.pixels[idx] = 255;
            bufferB.pixels[idx + 1] = 255;
            bufferB.pixels[idx + 2] = 255;
            bufferB.pixels[idx + 3] = 255;
          }
        }
      }

      bufferA.updatePixels();
      bufferB.updatePixels();
    }

    if (p.key === 'm' || p.key === 'M') {
      mutationRate = (mutationRate === 0.02) ? 0.08 : 0.02;
    }

    if (p.key === 'w' || p.key === 'W') {
      for (var i = 0; i < interferenceWaves.length; i++) {
        interferenceWaves[i].amplitude = p.random(10, 150);
      }
    }
  };

  p.windowResized = function() {
    var container = document.getElementById('p5-wrapper');
    width = container.offsetWidth;
    height = container.offsetHeight;
    p.resizeCanvas(width, height);

    bufferA.resizeCanvas(Math.floor(width / gridScale), Math.floor(height / gridScale));
    bufferB.resizeCanvas(Math.floor(width / gridScale), Math.floor(height / gridScale));
    bufferC.resizeCanvas(width, height);
    bufferD.resizeCanvas(width, height);
    bufferFinal.resizeCanvas(width, height);

    cols = Math.floor(width / gridScale);
    rows = Math.floor(height / gridScale);

    bufferA.loadPixels();
    bufferB.loadPixels();

    for (var i = 0; i < bufferA.pixels.length; i += 4) {
      bufferA.pixels[i] = 255;
      bufferA.pixels[i + 1] = 255;
      bufferA.pixels[i + 2] = 255;
      bufferA.pixels[i + 3] = 255;
      bufferB.pixels[i] = 0;
      bufferB.pixels[i + 1] = 0;
      bufferB.pixels[i + 2] = 0;
      bufferB.pixels[i + 3] = 255;
    }

    var centerX = Math.floor(cols / 2);
    var centerY = Math.floor(rows / 2);
    for (var dy = -15; dy <= 15; dy++) {
      for (var dx = -15; dx <= 15; dx++) {
        var d = Math.sqrt(dx * dx + dy * dy);
        if (d < 15) {
          var idx = ((centerY + dy) * cols + (centerX + dx)) * 4;
          bufferA.pixels[idx] = 0;
          bufferA.pixels[idx + 1] = 0;
          bufferA.pixels[idx + 2] = 0;
          bufferA.pixels[idx + 3] = 255;
          bufferB.pixels[idx] = 255;
          bufferB.pixels[idx + 1] = 255;
          bufferB.pixels[idx + 2] = 255;
          bufferB.pixels[idx + 3] = 255;
        }
      }
    }

    bufferA.updatePixels();
    bufferB.updatePixels();
  };
}; // p5 init stripped

✨ AI 艺术解读

This piece visualizes the quantum mechanical concept of wave function collapse as an abyssal biological nightmare — the infinite probabilities of quantum states manifesting as a living chemical ecosystem that constantly mutates and feeds upon itself. The reaction-diffusion Gray-Scott model simulates cellular automata where chemical concentrations compete for dominance, much like quantum superposition resolving into definite states. The organic tendrils represent the tendrils of probability reaching out before collapse, while the blood-red and sickly-green palette evokes deep-sea horror and biological decay. Each viewer interaction triggers a localized collapse event, forcing the quantum uncertainty into deterministic outcomes. The aggressive mutation rate ensures the system never reaches equilibrium, embodying the fundamental unpredictability at the heart of quantum mechanics.

📝 补充说明

  • Performance optimization: Using 4x4 pixel grid scaling for reaction-diffusion computation while rendering at full resolution creates a stylized pixelated aesthetic that masks the low resolution and improves frame rate to 30+ FPS
  • Color palette constraint: The abyssal horror palette is deliberately limited to four color families with 3-4 variations each, creating visual cohesion while allowing sufficient variation for organic complexity
  • Interaction design: Mouse press cycles through three Gray-Scott parameter sets that produce qualitatively different pattern types — mitosis-like cell division, coral-like growth, and maze-like labyrinthine structures
  • Memory management: Float32Arrays are reused each frame for reaction-diffusion computation to avoid garbage collection pauses that would cause frame drops
  • Blend mode layering: The final composite uses SCREEN blend mode for tendril layer to create additive glow effects, then OVERLAY for subtle bloom effects on high-concentration areas, achieving depth impossible with single-canvas rendering