One method I often use during development is to design the interface first, and implement it afterwards. This works for me most of the time :)
When I first got the idea to add shaders to css-doodle, I started with what it might look like:
background: @shaders(
/* code */
);
Later I realized there are two kinds of shaders, vertex shader and fragment shader.
They're usually separated in different <script>
tags or JavaScript variables.
In order to put them into single @shader()
together, I borrowed the idea of the CSS syntax,
that is, to group them with the syntax of CSS selectors.
background: @shaders(
fragment {
/* code */
}
vertex {
/* code */
}
);
This also extends well while adding textures
.
background: @shaders(
fragment { }
vertex { }
texture { }
);
Multiple textures can be identified by their prefixes.
background: @shaders(
fragment { }
vertex { }
/* multiple textures */
texture { }
texture_1 { }
);
If there's no fragment
group I'll treat the whole as fragment shader,
which makes the code much simpler.
background: @shaders(
void main() {
/* code */
}
);
Code inside Custom Properties
The length of the shader code increases easily so it's a good idea to place it to somewhere else. Using CSS Custom properties is an option.
--shaders: @shaders(
void main() {
/* code */
}
);
background: var(--shaders);
Since css-doodle supports writing rules in ordinary CSS files,
the code can be breakdown into smaller parts and join them together with var()
.
<style>
css-doodle {
--texture: (
/* code */
);
--fragment: (
/* code */
);
--rule: (
background: @shaders(
texture { var(--texture) }
fragment { var(--fragment) }
);
);
}
</style>
<css-doodle use="var(--rule)"></css-doodle>
Pitfalls
However, the values of custom property will be serialized by the CSS parser. All the numbers like 3.0
will be transformed to integer 3
, which makes the shader code fails to compile.
css-doodle {
--fragment: (
void main() {
float number = 3.0; /* won't work */
}
);
}
A workaround is to write 3.
instead of 3.0
.
css-doodle {
--fragment: (
void main() {
float number = 3.; /* ok */
}
);
}
Implementation
The implementation is quite straightforward.
Parse code inside @shader()
to read fragment/vertex shader and textures.
Transform textures into images before passing them to WebGL.
Handle shader compiling and linking.
Output the canvas as image data in the end.
Examples
So there's a basic example, fill the color of each pixel based on its coordinates.
@grid: 1 / 180px;
background: @shaders(
void main() {
vec2 p = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = vec4(p.xy, .8, 1.);
}
);
Make texture with css-doodle and load it with fragment shader. >
@grid: 1 / 180px;
background: @shaders(
texture_0 {
@grid: 1 / 100%;
background: linear-gradient(45deg, @stripe.@m20.@p(
#d62828, #fcbf49, #eae2b7, #000
));
}
fragment {
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
gl_FragColor = texture2D(texture_0, uv);
}
}
);
Now, I can finally make something that can't do with CSS and SVG.
@grid: 1 / 180px;
background: @shaders(
texture_0 {
/* ... */
}
fragment {
void main() {
vec2 p = gl_FragCoord.xy/u_resolution.xy;
vec2 c = vec2(.5, .5);
vec2 uv = p.xy - c;
float ratio = u_resolution.x/u_resolution.y;
float len = length(uv * vec2(ratio, 1.));
float t = atan(uv.y, uv.x) +
2. * 3.141593 * smoothstep(.5, 0., len);
float r = length(uv) + .1;
vec2 coords = vec2(r*cos(t),r*sin(t)) + c;
gl_FragColor = texture2D(texture_0, coords);
}
}
);
Although there's no animation yet, I'm already excited about it. See also the above demo on CodePen.
Next step
I'll try to add animation by introducing the uniform u_time
,
but then the whole thing can't be put into background
anymore.
I really wish the element() function
can be supported more widely soon.
Time to learn more GLSL!