Lección 9 - Dibujar elipses y rectángulos
En esta lección aprenderás lo siguiente:
- Derivar las representaciones SDF de la elipse y del rectángulo redondeado
- Añadir sombras exteriores e interiores al rectángulo redondeado y a otros SDF
- Determinar si un punto cualquiera está dentro de una elipse o de un rectángulo redondeado
En la Lección 2 usamos los SDF para dibujar círculos, y es fácil extenderlo a la elipse y al rectángulo. Funciones de distancia 2D ofrece más expresiones SDF para gráficos 2D:
float sdf_ellipse(vec2 p, vec2 r) {}
float sdf_rounded_box(vec2 p, vec2 b, vec4 r) {}En el shader, usa la variable shape para distinguir estas tres formas, de modo que podamos dibujarlas con el mismo conjunto de shaders:
if (shape < 0.5) {
outerDistance = sdf_circle(v_FragCoord, 1.0);
innerDistance = sdf_circle(v_FragCoord, r.x);
} else if (shape < 1.5) {
outerDistance = sdf_ellipse(v_FragCoord, vec2(wh, 1.0));
innerDistance = sdf_ellipse(v_FragCoord, r);
} else if (shape < 2.5) {
outerDistance = sdf_rounded_box(v_FragCoord, vec2(wh, 1.0), 0.0);
innerDistance = sdf_rounded_box(v_FragCoord, r, 0.0);
}A continuación, veamos cómo se deriva el SDF.
Rectángulo
El SDF de una caja y Aprovechar Rust y la GPU para renderizar interfaces de usuario a 120 FPS han mostrado el proceso de derivación en forma de vídeos y animaciones.
A partir de la simetría del rectángulo, al colocar su centro en el origen, podemos transformar el problema de la distancia de cualquier punto al borde del rectángulo al primer cuadrante mediante la función abs(), donde p es la coordenada de un punto cualquiera, b es la coordenada de la esquina superior derecha del rectángulo y, como el centro coincide con el origen, es [width / 2, height / 2], y q representa el vector desde el punto hasta la esquina superior derecha:
float sdf_box(vec2 p, vec2 b) {
vec2 q = abs(p) - b;
}
Después considera el caso en el que el punto está fuera del rectángulo. Si se extiende hacia fuera desde la esquina superior derecha del rectángulo, se puede dividir en cuatro cuadrantes. En el primer cuadrante de la figura siguiente, la distancia es la longitud del vector q, length(q). Si el punto cae en el segundo cuadrante, la distancia es q.y porque q.x es negativo y, del mismo modo, si cae en el cuarto cuadrante, la distancia es q.x.

El autor original usó con ingenio length(max(q, 0.0)) para unificar estos tres casos, y max() para eliminar las componentes negativas, reduciendo al máximo el juicio por ramas en el shader. Después considera el caso en el que el punto está dentro del rectángulo, es decir, en el tercer cuadrante de la figura anterior: en ese momento las dos componentes de q son negativas, max(q.x, q.y) puede obtener el valor absoluto de la distancia más cercana, y el min() más externo puede unificar el caso en el que el punto está dentro con los tres anteriores, de nuevo para reducir el juicio por ramas. El SDF completo es el siguiente:
float sdf_box(vec2 p, vec2 b) {
vec2 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
}Hasta aquí podemos dibujar rectángulos y, tomando como referencia SVG <rect>, añadir las propiedades siguientes:
const rect = new Rect({
x,
y,
width,
height,
fill,
});Añadir esquinas redondeadas
La figura siguiente procede de Redondear esquinas en los SDF. Si observamos el campo de distancia visualizado mediante curvas de nivel, podemos ver que el propio rectángulo está redondeado. Si tomamos como ejemplo los puntos cercanos a la esquina superior derecha del rectángulo, no hay un único punto a igual distancia, sino que se distribuyen sobre la circunferencia cuyo centro es esa esquina superior derecha.

De hecho, no solo los rectángulos: todos los gráficos representados mediante SDF se pueden convertir en versiones «redondeadas». La figura siguiente procede de Funciones de distancia 2D:
float opRound( in vec2 p, in float r ) {
return sdShape(p) - r;
}
Así, la representación SDF completa del rectángulo redondeado es la siguiente:
float sdf_rounded_box(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + r;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}Siguiendo la nomenclatura de Figma, usamos cornerRadius, pero el SDF por sí solo no puede lograr el efecto smoothing; consulta Ajustar el radio de esquina y el suavizado y En busca desesperada de los squircles para los detalles. Además, también es posible admitir un radio de esquina distinto en cada esquina; consulta Zed Blade WGSL, un shader que Zed escribió sobre el renderizador blade, con sintaxis WGSL.
$icCanvas2 = call(() => {
return document.createElement('ic-canvas-lesson9');
});call(() => {
const { Canvas, Rect } = Lesson9;
const stats = new Stats();
stats.showPanel(0);
const $stats = stats.dom;
$stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
$icCanvas2.parentElement.style.position = 'relative';
$icCanvas2.parentElement.appendChild($stats);
$icCanvas2.addEventListener('ic-ready', (e) => {
const canvas = e.detail;
for (let i = 0; i < 1000; i++) {
const fill = `rgb(${Math.floor(Math.random() * 255)},${Math.floor(
Math.random() * 255,
)},${Math.floor(Math.random() * 255)})`;
const rect = new Rect({
x: Math.random() * 1000,
y: Math.random() * 1000,
fill,
cornerRadius: 10,
});
// rect.x = Math.random() * 1000;
// rect.y = Math.random() * 1000;
rect.width = Math.random() * 40;
rect.height = Math.random() * 40;
canvas.appendChild(rect);
rect.addEventListener('pointerenter', () => {
rect.fill = 'red';
});
rect.addEventListener('pointerleave', () => {
rect.fill = fill;
});
}
});
$icCanvas2.addEventListener('ic-frame', (e) => {
stats.update();
});
});Añadir sombra exterior
Cuando se habla de sombras, puede que hayas oído hablar de box-shadow y de filter: drop-shadow() en CSS. La figura siguiente procede del artículo Drop-shadow: el filtro CSS infravalorado, que muestra de forma intuitiva la diferencia entre ambos:

El segundo suele usarse más, como en tailwindcss - sombra exterior. Así que añadimos las propiedades siguientes al rectángulo:
rect.dropShadowColor = 'black';
rect.dropShadowOffsetX = 10;
rect.dropShadowOffsetY = 10;
rect.dropShadowBlurRadius = 5;
A continuación usaremos WebGL / WebGPU para dibujar sombras de gráficos 2D. El enfoque habitual es usar un desenfoque gaussiano en el posprocesado, como el DropShadowFilter de Pixi.js. El efecto de desenfoque gaussiano 2D se puede descomponer en dos efectos 1D, horizontal y vertical, pero la operación de convolución sigue exigiendo muestrear los píxeles vecinos (según el tamaño del núcleo de convolución).
El CTO de Figma, Evan Wallace, presentó un método de aproximación más rápido en el artículo Sombras rápidas de rectángulos redondeados, que no requiere muestrear texturas, y el artículo Aprovechar Rust y la GPU para renderizar interfaces de usuario a 120 FPS también ofreció una introducción más detallada. La convolución de la función gaussiana con la función escalón equivale a la integral de la función gaussiana, que da como resultado la función de error Función de error (también conocida como erf). Por tanto, generar un rectángulo desenfocado equivale a desenfocar cada dimensión por separado y después tomar la intersección de los dos resultados, sin considerar el radio de esquina.
Función gaussiana:
La función de error es la integral de una función gaussiana y se usa para describir la función de distribución acumulada de una distribución normal.
Una aproximación habitual de la función de error procede de Abramowitz y Stegun. Manual de funciones matemáticas.
donde los coeficientes de los términos del polinomio son:
La implementación siguiente procede de Zed Blade WGSL, que hemos reescrito en GLSL. Rectángulos redondeados desenfocados también ofrece otra versión de erf.
vec2 erf(vec2 x) {
vec2 s = sign(x), a = abs(x);
x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
x *= x;
return s - s / (x * x);
}De momento dejemos de lado las esquinas redondeadas y calculemos el valor final de la máscara de sombra. Aquí el límite de la sombra lo determina la diferencia entre integral_x e integral_y. integral_x.x - integral_x.y calcula el ancho de la sombra en el eje x, e integral_y.x - integral_y.y calcula el alto de la sombra en el eje y. Multiplica estos dos valores para obtener el valor final de la máscara de sombra.
// Return the mask for the shadow of a box from lower to upper
float rect_shadow(vec2 pixel_position, vec2 origin, vec2 size, float sigma) {
vec2 bottom_right = origin + size;
vec2 x_distance = vec2(pixel_position.x - origin.x, pixel_position.x - bottom_right.x);
vec2 y_distance = vec2(pixel_position.y - origin.y, pixel_position.y - bottom_right.y);
vec2 integral_x = 0.5 + 0.5 * erf(x_distance * (sqrt(0.5) / sigma));
vec2 integral_y = 0.5 + 0.5 * erf(y_distance * (sqrt(0.5) / sigma));
return (integral_x.x - integral_x.y) * (integral_y.x - integral_y.y);
}Sin embargo, no existe una solución de forma cerrada como la anterior para la convolución 2D de un rectángulo redondeado con una gaussiana, porque la fórmula de un rectángulo redondeado no es separable. La astucia de la aproximación de Evan Wallace consiste en hacer una convolución exacta de forma cerrada a lo largo de un eje y, después, deslizar manualmente la gaussiana a lo largo del eje opuesto un número finito de veces:
float blur_along_x(float x, float y, float sigma, float corner, vec2 half_size) {
float delta = min(half_size.y - corner - abs(y), 0.);
float curved =
half_size.x - corner + sqrt(max(0., corner * corner - delta * delta));
vec2 integral =
0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
return integral.y - integral.x;
}// The signal is only non-zero in a limited range, so don't waste samples
float low = center_to_point.y - half_size.y;
float high = center_to_point.y + half_size.y;
float start = clamp(-3. * blur_radius, low, high);
float end = clamp(3. * blur_radius, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
float step = (end - start) / 4.;
float y = start + step * 0.5;
for (int i = 0; i < 4; i++) {
alpha += blur_along_x(center_to_point.x, center_to_point.y - y, blur_radius,
cornerRadius, half_size) *
gaussian(y, blur_radius) * step;
y += step;
}En la implementación, las sombras hay que dibujarlas por separado para cada rectángulo, lo que rompe el efecto del lote combinado anterior. El motivo es que tenemos que hacerlo en un orden de dibujo estricto, e incluso reordenar antes de cada repintado. El código siguiente procede de este artículo: Sombras rápidas de rectángulos redondeados. Antes de dibujar hay que ordenar todos los rectángulos por una profundidad preestablecida y después dibujar, por turno, la sombra y el cuerpo:
render() {
boxes.sort(function(a, b) {
return a.depth - b.depth;
});
for (var i = 0; i < boxes.length; i++) {
boxes[i].callback(); // Draw shadow first and then the rectangle itself.
}
}Como ejemplo, los dos rectángulos siguientes se dibujan en este orden: sombra del rectángulo verde, rectángulo verde, sombra del rectángulo rojo, rectángulo rojo. Si seguimos la idea anterior de fusionar las dos sombras y los dos cuerpos de los rectángulos en dos lotes de dibujo separados, no podremos hacer que las sombras de los rectángulos rojos se proyecten sobre los rectángulos verdes. Así que, al usarlo, hay que asignar batchable = false a los rectángulos con sombras.
$icCanvas3 = call(() => {
return document.createElement('ic-canvas-lesson9');
});call(() => {
const { Canvas, Rect } = Lesson9;
const stats = new Stats();
stats.showPanel(0);
const $stats = stats.dom;
$stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
$icCanvas3.parentElement.style.position = 'relative';
$icCanvas3.parentElement.appendChild($stats);
$icCanvas3.addEventListener('ic-ready', (e) => {
const canvas = e.detail;
const rect = new Rect({
x: 50,
y: 50,
fill: 'green',
cornerRadius: 50,
batchable: false,
dropShadowColor: 'black',
dropShadowOffsetX: 10,
dropShadowOffsetY: 10,
dropShadowBlurRadius: 10,
});
rect.width = 400;
rect.height = 100;
canvas.appendChild(rect);
const rect2 = new Rect({
x: 100,
y: 100,
fill: 'red',
batchable: false,
cornerRadius: 50,
dropShadowColor: 'black',
dropShadowBlurRadius: 10,
});
rect2.width = 400;
rect2.height = 100;
canvas.appendChild(rect2);
});
$icCanvas3.addEventListener('ic-frame', (e) => {
stats.update();
});
});Otra cosa que hay que tener en cuenta es que, debido al radio de desenfoque de la sombra, hay que hacer que el rectángulo se expanda un margen respecto de su tamaño original, que aquí se fija en 3 * dropShadowBlurRadius.
float margin = 3.0 * dropShadow.z;
origin += dropShadow.xy;
v_Origin = origin;
v_Size = size;
origin -= margin;
size += 2.0 * margin;
vec2 center = origin + size / 2.0;
v_Point = center + a_FragCoord * (size / 2.0);Por último, las sombras también afectan al cálculo de RenderBounds; si no, los rectángulos se descartarían de forma incorrecta cuando su cuerpo está fuera del viewport pero sus sombras están dentro:
this.renderBounds.addBounds(
new AABB(
x + dropShadowOffsetX - dropShadowBlurRadius,
y + dropShadowOffsetY - dropShadowBlurRadius,
x + dropShadowOffsetX + width + dropShadowBlurRadius,
y + dropShadowOffsetY + height + dropShadowBlurRadius,
),
);A partir de este enfoque también se pueden conseguir algunos efectos interesantes; consulta Efecto de desenfoque de lente de formas con SDF y WebGL.
Evidentemente, el método anterior solo funciona para rectángulos redondeados, pero ¿hay un método más general para círculos, elipses y otras representaciones SDF? Hay un ejemplo en Shadertoy: Sombra exterior de un rectángulo redondeado y, curiosamente, hay otro ejemplo basado en este que permite implementar tanto la sombra exterior como la interior. A continuación nos centramos en la implementación de la sombra interior.
Añadir sombra interior
La imagen siguiente muestra el efecto de sombra interior de Figma, que se usa a menudo en componentes de UI como un botón.

Añadamos los atributos siguientes:
rect.innerShadowColor = 'black';
rect.innerShadowOffsetX = 10;
rect.innerShadowOffsetY = 10;
rect.innerShadowBlurRadius = 5;Tomando como referencia el ejemplo de Shadertoy Sombra interior de un rectángulo redondeado, añadimos de forma parecida la lógica de dibujo de la sombra a cada una de las tres formas actuales. Aquí usamos sigmoid para suavizar:
float make_shadow(vec2 pos, vec2 halfSize, float cornerRd, float blurRd, float distMul, float shape) {
float distance;
if (shape < 0.5) {
distance = sdf_circle(pos, halfSize.x);
} else if (shape < 1.5) {
distance = sdf_ellipse(pos, halfSize);
} else if (shape < 2.5) {
distance = sdf_rounded_box(pos, halfSize, cornerRd + blurRd);
}
float dist = sigmoid(distMul * distance / blurRd);
return clamp(dist, 0.0, 1.0);
}$icCanvas4 = call(() => {
return document.createElement('ic-canvas-lesson9');
});call(() => {
const { Canvas, Rect, Circle, Ellipse } = Lesson9;
const stats = new Stats();
stats.showPanel(0);
const $stats = stats.dom;
$stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
$icCanvas4.parentElement.style.position = 'relative';
$icCanvas4.parentElement.appendChild($stats);
$icCanvas4.addEventListener('ic-ready', (e) => {
const canvas = e.detail;
for (let i = 0; i < 10; i++) {
const fill = `rgb(${Math.floor(Math.random() * 255)},${Math.floor(
Math.random() * 255,
)},${Math.floor(Math.random() * 255)})`;
const rect = new Rect({
x: Math.random() * 1000,
y: Math.random() * 1000,
fill,
cornerRadius: 50,
innerShadowColor: 'black',
innerShadowOffsetX: Math.random() * 20 - 10,
innerShadowOffsetY: Math.random() * 20 - 10,
innerShadowBlurRadius: Math.random() * 10,
});
rect.width = 200;
rect.height = 100;
canvas.appendChild(rect);
const circle = new Circle({
cx: Math.random() * 1000,
cy: Math.random() * 1000,
r: 100,
fill,
innerShadowColor: 'black',
innerShadowOffsetX: Math.random() * 20 - 10,
innerShadowOffsetY: Math.random() * 20 - 10,
innerShadowBlurRadius: Math.random() * 10,
});
canvas.appendChild(circle);
const ellipse = new Ellipse({
cx: Math.random() * 1000,
cy: Math.random() * 1000,
rx: 100,
ry: 50,
fill,
innerShadowColor: 'blue',
innerShadowOffsetX: Math.random() * 20 - 10,
innerShadowOffsetY: Math.random() * 20 - 10,
innerShadowBlurRadius: Math.random() * 10,
});
canvas.appendChild(ellipse);
}
});
$icCanvas4.addEventListener('ic-frame', (e) => {
stats.update();
});
});Elipse
A diferencia de determinar la distancia más cercana de un punto arbitrario a un círculo, el método exacto para analizar una elipse es mucho más complejo, y Distancia a una elipse ofrece dos métodos: la ecuación cuártica y el método de Newton.
Ecuación cuártica
Una elipse se representa mediante
Desarrollada:
Al derivar respecto de
La derivada en el punto más cercano es 0, y se obtiene al sustituir
Se trata de una ecuación cuártica de una variable para
Para reducir a 1 el coeficiente del término de cuarto grado y obtener un Polinomio mónico, divide cada uno de los coeficientes anteriores entre
Así se obtiene la Ecuación cuártica, con los coeficientes siguientes:
Como el coeficiente de mayor grado es 1, esto se puede transformar en un problema de resolver una ecuación cúbica Fórmula cúbica, usando la Cúbica resolvente:
donde los coeficientes se corresponden así:
Al sustituir se obtiene:
Resolver la Fórmula cúbica implica intentar eliminar los coeficientes binomiales, lo que se puede hacer mediante
Al sustituir
Si se sigue simplificando la ecuación anterior, sea:
La forma normalizada queda:
Este método es costoso en cálculo, y el ejemplo escrito en Shadertoy que se muestra en Distancia a una elipse tiene un FPS mucho menor que los dos métodos que se describen a continuación.
Método de Newton
El vector formado por cualquier punto
Donde:
Al sustituir se obtiene:
El Método de Newton es un método para resolver ecuaciones de forma aproximada en los dominios de los números reales y complejos.
Al sustituir se obtiene:
Distancia a una elipse. El autor original usó 5 iteraciones para lograr un resultado muy bueno:
for (int i=0; i<5; i++) {
vec2 cs = vec2(cos(w),sin(w));
vec2 u = ab*vec2( cs.x,cs.y);
vec2 v = ab*vec2(-cs.y,cs.x);
w = w + dot(p-u,v)/(dot(p-u,u)+dot(v,v));
}Lo único que falta a continuación es encontrar
bool s = dot(p/ab,p/ab)>1.0;Si está fuera de la elipse, se puede estirar hasta convertirla en un círculo (el método que se describe en la subsección siguiente también se usará), donde
Si está dentro de la elipse, elige el semieje mayor
float w = s ? atan(p.y*ab.x, p.x*ab.y) :
((ab.x*(p.x-ab.x)<ab.y*(p.y-ab.y))? 1.5707963 : 0.0);Comparado con el método anterior de resolver una ecuación cuártica de una variable, este método tiene menos coste. Aun así, contiene un bucle for con un número fijo de iteraciones, que se desenrolla al compilar GLSL; consulta Rendimiento de los bucles en un shader. Por último, presentaremos un método de estimación aproximada con el coste mínimo.
Método aproximado de estiramiento
SDF de elipsoide describe este método de estirar hasta un círculo unidad y después recuperar la elipse. Pero ¿cómo eliges los coeficientes de estiramiento al recuperar? Si el punto está en el eje X, el coeficiente es r.x, y en el eje Y es r.y, y primero se elige el menor de los dos:
float sdf_ellipse_V1( in vec2 p, in vec2 r )
{
float k1 = length(p/r);
return (k1-1.0)*min(r.x,r.y);
}El renderizado real revela que este enfoque tiene problemas importantes en el eje mayor y no se ajusta bien:

El autor original dio una versión mejorada:
float sdf_ellipse_V3( in vec3 p, in vec3 r )
{
float k1 = length(p/r);
return length(p)*(1.0-1.0/k1);
}Sin embargo, el renderizado real revela que este enfoque tiene un dentado importante en los bordes:

Al final los autores añadieron la consideración de los gradientes, que también usamos:
float sdf_ellipse_V2( in vec2 p, in vec2 r )
{
float k1 = length(p/r);
float k2 = length(p/(r*r));
return k1*(k1-1.0)/k2;
}Puedes ver que los resultados son mucho mejores.
$icCanvas = call(() => {
return document.createElement('ic-canvas-lesson9');
});call(() => {
const { Canvas, Ellipse } = Lesson9;
const stats = new Stats();
stats.showPanel(0);
const $stats = stats.dom;
$stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
$icCanvas.parentElement.style.position = 'relative';
$icCanvas.parentElement.appendChild($stats);
$icCanvas.addEventListener('ic-ready', (e) => {
const canvas = e.detail;
for (let i = 0; i < 1000; i++) {
const fill = `rgb(${Math.floor(Math.random() * 255)},${Math.floor(
Math.random() * 255,
)},${Math.floor(Math.random() * 255)})`;
const ellipse = new Ellipse({
cx: Math.random() * 1000,
cy: Math.random() * 1000,
rx: Math.random() * 20,
ry: Math.random() * 20,
fill,
});
canvas.appendChild(ellipse);
ellipse.addEventListener('pointerenter', () => {
ellipse.fill = 'red';
});
ellipse.addEventListener('pointerleave', () => {
ellipse.fill = fill;
});
}
});
$icCanvas.addEventListener('ic-frame', (e) => {
stats.update();
});
});Picking
Ahora mismo nuestro plugin de picking usa el Método geométrico, así que necesitamos implementar métodos de determinación separados para elipses y rectángulos redondeados, y más adelante presentaremos un método de picking más general, basado en la GPU y codificado por color. En los dos ejemplos anteriores puedes pasar el ratón por encima de cualquier forma para probar el efecto de picking.
Elipse
Las decisiones de picking de la elipse son más sencillas; por ejemplo, de forma predeterminada se tienen en cuenta las áreas de relleno y de trazo:
function isPointInEllipse(
x: number,
y: number,
h: number,
k: number,
a: number,
b: number,
) {
const dx = x - h;
const dy = y - k;
const squaredDistance = (dx * dx) / (a * a) + (dy * dy) / (b * b);
// Inside ellipse
return squaredDistance <= 1;
}Rectángulo redondeado
También es muy sencillo si no se consideran las esquinas redondeadas.
function isPointInRoundedRectangle(
x: number,
y: number,
x1: number,
y1: number,
x2: number,
y2: number,
r: number,
) {
// Determine if a point is inside the corners of a rectangle.
function isInsideCorner(
x: number,
y: number,
cornerX: number,
cornerY: number,
r: number,
) {
const distance = Math.sqrt(
Math.pow(x - cornerX, 2) + Math.pow(y - cornerY, 2),
);
return distance <= r;
}
// Determine if a point is inside a rounded rectangle
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {
// Points inside the rectangle
if (
isInsideCorner(x, y, x1 + r, y1 + r, r) || // top-left
isInsideCorner(x, y, x2 - r, y1 + r, r) || // top-right
isInsideCorner(x, y, x2 - r, y2 - r, r) || // bottom-right
isInsideCorner(x, y, x1 + r, y2 - r, r) // bottom-left
) {
return true; // Points inside the corner
}
return !(
x <= x1 + r ||
x >= x2 - r || // The point is on the non-circular boundary of the rectangle
y <= y1 + r ||
y >= y2 - r
);
}
return false; // Points outside the rectangle
}