blob: 3fab1d6357e9552719dd6fb3c67f2507633817a8 [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include "pls_paint.hpp"
#include "rive/pls/pls_image.hpp"
namespace rive::pls
{
PLSPaint::PLSPaint() {}
PLSPaint::~PLSPaint() {}
// 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<PLSGradient> PLSGradient::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};
PLSGradDataArray<ColorInt> newColors(colors, count);
PLSGradDataArray<float> 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 PLSGradient(PaintType::linearGradient,
std::move(newColors),
std::move(newStops),
count,
v.x,
v.y,
-simd::dot(v, start)));
}
rcp<PLSGradient> PLSGradient::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;
}
PLSGradDataArray<ColorInt> newColors(colors, count);
PLSGradDataArray<float> 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 PLSGradient(PaintType::radialGradient,
std::move(newColors),
std::move(newStops),
count,
cx,
cy,
radius));
}
bool PLSGradient::isOpaque() const
{
if (m_isOpaque == pls::TriState::unknown)
{
ColorInt allColors = ~0;
for (int i = 0; i < m_count; ++i)
{
allColors &= m_colors[i];
}
m_isOpaque = colorAlpha(allColors) == 0xff ? pls::TriState::yes : pls::TriState::no;
}
return m_isOpaque == pls::TriState::yes;
}
void PLSPaint::color(ColorInt color)
{
m_paintType = PaintType::solidColor;
m_simpleValue.color = color;
m_gradient.reset();
m_imageTexture.reset();
}
void PLSPaint::shader(rcp<RenderShader> shader)
{
m_gradient = static_rcp_cast<PLSGradient>(std::move(shader));
m_paintType = m_gradient ? m_gradient->paintType() : PaintType::solidColor;
// m_simpleValue.colorRampLocation is unused at this level. A new location for a this gradient's
// color ramp will decided by the render context every frame.
m_simpleValue.color = 0xff000000;
m_imageTexture.reset();
}
void PLSPaint::image(rcp<const PLSTexture> imageTexture, float opacity)
{
m_paintType = PaintType::image;
m_simpleValue.imageOpacity = opacity;
m_gradient.reset();
m_imageTexture = std::move(imageTexture);
}
void PLSPaint::clipUpdate(uint32_t outerClipID)
{
m_paintType = PaintType::clipUpdate;
m_simpleValue.outerClipID = outerClipID;
m_gradient.reset();
m_imageTexture.reset();
}
bool PLSPaint::getIsOpaque() const
{
switch (m_paintType)
{
case pls::PaintType::solidColor:
return colorAlpha(m_simpleValue.color) == 0xff;
case pls::PaintType::linearGradient:
case pls::PaintType::radialGradient:
return m_gradient->isOpaque();
case pls::PaintType::image:
case pls::PaintType::clipUpdate:
return false;
}
RIVE_UNREACHABLE();
}
} // namespace rive::pls