From 944d40f204924fc27eebceb536f326f59bc5458f Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Mon, 15 Jul 2024 18:10:17 -0700 Subject: [PATCH] Add mix-blend-mode effects to iOS (#45304) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/45304 Add support for most keyword values of mix-blend-mode on iOS and added RNTester Example Missing compositing operators and global values Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D59402969 fbshipit-source-id: b7e1aaed01fbf8f80e04ad0fa73d2ef63b5ad933 --- .../View/RCTViewComponentView.mm | 57 ++++++++++++++ .../renderer/components/view/BaseViewProps.h | 3 +- .../components/view/ViewShadowNode.cpp | 3 +- .../renderer/components/view/conversions.h | 22 ++++++ .../react/renderer/graphics/BlendMode.h | 76 +++++++++++++++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 4 + 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index b55105b25aed44..4faaa925c1294f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -20,6 +20,7 @@ #import #import #import +#import #ifdef RCT_DYNAMIC_FRAMEWORKS #import @@ -403,6 +404,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & _needsInvalidateLayer = YES; } + // `mixBlendMode` + if (oldViewProps.mixBlendMode != newViewProps.mixBlendMode) { + _needsInvalidateLayer = YES; + } + // `boxShadow` if (oldViewProps.boxShadow != newViewProps.boxShadow) { _needsInvalidateLayer = YES; @@ -776,6 +782,57 @@ - (void)invalidateLayer [self.layer addSublayer:_filterLayer]; } + switch (_props->mixBlendMode) { + case BlendMode::Multiply: + layer.compositingFilter = @"multiplyBlendMode"; + break; + case BlendMode::Screen: + layer.compositingFilter = @"screenBlendMode"; + break; + case BlendMode::Overlay: + layer.compositingFilter = @"overlayBlendMode"; + break; + case BlendMode::Darken: + layer.compositingFilter = @"darkenBlendMode"; + break; + case BlendMode::Lighten: + layer.compositingFilter = @"lightenBlendMode"; + break; + case BlendMode::ColorDodge: + layer.compositingFilter = @"colorDodgeBlendMode"; + break; + case BlendMode::ColorBurn: + layer.compositingFilter = @"colorBurnBlendMode"; + break; + case BlendMode::HardLight: + layer.compositingFilter = @"hardLightBlendMode"; + break; + case BlendMode::SoftLight: + layer.compositingFilter = @"softLightBlendMode"; + break; + case BlendMode::Difference: + layer.compositingFilter = @"differenceBlendMode"; + break; + case BlendMode::Exclusion: + layer.compositingFilter = @"exclusionBlendMode"; + break; + case BlendMode::Hue: + layer.compositingFilter = @"hueBlendMode"; + break; + case BlendMode::Saturation: + layer.compositingFilter = @"saturationBlendMode"; + break; + case BlendMode::Color: + layer.compositingFilter = @"colorBlendMode"; + break; + case BlendMode::Luminosity: + layer.compositingFilter = @"luminosityBlendMode"; + break; + case BlendMode::Normal: + layer.compositingFilter = nil; + break; + } + _boxShadowLayer = nil; if (!_props->boxShadow.empty()) { _boxShadowLayer = [CALayer layer]; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index 9602c719cddc8a..4d79b66943674d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +64,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { std::vector filter{}; // MixBlendMode - std::string mixBlendMode; + BlendMode mixBlendMode; // Transform Transform transform{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index c3ca8496af6c38..b0d24f24d8f95f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -61,7 +61,8 @@ void ViewShadowNode::initialize() noexcept { viewProps.accessibilityViewIsModal || viewProps.importantForAccessibility != ImportantForAccessibility::Auto || viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto || - !viewProps.filter.empty() || !viewProps.mixBlendMode.empty() || + !viewProps.filter.empty() || + viewProps.mixBlendMode != BlendMode::Normal || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps); bool formsView = formsStackingContext || diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 5e9bb59a0fe574..aa455d59bacf3c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1069,6 +1070,27 @@ inline void fromRawValue( result = filter; } +inline void fromRawValue( + const PropsParserContext& /*context*/, + const RawValue& value, + BlendMode& result) { + react_native_expect(value.hasType()); + result = BlendMode::Normal; + if (!value.hasType()) { + return; + } + + auto rawBlendMode = static_cast(value); + std::optional blendMode = blendModeFromString(rawBlendMode); + + if (!blendMode) { + LOG(ERROR) << "Could not parse blend mode: " << rawBlendMode; + return; + } + + result = blendMode.value(); +} + template inline std::string toString(const std::array vec) { std::string s; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h b/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h new file mode 100644 index 00000000000000..9399ff8e4b52cb --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/BlendMode.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace facebook::react { + +enum class BlendMode { + Normal, + Multiply, + Screen, + Overlay, + Darken, + Lighten, + ColorDodge, + ColorBurn, + HardLight, + SoftLight, + Difference, + Exclusion, + Hue, + Saturation, + Color, + Luminosity, +}; + +inline std::optional blendModeFromString( + std::string_view blendModeName) { + if (blendModeName == "normal") { + return BlendMode::Normal; + } else if (blendModeName == "multiply") { + return BlendMode::Multiply; + } else if (blendModeName == "screen") { + return BlendMode::Screen; + } else if (blendModeName == "overlay") { + return BlendMode::Overlay; + } else if (blendModeName == "darken") { + return BlendMode::Darken; + } else if (blendModeName == "lighten") { + return BlendMode::Lighten; + } else if (blendModeName == "color-dodge") { + return BlendMode::ColorDodge; + } else if (blendModeName == "color-burn") { + return BlendMode::ColorBurn; + } else if (blendModeName == "hard-light") { + return BlendMode::HardLight; + } else if (blendModeName == "soft-light") { + return BlendMode::SoftLight; + } else if (blendModeName == "difference") { + return BlendMode::Difference; + } else if (blendModeName == "exclusion") { + return BlendMode::Exclusion; + } else if (blendModeName == "hue") { + return BlendMode::Hue; + } else if (blendModeName == "saturation") { + return BlendMode::Saturation; + } else if (blendModeName == "color") { + return BlendMode::Color; + } else if (blendModeName == "luminosity") { + return BlendMode::Luminosity; + } else { + return std::nullopt; + } +} +} // namespace facebook::react diff --git a/packages/rn-tester/js/utils/RNTesterList.ios.js b/packages/rn-tester/js/utils/RNTesterList.ios.js index 7c2ab0ce383b75..fecfd4ee407428 100644 --- a/packages/rn-tester/js/utils/RNTesterList.ios.js +++ b/packages/rn-tester/js/utils/RNTesterList.ios.js @@ -298,6 +298,10 @@ const APIs: Array = ([ key: 'FilterExample', module: require('../examples/Filter/FilterExample'), }, + { + key: 'MixBlendModeExample', + module: require('../examples/MixBlendMode/MixBlendModeExample'), + }, { key: 'TurboModuleExample', module: require('../examples/TurboModule/TurboModuleExample'),