Docs
Shaders
SkSL runtime effects on cached nodes. Include the common prelude or wrap the body with withCommonShader. Not GLSL.
Shaders are SkSL runtime effects. They are not GLSL. Do not write #version 300 es, void main(), sampler2D, texture(), or outColor.
The entry point is half4 main(float2 coord).
Cached node
Put cache on the node that owns the shader. Children render into a texture. The fragment samples that texture. Use cachePadding if the effect bleeds past the box.
<Rect
cache
cachePadding={48}
width={() => this.frameWidth()}
height={() => this.frameHeight()}
shaders={{
fragment: GLASS_SKSL,
uniforms: {
uRefraction: () => this.refraction(),
},
}}
>
<Img
src={() => this.image()}
width={() => this.frameWidth()}
height={() => this.frameHeight()}
/>
</Rect>The image is a child of the cached rect. It is an asset input, not a second timeline clip. See Inputs.
Prelude
Every useful fragment needs sourceTexture, sampleSource, and computeSourceUV. Two equivalent ways to get them:
Include at the top of the inline fragment:
#include "@vidova/core/shaders/common.sksl"
half4 main(float2 coord) {
float2 uv = computeSourceUV(coord);
return sampleSource(uv);
}Or wrap the body:
import { withCommonShader } from '@vidova/core';
const PASS_THROUGH = withCommonShader(`
half4 main(float2 coord) {
float2 uv = computeSourceUV(coord);
return sampleSource(uv);
}
`);Custom components are a single .tsx file. Vidova expands that include before the runtime effect compiles. You do not import a separate .sksl file.
Helpers and uniforms
From the prelude:
| Name | Role |
|---|---|
sampleSource(uv) | Sample the cached node. uv is 0–1 |
computeSourceUV(coord) | Convert fragment coord to source UV |
sourceTexture | The cached children |
sourceSize, sourceTexelSize | Cache size in pixels |
resolution | Node size |
time, deltaTime, framerate, frame | Playback clock |
System uniforms are injected. You still declare them by including the prelude (or withCommonShader). Do not paste a fake GLSL preamble.
Destination sampling (what's already on screen) only happens if the fragment also declares uniform shader destinationTexture. Most image effects sample source only.
Custom uniforms
Keys on shaders.uniforms become SkSL uniform names. Signals are allowed.
| TypeScript | SkSL |
|---|---|
number | float |
[number, number] | float2 |
[number, number, number] | float3 |
[number, number, number, number] | float4 |
shaders={{
fragment: GLASS_SKSL,
uniforms: {
uRefraction: () => this.refraction(),
uFrost: () => this.frost(),
},
}}uniform float uRefraction;
uniform float uFrost;Pass-through
import { withCommonShader } from '@vidova/core';
const PASS_THROUGH = withCommonShader(`
half4 main(float2 coord) {
return sampleSource(computeSourceUV(coord));
}
`);Glass on an image input
Declare an image asset input. Keep @initial('') on the signal. Wrap <Img src={this.image} /> in a cached Rect and run SkSL on that cache.
import { Node, NodeProps, Img, Rect, signal, initial } from '@vidova/2d';
import { SignalValue, SimpleSignal, withCommonShader } from '@vidova/core';
const GLASS_SKSL = withCommonShader(`
uniform float uRefraction;
half4 main(float2 coord) {
float2 uv = computeSourceUV(coord);
float2 warped = uv + (uv - 0.5) * uRefraction;
return sampleSource(warped);
}
`);
export interface GlassImageProps extends NodeProps {
image?: SignalValue<string>;
refraction?: SignalValue<number>;
frameWidth?: SignalValue<number>;
frameHeight?: SignalValue<number>;
}
export class GlassImage extends Node {
@initial('')
@signal()
public declare readonly image: SimpleSignal<string, this>;
@initial(0.04)
@signal()
public declare readonly refraction: SimpleSignal<number, this>;
@initial(1600)
@signal()
public declare readonly frameWidth: SimpleSignal<number, this>;
@initial(900)
@signal()
public declare readonly frameHeight: SimpleSignal<number, this>;
public constructor(props?: GlassImageProps) {
super({ ...props });
this.add(
<Rect
cache
cachePadding={32}
width={() => this.frameWidth()}
height={() => this.frameHeight()}
shaders={{
fragment: GLASS_SKSL,
uniforms: { uRefraction: () => this.refraction() },
}}
>
<Img
src={() => this.image()}
width={() => this.frameWidth()}
height={() => this.frameHeight()}
/>
</Rect>,
);
}
}inputDefs for that class:
[
{ "name": "image", "type": "asset", "default": "", "assetTypes": ["image"] },
{ "name": "refraction", "type": "number", "default": 0.04 }
]Place only the component clip. Pass the image asset ID in componentInputs.image. Do not add a separate image clip underneath.
Failures
| What you wrote | What happens |
|---|---|
#version 300 es / void main() / sampler2D / outColor | Compile error: shaders are SkSL |
#include "local.sksl" | Compile error: only the common prelude expands |
Shader on a node without cache | Effect is skipped or samples the wrong buffer |
Asset UUID in @initial or <Img src> | Img.src received asset ID — use @initial('') |
| Extra image clip on the timeline | The component is unused; the still is just a media clip |
A create or edit that reports FAILED TO RENDER is broken. Fix the SkSL before you place the clip. Thumbnail render compiles shaders.
Layout of stacked chrome around the image: Layout. Wiring inputDefs: Inputs.
