Diamond Gradients: The Forgotten Primitive

Skia has gradients: linear, radial, conical (angular), sweep. All circular or linear.

Figma has diamond gradients—box-shaped color transitions. Think Manhattan distance instead of Euclidean distance.

Skia doesn't have these. We added them.

The Problem

A radial gradient uses distance = sqrt(dx² + dy²). Colors spread in circles.

A diamond gradient uses distance = max(|dx|, |dy|). Colors spread in squares.

Can't fake a diamond with a radial gradient and transforms. The math is fundamentally different.

First Attempt: Transform a Radial

Maybe scale a radial gradient differently in X and Y?

auto radial = SkGradientShader::MakeRadial(...);
SkMatrix matrix = SkMatrix::Scale(1.0, 2.0);  // Stretch vertically
paint.setShader(radial->makeWithLocalMatrix(matrix));

This created an ellipse, not a square. Radial gradients always produce ellipses under affine transforms, never rectangles.

The distance function needs to change. Transforms won't do it.

The Solution: New Gradient Type

Implement SkGradientShader::MakeDiamond() following Skia's gradient API pattern.

Distance function (src/shaders/gradients/SkDiamondGradient.cpp):

float distanceFromCenter(float2 pos) {
  return max(abs(pos.x), abs(pos.y));  // L∞ norm (Chebyshev distance)
}

Understanding Different Distance Metrics

The shape of a gradient comes from how you measure "distance from center". Let's compare three ways:

Euclidean distance (what radial gradients use):

distance = sqrt(x² + y²)

This is the straight-line distance you measure with a ruler. Points equidistant from the center form a circle.

Example: Point (3, 4) from origin:

distance = sqrt(3² + 4²) = sqrt(9 + 16) = sqrt(25) = 5

Manhattan distance (L₁ norm, taxi-cab distance):

distance = |x| + |y|

Named after walking in Manhattan—you can't cut diagonally through buildings, so you walk along streets (x) then avenues (y). Points equidistant form a diamond rotated 45°.

Example: Point (3, 4) from origin:

distance = |3| + |4| = 7

Chebyshev distance (L∞ norm, what we use):

distance = max(|x|, |y|)

The "king's move" in chess—a king can move one square in any direction (including diagonal), so what matters is the larger of the two coordinates. Points equidistant form an axis-aligned square.

Example: Point (3, 4) from origin:

distance = max(|3|, |4|) = 4

All points at distance 4: (4,0), (4,4), (0,4), (-4,4), (-4,0), (-4,-4), (0,-4), (4,-4), and everything in between on the square's edges.

Why "Diamond" for a Square Gradient?

In Figma's terminology, it's called "diamond" because visually the gradient spreads in a box/diamond shape. Mathematically it's the L∞ norm producing axis-aligned squares, but "diamond gradient" is the design tool name.

For a point at (3, 5): max(3, 5) = 5. Same distance as (5, 5), (5, 3), (5, 0), or (5, -5). All these points are equidistant—they form a square of side length 10 (from -5 to +5 in both directions).

Shader implementation:

sk_sp SkGradientShader::MakeDiamond(
    const SkPoint& center,
    SkScalar radius,
    const SkColor4f colors[],
    sk_sp colorSpace,
    const SkScalar pos[],
    int colorCount,
    SkTileMode mode,
    uint32_t flags,
    const SkMatrix* localMatrix) {

  // Similar structure to MakeRadial
  return sk_make_sp(center, radius, ...);
}

The actual rendering happens in a fragment shader that samples the distance, looks up the color in the gradient, and returns the result.

The SkColor Overload Issue

Added MakeDiamond() with SkColor4f parameter (modern API). Tried to use it:

SkColor4f colors[] = {{1, 0, 0, 1}, {0, 0, 1, 1}};
auto shader = SkGradientShader::MakeDiamond(center, radius, colors, ...);

Linker error: undefined reference to MakeDiamond(SkColor).

Turns out Skia has two parallel gradient APIs: one for SkColor (legacy, 32-bit RGBA), one for SkColor4f (modern, floating-point). We implemented the 4f version, but CanvasKit's JavaScript API still used the SkColor path internally.

Had to add the overload:

sk_sp SkGradientShader::MakeDiamond(
    const SkPoint& center,
    SkScalar radius,
    const SkColor colors[],  // Not SkColor4f
    const SkScalar pos[],
    int count,
    SkTileMode mode,
    uint32_t flags,
    const SkMatrix* localMatrix) {

  // Convert SkColor to SkColor4f and call the main implementation
  SkSTArray<4, SkColor4f> colors4f(count);
  for (int i = 0; i < count; i++) {
    colors4f.push_back(SkColor4f::FromColor(colors[i]));
  }
  return MakeDiamond(center, radius, colors4f.data(), nullptr, ...);
}

Now both APIs work. The SkColor version converts to SkColor4f and delegates.

Distance Field Properties

The diamond distance function max(|x|, |y|) is the L∞ norm (supremum norm). It's the limiting case of (|x|^p + |y|^p)^(1/p) as p → ∞.

Properties:

For gradients, the non-differentiability doesn't matter—we're sampling discrete color stops, not requiring smooth derivatives.

Results

Diamond gradients now work in CanvasKit. The implementation is ~200 lines, following Skia's existing gradient structure:

Figma files with diamond gradients now render correctly. The math is simpler than radial gradients (no sqrt), so it's actually slightly faster.

Sometimes the "missing" feature is just a different distance function. Skia had all the infrastructure—gradient shaders, color interpolation, tile modes. We just needed one new distance calculation.


Read next: Masking: Clips, Effects, and Coordinate Chaos - Getting mask→effect→blend sequencing correct.