Skip to content

Commit

Permalink
feat(🏞️): fix default image sampling and add full control of sampling…
Browse files Browse the repository at this point in the history
… options (#2880)

fixes #2507

This adds full control to the sampling options to the user but it also fix the default sampling options as well as making it consistent on all APIs (image, atlas, shader in declarative/imperative).
  • Loading branch information
wcandillon authored Jan 10, 2025
1 parent d54e29b commit 8b14242
Show file tree
Hide file tree
Showing 28 changed files with 279 additions and 112 deletions.
38 changes: 38 additions & 0 deletions apps/docs/docs/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 (
<Canvas style={{ flex: 1 }}>
<Image
image={image}
fit="contain"
x={0}
y={0}
width={256}
height={256}
sampling={CubicSampling}
/>
<Image
image={image}
fit="contain"
x={0}
y={0}
width={256}
height={256}
sampling={{ filter: FilterMode.Nearest, mipmap: MipmapMode.Nearest }}
/>
</Canvas>
);
};
```

### fit="contain"

![fit="contain"](assets/images/contain.png)
Expand Down
3 changes: 1 addition & 2 deletions apps/docs/docs/shaders/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/docs/docs/shapes/atlas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file modified apps/docs/static/img/atlas/hello-world.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/docs/static/img/mask/blend-mode-mask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion apps/paper/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1984,6 +1989,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 939f21fabf5d45a725c0bf175eb819dd25cf2e70
RNReanimated: 9d20a811e6987cba268ef4f56242dfabd40e85c1
RNScreens: b03d696c70cc5235ce4587fcc27ae1a93a48f98c
RNSVG: 3d2bdcaef51c8071880a9c0072fe324f4423a3ba
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae

Expand Down
2 changes: 1 addition & 1 deletion apps/remotion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions packages/skia/cpp/api/JsiSkCanvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
24 changes: 24 additions & 0 deletions packages/skia/cpp/api/JsiSkImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float>(object.getProperty(runtime, "B").asNumber());
auto C = static_cast<float>(object.getProperty(runtime, "C").asNumber());
samplingOptions = SkSamplingOptions({B, C});
} else if (object.hasProperty(runtime, "filter")) {
auto filter = static_cast<SkFilterMode>(
object.getProperty(runtime, "filter").asNumber());
if (object.hasProperty(runtime, "mipmap")) {
auto mipmap = static_cast<SkMipmapMode>(
object.getProperty(runtime, "mipmap").asNumber());
samplingOptions = SkSamplingOptions(filter, mipmap);
} else {
samplingOptions = SkSamplingOptions(filter);
}
}
}
return samplingOptions;
}

class JsiSkImage : public JsiSkWrappingSkPtrHostObject<SkImage> {
public:
// TODO-API: Properties?
Expand Down
6 changes: 5 additions & 1 deletion packages/skia/cpp/rnskia/dom/nodes/JsiAtlasNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "JsiDomDrawingNode.h"
#include "RSXformProp.h"
#include "RectProp.h"
#include "SamplingProp.h"
#include "SkImageProps.h"

#include <memory>
Expand All @@ -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(),
Expand All @@ -43,6 +45,7 @@ class JsiAtlasNode : public JsiDomDrawingNode,
_imageProp = container->defineProperty<ImageProp>("image");
_colorsProp = container->defineProperty<ColorsProp>("colors");
_blendModeProp = container->defineProperty<BlendModeProp>("blendMode");
_samplingProp = container->defineProperty<SamplingProp>("sampling");

_rectsProp->require();
_rsxFormsProp->require();
Expand All @@ -54,6 +57,7 @@ class JsiAtlasNode : public JsiDomDrawingNode,
RSXFormsProp *_rsxFormsProp;
ColorsProp *_colorsProp;
BlendModeProp *_blendModeProp;
SamplingProp *_samplingProp;
};

} // namespace RNSkia
12 changes: 8 additions & 4 deletions packages/skia/cpp/rnskia/dom/nodes/JsiImageNode.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "JsiDomDrawingNode.h"
#include "SamplingProp.h"
#include "SkImageProps.h"

#include <memory>
Expand All @@ -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<ImageProps>();
_samplingProp = container->defineProperty<SamplingProp>("sampling");
}

private:
ImageProps *_imageProps;
SamplingProp *_samplingProp;
};

} // namespace RNSkia
47 changes: 9 additions & 38 deletions packages/skia/cpp/rnskia/dom/nodes/JsiShaderNodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -120,30 +121,27 @@ 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:
void defineProperties(NodePropsContainer *container) override {
JsiDomDeclarationNode::defineProperties(container);
_txProp = container->defineProperty<TileModeProp>("tx");
_tyProp = container->defineProperty<TileModeProp>("ty");
_filterModeProp = container->defineProperty<NodeProp>("fm");
_mipmapModeProp = container->defineProperty<NodeProp>("mm");

_samplingProp = container->defineProperty<SamplingProp>("sampling");
_imageProps = container->defineProperty<ImageProps>();
_transformProp = container->defineProperty<TransformProp>("transform");
_originProp = container->defineProperty<PointProp>("origin");

_txProp->require();
_tyProp->require();
_filterModeProp->require();
_mipmapModeProp->require();

_transformProp->require();

Expand All @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions packages/skia/cpp/rnskia/dom/props/SamplingProp.h
Original file line number Diff line number Diff line change
@@ -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<SkSamplingOptions> {
public:
explicit SamplingProp(PropId name,
const std::function<void(BaseNodeProp *)> &onChange)
: DerivedProp(onChange) {
_prop = defineProperty<NodeProp>(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<float>(value.getValue(PropNameCubicB).getAsNumber());
auto C =
static_cast<float>(value.getValue(PropNameCubicC).getAsNumber());
samplingOptions = SkSamplingOptions({B, C});
} else if (value.hasValue(PropNameFilter)) {
auto filter = static_cast<SkFilterMode>(
value.getValue(PropNameFilter).getAsNumber());
if (value.hasValue(PropNameMipmap)) {
auto mipmap = static_cast<SkMipmapMode>(
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
Binary file modified packages/skia/src/__tests__/snapshots/animated-images/bird.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/demos/product.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/demos/product2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/images/bundle-android.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/images/bundle-ios.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/images/bundle-node.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/images/filter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8b14242

Please sign in to comment.