wandres.dev
DIBUJO Y GRÁFICOS · Canvas, Shape, Charts

Efectos visuales: gradientes, sombras, máscaras y materiales

ShapeStyle como álgebra de relleno, gradientes y su espacio de interpolación, el coste real de una sombra, enmascarado por alfa y luminancia, modos de mezcla separables y no separables, y los materiales del sistema con su vibrancia.

⏱ 17 min

El acabado visual de una app no se decide en la elección de colores sino en el entendimiento del pipeline de composición: qué se rellena, contra qué se mezcla, en qué espacio de color se interpola y cuántas pasadas fuera de pantalla cuesta cada efecto. SwiftUI expone ese pipeline mediante un puñado de abstracciones muy bien elegidas —ShapeStyle, máscaras, modos de mezcla y materiales— que parecen decorativas y son en realidad el modelo de composición completo. Dominarlas es la diferencia entre imitar un efecto y saber por qué funciona, cuánto cuesta y cómo se comporta en modo oscuro, con transparencia reducida o sobre un fondo que se mueve.

🎯 Al terminar esta lección sabrás
  • Usar ShapeStyle como un álgebra de rellenos: colores, gradientes, jerarquías y materiales.
  • Controlar la interpolación de gradientes y evitar bandas y mezclas turbias.
  • Aplicar máscaras por alfa y por luminancia, y entender el grupo de composición.
  • Elegir modos de mezcla y materiales del sistema conociendo su coste y su semántica.

ShapeStyle: un álgebra de rellenos

Todo lo que puede rellenar una superficie en SwiftUI conforma a un mismo protocolo. Color, los cuatro tipos de gradiente, los materiales, los estilos jerárquicos como secondary, el color de acento tint y las imágenes en mosaico mediante ImagePaint son intercambiables allí donde se espera un estilo. Esa uniformidad es lo que permite escribir código que no sabe con qué se pintará:

struct Insignia<Fondo: ShapeStyle>: View {
    let fondo: Fondo
    var body: some View {
        Text("PRO")
            .padding(.horizontal, 10).padding(.vertical, 4)
            .background(fondo, in: .capsule)
            .foregroundStyle(.white.shadow(.drop(radius: 1, y: 1)))
    }
}

Dos detalles de peso. Primero, los estilos se componen: shadow aplicado a un estilo produce otro estilo, de modo que la sombra puede vivir en el relleno en lugar de en la vista, incluida la variante interior. Segundo, cuando necesitas heterogeneidad en tiempo de ejecución existe AnyShapeStyle, que borra el tipo a cambio de una indirección; conviene reservarlo para las fronteras del diseño y no salpicarlo por todas partes.

Los gradientes se declaran por paradas. La forma corta a partir de un color, la versión explícita con posiciones y las variantes radial, angular y elíptica cubren casi todo; la malla de gradientes permite superficies bidimensionales con control por vértice:

LinearGradient(stops: [
    .init(color: .indigo, location: 0.0),
    .init(color: .purple, location: 0.55),
    .init(color: .pink,   location: 1.0),
], startPoint: .topLeading, endPoint: .bottomTrailing)

AngularGradient(colors: [.red, .yellow, .green, .red], center: .center)

MeshGradient(width: 3, height: 3,
             points: rejilla,          // nueve vertices en espacio unitario
             colors: paleta)           // un color por vertice

La malla merece atención aparte porque cambia la naturaleza del degradado: en vez de interpolar a lo largo de un eje o de un radio, define una superficie bidimensional donde cada vértice aporta color y posición, y desplazar esos vértices con el tiempo produce fondos orgánicos que antes exigían vídeo o imágenes. El precio es que se rasteriza como cualquier otro relleno: una malla animada a pantalla completa es fill rate puro, y conviene medirla en el dispositivo más modesto que soportes antes de darla por buena.

⚠️
La interpolación tiene espacio de color

Un degradado entre dos colores saturados y complementarios puede pasar por un gris sucio si se interpola en un espacio no lineal, y mostrar bandas visibles en superficies grandes con poca variación. Dos remedios: añadir paradas intermedias elegidas a mano para forzar el camino cromático deseado, y componer sobre un lienzo en modo extendedLinear cuando el degradado es el protagonista.

Sombras y el coste de una pasada extra

Una sombra proyectada no es un rectángulo difuminado detrás de la vista: es la silueta alfa del contenido, desplazada, desenfocada y compuesta debajo. Eso implica que el sistema debe rasterizar el contenido fuera de pantalla antes de poder desenfocarlo, y ese salto es el coste real.

tarjeta
    .compositingGroup()                       // una silueta, no una por hijo
    .shadow(color: .black.opacity(0.25), radius: 12, y: 6)

Circle()
    .fill(.orange.shadow(.inner(radius: 6, y: 3)))   // sombra dentro del relleno

compositingGroup es la pieza que más se olvida: sin él, un contenedor con varios hijos puede acabar generando una sombra por hijo, multiplicando pasadas y produciendo un resultado visualmente sucio en los solapes. Como regla operativa, aplica la sombra lo más cerca posible de una forma simple y bien definida, evita radios enormes en superficies grandes —el coste del desenfoque crece con el área y con el radio— y desconfía de las sombras dentro de listas que se desplazan, donde el gasto se paga en cada fotograma.

Máscaras y modos de mezcla

Enmascarar es multiplicar el alfa del contenido por el de otra vista. clipShape recorta con una geometría; mask admite cualquier vista como máscara, incluido un degradado, lo que da desvanecidos progresivos sin imágenes:

texto
    .mask(LinearGradient(colors: [.black, .clear],
                         startPoint: .leading, endPoint: .trailing))

ZStack {
    Rectangle().fill(.blue)
    Circle().blendMode(.destinationOut)      // agujero real
}
.compositingGroup()

El segundo ejemplo revela la regla que gobierna toda la mezcla: un modo de mezcla actúa contra el resultado acumulado del grupo de composición que lo contiene. Sin compositingGroup, ese agujero se abriría contra todo lo que hay detrás en la ventana, no contra el rectángulo azul. Comprender dónde empieza y acaba el grupo explica el noventa por ciento de los efectos que “no funcionan”.

🎨

Separables

multiply, screen, overlay y softLight operan canal a canal. Son los que oscurecen, aclaran o dan contraste.

🌈

No separables

hue, saturation, color y luminosity mezclan atributos perceptuales completos; sirven para teñir conservando la luz.

✂️

Porter-Duff

sourceAtop, destinationOut y plusLighter gestionan la geometría del alfa: recortar, perforar y acumular luz.

flowchart TD
A[Contenido de la vista] --> B[Grupo de composicion]
B --> C[Mascara multiplica el alfa]
C --> D[Modo de mezcla contra el fondo del grupo]
D --> E[Filtros como sombra o desenfoque]
E --> F[Composicion final en pantalla]
style B fill:#cba6f7,color:#11111b
style D fill:#89b4fa,color:#11111b

Materiales del sistema

Los materiales no son colores translúcidos: son un desenfoque del contenido que hay detrás, más una capa de tinte y saturación calibrada por Apple para cada nivel de grosor. Desde el más tenue al más opaco cubren una escala de cinco pasos, más variantes semánticas para barras y menús.

contenido
    .background(.regularMaterial, in: .rect(cornerRadius: 20))
    .overlay(alignment: .top) {
        Text("Encima del material")
            .foregroundStyle(.secondary)    // vibrancia, no gris fijo
    }

Sobre un material, los estilos jerárquicos —primary, secondary, tertiary, quaternary— activan vibrancia: el texto no se pinta con un gris constante sino que mezcla con el fondo desenfocado para mantener legibilidad en cualquier contexto. Usar un color literal encima de un material es el error más común y el que peor envejece al cambiar de tema.

Quedan dos consideraciones. La primera es de coste: cada material implica capturar y desenfocar el fondo, así que apilarlos o animarlos sobre superficies grandes se paga caro. La segunda es de accesibilidad: con la opción de reducir transparencia activada, el sistema sustituye el material por una superficie opaca, y tu diseño debe seguir siendo legible en ese estado, lo que en la práctica significa no confiar en el desenfoque para separar contenido crítico.

Un efecto no es una decoración: es una afirmación sobre la profundidad

La razón por la que las interfaces de Apple se sienten coherentes no es que usen degradados bonitos, sino que cada recurso visual codifica una relación espacial consistente. Una sombra afirma que algo está por encima; un material afirma que algo es una capa translúcida sobre un contenido que sigue existiendo debajo; la vibrancia afirma que el texto pertenece a esa capa y no al fondo. Cuando esos recursos se usan por su apariencia y no por su significado, el resultado tiene el síntoma inconfundible de la interfaz amateur: sombras en elementos que no flotan, materiales sobre fondos planos donde no hay nada que dejar entrever, mezclas que producen colores imposibles bajo iluminación real. La disciplina consiste en preguntarse siempre qué está afirmando el efecto sobre la estructura de la pantalla, y en verificar esa afirmación en los cuatro estados que no puedes ignorar: claro, oscuro, transparencia reducida y contraste aumentado.

⚔️ Compón profundidad de forma deliberada
  1. Construye una tarjeta con relleno de gradiente y sombra proyectada, y añade compositingGroup para comprobar la diferencia en los solapes.
  2. Sustituye la sombra proyectada por una sombra interior aplicada al estilo de relleno y describe cuándo es preferible cada una.
  3. Haz un desvanecido de texto con mask y un degradado que va de negro a transparente.
  4. Perfora un agujero en una forma con destinationOut y verifica que el grupo de composición delimita el efecto.
  5. Pon contenido sobre regularMaterial, usa estilos jerárquicos para el texto y prueba la app con la transparencia reducida activada.