/* * Copyright 2022 Rive */ #include "gradient.hpp" namespace rive::gpu { // Ensure the given gradient stops are in a format expected by PLS. static bool validate_gradient_stops(const ColorInt colors[], // [count] const float stops[], // [count] size_t count) { // Stops cannot be empty. if (count == 0) { return false; } for (size_t i = 0; i < count; ++i) { // Stops must be finite, real numbers in the range [0, 1]. if (!(0 <= stops[i] && stops[i] <= 1)) { return false; } } for (size_t i = 1; i < count; ++i) { // Stops must be ordered. if (!(stops[i - 1] <= stops[i])) { return false; } } return true; } rcp Gradient::MakeLinear(float sx, float sy, float ex, float ey, const ColorInt colors[], // [count] const float stops[], // [count] size_t count) { if (!validate_gradient_stops(colors, stops, count)) { return nullptr; } float2 start = {sx, sy}; float2 end = {ex, ey}; GradDataArray newColors(colors, count); GradDataArray newStops(stops, count); // If the stops don't begin and end on 0 and 1, transform the gradient so // they do. This allows us to take full advantage of the gradient's range of // pixels in the texture. float firstStop = stops[0]; float lastStop = stops[count - 1]; if ((firstStop != 0 || lastStop != 1) && lastStop - firstStop > math::EPSILON) { // Tighten the endpoints to align with the mininum and maximum gradient // stops. float4 newEndpoints = simd::precise_mix(start.xyxy, end.xyxy, float4{firstStop, firstStop, lastStop, lastStop}); start = newEndpoints.xy; end = newEndpoints.zw; newStops[0] = 0; newStops[count - 1] = 1; if (count > 2) { // Transform the stops into the range defined by the new endpoints. float m = 1.f / (lastStop - firstStop); float a = -firstStop * m; for (size_t i = 1; i < count - 1; ++i) { newStops[i] = stops[i] * m + a; } // Clamp the interior stops so they remain monotonically increasing. // newStops[0] and newStops[count - 1] are already 0 and 1, so this // also ensures they stay within 0..1. for (size_t i = 1; i < count - 1; ++i) { newStops[i] = fmaxf(newStops[i - 1], newStops[i]); } for (size_t i = count - 2; i != 0; --i) { newStops[i] = fminf(newStops[i], newStops[i + 1]); } } assert(validate_gradient_stops(newColors.get(), newStops.get(), count)); } float2 v = end - start; v *= 1.f / simd::dot(v, v); // dot(v, end - start) == 1 return rcp(new Gradient(gpu::PaintType::linearGradient, std::move(newColors), std::move(newStops), count, v.x, v.y, -simd::dot(v, start))); } rcp Gradient::MakeRadial(float cx, float cy, float radius, const ColorInt colors[], // [count] const float stops[], // [count] size_t count) { if (!validate_gradient_stops(colors, stops, count)) { return nullptr; } GradDataArray newColors(colors, count); GradDataArray newStops(stops, count); // If the stops don't end on 1, scale the gradient so they do. This allows // us to take better advantage of the gradient's full range of pixels in the // texture. // // TODO: If we want to take full advantage of the gradient texture pixels, // we could add an inner radius that specifies where t=0 begins (instead of // assuming it begins at the center). float lastStop = stops[count - 1]; if (lastStop != 1 && lastStop > math::EPSILON) { // Update the gradient to finish on 1. newStops[count - 1] = 1; // Scale the radius to align with the final stop. radius *= lastStop; // Scale the stops into the range defined by the new radius. float inverseLastStop = 1.f / lastStop; for (size_t i = 0; i < count - 1; ++i) { newStops[i] = stops[i] * inverseLastStop; } if (count > 1) { // Clamp the stops so they remain monotonically increasing. // newStops[count - 1] is already 1, so this also ensures they stay // within 0..1. newStops[0] = fmaxf(0, newStops[0]); for (size_t i = 1; i < count - 1; ++i) { newStops[i] = fmaxf(newStops[i - 1], newStops[i]); } for (size_t i = count - 2; i != -1; --i) { newStops[i] = fminf(newStops[i], newStops[i + 1]); } } assert(validate_gradient_stops(newColors.get(), newStops.get(), count)); } return rcp(new Gradient(gpu::PaintType::radialGradient, std::move(newColors), std::move(newStops), count, cx, cy, radius)); } bool Gradient::isOpaque() const { if (m_isOpaque == gpu::TriState::unknown) { ColorInt allColors = ~0; for (int i = 0; i < m_count; ++i) { allColors &= m_colors[i]; } m_isOpaque = colorAlpha(allColors) == 0xff ? gpu::TriState::yes : gpu::TriState::no; } return m_isOpaque == gpu::TriState::yes; } rcp Gradient::getModulated(float opacity) const { // Fast path: no modulation needed if (opacity == 1.0f) { return ref_rcp(const_cast(this)); } // Check single-entry cache if (m_lastModulatedOpacity == opacity && m_lastModulatedGradient) { return m_lastModulatedGradient; } // Create new modulated gradient GradDataArray newColors(m_count); for (size_t i = 0; i < m_count; ++i) { newColors[i] = colorModulateOpacity(m_colors[i], opacity); } GradDataArray newStops(m_stops.get(), m_count); m_lastModulatedGradient = rcp(new Gradient(m_paintType, std::move(newColors), std::move(newStops), m_count, m_coeffs[0], m_coeffs[1], m_coeffs[2])); m_lastModulatedOpacity = opacity; return m_lastModulatedGradient; } } // namespace rive::gpu