Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/docs/docs/canvas/canvas.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Behind the scenes, it is using its own React renderer.
| style? | `ViewStyle` | View style |
| ref? | `Ref<SkiaView>` | Reference to the `SkiaView` object |
| onSize? | `SharedValue<Size>` | Reanimated value to which the canvas size will be assigned (see [canvas size](#canvas-size)) |
| highBitDepth? | `boolean` | Render into a surface with more than 8 bits per channel (see [high bit depth](#high-bit-depth)) |
| androidWarmup? | `boolean` | Draw the first frame directly on the Android compositor. Use it for static icons or fully opaque drawings—animated or translucent canvases can misrender, so it remains opt-in. |

## Canvas size
Expand Down Expand Up @@ -85,6 +86,36 @@ const Demo = () => {
```


## High bit depth

By default the canvas renders into an 8-bit surface. With only 256 levels per channel, subtle gradients quantize into visible bands, especially in dark tones and on OLED displays.
With `highBitDepth`, the canvas renders into a 16-bit float surface on iOS and a 10-bit surface on Android.
Colors are identical to the default surface, only with more precision: this is about bit depth, not HDR.

```tsx twoslash
import {Canvas, Fill, LinearGradient, vec} from "@shopify/react-native-skia";

const Demo = () => {
return (
<Canvas style={{ flex: 1 }} opaque highBitDepth>
<Fill>
<LinearGradient
start={vec(0, 0)}
end={vec(0, 512)}
colors={["rgb(51, 56, 77)", "rgb(59, 64, 85)"]}
/>
</Fill>
</Canvas>
);
};
```

:::warning

On Android, `highBitDepth` requires the Graphite backend; with the default OpenGL backend the canvas falls back to 8-bit.

:::

## Getting a Canvas Snapshot

You can save your drawings as an image by using the `makeImageSnapshotAsync` method. This method returns a promise that resolves to an [Image](/docs/images).
Expand Down
3 changes: 3 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
LiquidGlass,
Pictures,
WebGPU,
HighBitDepth,
} from "./Examples";
import { CI, Tests } from "./Tests";
import { HomeScreen } from "./Home";
Expand Down Expand Up @@ -75,6 +76,7 @@ const linking: LinkingOptions<StackParamList> = {
Chat: "chat",
Pictures: "pictures",
WebGPU: "webgpu",
HighBitDepth: "high-bit-depth",
},
},
prefixes: ["rnskia://"],
Expand Down Expand Up @@ -244,6 +246,7 @@ const App = () => {
header: () => null,
}}
/>
<Stack.Screen name="HighBitDepth" component={HighBitDepth} />
</Stack.Navigator>
</NavigationContainer>
</GestureHandlerRootView>
Expand Down
90 changes: 90 additions & 0 deletions apps/example/src/Examples/HighBitDepth/HighBitDepth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import {
Canvas,
Fill,
LinearGradient,
vec,
useClock,
} from "@shopify/react-native-skia";
import type { SharedValue } from "react-native-reanimated";
import { useDerivedValue, useSharedValue } from "react-native-reanimated";

// A dark-blue vertical gradient (the classic "night sky" worst case for
// banding) that slowly drifts up and down. The endpoints are integer 8-bit
// values with the same delta (+8/255) on every channel, so all three channels
// quantize at the same height: each band edge is a simultaneous r+g+b step,
// the strongest possible contour. On the default 8-bit canvas ~8 fat bands
// crawl across the screen; with highBitDepth the gradient stays smooth. Both
// canvases must show the same colors.
const colorA = "rgb(51, 56, 77)";
const colorB = "rgb(59, 64, 85)";

interface GradientCanvasProps {
highBitDepth: boolean;
label: string;
clock: SharedValue<number>;
}

const GradientCanvas = ({
highBitDepth,
label,
clock,
}: GradientCanvasProps) => {
const size = useSharedValue({ width: 0, height: 0 });
const start = useDerivedValue(() => {
const phase = 0.15 * Math.sin(clock.value / 2000);
return vec(0, -phase * size.value.height);
});
const end = useDerivedValue(() => {
const phase = 0.15 * Math.sin(clock.value / 2000);
return vec(0, (1 - phase) * size.value.height);
});
return (
<View style={styles.column}>
<Canvas
style={styles.canvas}
opaque
highBitDepth={highBitDepth}
onSize={size}
>
<Fill>
<LinearGradient start={start} end={end} colors={[colorA, colorB]} />
</Fill>
</Canvas>
<Text style={styles.label}>{label}</Text>
</View>
);
};

export const HighBitDepth = () => {
const clock = useClock();
return (
<View style={styles.container}>
<GradientCanvas highBitDepth={false} label="8-bit" clock={clock} />
<GradientCanvas
highBitDepth={true}
label="high bit depth"
clock={clock}
/>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "row",
},
column: {
flex: 1,
},
canvas: {
flex: 1,
},
label: {
textAlign: "center",
padding: 8,
fontWeight: "bold",
},
});
1 change: 1 addition & 0 deletions apps/example/src/Examples/HighBitDepth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./HighBitDepth";
1 change: 1 addition & 0 deletions apps/example/src/Examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from "./Video";
export * from "./Chat";
export * from "./Pictures";
export * from "./WebGPU";
export * from "./HighBitDepth";
5 changes: 5 additions & 0 deletions apps/example/src/Home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ export const HomeScreen = () => {
route="WebGPU"
/>
)}
<HomeScreenButton
title="🎨 High Bit Depth"
description="8-bit vs high bit depth canvas"
route="HighBitDepth"
/>
</ScrollView>
);
};
1 change: 1 addition & 0 deletions apps/example/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export type StackParamList = {
Chat: undefined;
Pictures: undefined;
WebGPU: undefined;
HighBitDepth: undefined;
};
10 changes: 6 additions & 4 deletions packages/skia/android/cpp/jni/include/JniSkiaBaseView.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ class JniSkiaBaseView {

protected:
virtual void surfaceAvailable(jobject surface, int width, int height,
bool opaque) {
_skiaAndroidView->surfaceAvailable(surface, width, height, opaque);
bool opaque, bool highBitDepth) {
_skiaAndroidView->surfaceAvailable(surface, width, height, opaque,
highBitDepth);
}

virtual void surfaceSizeChanged(jobject surface, int width, int height,
bool opaque) {
_skiaAndroidView->surfaceSizeChanged(surface, width, height, opaque);
bool opaque, bool highBitDepth) {
_skiaAndroidView->surfaceSizeChanged(surface, width, height, opaque,
highBitDepth);
}

virtual void surfaceDestroyed() { _skiaAndroidView->surfaceDestroyed(); }
Expand Down
14 changes: 8 additions & 6 deletions packages/skia/android/cpp/jni/include/JniSkiaPictureView.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ class JniSkiaPictureView : public jni::HybridClass<JniSkiaPictureView>,
}

protected:
void surfaceAvailable(jobject surface, int width, int height,
bool opaque) override {
JniSkiaBaseView::surfaceAvailable(surface, width, height, opaque);
void surfaceAvailable(jobject surface, int width, int height, bool opaque,
bool highBitDepth) override {
JniSkiaBaseView::surfaceAvailable(surface, width, height, opaque,
highBitDepth);
}

void surfaceSizeChanged(jobject surface, int width, int height,
bool opaque) override {
JniSkiaBaseView::surfaceSizeChanged(surface, width, height, opaque);
void surfaceSizeChanged(jobject surface, int width, int height, bool opaque,
bool highBitDepth) override {
JniSkiaBaseView::surfaceSizeChanged(surface, width, height, opaque,
highBitDepth);
}

void surfaceDestroyed() override { JniSkiaBaseView::surfaceDestroyed(); }
Expand Down
11 changes: 10 additions & 1 deletion packages/skia/android/cpp/rnskia-android/OpenGLContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,17 @@ class OpenGLContext {
}

// TODO: remove width, height
std::unique_ptr<WindowContext> MakeWindow(ANativeWindow *window) {
std::unique_ptr<WindowContext> MakeWindow(ANativeWindow *window,
bool highBitDepth = false) {
auto display = OpenGLSharedContext::getInstance().getDisplay();
if (highBitDepth) {
// A 10-bit window surface would require the shared EGL context to be
// created without a config (EGL_KHR_no_config_context) for every app,
// which is too risky for existing 8-bit clients on brittle drivers.
RNSkLogger::logToConsole(
"highBitDepth is not supported on the OpenGL backend, falling back "
"to the 8-bit format (the Graphite backend supports it)");
}
return std::make_unique<OpenGLWindowContext>(
_directContext.get(), display, _glContext.get(), window,
OpenGLSharedContext::getInstance().getConfig());
Expand Down
16 changes: 8 additions & 8 deletions packages/skia/android/cpp/rnskia-android/RNSkAndroidView.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ namespace RNSkia {
class RNSkBaseAndroidView {
public:
virtual void surfaceAvailable(jobject surface, int width, int height,
bool opaque) = 0;
bool opaque, bool highBitDepth) = 0;

virtual void surfaceDestroyed() = 0;

virtual void surfaceSizeChanged(jobject surface, int width, int height,
bool opaque) = 0;
bool opaque, bool highBitDepth) = 0;

virtual float getPixelDensity() = 0;

Expand All @@ -34,10 +34,10 @@ class RNSkAndroidView : public T, public RNSkBaseAndroidView {
std::make_shared<RNSkOpenGLCanvasProvider>(
std::bind(&RNSkia::RNSkView::requestRedraw, this), context)) {}

void surfaceAvailable(jobject surface, int width, int height,
bool opaque) override {
void surfaceAvailable(jobject surface, int width, int height, bool opaque,
bool highBitDepth) override {
std::static_pointer_cast<RNSkOpenGLCanvasProvider>(T::getCanvasProvider())
->surfaceAvailable(surface, width, height, opaque);
->surfaceAvailable(surface, width, height, opaque, highBitDepth);
RNSkView::redraw();
}

Expand All @@ -46,10 +46,10 @@ class RNSkAndroidView : public T, public RNSkBaseAndroidView {
->surfaceDestroyed();
}

void surfaceSizeChanged(jobject surface, int width, int height,
bool opaque) override {
void surfaceSizeChanged(jobject surface, int width, int height, bool opaque,
bool highBitDepth) override {
std::static_pointer_cast<RNSkOpenGLCanvasProvider>(T::getCanvasProvider())
->surfaceSizeChanged(surface, width, height, opaque);
->surfaceSizeChanged(surface, width, height, opaque, highBitDepth);
// This is only need for the first time to frame, this renderImmediate call
// will invoke updateTexImage for the previous frame
RNSkView::redraw();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ bool RNSkOpenGLCanvasProvider::renderToCanvas(

void RNSkOpenGLCanvasProvider::surfaceAvailable(jobject jSurfaceTexture,
int width, int height,
bool opaque) {
bool opaque,
bool highBitDepth) {
// Release the old surface
_surfaceHolder = nullptr;

Expand Down Expand Up @@ -108,9 +109,11 @@ void RNSkOpenGLCanvasProvider::surfaceAvailable(jobject jSurfaceTexture,
window = ANativeWindow_fromSurface(env, jSurfaceTexture);
}
#if defined(SK_GRAPHITE)
_surfaceHolder = DawnContext::getInstance().MakeWindow(window, width, height);
_surfaceHolder = DawnContext::getInstance().MakeWindow(window, width, height,
highBitDepth);
#else
_surfaceHolder = OpenGLContext::getInstance().MakeWindow(window);
_surfaceHolder =
OpenGLContext::getInstance().MakeWindow(window, highBitDepth);
#endif

// Post redraw request to ensure we paint in the next draw cycle.
Expand All @@ -128,7 +131,8 @@ void RNSkOpenGLCanvasProvider::surfaceDestroyed() {
}

void RNSkOpenGLCanvasProvider::surfaceSizeChanged(jobject jSurface, int width,
int height, bool opaque) {
int height, bool opaque,
bool highBitDepth) {
if (width == 0 && height == 0) {
// Setting width/height to zero is nothing we need to care about when
// it comes to invalidating the surface.
Expand All @@ -137,7 +141,7 @@ void RNSkOpenGLCanvasProvider::surfaceSizeChanged(jobject jSurface, int width,

if (_surfaceHolder == nullptr) {
_surfaceHolder = nullptr;
surfaceAvailable(jSurface, width, height, opaque);
surfaceAvailable(jSurface, width, height, opaque, highBitDepth);
} else {
_surfaceHolder->resize(width, height);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ class RNSkOpenGLCanvasProvider

bool renderToCanvas(const std::function<void(SkCanvas *)> &cb) override;

void surfaceAvailable(jobject surface, int width, int height, bool opaque);
void surfaceAvailable(jobject surface, int width, int height, bool opaque,
bool highBitDepth);

void surfaceDestroyed();

void surfaceSizeChanged(jobject jSurface, int width, int height, bool opaque);
void surfaceSizeChanged(jobject jSurface, int width, int height, bool opaque,
bool highBitDepth);

private:
std::unique_ptr<WindowContext> _surfaceHolder = nullptr;
Expand Down
Loading
Loading