Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2);

}

/// <summary>
Expand Down Expand Up @@ -76,6 +94,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2);

}

/// <summary>
Expand Down Expand Up @@ -107,6 +143,24 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
/// <remarks>
/// The channels are floating point, so the blend never truncates to a representable step in the
/// first place and the rounded blend is literally the same operation. Forwarding keeps the two
/// modes from drifting apart.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => this.Lerp(a, b, w1, w2);

}

/// <summary>
Expand All @@ -126,4 +180,14 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork Lerp(in TWork a, in TWork b, int w1, int w2) => a;

/// <inheritdoc />
/// <remarks>Nothing is divided, so there is nothing to round.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => a;

/// <inheritdoc />
/// <remarks>Nothing is divided, so there is nothing to round.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => a;
}
148 changes: 148 additions & 0 deletions System.Drawing.Extensions/ColorProcessing/ColorMath/ColorLerpInt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b)
=> ColorFactory.Create3B<TWork>(
(byte)((a.C1 + b.C1 + 1) >> 1),
(byte)((a.C2 + b.C2 + 1) >> 1),
(byte)((a.C3 + b.C3 + 1) >> 1)
);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var total = w1 + w2;
var half = total >> 1;
return ColorFactory.Create3B<TWork>(
(byte)((a.C1 * w1 + b.C1 * w2 + half) / total),
(byte)((a.C2 * w1 + b.C2 * w2 + half) / total),
(byte)((a.C3 * w1 + b.C3 * w2 + half) / total)
);
}

}

/// <summary>
Expand Down Expand Up @@ -84,6 +105,29 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b)
=> ColorFactory.Create4B<TWork>(
(byte)((a.C1 + b.C1 + 1) >> 1),
(byte)((a.C2 + b.C2 + 1) >> 1),
(byte)((a.C3 + b.C3 + 1) >> 1),
(byte)((a.A + b.A + 1) >> 1)
);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var total = w1 + w2;
var half = total >> 1;
return ColorFactory.Create4B<TWork>(
(byte)((a.C1 * w1 + b.C1 * w2 + half) / total),
(byte)((a.C2 * w1 + b.C2 * w2 + half) / total),
(byte)((a.C3 * w1 + b.C3 * w2 + half) / total),
(byte)((a.A * w1 + b.A * w2 + half) / total)
);
}

}

/// <summary>
Expand Down Expand Up @@ -118,6 +162,31 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b)
=> ColorFactory.Create5B<TWork>(
(byte)((a.C1 + b.C1 + 1) >> 1),
(byte)((a.C2 + b.C2 + 1) >> 1),
(byte)((a.C3 + b.C3 + 1) >> 1),
(byte)((a.C4 + b.C4 + 1) >> 1),
(byte)((a.A + b.A + 1) >> 1)
);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var total = w1 + w2;
var half = total >> 1;
return ColorFactory.Create5B<TWork>(
(byte)((a.C1 * w1 + b.C1 * w2 + half) / total),
(byte)((a.C2 * w1 + b.C2 * w2 + half) / total),
(byte)((a.C3 * w1 + b.C3 * w2 + half) / total),
(byte)((a.C4 * w1 + b.C4 * w2 + half) / total),
(byte)((a.A * w1 + b.A * w2 + half) / total)
);
}

}

/// <summary>
Expand Down Expand Up @@ -158,6 +227,29 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// <see cref="UNorm32.Midpoint"/> already rounds a half-step upwards, so the 50/50 blend is the
/// same operation in both modes and this simply forwards.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var (a1, a2, a3, aa) = a.ToNormalized();
var (b1, b2, b3, ba) = b.ToNormalized();
var total = (ulong)(w1 + w2);
var half = total >> 1;
return ColorFactory.FromNormalized_4<TWork>(
UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)aa.RawValue * (uint)w1 + (ulong)ba.RawValue * (uint)w2 + half) / total))
);
}

}

/// <summary>
Expand Down Expand Up @@ -196,6 +288,28 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// <see cref="UNorm32.Midpoint"/> already rounds a half-step upwards, so the 50/50 blend is the
/// same operation in both modes and this simply forwards.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var (a1, a2, a3) = a.ToNormalized();
var (b1, b2, b3) = b.ToNormalized();
var total = (ulong)(w1 + w2);
var half = total >> 1;
return ColorFactory.FromNormalized_3<TWork>(
UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total))
);
}

}

/// <summary>
Expand Down Expand Up @@ -238,6 +352,30 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
);
}

/// <inheritdoc />
/// <remarks>
/// <see cref="UNorm32.Midpoint"/> already rounds a half-step upwards, so the 50/50 blend is the
/// same operation in both modes and this simply forwards.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => this.Lerp(a, b);

/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) {
var (a1, a2, a3, a4, aa) = a.ToNormalized();
var (b1, b2, b3, b4, ba) = b.ToNormalized();
var total = (ulong)(w1 + w2);
var half = total >> 1;
return ColorFactory.FromNormalized_5<TWork>(
UNorm32.FromRaw((uint)(((ulong)a1.RawValue * (uint)w1 + (ulong)b1.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a2.RawValue * (uint)w1 + (ulong)b2.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a3.RawValue * (uint)w1 + (ulong)b3.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)a4.RawValue * (uint)w1 + (ulong)b4.RawValue * (uint)w2 + half) / total)),
UNorm32.FromRaw((uint)(((ulong)aa.RawValue * (uint)w1 + (ulong)ba.RawValue * (uint)w2 + half) / total))
);
}

}

/// <summary>
Expand All @@ -257,4 +395,14 @@ public TWork Lerp(in TWork a, in TWork b, int w1, int w2) {
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork Lerp(in TWork a, in TWork b, int w1, int w2) => a;

/// <inheritdoc />
/// <remarks>Nothing is divided, so there is nothing to round.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b) => a;

/// <inheritdoc />
/// <remarks>Nothing is divided, so there is nothing to round.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TWork LerpRounded(in TWork a, in TWork b, int w1, int w2) => a;
}
42 changes: 42 additions & 0 deletions System.Drawing.Extensions/ColorProcessing/ColorMath/ILerp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ namespace Hawkynt.ColorProcessing.ColorMath;
/// <para>Used in scaling algorithms and color blending operations.</para>
/// <para>Implementations should operate in linear color space for correct results.</para>
/// <para>Uses integer weights for all interpolation to ensure pure integer operations in fast mode.</para>
/// <para>
/// Each blend comes in two rounding modes. <c>Lerp</c> truncates the division, which is what
/// fixed-point blenders have always done here; <c>LerpRounded</c> rounds to nearest instead. The
/// pair exists because some algorithms are specified against one mode or the other — xBRZ, for
/// example, truncates up to release 1.8 and rounds from 1.9 on — and the caller picks the lerp
/// implementation long before the algorithm gets to say which mode it wants.
/// </para>
/// <para>
/// The rounded members are declared, not defaulted. C# 8 default interface methods would keep the
/// interface source-compatible for outside implementors, but they need runtime support this package
/// cannot assume: net35..net48 do not have it and the compiler rejects them outright (CS8701). The
/// same constraint is why <see cref="Metrics.BatchDistanceDefaults"/> exists. An implementation for
/// which rounding is meaningless — anything in floating point, or a no-op blender — should forward
/// the rounded member to the truncating one.
/// </para>
/// </remarks>
public interface ILerp<T> where T : unmanaged {

Expand All @@ -47,4 +62,31 @@ public interface ILerp<T> where T : unmanaged {
/// <param name="w2">The weight for the second color.</param>
/// <returns>The weighted blend: (a * w1 + b * w2) / (w1 + w2).</returns>
T Lerp(in T a, in T b, int w1, int w2);

/// <summary>
/// Linearly interpolates between two colors at the midpoint (50/50 blend), rounding to nearest.
/// </summary>
/// <param name="a">The first color.</param>
/// <param name="b">The second color.</param>
/// <returns>The color at the midpoint between a and b, rounded rather than truncated.</returns>
/// <remarks>
/// Differs from <see cref="Lerp(in T, in T)"/> only where the midpoint falls between two
/// representable values; for a fixed-point channel that is a difference of at most one step.
/// </remarks>
T LerpRounded(in T a, in T b);

/// <summary>
/// Linearly interpolates between two colors with integer weights, rounding to nearest.
/// </summary>
/// <param name="a">The first color.</param>
/// <param name="b">The second color.</param>
/// <param name="w1">The weight for the first color.</param>
/// <param name="w2">The weight for the second color.</param>
/// <returns>The weighted blend (a * w1 + b * w2) / (w1 + w2), rounded rather than truncated.</returns>
/// <remarks>
/// Differs from <see cref="Lerp(in T, in T, int, int)"/> whenever the weighted sum leaves a
/// non-zero remainder against the total weight. Truncation biases every such blend downwards;
/// rounding does not.
/// </remarks>
T LerpRounded(in T a, in T b, int w1, int w2);
}
Loading
Loading