Skip to content

Commit

Permalink
Bug 1793105 - Update COLRv1 radial-gradient handling of negative radi…
Browse files Browse the repository at this point in the history
…i. r=gfx-reviewers,lsalzman

This implements the adjusted behavior agreed with Dominik in
googlefonts/colr-gradients-spec#367
and email discussion. Dominik is updating Blink to have the
same behavior.

Differential Revision: https://phabricator.services.mozilla.com/D158429
  • Loading branch information
jfkthame committed Oct 3, 2022
1 parent e67aee5 commit 43c339c
Showing 1 changed file with 201 additions and 33 deletions.
234 changes: 201 additions & 33 deletions gfx/thebes/COLRFonts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -628,46 +628,59 @@ struct ColorLineT {
aState.GetColor(stop->GetPaletteIndex(), stop->GetAlpha(aState)));
}

already_AddRefed<GradientStops> MakeGradientStops(
const PaintState& aState, float* aFirstStop = nullptr,
float* aLastStop = nullptr, bool aReverse = false) const {
// Retrieve the color stops into an array of GradientStop records. The stops
// are normalized to the range [0 .. 1], and the original offsets of the
// first and last stops are returned.
// If aReverse is true, the color line is reversed.
void CollectGradientStops(const PaintState& aState,
nsTArray<GradientStop>& aStops, float* aFirstStop,
float* aLastStop, bool aReverse = false) const {
MOZ_ASSERT(aStops.IsEmpty());
uint16_t count = numStops;
if (!count) {
return nullptr;
return;
}
AutoTArray<GradientStop, 8> stops;
const auto* stop = colorStops();
if (reinterpret_cast<const char*>(stop) + count * sizeof(T) >
aState.COLRv1BaseAddr() + aState.mCOLRLength) {
return nullptr;
return;
}
stops.SetCapacity(count);
aStops.SetCapacity(count);
for (uint16_t i = 0; i < count; ++i, ++stop) {
DeviceColor color =
aState.GetColor(stop->GetPaletteIndex(), stop->GetAlpha(aState));
stops.AppendElement(GradientStop{stop->GetStopOffset(aState), color});
aStops.AppendElement(GradientStop{stop->GetStopOffset(aState), color});
}
if (count == 1) {
*aFirstStop = *aLastStop = aStops[0].offset;
return;
}
stops.StableSort(nsDefaultComparator<GradientStop, GradientStop>());
aStops.StableSort(nsDefaultComparator<GradientStop, GradientStop>());
if (aReverse) {
float a = stops[0].offset;
float b = stops.LastElement().offset;
stops.Reverse();
for (auto& gs : stops) {
float a = aStops[0].offset;
float b = aStops.LastElement().offset;
aStops.Reverse();
for (auto& gs : aStops) {
gs.offset = a + b - gs.offset;
}
}
if (aFirstStop && aLastStop) {
// Normalize stops to the range 0.0 .. 1.0, and return the original
// start & end.
*aFirstStop = stops[0].offset;
*aLastStop = stops.LastElement().offset;
if (*aLastStop > *aFirstStop) {
float f = 1.0f / (*aLastStop - *aFirstStop);
for (auto& gs : stops) {
gs.offset = (gs.offset - *aFirstStop) * f;
}
// Normalize stops to the range 0.0 .. 1.0, and return the original
// start & end.
*aFirstStop = aStops[0].offset;
*aLastStop = aStops.LastElement().offset;
if ((*aLastStop > *aFirstStop) &&
(*aLastStop != 1.0f || *aFirstStop != 0.0f)) {
float f = 1.0f / (*aLastStop - *aFirstStop);
for (auto& gs : aStops) {
gs.offset = (gs.offset - *aFirstStop) * f;
}
}
}

// Create a gfx::GradientStops representing the given color line stops,
// applying our extend mode.
already_AddRefed<GradientStops> MakeGradientStops(
const PaintState& aState, nsTArray<GradientStop>& aStops) const {
auto mapExtendMode = [](uint8_t aExtend) -> ExtendMode {
switch (aExtend) {
case EXTEND_REPEAT:
Expand All @@ -680,7 +693,18 @@ struct ColorLineT {
}
};
return aState.mDrawTarget->CreateGradientStops(
stops.Elements(), stops.Length(), mapExtendMode(extend));
aStops.Elements(), aStops.Length(), mapExtendMode(extend));
}

already_AddRefed<GradientStops> MakeGradientStops(
const PaintState& aState, float* aFirstStop, float* aLastStop,
bool aReverse = false) const {
AutoTArray<GradientStop, 8> stops;
CollectGradientStops(aState, stops, aFirstStop, aLastStop, aReverse);
if (stops.IsEmpty()) {
return nullptr;
}
return MakeGradientStops(aState, stops);
}
};

Expand Down Expand Up @@ -919,6 +943,81 @@ struct PaintRadialGradient : public PaintPatternBase {
return NormalizeAndMakeGradient(aState, colorLine, c1, c2, r1, r2);
}

// Helper function to trim the gradient stops array at the start or end.
void TruncateGradientStops(nsTArray<GradientStop>& aStops, float aStart,
float aEnd) const {
// For pad mode, we may need a sub-range of the line: figure out which
// stops to trim, and interpolate as needed at truncation point(s).
// (Currently this is only ever used to trim one end of the color line,
// so edge cases that may occur when trimming both ends are untested.)
MOZ_ASSERT(aStart == 0.0f || aEnd == 1.0f,
"Trimming both ends of color-line is untested!");

// Create a color that is |r| of the way from c1 to c2.
auto interpolateColor = [](DeviceColor c1, DeviceColor c2, float r) {
return DeviceColor(
c2.r * r + c1.r * (1.0f - r), c2.g * r + c1.g * (1.0f - r),
c2.b * r + c1.b * (1.0f - r), c2.a * r + c1.a * (1.0f - r));
};

size_t count = aStops.Length();
MOZ_ASSERT(count > 1);

// Truncate at the start of the color line?
if (aStart > 0.0f) {
// Skip forward past any stops that can be dropped.
size_t i = 0;
while (i < count - 1 && aStops[i].offset < aStart) {
++i;
}
// If we're not truncating exactly at a color-stop offset, shift the
// preceding stop to the truncation offset and interpolate its color.
if (i && aStops[i].offset > aStart) {
auto& prev = aStops[i - 1];
auto& curr = aStops[i];
float ratio = (aStart - prev.offset) / (curr.offset - prev.offset);
prev.color = interpolateColor(prev.color, curr.color, ratio);
prev.offset = aStart;
--i; // We don't want to remove this stop, as we adjusted it.
}
aStops.RemoveElementsAt(0, i);
// Re-normalize the remaining stops to the [0, 1] range.
if (aStart < 1.0f) {
float r = 1.0f / (1.0f - aStart);
for (auto& gs : aStops) {
gs.offset = r * (gs.offset - aStart);
}
}
}

// Truncate at the end of the color line?
if (aEnd < 1.0f) {
// Skip back over any stops that can be dropped.
size_t i = count - 1;
while (i && aStops[i].offset > aEnd) {
--i;
}
// If we're not truncating exactly at a color-stop offset, shift the
// following stop to the truncation offset and interpolate its color.
if (i + 1 < count && aStops[i].offset < aEnd) {
auto& next = aStops[i + 1];
auto& curr = aStops[i];
float ratio = (aEnd - curr.offset) / (next.offset - curr.offset);
next.color = interpolateColor(curr.color, next.color, ratio);
next.offset = aEnd;
++i;
}
aStops.RemoveElementsAt(i + 1, count - i - 1);
// Re-normalize the remaining stops to the [0, 1] range.
if (aEnd > 0.0f) {
float r = 1.0f / aEnd;
for (auto& gs : aStops) {
gs.offset = r * gs.offset;
}
}
}
}

template <typename T>
UniquePtr<Pattern> NormalizeAndMakeGradient(const PaintState& aState,
const T* aColorLine, Point c1,
Expand All @@ -932,26 +1031,95 @@ struct PaintRadialGradient : public PaintPatternBase {
return solidColor;
}
float firstStop, lastStop;
RefPtr stops = aColorLine->MakeGradientStops(aState, &firstStop, &lastStop);
if (!stops) {
return nullptr;
AutoTArray<GradientStop, 8> stopArray;
aColorLine->CollectGradientStops(aState, stopArray, &firstStop, &lastStop);
if (stopArray.IsEmpty()) {
return MakeUnique<ColorPattern>(DeviceColor());
}
if (firstStop != 0.0 || lastStop != 1.0) {
// If the color stop offsets had to be normalized to the [0, 1] range,
// adjust the circle positions and radii to match.
if (firstStop != 0.0f || lastStop != 1.0f) {
if (firstStop == lastStop) {
if (aColorLine->extend != T::EXTEND_PAD) {
return MakeUnique<ColorPattern>(DeviceColor());
}
} else {
Point v = c2 - c1;
c1 += v * firstStop;
c2 -= v * (1.0f - lastStop);
// Adjust centers along the vector between them, and scale radii for
// gradient line defined from 0.0 to 1.0.
Point vec = c2 - c1;
c1 += vec * firstStop;
c2 -= vec * (1.0f - lastStop);
float deltaR = r2 - r1;
r1 = r1 + deltaR * firstStop;
r2 = r2 - deltaR * (1.0f - lastStop);
}
}
return MakeUnique<RadialGradientPattern>(c1, c2, fabsf(r1), fabsf(r2),
std::move(stops),
if ((r1 < 0.0f || r2 < 0.0f) && aColorLine->extend == T::EXTEND_PAD) {
// For EXTEND_PAD, we can restrict the gradient definition to just its
// visible portion because the shader doesn't need to see any part of the
// color line that extends into the negative-radius "virtual cone".
if (r1 < 0.0f && r2 < 0.0f) {
// If both radii are negative, then only the color at the closer circle
// will appear in the projected positive cone (or if they're equal,
// nothing will be visible at all).
if (r1 == r2) {
return MakeUnique<ColorPattern>(DeviceColor());
}
// The defined range of the color line is entirely in the invisible
// cone; all that will project into visible space is a single color.
if (r1 < r2) {
// Keep only the last color stop.
TruncateGradientStops(stopArray, 1.0f, 1.0f);
} else {
// Keep only the first color stop.
TruncateGradientStops(stopArray, 0.0f, 0.0f);
}
} else {
// Truncate the gradient at the tip of the visible cone: find the color
// stops closest to that point and interpolate between them.
if (r1 < r2) {
float start = r1 / (r1 - r2);
TruncateGradientStops(stopArray, start, 1.0f);
r1 = 0.0f;
c1 = c1 * (1.0f - start) + c2 * start;
} else if (r2 < r1) {
float end = 1.0f - r2 / (r2 - r1);
TruncateGradientStops(stopArray, 0.0f, end);
r2 = 0.0f;
c2 = c1 * (1.0f - end) + c2 * end;
}
}
}
// Handle negative radii, which the shader won't understand directly, by
// projecting the circles along the cones such that both radii are positive.
if (r1 < 0.0f || r2 < 0.0f) {
float deltaR = r2 - r1;
// If deltaR is zero, then nothing is visible because the cone has
// degenerated into a negative-radius cylinder, and does not project
// into visible space at all.
if (deltaR == 0.0f) {
return MakeUnique<ColorPattern>(DeviceColor());
}
Point vec = c2 - c1;
if (aColorLine->extend == T::EXTEND_REFLECT) {
deltaR *= 2.0f;
vec = vec * 2.0f;
}
if (r2 < r1) {
vec = -vec;
deltaR = -deltaR;
}
// Number of repeats by which we need to shift.
float n = std::ceil(std::max(-r1, -r2) / deltaR);
deltaR *= n;
r1 += deltaR;
r2 += deltaR;
vec = vec * n;
c1 += vec;
c2 += vec;
}
RefPtr stops = aColorLine->MakeGradientStops(aState, stopArray);
return MakeUnique<RadialGradientPattern>(c1, c2, r1, r2, std::move(stops),
Matrix::Scaling(1.0, -1.0));
}
};
Expand Down

0 comments on commit 43c339c

Please sign in to comment.