Saltar al contenido

Borrado completo frente a borrado parcial: las estelas

Elegir cómo limpiar el fotograma anterior, hacer estelas con un rectángulo translúcido, y entender por qué nunca desaparecen del todo y cómo evitarlo.

Borrar entero

Hay tres formas de dejar el canvas limpio y no son equivalentes.

JS
ctx.clearRect(0, 0, ancho, alto);        // deja negro transparentectx.fillStyle = '#1e1e2e';ctx.fillRect(0, 0, ancho, alto);         // deja un color opacocanvas.width = canvas.width;             // reinicia el bufer y TODO el estado

clearRect es la respuesta por defecto. Deja los píxeles en negro transparente, con lo que se ve lo que haya detrás del canvas en la página, y respeta la matriz vigente: si tienes puesta la transformación de densidad, las coordenadas que le pasas son lógicas.

fillRect con un color opaco es lo correcto cuando el canvas tiene fondo propio, y tiene una ventaja de rendimiento real: pintar opaco permite al motor descartar todo lo que hubiera debajo, mientras que borrar a transparente obliga a mantener la información de alfa. En un canvas creado con alpha: false es además lo único que tiene sentido, porque ahí no existe la transparencia.

Reasignar canvas.width libera y recrea el búfer. Borra también toda la pila de estado, la matriz, los estilos y el recorte. Es la opción correcta para liberar memoria de un canvas que ya no se usa, y una elección pésima como borrado por fotograma: asignar el ancho fuerza una reasignación completa del búfer en cada llamada.

Un detalle que sorprende: clearRect respeta la región de recorte pero ignora el operador de composición. Siempre borra. Si has dejado puesto un globalCompositeOperation exótico y esperas que el borrado se comporte de otra manera, no lo hará; el operador afecta a lo que pintas, no a lo que borras.

La estela por atenuación

En vez de borrar, se pinta encima un rectángulo casi transparente del color del fondo. Lo que había queda un poco más tenue en cada fotograma, y el resultado es una estela que se desvanece sin que hayas guardado ni una posición anterior.

HTML
<canvas id="c" style="width:640px;height:400px;display:block;background:#1e1e2e"></canvas><script type="module">const canvas = document.getElementById('c');const ctx = canvas.getContext('2d', { alpha: false });const dpr = window.devicePixelRatio || 1;let ancho = 640, alto = 400;function dimensionar() {  const caja = canvas.getBoundingClientRect();  ancho = caja.width; alto = caja.height;  canvas.width = Math.round(ancho * dpr);  canvas.height = Math.round(alto * dpr);  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);  ctx.fillStyle = '#1e1e2e';  ctx.fillRect(0, 0, ancho, alto);}const puntos = Array.from({ length: 60 }, (_, i) => ({  a: (i / 60) * Math.PI * 2,  v: 0.4 + (i % 7) * 0.06,  r: 60 + (i % 5) * 28,}));let anterior = null;function marco(ahora) {  if (anterior === null) anterior = ahora;  const dt = Math.min(50, ahora - anterior) / 1000;  anterior = ahora;  // Borrado parcial: la estela  ctx.fillStyle = 'rgba(30, 30, 46, 0.10)';  ctx.fillRect(0, 0, ancho, alto);  ctx.fillStyle = '#89b4fa';  for (const p of puntos) {    p.a += p.v * dt;    const x = ancho / 2 + Math.cos(p.a) * p.r;    const y = alto / 2 + Math.sin(p.a * 1.3) * p.r * 0.6;    ctx.beginPath();    ctx.arc(x, y, 3, 0, Math.PI * 2);    ctx.fill();  }  requestAnimationFrame(marco);}new ResizeObserver(dimensionar).observe(canvas);dimensionar();requestAnimationFrame(marco);</script>

El único parámetro es el alfa del rectángulo, y controla la longitud de la estela de forma inversa: 0.02 deja rastros larguísimos, 0.30 apenas un desenfoque de movimiento. Y como es un alfa por fotograma, la longitud de la estela depende de la frecuencia de la pantalla: la misma cifra da estelas más cortas en un monitor de 120 hercios. Corregirlo es la misma potencia de la lección anterior:

JS
const RESTANTE_POR_SEGUNDO = 0.002;      // cuanto queda de un rastro tras un segundoconst alfa = 1 - Math.pow(RESTANTE_POR_SEGUNDO, dt);ctx.fillStyle = `rgba(30, 30, 46, ${alfa})`;

Las cuatro técnicas, comparadas

Técnica Coste Se puede redibujar Residuo
Rectángulo translúcido sobre canvas opaco Un relleno de pantalla completa No Invisible
Rectángulo translúcido sobre canvas con alfa Un relleno de pantalla completa No Visible con el tiempo
destination-out translúcido Un relleno de pantalla completa No Leve pero presente
Historia de posiciones dibujada Proporcional al largo de la estela Sí Ninguno

La estela geométrica merece verse escrita, porque es más corta de lo que la gente imagina:

JS
const HISTORIA = 40;function actualizar(m, dt) {  for (const p of m.puntos) {    p.a += p.v * dt;    p.rastro.push({ x: p.x, y: p.y });    if (p.rastro.length > HISTORIA) p.rastro.shift();  }}function dibujarRastro(ctx, p) {  const n = p.rastro.length;  ctx.lineCap = 'round';  for (let i = 1; i < n; i++) {    const t = i / n;                            // 0 el mas viejo, 1 el mas nuevo    ctx.globalAlpha = t * t;                    // cuadratico: se apaga antes    ctx.lineWidth = 1 + t * 3;    ctx.strokeStyle = p.color;    ctx.beginPath();    ctx.moveTo(p.rastro[i - 1].x, p.rastro[i - 1].y);    ctx.lineTo(p.rastro[i].x, p.rastro[i].y);    ctx.stroke();  }  ctx.globalAlpha = 1;}

Un shift por fotograma sobre un array es más caro de lo que parece cuando hay miles de partículas; la versión de producción usa un búfer circular con un índice, que evita el desplazamiento de memoria.

Lo que la estela te quita

Merece la pena decirlo explícitamente porque condiciona todo el nivel siguiente: una estela por atenuación obliga a tocar todos los píxeles del canvas en cada fotograma. Eso elimina de raíz la posibilidad de redibujar solo la región que ha cambiado, que es la optimización de mayor retorno del canvas y el asunto de la región sucia.

En un canvas pequeño no importa. En uno de pantalla completa a densidad dos, un relleno translúcido de pantalla completa son varios millones de píxeles mezclados por fotograma, y ese solo relleno puede consumir la mitad del presupuesto. La forma de comprobarlo es directa: quita la estela, mide, y decide si el efecto vale su precio.