# 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.

```tsx
<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](/docs/custom-elements/inputs).

## Prelude

Every useful fragment needs `sourceTexture`, `sampleSource`, and `computeSourceUV`. Two equivalent ways to get them:

**Include** at the top of the inline fragment:

```sksl
#include "@vidova/core/shaders/common.sksl"

half4 main(float2 coord) {
    float2 uv = computeSourceUV(coord);
    return sampleSource(uv);
}
```

**Or** wrap the body:

```ts
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` |

```tsx
shaders={{
  fragment: GLASS_SKSL,
  uniforms: {
    uRefraction: () => this.refraction(),
    uFrost: () => this.frost(),
  },
}}
```

```sksl
uniform float uRefraction;
uniform float uFrost;
```

## Pass-through

```ts
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.

```tsx
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:

```json
[
  { "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](/docs/custom-elements/layout). Wiring `inputDefs`: [Inputs](/docs/custom-elements/inputs).
