| /* |
| * Copyright 2022 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "src/shaders/gradients/SkGradientBaseShader.h" |
| |
| #include "include/core/SkAlphaType.h" |
| #include "include/core/SkColor.h" |
| #include "include/core/SkColorSpace.h" |
| #include "include/core/SkColorType.h" |
| #include "include/core/SkData.h" |
| #include "include/core/SkImageInfo.h" |
| #include "include/core/SkShader.h" |
| #include "include/core/SkTileMode.h" |
| #include "include/private/SkColorData.h" |
| #include "include/private/base/SkFloatingPoint.h" |
| #include "include/private/base/SkMalloc.h" |
| #include "include/private/base/SkTArray.h" |
| #include "include/private/base/SkTPin.h" |
| #include "include/private/base/SkTo.h" |
| #include "src/base/SkArenaAlloc.h" |
| #include "src/base/SkFloatBits.h" |
| #include "src/base/SkVx.h" |
| #include "src/core/SkColorSpacePriv.h" |
| #include "src/core/SkColorSpaceXformSteps.h" |
| #include "src/core/SkConvertPixels.h" |
| #include "src/core/SkEffectPriv.h" |
| #include "src/core/SkPicturePriv.h" |
| #include "src/core/SkRasterPipeline.h" |
| #include "src/core/SkRasterPipelineOpContexts.h" |
| #include "src/core/SkRasterPipelineOpList.h" |
| #include "src/core/SkReadBuffer.h" |
| #include "src/core/SkWriteBuffer.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <optional> |
| #include <utility> |
| |
| using namespace skia_private; |
| |
| enum GradientSerializationFlags { |
| // Bits 29:31 used for various boolean flags |
| kHasPosition_GSF = 0x80000000, |
| kHasLegacyLocalMatrix_GSF = 0x40000000, |
| kHasColorSpace_GSF = 0x20000000, |
| |
| // Bits 12:28 unused |
| |
| // Bits 8:11 for fTileMode |
| kTileModeShift_GSF = 8, |
| kTileModeMask_GSF = 0xF, |
| |
| // Bits 4:7 for fInterpolation.fColorSpace |
| kInterpolationColorSpaceShift_GSF = 4, |
| kInterpolationColorSpaceMask_GSF = 0xF, |
| |
| // Bits 1:3 for fInterpolation.fHueMethod |
| kInterpolationHueMethodShift_GSF = 1, |
| kInterpolationHueMethodMask_GSF = 0x7, |
| |
| // Bit 0 for fInterpolation.fInPremul |
| kInterpolationInPremul_GSF = 0x1, |
| }; |
| |
| SkGradientBaseShader::Descriptor::Descriptor() { |
| sk_bzero(this, sizeof(*this)); |
| fTileMode = SkTileMode::kClamp; |
| } |
| SkGradientBaseShader::Descriptor::~Descriptor() = default; |
| |
| void SkGradientBaseShader::flatten(SkWriteBuffer& buffer) const { |
| uint32_t flags = 0; |
| if (fPositions) { |
| flags |= kHasPosition_GSF; |
| } |
| sk_sp<SkData> colorSpaceData = fColorSpace ? fColorSpace->serialize() : nullptr; |
| if (colorSpaceData) { |
| flags |= kHasColorSpace_GSF; |
| } |
| if (fInterpolation.fInPremul == Interpolation::InPremul::kYes) { |
| flags |= kInterpolationInPremul_GSF; |
| } |
| SkASSERT(static_cast<uint32_t>(fTileMode) <= kTileModeMask_GSF); |
| flags |= ((uint32_t)fTileMode << kTileModeShift_GSF); |
| SkASSERT(static_cast<uint32_t>(fInterpolation.fColorSpace) <= kInterpolationColorSpaceMask_GSF); |
| flags |= ((uint32_t)fInterpolation.fColorSpace << kInterpolationColorSpaceShift_GSF); |
| SkASSERT(static_cast<uint32_t>(fInterpolation.fHueMethod) <= kInterpolationHueMethodMask_GSF); |
| flags |= ((uint32_t)fInterpolation.fHueMethod << kInterpolationHueMethodShift_GSF); |
| |
| buffer.writeUInt(flags); |
| |
| // If we injected implicit first/last stops at construction time, omit those when serializing: |
| int colorCount = fColorCount; |
| const SkColor4f* colors = fColors; |
| const SkScalar* positions = fPositions; |
| if (fFirstStopIsImplicit) { |
| colorCount--; |
| colors++; |
| if (positions) { |
| positions++; |
| } |
| } |
| if (fLastStopIsImplicit) { |
| colorCount--; |
| } |
| |
| buffer.writeColor4fArray(colors, colorCount); |
| if (colorSpaceData) { |
| buffer.writeDataAsByteArray(colorSpaceData.get()); |
| } |
| if (positions) { |
| buffer.writeScalarArray(positions, colorCount); |
| } |
| } |
| |
| template <int N, typename T, bool MEM_MOVE> |
| static bool validate_array(SkReadBuffer& buffer, size_t count, STArray<N, T, MEM_MOVE>* array) { |
| if (!buffer.validateCanReadN<T>(count)) { |
| return false; |
| } |
| |
| array->resize_back(count); |
| return true; |
| } |
| |
| bool SkGradientBaseShader::DescriptorScope::unflatten(SkReadBuffer& buffer, |
| SkMatrix* legacyLocalMatrix) { |
| // New gradient format. Includes floating point color, color space, densely packed flags |
| uint32_t flags = buffer.readUInt(); |
| |
| fTileMode = (SkTileMode)((flags >> kTileModeShift_GSF) & kTileModeMask_GSF); |
| |
| fInterpolation.fColorSpace = (Interpolation::ColorSpace)( |
| (flags >> kInterpolationColorSpaceShift_GSF) & kInterpolationColorSpaceMask_GSF); |
| fInterpolation.fHueMethod = (Interpolation::HueMethod)( |
| (flags >> kInterpolationHueMethodShift_GSF) & kInterpolationHueMethodMask_GSF); |
| fInterpolation.fInPremul = (flags & kInterpolationInPremul_GSF) ? Interpolation::InPremul::kYes |
| : Interpolation::InPremul::kNo; |
| |
| fColorCount = buffer.getArrayCount(); |
| |
| if (!(validate_array(buffer, fColorCount, &fColorStorage) && |
| buffer.readColor4fArray(fColorStorage.begin(), fColorCount))) { |
| return false; |
| } |
| fColors = fColorStorage.begin(); |
| |
| if (SkToBool(flags & kHasColorSpace_GSF)) { |
| sk_sp<SkData> data = buffer.readByteArrayAsData(); |
| fColorSpace = data ? SkColorSpace::Deserialize(data->data(), data->size()) : nullptr; |
| } else { |
| fColorSpace = nullptr; |
| } |
| if (SkToBool(flags & kHasPosition_GSF)) { |
| if (!(validate_array(buffer, fColorCount, &fPositionStorage) && |
| buffer.readScalarArray(fPositionStorage.begin(), fColorCount))) { |
| return false; |
| } |
| fPositions = fPositionStorage.begin(); |
| } else { |
| fPositions = nullptr; |
| } |
| if (SkToBool(flags & kHasLegacyLocalMatrix_GSF)) { |
| SkASSERT(buffer.isVersionLT(SkPicturePriv::Version::kNoShaderLocalMatrix)); |
| buffer.readMatrix(legacyLocalMatrix); |
| } else { |
| *legacyLocalMatrix = SkMatrix::I(); |
| } |
| return buffer.isValid(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////// |
| |
| SkGradientBaseShader::SkGradientBaseShader(const Descriptor& desc, const SkMatrix& ptsToUnit) |
| : fPtsToUnit(ptsToUnit) |
| , fColorSpace(desc.fColorSpace ? desc.fColorSpace : SkColorSpace::MakeSRGB()) |
| , fFirstStopIsImplicit(false) |
| , fLastStopIsImplicit(false) |
| , fColorsAreOpaque(true) { |
| fPtsToUnit.getType(); // Precache so reads are threadsafe. |
| SkASSERT(desc.fColorCount > 1); |
| |
| fInterpolation = desc.fInterpolation; |
| |
| SkASSERT((unsigned)desc.fTileMode < kSkTileModeCount); |
| fTileMode = desc.fTileMode; |
| |
| /* Note: we let the caller skip the first and/or last position. |
| i.e. pos[0] = 0.3, pos[1] = 0.7 |
| In these cases, we insert entries to ensure that the final data |
| will be bracketed by [0, 1]. |
| i.e. our_pos[0] = 0, our_pos[1] = 0.3, our_pos[2] = 0.7, our_pos[3] = 1 |
| |
| Thus colorCount (the caller's value, and fColorCount (our value) may |
| differ by up to 2. In the above example: |
| colorCount = 2 |
| fColorCount = 4 |
| */ |
| fColorCount = desc.fColorCount; |
| // check if we need to add in start and/or end position/colors |
| if (desc.fPositions) { |
| fFirstStopIsImplicit = desc.fPositions[0] != 0; |
| fLastStopIsImplicit = desc.fPositions[desc.fColorCount - 1] != SK_Scalar1; |
| fColorCount += fFirstStopIsImplicit + fLastStopIsImplicit; |
| } |
| |
| size_t storageSize = |
| fColorCount * (sizeof(SkColor4f) + (desc.fPositions ? sizeof(SkScalar) : 0)); |
| fColors = reinterpret_cast<SkColor4f*>(fStorage.reset(storageSize)); |
| fPositions = desc.fPositions ? reinterpret_cast<SkScalar*>(fColors + fColorCount) : nullptr; |
| |
| // Now copy over the colors, adding the duplicates at t=0 and t=1 as needed |
| SkColor4f* colors = fColors; |
| if (fFirstStopIsImplicit) { |
| *colors++ = desc.fColors[0]; |
| } |
| for (int i = 0; i < desc.fColorCount; ++i) { |
| colors[i] = desc.fColors[i]; |
| fColorsAreOpaque = fColorsAreOpaque && (desc.fColors[i].fA == 1); |
| } |
| if (fLastStopIsImplicit) { |
| colors += desc.fColorCount; |
| *colors = desc.fColors[desc.fColorCount - 1]; |
| } |
| |
| if (desc.fPositions) { |
| SkScalar prev = 0; |
| SkScalar* positions = fPositions; |
| *positions++ = prev; // force the first pos to 0 |
| |
| int startIndex = fFirstStopIsImplicit ? 0 : 1; |
| int count = desc.fColorCount + fLastStopIsImplicit; |
| |
| bool uniformStops = true; |
| const SkScalar uniformStep = desc.fPositions[startIndex] - prev; |
| for (int i = startIndex; i < count; i++) { |
| // Pin the last value to 1.0, and make sure pos is monotonic. |
| auto curr = (i == desc.fColorCount) ? 1 : SkTPin(desc.fPositions[i], prev, 1.0f); |
| uniformStops &= SkScalarNearlyEqual(uniformStep, curr - prev); |
| |
| *positions++ = prev = curr; |
| } |
| |
| // If the stops are uniform, treat them as implicit. |
| if (uniformStops) { |
| fPositions = nullptr; |
| } |
| } |
| } |
| |
| SkGradientBaseShader::~SkGradientBaseShader() {} |
| |
| static void add_stop_color(SkRasterPipeline_GradientCtx* ctx, |
| size_t stop, |
| SkPMColor4f Fs, |
| SkPMColor4f Bs) { |
| (ctx->fs[0])[stop] = Fs.fR; |
| (ctx->fs[1])[stop] = Fs.fG; |
| (ctx->fs[2])[stop] = Fs.fB; |
| (ctx->fs[3])[stop] = Fs.fA; |
| |
| (ctx->bs[0])[stop] = Bs.fR; |
| (ctx->bs[1])[stop] = Bs.fG; |
| (ctx->bs[2])[stop] = Bs.fB; |
| (ctx->bs[3])[stop] = Bs.fA; |
| } |
| |
| static void add_const_color(SkRasterPipeline_GradientCtx* ctx, size_t stop, SkPMColor4f color) { |
| add_stop_color(ctx, stop, {0, 0, 0, 0}, color); |
| } |
| |
| // Calculate a factor F and a bias B so that color = F*t + B when t is in range of |
| // the stop. Assume that the distance between stops is 1/gapCount. |
| static void init_stop_evenly(SkRasterPipeline_GradientCtx* ctx, |
| float gapCount, |
| size_t stop, |
| SkPMColor4f c_l, |
| SkPMColor4f c_r) { |
| // Clankium's GCC 4.9 targeting ARMv7 is barfing when we use Sk4f math here, so go scalar... |
| SkPMColor4f Fs = { |
| (c_r.fR - c_l.fR) * gapCount, |
| (c_r.fG - c_l.fG) * gapCount, |
| (c_r.fB - c_l.fB) * gapCount, |
| (c_r.fA - c_l.fA) * gapCount, |
| }; |
| SkPMColor4f Bs = { |
| c_l.fR - Fs.fR * (stop / gapCount), |
| c_l.fG - Fs.fG * (stop / gapCount), |
| c_l.fB - Fs.fB * (stop / gapCount), |
| c_l.fA - Fs.fA * (stop / gapCount), |
| }; |
| add_stop_color(ctx, stop, Fs, Bs); |
| } |
| |
| // For each stop we calculate a bias B and a scale factor F, such that |
| // for any t between stops n and n+1, the color we want is B[n] + F[n]*t. |
| static void init_stop_pos(SkRasterPipeline_GradientCtx* ctx, |
| size_t stop, |
| float t_l, |
| float c_scale, |
| SkPMColor4f c_l, |
| SkPMColor4f c_r) { |
| // See note about Clankium's old compiler in init_stop_evenly(). |
| SkPMColor4f Fs = { |
| (c_r.fR - c_l.fR) * c_scale, |
| (c_r.fG - c_l.fG) * c_scale, |
| (c_r.fB - c_l.fB) * c_scale, |
| (c_r.fA - c_l.fA) * c_scale, |
| }; |
| SkPMColor4f Bs = { |
| c_l.fR - Fs.fR * t_l, |
| c_l.fG - Fs.fG * t_l, |
| c_l.fB - Fs.fB * t_l, |
| c_l.fA - Fs.fA * t_l, |
| }; |
| ctx->ts[stop] = t_l; |
| add_stop_color(ctx, stop, Fs, Bs); |
| } |
| |
| void SkGradientBaseShader::AppendGradientFillStages(SkRasterPipeline* p, |
| SkArenaAlloc* alloc, |
| const SkPMColor4f* pmColors, |
| const SkScalar* positions, |
| int count) { |
| // The two-stop case with stops at 0 and 1. |
| if (count == 2 && positions == nullptr) { |
| const SkPMColor4f c_l = pmColors[0], c_r = pmColors[1]; |
| |
| // See F and B below. |
| auto ctx = alloc->make<SkRasterPipeline_EvenlySpaced2StopGradientCtx>(); |
| (skvx::float4::Load(c_r.vec()) - skvx::float4::Load(c_l.vec())).store(ctx->f); |
| (skvx::float4::Load(c_l.vec())).store(ctx->b); |
| |
| p->append(SkRasterPipelineOp::evenly_spaced_2_stop_gradient, ctx); |
| } else { |
| auto* ctx = alloc->make<SkRasterPipeline_GradientCtx>(); |
| |
| // Note: In order to handle clamps in search, the search assumes a stop conceptully placed |
| // at -inf. Therefore, the max number of stops is fColorCount+1. |
| for (int i = 0; i < 4; i++) { |
| // Allocate at least at for the AVX2 gather from a YMM register. |
| ctx->fs[i] = alloc->makeArray<float>(std::max(count + 1, 8)); |
| ctx->bs[i] = alloc->makeArray<float>(std::max(count + 1, 8)); |
| } |
| |
| if (positions == nullptr) { |
| // Handle evenly distributed stops. |
| |
| size_t stopCount = count; |
| float gapCount = stopCount - 1; |
| |
| SkPMColor4f c_l = pmColors[0]; |
| for (size_t i = 0; i < stopCount - 1; i++) { |
| SkPMColor4f c_r = pmColors[i + 1]; |
| init_stop_evenly(ctx, gapCount, i, c_l, c_r); |
| c_l = c_r; |
| } |
| add_const_color(ctx, stopCount - 1, c_l); |
| |
| ctx->stopCount = stopCount; |
| p->append(SkRasterPipelineOp::evenly_spaced_gradient, ctx); |
| } else { |
| // Handle arbitrary stops. |
| |
| ctx->ts = alloc->makeArray<float>(count + 1); |
| |
| // Remove the default stops inserted by SkGradientBaseShader::SkGradientBaseShader |
| // because they are naturally handled by the search method. |
| int firstStop; |
| int lastStop; |
| if (count > 2) { |
| firstStop = pmColors[0] != pmColors[1] ? 0 : 1; |
| lastStop = pmColors[count - 2] != pmColors[count - 1] ? count - 1 : count - 2; |
| } else { |
| firstStop = 0; |
| lastStop = 1; |
| } |
| |
| size_t stopCount = 0; |
| float t_l = positions[firstStop]; |
| SkPMColor4f c_l = pmColors[firstStop]; |
| add_const_color(ctx, stopCount++, c_l); |
| // N.B. lastStop is the index of the last stop, not one after. |
| for (int i = firstStop; i < lastStop; i++) { |
| float t_r = positions[i + 1]; |
| SkPMColor4f c_r = pmColors[i + 1]; |
| SkASSERT(t_l <= t_r); |
| if (t_l < t_r) { |
| float c_scale = sk_ieee_float_divide(1, t_r - t_l); |
| if (SkIsFinite(c_scale)) { |
| init_stop_pos(ctx, stopCount, t_l, c_scale, c_l, c_r); |
| stopCount += 1; |
| } |
| } |
| t_l = t_r; |
| c_l = c_r; |
| } |
| |
| ctx->ts[stopCount] = t_l; |
| add_const_color(ctx, stopCount++, c_l); |
| |
| ctx->stopCount = stopCount; |
| p->append(SkRasterPipelineOp::gradient, ctx); |
| } |
| } |
| } |
| |
| void SkGradientBaseShader::AppendInterpolatedToDstStages(SkRasterPipeline* p, |
| SkArenaAlloc* alloc, |
| bool colorsAreOpaque, |
| const Interpolation& interpolation, |
| const SkColorSpace* intermediateColorSpace, |
| const SkColorSpace* dstColorSpace) { |
| using ColorSpace = Interpolation::ColorSpace; |
| bool colorIsPremul = static_cast<bool>(interpolation.fInPremul); |
| |
| // If we interpolated premul colors in any of the special color spaces, we need to unpremul |
| if (colorIsPremul && !colorsAreOpaque) { |
| switch (interpolation.fColorSpace) { |
| case ColorSpace::kLab: |
| case ColorSpace::kOKLab: |
| case ColorSpace::kOKLabGamutMap: |
| p->append(SkRasterPipelineOp::unpremul); |
| colorIsPremul = false; |
| break; |
| case ColorSpace::kLCH: |
| case ColorSpace::kOKLCH: |
| case ColorSpace::kOKLCHGamutMap: |
| case ColorSpace::kHSL: |
| case ColorSpace::kHWB: |
| p->append(SkRasterPipelineOp::unpremul_polar); |
| colorIsPremul = false; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // Convert colors in exotic spaces back to their intermediate SkColorSpace |
| switch (interpolation.fColorSpace) { |
| case ColorSpace::kLab: p->append(SkRasterPipelineOp::css_lab_to_xyz); break; |
| case ColorSpace::kOKLab: p->append(SkRasterPipelineOp::css_oklab_to_linear_srgb); break; |
| case ColorSpace::kOKLabGamutMap: |
| p->append(SkRasterPipelineOp::css_oklab_gamut_map_to_linear_srgb); |
| break; |
| case ColorSpace::kLCH: p->append(SkRasterPipelineOp::css_hcl_to_lab); |
| p->append(SkRasterPipelineOp::css_lab_to_xyz); break; |
| case ColorSpace::kOKLCH: p->append(SkRasterPipelineOp::css_hcl_to_lab); |
| p->append(SkRasterPipelineOp::css_oklab_to_linear_srgb); break; |
| case ColorSpace::kOKLCHGamutMap: |
| p->append(SkRasterPipelineOp::css_hcl_to_lab); |
| p->append(SkRasterPipelineOp::css_oklab_gamut_map_to_linear_srgb); |
| break; |
| case ColorSpace::kHSL: p->append(SkRasterPipelineOp::css_hsl_to_srgb); break; |
| case ColorSpace::kHWB: p->append(SkRasterPipelineOp::css_hwb_to_srgb); break; |
| default: break; |
| } |
| |
| // Now transform from intermediate to destination color space. |
| // See comments in GrGradientShader.cpp about the decisions here. |
| if (!dstColorSpace) { |
| dstColorSpace = sk_srgb_singleton(); |
| } |
| SkAlphaType intermediateAlphaType = colorIsPremul ? kPremul_SkAlphaType : kUnpremul_SkAlphaType; |
| // TODO(skia:13108): Get dst alpha type correctly |
| SkAlphaType dstAlphaType = kPremul_SkAlphaType; |
| |
| if (colorsAreOpaque) { |
| intermediateAlphaType = dstAlphaType = kUnpremul_SkAlphaType; |
| } |
| |
| alloc->make<SkColorSpaceXformSteps>( |
| intermediateColorSpace, intermediateAlphaType, dstColorSpace, dstAlphaType) |
| ->apply(p); |
| } |
| |
| bool SkGradientBaseShader::appendStages(const SkStageRec& rec, |
| const SkShaders::MatrixRec& mRec) const { |
| SkRasterPipeline* p = rec.fPipeline; |
| SkArenaAlloc* alloc = rec.fAlloc; |
| SkRasterPipeline_DecalTileCtx* decal_ctx = nullptr; |
| |
| std::optional<SkShaders::MatrixRec> newMRec = mRec.apply(rec, fPtsToUnit); |
| if (!newMRec.has_value()) { |
| return false; |
| } |
| |
| SkRasterPipeline_<256> postPipeline; |
| |
| this->appendGradientStages(alloc, p, &postPipeline); |
| |
| switch (fTileMode) { |
| case SkTileMode::kMirror: |
| p->append(SkRasterPipelineOp::mirror_x_1); |
| break; |
| case SkTileMode::kRepeat: |
| p->append(SkRasterPipelineOp::repeat_x_1); |
| break; |
| case SkTileMode::kDecal: |
| decal_ctx = alloc->make<SkRasterPipeline_DecalTileCtx>(); |
| decal_ctx->limit_x = SkBits2Float(SkFloat2Bits(1.0f) + 1); |
| // reuse mask + limit_x stage, or create a custom decal_1 that just stores the mask |
| p->append(SkRasterPipelineOp::decal_x, decal_ctx); |
| [[fallthrough]]; |
| |
| case SkTileMode::kClamp: |
| if (!fPositions) { |
| // We clamp only when the stops are evenly spaced. |
| // If not, there may be hard stops, and clamping ruins hard stops at 0 and/or 1. |
| // In that case, we must make sure we're using the general "gradient" stage, |
| // which is the only stage that will correctly handle unclamped t. |
| p->append(SkRasterPipelineOp::clamp_x_1); |
| } |
| break; |
| } |
| |
| // Transform all of the colors to destination color space, possibly premultiplied |
| SkColor4fXformer xformedColors(this, rec.fDstCS); |
| AppendGradientFillStages(p, alloc, |
| xformedColors.fColors.begin(), |
| xformedColors.fPositions, |
| xformedColors.fColors.size()); |
| AppendInterpolatedToDstStages(p, alloc, fColorsAreOpaque, fInterpolation, |
| xformedColors.fIntermediateColorSpace.get(), rec.fDstCS); |
| |
| if (decal_ctx) { |
| p->append(SkRasterPipelineOp::check_decal_mask, decal_ctx); |
| } |
| |
| p->extend(postPipeline); |
| |
| return true; |
| } |
| |
| bool SkGradientBaseShader::isOpaque() const { |
| return fColorsAreOpaque && (this->getTileMode() != SkTileMode::kDecal); |
| } |
| |
| bool SkGradientBaseShader::onAsLuminanceColor(SkColor4f* lum) const { |
| // We just compute an average color. There are several things we could do better: |
| // 1) We already have a different average_gradient_color helper later in this file, that weights |
| // contribution by the relative size of each band. |
| // 2) Colors should be converted to some standard color space! These could be in any space. |
| // 3) Do we want to average in the source space, sRGB, or some linear space? |
| SkColor4f color{0, 0, 0, 1}; |
| for (int i = 0; i < fColorCount; ++i) { |
| color.fR += fColors[i].fR; |
| color.fG += fColors[i].fG; |
| color.fB += fColors[i].fB; |
| } |
| const float scale = 1.0f / fColorCount; |
| color.fR *= scale; |
| color.fG *= scale; |
| color.fB *= scale; |
| *lum = color; |
| return true; |
| } |
| |
| static sk_sp<SkColorSpace> intermediate_color_space(SkGradientShader::Interpolation::ColorSpace cs, |
| SkColorSpace* dst) { |
| using ColorSpace = SkGradientShader::Interpolation::ColorSpace; |
| switch (cs) { |
| case ColorSpace::kDestination: |
| return sk_ref_sp(dst); |
| |
| // css-color-4 allows XYZD50 and XYZD65. For gradients, those are redundant. Interpolating |
| // in any linear RGB space, (regardless of white point), gives the same answer. |
| case ColorSpace::kSRGBLinear: |
| return SkColorSpace::MakeSRGBLinear(); |
| |
| case ColorSpace::kSRGB: |
| case ColorSpace::kHSL: |
| case ColorSpace::kHWB: |
| return SkColorSpace::MakeSRGB(); |
| |
| case ColorSpace::kLab: |
| case ColorSpace::kLCH: |
| // Conversion to Lab (and LCH) starts with XYZD50 |
| return SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, SkNamedGamut::kXYZ); |
| |
| case ColorSpace::kOKLab: |
| case ColorSpace::kOKLabGamutMap: |
| case ColorSpace::kOKLCH: |
| case ColorSpace::kOKLCHGamutMap: |
| // The "standard" conversion to these spaces starts with XYZD65. That requires extra |
| // effort to conjure. The author also has reference code for going directly from linear |
| // sRGB, so we use that. |
| // TODO(skia:13108): Even better would be to have an LMS color space, because the first |
| // part of the conversion is a matrix multiply, which could be absorbed into the |
| // color space xform. |
| return SkColorSpace::MakeSRGBLinear(); |
| } |
| SkUNREACHABLE; |
| } |
| |
| using ConvertColorProc = SkPMColor4f(*)(SkPMColor4f, bool*); |
| using PremulColorProc = SkPMColor4f(*)(SkPMColor4f); |
| |
| static SkPMColor4f srgb_to_hsl(SkPMColor4f rgb, bool* hueIsPowerless) { |
| float mx = std::max({rgb.fR, rgb.fG, rgb.fB}); |
| float mn = std::min({rgb.fR, rgb.fG, rgb.fB}); |
| float hue = 0, sat = 0, light = (mn + mx) / 2; |
| float d = mx - mn; |
| |
| if (d != 0) { |
| sat = (light == 0 || light == 1) ? 0 : (mx - light) / std::min(light, 1 - light); |
| if (mx == rgb.fR) { |
| hue = (rgb.fG - rgb.fB) / d + (rgb.fG < rgb.fB ? 6 : 0); |
| } else if (mx == rgb.fG) { |
| hue = (rgb.fB - rgb.fR) / d + 2; |
| } else { |
| hue = (rgb.fR - rgb.fG) / d + 4; |
| } |
| |
| hue *= 60; |
| } |
| if (sat == 0) { |
| *hueIsPowerless = true; |
| } |
| return {hue, sat * 100, light * 100, rgb.fA}; |
| } |
| |
| static SkPMColor4f srgb_to_hwb(SkPMColor4f rgb, bool* hueIsPowerless) { |
| SkPMColor4f hsl = srgb_to_hsl(rgb, hueIsPowerless); |
| float white = std::min({rgb.fR, rgb.fG, rgb.fB}); |
| float black = 1 - std::max({rgb.fR, rgb.fG, rgb.fB}); |
| return {hsl.fR, white * 100, black * 100, rgb.fA}; |
| } |
| |
| static SkPMColor4f xyzd50_to_lab(SkPMColor4f xyz, bool* /*hueIsPowerless*/) { |
| constexpr float D50[3] = {0.3457f / 0.3585f, 1.0f, (1.0f - 0.3457f - 0.3585f) / 0.3585f}; |
| |
| constexpr float e = 216.0f / 24389; |
| constexpr float k = 24389.0f / 27; |
| |
| SkPMColor4f f; |
| for (int i = 0; i < 3; ++i) { |
| float v = xyz[i] / D50[i]; |
| f[i] = (v > e) ? std::cbrtf(v) : (k * v + 16) / 116; |
| } |
| |
| return {(116 * f[1]) - 16, 500 * (f[0] - f[1]), 200 * (f[1] - f[2]), xyz.fA}; |
| } |
| |
| // The color space is technically LCH, but we produce HCL, so that all polar spaces have hue in the |
| // first component. This simplifies the hue handling for HueMethod and premul/unpremul. |
| static SkPMColor4f xyzd50_to_hcl(SkPMColor4f xyz, bool* hueIsPowerless) { |
| SkPMColor4f Lab = xyzd50_to_lab(xyz, hueIsPowerless); |
| float hue = sk_float_radians_to_degrees(atan2f(Lab[2], Lab[1])); |
| float chroma = sqrtf(Lab[1] * Lab[1] + Lab[2] * Lab[2]); |
| // The LCH math produces small-ish (but not tiny) chroma values for achromatic colors: |
| constexpr float kMaxChromaForPowerlessHue = 1e-2f; |
| if (chroma <= kMaxChromaForPowerlessHue) { |
| *hueIsPowerless = true; |
| } |
| return {hue >= 0 ? hue : hue + 360, chroma, Lab[0], xyz.fA}; |
| } |
| |
| // https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab |
| static SkPMColor4f lin_srgb_to_oklab(SkPMColor4f rgb, bool* /*hueIsPowerless*/) { |
| float l = 0.4122214708f * rgb.fR + 0.5363325363f * rgb.fG + 0.0514459929f * rgb.fB; |
| float m = 0.2119034982f * rgb.fR + 0.6806995451f * rgb.fG + 0.1073969566f * rgb.fB; |
| float s = 0.0883024619f * rgb.fR + 0.2817188376f * rgb.fG + 0.6299787005f * rgb.fB; |
| l = std::cbrtf(l); |
| m = std::cbrtf(m); |
| s = std::cbrtf(s); |
| return {0.2104542553f * l + 0.7936177850f * m - 0.0040720468f * s, |
| 1.9779984951f * l - 2.4285922050f * m + 0.4505937099f * s, |
| 0.0259040371f * l + 0.7827717662f * m - 0.8086757660f * s, |
| rgb.fA}; |
| } |
| |
| // The color space is technically OkLCH, but we produce HCL, so that all polar spaces have hue in |
| // the first component. This simplifies the hue handling for HueMethod and premul/unpremul. |
| static SkPMColor4f lin_srgb_to_okhcl(SkPMColor4f rgb, bool* hueIsPowerless) { |
| SkPMColor4f OKLab = lin_srgb_to_oklab(rgb, hueIsPowerless); |
| float hue = sk_float_radians_to_degrees(atan2f(OKLab[2], OKLab[1])); |
| float chroma = sqrtf(OKLab[1] * OKLab[1] + OKLab[2] * OKLab[2]); |
| // The OKLCH math produces very small chroma values for achromatic colors: |
| constexpr float kMaxChromaForPowerlessHue = 1e-6f; |
| if (chroma <= kMaxChromaForPowerlessHue) { |
| *hueIsPowerless = true; |
| } |
| return {hue >= 0 ? hue : hue + 360, chroma, OKLab[0], rgb.fA}; |
| } |
| |
| static SkPMColor4f premul_polar(SkPMColor4f hsl) { |
| return {hsl.fR, hsl.fG * hsl.fA, hsl.fB * hsl.fA, hsl.fA}; |
| } |
| |
| static SkPMColor4f premul_rgb(SkPMColor4f rgb) { |
| return {rgb.fR * rgb.fA, rgb.fG * rgb.fA, rgb.fB * rgb.fA, rgb.fA}; |
| } |
| |
| static bool color_space_is_polar(SkGradientShader::Interpolation::ColorSpace cs) { |
| using ColorSpace = SkGradientShader::Interpolation::ColorSpace; |
| switch (cs) { |
| case ColorSpace::kLCH: |
| case ColorSpace::kOKLCH: |
| case ColorSpace::kHSL: |
| case ColorSpace::kHWB: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| // Given `colors` in `src` color space, an interpolation space, and a `dst` color space, |
| // we are doing several things. First, some definitions: |
| // |
| // The interpolation color space is "special" if it can't be represented as an SkColorSpace. This |
| // applies to any color space that isn't an RGB space, like Lab or HSL. These need special handling |
| // because we have to run bespoke code to do the conversion (before interpolation here, and after |
| // interpolation in the backend shader/pipeline). |
| // |
| // The interpolation color space is "polar" if it involves hue (HSL, HWB, LCH, Oklch). These need |
| // special handling, becuase hue is never premultiplied, and because HueMethod comes into play. |
| // |
| // 1) Pick an `intermediate` SkColorSpace. If the interpolation color space is not "special", |
| // (kDestination, kSRGB, etc... ), then `intermediate` is exact. Otherwise, `intermediate` is the |
| // RGB space that prepares us to do the final conversion. For example, conversion to Lab starts |
| // with XYZD50, so `intermediate` will be XYZD50 if we're actually interpolating in Lab. |
| // 2) Transform all colors to the `intermediate` color space, leaving them unpremultiplied. |
| // 3) If the interpolation color space is "special", transform the colors to that space. |
| // 4) If the interpolation color space is "polar", adjust the angles to respect HueMethod. |
| // 5) If premul interpolation is requested, apply that. For "polar" interpolated colors, don't |
| // premultiply hue, only the other two channels. Note that there are four polar spaces. |
| // Two have hue as the first component, and two have it as the third component. To reduce |
| // complexity, we always store hue in the first component, swapping it with luminance for |
| // LCH and Oklch. The backend code (eg, shaders) needs to know about this. |
| SkColor4fXformer::SkColor4fXformer(const SkGradientBaseShader* shader, |
| SkColorSpace* dst, |
| bool forceExplicitPositions) { |
| using ColorSpace = SkGradientShader::Interpolation::ColorSpace; |
| using HueMethod = SkGradientShader::Interpolation::HueMethod; |
| |
| int colorCount = shader->fColorCount; |
| const SkGradientShader::Interpolation interpolation = shader->fInterpolation; |
| |
| // 0) Copy the shader's position pointer. Certain interpolation modes might force us to add |
| // new stops, in which case we'll allocate & edit the positions. |
| fPositions = shader->fPositions; |
| |
| // 1) Determine the color space of our intermediate colors. |
| fIntermediateColorSpace = intermediate_color_space(interpolation.fColorSpace, dst); |
| |
| // 2) Convert all colors to the intermediate color space |
| auto info = SkImageInfo::Make(colorCount, 1, kRGBA_F32_SkColorType, kUnpremul_SkAlphaType); |
| |
| auto dstInfo = info.makeColorSpace(fIntermediateColorSpace); |
| auto srcInfo = info.makeColorSpace(shader->fColorSpace); |
| |
| fColors.reset(colorCount); |
| SkAssertResult(SkConvertPixels(dstInfo, |
| fColors.begin(), |
| info.minRowBytes(), |
| srcInfo, |
| shader->fColors, |
| info.minRowBytes())); |
| |
| // 3) Transform to the interpolation color space (if it's special) |
| ConvertColorProc convertFn = nullptr; |
| switch (interpolation.fColorSpace) { |
| case ColorSpace::kHSL: convertFn = srgb_to_hsl; break; |
| case ColorSpace::kHWB: convertFn = srgb_to_hwb; break; |
| case ColorSpace::kLab: convertFn = xyzd50_to_lab; break; |
| case ColorSpace::kLCH: convertFn = xyzd50_to_hcl; break; |
| case ColorSpace::kOKLab: convertFn = lin_srgb_to_oklab; break; |
| case ColorSpace::kOKLabGamutMap: convertFn = lin_srgb_to_oklab; break; |
| case ColorSpace::kOKLCH: convertFn = lin_srgb_to_okhcl; break; |
| case ColorSpace::kOKLCHGamutMap: convertFn = lin_srgb_to_okhcl; break; |
| default: break; |
| } |
| |
| skia_private::STArray<4, bool> hueIsPowerless; |
| bool anyPowerlessHue = false; |
| hueIsPowerless.push_back_n(colorCount, false); |
| if (convertFn) { |
| for (int i = 0; i < colorCount; ++i) { |
| fColors[i] = convertFn(fColors[i], hueIsPowerless.data() + i); |
| anyPowerlessHue = anyPowerlessHue || hueIsPowerless[i]; |
| } |
| } |
| |
| if (anyPowerlessHue) { |
| // In theory, if we knew we were just going to adjust the existing colors (without adding |
| // new ones), we could do it all in-place. To keep things simple, we always generate the |
| // new colors in separate storage. |
| ColorStorage newColors; |
| PositionStorage newPositions; |
| |
| for (int i = 0; i < colorCount; ++i) { |
| const SkPMColor4f& curColor = fColors[i]; |
| float curPos = shader->getPos(i); |
| |
| if (!hueIsPowerless[i]) { |
| newColors.push_back(curColor); |
| newPositions.push_back(curPos); |
| continue; |
| } |
| |
| auto colorWithHueFrom = [](const SkPMColor4f& color, const SkPMColor4f& hueColor) { |
| // If we have any powerless hue, then all colors are already in (some) polar space, |
| // and they all store their hue in the red channel. |
| return SkPMColor4f{hueColor.fR, color.fG, color.fB, color.fA}; |
| }; |
| |
| // In each case, we might be copying a powerless (invalid) hue from the neighbor, but |
| // that should be fine, as it will match that neighbor perfectly, and any hue is ok. |
| if (i != 0) { |
| newPositions.push_back(curPos); |
| newColors.push_back(colorWithHueFrom(curColor, fColors[i - 1])); |
| } |
| if (i != colorCount - 1) { |
| newPositions.push_back(curPos); |
| newColors.push_back(colorWithHueFrom(curColor, fColors[i + 1])); |
| } |
| } |
| |
| fColors.swap(newColors); |
| fPositionStorage.swap(newPositions); |
| fPositions = fPositionStorage.data(); |
| colorCount = fColors.size(); |
| } |
| |
| // 4) For polar colors, adjust hue values to respect the hue method. We're using a trick here... |
| // The specification looks at adjacent colors, and adjusts one or the other. Because we store |
| // the stops in uniforms (and our backend conversions normalize the hue angle), we can |
| // instead always apply the adjustment to the *second* color. That lets us keep a running |
| // total, and do a single pass across all the colors to respect the requested hue method, |
| // without needing to do any extra work per-pixel. |
| if (color_space_is_polar(interpolation.fColorSpace)) { |
| float delta = 0; |
| for (int i = 0; i < colorCount - 1; ++i) { |
| float h1 = fColors[i].fR; |
| float& h2 = fColors[i + 1].fR; |
| h2 += delta; |
| switch (interpolation.fHueMethod) { |
| case HueMethod::kShorter: |
| if (h2 - h1 > 180) { |
| h2 -= 360; // i.e. h1 += 360 |
| delta -= 360; |
| } else if (h2 - h1 < -180) { |
| h2 += 360; |
| delta += 360; |
| } |
| break; |
| case HueMethod::kLonger: |
| if ((i == 0 && shader->fFirstStopIsImplicit) || |
| (i == colorCount - 2 && shader->fLastStopIsImplicit)) { |
| // Do nothing. We don't want to introduce a full revolution for these stops |
| // Full rationale at skbug.com/13941 |
| } else if (0 < h2 - h1 && h2 - h1 < 180) { |
| h2 -= 360; // i.e. h1 += 360 |
| delta -= 360; |
| } else if (-180 < h2 - h1 && h2 - h1 <= 0) { |
| h2 += 360; |
| delta += 360; |
| } |
| break; |
| case HueMethod::kIncreasing: |
| if (h2 < h1) { |
| h2 += 360; |
| delta += 360; |
| } |
| break; |
| case HueMethod::kDecreasing: |
| if (h1 < h2) { |
| h2 -= 360; // i.e. h1 += 360; |
| delta -= 360; |
| } |
| break; |
| } |
| } |
| } |
| |
| // 5) Apply premultiplication |
| PremulColorProc premulFn = nullptr; |
| if (static_cast<bool>(interpolation.fInPremul)) { |
| switch (interpolation.fColorSpace) { |
| case ColorSpace::kHSL: |
| case ColorSpace::kHWB: |
| case ColorSpace::kLCH: |
| case ColorSpace::kOKLCH: |
| premulFn = premul_polar; |
| break; |
| default: |
| premulFn = premul_rgb; |
| break; |
| } |
| } |
| |
| if (premulFn) { |
| for (int i = 0; i < colorCount; ++i) { |
| fColors[i] = premulFn(fColors[i]); |
| } |
| } |
| |
| // Ganesh requires that the positions be explicit (rather than implicitly evenly spaced) |
| if (forceExplicitPositions && !fPositions) { |
| fPositionStorage.reserve_exact(colorCount); |
| float posScale = 1.0f / (colorCount - 1); |
| for (int i = 0; i < colorCount; i++) { |
| fPositionStorage.push_back(i * posScale); |
| } |
| fPositions = fPositionStorage.data(); |
| } |
| } |
| |
| SkColorConverter::SkColorConverter(const SkColor* colors, int count) { |
| const float ONE_OVER_255 = 1.f / 255; |
| for (int i = 0; i < count; ++i) { |
| fColors4f.push_back({SkColorGetR(colors[i]) * ONE_OVER_255, |
| SkColorGetG(colors[i]) * ONE_OVER_255, |
| SkColorGetB(colors[i]) * ONE_OVER_255, |
| SkColorGetA(colors[i]) * ONE_OVER_255}); |
| } |
| } |
| |
| void SkGradientBaseShader::commonAsAGradient(GradientInfo* info) const { |
| if (info) { |
| if (info->fColorCount >= fColorCount) { |
| if (info->fColors) { |
| for (int i = 0; i < fColorCount; ++i) { |
| info->fColors[i] = this->getLegacyColor(i); |
| } |
| } |
| if (info->fColorOffsets) { |
| for (int i = 0; i < fColorCount; ++i) { |
| info->fColorOffsets[i] = this->getPos(i); |
| } |
| } |
| } |
| info->fColorCount = fColorCount; |
| info->fTileMode = fTileMode; |
| |
| info->fGradientFlags = |
| this->interpolateInPremul() ? SkGradientShader::kInterpolateColorsInPremul_Flag : 0; |
| } |
| } |
| |
| // Return true if these parameters are valid/legal/safe to construct a gradient |
| // |
| bool SkGradientBaseShader::ValidGradient(const SkColor4f colors[], |
| int count, |
| SkTileMode tileMode, |
| const Interpolation& interpolation) { |
| return nullptr != colors && count >= 1 && (unsigned)tileMode < kSkTileModeCount && |
| (unsigned)interpolation.fColorSpace < Interpolation::kColorSpaceCount && |
| (unsigned)interpolation.fHueMethod < Interpolation::kHueMethodCount; |
| } |
| |
| SkGradientBaseShader::Descriptor::Descriptor(const SkColor4f colors[], |
| sk_sp<SkColorSpace> colorSpace, |
| const SkScalar positions[], |
| int colorCount, |
| SkTileMode mode, |
| const Interpolation& interpolation) |
| : fColors(colors) |
| , fColorSpace(std::move(colorSpace)) |
| , fPositions(positions) |
| , fColorCount(colorCount) |
| , fTileMode(mode) |
| , fInterpolation(interpolation) { |
| SkASSERT(fColorCount > 1); |
| } |
| |
| static SkColor4f average_gradient_color(const SkColor4f colors[], |
| const SkScalar pos[], |
| int colorCount) { |
| // The gradient is a piecewise linear interpolation between colors. For a given interval, |
| // the integral between the two endpoints is 0.5 * (ci + cj) * (pj - pi), which provides that |
| // intervals average color. The overall average color is thus the sum of each piece. The thing |
| // to keep in mind is that the provided gradient definition may implicitly use p=0 and p=1. |
| skvx::float4 blend(0.0f); |
| for (int i = 0; i < colorCount - 1; ++i) { |
| // Calculate the average color for the interval between pos(i) and pos(i+1) |
| auto c0 = skvx::float4::Load(&colors[i]); |
| auto c1 = skvx::float4::Load(&colors[i + 1]); |
| |
| // when pos == null, there are colorCount uniformly distributed stops, going from 0 to 1, |
| // so pos[i + 1] - pos[i] = 1/(colorCount-1) |
| SkScalar w; |
| if (pos) { |
| // Match position fixing in SkGradientShader's constructor, clamping positions outside |
| // [0, 1] and forcing the sequence to be monotonic |
| SkScalar p0 = SkTPin(pos[i], 0.f, 1.f); |
| SkScalar p1 = SkTPin(pos[i + 1], p0, 1.f); |
| w = p1 - p0; |
| |
| // And account for any implicit intervals at the start or end of the positions |
| if (i == 0) { |
| if (p0 > 0.0f) { |
| // The first color is fixed between p = 0 to pos[0], so 0.5*(ci + cj)*(pj - pi) |
| // becomes 0.5*(c + c)*(pj - 0) = c * pj |
| auto c = skvx::float4::Load(&colors[0]); |
| blend += p0 * c; |
| } |
| } |
| if (i == colorCount - 2) { |
| if (p1 < 1.f) { |
| // The last color is fixed between pos[n-1] to p = 1, so 0.5*(ci + cj)*(pj - pi) |
| // becomes 0.5*(c + c)*(1 - pi) = c * (1 - pi) |
| auto c = skvx::float4::Load(&colors[colorCount - 1]); |
| blend += (1.f - p1) * c; |
| } |
| } |
| } else { |
| w = 1.f / (colorCount - 1); |
| } |
| |
| blend += 0.5f * w * (c1 + c0); |
| } |
| |
| SkColor4f avg; |
| blend.store(&avg); |
| return avg; |
| } |
| |
| // Except for special circumstances of clamped gradients, every gradient shape--when degenerate-- |
| // can be mapped to the same fallbacks. The specific shape factories must account for special |
| // clamped conditions separately, this will always return the last color for clamped gradients. |
| sk_sp<SkShader> SkGradientBaseShader::MakeDegenerateGradient(const SkColor4f colors[], |
| const SkScalar pos[], |
| int colorCount, |
| sk_sp<SkColorSpace> colorSpace, |
| SkTileMode mode) { |
| switch (mode) { |
| case SkTileMode::kDecal: |
| // normally this would reject the area outside of the interpolation region, so since |
| // inside region is empty when the radii are equal, the entire draw region is empty |
| return SkShaders::Empty(); |
| case SkTileMode::kRepeat: |
| case SkTileMode::kMirror: |
| // repeat and mirror are treated the same: the border colors are never visible, |
| // but approximate the final color as infinite repetitions of the colors, so |
| // it can be represented as the average color of the gradient. |
| return SkShaders::Color(average_gradient_color(colors, pos, colorCount), |
| std::move(colorSpace)); |
| case SkTileMode::kClamp: |
| // Depending on how the gradient shape degenerates, there may be a more specialized |
| // fallback representation for the factories to use, but this is a reasonable default. |
| return SkShaders::Color(colors[colorCount - 1], std::move(colorSpace)); |
| } |
| SkDEBUGFAIL("Should not be reached"); |
| return nullptr; |
| } |