Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: scaling when mask is set #2299

Merged
merged 12 commits into from
Jun 26, 2024
1 change: 1 addition & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
51 changes: 51 additions & 0 deletions TestsExample/src/Test1451.tsx
Original file line number Diff line number Diff line change
@@ -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: 2,
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 300 300">
<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>
);
};
83 changes: 39 additions & 44 deletions android/src/main/java/com/horcrux/svg/RenderableView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -329,71 +330,65 @@ public void setPropList(@Nullable ReadableArray propList) {
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();

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);
if (mask != null) {
// 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

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
// calculate mask bounds
float maskX = (float) relativeOnWidth(mask.mX);
jakex7 marked this conversation as resolved.
Show resolved Hide resolved
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);

Paint maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mask.draw(maskCanvas, maskPaint, 1);
// step 3 - combined layer
jakex7 marked this conversation as resolved.
Show resolved Hide resolved
canvas.saveLayer(null, dstInPaint);

// 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);
// 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
jakex7 marked this conversation as resolved.
Show resolved Hide resolved
});
luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha));
canvas.saveLayer(null, luminancePaint);
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);

for (int i = 0; i < nPixels; i++) {
int color = pixels[i];
mask.draw(canvas, paint, 1f);

int r = (color >> 16) & 0xFF;
int g = (color >> 8) & 0xFF;
int b = color & 0xFF;
int a = color >>> 24;
// close luminance layer
canvas.restore();

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;
}
// step 2 - alpha layer
canvas.saveLayer(null, dstInPaint);
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);

maskBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
mask.draw(canvas, paint, 1f);

// Render content of current SVG Renderable to image
draw(originalCanvas, paint, opacity);
// close alpha layer
canvas.restore();

// 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);
// close combined layer
canvas.restore();

// Render composited result into current render context
canvas.drawBitmap(result, 0, 0, paint);
// close element layer
canvas.restore();
} else {
draw(canvas, paint, opacity);
}
Expand Down
19 changes: 19 additions & 0 deletions apple/Filters/LuminanceToAlpha.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef LuminanceToAlpha_h
#define LuminanceToAlpha_h

static CIImage *applyLuminanceToAlphaFilter(CIImage *inputImage)
{
CIFilter *luminanceToAlpha = [CIFilter filterWithName:@"CIColorMatrix"];
[luminanceToAlpha setDefaults];
CGFloat alpha[4] = {0.2125, 0.7154, 0.0721, 0};
jakex7 marked this conversation as resolved.
Show resolved Hide resolved
CGFloat zero[4] = {0, 0, 0, 0};
[luminanceToAlpha setValue:inputImage forKey:@"inputImage"];
[luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"];
[luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"];
[luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"];
[luminanceToAlpha setValue:[CIVector vectorWithValues:alpha count:4] forKey:@"inputAVector"];
[luminanceToAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"];
return [luminanceToAlpha valueForKey:@"outputImage"];
}

#endif /* LuminanceToAlpha_h */
2 changes: 2 additions & 0 deletions apple/RNSVGRenderable.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@

- (void)resetProperties;

+ (CIContext *)sharedCIContext;

@end
Loading
Loading