La regla de relleno: nonzero frente a evenodd
Entender los dos algoritmos que deciden qué es interior en una forma con contornos cruzados, y usar el sentido del recorrido como herramienta de diseño.
Los dos algoritmos
Ambos parten de la misma idea: para saber si un punto está dentro, se lanza un rayo desde ese punto hacia el infinito en cualquier dirección y se examinan los cruces con el contorno.
evenodd cuenta los cruces. Si el número es impar, el punto está dentro; si es par, fuera. Es el algoritmo más simple y no le importa en qué dirección se cruza el contorno.
nonzero lleva un contador con signo. Empieza en cero; cada cruce de un contorno que va en un sentido suma uno, y cada cruce en el sentido contrario resta uno. Si el total es distinto de cero, el punto está dentro. Es el algoritmo por defecto del canvas y de SVG.
La consecuencia es que nonzero es sensible a la orientación de los contornos y evenodd no.
ctx.fill(); // nonzero, por defectoctx.fill('evenodd');ctx.fill(ruta, 'evenodd');ctx.clip('evenodd');ctx.isPointInPath(x, y, 'evenodd');El anillo, caso por caso
Dos círculos concéntricos, el exterior de radio 80 y el interior de radio 40. Un punto en la zona interior cruza dos contornos hacia fuera.
Con evenodd: dos cruces, número par, el punto está fuera. Hay agujero, siempre, independientemente del sentido.
Con nonzero depende de los sentidos. Si ambos círculos van en el mismo sentido, los dos cruces suman en la misma dirección y el contador vale 2, distinto de cero: no hay agujero. Si van en sentidos opuestos, uno suma y otro resta, el contador vale 0: sí hay agujero.
// A: mismo sentido + nonzero -> disco llenoctx.beginPath();ctx.arc(90, 100, 70, 0, Math.PI * 2, false);ctx.moveTo(130, 100);ctx.arc(90, 100, 35, 0, Math.PI * 2, false);ctx.fillStyle = '#89b4fa'; ctx.fill();// B: sentidos opuestos + nonzero -> anilloctx.beginPath();ctx.arc(260, 100, 70, 0, Math.PI * 2, false);ctx.moveTo(295, 100);ctx.arc(260, 100, 35, 0, Math.PI * 2, true); // invertidoctx.fillStyle = '#a6e3a1'; ctx.fill();// C: mismo sentido + evenodd -> anillo tambienctx.beginPath();ctx.arc(430, 100, 70, 0, Math.PI * 2, false);ctx.moveTo(465, 100);ctx.arc(430, 100, 35, 0, Math.PI * 2, false);ctx.fillStyle = '#f9e2af'; ctx.fill('evenodd');Las tres formas tienen exactamente la misma geometría y producen tres resultados distintos. Eso es todo lo que hay que entender de las reglas de relleno.
La estrella de cinco puntas
El ejemplo canónico del cruce consigo misma es el pentagrama trazado de un tirón, saltando vértices.
function pentagrama(ctx, cx, cy, r) { ctx.beginPath(); for (let i = 0; i < 5; i++) { const a = -Math.PI / 2 + (i * 4 * Math.PI) / 5; // saltar de 2 en 2 const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); } ctx.closePath();}pentagrama(ctx, 100, 110, 80);ctx.fillStyle = '#cba6f7'; ctx.fill(); // nonzero: estrella macizapentagrama(ctx, 300, 110, 80);ctx.fillStyle = '#f38ba8'; ctx.fill('evenodd'); // evenodd: pentagono huecoCon nonzero el pentágono central queda relleno porque todos los cruces van en el mismo sentido y el contador vale 2. Con evenodd queda hueco porque hay dos cruces. Es exactamente la diferencia entre la estrella maciza de una bandera y el pentagrama esotérico.
El sentido como herramienta
Una vez entendido, el sentido del recorrido se convierte en un mecanismo de diseño. La técnica se llama coloquialmente regla del contorno inverso y es la que usan los formatos de fuentes: los contornos exteriores de un glifo van en un sentido y los agujeros —el hueco de una o, los dos de una B— van en el contrario.
En el canvas hay tres formas de invertir un contorno:
// 1. arc y ellipse: el ultimo parametroctx.arc(cx, cy, r, 0, Math.PI * 2, true);// 2. rect: dimensiones negativasctx.rect(x + w, y + h, -w, -h);// 3. poligono a mano: recorrer los puntos al revesfor (let i = puntos.length - 1; i >= 0; i--) { /* lineTo */ }Con eso se construye una máscara de cualquier complejidad usando la regla por defecto:
/** Un panel con varios agujeros rectangulares. */function panelPerforado(ctx, x, y, w, h, agujeros) { ctx.beginPath(); ctx.rect(x, y, w, h); // horario for (const a of agujeros) { ctx.rect(a.x + a.w, a.y + a.h, -a.w, -a.h); // antihorario } ctx.fillStyle = 'rgba(17,17,27,0.85)'; ctx.fill();}// Efecto de foco: oscurecer todo menos unas zonaspanelPerforado(ctx, 0, 0, 500, 300, [ { x: 60, y: 60, w: 120, h: 80 }, { x: 260, y: 120, w: 160, h: 100 },]);Ese patrón —oscurecer la pantalla dejando huecos— es la base de los tours guiados y de los efectos de resaltado, y se resuelve sin recortes ni composición: solo con el sentido del recorrido.
Cuál usar
En la práctica el criterio es este.
Usa evenodd cuando la forma venga de datos o de un origen que no controlas, porque no depende del sentido y por tanto es predecible. Es lo que quieres para trazados importados, para geometría generada y para cualquier cosa donde no puedas garantizar la orientación.
Usa nonzero cuando construyas la forma tú y quieras control fino: te permite decidir por contorno si suma o resta, lo que hace posible formas que evenodd no puede expresar, como un contorno que se solapa consigo mismo y sigue siendo macizo.
Y una nota sobre el trazado: stroke no tiene regla de relleno. Traza todos los contornos independientemente de su orientación y de si se cruzan. Un anillo trazado se ve como dos círculos con línea, tanto si el relleno tendría agujero como si no.