From 5c58811bd591312e386287b43107ea0b08e1dd0d Mon Sep 17 00:00:00 2001 From: Joe Vilches Date: Wed, 13 Mar 2024 19:30:54 -0700 Subject: [PATCH] Add FilterHelper to get all RenderEffects needed to apply filters (#43357) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43357 This diff adds a class to get the needed RenderEffects to support CSS filters on Android. This diff does not add any of the plumbing for it to actually work, that comes in the next diff, but I figured this was complicated and isolated enough to be on its own. Note that I did not add blur or drop shadow as those are a bit more involved and I plan on adding them later. Changelog: [Internal] Reviewed By: javache Differential Revision: D54603892 fbshipit-source-id: 5780d7c846fdb1116e29e0a940ee02da609b01f5 --- .../react/uimanager/FilterHelper.java | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.java diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.java new file mode 100644 index 00000000000..56bfa21ee67 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.java @@ -0,0 +1,246 @@ +/* + * 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. + */ + +package com.facebook.react.uimanager; + +import android.annotation.TargetApi; +import android.graphics.ColorFilter; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.RenderEffect; +import android.graphics.Shader; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +@TargetApi(31) +class FilterHelper { + static @Nullable RenderEffect parseFilters(@Nullable ReadableArray filters) { + if (filters == null) { + return null; + } + + RenderEffect chainedEffects = null; + for (int i = 0; i < filters.size(); i++) { + ReadableMap filter = filters.getMap(i); + @Nullable String filterName = filter.getString("name"); + if (filterName == null) { + continue; + } + + switch (filterName) { + case "brightness": + float brightnessAmount = (float) filter.getDouble("amount"); + ColorFilter brightnessFilter = + new ColorMatrixColorFilter(getBrightnessColorMatrix(brightnessAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, brightnessFilter); + break; + case "contrast": + float contrastAmount = (float) filter.getDouble("amount"); + ColorFilter contrastFilter = + new ColorMatrixColorFilter(getContrastColorMatrix(contrastAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, contrastFilter); + break; + case "grayscale": + float grayscaleAmount = (float) filter.getDouble("amount"); + ColorFilter grayscaleFilter = + new ColorMatrixColorFilter(getGrayscaleColorMatrix(grayscaleAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, grayscaleFilter); + break; + case "sepia": + float sepiaAmount = (float) filter.getDouble("amount"); + ColorFilter sepiaFilter = new ColorMatrixColorFilter(getSepiaColorMatrix(sepiaAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, sepiaFilter); + break; + case "saturate": + float saturateAmount = (float) filter.getDouble("amount"); + ColorFilter saturateFilter = + new ColorMatrixColorFilter(getSaturateColorMatrix(saturateAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, saturateFilter); + break; + case "hue-rotate": + float hueRotateAmount = (float) filter.getDouble("amount"); + ColorFilter hueRotateFilter = + new ColorMatrixColorFilter(getHueRotateColorMatrix(hueRotateAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, hueRotateFilter); + break; + case "invert": + float invertAmount = (float) filter.getDouble("amount"); + ColorFilter invertColorFilter = + new ColorMatrixColorFilter(getInvertColorMatrix(invertAmount)); + chainedEffects = chainColorFilterEffect(chainedEffects, invertColorFilter); + break; + case "blur": + float blurAmount = (float) filter.getDouble("amount"); + chainedEffects = chainBlurFilterEffect(chainedEffects, blurAmount); + break; + default: + throw new IllegalArgumentException("Invalid filter name: " + filterName); + } + } + + return chainedEffects; + } + + // https://www.w3.org/TR/filter-effects-1/#blurEquivalent + private static RenderEffect chainBlurFilterEffect( + @Nullable RenderEffect chainedEffects, float std) { + // Android takes blur amount as a radius while web takes a sigma. This value + // is used under the hood to convert between them on Android + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/utils/Blur.cpp + float sigmaToRadiusRatio = 0.57735f; + float radius = (std - 0.5f) / sigmaToRadiusRatio; + float scaledRadius = PixelUtil.toPixelFromDIP(radius); + + return chainedEffects == null + ? RenderEffect.createBlurEffect(scaledRadius, scaledRadius, Shader.TileMode.DECAL) + : RenderEffect.createBlurEffect( + scaledRadius, scaledRadius, chainedEffects, Shader.TileMode.DECAL); + } + + private static RenderEffect chainColorFilterEffect( + @Nullable RenderEffect chainedEffects, ColorFilter colorFilter) { + return chainedEffects == null + ? RenderEffect.createColorFilterEffect(colorFilter) + : RenderEffect.createColorFilterEffect(colorFilter, chainedEffects); + } + + // https://www.w3.org/TR/filter-effects-1/#brightnessEquivalent + private static ColorMatrix getBrightnessColorMatrix(float amount) { + ColorMatrix matrix = new ColorMatrix(); + matrix.setScale(amount, amount, amount, 1); + + return matrix; + } + + // https://www.w3.org/TR/filter-effects-1/#contrastEquivalent + private static ColorMatrix getContrastColorMatrix(float amount) { + // Multiply by 255 as Android operates in [0, 255] while the spec operates in [0, 1]. + // This really only matters if there is an intercept that needs to be added + float intercept = 255 * (-(amount / 2.0f) + 0.5f); + + float[] colorMatrix = { + amount, 0, 0, 0, intercept, // + 0, amount, 0, 0, intercept, // + 0, 0, amount, 0, intercept, // + 0, 0, 0, 1, 0 + }; + + return new ColorMatrix(colorMatrix); + } + + // https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent + private static float[] getGrayscaleColorMatrix(float amount) { + float inverseAmount = 1 - amount; + float[] colorMatrix = { + 0.2126f + 0.7874f * inverseAmount, + 0.7152f - 0.7152f * inverseAmount, + 0.0722f - 0.0722f * inverseAmount, + 0, + 0, + 0.2126f - 0.2126f * inverseAmount, + 0.7152f + 0.2848f * inverseAmount, + 0.0722f - 0.0722f * inverseAmount, + 0, + 0, + 0.2126f - 0.2126f * inverseAmount, + 0.7152f - 0.7152f * inverseAmount, + 0.0722f + 0.9278f * inverseAmount, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + }; + + return colorMatrix; + } + + // https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent + private static float[] getSepiaColorMatrix(float amount) { + float inverseAmount = 1 - amount; + float[] colorMatrix = { + 0.393f + 0.607f * inverseAmount, + 0.769f - 0.769f * inverseAmount, + 0.189f - 0.189f * inverseAmount, + 0, + 0, + 0.349f - 0.349f * inverseAmount, + 0.686f + 0.314f * inverseAmount, + 0.168f - 0.168f * inverseAmount, + 0, + 0, + 0.272f - 0.272f * inverseAmount, + 0.534f - 0.534f * inverseAmount, + 0.131f + 0.869f * inverseAmount, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + }; + + return colorMatrix; + } + + // https://www.w3.org/TR/filter-effects-1/#saturateEquivalent + private static ColorMatrix getSaturateColorMatrix(float amount) { + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(amount); + + return matrix; + } + + // https://www.w3.org/TR/filter-effects-1/#huerotateEquivalent + private static float[] getHueRotateColorMatrix(float amount) { + double amountRads = Math.toRadians(amount); + float cos = (float) Math.cos(amountRads); + float sin = (float) Math.sin(amountRads); + float[] matrix = { + 0.213f + 0.787f * cos - 0.213f * sin, + 0.715f - 0.715f * cos - 0.715f * sin, + 0.072f - 0.072f * cos + 0.928f * sin, + 0, + 0, + 0.213f - 0.213f * cos + 0.143f * sin, + 0.715f + 0.285f * cos + 0.140f * sin, + 0.072f - 0.072f * cos - 0.283f * sin, + 0, + 0, + 0.213f - 0.213f * cos - 0.787f * sin, + 0.715f - 0.715f * cos + 0.715f * sin, + 0.072f + 0.928f * cos + 0.072f * sin, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + }; + + return matrix; + } + + // https://www.w3.org/TR/filter-effects-1/#invertEquivalent + private static float[] getInvertColorMatrix(float amount) { + float slope = 1 - 2 * amount; + float intercept = amount * 255; + float[] matrix = { + slope, 0, 0, 0, intercept, // + 0, slope, 0, 0, intercept, // + 0, 0, slope, 0, intercept, // + 0, 0, 0, 1, 0 + }; + + return matrix; + } +}