Skip to content
Reveal · Wipes & transitionsBeginner

Linear wipe

A soft straight edge that sweeps across the quad at any angle, easing in and out, and back again.

UVScreenTimeSurface
Open in Playground
  1. 01GoalReveal
  2. 02TechniqueWipes & transitions
  3. 03ExecutionLinear wipe
Full-screen quad

Parameters

2.4
20
0.060

Copy for your platform

  • GLSLCopy
  • GodotCopy

Node chain

Built step by step, in GLSL style, with its node names.

Built step by step, in Godot style, with its node names.

No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.

No Unity version of this card yet, so the nodes show the GLSL names.

No Blender version of this card yet, so the nodes show the GLSL names.

  1. clampClampclampclampclamp
    UV
    Screen
    Time

    01 Progress

  2. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    02 Ease in and out

  3. dotDotdotdotdot

    03 Position along the wipe

  4. mixMixmixmixmix

    04 Front

  5. smoothstepSmoothStepsmoothstepsmoothstepsmoothstep

    05 Mask

How it works

The card sweeps a white mask across the quad and back, over and over. Where the mask is 1 the new picture would show, where it is 0 the old one stays, and between them is a feathered edge. Direction turns the edge to any angle, Edge softness feathers it, and Duration sets how long one sweep takes.

1. Progress

A transition needs a number that goes from 0 to 1 over its duration. Here it comes from the clock so that the preview loops on its own: fract(i.time / p_duration) is a sawtooth that climbs from 0 to 1 every duration, and 1.0 - abs(1.0 - 2.0 * x) folds it into a triangle wave that climbs and then comes back down. Stretching that by 1.25 and clamping to 0..1 flattens the top and the bottom, so the wipe rests for a moment fully shown and fully hidden before it turns around. In a game or a page, this whole step is replaced by one uniform that a script animates (the platform guides show how).

2. Ease in and out

A linear progress makes the edge start at full speed and stop dead, which reads as mechanical. Running it through smoothstep from 0 to 1 is the cheapest ease-in-out: the cubic p * p * (3 - 2p) starts flat, speeds up through the middle and flattens again, so the edge accelerates away and settles into place. Any other easing curve from the easing fundamental can go here instead.

3. Position along the wipe

The shape of a linear wipe is each pixel’s distance along one direction. radians turns Direction into an angle, cos and sin turn the angle into a unit vector, and the dot product of the centered UV with it measures how far along that direction each pixel is.

That distance doesn’t run from 0 to 1 by itself. Along a diagonal, the quad’s corners are further from its center than its edges are, by 0.5 * (|x| + |y|) of the direction vector. Dividing by twice that reach and adding 0.5 maps the first corner the wipe touches to 0 and the last one to 1 at every angle, so a diagonal wipe takes as long as a horizontal one and never ends early.

4. Front

The front is where the edge stands at the current moment. mix(-p_softness, 1.0, eased) moves it from one softness before 0 to exactly 1. Starting one softness early means that at progress 0 even the feathered part of the edge is off the quad, so the mask is cleanly 0, and at progress 1 it is cleanly 1.

5. Mask

smoothstep(front, front + p_softness, along) is 0 behind the front and 1 ahead of it, with a smooth ramp of Edge softness in between. One minus it is the mask: the pixels the front has passed are 1. The card’s output role is a mask, so the value is not color-encoded: the preview shows it as it is, white where the new picture would be and black where the old one stays. A host reads it from the red channel and uses it as the opacity of a cover or as the blend factor between two pictures.

A hard step(front, along) would work too, but on a diagonal it shows stairs, one pixel tall. Even a softness of a few pixels hides them, which is why the softness never goes to zero.

Make it in GLSL

The copy button gives a complete WebGL2 fragment shader. Draw it on a quad or a full-screen triangle with a vertex shader that writes v_uv from 0 at the bottom-left to 1 at the top-right. main() also reads v_position, v_normal and v_view, so the vertex shader must declare and write them, but zeros are fine: the effect uses only uv.

As it is, the shader loops forever on u_time, which suits a preview. A real transition runs once, when you say so. Replace the first two lines of effect() with a uniform and drive it from JavaScript:

uniform float u_progress; // 0 = hidden, 1 = shown

// in effect(), instead of wave and progress:
float progress = u_progress;
const start = performance.now();
function frame(now) {
  const t = Math.min((now - start) / 600, 1); // a 600 ms wipe
  gl.uniform1f(progressLocation, t);
  draw();
  if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

The easing stays in the shader, so the script only has to send a straight ramp.

To use the mask as a transition between two pictures rather than as an overlay, keep the shape and the edge and mix two textures with it in your own fragment shader: fragColor = mix(texture(oldScene, uv), texture(newScene, uv), mask);. Both scenes must be rendered to textures first, which is the screen-post version of this technique.

Make it in Godot

In a VisualShader (CanvasItem mode), build the axis from the direction with Radians, Cos and Sin into a Vector2Compose, take a Dot of it with UV minus 0.5, and feed it to a SmoothStep whose edges are the front and the front plus the softness; a OneMinus of that goes to Alpha.

Add a ColorRect named Wipe inside a CanvasLayer, set its anchors to Full Rect so it covers the window, give it a new ShaderMaterial and paste the shader into it. It is a canvas_item shader and, as exported, draws the mask itself, opaque: white where the wipe has passed, black elsewhere.

For a scene transition, turn the mask into a cover: a color whose alpha is the mask, so the game shows through where the wipe hasn’t passed. Change the last line of fragment():

COLOR = vec4(0.0, 0.0, 0.0, frag_out.r);

Then make the wipe run once, on cue. Add a uniform and replace the first two lines of effect():

uniform float amount : hint_range(0.0, 1.0) = 0.0;

// in effect(), instead of wave and progress:
float progress = amount;

and tween it from a script, changing the scene while the screen is fully covered:

func change_scene(path: String) -> void:
	var material := $CanvasLayer/Wipe.material as ShaderMaterial
	var tween := create_tween()
	tween.tween_method(func(v: float) -> void: material.set_shader_parameter("amount", v), 0.0, 1.0, 0.5)
	await tween.finished
	get_tree().change_scene_to_file(path)
	tween = create_tween()
	tween.tween_method(func(v: float) -> void: material.set_shader_parameter("amount", v), 1.0, 0.0, 0.5)

The CanvasLayer must live in an autoload (or survive the change some other way) so the cover stays up while the next scene loads. The ease stays inside the shader, so a linear tween is enough.

No Unreal Engine 5 tutorial for this card yet.

No Unity tutorial for this card yet.

No Blender tutorial for this card yet.

Code

linear-wipe.fragGLSL
#version 300 es// Wipes & transitions → Linear wipe, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform float p_duration; // Duration (s): 2.4 (0.5 to 6.0)uniform float p_direction; // Direction (degrees): 20.0 (0.0 to 360.0)uniform float p_softness; // Edge softness: 0.06 (0.002 to 0.4)in vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs {	vec2 uv;	vec3 position;	vec3 normal;	vec3 view;	float time;	vec2 fragCoord;	vec2 resolution;};vec4 effect(Inputs i) {	// A triangle wave, stretched past 0..1 and clamped, so the wipe rests a moment when fully shown and fully hidden	float wave = 1.0 - abs(1.0 - 2.0 * fract(i.time / p_duration));	float progress = clamp(wave * 1.25 - 0.125, 0.0, 1.0);	float eased = smoothstep(0.0, 1.0, progress);	float angle = radians(p_direction);	vec2 axis = vec2(cos(angle), sin(angle));	// The quad's corners project to between -reach and +reach on the axis, so this maps corner to corner onto 0..1 at	// any angle, and the wipe takes the whole duration to cross	float reach = 0.5 * (abs(axis.x) + abs(axis.y));	float along = dot(i.uv - 0.5, axis) / (2.0 * reach) + 0.5;	// The front starts one softness before 0 and ends at 1, so the soft edge is fully off the quad at both ends	float front = mix(-p_softness, 1.0, eased);	float mask = 1.0 - smoothstep(front, front + p_softness, along);	return vec4(vec3(mask), 1.0);}void main() {	Inputs i;	i.uv = v_uv;	i.position = v_position;	// Interpolation shortens the vectors between vertices	i.normal = normalize(v_normal);	i.view = normalize(v_view);	i.time = u_time;	i.fragCoord = gl_FragCoord.xy;	i.resolution = u_resolution;	vec4 frag_out = effect(i);	// A mask shows as is: coverage in .r, not encoded; opaque, so alpha is 1	fragColor = vec4(clamp(frag_out.rgb, 0.0, 1.0), 1.0);}
linear-wipe.gdshaderGodot shader
// Wipes & transitions → Linear wipe, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform float duration : hint_range(0.5, 6.0, 0.1) = 2.4;uniform float direction : hint_range(0.0, 360.0, 1.0) = 20.0;uniform float softness : hint_range(0.002, 0.4, 0.002) = 0.06;struct Inputs {	vec2 uv;	vec3 position;	vec3 normal;	vec3 view;	float time;	vec2 fragCoord;	vec2 resolution;};vec4 effect(Inputs i) {	// A triangle wave, stretched past 0..1 and clamped, so the wipe rests a moment when fully shown and fully hidden	float wave = 1.0 - abs(1.0 - 2.0 * fract(i.time / duration));	float progress = clamp(wave * 1.25 - 0.125, 0.0, 1.0);	float eased = smoothstep(0.0, 1.0, progress);	float angle = radians(direction);	vec2 axis = vec2(cos(angle), sin(angle));	// The quad's corners project to between -reach and +reach on the axis, so this maps corner to corner onto 0..1 at	// any angle, and the wipe takes the whole duration to cross	float reach = 0.5 * (abs(axis.x) + abs(axis.y));	float along = dot(i.uv - 0.5, axis) / (2.0 * reach) + 0.5;	// The front starts one softness before 0 and ends at 1, so the soft edge is fully off the quad at both ends	float front = mix(-softness, 1.0, eased);	float mask = 1.0 - smoothstep(front, front + softness, along);	return vec4(vec3(mask), 1.0);}void fragment() {	Inputs i;	// UV starts at the top-left; the effect expects GLSL's bottom-left	i.uv = vec2(UV.x, 1.0 - UV.y);	// A flat quad facing the viewer, the same as the site's 2D preview	i.position = vec3(i.uv * 2.0 - 1.0, 0.0);	i.normal = vec3(0.0, 0.0, 1.0);	i.view = vec3(0.0, 0.0, 1.0);	i.time = TIME;	// FRAGCOORD's origin is the top-left too	i.resolution = 1.0 / SCREEN_PIXEL_SIZE;	i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y);	vec4 frag_out = effect(i);	// A mask shows as is: coverage in .r, not encoded	COLOR = vec4(clamp(frag_out.rgb, 0.0, 1.0), 1.0);}

No Unreal Engine 5 code for this card yet.

No Unity code for this card yet.

No Blender code for this card yet.

Further reading