We ship every week, so you can ship more great designs.
Everyone's talking about the cliche things like developing an application, software, fixing bugs, merging PRs etc etc... Who doesn't need a change, huh ! Everyone's impressed by the movie named Avatar [ just a hype ], right ? For a change, why don't you try something creative like that, I mean, obviously in a small scale, really small scale ?
I am talking about the same 3D thing that you have in mind. Enter BabylonJS - a cool, suave 3D Engine.
Babylon.js is a real time Javascript 3D engine for displaying 3D graphics in the browser s via HTML5. The source code is distributed under the Apache License 2.0, available on GitHub. It was initially released in 2013 under Microsoft Public License. It was initially developed by two Microsoft employees. David Catuhe created the 3D game engine and was helped by David Rousset (VR, Gamepad and IndexedDB support) mostly in their free time as a side-project. They were also helped by artist Michel Rousseau who contributed several 3D scenes.
With 18.6K stars, 547 watching and 2.9K forks, Babylonjs is rocking the 3D Engine world.
The source code is written in TypeScript, compiled into a JavaScript version. The JavaScript version is available to end users via NPM or CDN. The Babylon.js 3D engine make use of WebGL for the 3D rendering.
The created models are making use of a shader program, determinig the pixel positions and colors on the canvas on which the user rendered, using polygon models, textures, camera and lights along with 4 x 4 world matrices for each of the objects storing their positions, rotation and scaling. Producing photo realistic images is done using the same method of physically based rendering along with post-processing ones. For simulating collisions, either Cannon.js or Oimo has to be plugged in to BabylonJS. Animations are done using key frame methods called animatables. The full character animation is done using skeletons with blend weights.
The real kick comes when we render a scene on the canvas with some cool interactions. Before that, let's see the workflow of a Babylon project.
In Babylon, everything works inside a canvas. For a canvas to render stuff, there should be a rendering engine. For that, we use Babylon’s Rendering Engine. So basically the flow would be Babylon Engine -> Canvas -> What we see
Lets code...
First, We would like to type safe our initial variables like scene, engine, camera etc.
private scene: BABYLON.Scene;
private engine: BABYLON.Engine;
private canvas: HTMLCanvasElement;
Then in the useEffect (for Reactjs and Nextjs), we can give actual values to those variables.
useEffect(() => {
engine = new BABYLON.Engine(canvas, {...options});
scene = new BABYLON.Scene(engine);
engine.runRenderLoop(() => {
scene.render();
})
})
// canvas is selected using an id attribute
If you run the program now, you will see a white screen in the browser. Why? Because we are rendering nothing. Lets add a camera, we need one, don't you think ?
const camera = new BABYLON.FreeCamera('camera',
new BABYLON.Vector3(0, 5, -10),
scene
);
What we did was:
Now the camera has been defined. But nothing will appear if there is no light. Yeah.. thats right. You have to specify everything. Lets attach a light.
const light = new BABYLON.HemisphericLight('light',
new BABYLON.Vector3(0, 1, 0),
scene
);
What we did was:
There are many attributes for all the stuff that we have done so far. There is intensity of the light, if you want to adjust the intensity of the same, default is 1.0. Likewise, all the attributes of camera/light/scene etc can be changed according to our needs.
For the sake of the 3D creation excitement, lets create a sphere. Hey, one sec. If you are gonna create a sphere, where are you going to place it? In the vacuum??? Go, let's create a ground first. I think now you got what I meant by specifying whatever we need. Okay, First create a ground and then a sphere.
const ground = BABYLON.MeshBuilder.CreateGround('ground',
{ width: 10, height: 10 },
scene
);
What we did:
const sphere = BABYLON.MeshBuilder.CreateSphere('sphere',
{ diameter: 2, segments: 20 },
scene
);
sphere.position.y = 1;
What we did here is:
The final result when you run the app:
Here you can move and see around using the mouse just like we are in a game, but not navigate. Navigation can be implemented by using some advanced techniques, later on that.
Find the playground code here: https://playground.babylonjs.com/#2KRNG9#1140
Here is the first checkpoint, understanding the basics…
Now feel free to do a research on:
You can find cool BabylonJS examples here: BabylonJS Examples
Here are some cool ideas to get you started:
Dont think that BabylonJS is the only gun in the 3D world... The rivals include:
Nextjs Image is so optimized that it loads almost instantly after the first time. Next is doing a really great job behind the scenes regarding the type of image files and quality of the same and decides at which quality it should be rendered. Yes, even though the image might be 4K, its not rendered in 4K.
We can download the code of Nextjs from its github repo and take a look at the image component image.tsx. The associated files would be image-optimizer.ts and image-config.ts
image.tsx → packages/next/client/image.tsx
image-optimizer.tsx → packages/next/server/image-optimizer.ts
image-config.tsx → packages/next/shared/lib/image-config.ts
From the path itself, it is known that image.tsx is for client side and the optimizer is for server side.
Just a reminder that Image component takes a lot of props.
✅ For different layouts, Next set different sizes in terms of width
and height
.
✅ Next checks if there is blur
option from user. If there is, it sets the blur image as the user provided one and if not, it sets its own.
✅ If src
starts with ‘data:’ or ‘blob:’, it is set as unoptimized, shown as raw image
✅ Errors for LCP
, not providing width and height, not providing src etc are handled beautifully
⚡ For priority mode images, they should be loaded lot faster. Next does this by adding a Head tag with rel="preload"
along with rendering image
⚡ Behind the scenes, the img
tag make use of a useCallback of a function which handles loading, taking dependencies as src(attr), placeholder(attr), onLoadRef(fnc), onLoadingCompleteRef(fnc), setBlurComplete(fnc), onError, unoptimized(attr
⚡ When it comes to large resolution images
, Next only shows 2x resolution; i.e. 2x2 pixels per dot of resolution, because 2x is the max resolution a human eye can get the details from. Even if the details were 3x, humans cannot see that LOD through it and it consumes a large amount of data. This means, whatever the width that the user gives as attribute to the image component, the maximum resolution that Next would render is 2 times the width. If 250 is the width given, max of 640px would be rendered (2x would be 500. 640 is the next possible standard screen resolution)
And many other things are going on in the client side.
✅ Server side make use of Sharp, an image processing package, which is faster and lighter for processing images. Sharp has a lot and lot of methods for manipulating the image in whatever we want the image to be. You might wanna check the methods that are available, they are really cool
✅ Then, making use of a package called Squoosh, another cool image processing package, Next handles the processing of images with desired quality depending on the type
✅ After all the processing, headers like cache control
, content disposition
, content security policy
, ‘X-Nextjs-Cache’
, content type
, content length
etc are sent along with the response. The important thing to mention is that almost all file types are converted equivalent to that of webp for more optimized loading
⚡ When server calls the ImageOptimizerCache (which is the class name for image-optimizer), the image(s) is processed first like mentioned above and are cached inside ‘_next/image’, which is done by ImageRouting function inside server. Any stale cache are periodically removed by Next itself
All types of error cases, for width, height, src, srcSet, blurImage, onLoad, onLoadComplete, wrong path etc are handled by the Next beautifully and it can be understood if you look the code itself.
Combine the server actions with the front end actions, you get beautifully optimized Image component, which delivers super fast image loading time and cached response. Thats how Nextjs <Image/> works.
Reactflow is a JavaScript library that allows developers to create interactive, node-based user interfaces. It is built on top of the popular React library and is designed to be easy to use and customize.
With Reactflow, developers can create nodes that represent different pieces of data or functionality, and connect them together with edges to create a flow of data or actions. This makes it easy to build interactive applications that allow users to manipulate data or perform tasks in a visual way.
Workflow builder - Onesignal
Interactive Process documentation - Stripe
Visualizing complex logic - Typeform
Orchestrating functions and integrations - Baseten
Dependency visualizer - Bit
To create a node in Reactflow, you will need to import the Node component from the Reactflow library and use it in your React component. The Node component takes several props, including a label prop that is used to display text on the node and a data prop that can be used to store any data that you want to associate with the node.
For example, you could create a node like this:
import React from "react";
import ReactFlow, { Controls, Background } from "reactflow";
import "reactflow/dist/style.css";
const nodes = [
{
id: "1",
position: { x: 0, y: 0 },
data: { label: "Node" },
},
];
function Node() {
return (
<div style={{ height: "100%" }}>
<ReactFlow nodes={nodes}>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Node;
This would create a node with the label "Flow". You can customize the appearance of the node by using the various style props that are available, such as color, borderColor, and borderWidth. You would have noticed that the node is placed at (0,0).
More nodes can be added by either increasing the initial nodes provided or pushing node objects into the node array.
To connect nodes together and create a flow of data or actions, you can use the Edge component from the Reactflow library. The Edge component takes two props: source and target, which are used to specify the nodes that the edge should connect.
For example, you could create an edge like this:
import ReactFlow, { Controls, Background } from 'reactflow';
import 'reactflow/dist/style.css';
const edges = [{ id: '1-2', source: '1', target: '2' }];
const nodes = [
{
id: '1',
data: { label: 'Hello' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: '2',
data: { label: 'World' },
position: { x: 100, y: 100 },
},
];
function Flow() {
return (
<div style={{ height: '100%' }}>
<ReactFlow nodes={nodes} edges={edges}>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default Flow;
This would create an edge that connects the node with the ID "1" to the node with the ID "2". You can customize the appearance of the edge by using the various style props that are available, such as color and width.
Now, the flow has been created successfully.
Reactflow provides several props that you can use to add interactivity to your node-based user interfaces. For example, you can use the onNodesChange prop to specify a function that should be called when any action is taken on a node.. This function can be used to display additional information about the node or to perform some other action.
You can also use the onEdgesChange prop to specify a function that should be called when any action is taken on an edge. This function can be used to display additional information about the edge or to perform some other action.
const onNodesChange = useCallback(
(changes: NodeChange[]) =>
setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) =>
setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange} // nodeChanges here
onEdgesChange={onEdgesChange} // edgeChanges here
>
One of the key features of Reactflow is the ability to create custom nodes that represent different pieces of data or functionality. To create a custom node, you will need to use the Node component and provide your own content inside the node.
For example, you could create a custom node like this:
import { useCallback } from "react";
const handleStyle = { left: 10 };
function CustomNodeTest({ data }: any) {
const onChange = useCallback((evt: any) => {
console.log(evt.target.value);
}, []);
return (
<div className="p-2 rounded border border-slate-800">
<label htmlFor="text">Custom Node with text input: </label>
<input id="text" name="text" onChange={onChange} />
</div>
);
}
export default CustomNodeTest;
Then import this node in our main file and include this in the nodes array with its type attribute as the function/component name.
import CustomNodeTest from "./myCustomNode";
const nodeTypes = useMemo(() => ({ customNode: CustomNodeTest }), []);
const nodes = [
{
id: "node-1",
type: "MyCustomNode",
position: { x: 0, y: 0 },
data: { value: 123 },
},
];
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes} // nodeTypes here
>
NodeTypes are memoized or it should be outside the component definition. Otherwise there would be performance issues.
You can customize the appearance of the custom node by using the various style props that are available, such as color, borderColor, and borderWidth.
In addition to creating a flow with multiple nodes and edges, you can also create a sub flow by using the Flow component from the Reactflow library. A sub flow is a flow within a flow, and can be used to represent a subprocess or a group of related nodes. To create a sub flow, you will need to wrap your nodes and edges in a Flow component and provide a label prop to display a label on the sub flow.
For example, you could create a sub flow like this:
{
id: "parent_group1",
type: "group",
data: {
label: "parent 1",
},
position: {
x: 250,
y: 250,
},
style: {
width: 200,
height: 200,
},
},
{
id: "child1_group1",
type: "input",
data: { label: "child node 1" },
position: { x: 10, y: 10 },
parentNode: "parent_group1",
extent: "parent",
},
{
id: "child2_group1",
data: { label: "child node 2" },
position: { x: 10, y: 90 },
parentNode: "parent_group1",
extent: "parent",
}
Notice the 'type' of first node and 'parentNode' of the other two.
index.tsx
import Head from "next/head";
import Image from "next/image";
import { Inter } from "@next/font/google";
import styles from "../styles/Home.module.css";
import { useCallback, useMemo, useState } from "react";
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
applyEdgeChanges,
applyNodeChanges,
NodeChange,
EdgeChange,
Connection,
Edge,
MarkerType,
} from "reactflow";
// 👇 you need to import the reactflow styles
import "reactflow/dist/style.css";
import { Button } from "primereact/button";
import { Dialog } from "primereact/dialog";
import { InputText } from "primereact/inputtext";
import { InputNumber } from "primereact/inputnumber";
import CustomNodeTest from "./myCustomNode";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
const initialNodes = [
{
id: "1",
type: "input",
position: { x: 100, y: 100 },
data: { label: "Styled First Node" },
style: {
color: "white",
background: "purple",
padding: "2px",
borderRadius: "4px",
width: "75px",
fontSize: "10px",
},
},
{
id: "2",
position: { x: 100, y: 500 },
data: { label: "Second" },
type: "output",
},
{
id: "3",
position: { x: 350, y: 500 },
data: { label: "Third" },
type: "output",
},
{
id: "parent_group1",
type: "group",
data: {
label: "parent 1",
},
position: {
x: 250,
y: 250,
},
style: {
width: 200,
height: 200,
},
},
{
id: "child1_group1",
type: "input",
data: { label: "child node 1" },
position: { x: 10, y: 10 },
parentNode: "parent_group1",
extent: "parent",
},
{
id: "child2_group1",
data: { label: "child node 2" },
position: { x: 10, y: 90 },
parentNode: "parent_group1",
extent: "parent",
},
{
id: "custom-node-1",
type: "customNode",
position: { x: 200, y: 200 },
data: { value: 123 },
},
];
const nodeTypes = useMemo(() => ({ customNode: CustomNodeTest }), []);
const initialEdges = [
{
id: "e1-2",
source: "1",
target: "2",
label: "Edge with fill, color, arrowType",
labelStyle: { fill: "red", fontWeight: 700 },
labelBgPadding: [8, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: "#FFCC00", color: "#fff", fillOpacity: 0.7 },
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
];
const [displayDialog, setDisplayDialog] = useState<boolean>(false);
const [nodeName, setNodeName] = useState<string>("");
const [xPosition, setXPosition] = useState<number | null>(null);
const [yPosition, setYPosition] = useState<number | null>(null);
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes: NodeChange[]) =>
setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) =>
setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback(
(connection: Edge<any> | Connection) =>
setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
const renderFooter = () => {
return (
<div>
<Button
label="No"
icon="pi pi-times"
onClick={() => setDisplayDialog(false)}
className="p-button-text"
/>
<Button
label="Yes"
icon="pi pi-check"
onClick={() => {
handleSetupNode();
}}
autoFocus
/>
</div>
);
};
const handleAddNode = () => {
setDisplayDialog(true);
};
const handleSetupNode = () => {
if (xPosition && yPosition && nodeName) {
setNodes([
...nodes,
{
id: `${nodes.length + 1}`,
position: { x: xPosition ?? 0, y: yPosition ?? 0 },
data: { label: nodeName },
type: "default",
},
]);
setDisplayDialog(false);
} else return;
};
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="absolute top-0 right-0 z-[9999]">
<Button
label="Add Node"
className="cursor-pointer"
icon="pi pi-plus"
onClick={handleAddNode}
/>
</div>
<Dialog
header="Add Node"
visible={displayDialog}
style={{ width: "50vw" }}
footer={renderFooter()}
onHide={() => setDisplayDialog(false)}
>
<div className="flex flex-col gap-4">
<div className="pt-6">
<span className="p-float-label">
<InputText
id="nameOfNode"
value={nodeName}
onChange={(e) => setNodeName(e.target.value)}
/>
<label htmlFor="nameOfNode">Node Name: </label>
</span>
</div>
<div className="flex gap-4 pt-6">
<span className="p-float-label">
<InputNumber
id="xPos"
value={xPosition}
onChange={(e) => {
setXPosition(e.value!);
}}
/>
<label htmlFor="xPos">X Position: </label>
</span>
<span className="p-float-label">
<InputNumber
id="yPos"
value={yPosition}
onChange={(e) => setYPosition(e.value!)}
/>
<label htmlFor="yPos">Y Position: </label>
</span>
</div>
</div>
</Dialog>
<div style={{ height: "100vh", width: "100vw" }} className="relative">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
attributionPosition="top-right"
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
</div>
</>
);
}
myCustomNode.tsx
import { useCallback } from "react";
const handleStyle = { left: 10 };
function CustomNodeTest({ data }: any) {
const onChange = useCallback((evt: any) => {
console.log(evt.target.value);
}, []);
return (
<div className="p-2 rounded border border-slate-800">
<label htmlFor="text">Custom Node with text input: </label>
<input id="text" name="text" onChange={onChange} />
</div>
);
}
export default CustomNodeTest;
Codesandbox: https://codesandbox.io/s/example-react-flow-twwxhb
Reactflow is a powerful library that makes it easy to create interactive, node-based user interfaces. Whether you want to build a visual workflow application or a data visualization tool, Reactflow provides the tools you need to get started. With its simple API and customizable appearance, Reactflow is a great choice for any developer looking to build a node-based user interface.
Every month, One Lucky Duck gets free swag shipped to their doorstep, wherever in the world you are! All you have to do is join our Discord channel today and tweet about the amazing things we do. #nullcast #luckyduck
We will announce the winners on Twitter and through our discord channel.