namespace basegfx
{ // constructor with two colors to explicitly create a // BColorStops for a single StartColor @0.0 & EndColor @1.0
BColorStops::BColorStops(const BColor& rStart, const BColor& rEnd)
{
emplace_back(0.0, rStart);
emplace_back(1.0, rEnd);
}
/* Helper to grep the correct ColorStop out of ColorStopsandinterpolateasneededforgiven relativevalueinfPositionintherangeof[0.0..1.0]. Italsotakescareofevtl.givenRequestedSteps.
*/
BColor BColorStops::getInterpolatedBColor(double fPosition, sal_uInt32 nRequestedSteps,
BColorStopRange& rLastColorStopRange) const
{ // no color at all, done if (empty()) return BColor();
// outside range -> at start constdouble fMin(front().getStopOffset()); if (fPosition < fMin) return front().getStopColor();
// outside range -> at end constdouble fMax(back().getStopOffset()); if (fPosition > fMax) return back().getStopColor();
// special case for the 'classic' case with just two colors: // we can optimize that and keep the speed/resources low // by avoiding some calculations and an O(log(N)) array access if (2 == size())
{ // if same StopOffset use front color if (fTools::equal(fMin, fMax)) return front().getStopColor();
// we need to extend the interpolation to the local // range of ColorStops. Despite having two ColorStops // these are not necessarily at 0.0 and 1.0, so may be // not the classical Start/EndColor (what is allowed)
fPosition = (fPosition - fMin) / (fMax - fMin); return basegfx::interpolate(aCStart, aCEnd,
nSteps > 1 ? floor(fPosition * nSteps) / double(nSteps - 1)
: fPosition);
}
// check if we need to newly populate the needed interpolation data // or if we can re-use from last time. // If this scope is not entered, we do not need the binary search. It's // only a single buffered entry, and only used when more than three // ColorStops exist, but makes a huge difference compared with accessing // the sorted ColorStop vector each time. // NOTE: with this simple change I get very high hit rates, e.g. rotating // a donut with gradient test '1' hit rate is at 0.99909440357755486 if (rLastColorStopRange.mfOffsetStart == rLastColorStopRange.mfOffsetEnd
|| fPosition < rLastColorStopRange.mfOffsetStart
|| fPosition > rLastColorStopRange.mfOffsetEnd)
{ // access needed spot in sorted array using binary search // NOTE: This *seems* slow(er) when developing compared to just // looping/accessing, but that's just due to the extensive // debug test code created by the stl. In a pro version, // all is good/fast as expected constauto upperBound(std::upper_bound(begin(), end(), BColorStop(fPosition),
[](const BColorStop& x, const BColorStop& y) { return x.getStopOffset() < y.getStopOffset();
}));
// no upper bound, done if (end() == upperBound) return back().getStopColor();
// lower bound is one entry back, access that constauto lowerBound(upperBound - 1);
// no lower bound, done if (end() == lowerBound) return back().getStopColor();
// we have lower and upper bound, get colors and offsets
rLastColorStopRange.maColorStart = lowerBound->getStopColor();
rLastColorStopRange.maColorEnd = upperBound->getStopColor();
rLastColorStopRange.mfOffsetStart = lowerBound->getStopOffset();
rLastColorStopRange.mfOffsetEnd = upperBound->getStopOffset();
}
// when there are just two color steps this cannot happen, but when using // a range of colors this *may* be used inside the range to represent // single-colored regions inside a ColorRange. Use that color & done if (rLastColorStopRange.maColorStart == rLastColorStopRange.maColorEnd) return rLastColorStopRange.maColorStart;
// calculate number of steps and adapted proportional // range for scaler in [0.0 .. 1.0] constdouble fAdaptedScaler(
(fPosition - rLastColorStopRange.mfOffsetStart)
/ (rLastColorStopRange.mfOffsetEnd - rLastColorStopRange.mfOffsetStart)); const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
nRequestedSteps, rLastColorStopRange.maColorStart, rLastColorStopRange.maColorEnd));
/* Tooling method that allows to replace the StartColor in a vectorofColorStops.Avectorin'orderedstate'isexpected, soyoumayuse/haveusedsortAndCorrect. Thismethodisforconvenience&backwardscompatibility,please thinkabouthandlingmulti-coloredgradientsdirectly.
*/ void BColorStops::replaceStartColor(const BColor& rStart)
{
BColorStops::iterator a1stNonStartColor(begin());
// search for highest existing non-StartColor - CAUTION, // there might be none, one or multiple with StopOffset 0.0 while (a1stNonStartColor != end() && a1stNonStartColor->getStopOffset() <= 0.0)
a1stNonStartColor++;
// create new ColorStops by 1st adding new one and then all // non-StartColor entries
BColorStops aNewColorStops;
/* Tooling method that allows to replace the EndColor in a vectorofColorStops.Avectorin'orderedstate'isexpected, soyoumayuse/haveusedsortAndCorrectColorStops. Thismethodisforconvenience&backwardscompatibility,please thinkabouthandlingmulti-coloredgradientsdirectly.
*/ void BColorStops::replaceEndColor(const BColor& rEnd)
{ // erase all evtl. existing EndColor(s) while (!empty() && basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0))
pop_back();
// add at the end of existing ColorStops
emplace_back(1.0, rEnd);
}
/* Tooling method to linearly blend the Colors contained in agivenColorStopvectoragainstagivenColorusingthe givenintensityvalues. TheintensityvaluesfStartIntensity,fEndIntensityare intherangeof[0.0..1.0]anddescribehowmuchthe blendissupposedtobedoneatthestartcolorposition andtheendcolorpositionrespectively,where0.0means tofullyusethegivenBlendColor,1.0meanstonotchange theexistingcolorintheColorStop. EverycolorentryinthegivenColorStopisblended relativetoit'sStopPosition,interpolatingthe givenintensitieswiththerange[0.0..1.0]todoso.
*/ void BColorStops::blendToIntensity(double fStartIntensity, double fEndIntensity, const BColor& rBlendColor)
{ // no entries, done if (empty()) return;
// correct intensities (maybe assert when input was wrong)
fStartIntensity = std::max(std::min(1.0, fStartIntensity), 0.0);
fEndIntensity = std::max(std::min(1.0, fEndIntensity), 0.0);
// all 100%, no real blend, done if (basegfx::fTools::equal(fStartIntensity, 1.0) && basegfx::fTools::equal(fEndIntensity, 1.0)) return;
Allthisisdoneinonerun(sort+O(N))without creatingacopyofthedatainanyform
*/ void BColorStops::sortAndCorrect()
{ // no content, we are done if (empty()) return;
if (1 == size())
{ // no gradient at all, but preserve given color // evtl. correct offset to be in valid range [0.0 .. 1.0] // NOTE: This does not move it to 0.0 or 1.0, it *can* still // be somewhere in-between what is allowed const BColorStop aEntry(front());
clear();
emplace_back(std::max(0.0, std::min(1.0, aEntry.getStopOffset())), aEntry.getStopColor());
// done return;
}
// start with sorting the input data. Remember that // this preserves the order of equal entries, where // equal is defined here by offset (see use operator==)
std::sort(begin(), end());
// prepare status values
size_t write(0);
// use the paradigm of a band machine with two heads, read // and write with write <= read all the time. Step over the // data using read and check for valid entry. If valid, decide // how to keep it for (size_t read(0); read < size(); read++)
{ // get offset of entry at read position double fOff((*this)[read].getStopOffset());
if (fOff < 0.0 && read + 1 < size())
{ // value < 0.0 and we have a next entry. check for gradient snippet // containing 0.0 resp. StartColor constdouble fOff2((*this)[read + 1].getStopOffset());
if (fOff2 > 0.0)
{ // read is the start of a gradient snippet containing 0.0. Correct // entry to StartColor, interpolate to correct StartColor
(*this)[read]
= BColorStop(0.0, basegfx::interpolate((*this)[read].getStopColor(),
(*this)[read + 1].getStopColor(),
(0.0 - fOff) / (fOff2 - fOff)));
// adapt fOff
fOff = 0.0;
}
}
// step over < 0 values, these are outside and will be removed if (fOff < 0.0)
{ continue;
}
if (basegfx::fTools::less(fOff, 1.0) && read + 1 < size())
{ // value < 1.0 and we have a next entry. check for gradient snippet // containing 1.0 resp. EndColor constdouble fOff2((*this)[read + 1].getStopOffset());
if (basegfx::fTools::more(fOff2, 1.0))
{ // read is the start of a gradient snippet containing 1.0. Correct // next entry to EndColor, interpolate to correct EndColor
(*this)[read + 1]
= BColorStop(1.0, basegfx::interpolate((*this)[read].getStopColor(),
(*this)[read + 1].getStopColor(),
(1.0 - fOff) / (fOff2 - fOff)));
// adapt fOff
fOff = 1.0;
}
}
// step over > 1 values; even break, since all following // entries will also be bigger due to being sorted, so done if (basegfx::fTools::more(fOff, 1.0))
{ break;
}
// entry is valid value at read position // copy if write target is empty (write at start) or when // write target is different to read in color or offset if (0 == write || !((*this)[read] == (*this)[write - 1]))
{ if (write != read)
{ // copy read to write backwards to close gaps
(*this)[write] = (*this)[read];
}
// always forward write position
write++;
}
}
// correct size when length is reduced. write is always at // last used position + 1 if (size() > write)
{ if (0 == write)
{ // no valid entries at all, but not empty. This can only happen // when all entries are below 0.0 or above 1.0 (else a gradient // snippet spawning over both would have been detected) if (back().getStopOffset() < 0.0)
{ // all outside too low, rescue last due to being closest to content const BColor aBackColor(back().getStopColor());
clear();
emplace_back(0.0, aBackColor);
} else// if (basegfx::fTools::more(front().getStopOffset(), 1.0))
{ // all outside too high, rescue first due to being closest to content const BColor aFrontColor(front().getStopColor());
clear();
emplace_back(1.0, aFrontColor);
}
} else
{
resize(write);
}
}
}
bool BColorStops::checkPenultimate() const
{ // not needed when no ColorStops if (empty()) returnfalse;
// not needed when last ColorStop at the end or outside if (basegfx::fTools::moreOrEqual(back().getStopOffset(), 1.0)) returnfalse;
// get penultimate entry constauto penultimate(rbegin() + 1);
// if there is none, we need no correction and are done if (penultimate == rend()) returnfalse;
// not needed when the last two ColorStops have different offset, then // a visible range will be processed already if (!basegfx::fTools::equal(back().getStopOffset(), penultimate->getStopOffset())) returnfalse;
// not needed when the last two ColorStops have the same Color, then the // range before solves the problem if (back().getStopColor() == penultimate->getStopColor()) returnfalse;
returntrue;
}
/* Tooling method to check if a ColorStop vector is defined byasinglecolor.Itreturnstrueifthisisthecase. Iftrueisreturned,rSingleColorcontainsthatsingle colorforconvenience. NOTE:IfnoColorStopisdefined,afallbacktoBColor-default (whichisblack)andtruewillbereturned
*/ bool BColorStops::isSingleColor(BColor& rSingleColor) const
{ if (empty())
{
rSingleColor = BColor(); returntrue;
}
if (1 == size())
{
rSingleColor = front().getStopColor(); returntrue;
}
rSingleColor = front().getStopColor();
for (autoconst& rCandidate : *this)
{ if (rCandidate.getStopColor() != rSingleColor) returnfalse;
}
returntrue;
}
/* Tooling method to reverse ColorStops, including offsets. Whenalsomirroringoffsetsavalidsortkeepsvalid.
*/ void BColorStops::reverseColorStops()
{ // can use std::reverse, but also need to adapt offset(s)
std::reverse(begin(), end()); for (auto& candidate : *this)
candidate = BColorStop(1.0 - candidate.getStopOffset(), candidate.getStopColor());
}
// createSpaceAtStart creates fOffset space at start by // translating/scaling all entries to the right void BColorStops::createSpaceAtStart(double fOffset)
{ // nothing to do if empty if (empty()) return;
// removeSpaceAtStart removes fOffset space from start by // translating/scaling entries more or equal to fOffset // to the left. Entries less than fOffset will be removed void BColorStops::removeSpaceAtStart(double fOffset)
{ // nothing to do if empty if (empty()) return;
for (constauto& candidate : *this)
{ if (basegfx::fTools::moreOrEqual(candidate.getStopOffset(), fOffset))
{
aNewStops.emplace_back((candidate.getStopOffset() - fOffset) * fMul,
candidate.getStopColor());
}
}
*this = std::move(aNewStops);
}
// try to detect if an empty/no-color-change area exists // at the start and return offset to it. Returns 0.0 if not. double BColorStops::detectPossibleOffsetAtStart() const
{
BColor aSingleColor; constbool bSingleColor(isSingleColor(aSingleColor));
// no useful offset for single color if (bSingleColor) return0.0;
// here we know that we have at least two colors, so we have a // color change. Find colors left and right of that first color change
BColorStops::const_iterator aColorR(begin());
BColorStops::const_iterator aColorL(aColorR++);
// aColorR would 1st get equal to end(), so no need to also check aColorL // for end(). Loop as long as same color. Since we *have* a color change // not even aColorR can get equal to end() before color inequality, but // keep for safety while (aColorR != end() && aColorL->getStopColor() == aColorR->getStopColor())
{
aColorL++;
aColorR++;
}
// also for safety: access values at aColorL below *only* // if not equal to end(), but can theoretically not happen if (aColorL == end())
{ return0.0;
}
// return offset (maybe 0.0 what is OK) return aColorL->getStopOffset();
}
// checks whether the color stops are symmetrical in color and offset. bool BColorStops::isSymmetrical() const
{ if (empty()) returnfalse; if (1 == size()) return basegfx::fTools::equal(0.5, front().getStopOffset());
BColorStops::const_iterator aIter(begin()); // for going forward
BColorStops::const_iterator aRIter(end()); // for going backward
--aRIter; // We have at least two elements, so aIter <= aRIter fails before iterators no longer point to // an element. while (aIter <= aRIter && aIter->getStopColor().equal(aRIter->getStopColor())
&& basegfx::fTools::equal(aIter->getStopOffset(), 1.0 - aRIter->getStopOffset()))
{
++aIter;
--aRIter;
} return aIter > aRIter;
}
void BColorStops::doApplyAxial()
{ // prepare new ColorStops
basegfx::BColorStops aNewColorStops;
// add gradient stops in reverse order, scaled to [0.0 .. 0.5]
basegfx::BColorStops::const_reverse_iterator aRevCurrColor(rbegin());
// prepare non-reverse run
basegfx::BColorStops::const_iterator aCurrColor(begin());
if (basegfx::fTools::equalZero(aCurrColor->getStopOffset()))
{ // Caution: do not add 1st entry again, that would be double since it was // already added as last element of the inverse run above. But only if // the gradient has a start entry for 0.0 aka StartColor, else it is correct.
aCurrColor++;
}
// add gradient stops in non-reverse order, translated and scaled to [0.5 .. 1.0] while (aCurrColor != end())
{
aNewColorStops.emplace_back((aCurrColor->getStopOffset() * 0.5) + 0.5,
aCurrColor->getStopColor());
aCurrColor++;
}
// apply color stops
*this = std::move(aNewColorStops);
}
void BColorStops::doApplySteps(sal_uInt16 nStepCount)
{ // check for zero or invalid steps setting -> done if (0 == nStepCount || nStepCount > 100) return;
// no change needed if single color
BColor aSingleColor; if (isSingleColor(aSingleColor)) return;
// prepare new color stops, get L/R iterators for segments
basegfx::BColorStops aNewColorStops;
basegfx::BColorStops::const_iterator aColorR(begin());
basegfx::BColorStops::const_iterator aColorL(aColorR++);
while (aColorR != end())
{ // get start/end color for segment constdouble fStart(aColorL->getStopOffset()); constdouble fDelta(aColorR->getStopOffset() - fStart);
if (aNewColorStops.empty() || aNewColorStops.back() != *aColorL)
{ // add start color, but check if it is already there - which is the // case from the 2nd segment on due to a new segment starting with // the same color as the previous one ended
aNewColorStops.push_back(*aColorL);
} if (!basegfx::fTools::equalZero(fDelta))
{ // create in-between steps, always two at the same position to // define a 'hard' color stop. Get start/end color for the segment const basegfx::BColor& rStartColor(aColorL->getStopColor()); const basegfx::BColor& rEndColor(aColorR->getStopColor());
if (rStartColor != rEndColor)
{ // get relative single-step width // tdf155852 Use same method for the color as in rendering. constdouble fSingleStep(1.0 / static_cast<double>(nStepCount - 1)); constdouble fOffsetStep(fDelta / static_cast<double>(nStepCount));
for (sal_uInt16 a(1); a < nStepCount; a++)
{ // calculate stop position since being used twice constdouble fPosition(fStart + fOffsetStep * static_cast<double>(a));
// add end color of previous sub-segment
aNewColorStops.emplace_back(
fPosition, basegfx::interpolate(rStartColor, rEndColor, static_cast<double>(a - 1) * fSingleStep));
// add start color of current sub-segment
aNewColorStops.emplace_back(
fPosition, basegfx::interpolate(rStartColor, rEndColor, static_cast<double>(a) * fSingleStep));
}
}
}
// always add end color of segment
aNewColorStops.push_back(*aColorR);
// next segment
aColorL++;
aColorR++;
}
// apply the change to color stops
*this = std::move(aNewColorStops);
}
void BColorStops::tryToApplyBColorModifierStack(const BColorModifierStack& rBColorModifierStack)
{ if (0 == rBColorModifierStack.count()) // no content on stack, done return;
void BGradient::tryToRecreateBorder(basegfx::BColorStops* pAssociatedTransparencyStops)
{ // border already set, do not try to recreate if (0 != GetBorder()) return;
if (bIsAxial)
{ // for axial due to reverse used gradient work reversed
aColorStops.reverseColorStops(); if (nullptr != pAssociatedTransparencyStops)
pAssociatedTransparencyStops->reverseColorStops();
}
// check if we have space at start of range [0.0 .. 1.0] that // may be interpreted as 'border' -> same color. That may involve // different scenarios, e.g. 1st index > 0.0, but also a non-zero // number of same color entries, or a combination of both constdouble fOffset(aColorStops.detectPossibleOffsetAtStart());
if (!basegfx::fTools::equalZero(fOffset))
{ // we have a border area, indeed re-create
aColorStops.removeSpaceAtStart(fOffset); if (nullptr != pAssociatedTransparencyStops)
pAssociatedTransparencyStops->removeSpaceAtStart(fOffset);
// ...and create border value
SetBorder(static_cast<sal_uInt16>(std::lround(fOffset * 100.0)));
}
if (bIsAxial)
{ // take back reverse
aColorStops.reverseColorStops(); if (nullptr != pAssociatedTransparencyStops)
pAssociatedTransparencyStops->reverseColorStops();
}
}
void BGradient::tryToApplyBorder()
{ // no border to apply, done if (0 == GetBorder()) return;
// NOTE: no new start node is added. The new ColorStop // mechanism does not need entries at 0.0 and 1.0. // In case this is needed, do that in the caller constdouble fOffset(GetBorder() * 0.01);
if (css::awt::GradientStyle_AXIAL == GetGradientStyle())
{ // for axial due to reverse used gradient work reversed
aColorStops.reverseColorStops();
aColorStops.createSpaceAtStart(fOffset);
aColorStops.reverseColorStops();
} else
{ // apply border to GradientStops
aColorStops.createSpaceAtStart(fOffset);
}
// set changed values
SetBorder(0);
}
void BGradient::tryToApplyStartEndIntensity()
{ // already on default, nothing to apply if (100 == GetStartIntens() && 100 == GetEndIntens()) return;
// apply 'old' blend stuff, blend against black
aColorStops.blendToIntensity(GetStartIntens() * 0.01, GetEndIntens() * 0.01,
BColor()); // COL_BLACK
// set values to default
SetStartIntens(100);
SetEndIntens(100);
}
// Stretch the first half of the color stops to double width // and collect them in a new color stops vector.
BColorStops aAxialColorStops;
aAxialColorStops.reserve(std::ceil(GetColorStops().size() / 2.0));
BColorStops::const_iterator aIter(GetColorStops().begin()); while (basegfx::fTools::lessOrEqual(aIter->getStopOffset(), 0.5))
{
BColorStop aNextStop(std::clamp((*aIter).getStopOffset() * 2.0, 0.0, 1.0),
(*aIter).getStopColor());
aAxialColorStops.push_back(aNextStop);
++aIter;
} // Axial gradients have outmost color as last color stop.
aAxialColorStops.reverseColorStops();
SetColorStops(aAxialColorStops);
}
void BGradient::tryToApplyAxial()
{ // only need to do something if css::awt::GradientStyle_AXIAL, else done if (GetGradientStyle() != css::awt::GradientStyle_AXIAL) return;
// apply the change to color stops
aColorStops.doApplyAxial();
// set style to GradientStyle_LINEAR
SetGradientStyle(css::awt::GradientStyle_LINEAR);
}
void BGradient::tryToApplySteps()
{ // check for zero or invalid steps setting -> done if (0 == GetSteps() || GetSteps() > 100) return;
// do the action
aColorStops.doApplySteps(GetSteps());