There are some excellent resources online for learning shaders and WebGL, namely The Book of Shaders and WebGL Fundamentals. This post covers some of the basic patterns that I needed to learn in order to complete the Dream Builder's crop implementation.
Mix a Mask
float mask = step(edge, x);
vec3 color = mix(color1, color2, mask);
The code for each shape below can be abstracted to a separate function and called from main like so:
vec3 color = mix(color1, color2, getShapeMask(v));
Draw a Rectangle
varying vec2 vUv;
float rect(vec2 xy, vec2 wh, vec2 st) {
vec2 xyMask = step(xy, st);
vec2 whMask = 1.0 - step(xy + wh, st);
vec2 mask = xyMask * whMask;
return mask.x * mask.y;
}
void main() {
vec4 red = vec4(1.,0.,0.,1.);
vec4 blue = vec4(0.,0.,1.,1.);
vec2 xy = vec2(.1,.2);
vec2 wh = vec2(.3,.4);
gl_FragColor = mix(blue, red, rect(xy, wh, vUv));
}
Step Between Macro
This is used to return 1.0
if v
is between lo
and hi
, 0.0
otherwise:
#define between(lo, hi, v) step(lo, v) * step(v, hi)
This logic comes in very handy for the subsequent shapes.
Rectangle with Borders
Notice how max()
and min()
act as union/intersection on our mask in this
example:
float outlineRect(vec2 xy, vec2 wh, vec2 th, vec2 st) {
vec2 bl = between(xy, xy + th, st);
vec2 tr = between(xy + wh, xy + wh + th, st);
float l = min(bl.x, between(xy.y, xy.y + wh.y + th.y, st.y));
float b = min(bl.y, between(xy.x, xy.x + wh.x + th.x, st.x));
float r = min(tr.x, between(xy.y, xy.y + wh.y + th.y, st.y));
float t = min(tr.y, between(xy.x, xy.x + wh.x + th.x, st.x));
return max(max(l, b), max(r, t));
}
Full Border
Naively we can use our between
macro again:
float full_border(vec2 th, vec2 st) {
vec2 mask = between(th, 1.0 - th, st);
return 1.0 - min(mask.x, mask.y);
}
But if you scale this up and down (see below) you'll notice the border continues
infinitely beyond the edges of the clip space. It may be more desirable to use
our outlineRect
again to produce a finite border:
float full_border(vec2 th, vec2 st) {
return outlineRect(vec2(0.0), 1.0 - th, th, st);
}
Circle
A simple circle:
float circle(float radius, vec2 st) {
vec2 center = vec2(0.5, 0.5);
float size = radius;
return 1.0 - step(size, distance(center, st));
}
Shape Cutting
We can cut one mask with another. Here we cut a circle out of a full border which creates the effect of pointed arrow corners, ideal for scaling/resizing handles:
// these values produce the desired effect
vec2 border_thickness = vec2(0.025);
float radius = 0.6;
// shape cutting code
float border_mask = full_border(border_thickness, st);
vec4 color = mix(bg_color, border_color, border_mask);
gl_FragColor = mix(color, bg_color, min(border_mask, circle(radius, st)));
More from this series
This post is part of the Dream Builder Series.
The previous post was Setting up Shaders with React Three Fiber.
The next post is Implementing Crop.
Get in touch
If you have any questions or ideas, please email me at tom@bearjam.dev.