diff --git a/apps/docs/docs/image.md b/apps/docs/docs/image.md index 0239cc4b62..c4d34ce8ae 100644 --- a/apps/docs/docs/image.md +++ b/apps/docs/docs/image.md @@ -85,6 +85,7 @@ Images can be drawn by specifying the output rectangle and how the image should | width | `number` | The width of the destination image. | | height | `number` | The height of the destination image. | | fit? | `Fit` | The method used to fit the image into the rectangle. Values can be `contain`, `fill`, `cover`, `fitHeight`, `fitWidth`, `scaleDown`, or `none` (the default is `contain`). | +| sampling? | `Sampling` | The method used to sample the image. see ([sampling options](/docs/images#sampling-options)). | ### Example @@ -101,6 +102,43 @@ const ImageDemo = () => { }; ``` +### Sampling Options + +The `sampling` prop allows you to control how the image is sampled when it is drawn. +Use cubic sampling for best quality: you can use the default `sampling={CubicSampling}` (defaults to `{ B: 0, C: 0 }`) or any value you would like: `sampling={{ B: 0, C: 0.5 }}`. + +You can also use filter modes (`nearest` or `linear`) and mimap modes (`none`, `nearest`, or `linear`). Default is `nearest`. + +```tsx twoslash +import { Canvas, Image, useImage, CubicSampling, FilterMode, MipmapMode } from "@shopify/react-native-skia"; + +const ImageDemo = () => { + const image = useImage(require("./assets/oslo.jpg")); + return ( + + + + + ); +}; +``` + ### fit="contain" ![fit="contain"](assets/images/contain.png) diff --git a/apps/docs/docs/shaders/images.md b/apps/docs/docs/shaders/images.md index e348429834..9ee952708a 100644 --- a/apps/docs/docs/shaders/images.md +++ b/apps/docs/docs/shaders/images.md @@ -15,11 +15,10 @@ It will use cubic sampling. | image | `SkImage` | Image instance. | | tx? | `TileMode` | Can be `clamp`, `repeat`, `mirror`, or `decal`. | | ty? | `TileMode` | Can be `clamp`, `repeat`, `mirror`, or `decal`. | -| fm? | `FilterMode` | Can be `linear` or `nearest`. | -| mm? | `MipmapMode` | Can be `none`, `linear` or `nearest`. | | fit? | `Fit` | Calculate the transformation matrix to fit the rectangle defined by `fitRect`. See [images](/docs/images). | | rect? | `SkRect` | The destination rectangle to calculate the transformation matrix via the `fit` property. | | transform? | `Transforms2d` | see [transformations](/docs/group#transformations). | +| sampling? | `Sampling` | The method used to sample the image. see ([sampling options](/docs/images#sampling-options)). | ### Example ```tsx twoslash diff --git a/apps/docs/docs/shapes/atlas.md b/apps/docs/docs/shapes/atlas.md index 7270fa5b52..39f052578c 100644 --- a/apps/docs/docs/shapes/atlas.md +++ b/apps/docs/docs/shapes/atlas.md @@ -16,6 +16,7 @@ Its design particularly useful when using with [Reanimated](#animations). | transforms | `RSXform[]` | Rotation/scale transforms to be applied for each sprite. | | colors? | `SkColor[]` | Optional. Color to blend the sprites with. | | blendMode? | `BlendMode` | Optional. Blend mode used to combine sprites and colors together. | +| sampling? | `Sampling` | The method used to sample the image. see ([sampling options](/docs/images#sampling-options)). | ## RSXform diff --git a/apps/docs/static/img/atlas/hello-world.png b/apps/docs/static/img/atlas/hello-world.png index 7f9b4eed66..615f779a43 100644 Binary files a/apps/docs/static/img/atlas/hello-world.png and b/apps/docs/static/img/atlas/hello-world.png differ diff --git a/apps/docs/static/img/mask/blend-mode-mask.png b/apps/docs/static/img/mask/blend-mode-mask.png index 47cd28a665..2de47ce965 100644 Binary files a/apps/docs/static/img/mask/blend-mode-mask.png and b/apps/docs/static/img/mask/blend-mode-mask.png differ diff --git a/apps/paper/ios/Podfile.lock b/apps/paper/ios/Podfile.lock index 8c192df5e4..75dbdfd1bd 100644 --- a/apps/paper/ios/Podfile.lock +++ b/apps/paper/ios/Podfile.lock @@ -1694,6 +1694,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNSVG (15.9.0): + - React-Core - SocketRocket (0.7.0) - Yoga (0.0.0) @@ -1768,6 +1770,7 @@ DEPENDENCIES: - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) - RNReanimated (from `../../../node_modules/react-native-reanimated`) - RNScreens (from `../../../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1912,6 +1915,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-reanimated" RNScreens: :path: "../../../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" Yoga: :path: "../../../node_modules/react-native/ReactCommon/yoga" @@ -1952,7 +1957,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 1c08607305558666fd16678b85ef135e455d5c96 React-microtasksnativemodule: 87b8de96f937faefece8afd2cb3a518321b2ef99 react-native-safe-area-context: ab8f4a3d8180913bd78ae75dd599c94cce3d5e9a - react-native-skia: b9a7bb46f52a1b9a06b89d477ba6a8c1fe96a69d + react-native-skia: abdd2ba5abed61445a25d9461b0a87bc08c50f64 react-native-slider: 97ce0bd921f40de79cead9754546d5e4e7ba44f8 react-native-wgpu: 8d0437a304318e0e3d6ccbfed2a39880f8eae4dd React-nativeconfig: 57781b79e11d5af7573e6f77cbf1143b71802a6d @@ -1984,6 +1989,7 @@ SPEC CHECKSUMS: RNGestureHandler: 939f21fabf5d45a725c0bf175eb819dd25cf2e70 RNReanimated: 9d20a811e6987cba268ef4f56242dfabd40e85c1 RNScreens: b03d696c70cc5235ce4587fcc27ae1a93a48f98c + RNSVG: 3d2bdcaef51c8071880a9c0072fe324f4423a3ba SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae diff --git a/apps/remotion/package.json b/apps/remotion/package.json index 47c243b2cc..edb13f7fd2 100644 --- a/apps/remotion/package.json +++ b/apps/remotion/package.json @@ -4,7 +4,7 @@ "description": "My Remotion video", "scripts": { "start": "remotion preview src/index.tsx", - "build": "remotion render src/index.tsx Playground current.mov --gl=angle --pixel-format=yuva444p10le --codec=prores --prores-profile=4444", + "build-remotion": "remotion render src/index.tsx Playground current.mov --gl=angle --pixel-format=yuva444p10le --codec=prores --prores-profile=4444", "upgrade": "remotion upgrade", "build:sequence": "remotion render src/index.tsx Playground stills --sequence --gl=angle", "lint": "eslint . --ext .ts,.tsx --max-warnings 0 --cache", diff --git a/packages/skia/cpp/api/JsiSkCanvas.h b/packages/skia/cpp/api/JsiSkCanvas.h index 38b70bdd22..8395e5edd6 100644 --- a/packages/skia/cpp/api/JsiSkCanvas.h +++ b/packages/skia/cpp/api/JsiSkCanvas.h @@ -546,8 +546,10 @@ class JsiSkCanvas : public JsiSkHostObject { colors.push_back(color); } } - - SkSamplingOptions sampling(SkFilterMode::kLinear, SkMipmapMode::kNone); + SkSamplingOptions sampling(SkFilterMode::kLinear); + if (count > 6) { + sampling = SamplingOptionsFromValue(runtime, arguments[5]); + } _canvas->drawAtlas(atlas.get(), xforms.data(), skRects.data(), colors.data(), skRects.size(), blendMode, sampling, nullptr, paint.get()); diff --git a/packages/skia/cpp/api/JsiSkImage.h b/packages/skia/cpp/api/JsiSkImage.h index 23061e6417..8c949b3b22 100644 --- a/packages/skia/cpp/api/JsiSkImage.h +++ b/packages/skia/cpp/api/JsiSkImage.h @@ -36,6 +36,30 @@ namespace RNSkia { namespace jsi = facebook::jsi; +inline SkSamplingOptions SamplingOptionsFromValue(jsi::Runtime &runtime, + const jsi::Value &val) { + SkSamplingOptions samplingOptions(SkFilterMode::kLinear); + if (val.isObject()) { + auto object = val.asObject(runtime); + if (object.hasProperty(runtime, "B") && object.hasProperty(runtime, "C")) { + auto B = static_cast(object.getProperty(runtime, "B").asNumber()); + auto C = static_cast(object.getProperty(runtime, "C").asNumber()); + samplingOptions = SkSamplingOptions({B, C}); + } else if (object.hasProperty(runtime, "filter")) { + auto filter = static_cast( + object.getProperty(runtime, "filter").asNumber()); + if (object.hasProperty(runtime, "mipmap")) { + auto mipmap = static_cast( + object.getProperty(runtime, "mipmap").asNumber()); + samplingOptions = SkSamplingOptions(filter, mipmap); + } else { + samplingOptions = SkSamplingOptions(filter); + } + } + } + return samplingOptions; +} + class JsiSkImage : public JsiSkWrappingSkPtrHostObject { public: // TODO-API: Properties? diff --git a/packages/skia/cpp/rnskia/dom/nodes/JsiAtlasNode.h b/packages/skia/cpp/rnskia/dom/nodes/JsiAtlasNode.h index f57ac047fb..f0abef0a3c 100644 --- a/packages/skia/cpp/rnskia/dom/nodes/JsiAtlasNode.h +++ b/packages/skia/cpp/rnskia/dom/nodes/JsiAtlasNode.h @@ -3,6 +3,7 @@ #include "JsiDomDrawingNode.h" #include "RSXformProp.h" #include "RectProp.h" +#include "SamplingProp.h" #include "SkImageProps.h" #include @@ -28,7 +29,8 @@ class JsiAtlasNode : public JsiDomDrawingNode, ? *_blendModeProp->getDerivedValue() : SkBlendMode::kDstOver; auto paint = *context->getPaint(); - SkSamplingOptions sampling(SkFilterMode::kLinear, SkMipmapMode::kNone); + auto sampling = _samplingProp->isSet() ? *_samplingProp->getDerivedValue() + : SkSamplingOptions(SkFilterMode::kLinear); context->getCanvas()->drawAtlas( image.get(), transforms->data(), sprites->data(), colors == nullptr ? nullptr : colors->data(), sprites->size(), @@ -43,6 +45,7 @@ class JsiAtlasNode : public JsiDomDrawingNode, _imageProp = container->defineProperty("image"); _colorsProp = container->defineProperty("colors"); _blendModeProp = container->defineProperty("blendMode"); + _samplingProp = container->defineProperty("sampling"); _rectsProp->require(); _rsxFormsProp->require(); @@ -54,6 +57,7 @@ class JsiAtlasNode : public JsiDomDrawingNode, RSXFormsProp *_rsxFormsProp; ColorsProp *_colorsProp; BlendModeProp *_blendModeProp; + SamplingProp *_samplingProp; }; } // namespace RNSkia diff --git a/packages/skia/cpp/rnskia/dom/nodes/JsiImageNode.h b/packages/skia/cpp/rnskia/dom/nodes/JsiImageNode.h index bcdcf93b23..21268683ab 100644 --- a/packages/skia/cpp/rnskia/dom/nodes/JsiImageNode.h +++ b/packages/skia/cpp/rnskia/dom/nodes/JsiImageNode.h @@ -1,6 +1,7 @@ #pragma once #include "JsiDomDrawingNode.h" +#include "SamplingProp.h" #include "SkImageProps.h" #include @@ -20,19 +21,22 @@ class JsiImageNode : public JsiDomDrawingNode, if (image == nullptr) { return; } - - context->getCanvas()->drawImageRect( - image, rects->src, rects->dst, SkSamplingOptions(), - context->getPaint().get(), SkCanvas::kStrict_SrcRectConstraint); + auto sampling = _samplingProp->isSet() ? *_samplingProp->getDerivedValue() + : SkSamplingOptions(SkFilterMode::kLinear); + context->getCanvas()->drawImageRect(image, rects->src, rects->dst, sampling, + context->getPaint().get(), + SkCanvas::kStrict_SrcRectConstraint); } void defineProperties(NodePropsContainer *container) override { JsiDomDrawingNode::defineProperties(container); _imageProps = container->defineProperty(); + _samplingProp = container->defineProperty("sampling"); } private: ImageProps *_imageProps; + SamplingProp *_samplingProp; }; } // namespace RNSkia diff --git a/packages/skia/cpp/rnskia/dom/nodes/JsiShaderNodes.h b/packages/skia/cpp/rnskia/dom/nodes/JsiShaderNodes.h index 39f16f52d8..c18621d7ff 100644 --- a/packages/skia/cpp/rnskia/dom/nodes/JsiShaderNodes.h +++ b/packages/skia/cpp/rnskia/dom/nodes/JsiShaderNodes.h @@ -6,6 +6,7 @@ #include "ColorProp.h" #include "NodeProp.h" #include "NumbersProp.h" +#include "SamplingProp.h" #include "TileModeProp.h" #include "TransformsProps.h" #include "UniformsProp.h" @@ -120,12 +121,12 @@ class JsiImageShaderNode : public JsiDomDeclarationNode, } } - context->getShaders()->push(image->makeShader( - *_txProp->getDerivedValue(), *_tyProp->getDerivedValue(), - SkSamplingOptions( - getFilterModeFromString(_filterModeProp->value().getAsString()), - getMipmapModeFromString(_mipmapModeProp->value().getAsString())), - &_matrix)); + auto samplingOptions = _samplingProp->isSet() + ? *_samplingProp->getDerivedValue() + : SkSamplingOptions(SkFilterMode::kLinear); + context->getShaders()->push(image->makeShader(*_txProp->getDerivedValue(), + *_tyProp->getDerivedValue(), + samplingOptions, &_matrix)); } protected: @@ -133,17 +134,14 @@ class JsiImageShaderNode : public JsiDomDeclarationNode, JsiDomDeclarationNode::defineProperties(container); _txProp = container->defineProperty("tx"); _tyProp = container->defineProperty("ty"); - _filterModeProp = container->defineProperty("fm"); - _mipmapModeProp = container->defineProperty("mm"); + _samplingProp = container->defineProperty("sampling"); _imageProps = container->defineProperty(); _transformProp = container->defineProperty("transform"); _originProp = container->defineProperty("origin"); _txProp->require(); _tyProp->require(); - _filterModeProp->require(); - _mipmapModeProp->require(); _transformProp->require(); @@ -152,41 +150,14 @@ class JsiImageShaderNode : public JsiDomDeclarationNode, } private: - SkFilterMode getFilterModeFromString(const std::string &value) { - if (value == "last") { - return SkFilterMode::kLast; - } else if (value == "linear") { - return SkFilterMode::kLinear; - } else if (value == "nearest") { - return SkFilterMode::kNearest; - } - throw std::runtime_error("The value \"" + value + - "\" is not a valid Filter Mode."); - } - - SkMipmapMode getMipmapModeFromString(const std::string &value) { - if (value == "last") { - return SkMipmapMode::kLast; - } else if (value == "last") { - return SkMipmapMode::kLast; - } else if (value == "last") { - return SkMipmapMode::kLast; - } else if (value == "none") { - return SkMipmapMode::kNone; - } - throw std::runtime_error("The value \"" + value + - "\" is not a valid Mipmap Mode."); - } - SkMatrix _matrix; TileModeProp *_txProp; TileModeProp *_tyProp; - NodeProp *_filterModeProp; - NodeProp *_mipmapModeProp; ImageProps *_imageProps; TransformProp *_transformProp; PointProp *_originProp; + SamplingProp *_samplingProp; }; class JsiColorShaderNode : public JsiDomDeclarationNode, diff --git a/packages/skia/cpp/rnskia/dom/props/SamplingProp.h b/packages/skia/cpp/rnskia/dom/props/SamplingProp.h new file mode 100644 index 0000000000..4d5e5ed050 --- /dev/null +++ b/packages/skia/cpp/rnskia/dom/props/SamplingProp.h @@ -0,0 +1,54 @@ +#pragma once + +#include "include/core/SkSamplingOptions.h" + +namespace RNSkia { + +static PropId PropNameCubicB = JsiPropId::get("B"); +static PropId PropNameCubicC = JsiPropId::get("C"); +static PropId PropNameFilter = JsiPropId::get("filter"); +static PropId PropNameMipmap = JsiPropId::get("mipmap"); + +class SamplingProp : public DerivedProp { +public: + explicit SamplingProp(PropId name, + const std::function &onChange) + : DerivedProp(onChange) { + _prop = defineProperty(name); + } + + static SkSamplingOptions processSamplingOptions(const JsiValue &value) { + SkSamplingOptions samplingOptions(SkFilterMode::kLinear); + if (value.getType() == PropType::Object) { + if (value.hasValue(PropNameCubicB) && value.hasValue(PropNameCubicC)) { + auto B = + static_cast(value.getValue(PropNameCubicB).getAsNumber()); + auto C = + static_cast(value.getValue(PropNameCubicC).getAsNumber()); + samplingOptions = SkSamplingOptions({B, C}); + } else if (value.hasValue(PropNameFilter)) { + auto filter = static_cast( + value.getValue(PropNameFilter).getAsNumber()); + if (value.hasValue(PropNameMipmap)) { + auto mipmap = static_cast( + value.getValue(PropNameMipmap).getAsNumber()); + samplingOptions = SkSamplingOptions(filter, mipmap); + } else { + samplingOptions = SkSamplingOptions(filter); + } + } + } + + return samplingOptions; + } + + void updateDerivedValue() override { + if (_prop->isSet()) { + setDerivedValue(SamplingProp::processSamplingOptions(_prop->value())); + } + } + +private: + NodeProp *_prop; +}; +} // namespace RNSkia diff --git a/packages/skia/src/__tests__/snapshots/animated-images/bird.png b/packages/skia/src/__tests__/snapshots/animated-images/bird.png index 47b2f06994..b29f7ca30f 100644 Binary files a/packages/skia/src/__tests__/snapshots/animated-images/bird.png and b/packages/skia/src/__tests__/snapshots/animated-images/bird.png differ diff --git a/packages/skia/src/__tests__/snapshots/demos/product.png b/packages/skia/src/__tests__/snapshots/demos/product.png index ad507d62b0..528b8fc309 100644 Binary files a/packages/skia/src/__tests__/snapshots/demos/product.png and b/packages/skia/src/__tests__/snapshots/demos/product.png differ diff --git a/packages/skia/src/__tests__/snapshots/demos/product2.png b/packages/skia/src/__tests__/snapshots/demos/product2.png index 26345b9318..5f40bca177 100644 Binary files a/packages/skia/src/__tests__/snapshots/demos/product2.png and b/packages/skia/src/__tests__/snapshots/demos/product2.png differ diff --git a/packages/skia/src/__tests__/snapshots/images/bundle-android.png b/packages/skia/src/__tests__/snapshots/images/bundle-android.png index de558fd7c7..dd152a4e5c 100644 Binary files a/packages/skia/src/__tests__/snapshots/images/bundle-android.png and b/packages/skia/src/__tests__/snapshots/images/bundle-android.png differ diff --git a/packages/skia/src/__tests__/snapshots/images/bundle-ios.png b/packages/skia/src/__tests__/snapshots/images/bundle-ios.png index 2f5603cf78..6968b4c78e 100644 Binary files a/packages/skia/src/__tests__/snapshots/images/bundle-ios.png and b/packages/skia/src/__tests__/snapshots/images/bundle-ios.png differ diff --git a/packages/skia/src/__tests__/snapshots/images/bundle-node.png b/packages/skia/src/__tests__/snapshots/images/bundle-node.png index 649f04fa2a..34cd2a7e8b 100644 Binary files a/packages/skia/src/__tests__/snapshots/images/bundle-node.png and b/packages/skia/src/__tests__/snapshots/images/bundle-node.png differ diff --git a/packages/skia/src/__tests__/snapshots/images/filter.png b/packages/skia/src/__tests__/snapshots/images/filter.png index c1c81fb573..9022990275 100644 Binary files a/packages/skia/src/__tests__/snapshots/images/filter.png and b/packages/skia/src/__tests__/snapshots/images/filter.png differ diff --git a/packages/skia/src/dom/types/Drawings.ts b/packages/skia/src/dom/types/Drawings.ts index d53a88b0ed..8dcfb9fc63 100644 --- a/packages/skia/src/dom/types/Drawings.ts +++ b/packages/skia/src/dom/types/Drawings.ts @@ -17,6 +17,7 @@ import type { SkRect, SkRSXform, SkColor, + SamplingOptions, } from "../../skia/types"; import type { @@ -37,6 +38,7 @@ export type ImageProps = DrawingNodeProps & RectDef & { fit?: Fit; image: SkImage | null; + sampling?: SamplingOptions; }; export type CircleProps = CircleDef & DrawingNodeProps; @@ -65,6 +67,7 @@ export interface AtlasProps extends DrawingNodeProps { sprites: SkRect[]; transforms: SkRSXform[]; colors?: SkColor[]; + sampling?: SamplingOptions; } export interface CubicBezierHandle { diff --git a/packages/skia/src/dom/types/Shaders.ts b/packages/skia/src/dom/types/Shaders.ts index 68c4d4c5f4..821158ae0e 100644 --- a/packages/skia/src/dom/types/Shaders.ts +++ b/packages/skia/src/dom/types/Shaders.ts @@ -1,7 +1,6 @@ import type { Color, - FilterMode, - MipmapMode, + SamplingOptions, SkImage, SkRect, SkRuntimeEffect, @@ -26,11 +25,10 @@ export interface ShaderProps extends TransformProps, ChildrenProps { export interface ImageShaderProps extends TransformProps, Partial { tx: SkEnum; ty: SkEnum; - fm: SkEnum; - mm: SkEnum; fit: Fit; rect?: SkRect; image: SkImage | null; + sampling?: SamplingOptions; } export interface ColorProps { diff --git a/packages/skia/src/renderer/components/image/ImageShader.tsx b/packages/skia/src/renderer/components/image/ImageShader.tsx index 8a7d691644..09431c1492 100644 --- a/packages/skia/src/renderer/components/image/ImageShader.tsx +++ b/packages/skia/src/renderer/components/image/ImageShader.tsx @@ -6,24 +6,11 @@ import type { SkiaDefaultProps } from "../../processors"; export const ImageShader = ({ tx = "decal", ty = "decal", - fm = "nearest", - mm = "none", fit = "none", transform = [], ...props -}: SkiaDefaultProps< - ImageShaderProps, - "tx" | "ty" | "fm" | "mm" | "fit" | "transform" ->) => { +}: SkiaDefaultProps) => { return ( - + ); }; diff --git a/packages/skia/src/skia/types/Canvas.ts b/packages/skia/src/skia/types/Canvas.ts index b59b4a56f1..85420c786a 100644 --- a/packages/skia/src/skia/types/Canvas.ts +++ b/packages/skia/src/skia/types/Canvas.ts @@ -7,8 +7,7 @@ import type { MipmapMode, FilterMode, ImageInfo, - CubicResampler, - FilterOptions, + SamplingOptions, } from "./Image"; import type { SkSVG } from "./SVG"; import type { SkColor } from "./Color"; @@ -521,7 +520,7 @@ export interface SkCanvas { paint: SkPaint, blendMode?: BlendMode, colors?: SkColor[], - sampling?: CubicResampler | FilterOptions + sampling?: SamplingOptions ): void; /** Read Image pixels diff --git a/packages/skia/src/skia/types/Image/Image.ts b/packages/skia/src/skia/types/Image/Image.ts index 3af4b08568..59135e2963 100644 --- a/packages/skia/src/skia/types/Image/Image.ts +++ b/packages/skia/src/skia/types/Image/Image.ts @@ -32,6 +32,20 @@ export enum ImageFormat { WEBP = 6, } +export type SamplingOptions = CubicResampler | FilterOptions; + +export const isCubicSampling = ( + sampling: SamplingOptions +): sampling is CubicResampler => { + "worklet"; + return "B" in sampling && "C" in sampling; +}; + +export const MitchellCubicSampling = { B: 1 / 3.0, C: 1 / 3.0 }; +export const CatmullRomCubicSampling = { B: 0, C: 1 / 2.0 }; +export const CubicSampling = { B: 0, C: 0 }; +export const MakeCubic = (B: number, C: number) => ({ B, C }); + export interface SkImage extends SkJSIInstance<"Image"> { /** * Returns the possibly scaled height of the image. diff --git a/packages/skia/src/skia/web/JsiSkCanvas.ts b/packages/skia/src/skia/web/JsiSkCanvas.ts index 4ef41dc681..43b690691b 100644 --- a/packages/skia/src/skia/web/JsiSkCanvas.ts +++ b/packages/skia/src/skia/web/JsiSkCanvas.ts @@ -1,31 +1,37 @@ -import type { Canvas, CanvasKit } from "canvaskit-wasm"; - import type { - BlendMode, - ClipOp, - FilterMode, - MipmapMode, - PointMode, - SaveLayerFlag, - ImageInfo, - SkCanvas, - SkColor, - SkFont, - SkImage, - SkImageFilter, - SkMatrix, - SkPaint, - SkPath, - SkPicture, - SkPoint, - SkRect, - InputRRect, - SkSVG, - SkTextBlob, - SkVertices, - SkRSXform, - CubicResampler, - FilterOptions, + Canvas, + CanvasKit, + CubicResampler as CKCubicResampler, + FilterOptions as CKFilterOptions, +} from "canvaskit-wasm"; + +import { + type BlendMode, + type ClipOp, + type FilterMode, + type MipmapMode, + type PointMode, + type SaveLayerFlag, + type ImageInfo, + type SkCanvas, + type SkColor, + type SkFont, + type SkImage, + type SkImageFilter, + type SkMatrix, + type SkPaint, + type SkPath, + type SkPicture, + type SkPoint, + type SkRect, + type InputRRect, + type SkSVG, + type SkTextBlob, + type SkVertices, + type SkRSXform, + type CubicResampler, + type FilterOptions, + isCubicSampling, } from "../types"; import { getEnum, HostObject } from "./Host"; @@ -398,7 +404,7 @@ export class JsiSkCanvas paint: SkPaint, blendMode?: BlendMode, colors?: SkColor[], - _sampling?: CubicResampler | FilterOptions + sampling?: CubicResampler | FilterOptions ) { const src = srcs.flatMap((s) => Array.from(JsiSkRect.fromValue(this.CanvasKit, s)) @@ -412,6 +418,20 @@ export class JsiSkCanvas cls[i] = this.CanvasKit.ColorAsInt(r * 255, g * 255, b * 255, a * 255); } } + let ckSampling: CKCubicResampler | CKFilterOptions = { + filter: this.CanvasKit.FilterMode.Linear, + mipmap: this.CanvasKit.MipmapMode.None, + }; + if (sampling && isCubicSampling(sampling)) { + ckSampling = sampling; + } else if (sampling) { + ckSampling = { + filter: getEnum(this.CanvasKit.FilterMode, sampling.filter), + mipmap: sampling.mipmap + ? getEnum(this.CanvasKit.MipmapMode, sampling.mipmap) + : this.CanvasKit.MipmapMode.None, + }; + } this.ref.drawAtlas( JsiSkImage.fromValue(atlas), src, @@ -420,7 +440,8 @@ export class JsiSkCanvas blendMode ? getEnum(this.CanvasKit.BlendMode, blendMode) : this.CanvasKit.BlendMode.DstOver, - cls + cls, + ckSampling ); } diff --git a/packages/skia/src/sksg/Recorder/commands/Drawing.ts b/packages/skia/src/sksg/Recorder/commands/Drawing.ts index 94565bb988..c196a8b885 100644 --- a/packages/skia/src/sksg/Recorder/commands/Drawing.ts +++ b/packages/skia/src/sksg/Recorder/commands/Drawing.ts @@ -40,7 +40,10 @@ import { BlurStyle, ClipOp, FillType, + FilterMode, + isCubicSampling, isRRect, + MipmapMode, PointMode, VertexMode, } from "../../../skia/types"; @@ -117,7 +120,7 @@ export const drawBox = ( export const drawImage = (ctx: DrawingContext, props: ImageProps) => { "worklet"; - const { image } = props; + const { image, sampling } = props; if (image) { const fit = props.fit ?? "contain"; const rect = processRect(ctx.Skia, props); @@ -131,7 +134,25 @@ export const drawImage = (ctx: DrawingContext, props: ImageProps) => { }, rect ); - ctx.canvas.drawImageRect(image, src, dst, ctx.paint); + if (sampling && isCubicSampling(sampling)) { + ctx.canvas.drawImageRectCubic( + image, + src, + dst, + sampling.B, + sampling.C, + ctx.paint + ); + } else { + ctx.canvas.drawImageRectOptions( + image, + src, + dst, + sampling?.filter ?? FilterMode.Linear, + sampling?.mipmap ?? MipmapMode.None, + ctx.paint + ); + } } }; @@ -349,10 +370,18 @@ export const drawPicture = (ctx: DrawingContext, props: PictureProps) => { export const drawAtlas = (ctx: DrawingContext, props: AtlasProps) => { "worklet"; - const { image, sprites, transforms, colors, blendMode } = props; + const { image, sprites, transforms, colors, blendMode, sampling } = props; const blend = blendMode ? BlendMode[enumKey(blendMode)] : undefined; if (image) { - ctx.canvas.drawAtlas(image, sprites, transforms, ctx.paint, blend, colors); + ctx.canvas.drawAtlas( + image, + sprites, + transforms, + ctx.paint, + blend, + colors, + sampling + ); } }; diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index 1304d6f5ec..9ef6546339 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -19,9 +19,11 @@ import type { TurbulenceProps, TwoPointConicalGradientProps, } from "../../../dom/types"; +import type { SkShader } from "../../../skia/types"; import { BlendMode, FilterMode, + isCubicSampling, MipmapMode, processUniforms, TileMode, @@ -179,7 +181,7 @@ const declareTurbulenceShader = ( const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { "worklet"; - const { fit, image, tx, ty, fm, mm, ...imageShaderProps } = props; + const { fit, image, tx, ty, sampling, ...imageShaderProps } = props; if (!image) { return; } @@ -199,13 +201,24 @@ const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { const lm = ctx.Skia.Matrix(); lm.concat(m3); processTransformProps(lm, imageShaderProps); - const shader = image.makeShaderOptions( - TileMode[enumKey(tx)], - TileMode[enumKey(ty)], - FilterMode[enumKey(fm)], - MipmapMode[enumKey(mm)], - lm - ); + let shader: SkShader; + if (sampling && isCubicSampling(sampling)) { + shader = image.makeShaderCubic( + TileMode[enumKey(tx)], + TileMode[enumKey(ty)], + sampling.B, + sampling.C, + lm + ); + } else { + shader = image.makeShaderCubic( + TileMode[enumKey(tx)], + TileMode[enumKey(ty)], + sampling?.filter ?? FilterMode.Linear, + sampling?.mipmap ?? MipmapMode.None, + lm + ); + } ctx.shaders.push(shader); };