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
1 change: 1 addition & 0 deletions examples/common/installShaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export async function installShaders(stage: Stage, renderMode: string) {
stage.shManager.registerShaderType('RadialGradient', shaders.RadialGradient);
stage.shManager.registerShaderType('LinearGradient', shaders.LinearGradient);
stage.shManager.registerShaderType('RadialProgress', shaders.RadialProgress);
stage.shManager.registerShaderType('Blur', shaders.Blur);
}
29 changes: 29 additions & 0 deletions examples/tests/shader-blur.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import rockoImg from '../assets/rocko.png';
import type { ExampleSettings } from '../common/ExampleSettings.js';

export async function automation(settings: ExampleSettings) {
await test(settings);
await settings.snapshot();
}

export default async function test({ renderer, testRoot }: ExampleSettings) {
const amounts = [0, 2, 6, 12];
const size = 300;
const gap = 20;

for (let i = 0; i < amounts.length; i++) {
renderer.createNode({
x: 20 + i * (size + gap),
y: 20,
w: size,
h: size,
texture: renderer.createTexture('ImageTexture', {
src: rockoImg,
}),
shader: renderer.createShader('Blur', {
amount: amounts[i],
}),
parent: testRoot,
});
}
}
1 change: 1 addition & 0 deletions exports/canvas-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { HolePunch } from '../src/core/shaders/canvas/HolePunch.js';
export { LinearGradient } from '../src/core/shaders/canvas/LinearGradient.js';
export { RadialGradient } from '../src/core/shaders/canvas/RadialGradient.js';
export { RadialProgress } from '../src/core/shaders/canvas/RadialProgress.js';
export { Blur } from '../src/core/shaders/canvas/Blur.js';
1 change: 1 addition & 0 deletions exports/webgl-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export { HolePunch } from '../src/core/shaders/webgl/HolePunch.js';
export { LinearGradient } from '../src/core/shaders/webgl/LinearGradient.js';
export { RadialGradient } from '../src/core/shaders/webgl/RadialGradient.js';
export { RadialProgress } from '../src/core/shaders/webgl/RadialProgress.js';
export { Blur } from '../src/core/shaders/webgl/Blur.js';
export { Default } from '../src/core/shaders/webgl/Default.js';
23 changes: 23 additions & 0 deletions src/core/shaders/canvas/Blur.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CanvasShaderType } from '../../renderers/canvas/CanvasShaderNode.js';
import { BlurTemplate, type BlurProps } from '../templates/BlurTemplate.js';

export interface ComputedBlurValues {
filter: string;
}

/**
* Canvas2D Blur backed by the native `ctx.filter = 'blur(Npx)'`. Browsers that
* lack filter support (Chrome 38-52) will silently no-op — accept the
* degradation rather than emulating a multi-pass blur in JS.
*/
export const Blur: CanvasShaderType<BlurProps, ComputedBlurValues> = {
props: BlurTemplate.props,
saveAndRestore: true,
update() {
this.computed.filter = `blur(${this.props!.amount}px)`;
},
render(ctx, _node, renderContext) {
ctx.filter = this.computed.filter!;
renderContext();
},
};
23 changes: 23 additions & 0 deletions src/core/shaders/templates/BlurTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { BlurTemplate, type BlurProps } from './BlurTemplate.js';
import { resolveShaderProps } from '../../renderers/CoreShaderNode.js';

function resolve(input: Partial<BlurProps>): BlurProps {
const props = { ...input } as Record<string, unknown>;
resolveShaderProps(props, BlurTemplate.props as never);
return props as unknown as BlurProps;
}

describe('BlurTemplate', () => {
it('applies default amount when omitted', () => {
expect(resolve({}).amount).toBe(4);
});

it('passes through user-provided amount', () => {
expect(resolve({ amount: 10 }).amount).toBe(10);
});

it('passes through zero (no blur)', () => {
expect(resolve({ amount: 0 }).amount).toBe(0);
});
});
25 changes: 25 additions & 0 deletions src/core/shaders/templates/BlurTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { CoreShaderType } from '../../renderers/CoreShaderNode.js';

/**
* Properties of the {@link Blur} shader.
*
* Intended for nodes with an Image Texture. Applying it to color-only or text
* nodes will just blur the solid fill.
*/
export interface BlurProps {
/**
* Blur amount in node-space pixels.
*
* Single-pass kernel — small values (1-8) look best. Larger values still
* work but lose smoothness because there are only 9 samples per pixel.
*
* @default 4
*/
amount: number;
}

export const BlurTemplate: CoreShaderType<BlurProps> = {
props: {
amount: 4,
},
};
47 changes: 47 additions & 0 deletions src/core/shaders/webgl/Blur.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { WebGlShaderType } from '../../renderers/webgl/WebGlShaderNode.js';
import { BlurTemplate, type BlurProps } from '../templates/BlurTemplate.js';

/**
* Single-pass 3x3 Gaussian-approximation blur (1,2,1 / 2,4,2 / 1,2,1) / 16.
*
* 9 texture fetches, no second pass, no render target — designed for Image
* Textures on constrained devices. Larger blurs trade smoothness for speed;
* if a higher-quality blur is needed, stack multiple nodes or do a separable
* pass via a RenderTexture.
*/
export const Blur: WebGlShaderType<BlurProps> = {
props: BlurTemplate.props,
update() {
this.uniform1f('u_amount', this.props!.amount);
},
fragment: `
# ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
# else
precision mediump float;
# endif

uniform vec2 u_dimensions;
uniform sampler2D u_texture;
uniform float u_amount;

varying vec4 v_color;
varying vec2 v_textureCoords;

void main() {
vec2 px = vec2(u_amount) / u_dimensions;

vec4 c = texture2D(u_texture, v_textureCoords) * 0.25;
c += texture2D(u_texture, v_textureCoords + vec2( px.x, 0.0)) * 0.125;
c += texture2D(u_texture, v_textureCoords + vec2(-px.x, 0.0)) * 0.125;
c += texture2D(u_texture, v_textureCoords + vec2(0.0, px.y)) * 0.125;
c += texture2D(u_texture, v_textureCoords + vec2(0.0, -px.y)) * 0.125;
c += texture2D(u_texture, v_textureCoords + vec2( px.x, px.y)) * 0.0625;
c += texture2D(u_texture, v_textureCoords + vec2(-px.x, -px.y)) * 0.0625;
c += texture2D(u_texture, v_textureCoords + vec2( px.x, -px.y)) * 0.0625;
c += texture2D(u_texture, v_textureCoords + vec2(-px.x, px.y)) * 0.0625;

gl_FragColor = c * v_color;
}
`,
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading