- ×
curtains.js is a lightweight vanilla WebGL javascript library that turns HTML elements into interactive textured planes.
Filed under images › 3d graphicsShow AllWhat is it ?
Shaders are the next front-end web developpment big thing, with the ability to create very powerful 3D interactions and animations. A lot of very good javascript libraries already handle WebGL but with most of them it's kind of a headache to position your meshes relative to the DOM elements of your web page.
curtains.js was created with just that issue in mind. It is a small vanilla WebGL javascript library that converts HTML elements containing images and videos into 3D WebGL textured planes, allowing you to animate them via shaders.
You can define each plane size and position via CSS, which makes it super easy to add WebGL responsive planes all over your pages.Knowledge and technical requirements
It is easy to use but you will of course have to possess good basics of HTML, CSS and javascript.
If you've never heard about shaders, you may want to learn a bit more about them on The Book of Shaders for example. You will have to understand what are the vertex and fragment shaders, the use of uniforms as well as the GLSL syntax basics.
Installation
In a browser:<script src="curtains.min.js"></script>
Using npm:npm i curtainsjs
Load ES module:import {Curtains} from 'curtainsjs';
Examples
Images
Vertex coordinates helper
Simple plane
Multiple textures with a displacement shader
Multiple planes
Asynchronous textures loading
AJAX navigation
AJAX navigation with plane removal
Canvas size, performance and perspectiveVideo
Simple video plane
Multiple video textures with a displacement shaderCanvas
Basic setup example
HTML
The HTML set up is pretty easy. Just create a div that will hold your canvas and a div that will hold your image.
<body> <!-- div that will hold our WebGL canvas --> <div id="canvas"></div> <!-- div used to create our plane --> <div class="plane"> <!-- image that will be used as texture by our plane --> <img src="path/to/my-image.jpg" /> </div> </body>
CSS
The CSS is also very easy. Make sure the div that will wrap the canvas fits the document, and apply any size you want to your plane div element.
body { / make the body fits our viewport / position: relative; width: 100%; height: 100vh; margin: 0; overflow: hidden; } #canvas { / make the canvas wrapper fits the document / position: absolute; top: 0; right: 0; bottom: 0; left: 0; } .plane { / define the size of your plane / width: 80%; height: 80vh; margin: 10vh auto; } .plane img { / hide the img element / display: none; }
Javascript
There's a bit more work in the javascript part : we need to instanciate our WebGL context, create a plane with basic uniforms parameters and use it.
window.onload = function() { // get our canvas wrapper var canvasContainer = document.getElementById("canvas"); // set up our WebGL context and append the canvas to our wrapper var webGLCurtain = new Curtains("canvas"); // get our plane element var planeElement = document.getElementsByClassName("plane")[0]; // set our initial parameters (basic uniforms) var params = { vertexShaderID: "plane-vs", // our vertex shader ID fragmentShaderID: "plane-fs", // our framgent shader ID uniforms: { time: { name: "uTime", // uniform name that will be passed to our shaders type: "1f", // this means our uniform is a float value: 0, }, } } // create our plane mesh var plane = webGLCurtain.addPlane(planeElement, params); // use the onRender method of our plane fired at each requestAnimationFrame call plane.onRender(function() { plane.uniforms.time.value++; // update our time uniform value }); }
Shaders
Here are some basic vertex and fragment shaders. Just put it inside your body tag, right before you include the library.
<!-- vertex shader --> <script id="plane-vs" type="x-shader/x-vertex"> #ifdef GL_ES precision mediump float; #endif // those are the mandatory attributes that the lib sets attribute vec3 aVertexPosition; attribute vec2 aTextureCoord; // those are mandatory uniforms that the lib sets and that contain our model view and projection matrix uniform mat4 uMVMatrix; uniform mat4 uPMatrix; // if you want to pass your vertex and texture coords to the fragment shader varying vec3 vVertexPosition; varying vec2 vTextureCoord; void main() { vec3 vertexPosition = aVertexPosition; gl_Position = uPMatrix uMVMatrix vec4(vertexPosition, 1.0); // set the varyings vTextureCoord = aTextureCoord; vVertexPosition = vertexPosition; } </script> <!-- fragment shader --> <script id="plane-fs" type="x-shader/x-fragment"> #ifdef GL_ES precision mediump float; #endif // get our varyings varying vec3 vVertexPosition; varying vec2 vTextureCoord; // the uniform we declared inside our javascript uniform float uTime; // our texture sampler (default name, to use a different name please refer to the documentation) uniform sampler2D uSampler0; void main() { vec2 textureCoord = vec2(vTextureCoord.x, vTextureCoord.y); // displace our pixels along the X axis based on our time uniform // textures coords are ranging from 0.0 to 1.0 on both axis textureCoord.x += sin(textureCoord.y 25.0) cos(textureCoord.x 25.0) (cos(uTime / 50.0)) / 25.0; gl_FragColor = texture2D(uSampler0, textureCoord); } </script>
Et voilà !
Images uniform sampler names
Let's say you want to build a slideshow with 3 images and a displacement image to create a nice transition effect.
By default, the textures uniforms sampler will be named upon their indexes inside your plane element. If you got something like that :<!-- div used to create our plane --> <div class="plane"> <!-- images that will be used as textures by our plane --> <img src="path/to/displacement.jpg" /> <img src="path/to/my-image-1.jpg" /> <img src="path/to/my-image-2.jpg" /> <img src="path/to/my-image-3.jpg" /> </div>
Then, in your shaders, your textures samplers would have to be declared that way :
uniform sampler2D uSampler0 // bound to displacement.jpg uniform sampler2D uSampler1 // bound to my-image-1.jpg uniform sampler2D uSampler2 // bound to my-image-2.jpg uniform sampler2D uSampler3 // bound to my-image-3.jpg
It is handy but you could also get easily confused.
By using a data-sampler attribute on the <img /> tag, you could specify a custom uniform sampler name to use in your shader. With the example above, this would become :<!-- div used to create our plane --> <div class="plane"> <!-- images that will be used as textures by our plane --> <img src="path/to/displacement.jpg" data-sampler="uDisplacement" /> <img src="path/to/my-image-1.jpg" data-sampler="uSlide1" /> <img src="path/to/my-image-2.jpg" data-sampler="uSlide2" /> <img src="path/to/my-image-3.jpg" data-sampler="uLastSlide" /> </div>
uniform sampler2D uDisplacement // bound to displacement.jpg uniform sampler2D uSlide1 // bound to my-image-1.jpg uniform sampler2D uSlide2 // bound to my-image-2.jpg uniform sampler2D uLastSlide // bound to my-image-3.jpg
Using videos as textures
Yes, videos as textures are supported ! However there are a few downsides you need to know.
First, the videos will always fit the plane : your plane's size ratio would have to be the same as your videos so they won't appear distorted (you can handle that with CSS).
We can't autoplay videos without a user gesture on most mobile devices. Unless you don't care about mobile users, you will have to start the videos playback after a user interaction like a click event.
Besides that, videos are really easy to use (and can be mixed with images as well). Let's see how we can handle them :HTML
<!-- div used to create our plane --> <div class="plane"> <!-- video that will be used as a texture by our plane --> <video src="path/to/my-video.mp4"></video> </div>
Like with images, you can use a data-sampler attribute to set a uniform sampler name. You can use one or more videos, or mixed them with images if you want :
<!-- div used to create our plane --> <div class="plane"> <!-- elements that will be used as textures by our plane --> <img src="path/to/displacement.jpg" data-sampler="displacement" /> <video src="path/to/my-video-1.mp4" data-sampler="firstVideo"></video> <video src="path/to/my-video-2.mp4" data-sampler="secondVideo"></video> </div>
Javascript
There's only one change inside our javascript : we need to tell our plane when to start playing the videos. We've got a playVideos() method that we will put inside an event listener in our onReady() method :
window.onload = function() { // get our canvas wrapper var canvasContainer = document.getElementById("canvas"); // set up our WebGL context and append the canvas to our wrapper var webGLCurtain = new Curtains("canvas"); // get our plane element var planeElement = document.getElementsByClassName("plane")[0]; // set our initial parameters (basic uniforms) var params = { vertexShaderID: "plane-vs", // our vertex shader ID fragmentShaderID: "plane-fs", // our framgent shader ID uniforms: { time: { name: "uTime", // uniform name that will be passed to our shaders type: "1f", // this means our uniform is a float value: 0, }, } } // create our plane mesh var plane = webGLCurtain.addPlane(planeElement, params); plane.onReady(function() { // set an event listener to start our playback document.getElementbyId("start-playing").addEventListener("click", function() { plane.playVideos(); }); }).onRender(function() { // use the onRender method of our plane fired at each requestAnimationFrame call plane.uniforms.time.value++; // update our time uniform value }); }
And that's it. Check the video examples (and source codes) if you want to see what's possible.
Using canvas as texture
Last but not least, you can use a canvas as a texture. It is once again really easy to use. You just have to insert a canvas tag inside your HTML, or eventually create it in your javascript and load it using the loadCanvases() method.
HTML
<!-- div used to create our plane --> <div class="plane"> <!-- canvas that will be used as a texture by our plane --> <canvas id="canvas-texture"></canvas> </div>
You can use multiple canvases and data-sampler attributes as well, like you'd do with images or videos.
Javascript
The javascript code remains almost the same. We just set the size of our canvas, get its context and draw a simple rotating red rectangle inside our animation loop.
window.onload = function() { // get our canvas wrapper var canvasContainer = document.getElementById("canvas"); // set up our WebGL context and append the canvas to our wrapper var webGLCurtain = new Curtains("canvas"); // get our plane element var planeElement = document.getElementsByClassName("plane")[0]; // set our initial parameters (basic uniforms) var params = { vertexShaderID: "plane-vs", // our vertex shader ID fragmentShaderID: "plane-fs", // our framgent shader ID uniforms: { time: { name: "uTime", // uniform name that will be passed to our shaders type: "1f", // this means our uniform is a float value: 0, }, } } // create our plane mesh var plane = webGLCurtain.addPlane(planeElement, params); // our texture canvas var textureCanvas = document.getElementById("canvas-texture"); var textureCanvasContext = textureCanvas.getContext("2d"); // set the size of our canvas textureCanvas.width = planeElements[0].clientWidth; textureCanvas.height = planeElements[0].clientHeight; // use the onRender method of our plane fired at each requestAnimationFrame call plane.onRender(function() { plane.uniforms.time.value++; // update our time uniform value // here we will handle our canvas texture animation // clear scene textureCanvasContext.clearRect(0, 0, textureCanvas.width, textureCanvas.height); // continuously rotate the canvas textureCanvasContext.translate(textureCanvas.width / 2, textureCanvas.height / 2); textureCanvasContext.rotate(Math.PI / 360); textureCanvasContext.translate(-textureCanvas.width / 2, -textureCanvas.height / 2); // draw a red rectangle textureCanvasContext.fillStyle = "#ff0000"; textureCanvasContext.fillRect(textureCanvas.width / 2 - textureCanvas.width / 8, textureCanvas.height / 2 - textureCanvas.height / 8, textureCanvas.width / 4, textureCanvas.height / 4); }); }
Documentation
Curtains object
Instanciate
You will have to create a Curtains object first that will handle the scene containing all your planes. It will also create the WebGL context, append the canvas and handle the requestAnimationFrame loop. You just have to pass the ID of the HTML element that will wrap the canvas :
var curtains = new Curtains("canvas"); // "canvas" is the ID of our HTML element
You can pass a boolean as a second parameter to indicate whether you want to use the library in production mode. If set to true it will remove all console warnings. Default to false.
var curtains = new Curtains("canvas", true); // use the library in "production" mode
Methods
-
addPlane(planeElement, params) :
planeElement (HTML element) : a HTML element
params (object) : an object containing the plane parameters (see the Plane object).This function will add a plane to our Curtains wrapper.
-
removePlane(plane) :
plane (plane object) : the plane to remove
This function will remove a plane from our Curtains object.
-
onError() :
This function will be called if there's an error during the initialisation, or if the WebGL context could not be created.
-
dispose() :
This function will cancel the requestAnimationFrame loop, remove all planes and delete the WebGL context.
Plane object
Those are the planes we will be manipulating. They are instanciate internally each time you call the addPlane method on the parent Curtains object.
Properties
- vertexShader (string) : Your vertex shader as a string. Be careful with the line-breaks as it may throw javascript errors. Will look for vertexShaderID param if not specified.
- vertexShaderID (string) : the vertex shader ID. If ommited, will look for a data attribute data-vs-id on the plane HTML element. Will use a default vertex shader and throw a warning if nothing specified.
- fragmentShader (string) : Your fragment shader as a string. Be careful with the line-breaks as it may throw javascript errors. Will look for fragmentShaderID param if not specified.
- fragmentShaderID (string) : the fragment shader ID. If ommited, will look for a data attribute data-fs-id on the plane HTML element. Will use a default fragment shader that draws only black pixels and throw a warning if nothing specified.
- widthSegments (integer, optionnal) : plane definition along the X axis (1 by default).
- heightSegments (integer, optionnal) : plane definition along the Y axis (1 by default).
- mimicCSS (bool, optionnal) : define if the plane should copy it's HTML element position (true by default).
- imageCover (bool, optionnal) : define if the images must imitate css background-cover or just fit the plane (true by default).
- crossOrigin (string, optionnal) : define the cross origin process to load images if any.
- fov (integer, optionnal) : define the perspective field of view (default to 75).
- uniforms (object, otpionnal): the uniforms that will be passed to the shaders (if no uniforms specified there won't be any interaction with the plane). Each uniform should have three properties : a name (string), a type (string, see here) and a value.
Parameters basic example
var params = { vertexShaderID: "plane-vs", // our vertex shader ID fragmentShaderID: "plane-fs", // our framgent shader ID uniforms: { time: { name: "uTime", // uniform name that will be passed to our shaders type: "1f", // this means our uniform is a float value: 0, }, } }
Methods
-
loadImages(imgElements) :
imgElements (HTML image elements) : a collection of HTML image elements to load into your plane.This function is automatically called internally on a new Plane instanciation, but you can use it if you want to create an empty plane and then assign it some textures later. See asynchronous textures loading example.
-
loadVideos(videoElements) :
videoElements (HTML video elements) : a collection of HTML video elements to load into your plane.This function is automatically called internally on a new Plane instanciation. It works exactly the same as the loadImages() method.
-
loadCanvases(canvasElements) :
canvasElements (HTML canvas elements) : a collection of HTML canvas elements to load into your plane.This function is automatically called internally on a new Plane instanciation. It works exactly the same as the loadImages() method.
-
onLoading() :
This function will be fired each time an image of the plane has been loaded. Useful to handle a loader.
-
onReady() :
This function will be called once our plane is all set up and ready to be drawn. This is where you may want to add event listener to interact with it or update its uniforms.
-
onRender() :
This function will be triggered at each requestAnimationFrame call. Useful to update a time uniform, change plane rotation, scale, etc.
-
playVideos() :
This function will automatically start all of your plane videos playback. If you are not calling it after a user action it might not work on mobile.
-
planeResize() :
This method is called internally each time the WebGL canvas is resized, but you should call it manually each time you're updating a plane size, either via CSS animations or directly in javascript. Updates position as well if the mimicCSS property is set to true.
-
updatePosition() :
The planes positions are updated only when the canvas container is resized. But if you are updating your plane HTML element position without resizing the container (typically animating its CSS position or transform values), call this method in your animation loop at the same time.
Only effective if the mimicCSS property is set to true. -
setPerspective(fieldOfView, nearPlane, farPlane) :
fieldOfView (integer) : the perspective field of view. Should be greater than 0 and lower than 180. Default to 75.
nearPlane (float, optionnal) : closest point where a mesh vertex is displayed. Default to 0.1.
farPlane (float, optionnal) : farthest point where a mesh vertex is displayed. Default to 150 (two times the field of view).Reset the perspective. The smaller the field of view, the more perspective.
-
setScale(scaleX, scaleY) :
scaleX (float) : the scale to set along the X axis.
scaleY (float) : the scale to set along the Y axis.Set the plane new scale.
-
setRotation(angleX, angleY, angleZ) :
angleX (float) : the angle in radians to rotate around the X axis.
angleY (float) : the angle in radians to rotate around the Y axis.
angleZ (float) : the angle in radians to rotate around the Z axis.Set the plane rotation.
-
setRelativePosition(translationX, translationY) :
translationX (float) : the translation value to apply on the X axis in pixel.
translationY (float) : the translation value to apply on the Y axis in pixel.Set the plane translation based on pixel units.
-
mouseToPlaneCoords(xMousePosition, yMousePosition) :
xMousePosition (float) : mouse event clientX value.
yMousePosition (float) : mouse event clientY value.Get the mouse coordinates relative to the plane clip space values. Use it to send to a uniform and interact with your plane. A plane coordinates ranges from (-1, 1) in the top left corner to (1, -1) in the bottom right corner, which means the values along the Y axis are inverted.
-
enableDepthTest(shouldEnableDepthTest) :
shouldEnableDepthTest (bool) : enable or disable the depth test for that plane.Switches on/off the depth test for that plane. You might want to disable the depth test if you got transparency issues.
-
moveToFront() :
Let the plane overlay all other planes. Be careful as it is silently disabling depth test for that plane, you might want to switch it back on later.
Canvas height, perspective and performance
The canvas size will directly impact the global perspective and performance.
The perspective is calculated based on each plane position inside the canvas as well as the canvas width and height. The center of the field of view will be located at the center of your canvas, which means that with tall or wide canvas you might end with exagerated perspectives.
Also keep in mind that a wide or tall canvas will pretty much impact performance (thanks to Colin Peyrat for pointing that out).There's a way to avoid these annoyances: set your canvas to fit the window size instead of the document size, and set the position of each plane based on the scroll inside your render loop. Check the canvas size, performance and perspective example if you want to see the differences.
Other performance tips
- Plane's videos and canvases textures are updated at each frame, which has a significant impact on performance. When those textures are not visible (if they are hidden by another texture, or if the plane is not visible in the viewport...), you should set those videos or canvases shouldUpdate property to false, and switch it back to true before displaying them.
- Be careful with each plane definition. A lot of vertices implies a big impact on performance. If you plan to use more than one plane, try to reduce the number of vertices.
- Large images have a bigger impact on performance. Try to scale your images so they will fit your plane maximum size. It goes the same for videos of course: try to keep them as light as possible.
- Try to use as less javascript as possible in the onRender() planes methods as this get executed at each draw call. Try not to use too many uniforms as they are updated at every draw call as well.
- If you use multiple planes with multiple textures, you should set the dimensions of your plane to fit the aspect ratio of your images in CSS (you could use the padding-bottom hack, see the multiple planes example HTML & CSS) and set the imageCover plane property to false when adding it.
Changelog
Version 1.7
- setScale() method now adjusts the textures sizes as well.
- Fixed a bug where CSS scale and plane's sizes were not synced.
- All plane's images, canvases and videos now have a shouldUpdate property set to true by default. When those associated textures are not visible, you could set this property to false to get a performance boost.
- Added an onError() callback to the Curtains object to handle errors during initialisation and updated all examples.
- Fixed a bug with video textures (props to @deJong)
Version 1.6
- Added an updatePosition() method to the Plane object.
- Added a "production" parameter to the Curtains instanciation process to remove console warnings in production.
- Improved video handling and removed warnings.
Version 1.5
- Added a removePlane() method to the Curtains object.
- Slightly improved video textures performance.
Version 1.4
- Added support for canvases as textures.
- Improved pixel ratio handling.
Version 1.3
- Added the possibility to set/unset depth test for each plane via a enableDepthTest() method. This might be useful to handle transparency problems.
- Added a plane moveToFront() method so that a plane could overlay all other planes.
Version 1.2
- Added support for videos as textures.
- Sort planes by their vertices length in order to avoid redundant buffer binding calls during draw loop.
- Refactored and cleaned code.
Version 1.1
- WebGL context viewport size now based on drawingBufferWidth and drawingBufferHeight.
- Cleaned and refactored code in order to add support for lost and restored context events.
About
This library is released under the MIT license which means it is free to use for personnal and commercial projects.
All images used in the examples were taken by Marion Bornaz during the Mirage Festival.
All examples video footages were shot by Analogue Production.
Many thanks to webglfundamentals.org tutorials which helped me a lot.
Author of this library is Martin Laxenaire, a french creative front-end developper based in Lyon.
Found a bug ? Have questions ? Do not hesitate to email me or send me a tweet : @webdesign_ml.Showcase
Here's a list of websites that use curtains.js with their own custom shaders:
-