Skip to content

Commit 1439b10

Browse files
committed
Added NEAT blog post
1 parent a829f98 commit 1439b10

File tree

9 files changed

+115
-30
lines changed

9 files changed

+115
-30
lines changed

website/blog/2024-10-15-neat.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
slug: neat_3d_gradient
3+
title: How we built NEAT, the 3D gradient tool
4+
author: Francesco Gatti
5+
image: /img/avatars/francesco_avatar.jpg
6+
author_url: https://www.linkedin.com/in/fgatti675
7+
author_image_url: https://avatars.githubusercontent.com/u/5120271?v=4
8+
---
9+
10+
<video class="demo" loop autoplay muted width="100%">
11+
<source src="/img/blog/neat_stripe.webm" type="video/mp4"/>
12+
</video>
13+
14+
The first time I saw the Stripe shader animation on their website, **I was hooked.** I really wanted to have something
15+
that cool for the websites I was working on and tried to reverse engineer what Stripe does. I must say, it wasn't easy!
16+
I had to learn a lot about shaders, WebGL, and three.js to get to the point where I could create something similar.
17+
18+
We are going to be using a lot of **Perlin noise functions** to generate the waves and the gradient. **Perlin noise** is
19+
a type of gradient noise used in computer graphics to create natural-looking textures. It was invented by Ken Perlin in
20+
the 1980s. The noise is generated by interpolating between random values. It is locally stable, so it is well-suited for
21+
generating animations by including a time parameter in the noise function.
22+
23+
![Perlin Noise Texture](../static/img/blog/perlin-noise-texture.png)
24+
25+
The Stripe animation is really a 3D shape, a plane divided in triangles acting like a flag that has waves passing
26+
through it. On top of that, there is a gradient that changes color and brightness. That is really the essence of the
27+
effect.
28+
29+
I tried achieving the same effect with basic three.js, but it was really hard to get it right. I quickly realized that
30+
**I need to use custom shaders**, one for determining the position of each vertex in the plane and another one for
31+
assigning the color of each pixel. The first iterations were calculating the position of each vertex in the CPU and
32+
sending it to the GPU. This was really slow, and I had to find a way to calculate the position of each vertex in the
33+
GPU.
34+
35+
## Creating the Vertex Shader
36+
37+
<video class="demo" loop autoplay muted width="100%">
38+
<source src="/img/blog/neat_vertex_shader.mp4" type="video/mp4"/>
39+
</video>
40+
41+
The **vertex shader** is in charge of determining the position of each vertex. We have this loop running on the CPU
42+
using `requestAnimationFrame` that triggers updates. Each vertex is passed through a Perlin noise function. This Perlin
43+
function receives the coordinates of the shader, as well as the elapsed time and some parameters that we use to tweak
44+
the waves animation. **We can change the frequency of the waves, the amplitude, the speed, and the horizontal and
45+
vertical pressure.**
46+
47+
## Creating the Fragment Shader
48+
49+
<video class="demo" loop autoplay muted width="100%">
50+
<source src="/img/blog/neat_color_shader.webm" type="video/mp4"/>
51+
</video>
52+
53+
The **fragment shader** is in charge of determining the color of each pixel. NEAT uses from 1 to 5 colors as input. Each
54+
of these colors is passed through a Perlin noise function that generates a pattern like the one represented above. **The
55+
colors are then mixed together based on the noise value.** If we don't apply any blending to the colors, the result will
56+
be a sharp transition between the colors, as represented in the video above. We can apply a blending factor to the
57+
colors so that the transition between them is smoother. We can also apply horizontal and vertical pressure to the color
58+
patterns, so they are more or less pronounced.
59+
60+
## Post Processing
61+
62+
If we put it all together, we get a **3D gradient that morphs and changes.** At this point, we can apply some
63+
post-processing effects to the final image. We can change the brightness and saturation of the colors. It turns out this
64+
is pretty easy to do with WebGL shaders once you have calculated the color of each pixel. **We can also add some shadows
65+
and highlights based on the vertex normals.** This is a simple way to add some depth to the image. In the latest
66+
versions, we added some grain to the image to make it look more like a film. We can change the scale, intensity, and
67+
speed of the grain.
68+
69+
## The Editor
70+
71+
Having all this power in a single shader is really cool, but it's not easy to tweak the parameters. **That's why we
72+
created a simple editor that allows you to change the parameters in real-time.** You can change the speed, the frequency
73+
of the waves, the amplitude, the colors, the blending, the brightness, the saturation, the grain, and the resolution of
74+
the image, among other things. All this is performed in real-time. Every change is passed to the shaders as a uniform
75+
variable, and the image is re-rendered.
76+
77+
**This is really cool because you can see the changes immediately.** Until we had the editor, we were tweaking the
78+
parameters in the code and reloading the page to see the changes. This was really slow and cumbersome. It was also hard
79+
to know if we were going in the right direction. It was more a process of trial and error. It was not until we had the
80+
editor that we could really see the potential of the tool. **We could create a lot of different patterns and see how
81+
they look in real-time.** We could also save the patterns we liked and keep them as presets.
82+
83+
At this point, we were able to fine-tune a gradient to make it look exactly like the Stripe gradient. It has been quite
84+
a journey, but we are really happy with the results. Once you have a gradient you like, you can export it as a JSON
85+
object and use it in your projects. **We have created a lot of patterns with NEAT, and we are really happy with the
86+
results.** We have used it in many projects, and it has always been a hit. We are really proud of it, and we hope you
87+
like it too.
88+
89+
Check the [NEAT website](https://neat.firecms.co) to see some examples of what you can do with it. You can also check
90+
the [NEAT repository](https://github.com/firecmsco/neat) to see the code and the editor in action.

website/src/partials/features/SchemaEditorIntro.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,10 @@ export const SchemaEditorIntro = () => {
2727
The best data schema editor
2828
</h2>
2929

30-
<div className={"mt-4 mx-auto text-xl"}>
30+
<div className={"mt-4 mx-auto text-xl pb-20"}>
3131
<p>
32-
<b>FireCMS Cloud</b> is a hosted version of FireCMS
33-
that allows you to create your own headless CMS
34-
in minutes. It includes a new content schema
35-
editor that allows you to create your own
36-
content models and collections.
32+
Edit your data schema with a powerful and intuitive
33+
interface.
3734
</p>
3835
<p>If you have an <strong>existing
3936
project</strong>, let FireCMS
@@ -48,18 +45,6 @@ export const SchemaEditorIntro = () => {
4845

4946
</div>
5047

51-
52-
<div className={"my-8 pb-16"}>
53-
<a
54-
className={CTAOutlinedButtonMixin}
55-
href="http://app.firecms.co"
56-
rel="noopener noreferrer"
57-
target="_blank"
58-
>
59-
Go to FireCMS Cloud
60-
<CTACaret/>
61-
</a>
62-
</div>
6348
</div>
6449
</div>
6550
</div>

website/src/partials/pricing/FireCMSCloudVersions.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function FireCMSCloudVersions() {
77

88
const freeTier = (
99
<div
10-
className={clsx("w-full max-w-xl lg:max-w-none h-full mx-4 p-6 border rounded-lg dark:border-gray-700 flex flex-col", defaultBorderMixin)}>
10+
className={clsx("w-full max-w-xl lg:max-w-none h-full p-6 border rounded-lg dark:border-gray-700 flex flex-col", defaultBorderMixin)}>
1111

1212
<h3 className={"text-3xl md:text-4xl font-bold text-center text-gray-700 uppercase my-2"}>
1313
Free
@@ -38,7 +38,7 @@ export function FireCMSCloudVersions() {
3838

3939
const plusTier = (
4040
<div
41-
className={clsx("w-full max-w-xl lg:max-w-none h-full mx-4 p-6 rounded-lg flex flex-col outline-none ring-2 ring-primary ring-opacity-75 ring-offset-2 ring-offset-transparent")}>
41+
className={clsx("w-full max-w-xl lg:max-w-none h-full p-6 rounded-lg flex flex-col outline-none ring-2 ring-primary ring-opacity-75 ring-offset-2 ring-offset-transparent")}>
4242

4343
<h3 className={"text-3xl md:text-4xl font-bold text-center text-primary uppercase my-2"}>
4444
Plus
@@ -78,7 +78,7 @@ export function FireCMSCloudVersions() {
7878

7979
const proTier = (
8080
<div
81-
className={clsx("w-full max-w-xl lg:max-w-none h-full mx-4 p-6 border rounded-lg flex flex-col", defaultBorderMixin)}>
81+
className={clsx("w-full max-w-xl lg:max-w-none h-full p-6 border rounded-lg flex flex-col", defaultBorderMixin)}>
8282

8383
<h3 className={"text-3xl md:text-4xl font-bold text-center text-gray-700 uppercase my-2"}>
8484
Pro
@@ -99,22 +99,21 @@ export function FireCMSCloudVersions() {
9999
<li className={"list-disc"}>Roadmap prioritization</li>
100100
</ul>
101101
</div>
102-
<div className={"w-fit m-auto mt-4 flex flex-row gap-2 items-center"}>
102+
<div className={"w-fit m-auto mt-8 flex flex-row gap-2 items-center"}>
103103
<div
104104
className={"rounded-lg w-fit h-fit font-regular m-auto gap-1 text-ellipsis px-4 py-1.5 text-sm font-semibold"}
105105
style={{
106106
backgroundColor: "rgb(255, 214, 110)",
107107
color: "rgb(59, 37, 1)"
108108
}}>
109109
TRY OUT FOR FREE
110-
111110
</div>
112-
<a className={clsx("btn px-4 py-2 uppercase rounded border-solid text-inherit dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900 dark:hover:border-gray-800 text-center")}
111+
<a className={clsx("btn px-4 py-2 uppercase rounded border-solid text-base dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900 dark:hover:border-gray-800 text-center")}
113112
href="/pro">
114113
More info
115114
</a>
116115
</div>
117-
<div className={"text-center mt-4 text-gray-600 w-full"}>
116+
<div className={"text-center text-gray-600 w-full"}>
118117
<span className={"text-2xl font-bold "}>Starting at €29.99</span>
119118
</div>
120119
</div>

website/src/partials/pricing/PricingQuote.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function PricingQuote() {
3939
d="M9.983 3v7.391c0 5.704-3.731 9.57-8.983 10.609l-.995-2.151c2.432-.917 3.995-3.638 3.995-5.849h-4v-10h9.983zm14.017 0v7.391c0 5.704-3.748 9.571-9 10.609l-.996-2.151c2.433-.917 3.996-3.638 3.996-5.849h-3.983v-10h9.983z"/>
4040
</svg>
4141

42-
<p className="mb-4 text-3xl mt-4 font-bold leading-snug tracking-tight text-center">
42+
<p className="mb-4 text-4xl mt-4 font-bold leading-snug tracking-tight text-center">
4343
{quote}
4444
</p>
4545
</div>

website/src/partials/pricing/VersionsComparison.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import useBaseUrl from "@docusaurus/useBaseUrl";
22
import React from "react";
3-
import { ContainerInnerPaddingMixin, CTAButtonMixin, defaultBorderMixin } from "../styles";
3+
import { ContainerInnerPaddingMixin, CTAButtonMixin, CTAOutlinedButtonMixin, defaultBorderMixin } from "../styles";
44
import CheckIcon from "@site/static/img/icons/check.svg";
55
import RemoveIcon from "@site/static/img/icons/remove.svg";
66
import ScheduleIcon from "@site/static/img/icons/schedule.svg";
@@ -265,7 +265,7 @@ export function VersionsComparison() {
265265
(
266266
<tr className="border-b ">
267267
<td scope="row"
268-
className="bg-gray-50 mx-2 border-none rounded-lg px-6 py-2 text-gray-800 font-bold">
268+
className="bg-gray-50 mx-2 border-none rounded-lg px-6 py-2 text-gray-800 font-bold md:min-w-[360px]">
269269
{row.feature}
270270
</td>
271271
<td className={"bg-gray-50 mx-2 border-none rounded-lg px-4 py-2 text-gray-800 text-center " + getEntryClass(row.cloud)}>
@@ -293,13 +293,24 @@ export function VersionsComparison() {
293293
</th>
294294
<th
295295
className={" table-cell p-0 border-none"}
296-
colSpan={3}
296+
colSpan={2}
297297
>
298298
<a className={CTAButtonMixin + " w-full"}
299299
rel="noopener noreferrer"
300300
target="_blank"
301301
href={"https://app.firecms.co"}>
302-
Get started
302+
Go to FireCMS Cloud
303+
</a>
304+
</th>
305+
<th
306+
className={" table-cell p-0 border-none"}
307+
colSpan={1}
308+
>
309+
<a className={CTAOutlinedButtonMixin + " w-full"}
310+
rel="noopener noreferrer"
311+
target="_blank"
312+
href={"/pro"}>
313+
More info
303314
</a>
304315
</th>
305316
{/*<a*/}
7.01 MB
Binary file not shown.
2.37 MB
Binary file not shown.
3.97 MB
Binary file not shown.
74.7 KB
Loading

0 commit comments

Comments
 (0)