Cómo Agregar un Efecto Viñeta a Imágenes: Guía Técnica Completa
Implementa el clásico oscurecimiento de bordes para mejorar tus proyectos visuales.
Por HolaGPT Editorial · 10 min lectura · 2026-07-11
El efecto viñeta, ese suave oscurecimiento de los bordes de una imagen, es un recurso clásico en fotografía y diseño digital. Suaviza la transición hacia el fondo, dirige la atención al centro y añade profundidad sin distraer. Aunque los editores gráficos lo aplican con un clic, un profesional del desarrollo necesita implementarlo mediante código, ya sea en una aplicación web, un pipeline de procesamiento por lotes o una interfaz interactiva.
En esta guía exploramos las matemáticas detrás del viñeteado y recorremos las implementaciones más eficientes usando CSS, JavaScript sobre lienzo y Python con bibliotecas como Pillow u OpenCV. Cada enfoque incluye ejemplos funcionales, ajustables y optimizados para tu flujo de trabajo. Si alguna vez te preguntaste cómo pasar de un degradado radial a una imagen procesada en tiempo real, aquí tienes la respuesta.
No se trata de una receta mágica, sino de entender el mecanismo y adaptarlo a tus necesidades. Al finalizar, podrás decidir cuándo usar una máscara circular, un filtro canvas o una simple hoja de estilos, y automatizar el proceso usando herramientas que entienden lenguaje natural, como HolaGPT, para generar los fragmentos iniciales y acelerar la implementación.
1. El efecto viñeta: definición y fundamentos matemáticos
Una viñeta consiste en reducir progresivamente el brillo o la luminosidad de los píxeles desde el centro hacia los bordes de una imagen. La transición puede ser circular, elíptica o seguir una forma personalizada. Matemáticamente, se modela como un mapa de peso w(x, y) que multiplica cada canal del píxel original. El peso máximo (1.0) está en el centro y decae hacia la periferia, a menudo según una curva gaussiana o coseno elevado.
La fórmula más común es la radial: w(x, y) = 1 - d^n, donde d es la distancia normalizada desde el centro (0 en el centro, 1 en la esquina más lejana) y n controla la intensidad. Para una caída suave se usa w = cos(d * pi/2) o una función sigmoide. Al aplicar el peso, el píxel resultante es pixel_nuevo = pixel_original * w. Esto oscurece sin alterar el matiz. Si buscas un efecto más dramático, puedes interpolar hacia un color fijo (por ejemplo, negro) en lugar de simplemente oscurecer.
Comprender la base matemática permite portar el efecto a cualquier entorno de programación gráfica, desde shaders GLSL hasta funciones lambda en AWS. A continuación, veremos cómo llevarlo a la práctica en los tres dominios más solicitados: web, JavaScript canvas y Python.
2. Viñeta con CSS: la vía más rápida para la web
Cuando trabajas con elementos <img> o contenedores en una interfaz web, CSS ofrece soluciones ligeras y con aceleración por hardware. La clave es generar un degradado radial transparente a oscuro que actúe como máscara o como overlay.
2.1. Overlay con background radial-gradient
Superpón un pseudo-elemento ::after con un fondo radial que transite de transparente (rgba(0,0,0,0)) a semitransparente negro. Ajusta la opacidad final y la extensión del degradado para controlar la fuerza de la viñeta.
.vignette-container {
position: relative;
display: inline-block;
}
.vignette-container::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at center, rgba(0,0,0,0) 60%, rgba(0,0,0,0.7) 100%);
pointer-events: none; /* para no bloquear clics */
}Este método no modifica la imagen original y se adapta a cualquier tamaño. La propiedad ellipse at center produce una viñeta ovalada clásica; puedes cambiar a circle si prefieres un degradado estrictamente circular. La intensidad se modifica en el valor final del rgba y el punto de inicio del degradado (60% en el ejemplo).
2.2. Máscara con mask-image
Alternativamente, usa mask-image para aplicar viñeta directamente sobre la imagen. Tiene mejor rendimiento en animaciones porque evita composiciones extra.
img.vignette-mask {
mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%);
}Aquí el negro actúa como opaco (imagen visible) y el transparente oculta los bordes. La desventaja es que algunos navegadores antiguos no soportan mask-image completamente; verifica compatibilidad si tu audiencia utiliza equipos heredados.
2.3. Filtro CSS y backdrop-filter
Una tercera alternativa usa filter: blur() combinado con un overlay para suavizar bordes, pero no es una viñeta genuina. El soporte para backdrop-filter permite difuminar el fondo de un contenedor, imitando un efecto de profundidad similar a viñeta, aunque con menor control del oscurecimiento. Recomiendo las dos primeras opciones por su simplicidad y fidelidad al concepto.
3. Implementación con JavaScript y Canvas
Cuando necesitas procesar la imagen del lado del cliente, ya sea para previsualizaciones, editores fotográficos en línea o exportación dinámica, el elemento <canvas> de HTML5 es la herramienta correcta. Trabajamos directamente sobre los datos de píxeles, lo que brinda un control total.
3.1. Estructura básica
Carga una imagen, dibújala en un canvas y luego itera sobre la matriz ImageData para aplicar el mapa de pesos. Primero necesitamos la imagen cargada. Supongamos que ya disponemos de un objeto img y un canvas con contexto 2D.
const canvas = document.getElementById('lienzo');
const ctx = canvas.getContext('2d');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;3.2. Cálculo del peso radial
Definimos el centro y el radio máximo como la distancia desde el centro hasta una esquina. Para cada píxel, calculamos la distancia normalizada d y el peso w. Luego multiplicamos los canales R, G y B (no alfa), manteniendo el canal alfa original.
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const maxDist = Math.sqrt(cx * cx + cy * cy);
for (let i = 0; i < data.length; i += 4) {
const x = (i / 4) % canvas.width;
const y = Math.floor((i / 4) / canvas.width);
const dx = x - cx;
const dy = y - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const d = dist / maxDist; // normalizado 0-1
const w = 1 - Math.pow(d, 2.5); // exponente controla intensidad (2-4 típico)
data[i] = data[i] * w; // R
data[i + 1] = data[i + 1] * w; // G
data[i + 2] = data[i + 2] * w; // B
// data[i+3] sin modificar
}
ctx.putImageData(imageData, 0, 0);El exponente 2.5 es un buen punto de partida; increméntalo para una viñeta más cerrada o redúcelo para un degradado muy sutil. Puedes usar también w = Math.cos(d * Math.PI / 2) para una curva más suave.
3.3. Optimización y variantes
El acceso directo a data es rápido, pero si necesitas procesar imágenes grandes en tiempo real (por ejemplo, al deslizar un control de intensidad), conviene implementar una tabla de pesos precalculada o trabajar con requestAnimationFrame y un canvas fuera de pantalla. También puedes variar la forma elíptica ajustando el cálculo de d con factores diferentes para el eje X e Y: d = Math.sqrt((dx*dx)/(a*a) + (dy*dy)/(b*b)), donde a y b son semiejes. Así obtienes viñetas ovaladas.
4. Procesamiento por lotes con Python
Del lado servidor o en automatizaciones, Python ofrece bibliotecas maduras para manipular imágenes. Dos grandes protagonistas: Pillow (PIL) y OpenCV. Ambas pueden crear una máscara de viñeta y fusionarla con la imagen original.
4.1. Viñeta con Pillow
Pillow permite generar un degradado radial como imagen en escala de grises (modo 'L') y luego multiplicarlo usando Image.composite o Image.blend. Un enfoque directo: crear una máscara con Image.radial_gradient (desde la versión 9.2.0) y usarla para oscurecer.
from PIL import Image, ImageFilter
import math
def vignette_pillow(im, intensity=0.7, center_pct=60):
# Crear máscara radial
width, height = im.size
mask = Image.new('L', (width, height))
# Generar degradado radial calculando pixel a pixel (alternativa si no se dispone de radial_gradient)
for y in range(height):
for x in range(width):
dx = (x - width/2) / (width/2)
dy = (y - height/2) / (height/2)
d = math.sqrt(dx*dx + dy*dy)
d = min(d, 1.0)
# Usar función coseno para suavidad
val = int(255 * (1 - math.cos(d * math.pi / 2)) * intensity)
mask.putpixel((x, y), val)
# Aplicar suavizado gaussiano para evitar transiciones bruscas
mask = mask.filter(ImageFilter.GaussianBlur(radius=width*0.02))
# Oscurecer: multiplicar canales con la máscara invertida
# Convertir máscara a RGB y usar blend
dark = Image.new('RGB', (width, height), (0, 0, 0))
return Image.blend(im, dark, alpha=mask.point(lambda p: p / 255.0 * intensity))
img = Image.open('foto.jpg')
result = vignette_pillow(img, intensity=0.8)
result.save('foto_vineta.jpg')El código calcula la máscara pixel a pixel y aplica desenfoque para suavizar el borde. Luego usa Image.blend con una imagen negra y la máscara como canal alfa de oscurecimiento. Esto interpola hacia el negro; si prefieres simplemente oscurecer, multiplica los canales directamente con la máscara usando ImageMath.
4.2. Viñeta con OpenCV
OpenCV es ideal cuando ya trabajas en un entorno de visión artificial. La misma lógica: crear una matriz de pesos con forma radial y multiplicarla por la imagen.
import cv2
import numpy as np
def vignette_opencv(img, exponent=2.0):
rows, cols = img.shape[:2]
X_resultant_kernel = cv2.getGaussianKernel(cols, sigma=cols/4)
Y_resultant_kernel = cv2.getGaussianKernel(rows, sigma=rows/4)
kernel = Y_resultant_kernel * X_resultant_kernel.T
mask = kernel / kernel.max()
mask = np.power(mask, exponent)
mask = cv2.GaussianBlur(mask, (0, 0), sigmaX=30, sigmaY=30)
vignette = np.zeros_like(img)
for c in range(3): # R,G,B
vignette[:,:,c] = img[:,:,c] * mask
return vignette
img = cv2.imread('foto.jpg')
result = vignette_opencv(img, exponent=2.5)
cv2.imwrite('foto_vineta.jpg', result)Este método usa kernels gaussianos 1D para generar un degradado bidimensional suave, ajustando la intensidad con un exponente. La operación final es una multiplicación elemento a elemento. OpenCV maneja eficientemente grandes volúmenes y es compatible con aceleración GPU si se configura con CUDA.
5. Comparativa y casos de uso
La elección de la técnica depende del contexto. CSS es imbatible cuando la imagen ya está en el DOM y no necesitas guardar cambios: consume cero CPU adicional porque delega en la composición del navegador. Ideal para galerías fotográficas, tarjetas de perfil o superposiciones decorativas.
JavaScript canvas brilla en editores interactivos: el usuario puede ajustar intensidad, forma y desenfoque en tiempo real, y luego exportar la imagen final. La desventaja es el costo computacional en imágenes muy grandes, aunque puedes subsamplear para previsualización.
Python con Pillow u OpenCV es la alternativa para servidores, procesos batch o integraciones con sistemas de machine learning. Puedes encadenar la viñeta con otras transformaciones (redimensionamiento, recorte inteligente) y automatizar flujos enteros con scripts. Además, al integrar una API que entienda lenguaje natural, como HolaGPT, puedes describir “aplica viñeta a estas 500 fotos con intensidad 0.7” y obtener el código Python generado automáticamente, ahorrando tiempo de escritura manual.
6. Ajustes avanzados y personalización
Más allá del triángulo CSS-Canvas-Python, existen variantes que elevan la calidad visual:
- Viñeta selectiva: limita el efecto a una región de interés detectada (por ejemplo, un rostro). Basta con desplazar el centro del degradado a las coordenadas de la ROI.
- Viñeta con color: en lugar de oscurecer hacia el negro, puedes derivar hacia un tono sepia o azul para evocar nostalgia o frío. Cambia la imagen oscura en el blend de Pillow por un color sólido.
- Transición no lineal: funciones sigmoides o polinomios de Bernstein ofrecen curvas más estilizadas. El código abierto de herramientas como ImageMagick (con su operador
-vignette) emplea estas técnicas; puedes estudiarlas para inspirarte. - Viñeta como filtro en cámara en vivo: con WebGL y shaders puedes aplicar el efecto sobre un flujo de video en tiempo real, útil para aplicaciones de fotomatón o videoconferencia.
La mayoría de estas personalizaciones parten del mismo principio: un mapa de pesos que se multiplica o interpola con el píxel original. Una vez que dominas el patrón, las posibilidades son ilimitadas.
7. Buenas prácticas y rendimiento
Independientemente de la plataforma, ten en cuenta estos puntos:
- Precálculo: si el tamaño de la imagen es fijo, genera la máscara una sola vez y reutilízala.
- Espacio de color: opera en RGB lineal si buscas fidelidad fotográfica, aunque en la práctica el espacio sRGB es suficiente para la mayoría de aplicaciones.
- Profundidad de bits: al procesar imágenes de 16 bits por canal (común en RAW), mantén esa profundidad durante el cálculo para evitar posterización en degradados muy suaves.
- Pruebas cross-browser en CSS: usa
@supports (mask-image: ...)para ofrecer una alternativa con::afteren navegadores sin soporte. - Manejo de memoria en Python: si loteas miles de imágenes, usa
with Image.open()y libera explícitamente con.close()para evitar fugas de memoria.