mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-06 07:06:11 +00:00
fix: scaling when mask is set (#2299)
# Summary This PR resolves an issue raised in #1451. Currently, when a mask is used, we render the element as a bitmap (or platform equivalent), but the bitmap's size does not update accordingly with transformations. With these changes, the problem is addressed as follows: * **Android**: We utilize the original canvas layers to render the mask and element with the appropriate blending mode. * **iOS**: We create an offscreen context with the size multiplied by the screen scale and apply the original UIGraphics CTM (current transformation matrix) to the offscreen context. This ensures that the same transformations are applied as on the original context. Additionally, there is a significant performance improvement on Android as we are not creating three new Bitmaps and three new Canvases. ## Test Plan There are many ways for testing these changes, but the required ones are: * `TestsExample` app -> `Test1451.tsx` * `Example` app -> Mask section * `FabricExample` app -> Mask section ## Compatibility | OS | Implemented | | ------- | :---------: | | Android | ✅ | | iOS | ✅ | ## Preview <img width="337" alt="image" src="https://github.com/software-mansion/react-native-svg/assets/39670088/93dbae85-edbd-452a-84b0-9a50107b1361"> <img width="337" alt="image" src="https://github.com/software-mansion/react-native-svg/assets/39670088/07838dff-cb2d-4072-a2fc-5c16a76f6c33">
This commit is contained in:
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import ColorTest from './src/ColorTest';
|
||||
import PointerEventsBoxNone from './src/PointerEventsBoxNone';
|
||||
import Test1374 from './src/Test1374';
|
||||
import Test1451 from './src/Test1451';
|
||||
import Test1718 from './src/Test1718';
|
||||
import Test1813 from './src/Test1813';
|
||||
import Test1845 from './src/Test1845';
|
||||
|
||||
51
TestsExample/src/Test1451.tsx
Normal file
51
TestsExample/src/Test1451.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {Animated, View} from 'react-native';
|
||||
import {Circle, Mask, Path, Rect, Svg} from 'react-native-svg';
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle as any);
|
||||
|
||||
export default () => {
|
||||
const zoom = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(zoom, {
|
||||
toValue: 4,
|
||||
duration: 2000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(zoom, {
|
||||
toValue: 1,
|
||||
duration: 2000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
).start();
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
||||
<View style={{borderWidth: 3}}>
|
||||
<Svg width="400" height="400" viewBox="0 0 400 400">
|
||||
<Mask id="myMask">
|
||||
<Rect x="0" y="0" width="100" height="100" fill="white" />
|
||||
<Path
|
||||
d="M10,35 A20,20,0,0,1,50,35 A20,20,0,0,1,90,35 Q90,65,50,95 Q10,65,10,35 Z"
|
||||
fill="black"
|
||||
/>
|
||||
</Mask>
|
||||
<Rect x="0" y="0" width="150" height="150" fill="pink" />
|
||||
<AnimatedCircle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="50"
|
||||
fill="purple"
|
||||
mask="url(#myMask)"
|
||||
matrix={[zoom, 0, 0, zoom, 0, 0]}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
package com.horcrux.svg;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
@@ -325,71 +326,70 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private static double saturate(double v) {
|
||||
return v <= 0 ? 0 : (v >= 1 ? 1 : v);
|
||||
}
|
||||
|
||||
void render(Canvas canvas, Paint paint, float opacity) {
|
||||
MaskView mask = null;
|
||||
|
||||
if (mMask != null) {
|
||||
SvgView root = getSvgView();
|
||||
mask = (MaskView) root.getDefinedMask(mMask);
|
||||
}
|
||||
|
||||
if (mask != null) {
|
||||
Rect clipBounds = canvas.getClipBounds();
|
||||
int height = clipBounds.height();
|
||||
int width = clipBounds.width();
|
||||
// https://www.w3.org/TR/SVG11/masking.html
|
||||
// Adding a mask involves several steps
|
||||
// 1. applying luminanceToAlpha to the mask element
|
||||
// 2. merging the alpha channel of the element with the alpha channel from the previous step
|
||||
// 3. applying the result from step 2 to the target element
|
||||
|
||||
Bitmap maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Bitmap original = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
canvas.saveLayer(null, paint);
|
||||
draw(canvas, paint, opacity);
|
||||
|
||||
Canvas originalCanvas = new Canvas(original);
|
||||
Canvas maskCanvas = new Canvas(maskBitmap);
|
||||
Canvas resultCanvas = new Canvas(result);
|
||||
Paint dstInPaint = new Paint();
|
||||
dstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
|
||||
|
||||
// Clip to mask bounds and render the mask
|
||||
// prepare step 3 - combined layer
|
||||
canvas.saveLayer(null, dstInPaint);
|
||||
|
||||
// step 1 - luminance layer
|
||||
// prepare maskPaint with luminanceToAlpha
|
||||
// https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent
|
||||
Paint luminancePaint = new Paint();
|
||||
ColorMatrix luminanceToAlpha =
|
||||
new ColorMatrix(
|
||||
new float[] {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125f, 0.7154f, 0.0721f, 0, 0
|
||||
});
|
||||
luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha));
|
||||
canvas.saveLayer(null, luminancePaint);
|
||||
|
||||
// calculate mask bounds
|
||||
float maskX = (float) relativeOnWidth(mask.mX);
|
||||
float maskY = (float) relativeOnHeight(mask.mY);
|
||||
float maskWidth = (float) relativeOnWidth(mask.mW);
|
||||
float maskHeight = (float) relativeOnHeight(mask.mH);
|
||||
maskCanvas.clipRect(maskX, maskY, maskWidth + maskX, maskHeight + maskY);
|
||||
// clip to mask bounds
|
||||
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);
|
||||
|
||||
Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mask.draw(maskCanvas, maskPaint, 1);
|
||||
mask.draw(canvas, paint, 1f);
|
||||
|
||||
// Apply luminanceToAlpha filter primitive
|
||||
// https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement
|
||||
int nPixels = width * height;
|
||||
int[] pixels = new int[nPixels];
|
||||
maskBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||
// close luminance layer
|
||||
canvas.restore();
|
||||
|
||||
for (int i = 0; i < nPixels; i++) {
|
||||
int color = pixels[i];
|
||||
// step 2 - alpha layer
|
||||
canvas.saveLayer(null, dstInPaint);
|
||||
// clip to mask bounds
|
||||
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);
|
||||
|
||||
int r = (color >> 16) & 0xFF;
|
||||
int g = (color >> 8) & 0xFF;
|
||||
int b = color & 0xFF;
|
||||
int a = color >>> 24;
|
||||
mask.draw(canvas, paint, 1f);
|
||||
|
||||
double luminance = saturate(((0.299 * r) + (0.587 * g) + (0.144 * b)) / 255);
|
||||
int alpha = (int) (a * luminance);
|
||||
int pixel = (alpha << 24);
|
||||
pixels[i] = pixel;
|
||||
}
|
||||
// close alpha layer
|
||||
canvas.restore();
|
||||
|
||||
maskBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
// close combined layer
|
||||
canvas.restore();
|
||||
|
||||
// Render content of current SVG Renderable to image
|
||||
draw(originalCanvas, paint, opacity);
|
||||
|
||||
// Blend current element and mask
|
||||
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
|
||||
resultCanvas.drawBitmap(original, 0, 0, null);
|
||||
resultCanvas.drawBitmap(maskBitmap, 0, 0, maskPaint);
|
||||
|
||||
// Render composited result into current render context
|
||||
canvas.drawBitmap(result, 0, 0, paint);
|
||||
// close element layer
|
||||
canvas.restore();
|
||||
} else {
|
||||
draw(canvas, paint, opacity);
|
||||
}
|
||||
|
||||
@@ -241,10 +241,8 @@ UInt32 saturate(CGFloat value)
|
||||
if (self.mask) {
|
||||
// https://www.w3.org/TR/SVG11/masking.html#MaskElement
|
||||
RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask];
|
||||
CGRect bounds = CGContextGetClipBoundingBox(context);
|
||||
CGSize boundsSize = bounds.size;
|
||||
CGFloat height = boundsSize.height;
|
||||
CGFloat width = boundsSize.width;
|
||||
CGFloat height = rect.size.height;
|
||||
CGFloat width = rect.size.width;
|
||||
CGFloat scale = 0.0;
|
||||
#if TARGET_OS_OSX
|
||||
scale = [[NSScreen mainScreen] backingScaleFactor];
|
||||
@@ -263,7 +261,10 @@ UInt32 saturate(CGFloat value)
|
||||
NSUInteger scaledHeight = iheight * iscale;
|
||||
NSUInteger scaledWidth = iwidth * iscale;
|
||||
NSUInteger npixels = scaledHeight * scaledWidth;
|
||||
CGRect drawBounds = CGRectMake(0, 0, width, height);
|
||||
CGAffineTransform screenScaleCTM = CGAffineTransformMake(scale, 0, 0, scale, 0, 0);
|
||||
CGRect scaledRect = CGRectApplyAffineTransform(rect, screenScaleCTM);
|
||||
// Get current context transformations for offscreenContext
|
||||
CGAffineTransform currentCTM = CGContextGetCTM(context);
|
||||
|
||||
// Allocate pixel buffer and bitmap context for mask
|
||||
NSUInteger bytesPerPixel = 4;
|
||||
@@ -279,16 +280,16 @@ UInt32 saturate(CGFloat value)
|
||||
bytesPerRow,
|
||||
colorSpace,
|
||||
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
||||
CGContextScaleCTM(bcontext, iscale, iscale);
|
||||
CGContextConcatCTM(bcontext, currentCTM);
|
||||
|
||||
// Clip to mask bounds and render the mask
|
||||
CGFloat x = [self relativeOn:[_maskNode x] relative:width];
|
||||
CGFloat y = [self relativeOn:[_maskNode y] relative:height];
|
||||
CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width];
|
||||
CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height];
|
||||
CGRect maskBounds = CGRectMake(x, y, w, h);
|
||||
CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM);
|
||||
CGContextClipToRect(bcontext, maskBounds);
|
||||
[_maskNode renderLayerTo:bcontext rect:rect];
|
||||
[_maskNode renderLayerTo:bcontext rect:scaledRect];
|
||||
|
||||
// Apply luminanceToAlpha filter primitive
|
||||
// https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement
|
||||
@@ -313,26 +314,33 @@ UInt32 saturate(CGFloat value)
|
||||
|
||||
#if !TARGET_OS_OSX // [macOS]
|
||||
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
format.scale = scale;
|
||||
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:boundsSize format:format];
|
||||
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size format:format];
|
||||
|
||||
// Get the content image
|
||||
UIImage *contentImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) {
|
||||
CGContextTranslateCTM(rendererContext.CGContext, 0.0, height);
|
||||
CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0);
|
||||
[self renderLayerTo:rendererContext.CGContext rect:rect];
|
||||
CGContextConcatCTM(
|
||||
rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext)));
|
||||
CGContextConcatCTM(rendererContext.CGContext, currentCTM);
|
||||
[self renderLayerTo:rendererContext.CGContext rect:scaledRect];
|
||||
}];
|
||||
|
||||
// Blend current element and mask
|
||||
UIImage *blendedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) {
|
||||
CGContextConcatCTM(
|
||||
rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext)));
|
||||
CGContextTranslateCTM(rendererContext.CGContext, 0.0, scaledHeight);
|
||||
CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0);
|
||||
|
||||
CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeCopy);
|
||||
CGContextDrawImage(rendererContext.CGContext, drawBounds, maskImage);
|
||||
CGContextDrawImage(rendererContext.CGContext, scaledRect, maskImage);
|
||||
CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeSourceIn);
|
||||
CGContextDrawImage(rendererContext.CGContext, drawBounds, contentImage.CGImage);
|
||||
CGContextDrawImage(rendererContext.CGContext, scaledRect, contentImage.CGImage);
|
||||
}];
|
||||
|
||||
// Render blended result into current render context
|
||||
[blendedImage drawInRect:drawBounds];
|
||||
CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM));
|
||||
[blendedImage drawInRect:scaledRect];
|
||||
CGContextConcatCTM(context, currentCTM);
|
||||
|
||||
// Render blended result into current render context
|
||||
CGImageRelease(maskImage);
|
||||
|
||||
Reference in New Issue
Block a user