basegfx/source/tools/gradienttools.cxx | 101 +++++++++++++++- drawinglayer/source/attribute/fillgradientattribute.cxx | 40 ++++-- drawinglayer/source/texture/texture.cxx | 95 +++++++++------ include/basegfx/utils/gradienttools.hxx | 23 +++ svx/source/sdr/primitive2d/sdrattributecreator.cxx | 94 +++++++++++++- 5 files changed, 292 insertions(+), 61 deletions(-)
New commits: commit c4f252035cd71f9a1b226e69095cc9f4e8f19f3e Author: Armin Le Grand (allotropia) <armin.le.grand.ext...@allotropia.de> AuthorDate: Tue Feb 21 18:35:39 2023 +0100 Commit: Armin Le Grand <armin.le.gr...@me.com> CommitDate: Wed Feb 22 09:13:20 2023 +0000 MCGR: Adapted GradientLinear to make use of MCGR Added to make GradientLinear work using the MCGR as 1st of six types. Had to do quite some tickeling to get it all work, but looks good. Five more to go, already started to put some things to tooling to make re-usable for the other types. Besides adapting this the main change is that the adaption of defined step-count (versus automatic) has to be done in modifyBColor now instead of the back-mapping methods (e.g. getLinearGradientAlpha). It is still 100% backward-compatible, so as long as there is no source using this it will stay invisible - by purpose. I started to do quite some tests (and fixes/ adaptions in consequence), see the static variable nUseGradientSteps. If you want to play with this, you might set it to '1' instead of '0' and use a linear gradient on an object. Change-Id: I9d61934defb0674456247f2879f0a89b6a5e50f7 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/147413 Tested-by: Jenkins Reviewed-by: Armin Le Grand <armin.le.gr...@me.com> diff --git a/basegfx/source/tools/gradienttools.cxx b/basegfx/source/tools/gradienttools.cxx index b2635b3f09cc..43cca3f1c7ea 100644 --- a/basegfx/source/tools/gradienttools.cxx +++ b/basegfx/source/tools/gradienttools.cxx @@ -21,6 +21,8 @@ #include <basegfx/point/b2dpoint.hxx> #include <basegfx/range/b2drange.hxx> #include <basegfx/matrix/b2dhommatrixtools.hxx> + +#include <algorithm> #include <cmath> namespace basegfx @@ -261,6 +263,95 @@ namespace basegfx namespace utils { + BColor modifyBColor( + const ColorSteps& rColorSteps, + double fScaler, + sal_uInt32 nRequestedSteps) + { + // no color at all, done + if (rColorSteps.empty()) + return BColor(); + + // outside range -> at start + if (fScaler <= 0.0) + return rColorSteps.front().getColor(); + + // outside range -> at end + if (fScaler >= 1.0) + return rColorSteps.back().getColor(); + + // special case for the 'classic' case with just two colors: + // we can optimize that and keep the speed/ressources low + // by avoiding some calculatins and an O(log(N)) array access + if (2 == rColorSteps.size()) + { + const basegfx::BColor aCStart(rColorSteps.front().getColor()); + const basegfx::BColor aCEnd(rColorSteps.back().getColor()); + const sal_uInt32 nSteps( + calculateNumberOfSteps( + nRequestedSteps, + aCStart, + aCEnd)); + + return basegfx::interpolate( + aCStart, + aCEnd, + nSteps > 1 ? floor(fScaler * nSteps) / double(nSteps - 1) : fScaler); + } + + // 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 + const auto upperBound( + std::lower_bound( + rColorSteps.begin(), + rColorSteps.end(), + ColorStep(fScaler), + [](const ColorStep& x, const ColorStep& y) { return x.getOffset() < y.getOffset(); })); + + // no upper bound, done + if (rColorSteps.end() == upperBound) + return rColorSteps.back().getColor(); + + // lower bound is one entry back + const auto lowerBound(upperBound - 1); + + // no lower bound, done + if (rColorSteps.end() == lowerBound) + return rColorSteps.back().getColor(); + + // we have lower and upper bound, get colors + const BColor aCStart(lowerBound->getColor()); + const BColor aCEnd(upperBound->getColor()); + + // 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 (aCStart == aCEnd) + return aCStart; + + // calculate number of steps + const sal_uInt32 nSteps( + calculateNumberOfSteps( + nRequestedSteps, + aCStart, + aCEnd)); + + // get offsets and scale to new [0.0 .. 1.0] relative range for + // partial outer range + const double fOffsetStart(lowerBound->getOffset()); + const double fOffsetEnd(upperBound->getOffset()); + const double fAdaptedScaler((fScaler - fOffsetStart) / (fOffsetEnd - fOffsetStart)); + + // interpolate & evtl. apply steps + return interpolate( + aCStart, + aCEnd, + nSteps > 1 ? floor(fAdaptedScaler * nSteps) / double(nSteps - 1) : fAdaptedScaler); + } + sal_uInt32 calculateNumberOfSteps( sal_uInt32 nRequestedSteps, const BColor& rStart, @@ -392,12 +483,12 @@ namespace basegfx return 1.0; // end value for outside } - const sal_uInt32 nSteps(rGradInfo.getRequestedSteps()); + // const sal_uInt32 nSteps(rGradInfo.getRequestedSteps()); - if(nSteps) - { - return floor(aCoor.getY() * nSteps) / double(nSteps - 1); - } + // if(nSteps) + // { + // return floor(aCoor.getY() * nSteps) / double(nSteps - 1); + // } return aCoor.getY(); } diff --git a/drawinglayer/source/attribute/fillgradientattribute.cxx b/drawinglayer/source/attribute/fillgradientattribute.cxx index c653a6e84057..2056d58acf7f 100644 --- a/drawinglayer/source/attribute/fillgradientattribute.cxx +++ b/drawinglayer/source/attribute/fillgradientattribute.cxx @@ -59,32 +59,45 @@ namespace drawinglayer::attribute // if we have given ColorSteps, integrate these if(nullptr != pColorSteps && !pColorSteps->empty()) { - // append early & sort to local to prepare processing and correction(s) + // append early to local & sort to prepare the following correction(s) + // and later processing maColorSteps.insert(maColorSteps.end(), pColorSteps->begin(), pColorSteps->end()); - std::sort(maColorSteps.begin(), maColorSteps.end()); + + // no need to sort knowingly lowest entry StartColor, also guarantees that + // entry to stay at begin + std::sort(maColorSteps.begin() + 1, maColorSteps.end()); // use two r/w heads on the data band maColorSteps size_t curr(0), next(1); - // check if all colors are the same. We know the StartColor, so - // to all be the same all have to be equal to StartColor, including - // EndColor + // during procesing, check if all colors are the same. We know the + // StartColor, so to all be the same, all also have to be equal to + // StartColor (including EndColor, use to initialize) bool bAllTheSameColor(rStartColor == rEndColor); - // remove entries < 0.0, > 1.0 and with equal offset. do + // remove entries <= 0.0, >= 1.0 and with equal offset. do // this inside the already sorted local vector by evtl. - // moving entries towards begin to keep and adapting size + // moving entries closer to begin to keep and adapting size // at the end for(; next < maColorSteps.size(); next++) { const double fNextOffset(maColorSteps[next].getOffset()); - if(basegfx::fTools::less(fNextOffset, 0.0)) + // check for < 0.0 (should not really happen, see ::ColorStep) + // also check for == 0.0 which would mean than an implicit + // StartColor was given in ColorSteps - ignore that, we want + // the explicitely given StartColor to always win + if(basegfx::fTools::lessOrEqual(fNextOffset, 0.0)) continue; - if(basegfx::fTools::more(fNextOffset, 1.0)) + // check for > 1.0 (should not really happen, see ::ColorStep) + // also check for == 1.0 which would mean than an implicit + // EndColor was given in ColorSteps - ignore that, we want + // the explicitely given EndColor to always win + if(basegfx::fTools::moreOrEqual(fNextOffset, 1.0)) continue; + // check for equal current offset const double fCurrOffset(maColorSteps[curr].getOffset()); if(basegfx::fTools::equal(fNextOffset, fCurrOffset)) continue; @@ -97,18 +110,18 @@ namespace drawinglayer::attribute maColorSteps[curr] = maColorSteps[next]; } - // new entry added, check it for all the same color + // new valid entry detected, check it for all the same color bAllTheSameColor = bAllTheSameColor && maColorSteps[curr].getColor() == rStartColor; } if(bAllTheSameColor) { - // if all the same, reset to StartColor only + // if all are the same color, reset to StartColor only maColorSteps.resize(1); } else { - // adapt size to useful entries + // adapt size to detected useful entries curr++; if(curr != maColorSteps.size()) { @@ -116,7 +129,8 @@ namespace drawinglayer::attribute } // add EndColor if in-between colors were added - if(curr > 1) + // or StartColor != EndColor + if(curr > 1 || rStartColor != rEndColor) { maColorSteps.emplace_back(1.0, rEndColor); } diff --git a/drawinglayer/source/texture/texture.cxx b/drawinglayer/source/texture/texture.cxx index 623336187495..326d3bbe9146 100644 --- a/drawinglayer/source/texture/texture.cxx +++ b/drawinglayer/source/texture/texture.cxx @@ -134,17 +134,23 @@ namespace drawinglayer::texture std::vector< B2DHomMatrixAndBColor >& rEntries, basegfx::BColor& rOuterColor) { - if(mnColorSteps.size() <= 1) + // no color at all, done + if (mnColorSteps.empty()) return; - const basegfx::BColor maStart(mnColorSteps.front().getColor()); - const basegfx::BColor maEnd(mnColorSteps.back().getColor()); - const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps( - maGradientInfo.getRequestedSteps(), maStart, maEnd)); + // get start color and also cc to rOuterColor + basegfx::BColor aCStart(mnColorSteps[0].getColor()); + rOuterColor = aCStart; - rOuterColor = maStart; - const double fStripeWidth(1.0 / nSteps); - B2DHomMatrixAndBColor aB2DHomMatrixAndBColor; + // only one color, done + if (mnColorSteps.size() < 2) + return; + + // here we could check that fOffsetStart == mnColorSteps[0].getOffset(), + // but just assume/correct that StartColor is at 0.0 + double fOffsetStart(0.0 /*mnColorSteps[0].getOffset()*/); + + // prepare unit range transform basegfx::B2DHomMatrix aPattern; // bring from unit circle [-1, -1, 1, 1] to unit range [0, 0, 1, 1] @@ -155,47 +161,72 @@ namespace drawinglayer::texture aPattern.scale(mfUnitWidth, 1.0); aPattern.translate(mfUnitMinX, 0.0); - for(sal_uInt32 a(1); a < nSteps; a++) + for (size_t outerLoop(1); outerLoop < mnColorSteps.size(); outerLoop++) { - const double fPos(fStripeWidth * a); - basegfx::B2DHomMatrix aNew(aPattern); + const basegfx::BColor aCEnd(mnColorSteps[outerLoop].getColor()); + const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps( + maGradientInfo.getRequestedSteps(), aCStart, aCEnd)); + const double fOffsetEnd(mnColorSteps[outerLoop].getOffset()); - // scale and translate in Y - double fHeight(1.0 - fPos); + // nSteps is >= 1, see getRequestedSteps, so no check needed here + const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps); - if(a + 1 == nSteps && mfUnitMaxY > 1.0) + // for the 1st color range we do not need to create the 1st step + // since it will be equal to StartColor and thus rOuterColor, so + // will be painted by the 1st, always-created background polygon + // colored using rOuterColor. + // We *need* to create this though for all 'inner' color ranges + // to get a correct start + const sal_uInt32 nStartInnerLoop(1 == outerLoop ? 1 : 0); + + for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++) { - fHeight += mfUnitMaxY - 1.0; - } + // calculate pos in Y + const double fPos(fOffsetStart + (fStripeWidth * innerLoop)); - aNew.scale(1.0, fHeight); - aNew.translate(0.0, fPos); + // scale and translate in Y. For GradientLinear we always have + // the full height + double fHeight(1.0 - fPos); - // set at target - aB2DHomMatrixAndBColor.maB2DHomMatrix = maGradientInfo.getTextureTransform() * aNew; + if (mfUnitMaxY > 1.0) + { + // extend when difference between definition and OutputRange exists + fHeight += mfUnitMaxY - 1.0; + } - // interpolate and set color - aB2DHomMatrixAndBColor.maBColor = interpolate(maStart, maEnd, double(a) / double(nSteps - 1)); + basegfx::B2DHomMatrix aNew(aPattern); + aNew.scale(1.0, fHeight); + aNew.translate(0.0, fPos); - rEntries.push_back(aB2DHomMatrixAndBColor); + // set and add at target + B2DHomMatrixAndBColor aB2DHomMatrixAndBColor; + + aB2DHomMatrixAndBColor.maB2DHomMatrix = maGradientInfo.getTextureTransform() * aNew; + aB2DHomMatrixAndBColor.maBColor = interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1)); + rEntries.push_back(aB2DHomMatrixAndBColor); + } + + aCStart = aCEnd; + fOffsetStart = fOffsetEnd; } } void GeoTexSvxGradientLinear::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const { - if(mnColorSteps.size() <= 1) + // no color at all, done + if (mnColorSteps.empty()) + return; + + // just single color, done + if (mnColorSteps.size() < 2) { rBColor = mnColorSteps.front().getColor(); + return; } - else - { - const double fScaler(basegfx::utils::getLinearGradientAlpha(rUV, maGradientInfo)); - - const basegfx::BColor maStart(mnColorSteps.front().getColor()); - const basegfx::BColor maEnd(mnColorSteps.back().getColor()); - rBColor = basegfx::interpolate(maStart, maEnd, fScaler); - } + // texture-back-transform X/Y -> t [0.0..1.0] and determine color + const double fScaler(basegfx::utils::getLinearGradientAlpha(rUV, maGradientInfo)); + rBColor = basegfx::utils::modifyBColor(mnColorSteps, fScaler, mnRequestedSteps); } GeoTexSvxGradientAxial::GeoTexSvxGradientAxial( diff --git a/include/basegfx/utils/gradienttools.hxx b/include/basegfx/utils/gradienttools.hxx index 289fc730b790..793d07a75577 100644 --- a/include/basegfx/utils/gradienttools.hxx +++ b/include/basegfx/utils/gradienttools.hxx @@ -58,14 +58,18 @@ namespace basegfx class UNLESS_MERGELIBS(BASEGFX_DLLPUBLIC) ColorStep { private: + // offset in the range of [0.0 .. 1.0], checked & force by constructor double mfOffset; + + // color of ColorStep entry BColor maColor; public: // constructor - defaults are needed to have a default constructor // e.g. for usage in std::vector::insert + // ensure [0.0 .. 1.0] range for mfOffset ColorStep(double fOffset = 0.0, const BColor& rColor = BColor()) - : mfOffset(fOffset) + : mfOffset(std::max(0.0, std::min(fOffset, 1.0))) , maColor(rColor) { } @@ -86,9 +90,10 @@ namespace basegfx /* MCGR: Provide ColorSteps definition to the FillGradientAttribute - This array is sorted ascending by offsets, from lowest to - highest. Since all this primitive data definition is read-only, - this can be guaranteed by forcing/checking this in the constructor. + This array should be sorted ascending by offsets, from lowest to + highest. Since all the primitive data definition where it is used + is read-only, this can/will be guaranteed by forcing/checking this + in the constructor, see ::FillGradientAttribute */ typedef std::vector<ColorStep> ColorSteps; @@ -179,6 +184,16 @@ namespace basegfx namespace utils { + /* Helper to grep the correct ColorStep out of + ColorSteps and interpolate as needed for given + relative value in fScaler in the range of [0.0 .. 1.0]. + It also takes care of evtl. given RequestedSteps. + */ + BASEGFX_DLLPUBLIC BColor modifyBColor( + const ColorSteps& rColorSteps, + double fScaler, + sal_uInt32 nRequestedSteps); + /* Helper to calculate numberOfSteps needed to represent gradient for the given two colors: - to define only based on color distance, give 0 == nRequestedSteps diff --git a/svx/source/sdr/primitive2d/sdrattributecreator.cxx b/svx/source/sdr/primitive2d/sdrattributecreator.cxx index 434be3ee3197..048d5430f6f8 100644 --- a/svx/source/sdr/primitive2d/sdrattributecreator.cxx +++ b/svx/source/sdr/primitive2d/sdrattributecreator.cxx @@ -495,13 +495,93 @@ namespace drawinglayer::primitive2d basegfx::ColorSteps aColorSteps; - // test code here, will be removed later - // if(false) - // { - // aStart = basegfx::BColor(1.0, 0.0, 0.0); // red - // aEnd = basegfx::BColor(0.0, 0.0, 1.0); // blue - // aColorSteps.emplace_back(0.25, basegfx::BColor(0.0, 1.0, 0.0)); // green@25% - // } + // test code here, can/will be removed later + static sal_uInt32 nUseGradientSteps(0); + switch(nUseGradientSteps) + { + case 1: + { + // just test a nice valid gradient + aStart = basegfx::BColor(1.0, 0.0, 0.0); // red + aEnd = basegfx::BColor(0.0, 0.0, 1.0); // blue + aColorSteps.emplace_back(0.25, basegfx::BColor(0.0, 1.0, 0.0)); // green@25% + aColorSteps.emplace_back(0.50, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + aColorSteps.emplace_back(0.75, basegfx::BColor(1.0, 0.0, 1.0)); // pink@75% + break; + } + + case 2: + { + // single added in-between, no change of start/end + aColorSteps.emplace_back(0.5, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 3: + { + // check additional StartColor, the one given directly has to win + aColorSteps.emplace_back(0.0, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 4: + { + // check additional EndColor, the one given directly has to win + aColorSteps.emplace_back(1.0, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 5: + { + // check invalid color (too low index), has to be ignored + aColorSteps.emplace_back(-1.0, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 6: + { + // check invalid color (too high index), has to be ignored + aColorSteps.emplace_back(2.0, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 7: + { + // check in-between single-color part + aColorSteps.emplace_back(0.3, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + aColorSteps.emplace_back(0.7, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 8: + { + // check in-between single-color parts + aColorSteps.emplace_back(0.2, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + aColorSteps.emplace_back(0.4, aEnd); + aColorSteps.emplace_back(0.6, aStart); + aColorSteps.emplace_back(0.8, basegfx::BColor(1.0, 1.0, 0.0)); // yellow@50% + break; + } + + case 9: + { + // check single-color start area + aColorSteps.emplace_back(0.6, aStart); + break; + } + + case 10: + { + // check single-color end area + aColorSteps.emplace_back(0.6, aEnd); + break; + } + + default: + { + break; + } + } aGradient = attribute::FillGradientAttribute( XGradientStyleToGradientStyle(aXGradient.GetGradientStyle()),