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
Binary file added examples/public/tiger.riv
Binary file not shown.
17 changes: 17 additions & 0 deletions examples/src/components/Inkbleed.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';

import Inkbleed from './Inkbleed';

const meta = {
title: 'Inkbleed',
component: Inkbleed,
parameters: {
layout: 'fullscreen',
},
args: {},
} satisfies Meta<typeof Inkbleed>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};
142 changes: 142 additions & 0 deletions examples/src/components/Inkbleed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { useRive, Fit, Alignment, Layout } from '@rive-app/react-webgl2';
import type { UseRiveParameters, UseRiveOptions } from '@rive-app/react-webgl2';

/**
* Inkbleed — experimental 1x analytic antialiasing (webgl2 only)
*/

const CARD_SIZE = 380;
const LAYOUT = new Layout({ fit: Fit.Contain, alignment: Alignment.Center });

interface CardProps {
title: string;
expectInkbleed: boolean;
params: UseRiveParameters;
opts?: Partial<UseRiveOptions>;
}

const Card = ({ title, expectInkbleed, params, opts }: CardProps) => {
const { rive, RiveComponent } = useRive(params, opts);
// What this card currently asks for; the Toggle button flips it.
const [expected, setExpected] = useState(expectInkbleed);

let label = 'loading…';
let color: string = COLORS.textMuted;

if (rive) {
const on = rive.inkbleed;
const ok = on === expected;
label = `inkbleed ${on ? 'on' : 'off'} ${
ok ? '✓' : `✗ expected ${expected ? 'on' : 'off'}`
}`;
color = ok ? COLORS.pass : COLORS.fail;
}

return (
<div style={styles.card}>
<h3 style={styles.cardTitle}>{title}</h3>
<div style={{ width: CARD_SIZE, height: CARD_SIZE }}>
<RiveComponent
style={{ width: '100%', height: '100%', display: 'block' }}
/>
</div>
<div style={styles.footer}>
<span style={{ ...styles.status, color }}>{label}</span>
{params?.enableInkbleed && (
<button
style={styles.button}
disabled={!rive}
onClick={() => {
if (!rive) return;
const next = !expected;
rive.inkbleed = next;
setExpected(next);
}}
>
Toggle
</button>
)}
</div>
</div>
);
};

const params = (extra?: Partial<UseRiveParameters>): UseRiveParameters => ({
src: 'tiger.riv',
autoplay: true,
layout: LAYOUT,
...extra,
});

const Inkbleed = () => (
<div style={styles.page}>
<div style={styles.inner}>
<h1 style={styles.h1}>Inkbleed</h1>
<p style={styles.intro}>
Inkbleed replaces MSAA with 1x analytic antialiasing. Everything bleeds
out half a pixel, which is most visible on thin strokes like the
whiskers. Fills that don't use the clockwise fill rule are converted to
clockwise on a best-effort basis.
</p>

<section style={styles.section}>
<div style={styles.grid}>
<Card
title="enableInkbleed"
expectInkbleed
params={params({ enableInkbleed: true })}
/>
</div>
</section>
</div>
</div>
);

const COLORS = {
bg: '#1a1a1a',
panel: '#252525',
border: '#3a3a3a',
text: '#e0e0e0',
textMuted: '#999',
pass: '#4caf50',
fail: '#ff4d40',
};

const styles: Record<string, React.CSSProperties> = {
page: {
background: COLORS.bg,
color: COLORS.text,
fontFamily: 'system-ui, sans-serif',
minHeight: '100vh',
boxSizing: 'border-box',
padding: 24,
},
inner: { width: 'min(92%, 1240px)', margin: '0 auto' },
h1: { margin: '0 0 6px', fontSize: 22, fontWeight: 600 },
intro: {
color: COLORS.textMuted,
fontSize: 14,
lineHeight: 1.6,
margin: '0 0 20px',
},
section: { marginBottom: 28 },
grid: { display: 'flex', flexWrap: 'wrap', gap: 16 },
card: {
background: COLORS.panel,
border: `1px solid ${COLORS.border}`,
borderRadius: 8,
padding: 12,
},
cardTitle: { fontSize: 13, fontWeight: 600, margin: '0 0 8px' },
footer: {
marginTop: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
status: { fontSize: 13, fontWeight: 600 },
button: { fontSize: 12, padding: '2px 10px' },
};

export default Inkbleed;
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
},
"homepage": "https://github.com/rive-app/rive-react#readme",
"dependencies": {
"@rive-app/canvas": "2.44.0",
"@rive-app/canvas-lite": "2.44.0",
"@rive-app/webgl2": "2.44.0"
"@rive-app/canvas": "2.45.0",
"@rive-app/canvas-lite": "2.45.0",
"@rive-app/webgl2": "2.45.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0"
Expand Down
19 changes: 18 additions & 1 deletion src/components/Rive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ export interface RiveProps {
* - `true` normally, so multiple Rive graphics on a page share one context.
* - `false` when `enableGPUCanvas` is on, because GPU Canvas needs a context
* of its own for each `<canvas>`.
* - `false` when `enableInkbleed` is on, because inkbleed needs a context of
* its own for each `<canvas>`.
*
* Setting it explicitly always wins. Setting it to `true` alongside
* `enableGPUCanvas` means GPU Canvas content will not draw.
* `enableGPUCanvas` means GPU Canvas content will not draw, and alongside
* `enableInkbleed` means inkbleed stays off.
*/
useOffscreenRenderer?: boolean;
/**
Expand All @@ -66,6 +69,18 @@ export interface RiveProps {
* have (the limit varies by browser), so enabling it on too many at once can throw.
*/
enableGPUCanvas?: boolean;
/**
* @experimental (WebGL2 only) Renders with inkbleed antialiasing instead
* of MSAA. Fills that don't use the clockwise fill rule are converted to clockwise on a
* best-effort basis. False by default.
*
* IMPORTANT: Turning this on makes `useOffscreenRenderer` default to `false` for this
* component if it was not specified, since inkbleed needs a WebGL context of its own.
*
* Each of these takes its own WebGL context, and browsers cap how many a page may
* have (the limit varies by browser), so enabling it on too many at once can throw.
*/
enableInkbleed?: boolean;
/**
* Specify whether to disable Rive listeners on the canvas, thus preventing any event listeners to be attached to the canvas element
*/
Expand Down Expand Up @@ -104,6 +119,7 @@ const Rive = ({
// undefined, it falls through to defaultOptions — which will still be `true`.
useOffscreenRenderer,
enableGPUCanvas,
enableInkbleed,
shouldDisableRiveListeners = false,
shouldResizeCanvasToContainer = true,
automaticallyHandleEvents = false,
Expand All @@ -121,6 +137,7 @@ const Rive = ({
shouldDisableRiveListeners,
automaticallyHandleEvents,
enableGPUCanvas,
enableInkbleed,
};

const options = {
Expand Down
21 changes: 20 additions & 1 deletion src/hooks/useRive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export default function useRive(
* is React runtime's (see `defaultOptions`). So opting into GPU Canvas only has
* to unwind React's default — an explicit value from the caller still wins, and
* still loses GPU Canvas, which the effect below warns about.
*
* Inkbleed is the same: it needs the canvas's own context, created with
* depth/stencil and without MSAA, which the shared offscreen context isn't.
*/
const explicitOffscreenRenderer =
riveParams?.useOffscreenRenderer ?? opts.useOffscreenRenderer;
Expand All @@ -99,9 +102,13 @@ export default function useRive(
Boolean(riveParams?.enableGPUCanvas) ||
Boolean(riveParams?.riveFile?.deferredRequested);

const wantsInkbleed = Boolean(riveParams?.enableInkbleed);

const useOffscreenRenderer =
explicitOffscreenRenderer ??
(wantsGPUCanvas ? false : defaultOptions.useOffscreenRenderer);
(wantsGPUCanvas || wantsInkbleed
? false
: defaultOptions.useOffscreenRenderer);

// The runtime warns about this too, but from where it sits it cannot say that
// a hook option is what turned the offscreen renderer on.
Expand All @@ -117,6 +124,18 @@ export default function useRive(
}
}, [wantsGPUCanvas, useOffscreenRenderer]);

useEffect(() => {
if (wantsInkbleed && useOffscreenRenderer) {
console.warn(
'[Rive] `enableInkbleed` and `useOffscreenRenderer` cannot both be on. ' +
'Inkbleed needs a WebGL context of its own, while the offscreen renderer ' +
'shares one context across every <canvas> on the page. This instance ' +
'renders without inkbleed — drop the explicit `useOffscreenRenderer: true` ' +
'to use it.'
);
}
}, [wantsInkbleed, useOffscreenRenderer]);

const devicePixelRatio = useDevicePixelRatio();

/**
Expand Down
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ export type UseRiveOptions = {
* - `true` normally, so multiple Rive graphics on a page share one context.
* - `false` when `enableGPUCanvas` is on, because GPU Canvas needs a context
* of its own for each `<canvas>`.
* - `false` when `enableInkbleed` is on, because inkbleed needs a context of
* its own for each `<canvas>`.
*
* Setting it explicitly always wins. Setting it to `true` alongside
* `enableGPUCanvas` means GPU Canvas content will not draw.
* `enableGPUCanvas` means GPU Canvas content will not draw, and alongside
* `enableInkbleed` means inkbleed stays off.
*/
useOffscreenRenderer: boolean;
shouldResizeCanvasToContainer: boolean;
Expand Down
Loading
Loading