Recreating Instagram Filter Functionality with CSS and Canvas APIs
Recreating UI components, especially those in native apps, has always led me to interesting unknown web APIs. It also helps me see things from the app developers' perspective and understand why a particular task is done in a particular way. Going forward, I will jot down the making-of of each clone and share the lessons I've learned. I hope you will also learn a thing or two and starting cloning your favorite components.
Today, I want to share my recent clone of the Instagram Filter page which turned out to be much simpler than I expected (if I ignore a browser).
Goal: Users can select a photo from their device, apply filters, see preview, and export(download) the result.
The Process
Each Instagram filter is made of a set of basic filter effects, e.g. brightness, contrast, saturate, etc, and some overlays. With the help of CSS filter
and mix-blend-mode
, stacking filters and overlays to recreate an Instagram filter is pretty much an eyeballing task. Thanks to this brilliant work by Una which did exactly that, 75% of my goal is completed. From there, I only have to figure out a way to export the result out since the CSS is changing the appearance but not the actual image. Luckily, I found there are two Canvas APIs that do very similar things and they are canvas filter
and globalCompositeOperation.
With them, I can perform the same operation to the image drawn on canvas and use the method toDataURL
to export it out. 🍬
Unfortunately, the canvas filter API is experimental and is only supported in Chrome, Firefox, Edge, but not in Safari. So the clone is not fully functioning on the iPhone. augh, safari!
Some Details
You can see the full implementation in the CodeSandbox. Note that the exporting function does not work in CodeSandbox's iframe
browser, open the app in a new window instead. Here are some implementation details:
I used an array to store the filter configurations:
// effects.jsconst effects = [{name: "noraml",filter: "none",overlays: []},{name: "clarendon",filter: "contrast(1.2) saturate(1.35)",overlays: [{backgroundColor: "rgba(127, 187, 227, 0.2)",mixBlendMode: "overlay"}]},]
When the "clarendon" filter is turned into HTML and CSS:
<!--image with "Clarendon" filter applied --><figure style="filter: contrast(1.2) saturate(1.35);"><img src="/plitvice-lakes.jpg"><div style="background-color: rgba(127, 187, 227, 0.2); mix-blend-mode: overlay;"></div></figure>
This is how a Instagram filter(i called it effect
in the code) is applied on canvas:
function applyEffect(name) {// find effect by nameconst effect = effects.find(eff => eff.name === name);const { width, height } = previewCanvas;// clear canvasctx.clearRect(0, 0, width, height);// apply filterctx.filter = effect.filter;// draw the imagectx.globalCompositeOperation = "source-over";ctx.drawImage(previewImg, 0, 0, width, height);// loop through overlays and fill with corresponding color and blend modeeffect.overlays.forEach(overlay => {ctx.globalCompositeOperation = overlay.mixBlendMode;ctx.fillStyle = overlay.backgroundColor;ctx.fillRect(0, 0, width, height);});}
Lessons Learned
- Both CSS
filter
andmix-blend-mode
are handy if you want to alter the look of your page without reaching for graphics editors. They can be applied to not only image but every element. - Canvas declarative
filter
API lower the entry barriers to image processing. - These APIs all make use of the GPU so they are performant.
- The CSS properties have wide browser support but not the canvas
filter
API. - For production, use WebGL or a thrid-party library instead.
Safari is the new IE.
Afterthoughts
Since I had played with WebGL before, I am well aware these effects can be achieved using WebGL. (Take a look at gl-react if you are a React developer) But this time, I am experimenting with an even simpler solution. And thanks to the declarative APIs (and also CSSgram!), recreating those Instagram effects and the export function is not complicated at all. I am interested in how you are using these CSS properties so please let me know! 😉
Thank you for reading! Until next time! 👋