How to Build a Simple API: A Friendly Guide
Source link
نویسنده: post Bina
-
How to Build a Simple API: A Friendly Guide
-
Designer Spotlight: Ivan Ermakov | Codrops
Hi, I’m Ivan—a Dubai-based designer focused on fintech products and branding. I run Moonsight, where we craft thoughtful digital experiences and sharp visual identities for financial companies around the world.
Background
My path into design wasn’t a childhood calling—I wasn’t drawing wireframes at age ten or dreaming of Helvetica (can you imagine XD). I just knew I didn’t want the typical office life. I wanted freedom, movement, and a way to create things that felt useful. Design turned out to be the sweet spot between independence and impact.
So I studied design at university by day, and took on agency work by night—what you might call the full-stack student hustle. That rhythm—study, work, repeat—taught me discipline. I also kept learning on the side, exploring tools, trends, and techniques to sharpen my craft.
Eventually, I found myself gravitating toward fintech.
Why fintech? Because it’s real. It’s personal. Everyone interacts with money. And when you build something that helps them feel more in control of it—you’re not just improving UX, you’re improving lives.
You’re designing trust. That’s a responsibility I take seriously.
From there, I explored both sides of the industry: in-house roles at product companies, and fast-paced agency work. Later, I shifted into consultancy—partnering with fintechs across Europe, the Gulf, and Asia. That chapter taught me a lot—not just about design, but about people, culture, and how different teams think about trust and money.
All of that led me to start Moonsight—a space where I could bring all those experiences together. Today, we partner with fintechs and financial companies to create sharp, useful, brand-led digital experiences. And while I still stay hands-on, I’m also building a team that’s just as obsessed with clarity, thoughtfulness, and execution as I am.
Featured Work
Monetto
A game-changer in the world of freelancing. Designed to simplify and elevate the financial journey for freelancers, Monetto is more than just an app – it’s a holistic solution that empowers creatives like me to manage their finances with confidence.
BlastUp
Blastup’s mission is simple—help users grow their social media presence, fast. We crafted a bold, dynamic identity that reflects Blastup’s energetic and friendly personality as well is their website.
Alinma Bank
This project for Alinma Bank involved a comprehensive redesign across all brand touchpoints: the logo, physical cards, website, and mobile app. The goal was to modernize and streamline the visual identity while maintaining the bank’s core values.
Coinly
Coinly is more than just a banking app — it’s a full-fledged financial literacy ecosystem for kids, designed to empower the next generation with money skills that grow with them. Built around an engaging coin mascot and a colorful 3D world, Coinly blends gamification, interactive storytelling, and real financial tools.
Design Philosophy
Design should be highly functional and intuitive, solving both business and user problems while delivering an engaging experience that users want to return to.
Design is clarity. And clarity builds trust.
Especially in fintech—where most of my projects happen—you don’t have the luxury of vague. Your design has to work, first and foremost. It has to feel smart, trustworthy, smooth. When people trust your interface, they trust your product. And when they trust your product, they’re more likely to use it again. That’s where design really proves its value.
My job is to make things useful first, beautiful second. But ideally, both at once.
The way I approach projects is structured but adaptable.
I start with full immersion—understanding the business, the audience, and the problem we’re solving. From there, I look for a unique angle, something that gives the product or brand a distinct voice. Then I push that idea as far as I can—visually, functionally, and emotionally.
And no, I don’t believe in reinventing everything 🙂
Use the patterns that work. But when something feels off or underwhelming, be bold enough to rethink it. That’s where the real creative work lives—not in chaos, but in considered evolution.
I don’t want to be known for a style. I want to be known for range.
For every project, I try to find a distinct visual language. That means experimenting—pulling in 3D, motion, illustration—whatever it takes to bring the concept to life.
And I rarely do it alone.
I collaborate closely with animators, developers, motion designers, illustrators—the kind of people who not only support the vision, but expand it. When everyone brings their strengths to the table, the result is always richer, sharper, more memorable.
What matters most is that the end result has presence. That it feels alive, intentional, and built with care.
And I care deeply about how work is presented. Every project—client or personal—is framed with context, rationale, and craft. Because good design solves problems, but great design tells a story.
Process In Bits
My process is structured, but not rigid. Usually, it looks something like this:
Polish and present
Clear storytelling. Clean handoff. Confident rationale.Understand the business
What’s broken? What’s needed? What are we really solving?Understand the user
What do they expect? What’s familiar to them? What do they fear?Explore the visual angle
Moodboards, motion cues, layout patterns, unexpected directionsBuild and iterate
Fast feedback loops with clients and the teamOne benchmark I use: if I don’t understand what I designed, how can I expect a user to?
For me, good design starts with intention. Every screen, every button, every microinteraction—there should be a reason it exists. So when a feature’s built, I walk through it in my head as if I’ve never seen it before. What would I click? What would I expect next? Can I explain what each part does without second-guessing?
After working on financial interfaces for so long, you start to internalize these flows—you almost know them by muscle memory. But that doesn’t mean you skip the test. You still go through each stage. You still assume nothing.
Sometimes, the best insights come from a teammate asking, “Wait, what does this do?” That’s your cue to look closer.
And when it comes to working with clients?
I walk clients through every stage—from moodboards to microinteractions—so there are no surprises and no last-minute pivots.
It’s about mutual trust: they trust my process, and I trust their vision.
This structure helps me manage expectations, prevent scope drift, and deliver thoughtful work—on time, without the drama.
What keeps me inspired? Looking outside the bubble.
I don’t have a list of designers I religiously follow. What inspires me is great work—wherever it lives. Sometimes it’s a slick piece of web design, sometimes a brutalist poster on the street, art style from a video game, or the typography on a jazz record sleeve.
Music plays a huge role in my creative life—I sing a bit, and I think that kind of rhythm and structure naturally finds its way into how I build interfaces.
I’m also a huge gamer, and I’m fascinated by how game mechanics influence user behavior. There’s a lot designers can learn from how games guide, reward, and surprise users.
Sometimes I’ll see a cool effect, a character design, or even just a motion detail and immediately think:
That could be the anchor for a whole experience
Not necessarily for the project I’m working on in the moment, but something I’d love to build around later. So I sort, I collect, I sketch.
I’m often looking for inspiration for one project, but bookmarking ideas for two or three others. It’s not just moodboarding—it’s pattern recognition, and planting seeds for future concepts.
Inspiration can come from anywhere—but only if you keep your eyes open.
What’s Next
Right now, I’m fully focused on building Moonsight into a studio known for bold, strategic fintech design—especially across the MENA region.
On my personal radar:
- Master 3D
- Launch my own product
- Speak at more design events
- Make Moonsight’s design Conference in Dubai happen
- Join awwwards jury panel
- Do more meaningful work
- Mostly? Just grow. As a designer, a founder, and a creative
Parting Thoughts
If I could give one piece of advice to younger designers, it would be this:
Find what excites you. Stay obsessed with it. And don’t waste time comparing yourself to others.
We’re overexposed to each other’s work these days. It’s easy to feel behind.
But your only competition is yourself a year ago. That’s where growth lives.
This industry moves fast. But if you move with intent, your work will always find its place.
-
6.61 Million Google Clicks! 💸
Yesterday Online PNG Tools smashed through 6.60M Google clicks and today it’s smashed through 6.61M Google clicks! That’s 10,000 new clicks in a single day – the smash train keeps on rollin’!
What Are Online PNG Tools?
Online PNG Tools offers a collection of easy-to-use web apps that help you work with PNG images right in your browser. It’s like a Swiss Army Knife for anything PNG-related. On this site, you can create transparent PNGs, edit icons, clean up logos, crop stamps, change colors of signatures, and customize stickers – there’s a tool for it all. The best part is that you don’t need to install anything or be a graphic designer. All tools are made for regular people who just want to get stuff done with their images. No sign-ups, no downloads – just quick and easy PNG editing tools.
Who Created Online PNG Tools?
Online PNG Tools were created by me and my team at Browserling. We’ve build simple, browser-based tools that anyone can use without needing to download or install anything. Along with PNG tools, we also work on cross-browser testing to help developers make sure their websites work great on all web browsers. Our mission is to make online tools that are fast, easy to use, and that are helpful for everyday tasks like editing icons, logos, and signatures.
Who Uses Online PNG Tools?
Online PNG Tools and Browserling are used by everyone – from casual users to professionals and even Fortune 100 companies. Casual users often use them to make memes, edit profile pictures, or remove backgrounds. Professionals use them to clean up logos, design icons, or prepare images for websites and apps.
Smash too and see you tomorrow at 6.62M clicks! 📈
PS. Use coupon code
SMASHLING
for a 30% discount on these tools at onlinePNGtools.com/pricing. 💸 -
Three.js Instances: Rendering Multiple Objects Simultaneously
When building the basement studio site, we wanted to add 3D characters without compromising performance. We used instancing to render all the characters simultaneously. This post introduces instances and how to use them with React Three Fiber.
Introduction
Instancing is a performance optimization that lets you render many objects that share the same geometry and material simultaneously. If you have to render a forest, you’d need tons of trees, rocks, and grass. If they share the same base mesh and material, you can render all of them in a single draw call.
A draw call is a command from the CPU to the GPU to draw something, like a mesh. Each unique geometry or material usually needs its own call. Too many draw calls hurt performance. Instancing reduces that by batching many copies into one.
Basic instancing
As an example, let’s start by rendering a thousand boxes in a traditional way, and let’s loop over an array and generate some random boxes:
const boxCount = 1000 function Scene() { return ( <> {Array.from({ length: boxCount }).map((_, index) => ( <mesh key={index} position={getRandomPosition()} scale={getRandomScale()} > <boxGeometry /> <meshBasicMaterial color={getRandomColor()} /> </mesh> ))} </> ) }
Live | Source If we add a performance monitor to it, we’ll notice that the number of “calls” matches our
boxCount
.A quick way to implement instances in our project is to use drei/instances.
The
Instances
component acts as a provider; it needs a geometry and materials as children that will be used each time we add an instance to our scene.The
Instance
component will place one of those instances in a particular position/rotation/scale. EveryInstance
will be rendered simultaneously, using the geometry and material configured on the provider.import { Instance, Instances } from "@react-three/drei" const boxCount = 1000 function Scene() { return ( <Instances limit={boxCount}> <boxGeometry /> <meshBasicMaterial /> {Array.from({ length: boxCount }).map((_, index) => ( <Instance key={index} position={getRandomPosition()} scale={getRandomScale()} color={getRandomColor()} /> ))} </Instances> ) }
Notice how “calls” is now reduced to 1, even though we are showing a thousand boxes.
Live | Source What is happening here? We are sending the geometry of our box and the material just once to the GPU, and ordering that it should reuse the same data a thousand times, so all boxes are drawn simultaneously.
Notice that we can have multiple colors even though they use the same material because Three.js supports this. However, other properties, like the
map
, should be the same because all instances share the exact same material.We’ll see how we can hack Three.js to support multiple maps later in the article.
Having multiple sets of instances
If we are rendering a forest, we may need different instances, one for trees, another for rocks, and one for grass. However, the example from before only supports one instance in its provider. How can we handle that?
The
creteInstnace()
function from drei allows us to create multiple instances. It returns two React components, the first one a provider that will set up our instance, the second, a component that we can use to position one instance in our scene.Let’s see how we can set up a provider first:
import { createInstances } from "@react-three/drei" const boxCount = 1000 const sphereCount = 1000 const [CubeInstances, Cube] = createInstances() const [SphereInstances, Sphere] = createInstances() function InstancesProvider({ children }: { children: React.ReactNode }) { return ( <CubeInstances limit={boxCount}> <boxGeometry /> <meshBasicMaterial /> <SphereInstances limit={sphereCount}> <sphereGeometry /> <meshBasicMaterial /> {children} </SphereInstances> </CubeInstances> ) }
Once we have our instance provider, we can add lots of Cubes and Spheres to our scene:
function Scene() { return ( <InstancesProvider> {Array.from({ length: boxCount }).map((_, index) => ( <Cube key={index} position={getRandomPosition()} color={getRandomColor()} scale={getRandomScale()} /> ))} {Array.from({ length: sphereCount }).map((_, index) => ( <Sphere key={index} position={getRandomPosition()} color={getRandomColor()} scale={getRandomScale()} /> ))} </InstancesProvider> ) }
Notice how even though we are rendering two thousand objects, we are just running two draw calls on our GPU.
Live | Source Instances with custom shaders
Until now, all the examples have used Three.js’ built-in materials to add our meshes to the scene, but sometimes we need to create our own materials. How can we add support for instances to our shaders?
Let’s first set up a very basic shader material:
import * as THREE from "three" const baseMaterial = new THREE.RawShaderMaterial({ vertexShader: /*glsl*/ ` attribute vec3 position; attribute vec3 instanceColor; attribute vec3 normal; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; void main() { vec4 modelPosition = modelMatrix * vec4(position, 1.0); vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition; gl_Position = projectionPosition; } `, fragmentShader: /*glsl*/ ` void main() { gl_FragColor = vec4(1, 0, 0, 1); } ` }) export function Scene() { return ( <mesh material={baseMaterial}> <sphereGeometry /> </mesh> ) }
Now that we have our testing object in place, let’s add some movement to the vertices:
We’ll add some movement on the X axis using a
time and amplitude uniform
and use it to create a blob shape:const baseMaterial = new THREE.RawShaderMaterial({ // some unifroms uniforms: { uTime: { value: 0 }, uAmplitude: { value: 1 }, }, vertexShader: /*glsl*/ ` attribute vec3 position; attribute vec3 instanceColor; attribute vec3 normal; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; // Added this code to shift the vertices uniform float uTime; uniform float uAmplitude; vec3 movement(vec3 position) { vec3 pos = position; pos.x += sin(position.y + uTime) * uAmplitude; return pos; } void main() { vec3 blobShift = movement(position); vec4 modelPosition = modelMatrix * vec4(blobShift, 1.0); vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition; gl_Position = projectionPosition; } `, fragmentShader: /*glsl*/ ` void main() { gl_FragColor = vec4(1, 0, 0, 1); } `, }); export function Scene() { useFrame((state) => { // update the time uniform baseMaterial.uniforms.uTime.value = state.clock.elapsedTime; }); return ( <mesh material={baseMaterial}> <sphereGeometry args={[1, 32, 32]} /> </mesh> ); }
Now, we can see the sphere moving around like a blob:
Live | Source Now, let’s render a thousand blobs using instancing. First, we need to add the instance provider to our scene:
import { createInstances } from '@react-three/drei'; const [BlobInstances, Blob] = createInstances(); function Scene() { useFrame((state) => { baseMaterial.uniforms.uTime.value = state.clock.elapsedTime; }); return ( <BlobInstances material={baseMaterial} limit={sphereCount}> <sphereGeometry args={[1, 32, 32]} /> {Array.from({ length: sphereCount }).map((_, index) => ( <Blob key={index} position={getRandomPosition()} /> ))} </BlobInstances> ); }
The code runs successfully, but all spheres are in the same place, even though we added different positions.
This is happening because when we calculated the position of each vertex in the vertexShader, we returned the same position for all vertices, all these attributes are the same for all spheres, so they end up in the same spot:
vec3 blobShift = movement(position); vec4 modelPosition = modelMatrix * vec4(deformedPosition, 1.0); vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition; gl_Position = projectionPosition;
To solve this issue, we need to use a new attribute called
instanceMatrix
. This attribute will be different for each instance that we are rendering.attribute vec3 position; attribute vec3 instanceColor; attribute vec3 normal; attribute vec2 uv; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; // this attribute will change for each instance attribute mat4 instanceMatrix; uniform float uTime; uniform float uAmplitude; vec3 movement(vec3 position) { vec3 pos = position; pos.x += sin(position.y + uTime) * uAmplitude; return pos; } void main() { vec3 blobShift = movement(position); // we can use it to transform the position of the model vec4 modelPosition = instanceMatrix * modelMatrix * vec4(blobShift, 1.0); vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition; gl_Position = projectionPosition; }
Now that we have used the
instanceMatrix
attribute, each blob is in its corresponding position, rotation, and scale.Live | Source Changing attributes per instance
We managed to render all the blobs in different positions, but since the uniforms are shared across all instances, they all end up having the same animation.
To solve this issue, we need a way to provide custom information for each instance. We actually did this before, when we used the
instanceMatrix
to move each instance to its corresponding location. Let’s debug the magic behindinstanceMatrix
, so we can learn how we can create own instanced attributes.Taking a look at the implementation of
instancedMatrix
we can see that it is using something called InstancedAttribute:https://github.com/mrdoob/three.js/blob/master/src/objects/InstancedMesh.js#L57 InstancedBufferAttribute
allows us to create variables that will change for each instance. Let’s use it to vary the animation of our blobs.Drei has a component to simplify this called
InstancedAttribute
that allows us to define custom attributes easily.// Tell typescript about our custom attribute const [BlobInstances, Blob] = createInstances<{ timeShift: number }>() function Scene() { useFrame((state) => { baseMaterial.uniforms.uTime.value = state.clock.elapsedTime }) return ( <BlobInstances material={baseMaterial} limit={sphereCount}> {/* Declare an instanced attribute with a default value */} <InstancedAttribute name="timeShift" defaultValue={0} /> <sphereGeometry args={[1, 32, 32]} /> {Array.from({ length: sphereCount }).map((_, index) => ( <Blob key={index} position={getRandomPosition()} // Set the instanced attribute value for this instance timeShift={Math.random() * 10} /> ))} </BlobInstances> ) }
We’ll use this time shift attribute in our shader material to change the blob animation:
uniform float uTime; uniform float uAmplitude; // custom instanced attribute attribute float timeShift; vec3 movement(vec3 position) { vec3 pos = position; pos.x += sin(position.y + uTime + timeShift) * uAmplitude; return pos; }
Now, each blob has its own animation:
Live | Source Creating a forest
Let’s create a forest using instanced meshes. I’m going to use a 3D model from SketchFab: Stylized Pine Tree Tree by Batuhan13.
import { useGLTF } from "@react-three/drei" import * as THREE from "three" import { GLTF } from "three/examples/jsm/Addons.js" // I always like to type the models so that they are safer to work with interface TreeGltf extends GLTF { nodes: { tree_low001_StylizedTree_0: THREE.Mesh< THREE.BufferGeometry, THREE.MeshStandardMaterial > } } function Scene() { // Load the model const { nodes } = useGLTF( "/stylized_pine_tree_tree.glb" ) as unknown as TreeGltf return ( <group> {/* add one tree to our scene */ } <mesh scale={0.02} geometry={nodes.tree_low001_StylizedTree_0.geometry} material={nodes.tree_low001_StylizedTree_0.material} /> </group> ) }
(I added lights and a ground in a separate file.)
Now that we have one tree, let’s apply instancing.
const getRandomPosition = () => { return [ (Math.random() - 0.5) * 10000, 0, (Math.random() - 0.5) * 10000 ] as const } const [TreeInstances, Tree] = createInstances() const treeCount = 1000 function Scene() { const { scene, nodes } = useGLTF( "/stylized_pine_tree_tree.glb" ) as unknown as TreeGltf return ( <group> <TreeInstances limit={treeCount} scale={0.02} geometry={nodes.tree_low001_StylizedTree_0.geometry} material={nodes.tree_low001_StylizedTree_0.material} > {Array.from({ length: treeCount }).map((_, index) => ( <Tree key={index} position={getRandomPosition()} /> ))} </TreeInstances> </group> ) }
Our entire forest is being rendered in only three draw calls: one for the skybox, another one for the ground plane, and a third one with all the trees.
To make things more interesting, we can vary the height and rotation of each tree:
const getRandomPosition = () => { return [ (Math.random() - 0.5) * 10000, 0, (Math.random() - 0.5) * 10000 ] as const } function getRandomScale() { return Math.random() * 0.7 + 0.5 } // ... <Tree key={index} position={getRandomPosition()} scale={getRandomScale()} rotation-y={Math.random() * Math.PI * 2} /> // ...
Live | Source Further reading
There are some topics that I didn’t cover in this article, but I think they are worth mentioning:
- Batched Meshes: Now, we can render one geometry multiple times, but using a batched mesh will allow you to render different geometries at the same time, sharing the same material. This way, you are not limited to rendering one tree geometry; you can vary the shape of each one.
- Skeletons: They are not currently supported with instancing, to create the latest basement.studio site we managed to hack our own implementation, I invite you to read our implementation there.
- Morphing with batched mesh: Morphing is supported with instances but not with batched meshes. If you want to implement it yourself, I’d suggest you read these notes.
-
6.62 Million Google Clicks! 💸
Yesterday Online PNG Tools smashed through 6.61M Google clicks and today it’s smashed through 6.62M Google clicks! That’s 10,000 new clicks in a single day – the smash train keeps on rollin’!
What Are Online PNG Tools?
Online PNG Tools offers a collection of easy-to-use web apps that help you work with PNG images right in your browser. It’s like a Swiss Army Knife for anything PNG-related. On this site, you can create transparent PNGs, edit icons, clean up logos, crop stamps, change colors of signatures, and customize stickers – there’s a tool for it all. The best part is that you don’t need to install anything or be a graphic designer. All tools are made for regular people who just want to get stuff done with their images. No sign-ups, no downloads – just quick and easy PNG editing tools.
Who Created Online PNG Tools?
Online PNG Tools were created by me and my team at Browserling. We’ve build simple, browser-based tools that anyone can use without needing to download or install anything. Along with PNG tools, we also work on cross-browser testing to help developers make sure their websites work great on all web browsers. Our mission is to make online tools that are fast, easy to use, and that are helpful for everyday tasks like editing icons, logos, and signatures.
Who Uses Online PNG Tools?
Online PNG Tools and Browserling are used by everyone – from casual users to professionals and even Fortune 100 companies. Casual users often use them to make memes, edit profile pictures, or remove backgrounds. Professionals use them to clean up logos, design icons, or prepare images for websites and apps.
Smash too and see you tomorrow at 6.63M clicks! 📈
PS. Use coupon code
SMASHLING
for a 30% discount on these tools at onlinePNGtools.com/pricing. 💸 -
Try a Virtual Browser! (For Free!)
TLDR: You can browse the Internet safely and anonymously using a virtual browser at browserling.com/browse. It runs in your browser, so there’s nothing to download or install.
What Is a Virtual Browser?
It’s a real browser running on a remote machine that you control through your browser. Everything you do happens on a secure server, so your device never touches the website directly.
Is It Safe to Visit Weird Websites With It?
Yes, because the risky stuff stays on the remote machine, not your own. Malware, pop-ups, viruses, and trackers never get near your real computer.
Can I Test Suspicious Links With It?
Yes, it’s made for testing suspicious URLs without risking your system.
Just paste the link into the virtual browser and see what it does.Can I Open Dangerous Email Attachments?
Yes, you can upload attachments to the virtual browser and open them there. This helps protect your actual files and avoids infecting your computer with malware hidden in shady attachments.
Is It Good for Cybersecurity Testing?
Totally. Virtual browsers are often used in security testing, link analysis, sandboxing, and checking how websites behave under different conditions without exposing a real system.
How Is This Different From Incognito Mode?
Incognito just hides your history. It doesn’t protect you from viruses or sketchy websites. A virtual browser, on the other hand, acts like a shield, running everything remotely and keeping your device safe.
Do I Need to Install Anything?
Nope, it works straight from your browser. Just open a virtual browser in your browser and start browsing!
Can It Help With Online Privacy?
Absolutely. Since all browsing happens on a remote server, your IP address, cookies, and local data are never exposed to the sites you visit.
Can I Use It to Test Different Browsers?
Yeah, you can pick Chrome, Firefox, Edge, Safari, and others. It’s super helpful for developers, QA testers, or curious users who want to see how sites look in different browsers.
Is It Free?
There’s a free version with limited time, and paid plans for more features. If you just need quick tests or occasional safe browsing, the free plan is usually enough.
Is It On GitHub?
Absolutely. You can contribute to virtual browser repository on GitHub.
What Is Browserling?
Browserling is a virtual browser service that lets you use real web browsers on other computers, right from your own browser. It’s great for testing websites or visiting stuff safely without messing up your device.
Who Uses Browserling?
Browserling is a popular virtual browser tool used by people in tech, like cybersecurity pros, IT teams, and even researchers who check out the dark web. It’s trusted by millions of users every month, including big names like banks, governments, schools, news sites, and huge companies around the world.
Happy browsing!
-
How To Create Kinetic Image Animations with React-Three-Fiber
For the past few months, I’ve been exploring different kinetic motion designs with text and images. The style looks very intriguing, so I decided to create some really cool organic animations using images and React Three Fiber.
In this article, we’ll learn how to create the following animation using Canvas2D and React Three Fiber.
Setting Up the View & Camera
The camera’s field of view (FOV) plays a huge role in this project. Let’s keep it very low so it looks like an orthographic camera. You can experiment with different perspectives later. I prefer using a perspective camera over an orthographic one because we can always try different FOVs. For more detailed implementation check source code.
<PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} />
Setting Up Our 3D Shapes
First, let’s create and position 3D objects that will display our images. For this example, we need to make 2 components:
Billboard.tsx – This is a cylinder that will show our stack of images
'use client'; import { useRef } from 'react'; import * as THREE from 'three'; function Billboard({ radius = 5, ...props }) { const ref = useRef(null); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, 2, 100, 1, true]} /> <meshBasicMaterial color="red" side={THREE.DoubleSide} /> </mesh> ); }
Banner.tsx – This is another cylinder that will work like a moving banner
'use client'; import * as THREE from 'three'; import { useRef } from 'react'; function Banner({ radius = 1.6, ...props }) { const ref = useRef(null); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, radius * 0.07, radius * 80, radius * 10, true]} /> <meshBasicMaterial color="blue" side={THREE.DoubleSide} /> </mesh> ); } export default Banner;
Once we have our components ready, we can use them on our page.
Now let’s build the whole shape:
1. Create a wrapper group – We’ll make a group that wraps all our components. This will help us rotate everything together later.
page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; export default function Home() { return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} /> <group> </group> </View> </div> ); }
2. Render Billboard and Banner components in the loop – Inside our group, we’ll create a loop to render our Billboards and Banners multiple times.
page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; export default function Home() { return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} /> <group> {Array.from({ length: COUNT }).map((_, index) => [ <Billboard key={`billboard-${index}`} radius={5} />, <Banner key={`banner-${index}`} radius={5} />, ])} </group> </View> </div> ); }
3. Stack them up – We’ll use the index from our loop and the y position to stack our items on top of each other. Here’s how it looks so far:
page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; const COUNT = 10; const GAP = 3.2; export default function Home() { return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} /> <group> {Array.from({ length: COUNT }).map((_, index) => [ <Billboard key={`billboard-${index}`} radius={5} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP, 0]} />, <Banner key={`banner-${index}`} radius={5} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP - GAP * 0.5, 0]} />, ])} </group> </View> </div> ); }
4. Add some rotation – Let’s rotate things a bit! First, I’ll hard-code the rotation of our banners to make them more curved and fit nicely with the Billboard component. We’ll also make the radius a bit bigger.
page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; const COUNT = 10; const GAP = 3.2; export default function Home() { return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} /> <group> {Array.from({ length: COUNT }).map((_, index) => [ <Billboard key={`billboard-${index}`} radius={5} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP, 0]} rotation={[0, index * Math.PI * 0.5, 0]} // <-- rotation of the billboard />, <Banner key={`banner-${index}`} radius={5} rotation={[0, 0, 0.085]} // <-- rotation of the banner position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP - GAP * 0.5, 0]} />, ])} </group> </View> </div> ); }
5. Tilt the whole thing – Now let’s rotate our entire group to make it look like the Leaning Tower of Pisa.
page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; const COUNT = 10; const GAP = 3.2; export default function Home() { return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 70]} near={0.01} far={100000} /> <group rotation={[-0.15, 0, -0.2]}> // <-- rotate the group {Array.from({ length: COUNT }).map((_, index) => [ <Billboard key={`billboard-${index}`} radius={5} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP, 0]} rotation={[0, index * Math.PI * 0.5, 0]} />, <Banner key={`banner-${index}`} radius={5} rotation={[0, 0, 0.085]} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP - GAP * 0.5, 0]} />, ])} </group> </View> </div> ); }
6. Perfect! – Our 3D shapes are all set up. Now we can add our images to them.
Creating a Texture from Our Images Using Canvas
Here’s the cool part: we’ll put all our images onto a canvas, then use that canvas as a texture on our Billboard shape.
To make this easier, I created some helper functions that simplify the whole process.
getCanvasTexture.js
import * as THREE from 'three'; /** * Preloads an image and calculates its dimensions */ async function preloadImage(imageUrl, axis, canvasHeight, canvasWidth) { const img = new Image(); img.crossOrigin = 'anonymous'; await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`)); img.src = imageUrl; }); const aspectRatio = img.naturalWidth / img.naturalHeight; let calculatedWidth; let calculatedHeight; if (axis === 'x') { // Horizontal layout: scale to fit canvasHeight calculatedHeight = canvasHeight; calculatedWidth = canvasHeight * aspectRatio; } else { // Vertical layout: scale to fit canvasWidth calculatedWidth = canvasWidth; calculatedHeight = canvasWidth / aspectRatio; } return { img, width: calculatedWidth, height: calculatedHeight }; } function calculateCanvasDimensions(imageData, axis, gap, canvasHeight, canvasWidth) { if (axis === 'x') { const totalWidth = imageData.reduce( (sum, data, index) => sum + data.width + (index > 0 ? gap : 0), 0); return { totalWidth, totalHeight: canvasHeight }; } else { const totalHeight = imageData.reduce( (sum, data, index) => sum + data.height + (index > 0 ? gap : 0), 0); return { totalWidth: canvasWidth, totalHeight }; } } function setupCanvas(canvasElement, context, dimensions) { const { totalWidth, totalHeight } = dimensions; const devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2); canvasElement.width = totalWidth * devicePixelRatio; canvasElement.height = totalHeight * devicePixelRatio; if (devicePixelRatio !== 1) context.scale(devicePixelRatio, devicePixelRatio); context.fillStyle = '#ffffff'; context.fillRect(0, 0, totalWidth, totalHeight); } function drawImages(context, imageData, axis, gap) { let currentX = 0; let currentY = 0; context.save(); for (const data of imageData) { context.drawImage(data.img, currentX, currentY, data.width, data.height); if (axis === 'x') currentX += data.width + gap; else currentY += data.height + gap; } context.restore(); } function createTextureResult(canvasElement, dimensions) { const texture = new THREE.CanvasTexture(canvasElement); texture.needsUpdate = true; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.ClampToEdgeWrapping; texture.generateMipmaps = false; texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; return { texture, dimensions: { width: dimensions.totalWidth, height: dimensions.totalHeight, aspectRatio: dimensions.totalWidth / dimensions.totalHeight, }, }; } export async function getCanvasTexture({ images, gap = 10, canvasHeight = 512, canvasWidth = 512, canvas, ctx, axis = 'x', }) { if (!images.length) throw new Error('No images'); // Create canvas and context if not provided const canvasElement = canvas || document.createElement('canvas'); const context = ctx || canvasElement.getContext('2d'); if (!context) throw new Error('No context'); // Preload all images in parallel const imageData = await Promise.all( images.map((image) => preloadImage(image.url, axis, canvasHeight, canvasWidth)) ); // Calculate total canvas dimensions const dimensions = calculateCanvasDimensions(imageData, axis, gap, canvasHeight, canvasWidth); // Setup canvas setupCanvas(canvasElement, context, dimensions); // Draw all images drawImages(context, imageData, axis, gap); // Create and return texture result return createTextureResult(canvasElement, dimensions) }
Then we can also create a
useCollageTexture
hook that we can easily use in our components.useCollageTexture.jsx
import { useState, useEffect, useCallback } from 'react'; import { getCanvasTexture } from '@/webgl/helpers/getCanvasTexture'; export function useCollageTexture(images, options = {}) { const [textureResults, setTextureResults] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { gap = 0, canvasHeight = 512, canvasWidth = 512, axis = 'x' } = options; const createTexture = useCallback(async () => { try { setIsLoading(true); setError(null); const result = await getCanvasTexture({ images, gap, canvasHeight, canvasWidth, axis, }); setTextureResults(result); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to create texture')); } finally { setIsLoading(false); } }, [images, gap, canvasHeight, canvasWidth, axis]); useEffect(() => { if (images.length > 0) createTexture(); }, [images.length, createTexture]); return { texture: textureResults?.texture || null, dimensions: textureResults?.dimensions || null, isLoading, error, }; }
Adding the Canvas to Our Billboard
Now let’s use our
useCollageTexture
hook on our page. We’ll create some simple loading logic. It takes a second to fetch all the images and put them onto the canvas. Then we’ll pass our texture and dimensions of canvas into the Billboard component.page.jsx
'use client'; import styles from './page.module.scss'; import Billboard from '@/components/webgl/Billboard/Billboard'; import Banner from '@/components/webgl/Banner/Banner'; import Loader from '@/components/ui/modules/Loader/Loader'; import images from '@/data/images'; import { View } from '@/webgl/View'; import { PerspectiveCamera } from '@react-three/drei'; import { useCollageTexture } from '@/hooks/useCollageTexture'; const COUNT = 10; const GAP = 3.2; export default function Home() { const { texture, dimensions, isLoading } = useCollageTexture(images); // <-- getting the texture and dimensions from the useCollageTexture hook if (isLoading) return <Loader />; // <-- showing the loader when the texture is loading return ( <div className={styles.page}> <View className={styles.view} orbit={false}> <PerspectiveCamera makeDefault fov={7} position={[0, 0, 100]} near={0.01} far={100000} /> <group rotation={[-0.15, 0, -0.2]}> {Array.from({ length: COUNT }).map((_, index) => [ <Billboard key={`billboard-${index}`} radius={5} rotation={[0, index * Math.PI * 0.5, 0]} position={[0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP, 0]} texture={texture} // <--passing the texture to the billboard dimensions={dimensions} // <--passing the dimensions to the billboard />, <Banner key={`banner-${index}`} radius={5.035} rotation={[0, 0, 0.085]} position={[ 0, (index - (Math.ceil(COUNT / 2) - 1)) * GAP - GAP * 0.5, 0, ]} />, ])} </group> </View> </div> ); }
Inside the Billboard component, we need to properly map this texture to make sure everything fits correctly. The width of our canvas will match the circumference of the cylinder, and we’ll center the y position of the texture. This way, all the images keep their resolution and don’t get squished or stretched.
Billboard.jsx
'use client'; import * as THREE from 'three'; import { useRef } from 'react'; function setupCylinderTextureMapping(texture, dimensions, radius, height) { const cylinderCircumference = 2 * Math.PI * radius; const cylinderHeight = height; const cylinderAspectRatio = cylinderCircumference / cylinderHeight; if (dimensions.aspectRatio > cylinderAspectRatio) { // Canvas is wider than cylinder proportionally texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio; texture.repeat.y = 1; texture.offset.x = (1 - texture.repeat.x) / 2; } else { // Canvas is taller than cylinder proportionally texture.repeat.x = 1; texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio; } // Center the texture texture.offset.y = (1 - texture.repeat.y) / 2; } function Billboard({ texture, dimensions, radius = 5, ...props }) { const ref = useRef(null); setupCylinderTextureMapping(texture, dimensions, radius, 2); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, 2, 100, 1, true]} /> <meshBasicMaterial map={texture} side={THREE.DoubleSide} /> </mesh> ); } export default Billboard;
Now let’s animate them using the
useFrame
hook. The trick to animating these images is to just move the X offset of the texture. This gives us the effect of a rotating mesh, when really we’re just moving the texture offset.Billboard.jsx
'use client'; import * as THREE from 'three'; import { useRef } from 'react'; import { useFrame } from '@react-three/fiber'; function setupCylinderTextureMapping(texture, dimensions, radius, height) { const cylinderCircumference = 2 * Math.PI * radius; const cylinderHeight = height; const cylinderAspectRatio = cylinderCircumference / cylinderHeight; if (dimensions.aspectRatio > cylinderAspectRatio) { // Canvas is wider than cylinder proportionally texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio; texture.repeat.y = 1; texture.offset.x = (1 - texture.repeat.x) / 2; } else { // Canvas is taller than cylinder proportionally texture.repeat.x = 1; texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio; } // Center the texture texture.offset.y = (1 - texture.repeat.y) / 2; } function Billboard({ texture, dimensions, radius = 5, ...props }) { const ref = useRef(null); setupCylinderTextureMapping(texture, dimensions, radius, 2); useFrame((state, delta) => { if (texture) texture.offset.x += delta * 0.001; }); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, 2, 100, 1, true]} /> <meshBasicMaterial map={texture} side={THREE.DoubleSide} /> </mesh> ); } export default Billboard;
I think it would look even better if we made the back of the images a little darker. To do this, I created
MeshImageMaterial
– it’s just an extension ofMeshBasicMaterial
that makes our backface a bit darker.MeshImageMaterial.js
import * as THREE from 'three'; import { extend } from '@react-three/fiber'; export class MeshImageMaterial extends THREE.MeshBasicMaterial { constructor(parameters = {}) { super(parameters); this.setValues(parameters); } onBeforeCompile = (shader) => { shader.fragmentShader = shader.fragmentShader.replace( '#include <color_fragment>', /* glsl */ `#include <color_fragment> if (!gl_FrontFacing) { vec3 blackCol = vec3(0.0); diffuseColor.rgb = mix(diffuseColor.rgb, blackCol, 0.7); } ` ); }; } extend({ MeshImageMaterial });
Billboard.jsx
'use client'; import * as THREE from 'three'; import { useRef } from 'react'; import { useFrame } from '@react-three/fiber'; import '@/webgl/materials/MeshImageMaterial'; function setupCylinderTextureMapping(texture, dimensions, radius, height) { const cylinderCircumference = 2 * Math.PI * radius; const cylinderHeight = height; const cylinderAspectRatio = cylinderCircumference / cylinderHeight; if (dimensions.aspectRatio > cylinderAspectRatio) { // Canvas is wider than cylinder proportionally texture.repeat.x = cylinderAspectRatio / dimensions.aspectRatio; texture.repeat.y = 1; texture.offset.x = (1 - texture.repeat.x) / 2; } else { // Canvas is taller than cylinder proportionally texture.repeat.x = 1; texture.repeat.y = dimensions.aspectRatio / cylinderAspectRatio; } // Center the texture texture.offset.y = (1 - texture.repeat.y) / 2; } function Billboard({ texture, dimensions, radius = 5, ...props }) { const ref = useRef(null); setupCylinderTextureMapping(texture, dimensions, radius, 2); useFrame((state, delta) => { if (texture) texture.offset.x += delta * 0.001; }); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, 2, 100, 1, true]} /> <meshImageMaterial map={texture} side={THREE.DoubleSide} toneMapped={false} /> </mesh> ); } export default Billboard;
And now we have our images moving around cylinders. Next, we’ll focus on banners (or marquees, whatever you prefer).
Adding Texture to the Banner
The last thing we need to fix is our Banner component. I wrapped it with this texture. Feel free to take it and edit it however you want, but remember to keep the proper dimensions of the texture.
We simply import our texture using the
useTexture
hook, map it onto our material, and animate the texture offset just like we did in our Billboard component.Billboard.jsx
'use client'; import * as THREE from 'three'; import bannerTexture from '@/assets/images/banner.jpg'; import { useTexture } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import { useRef } from 'react'; function Banner({ radius = 1.6, ...props }) { const ref = useRef(null); const texture = useTexture(bannerTexture.src); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; useFrame((state, delta) => { if (!ref.current) return; const material = ref.current.material; if (material.map) material.map.offset.x += delta / 30; }); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, radius * 0.07, radius * 80, radius * 10, true]} /> <meshBasicMaterial map={texture} map-anisotropy={16} map-repeat={[15, 1]} side={THREE.DoubleSide} toneMapped={false} backfaceRepeatX={3} /> </mesh> ); } export default Banner;
Nice! Now we have something cool, but I think it would look even cooler if we replaced the backface with something different. Maybe a gradient? For this, I created another extension of
MeshBasicMaterial
calledMeshBannerMaterial
. As you probably guessed, we just put a gradient on the backface. That’s it! Let’s use it in our Banner component.We replace the
MeshBasicMaterial
withMeshBannerMaterial
and now it looks like this!MeshBannerMaterial.js
import * as THREE from 'three'; import { extend } from '@react-three/fiber'; export class MeshBannerMaterial extends THREE.MeshBasicMaterial { constructor(parameters = {}) { super(parameters); this.setValues(parameters); this.backfaceRepeatX = 1.0; if (parameters.backfaceRepeatX !== undefined) this.backfaceRepeatX = parameters.backfaceRepeatX; } onBeforeCompile = (shader) => { shader.uniforms.repeatX = { value: this.backfaceRepeatX * 0.1 }; shader.fragmentShader = shader.fragmentShader .replace( '#include <common>', /* glsl */ `#include <common> uniform float repeatX; vec3 pal( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) { return a + b*cos( 6.28318*(c*t+d) ); } ` ) .replace( '#include <color_fragment>', /* glsl */ `#include <color_fragment> if (!gl_FrontFacing) { diffuseColor.rgb = pal(vMapUv.x * repeatX, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,1.0),vec3(0.0,0.10,0.20) ); } ` ); }; } extend({ MeshBannerMaterial });
Banner.jsx
'use client'; import * as THREE from 'three'; import bannerTexture from '@/assets/images/banner.jpg'; import { useTexture } from '@react-three/drei'; import { useFrame } from '@react-three/fiber'; import { useRef } from 'react'; import '@/webgl/materials/MeshBannerMaterial'; function Banner({ radius = 1.6, ...props }) { const ref = useRef(null); const texture = useTexture(bannerTexture.src); texture.wrapS = texture.wrapT = THREE.RepeatWrapping; useFrame((state, delta) => { if (!ref.current) return; const material = ref.current.material; if (material.map) material.map.offset.x += delta / 30; }); return ( <mesh ref={ref} {...props}> <cylinderGeometry args={[radius, radius, radius * 0.07, radius * 80, radius * 10, true]} /> <meshBannerMaterial map={texture} map-anisotropy={16} map-repeat={[15, 1]} side={THREE.DoubleSide} toneMapped={false} backfaceRepeatX={3} /> </mesh> ); } export default Banner;
And now we have it ✨
Check out the demo You can experiment with this method in lots of ways. For example, I created 2 more examples with shapes I made in Blender, and mapped canvas textures on them. You can check them out here:
Final Words
Check out the final versions of all demos:
I hope you enjoyed this tutorial and learned something new!
Feel free to check out the source code for more details!
-
Bloom Paris TV: Where Refined Art Direction Meets World-Class Production
In today’s oversaturated landscape of production service companies vying for attention, Bloom Paris TV approached our studio with an extraordinarily bold ambition: to distinguish themselves through an uncompromising combination of refined style and substantive expertise. Strategically positioned in the cultural and creative heart of Paris, Bloom offers international productions comprehensive and seamless on-the-ground support throughout France — meticulously handling everything from complex technical logistics to complex administrative workflows and regulatory requirements.
But what truly sets Bloom apart is that they don’t merely facilitate shoots — they orchestrate them with exceptional precision, artistic vision, and unwavering reliability. In an industry where every minute counts, their discerning clients demand speed without sacrificing quality, complete trust in execution, and uncompromising excellence at every touchpoint. Bloom consistently delivers all three elements — seemingly effortlessly and with characteristic French sophistication.
Our mission became crystal clear: design and develop a digital experience that authentically reflects the remarkable sharpness of their creative eye, the methodical structure of their production process, and the sophisticated elegance of their flawless execution across every project they undertake.
The Concept
We approached the website design with one unambiguous and defining intention: make an immediate, memorable impact upon first impression.
Operating in a fast-paced industry where critical decisions are often made in mere seconds, we recognized that the digital experience needed to be simultaneously bold, fluid, and instantaneously engaging. Our strategic approach centered on minimalism with deliberate intent — methodically stripping away all superfluous elements while preserving only the absolute essentials, then thoughtfully amplifying Bloom’s distinctive core identity throughout the interface.
At the conceptual heart of Bloom’s sophisticated logo lies a deceptively simple dot — subtle in appearance yet powerful in significance. We strategically extended this symbolic element across the entire user interface: integrating it within interactive buttons, intuitive navigation elements, typographic superscripts, and responsive interaction states. This visual motif evolved into the unifying thread throughout the experience, functioning as a recurring punctuation mark that guides users through a clean, cinematic narrative journey.
Typography & Color System
After careful consideration, we selected a commanding, contemporary sans-serif typeface specifically chosen to convey professional confidence and exceptional clarity. This distinctive font effectively anchors the entire site within a precisely calibrated, almost editorial layout structure — creating a harmonious balance between the dynamically asymmetric grid system and the meticulously structured, authoritative tone of voice that characterizes Bloom’s communication style.
The carefully curated color palette features a sophisticated high-contrast dialogue between rich soft black and warm, inviting light grey, consciously avoiding the harshness of traditional monochrome combinations. A strategically placed vibrant yellow accent punctuates key interactive elements throughout the interface — subtly referencing cinematic film titles and professional cue markers, while simultaneously introducing a welcome sense of warmth, energy and approachability to the otherwise restrained interface design.
Technology Stack
Beneath the visually striking surface, the site is meticulously constructed with a powerful combination of technologies:
- WordPress implemented as a robust, infinitely customizable content management system, providing Bloom with comprehensive control over their content strategy and presentation
- GSAP for implementing buttery-smooth, cinematically-inspired animations and seamless page transitions throughout the experience
- Custom-developed SVG masking techniques meticulously crafted to achieve elegantly seamless panel-based transitions between content sections
- A fully responsive, thoroughly performance-optimized front-end architecture that ensures consistent excellence across all devices and connection speeds
Loader & Page Transitions
From the earliest conceptual discussions, we were determined to ensure that every transition moment within the experience would feel authentically cinematic and emotionally resonant.
Each individual page opens with a dynamically animated panel that dramatically reveals the upcoming section title with a sweeping, theatrical gesture. This carefully choreographed visual sequence not only significantly enhances user orientation within the site architecture, but deliberately sets the sophisticated tone for a fluid, immersive journey through Bloom’s professional world.
The distinctive homepage loader was specifically designed to create instant emotional resonance and connection: a fullscreen mask elegantly opens to dramatically reveal Bloom’s captivating showreel — creating an unforgettable first impression that immediately communicates their production capabilities. Thoughtfully combined with an interactive progress indicator, this element transforms into an engaging interactive curtain, gracefully inviting users to step into Bloom’s compelling narrative universe.
Project Grid & Hover States
Throughout the portfolio section, Bloom’s impressive projects are presented within a sophisticated asymmetric editorial grid structure, deliberately breaking the predictable monotony of conventional layouts while thoughtfully echoing the dynamic rhythm of visual storytelling. Individual content sizes and positions shift intuitively throughout the composition, creating intentional moments of both contemplative pause and energetic flow.
During user interaction, the signature dot elegantly reappears as an intuitive focus indicator, while a smoothly animated marquee title gracefully glides over the preview image — simultaneously drawing attention and adding perceptual depth to the experience. This carefully considered combination creates a remarkably tactile, multi-layered effect that meaningfully rewards user interaction without overwhelming the visual hierarchy or distracting from the exceptional quality of Bloom’s project portfolio.
Footer
Thoughtfully designed as a final memorable touchpoint rather than an afterthought, the site’s footer functions as much more than a mere sign-off — it serves as an compelling invitation to further engagement.
The footer section artfully reprises elements from the initial showreel presentation, elegantly contained within a precisely masked frame that maintains consistent visual language throughout the experience. Both functionally informative and poetically expressive, this distinctive footer ensures that Bloom’s powerful brand experience lingers in the user’s memory — even long after the final scroll action concludes their immediate journey.
Who We Are
We proudly define ourselves as a specialized digital design studio operating at the fascinating intersection of compelling narrative, intuitive interaction design, and cutting-edge technology implementation. We fundamentally believe in the transformative power of crafting digital interfaces that move with deliberate intention and purpose, thoughtfully combining minimalist aesthetic principles with boldly distinctive creative identity expressions.
With each project we undertake, we consistently strive to create memorable digital experiences that communicate with exceptional clarity, move with captivating beauty, and feel genuinely alive and responsive to human interaction.
-
Try an Online Browser! (For Free!)
TLDR: You can get instant access to an online browser at browserling.com/browse. It runs entirely in your own browser. No downloads, no installs.
What’s An Online Browser?
It’s a browser hosted elsewhere, streamed to you in real time. You use it like any regular browser, but it runs safely outside your device on a remote server.
Is It Safe For Sketchy Sites?
Absolutely. Any harmful scripts or shady behavior stay isolated on the remote machine. Your computer stays untouched and safe from viruses, malware, and phishing traps.
Can I Test Suspicious Links?
Yes, you can open any link inside an online browser without risking your own device. Using an online browser is one of the safest ways to check unknown URLs, especially if you’re worried about phishing or malware.
What About Email Attachments?
You can use an online browser to open files or attachments from emails without downloading them locally. This is a smart trick for checking PDFs or Office files that might contain malicious scripts.
Is It Good For Cybersecurity?
Absolutely. Online browsers are a big help for threat hunters and analysts. They let you investigate risky sites, test exploits, and open shady content without ever touching your network.
Do I Need To Install Anything?
No installation needed. It works instantly in your browser. Just click and go. No plugins, no setup, nothing to configure.
Can I Test Different Browsers?
Yes! You can choose from Chrome, Firefox, Edge, Safari, and more to test how sites look and behave across platforms. This is super useful for developers checking cross-browser compatibility, or QA testers fixing layout bugs.
Is It Free?
There’s a free version with time limits, and paid plans that unlock full access and extra features. The free plan is good for quick tasks, and the premium plans are built for teams, security testing, and daily use.
Is It On GitHub?
Yes. You can contribute to online browser repository on GitHub.
What Is Browserling?
Browserling is an online browser service that gives you instant access to real browsers running on remote systems. It’s made for testing, development, and secure browsing.
Who Uses Browserling?
Tech professionals around the world rely on Browserling. From cybersecurity experts and IT teams to cybersecurity experts exploring high-risk parts of the web. It’s trusted by millions each month, including major banks, universities, media outlets, government agencies, and Fortune 100 companies.
Happy browsing!
-
Try a Browser Sandbox! (For Free!)
TLDR: Want to browse the web safely without messing up your computer? Try a browser sandbox at browserling.com/browse. It runs straight in your browser. No installs, no downloads.
What’s a Browser Sandbox?
A browser sandbox is like a “browser inside a browser”. It runs on another computer in the cloud, and you control it from your own screen. You get to surf the web, but the websites never touch your actual device.
Is It Safe to Use?
Yep! You can click on sketchy links or check out weird websites without any risk. All the dangerous stuff stays far away – on the remote computer, not yours. Even if a site tries to install a virus or download something, it won’t reach your actual system.
Can I Open Suspicious Emails Safely?
Yes, with a browser sandbox you can open sketchy emails or attachments without danger. If the attachment contains malware, it gets trapped inside the sandbox and can’t harm your real device.
What About Testing Suspicious URLs?
Absolutely. A browser sandbox is the safest way to test unknown URLs. It keeps malicious scripts, drive-by downloads, and tracking attempts locked away from your real system.
Can I Use It for Digital Forensics?
Yes, browser sandboxes are super useful for digital forensics work. Investigators can safely open phishing emails, suspicious websites, or malware links without risking their machines or leaking any data.
Do I Need to Download Anything?
Nope. Just open the sandbox, pick a browser, and start browsing. It’s that easy. Everything runs in your web browser via HTML5, JavaScript, and WebSockets, so there’s no software setup or weird permissions needed.
Can I Try Different Browsers?
Totally. You can switch between Chrome, Firefox, Edge, Safari, and even older versions if you’re testing an exploit that detonates in a particular browser version. This makes it useful for developers, bug bounty hunters, and cybersecurity researchers.
Is It Free?
There’s a free version with limited time. If you need more time or features, then there are paid plans too. The paid plans offer longer sessions, more browsers, and even persistent browser sessions.
What Is Browserling?
Browserling is an online tool that gives you access to real sandboxed browsers running on remote machines. Its use cases include safe browsing, testing websites in different browsers, and opening suspicious files and PDFs.
Who Uses Browserling?
Millions of people! Tech experts, digital forensics teams, IT departments, schools, and even government workers use Browserling. Big companies and researchers trust it too. Especially when checking out risky sites or testing code in different browsers.
Happy browsing!