diff --git a/apps/docs/static/img/advanced-image-filters/arithmetic-neon-glow.png b/apps/docs/static/img/advanced-image-filters/arithmetic-neon-glow.png new file mode 100644 index 0000000000..d7117ba39f Binary files /dev/null and b/apps/docs/static/img/advanced-image-filters/arithmetic-neon-glow.png differ diff --git a/apps/docs/static/img/advanced-image-filters/crop-viewport-portal.png b/apps/docs/static/img/advanced-image-filters/crop-viewport-portal.png new file mode 100644 index 0000000000..dd52a56e99 Binary files /dev/null and b/apps/docs/static/img/advanced-image-filters/crop-viewport-portal.png differ diff --git a/apps/docs/static/img/advanced-image-filters/empty-silhouette.png b/apps/docs/static/img/advanced-image-filters/empty-silhouette.png new file mode 100644 index 0000000000..f94268e4e6 Binary files /dev/null and b/apps/docs/static/img/advanced-image-filters/empty-silhouette.png differ diff --git a/apps/docs/static/img/advanced-image-filters/makeimage-mosaic.png b/apps/docs/static/img/advanced-image-filters/makeimage-mosaic.png new file mode 100644 index 0000000000..46f26ee01e Binary files /dev/null and b/apps/docs/static/img/advanced-image-filters/makeimage-mosaic.png differ diff --git a/apps/docs/static/img/advanced-image-filters/matrix-convolution-embossed-metal.png b/apps/docs/static/img/advanced-image-filters/matrix-convolution-embossed-metal.png new file mode 100644 index 0000000000..003babb103 Binary files /dev/null and b/apps/docs/static/img/advanced-image-filters/matrix-convolution-embossed-metal.png differ diff --git a/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice.png b/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice.png new file mode 100644 index 0000000000..222c7ac5f1 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice.png differ diff --git a/apps/docs/static/img/lighting-image-filters/distant-lit-diffuse.png b/apps/docs/static/img/lighting-image-filters/distant-lit-diffuse.png new file mode 100644 index 0000000000..5c4eac3d5a Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/distant-lit-diffuse.png differ diff --git a/apps/docs/static/img/lighting-image-filters/distant-lit-specular.png b/apps/docs/static/img/lighting-image-filters/distant-lit-specular.png new file mode 100644 index 0000000000..2e8ce4311b Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/distant-lit-specular.png differ diff --git a/apps/docs/static/img/lighting-image-filters/point-lit-diffuse.png b/apps/docs/static/img/lighting-image-filters/point-lit-diffuse.png new file mode 100644 index 0000000000..127955a607 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/point-lit-diffuse.png differ diff --git a/apps/docs/static/img/lighting-image-filters/point-lit-specular.png b/apps/docs/static/img/lighting-image-filters/point-lit-specular.png new file mode 100644 index 0000000000..9ae34a29d2 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/point-lit-specular.png differ diff --git a/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse.png b/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse.png new file mode 100644 index 0000000000..3caa0d0d2a Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse.png differ diff --git a/apps/docs/static/img/lighting-image-filters/spot-lit-specular.png b/apps/docs/static/img/lighting-image-filters/spot-lit-specular.png new file mode 100644 index 0000000000..80c57e7647 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/spot-lit-specular.png differ diff --git a/packages/skia/cpp/api/JsiSkImageFilterFactory.h b/packages/skia/cpp/api/JsiSkImageFilterFactory.h index 3488401b04..b9017b106b 100644 --- a/packages/skia/cpp/api/JsiSkImageFilterFactory.h +++ b/packages/skia/cpp/api/JsiSkImageFilterFactory.h @@ -7,12 +7,14 @@ #include "JsiSkHostObjects.h" #include "JsiSkImageFilter.h" +#include "JsiSkPicture.h" #include "JsiSkRuntimeShaderBuilder.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" #include "include/core/SkImageFilter.h" +#include "include/core/SkPoint3.h" #pragma clang diagnostic pop @@ -20,46 +22,64 @@ namespace RNSkia { namespace jsi = facebook::jsi; +inline bool hasOptionalArgument(const jsi::Value *arguments, size_t count, + size_t index) { + return (index < count && !arguments[index].isNull() && + !arguments[index].isUndefined()); +} + class JsiSkImageFilterFactory : public JsiSkHostObject { public: JSI_HOST_FUNCTION(MakeBlur) { float sigmaX = arguments[0].asNumber(); float sigmaY = arguments[1].asNumber(); int tileMode = arguments[2].asNumber(); - sk_sp imageFilter; - if (!arguments[3].isNull()) { + sk_sp imageFilter = nullptr; + if (hasOptionalArgument(arguments, count, 3)) { imageFilter = JsiSkImageFilter::fromValue(runtime, arguments[3]); } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 4)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[4]); + } return jsi::Object::createFromHostObject( runtime, std::make_shared( getContext(), SkImageFilters::Blur(sigmaX, sigmaY, (SkTileMode)tileMode, - imageFilter))); + imageFilter, cropRect))); } JSI_HOST_FUNCTION(MakeColorFilter) { auto cf = JsiSkColorFilter::fromValue(runtime, arguments[0]); - sk_sp input; - if (!arguments[1].isNull()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 1)) { input = JsiSkImageFilter::fromValue(runtime, arguments[1]); } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 2)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[2]); + } return jsi::Object::createFromHostObject( runtime, std::make_shared( getContext(), SkImageFilters::ColorFilter( - std::move(cf), std::move(input)))); + std::move(cf), std::move(input), cropRect))); } JSI_HOST_FUNCTION(MakeOffset) { auto x = arguments[0].asNumber(); auto y = arguments[1].asNumber(); - sk_sp input; - if (!arguments[2].isNull()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { input = JsiSkImageFilter::fromValue(runtime, arguments[2]); } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 3)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[3]); + } return jsi::Object::createFromHostObject( runtime, std::make_shared( - getContext(), SkImageFilters::Offset(x, y, std::move(input)))); + getContext(), SkImageFilters::Offset(x, y, std::move(input), cropRect))); } JSI_HOST_FUNCTION(MakeDisplacementMap) { @@ -69,32 +89,45 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { static_cast(arguments[1].asNumber()); auto scale = arguments[2].asNumber(); auto in2 = JsiSkImageFilter::fromValue(runtime, arguments[3]); - sk_sp input; - if (!arguments[4].isNull()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 4)) { input = JsiSkImageFilter::fromValue(runtime, arguments[4]); } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 5)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[5]); + } return jsi::Object::createFromHostObject( runtime, std::make_shared( getContext(), SkImageFilters::DisplacementMap( fXChannelSelector, fYChannelSelector, scale, - std::move(in2), std::move(input)))); + std::move(in2), std::move(input), cropRect))); } JSI_HOST_FUNCTION(MakeShader) { auto shader = JsiSkShader::fromValue(runtime, arguments[0]); + SkImageFilters::Dither dither = SkImageFilters::Dither::kNo; + if (hasOptionalArgument(arguments, count, 1)) { + dither = arguments[1].asBool() ? SkImageFilters::Dither::kYes + : SkImageFilters::Dither::kNo; + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 2)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[2]); + } return jsi::Object::createFromHostObject( runtime, std::make_shared( - getContext(), SkImageFilters::Shader(std::move(shader)))); + getContext(), SkImageFilters::Shader(std::move(shader), dither, cropRect))); } JSI_HOST_FUNCTION(MakeCompose) { - sk_sp outer; - if (!arguments[0].isNull() && !arguments[0].isUndefined()) { + sk_sp outer = nullptr; + if (hasOptionalArgument(arguments, count, 0)) { outer = JsiSkImageFilter::fromValue(runtime, arguments[0]); } - sk_sp inner; - if (!arguments[1].isNull() && !arguments[1].isUndefined()) { + sk_sp inner = nullptr; + if (hasOptionalArgument(arguments, count, 1)) { inner = JsiSkImageFilter::fromValue(runtime, arguments[1]); } return jsi::Object::createFromHostObject( @@ -109,12 +142,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { JsiSkImageFilter::fromValue(runtime, arguments[1]); sk_sp foreground = nullptr; - if (count > 2 && !arguments[2].isNull()) { + if (hasOptionalArgument(arguments, count, 2)) { foreground = JsiSkImageFilter::fromValue(runtime, arguments[2]); } SkImageFilters::CropRect cropRect = {}; - if (count > 3 && !arguments[3].isUndefined()) { + if (hasOptionalArgument(arguments, count, 3)) { cropRect = *JsiSkRect::fromValue(runtime, arguments[3]); } @@ -131,12 +164,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { auto sigmaX = arguments[2].asNumber(); auto sigmaY = arguments[3].asNumber(); auto color = JsiSkColor::fromValue(runtime, arguments[4]); - sk_sp input; - if (!arguments[5].isNull() && !arguments[5].isUndefined()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { input = JsiSkImageFilter::fromValue(runtime, arguments[5]); } SkImageFilters::CropRect cropRect = {}; - if (count > 6 && !arguments[6].isUndefined()) { + if (hasOptionalArgument(arguments, count, 6)) { cropRect = *JsiSkRect::fromValue(runtime, arguments[6]); } return jsi::Object::createFromHostObject( @@ -152,12 +185,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { auto sigmaX = arguments[2].asNumber(); auto sigmaY = arguments[3].asNumber(); auto color = JsiSkColor::fromValue(runtime, arguments[4]); - sk_sp input; - if (!arguments[5].isNull() && !arguments[5].isUndefined()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { input = JsiSkImageFilter::fromValue(runtime, arguments[5]); } SkImageFilters::CropRect cropRect = {}; - if (count > 6 && !arguments[6].isUndefined()) { + if (hasOptionalArgument(arguments, count, 6)) { cropRect = *JsiSkRect::fromValue(runtime, arguments[6]); } return jsi::Object::createFromHostObject( @@ -170,12 +203,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { JSI_HOST_FUNCTION(MakeErode) { auto rx = arguments[0].asNumber(); auto ry = arguments[1].asNumber(); - sk_sp input; - if (!arguments[2].isNull() && !arguments[2].isUndefined()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { input = JsiSkImageFilter::fromValue(runtime, arguments[2]); } SkImageFilters::CropRect cropRect = {}; - if (count > 3 && !arguments[3].isUndefined()) { + if (hasOptionalArgument(arguments, count, 3)) { cropRect = *JsiSkRect::fromValue(runtime, arguments[3]); } return jsi::Object::createFromHostObject( @@ -187,12 +220,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { JSI_HOST_FUNCTION(MakeDilate) { auto rx = arguments[0].asNumber(); auto ry = arguments[1].asNumber(); - sk_sp input; - if (!arguments[2].isNull() && !arguments[2].isUndefined()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { input = JsiSkImageFilter::fromValue(runtime, arguments[2]); } SkImageFilters::CropRect cropRect = {}; - if (count > 3 && !arguments[3].isUndefined()) { + if (hasOptionalArgument(arguments, count, 3)) { cropRect = *JsiSkRect::fromValue(runtime, arguments[3]); } return jsi::Object::createFromHostObject( @@ -205,12 +238,12 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { auto rtb = JsiSkRuntimeShaderBuilder::fromValue(runtime, arguments[0]); const char *childName = ""; - if (!arguments[1].isNull() && !arguments[1].isUndefined()) { + if (hasOptionalArgument(arguments, count, 1)) { childName = arguments[1].asString(runtime).utf8(runtime).c_str(); } - sk_sp input; - if (!arguments[2].isNull() && !arguments[2].isUndefined()) { + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { input = JsiSkImageFilter::fromValue(runtime, arguments[2]); } return jsi::Object::createFromHostObject( @@ -219,6 +252,393 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { *rtb, childName, std::move(input)))); } + JSI_HOST_FUNCTION(MakeArithmetic) { + float k1 = arguments[0].asNumber(); + float k2 = arguments[1].asNumber(); + float k3 = arguments[2].asNumber(); + float k4 = arguments[3].asNumber(); + bool enforcePMColor = arguments[4].asBool(); + sk_sp background = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { + background = JsiSkImageFilter::fromValue(runtime, arguments[5]); + } + sk_sp foreground = nullptr; + if (hasOptionalArgument(arguments, count, 6)) { + foreground = JsiSkImageFilter::fromValue(runtime, arguments[6]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 7)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[7]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), + SkImageFilters::Arithmetic( + k1, k2, k3, k4, enforcePMColor, std::move(background), + std::move(foreground), cropRect))); + } + + JSI_HOST_FUNCTION(MakeCrop) { + SkRect rect = *JsiSkRect::fromValue(runtime, arguments[0]); + SkTileMode tileMode = SkTileMode::kDecal; + if (hasOptionalArgument(arguments, count, 1)) { + tileMode = (SkTileMode)arguments[1].asNumber(); + } + sk_sp imageFilter = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { + imageFilter = JsiSkImageFilter::fromValue(runtime, arguments[2]); + } + return jsi::Object::createFromHostObject( + runtime, + std::make_shared( + getContext(), + SkImageFilters::Crop(rect, tileMode, std::move(imageFilter)))); + } + + JSI_HOST_FUNCTION(MakeEmpty) { + return jsi::Object::createFromHostObject( + runtime, std::make_shared(getContext(), + SkImageFilters::Empty())); + } + + inline SkPoint3 SkPoint3FromValue(jsi::Runtime &runtime, + const jsi::Value &obj) { + const auto &object = obj.asObject(runtime); + auto x = object.getProperty(runtime, "x").asNumber(); + auto y = object.getProperty(runtime, "y").asNumber(); + auto z = object.getProperty(runtime, "z").asNumber(); + return SkPoint3::Make(x, y, z); + } + + JSI_HOST_FUNCTION(MakeDistantLitDiffuse) { + SkPoint3 direction = SkPoint3FromValue(runtime, arguments[0]); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[1]); + float surfaceScale = arguments[2].asNumber(); + float kd = arguments[3].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 4)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[4]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 5)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[5]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::DistantLitDiffuse( + direction, lightColor, surfaceScale, kd, + std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakePointLitDiffuse) { + SkPoint3 location = SkPoint3FromValue(runtime, arguments[0]); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[1]); + float surfaceScale = arguments[2].asNumber(); + float kd = arguments[3].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 4)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[4]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 5)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[5]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::PointLitDiffuse( + location, lightColor, surfaceScale, kd, + std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakeSpotLitDiffuse) { + SkPoint3 location = SkPoint3FromValue(runtime, arguments[0]); + SkPoint3 target = SkPoint3FromValue(runtime, arguments[1]); + float falloffExponent = arguments[2].asNumber(); + float cutoffAngle = arguments[3].asNumber(); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[4]); + float surfaceScale = arguments[5].asNumber(); + float kd = arguments[6].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 7)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[7]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 8)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[8]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::SpotLitDiffuse( + location, target, falloffExponent, + cutoffAngle, lightColor, surfaceScale, + kd, std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakeDistantLitSpecular) { + SkPoint3 direction = SkPoint3FromValue(runtime, arguments[0]); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[1]); + float surfaceScale = arguments[2].asNumber(); + float ks = arguments[3].asNumber(); + float shininess = arguments[4].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[5]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 6)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[6]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::DistantLitSpecular( + direction, lightColor, surfaceScale, ks, + shininess, std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakePointLitSpecular) { + SkPoint3 location = SkPoint3FromValue(runtime, arguments[0]); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[1]); + float surfaceScale = arguments[2].asNumber(); + float ks = arguments[3].asNumber(); + float shininess = arguments[4].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[5]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 6)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[6]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::PointLitSpecular( + location, lightColor, surfaceScale, ks, + shininess, std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakeSpotLitSpecular) { + SkPoint3 location = SkPoint3FromValue(runtime, arguments[0]); + SkPoint3 target = SkPoint3FromValue(runtime, arguments[1]); + float falloffExponent = arguments[2].asNumber(); + float cutoffAngle = arguments[3].asNumber(); + SkColor lightColor = JsiSkColor::fromValue(runtime, arguments[4]); + float surfaceScale = arguments[5].asNumber(); + float ks = arguments[6].asNumber(); + float shininess = arguments[7].asNumber(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 8)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[8]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 9)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[9]); + } + return jsi::Object::createFromHostObject( + runtime, + std::make_shared( + getContext(), + SkImageFilters::SpotLitSpecular( + location, target, falloffExponent, cutoffAngle, lightColor, + surfaceScale, ks, shininess, std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakeImage) { + sk_sp image = JsiSkImage::fromValue(runtime, arguments[0]); + SkRect srcRect; + if (hasOptionalArgument(arguments, count, 1)) { + srcRect = *JsiSkRect::fromValue(runtime, arguments[1]); + } else { + srcRect = SkRect::Make(image->bounds()); + } + SkRect dstRect; + if (hasOptionalArgument(arguments, count, 2)) { + dstRect = *JsiSkRect::fromValue(runtime, arguments[2]); + } else { + dstRect = srcRect; + } + SkFilterMode filterMode = SkFilterMode::kNearest; + if (hasOptionalArgument(arguments, count, 3)) { + filterMode = (SkFilterMode)arguments[3].asNumber(); + } + SkMipmapMode mipmap = SkMipmapMode::kNone; + if (hasOptionalArgument(arguments, count, 4)) { + mipmap = (SkMipmapMode)arguments[4].asNumber(); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::Image( + std::move(image), srcRect, dstRect, + SkSamplingOptions(filterMode, mipmap)))); + } + + JSI_HOST_FUNCTION(MakeMagnifier) { + SkRect lensBounds = *JsiSkRect::fromValue(runtime, arguments[0]); + float zoomAmount = arguments[1].asNumber(); + float inset = arguments[2].asNumber(); + SkFilterMode filterMode = SkFilterMode::kNearest; + if (hasOptionalArgument(arguments, count, 3)) { + filterMode = (SkFilterMode)arguments[3].asNumber(); + } + SkMipmapMode mipmap = SkMipmapMode::kNone; + if (hasOptionalArgument(arguments, count, 4)) { + mipmap = (SkMipmapMode)arguments[4].asNumber(); + } + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 5)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[5]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 6)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[6]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), SkImageFilters::Magnifier( + lensBounds, zoomAmount, inset, + SkSamplingOptions(filterMode, mipmap), + input, cropRect))); + } + + JSI_HOST_FUNCTION(MakeMatrixConvolution) { + SkISize kernelSize = + SkISize(arguments[0].asNumber(), arguments[1].asNumber()); + std::vector kernel; + auto kernelArray = arguments[2].asObject(runtime).asArray(runtime); + auto size = kernelArray.size(runtime); + for (size_t i = 0; i < size; i++) { + kernel.push_back(kernelArray.getValueAtIndex(runtime, i).asNumber()); + } + auto gain = arguments[3].asNumber(); + auto bias = arguments[4].asNumber(); + SkIPoint kernelOffset = + SkIPoint(arguments[5].asNumber(), arguments[6].asNumber()); + SkTileMode tileMode = (SkTileMode)arguments[7].asNumber(); + bool convolveAlpha = arguments[8].asBool(); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 9)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[9]); + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 10)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[10]); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), + SkImageFilters::MatrixConvolution( + kernelSize, kernel.data(), gain, bias, kernelOffset, + tileMode, convolveAlpha, std::move(input), cropRect))); + } + + JSI_HOST_FUNCTION(MakeMatrixTransform) { + SkMatrix matrix = *JsiSkMatrix::fromValue(runtime, arguments[0]); + SkFilterMode filterMode = SkFilterMode::kNearest; + if (hasOptionalArgument(arguments, count, 1)) { + filterMode = (SkFilterMode)arguments[1].asNumber(); + } + SkMipmapMode mipmap = SkMipmapMode::kNone; + if (hasOptionalArgument(arguments, count, 2)) { + mipmap = (SkMipmapMode)arguments[2].asNumber(); + } + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 3)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[3]); + } + return jsi::Object::createFromHostObject( + runtime, + std::make_shared( + getContext(), SkImageFilters::MatrixTransform( + matrix, SkSamplingOptions(filterMode, mipmap), + std::move(input)))); + } + + JSI_HOST_FUNCTION(MakeMerge) { + std::vector> filters; + auto filtersArray = arguments[0].asObject(runtime).asArray(runtime); + auto filtersCount = filtersArray.size(runtime); + for (size_t i = 0; i < filtersCount; ++i) { + auto element = filtersArray.getValueAtIndex(runtime, i); + if (element.isNull()) { + filters.push_back(nullptr); + } else { + filters.push_back(JsiSkImageFilter::fromValue(runtime, element)); + } + } + SkImageFilters::CropRect cropRect = {}; + if (hasOptionalArgument(arguments, count, 1)) { + cropRect = *JsiSkRect::fromValue(runtime, arguments[1]); + } + return jsi::Object::createFromHostObject( + runtime, + std::make_shared( + getContext(), + SkImageFilters::Merge(filters.data(), filtersCount, cropRect))); + } + + JSI_HOST_FUNCTION(MakePicture) { + sk_sp picture = JsiSkPicture::fromValue(runtime, arguments[0]); + SkRect targetRect; + if (hasOptionalArgument(arguments, count, 1)) { + targetRect = *JsiSkRect::fromValue(runtime, arguments[1]); + } else { + targetRect = picture ? picture->cullRect() : SkRect::MakeEmpty(); + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), + SkImageFilters::Picture(std::move(picture), targetRect))); + } + + JSI_HOST_FUNCTION(MakeRuntimeShaderWithChildren) { + auto rtb = JsiSkRuntimeShaderBuilder::fromValue(runtime, arguments[0]); + float maxSampleRadius = arguments[1].asNumber(); + std::vector childNames; + auto childNamesJS = arguments[2].asObject(runtime).asArray(runtime); + size_t length = childNamesJS.size(runtime); + for (size_t i = 0; i < length; ++i) { + auto element = childNamesJS.getValueAtIndex(runtime, i); + childNames.push_back(element.asString(runtime).utf8(runtime).c_str()); + } + std::vector childNamesStringView; + childNamesStringView.reserve(childNames.size()); + for (const auto &name : childNames) { + childNamesStringView.push_back(std::string_view(name)); + } + + std::vector> inputs; + auto inputsJS = arguments[3].asObject(runtime).asArray(runtime); + if (inputsJS.size(runtime) != length) { + return jsi::Value::null(); + } + for (size_t i = 0; i < length; ++i) { + auto element = inputsJS.getValueAtIndex(runtime, i); + if (element.isNull()) { + inputs.push_back(nullptr); + } else { + inputs.push_back(JsiSkImageFilter::fromValue(runtime, element)); + } + } + return jsi::Object::createFromHostObject( + runtime, std::make_shared( + getContext(), + SkImageFilters::RuntimeShader(*rtb, maxSampleRadius, + childNamesStringView.data(), + inputs.data(), length))); + } + + JSI_HOST_FUNCTION(MakeTile) { + SkRect src = *JsiSkRect::fromValue(runtime, arguments[0]); + SkRect dst = *JsiSkRect::fromValue(runtime, arguments[1]); + sk_sp input = nullptr; + if (hasOptionalArgument(arguments, count, 2)) { + input = JsiSkImageFilter::fromValue(runtime, arguments[2]); + } + return jsi::Object::createFromHostObject( + runtime, + std::make_shared( + getContext(), SkImageFilters::Tile(src, dst, std::move(input)))); + } + JSI_EXPORT_FUNCTIONS( JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeBlur), JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeOffset), @@ -231,7 +651,24 @@ class JsiSkImageFilterFactory : public JsiSkHostObject { JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeBlend), JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeDropShadow), JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeDropShadowOnly), - JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeRuntimeShader)) + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeRuntimeShader), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeArithmetic), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeCrop), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeEmpty), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeImage), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeMagnifier), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeMatrixConvolution), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeMatrixTransform), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeMerge), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakePicture), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeRuntimeShaderWithChildren), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeTile), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeDistantLitDiffuse), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakePointLitDiffuse), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeSpotLitDiffuse), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeDistantLitSpecular), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakePointLitSpecular), + JSI_EXPORT_FUNC(JsiSkImageFilterFactory, MakeSpotLitSpecular)) explicit JsiSkImageFilterFactory(std::shared_ptr context) : JsiSkHostObject(std::move(context)) {} diff --git a/packages/skia/src/renderer/__tests__/e2e/AdvancedImageFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/AdvancedImageFilters.spec.tsx new file mode 100644 index 0000000000..e739bbb476 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/AdvancedImageFilters.spec.tsx @@ -0,0 +1,492 @@ +import { docPath, checkImage, itRunsE2eOnly } from "../../../__tests__/setup"; +import { + BlendMode, + FilterMode, + MipmapMode, + PaintStyle, + TileMode, +} from "../../../skia/types"; +import { surface, importSkia, images } from "../setup"; + +const checkResult = (base64: string, path: string) => { + const { Skia } = importSkia(); + const rData = Skia.Data.fromBase64(base64); + const image = Skia.Image.MakeImageFromEncoded(rData)!; + expect(rData).toBeDefined(); + checkImage(image, docPath(path)); +}; + +describe("Advanced Image Filters", () => { + itRunsE2eOnly("Arithmetic - Neon Glow", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.Make(768, 768)!; + const canvas = sur.getCanvas(); + const paint = Skia.Paint(); + // Create a blurred version of the image for the glow effect + const CLAMP = 0; + const blurFilter = Skia.ImageFilter.MakeBlur(5.0, 5.0, CLAMP); + + // Original image + const originalImage = null; // This uses the source bitmap + + // Neon Glow effect + const neonGlowFilter = Skia.ImageFilter.MakeArithmetic( + 0.0, // k1: No multiplication between foreground and background + 1.5, // k2: Amplify the blurred foreground (the glow) + 1.0, // k3: Keep the original background fully + 0.0, // k4: No constant addition + true, // enforcePMColor: Clamp RGB channels to alpha + originalImage, // background: Original sharp image + blurFilter, // foreground: Blurred version creates the glow + null // cropRect: No cropping + ); + paint.setImageFilter(neonGlowFilter); + canvas.drawImage(ctx.skiaLogoPng, 0, 0, paint); + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng } + ); + checkResult(base64, "advanced-image-filters/arithmetic-neon-glow.png"); + }); + + itRunsE2eOnly("Crop - Viewport Portal", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.Make(768, 768)!; + const canvas = sur.getCanvas(); + + // Draw background image first + canvas.drawImage(ctx.skiaLogoPng, 0, 0); + + // Create a paint for our portal effect + const paint = Skia.Paint(); + + // Create an input filter that applies a colorization and rotation to the secondary image + const colorMatrix = [ + 0.8, + 0.2, + 0, + 0, + 0, // Add some red tint + 0.2, + 0.8, + 0, + 0, + 0, // Add some green tint + 0, + 0.2, + 1.0, + 0, + 0, // Add some blue boost + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]; + + // Create color matrix filter + const colorFilter = Skia.ColorFilter.MakeMatrix(colorMatrix); + + // Create the image filter from the secondary image + const secondaryImageFilter = Skia.ImageFilter.MakeColorFilter( + colorFilter, + Skia.ImageFilter.MakeImage(ctx.skiaLogoPng) + ); + + // Define a circular viewport in the center + const centerX = 384; + const centerY = 384; + const radius = 150; + + // Create a circular crop rect (positioned to center of canvas) + const cropRect = Skia.XYWHRect( + centerX - radius, + centerY - radius, + radius * 2, + radius * 2 + ); + + // Use MIRROR tiling mode for an interesting effect at the edges + const MIRROR = ctx.TileMode.Mirror; + + // Create the crop filter with mirror tiling + const portalFilter = Skia.ImageFilter.MakeCrop( + cropRect, + MIRROR, + secondaryImageFilter + ); + + // Set the filter to our paint + paint.setImageFilter(portalFilter); + + // Draw a circle that will contain our portal effect + canvas.drawCircle(centerX, centerY, radius, paint); + + // Optional: Add a stroke around the portal for definition + const strokePaint = Skia.Paint(); + strokePaint.setStyle(ctx.PaintStyle.Stroke); + strokePaint.setStrokeWidth(8); + strokePaint.setColor(Skia.Color("white")); + canvas.drawCircle(centerX, centerY, radius, strokePaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, PaintStyle, TileMode } + ); + checkResult(base64, "advanced-image-filters/crop-viewport-portal.png"); + }); + + itRunsE2eOnly("Empty - Silhouette Effect", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.Make(768, 768)!; + const canvas = sur.getCanvas(); + + // Fill background with a gradient + const bgPaint = Skia.Paint(); + const shader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 768, y: 768 }, + [ + Skia.Color("rgba(20, 20, 100, 1)"), + Skia.Color("rgba(100, 40, 150, 1)"), + ], + null, + ctx.TileMode.Clamp + ); + bgPaint.setShader(shader); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create our silhouette effect + const paint = Skia.Paint(); + + // First, we'll create a mask using the original image and a blur + const imageMask = Skia.ImageFilter.MakeImage(ctx.skiaLogoPng); + const blurredMask = Skia.ImageFilter.MakeBlur( + 3, + 3, + ctx.TileMode.Decal, + imageMask + ); + + // Create an empty filter (transparent black) + const emptyFilter = Skia.ImageFilter.MakeEmpty(); + + // Combine the mask with empty filter using MakeBlend + // This creates a silhouette where the image is + const BLEND_MODE = ctx.BlendMode.Src; + const silhouetteFilter = Skia.ImageFilter.MakeBlend( + BLEND_MODE, + emptyFilter, // Using the empty filter for the black silhouette + blurredMask // Using the blurred image as the mask + ); + + // Set our filter to the paint + paint.setImageFilter(silhouetteFilter); + paint.setColor(Skia.Color("rgba(0,0,0,0.8)")); // Semi-transparent black + + // Draw a rectangle covering the entire canvas with our filter + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), paint); + + // Add some decorative highlights at the edges + const highlightPaint = Skia.Paint(); + highlightPaint.setColor(Skia.Color("rgba(255, 255, 255, 0.4)")); + highlightPaint.setImageFilter(blurredMask); + canvas.drawRect(Skia.XYWHRect(5, 5, 758, 758), highlightPaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, BlendMode, TileMode } + ); + checkResult(base64, "advanced-image-filters/empty-silhouette.png"); + }); + + itRunsE2eOnly("MakeImage - Dynamic Mosaic", async () => { + const { skiaLogoPng, mask } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.Make(768, 768)!; + const canvas = sur.getCanvas(); + + // Draw a background + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("gray")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create a mosaic effect using MakeImage filter + const mosaicPaint = Skia.Paint(); + + // We'll create a 4x4 grid of tiles + const numTiles = 4; + const tileSize = 768 / numTiles; + + // We'll alternate between the two images + const allImages = [ctx.skiaLogoPng, ctx.mask]; + + // Draw each tile with a different srcRect using MakeImage + for (let y = 0; y < numTiles; y++) { + for (let x = 0; x < numTiles; x++) { + // Alternate images based on checkerboard pattern + const imageIndex = (x + y) % 2; + const currentImage = allImages[imageIndex]; + + // Calculate source rectangle (use different areas of the source image) + // We'll create a dynamic effect by using different parts of the image + const srcX = (x * currentImage.width()) / numTiles; + const srcY = (y * currentImage.height()) / numTiles; + const srcWidth = currentImage.width() / numTiles; + const srcHeight = currentImage.height() / numTiles; + + const srcRect = Skia.XYWHRect(srcX, srcY, srcWidth, srcHeight); + + // Calculate destination rectangle with some margin for visual separation + const margin = 8; + const dstX = x * tileSize + margin / 2; + const dstY = y * tileSize + margin / 2; + const dstWidth = tileSize - margin; + const dstHeight = tileSize - margin; + + const dstRect = Skia.XYWHRect(dstX, dstY, dstWidth, dstHeight); + + // Create the image filter with Linear filtering for smoother scaling + const imageFilter = Skia.ImageFilter.MakeImage( + currentImage, + srcRect, + dstRect, + ctx.FilterMode.Linear, + ctx.MipmapMode.Linear + ); + + // Set the filter and draw the tile + mosaicPaint.setImageFilter(imageFilter); + canvas.drawRect(dstRect, mosaicPaint); + + // Add a subtle border around each tile + const borderPaint = Skia.Paint(); + borderPaint.setStyle(ctx.PaintStyle.Stroke); + borderPaint.setStrokeWidth(2); + borderPaint.setColor(Skia.Color("gray")); + canvas.drawRect(dstRect, borderPaint); + } + } + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, FilterMode, MipmapMode, PaintStyle, TileMode, mask } + ); + checkResult(base64, "advanced-image-filters/makeimage-mosaic.png"); + }); + itRunsE2eOnly("MatrixConvolution - Embossed Metallic Effect", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.Make(768, 768)!; + const canvas = sur.getCanvas(); + + // First, let's create a metallic gradient background + const bgPaint = Skia.Paint(); + const shader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 768, y: 768 }, + [ + Skia.Color("rgb(180, 180, 190)"), // Light silver + Skia.Color("rgb(100, 100, 110)"), // Medium gray + Skia.Color("rgb(140, 140, 160)"), // Silver again + Skia.Color("rgb(70, 70, 90)"), // Dark silver + ], + [0.0, 0.3, 0.7, 1.0], + ctx.TileMode.Clamp + ); + bgPaint.setShader(shader); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create our emboss effect paint + const embossPaint = Skia.Paint(); + + // For an emboss effect, we use a 3x3 kernel that highlights + // edges in a directional way (typically top-left to bottom-right) + // This creates the illusion of depth + const kernelSizeX = 3; + const kernelSizeY = 3; + + // Emboss kernel - this will create a 3D effect with light coming from top-left + const kernel = [ + -2, + -1, + 0, // Top row + -1, + 1, + 1, // Middle row + 0, + 1, + 2, // Bottom row + ]; + + // Set kernel parameters + const gain = 1.0; // Standard scaling + const bias = 128.0; // Add a mid-gray to make the effect visible + + // Center the kernel over each pixel + const kernelOffsetX = 1; + const kernelOffsetY = 1; + + // Create the matrix convolution filter + const embossFilter = Skia.ImageFilter.MakeMatrixConvolution( + kernelSizeX, + kernelSizeY, + kernel, + gain, + bias, + kernelOffsetX, + kernelOffsetY, + ctx.TileMode.Clamp, // Use clamp for edge pixels + false, // Don't convolve alpha to maintain shape + null, // Use source bitmap as input + null // No crop rect + ); + + // Set the filter to our paint + embossPaint.setImageFilter(embossFilter); + + // To enhance the metallic look, let's add some color desaturation + // Grayscale color matrix with a slight blue-silver tint + const colorMatrix = [ + 0.33, + 0.33, + 0.33, + 0, + 0, // Red channel + 0.33, + 0.33, + 0.33, + 0, + 0, // Green channel + 0.34, + 0.34, + 0.34, + 0, + 10, // Blue channel (slightly boosted) + 0, + 0, + 0, + 1, + 0, // Alpha channel unchanged + ]; + + const colorFilter = Skia.ColorFilter.MakeMatrix(colorMatrix); + embossPaint.setColorFilter(colorFilter); + + // Now let's draw our image with the emboss effect + // Scale down slightly to leave a border + const padding = 40; + const imageRect = Skia.XYWHRect( + padding, + padding, + 768 - padding * 2, + 768 - padding * 2 + ); + + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + imageRect, + embossPaint + ); + + // Add a subtle bevel frame to enhance the metallic appearance + const framePaint = Skia.Paint(); + + // Outer edge - dark shadow + framePaint.setStyle(ctx.PaintStyle.Stroke); + framePaint.setStrokeWidth(8); + framePaint.setColor(Skia.Color("rgba(60, 60, 70, 0.8)")); + canvas.drawRect( + Skia.XYWHRect( + padding - 10, + padding - 10, + 768 - padding * 2 + 20, + 768 - padding * 2 + 20 + ), + framePaint + ); + + // Inner edge - highlight + framePaint.setStrokeWidth(4); + framePaint.setColor(Skia.Color("rgba(220, 220, 230, 0.8)")); + canvas.drawRect( + Skia.XYWHRect( + padding - 4, + padding - 4, + 768 - padding * 2 + 8, + 768 - padding * 2 + 8 + ), + framePaint + ); + + // Add some decorative rivets on the corners to enhance metallic look + const rivetPaint = Skia.Paint(); + rivetPaint.setColor(Skia.Color("rgb(160, 160, 180)")); + + // Draw rivets at the corners + const rivetRadius = 12; + const rivetOffset = 24; + const rivetPositions = [ + { x: padding - rivetOffset, y: padding - rivetOffset }, + { x: 768 - padding + rivetOffset, y: padding - rivetOffset }, + { x: padding - rivetOffset, y: 768 - padding + rivetOffset }, + { x: 768 - padding + rivetOffset, y: 768 - padding + rivetOffset }, + ]; + + // Draw each rivet with a metallic gradient + rivetPositions.forEach((pos) => { + // Rivet base + rivetPaint.setShader( + Skia.Shader.MakeRadialGradient( + { x: pos.x, y: pos.y }, + rivetRadius, + [ + Skia.Color("rgb(200, 200, 210)"), + Skia.Color("rgb(130, 130, 140)"), + ], + [0.2, 1.0], + ctx.TileMode.Clamp + ) + ); + canvas.drawCircle(pos.x, pos.y, rivetRadius, rivetPaint); + + // Rivet highlight + const highlightPaint = Skia.Paint(); + highlightPaint.setColor(Skia.Color("rgba(240, 240, 250, 0.8)")); + canvas.drawCircle( + pos.x - 3, + pos.y - 3, + rivetRadius / 3, + highlightPaint + ); + }); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, TileMode, PaintStyle } + ); + checkResult( + base64, + "advanced-image-filters/matrix-convolution-embossed-metal.png" + ); + }); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/LightingImageFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/LightingImageFilters.spec.tsx new file mode 100644 index 0000000000..a8d05f07e7 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/LightingImageFilters.spec.tsx @@ -0,0 +1,1466 @@ +import { importSkia, surface, images } from "../setup"; +import type { SkColor } from "../../../skia/types"; +import { BlendMode, ClipOp, PaintStyle, TileMode } from "../../../skia/types"; +import { checkImage, docPath, itRunsE2eOnly } from "../../../__tests__/setup"; + +const checkResult = (base64: string, path: string) => { + const { Skia } = importSkia(); + const rData = Skia.Data.fromBase64(base64); + const image = Skia.Image.MakeImageFromEncoded(rData)!; + expect(rData).toBeDefined(); + checkImage(image, docPath(path)); +}; + +describe("Lighting Image Filters", () => { + itRunsE2eOnly("DistantLitDiffuse - Dramatic Relief", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Create a dark background to enhance contrast + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(20, 20, 30)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create high-contrast emboss effect + const paint = Skia.Paint(); + + // Pre-process the image with a threshold filter to make shapes more defined + // This will help the lighting effect appear more dramatic + const inputFilter = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.5, + 0, + 0, + 0, + -20, // Increased contrast for red + 0, + 1.5, + 0, + 0, + -20, // Increased contrast for green + 0, + 0, + 1.5, + 0, + -20, // Increased contrast for blue + 0, + 0, + 0, + 1.2, + 0, // Slightly boosted alpha + ]), + null + ); + + // Direction vector for dramatic side lighting + const direction = { x: -3, y: -0.5, z: 0.5 }; + + // Light color (stark white for contrast) + const lightColor = Skia.Color("rgb(255, 255, 255)"); + + // Higher surface scale for more dramatic relief + const surfaceScale = 4.0; + const kd = 2.5; // Stronger diffuse coefficient + + // Create the distant light diffuse filter + const distantLitFilter = Skia.ImageFilter.MakeDistantLitDiffuse( + direction, + lightColor, + surfaceScale, + kd, + inputFilter, // Use high-contrast image as input + null // No crop rect + ); + + // Set the filter to our paint + paint.setImageFilter(distantLitFilter); + + // Add a subtle color tint + paint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.2, + 0, + 0, + 0, + 10, // Boosted red + 0, + 1.0, + 0, + 0, + 0, // Normal green + 0, + 0, + 0.8, + 0, + 0, // Reduced blue for warmer tone + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]) + ); + + // Draw the image with the dramatic lighting + const padding = 30; + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + Skia.XYWHRect(padding, padding, 768 - padding * 2, 768 - padding * 2), + paint + ); + + // Add a subtle vignette effect to enhance depth + const vignetteRect = Skia.XYWHRect(-100, -100, 968, 968); + const vignettePaint = Skia.Paint(); + const vignetteShader = Skia.Shader.MakeRadialGradient( + { x: 384, y: 384 }, + 500, + [Skia.Color("rgba(0,0,0,0)"), Skia.Color("rgba(0,0,0,0.7)")], + [0.5, 1.0], + ctx.TileMode.Clamp + ); + vignettePaint.setShader(vignetteShader); + canvas.drawRect(vignetteRect, vignettePaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, TileMode } + ); + checkResult(base64, "lighting-image-filters/distant-lit-diffuse.png"); + }); + + itRunsE2eOnly("PointLitDiffuse - Glowing Core", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Create a dark background (not completely black) for better visibility + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(10, 10, 15)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create a glowing center effect + const paint = Skia.Paint(); + + // Position light inside the image for a glowing core effect + // Moved light closer to surface (z is now positive) + const location = { x: 384, y: 384, z: 200 }; + + // Light color (intense orange-yellow for a fiery glow) + const lightColor = Skia.Color("rgb(255, 200, 50)"); + + // Parameters for the filter + const surfaceScale = 2.0; // Height effect + const kd = 1.5; // Diffuse reflection strength + + // Create the point light diffuse filter + const pointLitFilter = Skia.ImageFilter.MakePointLitDiffuse( + location, + lightColor, + surfaceScale, + kd, + null, // Use source bitmap as input + null // No crop rect + ); + + // Set the filter to our paint + paint.setImageFilter(pointLitFilter); + + // Add a color boost to make the effect more visible + paint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.2, + 0, + 0, + 0, + 20, // Boosted red + 0, + 1.1, + 0, + 0, + 10, // Boosted green + 0, + 0, + 1.0, + 0, + 0, // Blue unchanged + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]) + ); + + // Draw the image with the glowing core effect + canvas.drawImage(ctx.skiaLogoPng, 0, 0, paint); + + // Add a second layer of lighting for more intensity + const accentPaint = Skia.Paint(); + const accentFilter = Skia.ImageFilter.MakePointLitDiffuse( + { x: 384, y: 384, z: 50 }, // Closer light source + Skia.Color("rgb(255, 255, 200)"), // Brighter light + surfaceScale * 0.5, + kd * 0.8, + null, + null + ); + + accentPaint.setImageFilter(accentFilter); + accentPaint.setBlendMode(ctx.BlendMode.Plus); // Additive lighting + + // Draw second layer + canvas.drawImage(ctx.skiaLogoPng, 0, 0, accentPaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, BlendMode } + ); + checkResult(base64, "lighting-image-filters/point-lit-diffuse.png"); + }); + + itRunsE2eOnly("SpotLitDiffuse - Theatrical Spotlight", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Fill background with deep black for theatrical effect + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(0, 0, 0)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Add a very subtle gradient stage background + const stagePaint = Skia.Paint(); + const stageShader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 600 }, + { x: 0, y: 768 }, + [ + Skia.Color("rgba(20, 10, 30, 0)"), + Skia.Color("rgba(40, 20, 60, 0.6)"), + ], + null, + ctx.TileMode.Clamp + ); + stagePaint.setShader(stageShader); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), stagePaint); + + // Create multiple spotlight effects for dramatic theatre lighting + const createSpotlight = ( + location: { x: number; y: number; z: number }, + target: { x: number; y: number; z: number }, + color: SkColor, + intensity: number, + angle: number + ) => { + const paint = Skia.Paint(); + + // Spotlight parameters + const falloffExponent = 2.5; // Sharp falloff + const cutoffAngle = angle; // Narrow spotlight cone + + // Light color + const lightColor = color; + + // Surface parameters + const surfaceScale = 2.0; + const kd = intensity; // Strong diffuse coefficient for dramatic effect + + // Create the spotlight diffuse filter + const spotLitFilter = Skia.ImageFilter.MakeSpotLitDiffuse( + location, + target, + falloffExponent, + cutoffAngle, + lightColor, + surfaceScale, + kd, + null, // Use source bitmap as input + null // No crop rect + ); + + // Set the filter and blend mode + paint.setImageFilter(spotLitFilter); + paint.setBlendMode(ctx.BlendMode.Plus); // Additive lighting + + // Draw the image with spotlight effect + canvas.drawImage(ctx.skiaLogoPng, 0, 0, paint); + }; + + // Main spotlight from top-right + createSpotlight( + { x: 600, y: 100, z: 400 }, + { x: 384, y: 384, z: 0 }, + Skia.Color("rgb(255, 220, 180)"), // Warm white + 2.5, + 30.0 + ); + + // Accent light from left + createSpotlight( + { x: 100, y: 300, z: 300 }, + { x: 300, y: 384, z: 0 }, + Skia.Color("rgb(90, 160, 255)"), // Cool blue + 1.2, + 40.0 + ); + + // Subtle rim light from below + createSpotlight( + { x: 384, y: 650, z: 150 }, + { x: 384, y: 500, z: 0 }, + Skia.Color("rgb(255, 100, 50)"), // Warm orange/red + 0.8, + 60.0 + ); + + // Add volumetric light rays + const raysPaint = Skia.Paint(); + raysPaint.setColor(Skia.Color("rgba(255, 230, 180, 0.2)")); + + // Primary light source position + const lightX = 600; + const lightY = 100; + + // Draw 12 light rays from the main spotlight + for (let i = 0; i < 12; i++) { + const angle = (i / 12) * Math.PI * 0.5 + Math.PI * 0.75; // Angles for right-top quadrant + const length = 300 + 0.5 * 200; + const endX = lightX + Math.cos(angle) * length; + const endY = lightY + Math.sin(angle) * length; + + const rayPaint = Skia.Paint(); + rayPaint.setColor(Skia.Color("rgba(255, 230, 180, 0.1)")); + rayPaint.setStrokeWidth(2 + 0.5 * 4); + rayPaint.setStyle(ctx.PaintStyle.Stroke); + rayPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(3, 3, ctx.TileMode.Decal) + ); + + canvas.drawLine(lightX, lightY, endX, endY, rayPaint); + } + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, TileMode, BlendMode, PaintStyle } + ); + checkResult(base64, "lighting-image-filters/spot-lit-diffuse.png"); + }); + + itRunsE2eOnly("DistantLitSpecular - Metallic Gold", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Draw a rich dark background with subtle texture + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(25, 15, 10)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Add subtle noise texture to background + const noisePaint = Skia.Paint(); + + // Use a fine turbulence noise as a background texture + for (let y = 0; y < 768; y += 4) { + for (let x = 0; x < 768; x += 4) { + // Simple noise function + const noise = Math.sin(x * 0.1) * Math.cos(y * 0.1) * 0.5 + 0.5; + const alpha = noise * 0.15; // Very subtle + + noisePaint.setColor(Skia.Color(`rgba(50, 30, 10, ${alpha})`)); + canvas.drawRect(Skia.XYWHRect(x, y, 4, 4), noisePaint); + } + } + + // Create our metallic gold effect + const paint = Skia.Paint(); + + // Preprocess the image with a blur and contrast enhancement + const preprocessFilter = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeMatrix([ + 2.0, + 0, + 0, + 0, + -50, // High red contrast + 0, + 2.0, + 0, + 0, + -50, // High green contrast + 0, + 0, + 2.0, + 0, + -50, // High blue contrast + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]), + Skia.ImageFilter.MakeBlur(1, 1, ctx.TileMode.Decal) + ); + + // Direction vectors for multiple lights to create more complex highlights + const direction = { x: 0.5, y: -1, z: 0.3 }; + + // Gold light color with intense highlights + const lightColor = Skia.Color("rgb(255, 240, 180)"); + + // Parameters for the filter - high shininess for gold + const surfaceScale = 0.6; // Not too high for gold + const ks = 0.9; // Strong specular coefficient + const shininess = 50.0; // Very high shininess for metallic look + + // Create the distant specular filter + const specularFilter = Skia.ImageFilter.MakeDistantLitSpecular( + direction, + lightColor, + surfaceScale, + ks, + shininess, + preprocessFilter, // Use enhanced image as input + null // No crop rect + ); + + // Set the filter to our paint + paint.setImageFilter(specularFilter); + + // Add a gold color tint + const goldMatrix = [ + 1.2, + 0.3, + 0.0, + 0, + 30, // Boost red with some green mixed in + 0.2, + 1.0, + 0.0, + 0, + 20, // Moderate green + 0.0, + 0.1, + 0.4, + 0, + 0, // Minimal blue for gold + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]; + + const colorFilter = Skia.ColorFilter.MakeMatrix(goldMatrix); + paint.setColorFilter(colorFilter); + + // Add a second light from another angle for more complex highlights + const secondaryPaint = Skia.Paint(); + const secondDirection = { x: -0.5, y: -0.8, z: 0.2 }; + + const secondaryFilter = Skia.ImageFilter.MakeDistantLitSpecular( + secondDirection, + Skia.Color("rgb(255, 255, 255)"), // White highlights + surfaceScale * 0.5, + ks * 0.3, + shininess * 1.5, // Even sharper highlights + preprocessFilter, + null + ); + + secondaryPaint.setImageFilter(secondaryFilter); + secondaryPaint.setBlendMode(ctx.BlendMode.Plus); // Additive blending + + // Draw with padding for a framed effect + const padding = 40; + const imageRect = Skia.XYWHRect( + padding, + padding, + 768 - padding * 2, + 768 - padding * 2 + ); + + // Create an ornate frame border + const framePaint = Skia.Paint(); + framePaint.setStyle(ctx.PaintStyle.Stroke); + framePaint.setStrokeWidth(10); + framePaint.setColor(Skia.Color("rgb(80, 50, 10)")); + canvas.drawRect( + Skia.XYWHRect( + padding - 15, + padding - 15, + 768 - padding * 2 + 30, + 768 - padding * 2 + 30 + ), + framePaint + ); + + // Add gold leaf effect to frame + const frameGoldPaint = Skia.Paint(); + frameGoldPaint.setStyle(ctx.PaintStyle.Stroke); + frameGoldPaint.setStrokeWidth(6); + frameGoldPaint.setColor(Skia.Color("rgb(200, 170, 40)")); + canvas.drawRect( + Skia.XYWHRect( + padding - 15, + padding - 15, + 768 - padding * 2 + 30, + 768 - padding * 2 + 30 + ), + frameGoldPaint + ); + + // Draw the main image + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + imageRect, + paint + ); + + // Apply secondary highlights + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + imageRect, + secondaryPaint + ); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, TileMode, BlendMode, PaintStyle } + ); + checkResult(base64, "lighting-image-filters/distant-lit-specular.png"); + }); + + itRunsE2eOnly("PointLitSpecular - Wet Surface", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Create a dark wet-looking surface + const bgPaint = Skia.Paint(); + const bgShader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 768, y: 768 }, + [Skia.Color("rgb(10, 20, 30)"), Skia.Color("rgb(20, 40, 60)")], + null, + ctx.TileMode.Clamp + ); + bgPaint.setShader(bgShader); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Add water droplet texture in background + const dropletPaint = Skia.Paint(); + dropletPaint.setColor(Skia.Color("rgba(255, 255, 255, 0.1)")); + + // Random water droplets + const numDroplets = 200; + for (let i = 0; i < numDroplets; i++) { + const x = Math.random() * 768; + const y = Math.random() * 768; + const size = 1 + Math.random() * 5; + + // Make some droplets blurry for depth + if (Math.random() > 0.7) { + dropletPaint.setImageFilter( + Skia.ImageFilter.MakeBlur( + 1 + Math.random() * 2, + 1 + Math.random() * 2, + ctx.TileMode.Decal + ) + ); + } else { + dropletPaint.setImageFilter(null); + } + + canvas.drawCircle(x, y, size, dropletPaint); + } + + // Create wet surface effect with high specularity + const mainPaint = Skia.Paint(); + + // Create the point specular light source + const mainLight = { x: 200, y: 200, z: 300 }; + const specularFilter = Skia.ImageFilter.MakePointLitSpecular( + mainLight, + Skia.Color("rgb(240, 250, 255)"), // Slightly blue-white for water highlights + 1.0, // Moderate surface scale + 1.0, // Full specular strength + 80.0, // Very high shininess for wet look + null, + null + ); + + mainPaint.setImageFilter(specularFilter); + + // Create second light for additional highlights + const secondaryPaint = Skia.Paint(); + const secondLight = { x: 600, y: 400, z: 400 }; + const secondFilter = Skia.ImageFilter.MakePointLitSpecular( + secondLight, + Skia.Color("rgb(200, 220, 255)"), // Cooler light + 0.8, + 0.7, + 100.0, // Even higher shininess + null, + null + ); + + secondaryPaint.setImageFilter(secondFilter); + secondaryPaint.setBlendMode(ctx.BlendMode.Plus); + + // Create diffuse lighting for the base image + const diffusePaint = Skia.Paint(); + const diffuseFilter = Skia.ImageFilter.MakePointLitDiffuse( + { x: 384, y: 384, z: 300 }, + Skia.Color("rgb(150, 160, 170)"), // Soft gray-blue + 1.5, + 1.0, + null, + null + ); + + diffusePaint.setImageFilter(diffuseFilter); + + // Add a blue-tinted color adjustment for underwater effect + diffusePaint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 0.8, 0.0, 0.0, 0, 0, 0.0, 0.9, 0.1, 0, 0, 0.1, 0.1, 1.0, 0, 20, 0, + 0, 0, 1, 0, + ]) + ); + + // Draw with the base diffuse lighting first + canvas.drawImage(ctx.skiaLogoPng, 0, 0, diffusePaint); + + // Draw with specular highlights + canvas.drawImage(ctx.skiaLogoPng, 0, 0, mainPaint); + canvas.drawImage(ctx.skiaLogoPng, 0, 0, secondaryPaint); + + // Add water flowing/dripping effect + const waterStreakPaint = Skia.Paint(); + waterStreakPaint.setColor(Skia.Color("rgba(220, 230, 255, 0.15)")); + + // Create several water streaks + for (let i = 0; i < 8; i++) { + const startX = 50 + Math.random() * 700; + const startY = 20 + Math.random() * 100; + let currentX = startX; + let currentY = startY; + + const flowPath = Skia.Path.Make(); + flowPath.moveTo(currentX, currentY); + + // Create a wavy downward path + const length = 100 + Math.random() * 600; + const segments = 10 + Math.floor(length / 30); + + for (let j = 0; j < segments; j++) { + // Gravity pulls downward + currentY += length / segments; + // Random side-to-side waviness + currentX += (Math.random() - 0.5) * 30; + + flowPath.lineTo(currentX, currentY); + } + + // Make the water streak taper and blur + const streakPaint = Skia.Paint(); + streakPaint.setStyle(ctx.PaintStyle.Stroke); + streakPaint.setColor(Skia.Color("rgba(200, 240, 255, 0.2)")); + streakPaint.setStrokeWidth(1 + Math.random() * 3); + streakPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(1, 1, ctx.TileMode.Decal) + ); + + canvas.drawPath(flowPath, streakPaint); + } + + // Add water puddle at bottom + const puddlePaint = Skia.Paint(); + const puddleShader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 600 }, + { x: 0, y: 768 }, + [ + Skia.Color("rgba(40, 80, 120, 0)"), + Skia.Color("rgba(40, 80, 120, 0.4)"), + ], + null, + ctx.TileMode.Clamp + ); + puddlePaint.setShader(puddleShader); + canvas.drawRect(Skia.XYWHRect(0, 600, 768, 168), puddlePaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, BlendMode, TileMode, PaintStyle } + ); + checkResult(base64, "lighting-image-filters/point-lit-specular.png"); + }); + itRunsE2eOnly("SpotLitSpecular - Crystal Prism", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Create a black background for maximum contrast with crystal effect + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(0, 0, 0)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create a "crystal prism" effect with rainbow colors + + // Draw colored ambient light beams in background + const beamColors = [ + "rgba(255, 50, 50, 0.2)", // Red + "rgba(255, 150, 50, 0.2)", // Orange + "rgba(255, 255, 50, 0.2)", // Yellow + "rgba(50, 255, 50, 0.2)", // Green + "rgba(50, 150, 255, 0.2)", // Blue + "rgba(150, 50, 255, 0.2)", // Purple + ]; + + // Create rainbow beams + for (let i = 0; i < beamColors.length; i++) { + const beamPaint = Skia.Paint(); + beamPaint.setColor(Skia.Color(beamColors[i])); + + // Calculate angle for each beam + const angle = (i / beamColors.length) * Math.PI + Math.PI / 2; + + // Create beam path + const beamPath = Skia.Path.Make(); + beamPath.moveTo(384, 384); + + // End coordinates based on angle + const endX = 384 + Math.cos(angle) * 900; + const endY = 384 + Math.sin(angle) * 900; + + beamPath.lineTo(endX, endY); + + // Draw wide blurred beam + beamPaint.setStyle(ctx.PaintStyle.Stroke); + beamPaint.setStrokeWidth(60 + i * 10); + beamPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(20, 20, ctx.TileMode.Decal) + ); + + canvas.drawPath(beamPath, beamPaint); + } + + // Apply crystalline effect to the main image + // First, prepare a base with enhanced contrast + const basePaint = Skia.Paint(); + basePaint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.5, + 0, + 0, + 0, + -30, // Boosted red + 0, + 1.5, + 0, + 0, + -30, // Boosted green + 0, + 0, + 1.5, + 0, + -30, // Boosted blue + 0, + 0, + 0, + 1.2, + 0, // Slightly boosted alpha + ]) + ); + + // Draw base layer at reduced size to create crystal border effect + const padding = 100; + const imageRect = Skia.XYWHRect( + padding, + padding, + 768 - padding * 2, + 768 - padding * 2 + ); + + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + imageRect, + basePaint + ); + + // Now create multiple specular highlights from different angles + // These simulate light refracting through crystal facets + + // Function to create a colored specular highlight + const createCrystalFacet = ( + location: { x: number; y: number; z: number }, + target: { x: number; y: number; z: number }, + color: SkColor, + cutoffAngle: number, + shininess: number + ) => { + const paint = Skia.Paint(); + + // Create the spotlight specular filter + const spotLitFilter = Skia.ImageFilter.MakeSpotLitSpecular( + location, + target, + 3.0, // Sharp falloff + cutoffAngle, // Narrow spotlight cone + color, // Colored light + 1.0, // Surface scale + 0.9, // Strong specular coefficient + shininess, // Very high shininess + null, // Use source bitmap as input + null // No crop rect + ); + + // Set the filter and blend mode + paint.setImageFilter(spotLitFilter); + paint.setBlendMode(ctx.BlendMode.Plus); // Additive lighting + + // Draw the specular highlight + canvas.drawImageRect( + ctx.skiaLogoPng, + Skia.XYWHRect( + 0, + 0, + ctx.skiaLogoPng.width(), + ctx.skiaLogoPng.height() + ), + imageRect, + paint + ); + }; + + // Create crystal facets with rainbow colors + createCrystalFacet( + { x: 500, y: 200, z: 300 }, + { x: 384, y: 384, z: 0 }, + Skia.Color("rgb(255, 50, 50)"), // Red + 25.0, + 100.0 + ); + + createCrystalFacet( + { x: 200, y: 600, z: 300 }, + { x: 300, y: 384, z: 0 }, + Skia.Color("rgb(255, 150, 50)"), // Orange + 30.0, + 120.0 + ); + + createCrystalFacet( + { x: 600, y: 600, z: 300 }, + { x: 500, y: 400, z: 0 }, + Skia.Color("rgb(255, 255, 50)"), // Yellow + 20.0, + 150.0 + ); + + createCrystalFacet( + { x: 200, y: 200, z: 300 }, + { x: 300, y: 300, z: 0 }, + Skia.Color("rgb(50, 255, 50)"), // Green + 15.0, + 200.0 + ); + + createCrystalFacet( + { x: 400, y: 100, z: 300 }, + { x: 400, y: 300, z: 0 }, + Skia.Color("rgb(50, 150, 255)"), // Blue + 25.0, + 180.0 + ); + + createCrystalFacet( + { x: 700, y: 384, z: 300 }, + { x: 500, y: 384, z: 0 }, + Skia.Color("rgb(150, 50, 255)"), // Purple + 20.0, + 160.0 + ); + + // Add intense white highlights for sharp crystal edges + createCrystalFacet( + { x: 384, y: 100, z: 400 }, + { x: 384, y: 384, z: 0 }, + Skia.Color("rgb(255, 255, 255)"), // Pure white + 10.0, + 250.0 + ); + + // Add crystal border effect + const borderPaint = Skia.Paint(); + + // Create a crystal-like faceted border + const borderPath = Skia.Path.Make(); + + // Create an irregular crystal shape around the image + const centerX = 384; + const centerY = 384; + const facets = 12; + + borderPath.moveTo( + centerX + (padding - 20) * Math.cos(0), + centerY + (padding - 20) * Math.sin(0) + ); + + for (let i = 1; i <= facets; i++) { + const angle = (i / facets) * Math.PI * 2; + + // Vary the radius for each facet + const radiusVariation = 30 + Math.random() * 40; + const radius = padding - 20 + radiusVariation; + + borderPath.lineTo( + centerX + radius * Math.cos(angle), + centerY + radius * Math.sin(angle) + ); + } + + borderPath.close(); + + // Draw crystal border with gradient effect + const borderShader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 768, y: 768 }, + [ + Skia.Color("rgba(220, 240, 255, 0.6)"), + Skia.Color("rgba(180, 220, 255, 0.3)"), + Skia.Color("rgba(150, 200, 255, 0.6)"), + ], + null, + ctx.TileMode.Clamp + ); + + borderPaint.setShader(borderShader); + borderPaint.setStyle(ctx.PaintStyle.Stroke); + borderPaint.setStrokeWidth(30); + borderPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(8, 8, ctx.TileMode.Decal) + ); + + canvas.drawPath(borderPath, borderPaint); + + // Add some star-shaped highlights + const starPaint = Skia.Paint(); + starPaint.setColor(Skia.Color("rgb(255, 255, 255)")); + + const createStar = (x: number, y: number, size: number) => { + const starPath = Skia.Path.Make(); + + // Draw 4-point star + starPath.moveTo(x, y - size); + starPath.lineTo(x + size / 4, y - size / 4); + starPath.lineTo(x + size, y); + starPath.lineTo(x + size / 4, y + size / 4); + starPath.lineTo(x, y + size); + starPath.lineTo(x - size / 4, y + size / 4); + starPath.lineTo(x - size, y); + starPath.lineTo(x - size / 4, y - size / 4); + starPath.close(); + + // Add blur for glow + const glowPaint = Skia.Paint(); + glowPaint.setColor(Skia.Color("rgba(255, 255, 255, 0.8)")); + glowPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(size / 4, size / 4, ctx.TileMode.Decal) + ); + + canvas.drawPath(starPath, glowPaint); + + // Draw sharp center + const centerPaint = Skia.Paint(); + centerPaint.setColor(Skia.Color("rgb(255, 255, 255)")); + canvas.drawCircle(x, y, size / 10, centerPaint); + }; + + // Add several highlight stars + createStar(150, 200, 20); + createStar(600, 150, 15); + createStar(500, 600, 25); + createStar(200, 500, 18); + createStar(350, 250, 12); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, BlendMode, TileMode, PaintStyle } + ); + checkResult(base64, "lighting-image-filters/spot-lit-specular.png"); + }); + + itRunsE2eOnly("Combined Lighting - Elemental Fire & Ice", async () => { + const { skiaLogoPng } = images; + const base64 = await surface.eval( + (Skia, ctx) => { + const sur = Skia.Surface.MakeOffscreen(768, 768)!; + const canvas = sur.getCanvas(); + + // Fill with dark background + const bgPaint = Skia.Paint(); + bgPaint.setColor(Skia.Color("rgb(5, 10, 20)")); + canvas.drawRect(Skia.XYWHRect(0, 0, 768, 768), bgPaint); + + // Create a split-screen effect with fire and ice + + // First, create a mask for each half + const leftMask = Skia.Path.Make(); + leftMask.addRect(Skia.XYWHRect(0, 0, 384, 768)); + + const rightMask = Skia.Path.Make(); + rightMask.addRect(Skia.XYWHRect(384, 0, 384, 768)); + + // Prepare the image with enhanced contrast + const preprocessFilter = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.8, + 0, + 0, + 0, + -40, // High contrast red + 0, + 1.8, + 0, + 0, + -40, // High contrast green + 0, + 0, + 1.8, + 0, + -40, // High contrast blue + 0, + 0, + 0, + 1.2, + 0, // Slightly boosted alpha + ]), + Skia.ImageFilter.MakeBlur(1, 1, ctx.TileMode.Decal) + ); + + // ******** FIRE SIDE (LEFT) ******** + canvas.save(); + canvas.clipPath(leftMask, ctx.ClipOp.Intersect, true); + + // Create fiery background + const fireBgPaint = Skia.Paint(); + const fireShader = Skia.Shader.MakeLinearGradient( + { x: 0, y: 768 }, + { x: 384, y: 0 }, + [ + Skia.Color("rgb(80, 0, 0)"), + Skia.Color("rgb(180, 30, 0)"), + Skia.Color("rgb(255, 150, 0)"), + ], + [0.2, 0.5, 0.9], + ctx.TileMode.Clamp + ); + fireBgPaint.setShader(fireShader); + canvas.drawRect(Skia.XYWHRect(0, 0, 384, 768), fireBgPaint); + + // Draw stylized flames in background + const drawFlame = ( + x: number, + y: number, + width: number, + height: number + ) => { + const flamePath = Skia.Path.Make(); + + // Start at bottom center + flamePath.moveTo(x, y + height); + + // Define control points for bezier curve + // Left side of flame + flamePath.cubicTo( + x - width * 0.5, + y + height * 0.7, // Control point 1 + x - width * 0.2, + y + height * 0.3, // Control point 2 + x, + y // End point (top of flame) + ); + + // Right side of flame + flamePath.cubicTo( + x + width * 0.2, + y + height * 0.3, // Control point 1 + x + width * 0.5, + y + height * 0.7, // Control point 2 + x, + y + height // End point (back to start) + ); + + // Fill with gradient + const flamePaint = Skia.Paint(); + const flameShader = Skia.Shader.MakeLinearGradient( + { x: x, y: y + height }, + { x: x, y: y }, + [ + Skia.Color("rgb(255, 60, 0)"), + Skia.Color("rgb(255, 150, 0)"), + Skia.Color("rgb(255, 220, 120)"), + ], + [0.2, 0.6, 0.9], + ctx.TileMode.Clamp + ); + flamePaint.setShader(flameShader); + + // Add glow with blur + flamePaint.setImageFilter( + Skia.ImageFilter.MakeBlur( + width * 0.1, + width * 0.1, + ctx.TileMode.Decal + ) + ); + + canvas.drawPath(flamePath, flamePaint); + }; + + // Draw multiple flames + for (let i = 0; i < 8; i++) { + const x = 50 + Math.random() * 300; + const baseHeight = 200 + Math.random() * 300; + drawFlame(x, 768 - baseHeight, 20 + Math.random() * 50, baseHeight); + } + + // Create fire lighting with composite of diffuse and specular + + // Create fiery diffuse lighting from below + const fireLight = { x: 200, y: 700, z: 100 }; + const fireColor = Skia.Color("rgb(255, 180, 50)"); + + const fireDiffuseFilter = Skia.ImageFilter.MakePointLitDiffuse( + fireLight, + fireColor, + 2.0, // Exaggerated surface scale + 2.0, // Strong diffuse + preprocessFilter, // Use preprocessed image + null + ); + + // Create specular highlights for embers and sparks + const fireSpecularFilter = Skia.ImageFilter.MakePointLitSpecular( + { x: 150, y: 550, z: 200 }, + Skia.Color("rgb(255, 230, 150)"), + 1.0, + 0.7, + 50.0, // High shininess for sparks + preprocessFilter, + null + ); + + // Combine diffuse and specular with Plus blending + const fireFilter = Skia.ImageFilter.MakeBlend( + ctx.BlendMode.Plus, + fireDiffuseFilter, + fireSpecularFilter + ); + + // Create the fire paint + const firePaint = Skia.Paint(); + firePaint.setImageFilter(fireFilter); + + // Add fiery color tint + firePaint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 1.5, + 0.3, + 0.1, + 0, + 20, // Strong red + 0.2, + 1.0, + 0.1, + 0, + 0, // Moderate green + 0.0, + 0.1, + 0.5, + 0, + -20, // Reduced blue + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]) + ); + + // Draw the image with fire effect + canvas.drawImage(ctx.skiaLogoPng, 0, 0, firePaint); + + // Add ember particles + const emberPaint = Skia.Paint(); + + for (let i = 0; i < 60; i++) { + const x = Math.random() * 384; + const y = 300 + Math.random() * 468; + const size = 1 + Math.random() * 3; + + const brightness = 150 + Math.random() * 105; + emberPaint.setColor( + Skia.Color(`rgba(${brightness}, ${brightness * 0.6}, 0, 0.8)`) + ); + + // Add glow to some embers + if (Math.random() > 0.6) { + emberPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(size * 2, size * 2, ctx.TileMode.Decal) + ); + } else { + emberPaint.setImageFilter(null); + } + + canvas.drawCircle(x, y, size, emberPaint); + } + + canvas.restore(); + + // ******** ICE SIDE (RIGHT) ******** + canvas.save(); + canvas.clipPath(rightMask, ctx.ClipOp.Intersect, true); + + // Create icy background + const iceBgPaint = Skia.Paint(); + const iceShader = Skia.Shader.MakeLinearGradient( + { x: 768, y: 768 }, + { x: 384, y: 0 }, + [ + Skia.Color("rgb(0, 20, 50)"), + Skia.Color("rgb(30, 70, 120)"), + Skia.Color("rgb(180, 220, 255)"), + ], + [0.2, 0.5, 0.9], + ctx.TileMode.Clamp + ); + iceBgPaint.setShader(iceShader); + canvas.drawRect(Skia.XYWHRect(384, 0, 384, 768), iceBgPaint); + + // Draw ice crystals in background + const drawCrystal = ( + x: number, + y: number, + size: number, + rotation: number + ) => { + const crystalPaint = Skia.Paint(); + crystalPaint.setColor(Skia.Color("rgba(200, 230, 255, 0.4)")); + + // Save canvas state for rotation + canvas.save(); + canvas.translate(x, y); + canvas.rotate(rotation, 0, 0); + + // Draw crystal shape + const crystalPath = Skia.Path.Make(); + crystalPath.moveTo(0, -size); // Top + crystalPath.lineTo(size / 3, -size / 2); // Upper right + crystalPath.lineTo(size / 2, size / 2); // Lower right + crystalPath.lineTo(0, size); // Bottom + crystalPath.lineTo(-size / 2, size / 2); // Lower left + crystalPath.lineTo(-size / 3, -size / 2); // Upper left + crystalPath.close(); + + // Add light refraction effect + crystalPaint.setImageFilter( + Skia.ImageFilter.MakeBlur( + size * 0.1, + size * 0.1, + ctx.TileMode.Decal + ) + ); + + canvas.drawPath(crystalPath, crystalPaint); + + // Add highlight + const highlightPaint = Skia.Paint(); + highlightPaint.setColor(Skia.Color("rgba(255, 255, 255, 0.7)")); + + const highlightPath = Skia.Path.Make(); + highlightPath.moveTo(0, -size * 0.8); + highlightPath.lineTo(size / 5, -size / 3); + highlightPath.lineTo(-size / 5, -size / 3); + highlightPath.close(); + + canvas.drawPath(highlightPath, highlightPaint); + + // Restore canvas + canvas.restore(); + }; + + // Draw multiple crystals + for (let i = 0; i < 15; i++) { + const x = 384 + Math.random() * 384; + const y = Math.random() * 768; + const size = 20 + Math.random() * 60; + const rotation = Math.random() * 360; + + drawCrystal(x, y, size, rotation); + } + + // Create ice lighting with diffuse and specular + + // Create ice diffuse lighting from top + const iceLight = { x: 600, y: 100, z: 200 }; + const iceColor = Skia.Color("rgb(200, 230, 255)"); + + const iceDiffuseFilter = Skia.ImageFilter.MakePointLitDiffuse( + iceLight, + iceColor, + 1.5, // Moderate surface scale + 1.2, // Strong diffuse + preprocessFilter, // Use preprocessed image + null + ); + + // Create specular highlights for ice crystals + const iceSpecularFilter = Skia.ImageFilter.MakePointLitSpecular( + { x: 620, y: 200, z: 300 }, + Skia.Color("rgb(240, 250, 255)"), + 1.0, + 0.8, + 80.0, // Very high shininess for ice + preprocessFilter, + null + ); + + // Combine diffuse and specular with Screen blending + const iceFilter = Skia.ImageFilter.MakeBlend( + ctx.BlendMode.Screen, + iceDiffuseFilter, + iceSpecularFilter + ); + + // Create the ice paint + const icePaint = Skia.Paint(); + icePaint.setImageFilter(iceFilter); + + // Add icy color tint + icePaint.setColorFilter( + Skia.ColorFilter.MakeMatrix([ + 0.7, + 0.0, + 0.0, + 0, + -20, // Reduced red + 0.0, + 0.9, + 0.2, + 0, + 0, // Boosted green slightly + 0.2, + 0.3, + 1.2, + 0, + 30, // Strong blue + 0, + 0, + 0, + 1, + 0, // Alpha unchanged + ]) + ); + + // Draw the image with ice effect + canvas.drawImage(ctx.skiaLogoPng, 0, 0, icePaint); + + // Add snowflake particles + const snowPaint = Skia.Paint(); + snowPaint.setColor(Skia.Color("rgba(255, 255, 255, 0.8)")); + + for (let i = 0; i < 60; i++) { + const x = 384 + Math.random() * 384; + const y = Math.random() * 768; + const size = 1 + Math.random() * 3; + + // Add glow to some snowflakes + if (Math.random() > 0.7) { + snowPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(size * 2, size * 2, ctx.TileMode.Decal) + ); + canvas.drawCircle(x, y, size * 1.5, snowPaint); + } else { + snowPaint.setImageFilter(null); + canvas.drawCircle(x, y, size, snowPaint); + } + } + + canvas.restore(); + + // Draw dividing line with glowing effect + const dividerPaint = Skia.Paint(); + const dividerShader = Skia.Shader.MakeLinearGradient( + { x: 384 - 20, y: 0 }, + { x: 384 + 20, y: 0 }, + [ + Skia.Color("rgba(255, 120, 0, 0.7)"), + Skia.Color("rgba(255, 255, 255, 1.0)"), + Skia.Color("rgba(100, 180, 255, 0.7)"), + ], + [0.0, 0.5, 1.0], + ctx.TileMode.Clamp + ); + dividerPaint.setShader(dividerShader); + dividerPaint.setImageFilter( + Skia.ImageFilter.MakeBlur(10, 10, ctx.TileMode.Decal) + ); + + canvas.drawRect(Skia.XYWHRect(384 - 5, 0, 10, 768), dividerPaint); + + sur.flush(); + return sur.makeImageSnapshot().encodeToBase64(); + }, + { skiaLogoPng, BlendMode, TileMode, PaintStyle, ClipOp } + ); + checkResult( + base64, + "lighting-image-filters/combined-lighting-fire-ice.png" + ); + }); +}); diff --git a/packages/skia/src/skia/types/ImageFilter/ImageFilterFactory.ts b/packages/skia/src/skia/types/ImageFilter/ImageFilterFactory.ts index ee691877c5..8cee3b0752 100644 --- a/packages/skia/src/skia/types/ImageFilter/ImageFilterFactory.ts +++ b/packages/skia/src/skia/types/ImageFilter/ImageFilterFactory.ts @@ -1,6 +1,9 @@ import type { SkColor } from "../Color"; import type { SkColorFilter } from "../ColorFilter/ColorFilter"; +import type { FilterMode, MipmapMode, SkImage } from "../Image/Image"; +import type { SkMatrix } from "../Matrix"; import type { BlendMode } from "../Paint"; +import type { SkPicture } from "../Picture"; import type { SkRect } from "../Rect"; import type { SkRuntimeShaderBuilder } from "../RuntimeEffect"; import type { SkShader } from "../Shader"; @@ -21,11 +24,13 @@ export interface ImageFilterFactory { * @param dx - Offset along the X axis * @param dy - Offset along the X axis * @param input - if null, it will use the dynamic source image + * @param cropRect - Optional rectangle that crops the input and output */ MakeOffset( dx: number, dy: number, - input: SkImageFilter | null + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter; /** * Spatially displace pixel values of the filtered image @@ -35,45 +40,57 @@ export interface ImageFilterFactory { * @param scale - Scale factor to be used in the displacement * @param in1 - Source image filter to use for the displacement * @param input - if null, it will use the dynamic source image + * @param cropRect - Optional rectangle that crops the input and output */ MakeDisplacementMap( channelX: ColorChannel, channelY: ColorChannel, scale: number, in1: SkImageFilter, - input: SkImageFilter | null + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter; /** * Transforms a shader into an impage filter * * @param shader - The Shader to be transformed - * @param input - if null, it will use the dynamic source image + * @param dither - Whether to apply dithering to the shader + * @param cropRect - Optional rectangle that crops the input and output */ - MakeShader(shader: SkShader, input: SkImageFilter | null): SkImageFilter; + MakeShader( + shader: SkShader, + dither?: boolean, + cropRect?: SkRect | null + ): SkImageFilter; /** * Create a filter that blurs its input by the separate X and Y sigmas. The provided tile mode * is used when the blur kernel goes outside the input image. * * @param sigmaX - The Gaussian sigma value for blurring along the X axis. * @param sigmaY - The Gaussian sigma value for blurring along the Y axis. - * @param mode + * @param mode - The tile mode to use when blur kernel goes outside the image * @param input - if null, it will use the dynamic source image (e.g. a saved layer) + * @param cropRect - Optional rectangle that crops the input and output */ MakeBlur( sigmaX: number, sigmaY: number, mode: TileMode, - input: SkImageFilter | null + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter; /** * Create a filter that applies the color filter to the input filter results. - * @param cf + * + * @param colorFilter - The color filter to apply * @param input - if null, it will use the dynamic source image (e.g. a saved layer) + * @param cropRect - Optional rectangle that crops the input and output */ MakeColorFilter( - cf: SkColorFilter, - input: SkImageFilter | null + colorFilter: SkColorFilter, + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter; /** @@ -105,8 +122,8 @@ export interface ImageFilterFactory { sigmaX: number, sigmaY: number, color: SkColor, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ) => SkImageFilter; /** * Create a filter that renders a drop shadow, in exactly the same manner as ::DropShadow, except @@ -126,8 +143,8 @@ export interface ImageFilterFactory { sigmaX: number, sigmaY: number, color: SkColor, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ) => SkImageFilter; /** * Create a filter that erodes each input pixel's channel values to the minimum channel value @@ -140,8 +157,8 @@ export interface ImageFilterFactory { MakeErode: ( rx: number, ry: number, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ) => SkImageFilter; /** * Create a filter that dilates each input pixel's channel values to the max value within the @@ -154,21 +171,21 @@ export interface ImageFilterFactory { MakeDilate: ( rx: number, ry: number, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ) => SkImageFilter; /** * This filter takes an SkBlendMode and uses it to composite the two filters together. * @param mode The blend mode that defines the compositing operation * @param background The Dst pixels used in blending, if null the source bitmap is used. * @param foreground The Src pixels used in blending, if null the source bitmap is used. - * @cropRect Optional rectangle to crop input and output. + * @param cropRect Optional rectangle to crop input and output. */ MakeBlend: ( mode: BlendMode, background: SkImageFilter, - foreground: SkImageFilter | null, - cropRect?: SkRect + foreground?: SkImageFilter | null, + cropRect?: SkRect | null ) => SkImageFilter; /** * Create a filter that fills the output with the per-pixel evaluation of the SkShader produced @@ -187,6 +204,359 @@ export interface ImageFilterFactory { MakeRuntimeShader: ( builder: SkRuntimeShaderBuilder, childShaderName: string | null, - input: SkImageFilter | null + input?: SkImageFilter | null + ) => SkImageFilter; + + /** + * Create a filter that implements a custom blend mode. Each output pixel is the result of + * combining the corresponding background and foreground pixels using the 4 coefficients: + * k1 * foreground * background + k2 * foreground + k3 * background + k4 + * + * @param k1, k2, k3, k4 The four coefficients used to combine the foreground and background. + * @param enforcePMColor If true, the RGB channels will be clamped to the calculated alpha. + * @param background The background content, using the source bitmap when this is null. + * @param foreground The foreground content, using the source bitmap when this is null. + * @param cropRect Optional rectangle that crops the inputs and output. + */ + MakeArithmetic( + k1: number, + k2: number, + k3: number, + k4: number, + enforcePMColor: boolean, + background?: SkImageFilter | null, + foreground?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that applies a crop to the result of the 'input' filter. Pixels within the + * crop rectangle are unmodified from what 'input' produced. Pixels outside of crop match the + * provided SkTileMode (defaulting to kDecal). + * + * NOTE: The optional CropRect argument for many of the factories is equivalent to creating the + * filter without a CropRect and then wrapping it in ::Crop(rect, kDecal). Explicitly adding + * Crop filters lets you control their tiling and use different geometry for the input and the + * output of another filter. + * + * @param rect The cropping rect + * @param tileMode The TileMode applied to pixels *outside* of 'crop' @default TileMode.Decal + * @param input The input filter that is cropped, uses source image if this is null + */ + MakeCrop( + rect: SkRect, + tileMode?: TileMode | null, + input?: SkImageFilter | null + ): SkImageFilter; + + /** + * Create a filter that always produces transparent black. + */ + MakeEmpty(): SkImageFilter; + + /** + * Create a filter that draws the 'srcRect' portion of image into 'dstRect' using the given + * filter quality. Similar to SkCanvas::drawImageRect. The returned image filter evaluates + * to transparent black if 'image' is null. + * + * @param image The image that is output by the filter, subset by 'srcRect'. + * @param srcRect The source pixels sampled into 'dstRect', if null the image bounds are used. + * @param dstRect The local rectangle to draw the image into, if null the srcRect is used. + * @param filterMode The filter mode to use when sampling the image @default FilterMode.Nearest + * @param mipmap The mipmap mode to use when sampling the image @default MipmapMode.None + */ + MakeImage( + image: SkImage, + srcRect?: SkRect | null, + dstRect?: SkRect | null, + filterMode?: FilterMode, + mipmap?: MipmapMode + ): SkImageFilter; + + /** + * Create a filter that fills 'lensBounds' with a magnification of the input. + * + * @param lensBounds The outer bounds of the magnifier effect + * @param zoomAmount The amount of magnification applied to the input image + * @param inset The size or width of the fish-eye distortion around the magnified content + * @param filterMode The filter mode to use when sampling the image @default FilterMode.Nearest + * @param mipmap The mipmap mode to use when sampling the image @default MipmapMode.None + * @param input The input filter that is magnified; if null the source bitmap is used + * @param cropRect Optional rectangle that crops the input and output. + */ + MakeMagnifier( + lensBounds: SkRect, + zoomAmount: number, + inset: number, + filterMode?: FilterMode, + mipmap?: MipmapMode, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that applies an NxM image processing kernel to the input image. This can be + * used to produce effects such as sharpening, blurring, edge detection, etc. + * @param kernelSizeX The width of the kernel. Must be greater than zero. + * @param kernelSizeY The height of the kernel. Must be greater than zero. + * @param kernel The image processing kernel. Must contain kernelSizeX * kernelSizeY elements, in row order. + * @param gain A scale factor applied to each pixel after convolution. This can be + * used to normalize the kernel, if it does not already sum to 1. + * @param bias A bias factor added to each pixel after convolution. + * @param kernelOffsetX An offset applied to each pixel coordinate before convolution. + * This can be used to center the kernel over the image + * (e.g., a 3x3 kernel should have an offset of {1, 1}). + * @param kernelOffsetY An offset applied to each pixel coordinate before convolution. + * This can be used to center the kernel over the image + * (e.g., a 3x3 kernel should have an offset of {1, 1}). + * @param tileMode How accesses outside the image are treated. TileMode.Mirror is not supported. + * @param convolveAlpha If true, all channels are convolved. If false, only the RGB channels + * are convolved, and alpha is copied from the source image. + * @param input The input image filter, if null the source bitmap is used instead. + * @param cropRect Optional rectangle to which the output processing will be limited. + */ + MakeMatrixConvolution( + kernelSizeX: number, + kernelSizeY: number, + kernel: number[], + gain: number, + bias: number, + kernelOffsetX: number, + kernelOffsetY: number, + tileMode: TileMode, + convolveAlpha: boolean, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that transforms the input image by 'matrix'. This matrix transforms the + * local space, which means it effectively happens prior to any transformation coming from the + * SkCanvas initiating the filtering. + * @param matrix The matrix to apply to the original content. + * @param filterMode The filter mode to use when sampling the image @default FilterMode.Nearest + * @param mipmap The mipmap mode to use when sampling the image @default MipmapMode.None + * @param input The image filter to transform, or null to use the source image. + */ + MakeMatrixTransform( + matrix: SkMatrix, + filterMode?: FilterMode, + mipmap?: MipmapMode, + input?: SkImageFilter | null + ): SkImageFilter; + + /** + * Create a filter that merges filters together by drawing their results in order + * with src-over blending. + * @param filters The input filter array to merge. Any null + * filter pointers will use the source bitmap instead. + * @param cropRect Optional rectangle that crops all input filters and the output. + */ + MakeMerge( + filters: Array, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that produces the SkPicture as its output, clipped to both 'targetRect' and + * the picture's internal cull rect. + * + * If 'pic' is null, the returned image filter produces transparent black. + * + * @param picture The picture that is drawn for the filter output. + * @param targetRect The drawing region for the picture. If null, the picture's bounds are used. + */ + MakePicture(picture: SkPicture, targetRect?: SkRect | null): SkImageFilter; + + /** + * Create a filter that fills the output with the per-pixel evaluation of the SkShader produced + * by the SkRuntimeEffectBuilder. The shader is defined in the image filter's local coordinate + * system, so it will automatically be affected by SkCanvas' transform. + * + * This requires a GPU backend or SkSL to be compiled in. + * + * @param builder The builder used to produce the runtime shader, that will in turn + * fill the result image + * @param sampleRadius defines the sampling radius of 'childShaderName' relative to + * the runtime shader produced by 'builder'. + * If greater than 0, the coordinate passed to childShader.eval() will + * be up to 'sampleRadius' away (maximum absolute offset in 'x' or 'y') + * from the coordinate passed into the runtime shader. + * @param childShaderNames The names of the child shaders defined in the builder that will be + * bound to the input params (or the source image if the input param + * is null). If any name is null, or appears more than once, factory + * fails and returns nullptr. + * @param inputs The image filters that will be provided as input to the runtime + * shader. If any are null, the implicit source image is used instead. + */ + MakeRuntimeShaderWithChildren: ( + builder: SkRuntimeShaderBuilder, + sampleRadius: number, + childShaderNames: string[], + inputs: Array ) => SkImageFilter; + + /** + * Create a tile image filter. + * @param src Defines the pixels to tile + * @param dst Defines the pixel region that the tiles will be drawn to + * @param input The input that will be tiled, if null the source bitmap is used instead. + */ + MakeTile( + src: SkRect, + dst: SkRect, + input?: SkImageFilter | null + ): SkImageFilter; + + /** + * Create a filter that calculates the diffuse illumination from a distant light source, + * interpreting the alpha channel of the input as the height profile of the surface (to + * approximate normal vectors). + * @param direction The direction to the distance light. + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakeDistantLitDiffuse( + direction: SkPoint3, + lightColor: SkColor, + surfaceScale: number, + kd: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that calculates the diffuse illumination from a point light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). + * @param location The location of the point light. + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakePointLitDiffuse( + location: SkPoint3, + lightColor: SkColor, + surfaceScale: number, + kd: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that calculates the diffuse illumination from a spot light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). The spot light is restricted to be within 'cutoffAngle' of the vector between + * the location and target. + * @param location The location of the spot light. + * @param target The location that the spot light is point towards + * @param falloffExponent Exponential falloff parameter for illumination outside of cutoffAngle + * @param cutoffAngle Maximum angle from lighting direction that receives full light + * @param lightColor The color of the diffuse light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param kd Diffuse reflectance coefficient. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakeSpotLitDiffuse( + location: SkPoint3, + target: SkPoint3, + falloffExponent: number, + cutoffAngle: number, + lightColor: SkColor, + surfaceScale: number, + kd: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that calculates the specular illumination from a distant light source, + * interpreting the alpha channel of the input as the height profile of the surface (to + * approximate normal vectors). + * @param direction The direction to the distance light. + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakeDistantLitSpecular( + direction: SkPoint3, + lightColor: SkColor, + surfaceScale: number, + ks: number, + shininess: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that calculates the specular illumination from a point light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). + * @param location The location of the point light. + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakePointLitSpecular( + location: SkPoint3, + lightColor: SkColor, + surfaceScale: number, + ks: number, + shininess: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; + + /** + * Create a filter that calculates the specular illumination from a spot light source, using + * alpha channel of the input as the height profile of the surface (to approximate normal + * vectors). The spot light is restricted to be within 'cutoffAngle' of the vector between + * the location and target. + * @param location The location of the spot light. + * @param target The location that the spot light is point towards + * @param falloffExponent Exponential falloff parameter for illumination outside of cutoffAngle + * @param cutoffAngle Maximum angle from lighting direction that receives full light + * @param lightColor The color of the specular light source. + * @param surfaceScale Scale factor to transform from alpha values to physical height. + * @param ks Specular reflectance coefficient. + * @param shininess The specular exponent determining how shiny the surface is. + * @param input The input filter that defines surface normals (as alpha), or uses the + * source bitmap when null. + * @param cropRect Optional rectangle that crops the input and output. + */ + MakeSpotLitSpecular( + location: SkPoint3, + target: SkPoint3, + falloffExponent: number, + cutoffAngle: number, + lightColor: SkColor, + surfaceScale: number, + ks: number, + shininess: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ): SkImageFilter; } + +export type SkPoint3 = { + x: number; + y: number; + z: number; +}; diff --git a/packages/skia/src/skia/web/JsiSkImageFilterFactory.ts b/packages/skia/src/skia/web/JsiSkImageFilterFactory.ts index 89c58f2e9a..71ec3484a1 100644 --- a/packages/skia/src/skia/web/JsiSkImageFilterFactory.ts +++ b/packages/skia/src/skia/web/JsiSkImageFilterFactory.ts @@ -11,6 +11,12 @@ import type { SkRuntimeShaderBuilder, SkShader, TileMode, + FilterMode, + MipmapMode, + SkImage, + SkMatrix, + SkPicture, + SkPoint3, } from "../types"; import { Host, throwNotImplementedOnRNWeb, getEnum } from "./Host"; @@ -24,10 +30,180 @@ export class JsiSkImageFilterFactory constructor(CanvasKit: CanvasKit) { super(CanvasKit); } + MakeRuntimeShaderWithChildren( + _builder: SkRuntimeShaderBuilder, + _sampleRadius: number, + _childShaderNames: string[], + _inputs: Array + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeArithmetic( + _k1: number, + _k2: number, + _k3: number, + _k4: number, + _enforcePMColor: boolean, + _background?: SkImageFilter | null, + _foreground?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeCrop( + _rect: SkRect, + _tileMode?: TileMode | null, + _input?: SkImageFilter | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeEmpty(): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeImage( + _image: SkImage, + _srcRect?: SkRect | null, + _dstRect?: SkRect | null, + _filterMode?: FilterMode, + _mipmap?: MipmapMode + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeMagnifier( + _lensBounds: SkRect, + _zoomAmount: number, + _inset: number, + _filterMode?: FilterMode, + _mipmap?: MipmapMode, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeMatrixConvolution( + _kernelSizeX: number, + _kernelSizeY: number, + _kernel: number[], + _gain: number, + _bias: number, + _kernelOffsetX: number, + _kernelOffsetY: number, + _tileMode: TileMode, + _convolveAlpha: boolean, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeMatrixTransform( + _matrix: SkMatrix, + _filterMode?: FilterMode, + _mipmap?: MipmapMode, + _input?: SkImageFilter | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeMerge( + _filters: Array, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakePicture(_picture: SkPicture, _targetRect?: SkRect | null): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeTile( + _src: SkRect, + _dst: SkRect, + _input?: SkImageFilter | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeDistantLitDiffuse( + _direction: SkPoint3, + _lightColor: SkColor, + _surfaceScale: number, + _kd: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakePointLitDiffuse( + _location: SkPoint3, + _lightColor: SkColor, + _surfaceScale: number, + _kd: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeSpotLitDiffuse( + _location: SkPoint3, + _target: SkPoint3, + _falloffExponent: number, + _cutoffAngle: number, + _lightColor: SkColor, + _surfaceScale: number, + _kd: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeDistantLitSpecular( + _direction: SkPoint3, + _lightColor: SkColor, + _surfaceScale: number, + _ks: number, + _shininess: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakePointLitSpecular( + _location: SkPoint3, + _lightColor: SkColor, + _surfaceScale: number, + _ks: number, + _shininess: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } + MakeSpotLitSpecular( + _location: SkPoint3, + _target: SkPoint3, + _falloffExponent: number, + _cutoffAngle: number, + _lightColor: SkColor, + _surfaceScale: number, + _ks: number, + _shininess: number, + _input?: SkImageFilter | null, + _cropRect?: SkRect | null + ): SkImageFilter { + throw throwNotImplementedOnRNWeb(); + } - MakeOffset(dx: number, dy: number, input: SkImageFilter | null) { + MakeOffset( + dx: number, + dy: number, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ) { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); + if (cropRect) { + console.warn( + "cropRect is not supported on React Native Web for MakeOffset" + ); + } const filter = this.CanvasKit.ImageFilter.MakeOffset(dx, dy, inputFilter); return new JsiSkImageFilter(this.CanvasKit, filter); } @@ -37,10 +213,18 @@ export class JsiSkImageFilterFactory channelY: ColorChannel, scale: number, in1: SkImageFilter, - input: SkImageFilter | null + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); + if (cropRect) { + console.warn( + "cropRect is not supported on React Native Web for MakeDisplacementMap" + ); + } const filter = this.CanvasKit.ImageFilter.MakeDisplacementMap( getEnum(this.CanvasKit, "ColorChannel", channelX), getEnum(this.CanvasKit, "ColorChannel", channelY), @@ -51,7 +235,21 @@ export class JsiSkImageFilterFactory return new JsiSkImageFilter(this.CanvasKit, filter); } - MakeShader(shader: SkShader, _input: SkImageFilter | null): SkImageFilter { + MakeShader( + shader: SkShader, + dither?: boolean, + cropRect?: SkRect | null + ): SkImageFilter { + if (dither !== undefined) { + console.warn( + "dither parameter is not supported on React Native Web for MakeShader" + ); + } + if (cropRect) { + console.warn( + "cropRect is not supported on React Native Web for MakeShader" + ); + } const filter = this.CanvasKit.ImageFilter.MakeShader( JsiSkImageFilter.fromValue(shader) ); @@ -62,25 +260,44 @@ export class JsiSkImageFilterFactory sigmaX: number, sigmaY: number, mode: TileMode, - input: SkImageFilter | null + input?: SkImageFilter | null, + cropRect?: SkRect | null ) { + if (cropRect) { + console.warn( + "cropRect is not supported on React Native Web for MakeBlur" + ); + } return new JsiSkImageFilter( this.CanvasKit, this.CanvasKit.ImageFilter.MakeBlur( sigmaX, sigmaY, getEnum(this.CanvasKit, "TileMode", mode), - input === null ? null : JsiSkImageFilter.fromValue(input) + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input) ) ); } - MakeColorFilter(cf: SkColorFilter, input: SkImageFilter | null) { + MakeColorFilter( + colorFilter: SkColorFilter, + input?: SkImageFilter | null, + cropRect?: SkRect | null + ) { + if (cropRect) { + console.warn( + "cropRect is not supported on React Native Web for MakeColorFilter" + ); + } return new JsiSkImageFilter( this.CanvasKit, this.CanvasKit.ImageFilter.MakeColorFilter( - JsiSkColorFilter.fromValue(cf), - input === null ? null : JsiSkImageFilter.fromValue(input) + JsiSkColorFilter.fromValue(colorFilter), + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input) ) ); } @@ -101,13 +318,17 @@ export class JsiSkImageFilterFactory sigmaX: number, sigmaY: number, color: SkColor, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); if (cropRect) { - throwNotImplementedOnRNWeb(); + console.warn( + "cropRect is not supported on React Native Web for MakeDropShadow" + ); } const filter = this.CanvasKit.ImageFilter.MakeDropShadow( dx, @@ -126,13 +347,17 @@ export class JsiSkImageFilterFactory sigmaX: number, sigmaY: number, color: SkColor, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); if (cropRect) { - throwNotImplementedOnRNWeb(); + console.warn( + "cropRect is not supported on React Native Web for MakeDropShadowOnly" + ); } const filter = this.CanvasKit.ImageFilter.MakeDropShadowOnly( dx, @@ -148,13 +373,17 @@ export class JsiSkImageFilterFactory MakeErode( rx: number, ry: number, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); if (cropRect) { - throwNotImplementedOnRNWeb(); + console.warn( + "cropRect is not supported on React Native Web for MakeErode" + ); } const filter = this.CanvasKit.ImageFilter.MakeErode(rx, ry, inputFilter); return new JsiSkImageFilter(this.CanvasKit, filter); @@ -163,13 +392,17 @@ export class JsiSkImageFilterFactory MakeDilate( rx: number, ry: number, - input: SkImageFilter | null, - cropRect?: SkRect + input?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - input === null ? null : JsiSkImageFilter.fromValue(input); + input === null || input === undefined + ? null + : JsiSkImageFilter.fromValue(input); if (cropRect) { - throwNotImplementedOnRNWeb(); + console.warn( + "cropRect is not supported on React Native Web for MakeDilate" + ); } const filter = this.CanvasKit.ImageFilter.MakeDilate(rx, ry, inputFilter); return new JsiSkImageFilter(this.CanvasKit, filter); @@ -178,15 +411,17 @@ export class JsiSkImageFilterFactory MakeBlend( mode: BlendMode, background: SkImageFilter, - foreground: SkImageFilter | null, - cropRect?: SkRect + foreground?: SkImageFilter | null, + cropRect?: SkRect | null ): SkImageFilter { const inputFilter = - foreground === null + foreground === null || foreground === undefined ? null : JsiSkImageFilter.fromValue(foreground); if (cropRect) { - throwNotImplementedOnRNWeb(); + console.warn( + "cropRect is not supported on React Native Web for MakeBlend" + ); } const filter = this.CanvasKit.ImageFilter.MakeBlend( getEnum(this.CanvasKit, "BlendMode", mode), @@ -199,7 +434,7 @@ export class JsiSkImageFilterFactory MakeRuntimeShader( _builder: SkRuntimeShaderBuilder, _childShaderName: string | null, - _input: SkImageFilter | null + _input?: SkImageFilter | null ) { return throwNotImplementedOnRNWeb(); } diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index c2f0424e8a..b3731f18d2 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -151,7 +151,7 @@ const declareDisplacementMapImageFilter = ( if (!shader) { throw new Error("DisplacementMap expects a shader as child"); } - const map = ctx.Skia.ImageFilter.MakeShader(shader, null); + const map = ctx.Skia.ImageFilter.MakeShader(shader); const imgf = ctx.Skia.ImageFilter.MakeDisplacementMap( ColorChannel[enumKey(channelX)], ColorChannel[enumKey(channelY)],