惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Schneier on Security
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Attack and Defense Labs
Attack and Defense Labs
H
Hacker News: Front Page
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
Cisco Talos Blog
Cisco Talos Blog
T
Tenable Blog
G
Google Developers Blog
A
About on SuperTechFans
The Cloudflare Blog
S
Securelist
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Forbes - Security
Forbes - Security
腾讯CDC
Application and Cybersecurity Blog
Application and Cybersecurity Blog
V
Vulnerabilities – Threatpost
IT之家
IT之家
博客园_首页
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
月光博客
月光博客
NISL@THU
NISL@THU
爱范儿
爱范儿
S
Secure Thoughts
K
Kaspersky official blog
Security Latest
Security Latest
T
Tailwind CSS Blog
博客园 - Franky
D
Darknet – Hacking Tools, Hacker News & Cyber Security
TaoSecurity Blog
TaoSecurity Blog
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
S
SegmentFault 最新的问题
H
Help Net Security
T
Tor Project blog
L
LINUX DO - 热门话题
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
O
OpenAI News
S
Schneier on Security

Supabase Blog

AI Agents Know About Supabase. They Don't Always Use It Right. Custom OIDC Providers for Supabase Auth 100,000 GitHub stars Supabase docs over SSH Navigating Regional Network Blocks Supabase Joins the Stripe Projects Developer Preview Log Drains: Now available on Pro Supabase Storage: major performance, security, and reliability updates Supabase incident on February 12, 2026 Hydra joins Supabase X / Twitter OAuth 2.0 is now available for Supabase Auth BKND joins Supabase Supabase is now an official Claude connector Supabase PrivateLink is now available Introducing: Postgres Best Practices When to use Read Replicas vs. bigger compute Introducing TRAE SOLO integration with Supabase Supabase Security Retro: 2025 Sync Stripe Data to Your Supabase Database in One Click Building ChatGPT Apps with Supabase Edge Functions and mcp-use Own Your Observability: Supabase Metrics API Introducing iceberg-js: A JavaScript Client for Apache Iceberg Introducing Supabase for Platforms Adding Async Streaming to Postgres Foreign Data Wrappers Build "Sign in with Your App" using Supabase Auth Introducing Seven New Email Templates for Supabase Auth The new Supabase power for Kiro Introducing Supabase ETL Introducing Analytics Buckets Introducing Vector Buckets Snap, Inc. Launches Snap Cloud, Powered by Supabase Triplit joins Supabase Supabase Series E 1000 Y Combinator Founders Choose Supabase gm 👋 web3, welcome aboard to Sign in with Web3 (Solana, Ethereum) Announcing the Supabase Remote MCP Server Enterprise speed, enterprise standards with Bolt Cloud + Supabase PostgREST 13 Lovable Cloud + Supabase: The Default Platform for AI Builders Processing large jobs with Edge Functions, Cron, and Queues Defense in Depth for MCP Servers OrioleDB Patent: now freely available to the Postgres community Supabase Launch Week 15 Hackathon Winner Announcement The Vibe Coder's Guide to Supabase Environments Testing for Vibe Coders: From Zero to Production Confidence The Vibe Coding Master Checklist Vibe Coding: Best Practices for Prompting Supabase Auth: Build vs. Buy Top 10 Launches of Launch Week 15 Supabase Launch Week 15 Hackathon Storage: 10x Larger Uploads, 3x Cheaper Cached Egress, and 2x Egress Quota Persistent Storage and 97% Faster Cold Starts for Edge Functions Algolia Connector for Supabase New Observability Features in Supabase Improved Security Controls and A New Home for Security Introducing Branching 2.0 Stripe-To-Postgres Sync Engine as standalone Library Supabase Analytics Buckets with Iceberg Support Create a Supabase backend using Figma Make Introducing JWT Signing Keys Supabase UI: Platform Kit Build a Personalized AI Assistant with Postgres Announcing Multigres: Vitess for Postgres Building on open table formats Open Data Standards: Postgres, OTel, and Iceberg Simplifying back-end complexity with Supabase Data APIs PostgreSQL Event Triggers without superuser access Top 10 Launches of Launch Week 14 Supabase MCP Server Data API Routes to Nearest Read Replica Declarative Schemas for Simpler Database Management Realtime: Broadcast from Database Keeping Tabs on What's New in Supabase Studio Edge Functions: Deploy from the Dashboard + Deno 2.1 Automatic Embeddings in Postgres Introducing the Supabase UI Library Supabase Auth: Bring Your Own Clerk Postgres Language Server: Initial Release Migrating from Fauna to Supabase Migrating from the MongoDB Data API to Supabase Dedicated Poolers Postgres as a Graph Database: (Ab)using pgRouting AI Hackathon at Y Combinator Calendars in Postgres using Foreign Data Wrappers Supabase Launch Week 13 Hackathon Winners How to Hack the Base! Running Durable Workflows in Postgres using DBOS database.build v2: Bring-your-own-LLM Restore to a New Project Hack the Base! with Supabase Top 10 Launches of Launch Week 13 Supabase Queues High Performance Disk Supabase Cron Supabase CLI v2: Config as Code Supabase Edge Functions: Introducing Background Tasks, Ephemeral Storage, and WebSockets Supabase AI Assistant v2 OrioleDB Public Alpha Executing Dynamic JavaScript Code on Supabase with Edge Functions ClickHouse Partnership, improved Postgres Replication, and Disk Management
Coding the stars - an interactive constellation with Three.js and React Three Fiber
Francesco Sansalvadore · 2023-08-04 · via Supabase Blog

Coding the stars - an interactive constellation with Three.js and React Three Fiber

Every Launch Week is an opportunity for Supabase to experiment, try some spicy new designs, and dogfood our own technology. During our previous Launch Week we took Generative AI for a spin. This time we decided to shoot for the stars.

For Launch Week 8, we wanted to make the user-generated tickets a central piece of the launch week theme. To do this, we built a “constellation” of stars - an animated night sky where every user signup was represented as a star, in the form of an “8” shape.

We could approach this animation in a few ways.

For example, animating the stroke-dashoffset on an SVG path, similar to this example, was a good option, but it would have been difficult to randomize and change the shape at a later stage. Other approaches included 2D animation libraries, like Framer Motion , gsap or PixiJS .

Ultimately we decided to take Three.js for a spin using React Three Fiber (R3F) giving us a more powerful toolset to enable us to achieve the best possible result.

Learning Three.js is not a walk in the park but R3F abstracted many of its complexities such as cameras and renderers, to name a few. If you're new to R3F, some of the core primitives they provide for a basic scene include:

  • Geometries: used to create and define shapes
  • Materials: manage the texture and color of objects
  • Mesh: used to instantiate polygonal objects by combining a Geometry with a Material
  • Lights: to shine bright like a diamond 💎🎵
  • Canvas: where you define your R3F Scene

If you want to dive a little deeper, here are a few good resources we found to get a solid grasp on the topic:

In this article, we’re going to break down the steps to reproduce the Launch Week 8 animation using React Three Fiber in NextJs.

These are the dependencies we’ll need:


_10

npm install three @react-three/fiber


If you’re using React 17, we’ll spare you the trouble of finding the last compatible R3F version:


_10

npm install three @react-three/fiber@7.0.29


All we need for each particle is a circle geometry with a minimal amount of sides to minimize complexity.


_10

import { useMemo } from 'react'

_10

_10

const Geometry = useMemo(() => () => <circleGeometry args={[1.5, 5]} />, [])


A basic standard material with a white color will do just fine. Using the AdditiveBlending module from three provides a more interesting touch when particles happen to overlap, making them shine brighter:


_10

import { AdditiveBlending } from 'three'

_10

_10

const Material = () =>

_10

useMemo(() => <meshStandardMaterial color="#ffffff" blending={AdditiveBlending} />, [])


Let’s put it together in an R3F Canvas element and wrap up the initial setup with an ambientLight, which will make objects visible, just as real light does:


_40

import { useMemo } from 'react'

_40

import { Canvas } from '@react-three/fiber'

_40

import { AdditiveBlending } from 'three'

_40

_40

const Geometry = useMemo(

_40

() => () => <circleGeometry args={[1.5, 5]} />,

_40

[]

_40

)

_40

_40

const Material = () =>

_40

useMemo(

_40

() => (

_40

<meshStandardMaterial

_40

color="#ffffff"

_40

blending={AdditiveBlending}

_40

/>

_40

),

_40

[]

_40

)

_40

_40

return (

_40

<div style={{ width: 100vw, height: 100vh, background: "#000000" }}>

_40

<Canvas

_40

dpr={[1, 2]}

_40

camera={{ fov: 75, position: [0, 0, 500] }}

_40

>

_40

<ambientLight intensity={0.3} />

_40

<group>

_40

{particles?.map((particle, index) => (

_40

<mesh

_40

key={particle.username}

_40

>

_40

<Geometry />

_40

<Material />

_40

</mesh>

_40

))}

_40

</group>

_40

</Canvas>

_40

</div>

_40

)


For more context, the dpr values help with pixelation issues and the camera [0, 0, 500] position means that the camera is moved 500 units back in the z-axis to actually see the center [0,0,0] of the scene.

One thing to note is that the R3F Canvas renders a transparent background, so in order to see the white particle, we need to set the background of the parent html element to a dark color.

We created a separate component for the Particle, which will later encapsulate the animation logic.


_10

import React, { useRef } from 'react'

_10

_10

const Particle = ({ children }) => {

_10

const particle = useRef(null)

_10

_10

return <mesh ref={particle}>{children}</mesh>

_10

}

_10

_10

export default Particle


You might have noticed we haven’t instantiated the particles yet. As we mentioned earlier, we wanted each particle to represent a ticket generated by a user and stored in the database. Let’s fetch the signups from the tickets table in our Supabase project (you might need to start your own Launch Week to fill your table):


_10

const [particles, setParticles] = useState([])

_10

_10

const loadUsers = async () => {

_10

return await supabase.from('lw8_tickets').select('*')

_10

}

_10

_10

useEffect(() => {

_10

const { data: users } = loadUsers()

_10

setParticles(users)

_10

}, [])


We updated the constellation in realtime whenever a new ticket was generated, but we’ll skip over this part to keep the article shorter. Since it’s all open-source, you can dive deeper here if you wish.

Animating the particles#

To move the particle around the screen we are going to leverage a few different concepts: useFrame and trigonometry 🤯

useFrame#

Generally, the most optimal way to animate things in a browser viewport, using javascript, is by leveraging a method called requestAnimationFrame , which “tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation right before the next repaint.”. R3F has a similar hook called useFrame that lets you execute code on every frame of Fiber's render loop. We’ll use this to change the position of the particles over time in a few moments.

Using time as an animation variable#

We can extract time information from the useFrame clock parameter, to know how much time has elapsed in our application, and use that time to animate a value. Updating the x position with Math.sin() generates a horizontal oscillating movement. Multiply it with a widthRadius variable to customize the amplitude of the movement.


_13

const particle = useRef(null)

_13

_13

const widthRadius = 100

_13

const heightRadius = 100

_13

_13

useFrame(({ clock }) => {

_13

const timer = clock.getElapsedTime()

_13

_13

particle.current.position.x = Math.sin(timer) * widthRadius

_13

}

_13

})

_13

_13

return <mesh ref={particle}>{children}</mesh>


Combine the previous horizontal movement with a Math.cos() on the y position to draw a circle:


_10

particle.current.position.y = Math.cos(timer) * heightRadius


Calculating the circumference we can get the time the x position takes to complete a full circle.


_10

const circumference = (config.widthRadius * Math.PI * 2) / 100


When that happens, we can invert the cos sign on every other loop to obtain a basic 8 shape.


_10

const isEven = Math.floor(timer / circumference) % 2 == 0

_10

_10

particle.current.position.x = Math.sin(timer) * widthRadius

_10

particle.current.position.y = isEven

_10

? Math.cos(timer) * heightRadius - heightRadius

_10

: -Math.cos(timer) * heightRadius + heightRadius


At this point, we played around with a number of parameters that made the animation more randomized and interesting.

For example, we randomized the speed and the delay of each particle:


_10

const minSpeed = -0.3

_10

const maxSpeed = 0.4

_10

const speed = Math.random() * (minSpeed - maxSpeed) + maxSpeed

_10

_10

const delayOffsetFactor = 100

_10

const delayOffset = Math.random() * delayOffsetFactor

_10

_10

[...]

_10

_10

const timer = clock.getElapsedTime() *** speed + delayOffset**


We offset the shape on the x-axis, to concentrate most of the particles in the core of the 8 shape and leave a smaller amount externally, by playing around with exponentials using Math.pow() in combination with some more randomization.


_13

const xThickness = 7

_13

const xRandomnessShape = 2.2

_13

const xRandomness = 5

_13

_13

const pathOffset =

_13

Math.pow(

_13

Math.random() * xRandomnessShape,

_13

xRandomness - xRandomness / 2

_13

) * xThickness

_13

_13

...

_13

_13

particle.current.position.x = Math.sin(timer) * widthRadius + pathOffset


Honestly, this was the result of a lot of playing and tweaking around, and we certainly didn’t hit the best possible result on the first try. Perhaps you want to take some time to experiment with the math - you might find even better and more configurable results.

What really helped to visualize the shape, gather feedback, and decide on a final design was adding a GUI to play around with the values. You can try for yourself by appending #debug to the supabase.com/launch-week#debug url. Go crazy with it.

We used the dat.gui library:


_10

npm install dat.gui@0.7.9


Which needs to be loaded asynchronously, otherwise it raises a window is not defined error.


_10

const init = async () => {

_10

const dat = await import('dat.gui')

_10

const gui = new dat.GUI()

_10

}

_10

_10

useEffect(() => {

_10

init()

_10

}, [])


Then we prepared a useParticlesConfig hook with all the configuration wired up to the GUI. Whenever the GUI updated, we also updated react state.


_66

import { useEffect, useState } from 'react'

_66

import { range } from 'lodash'

_66

_66

let defaultConfig = {

_66

particles: 1500,

_66

widthRadius: 100,

_66

topHeightRadius: 80,

_66

bottomHeightRadius: 100,

_66

xThickness: 7,

_66

xRandomnessFactor: 2.2,

_66

xRandomnessShape: 2.2,

_66

xRandomness: 5,

_66

yThickness: 20,

_66

max_speed: 0.1,

_66

min_speed: -0.1,

_66

}

_66

_66

const useParticlesConfig = (): any => {

_66

if (typeof window === 'undefined') return null

_66

const hash = window.location.hash

_66

const isDebugMode = hash.includes('#debug')

_66

const [particles, setParticles] = useState(range(0, defaultConfig.particles))

_66

_66

const [config, setConfig] = useState(defaultConfig)

_66

_66

const handleSetConfig = (name, value) => {

_66

setConfig((prevConfig) => ({ ...prevConfig, [name]: value }))

_66

}

_66

_66

const init = async () => {

_66

if (!isDebugMode) return

_66

const dat = await import('dat.gui')

_66

const gui = new dat.GUI()

_66

const particlesFolder = gui.addFolder('Particles')

_66

const shapeFolder = gui.addFolder('Shape')

_66

_66

particlesFolder

_66

.add(config, 'particles')

_66

.min(1)

_66

.max(5000)

_66

.step(1)

_66

.onChange((value) => {

_66

handleSetConfig('particles', value)

_66

setParticles(range(0, value))

_66

})

_66

shapeFolder

_66

.add(config, 'widthRadius')

_66

.min(1)

_66

.max(200)

_66

.step(1)

_66

.onChange((value) => handleSetConfig('widthRadius', value))

_66

_66

// add desired folders and parameters

_66

_66

particlesFolder.open()

_66

shapeFolder.open()

_66

}

_66

_66

useEffect(() => {

_66

init()

_66

}, [])

_66

_66

return { config, handleSetConfig, particles, setParticles, isDebugMode }

_66

}

_66

_66

export default useParticlesConfig


Here is the final code:


_84

import React, { useMemo, useEffect, useState } from 'react'

_84

import { Canvas, useFrame } from '@react-three/fiber'

_84

import { AdditiveBlending } from 'three'

_84

import useParticlesConfig from './hooks/useParticlesConfig'

_84

_84

const ParticlesCanvas = () => {

_84

if (typeof window === 'undefined') return null

_84

const { config, particles } = useParticlesConfig()

_84

_84

const Geometry = useMemo(

_84

() => () => <circleGeometry args={[config.particlesSize, config.particlesSides]} />,

_84

[]

_84

)

_84

_84

const Material = () =>

_84

useMemo(

_84

() => (

_84

<meshStandardMaterial

_84

color={config.color}

_84

blending={config.particlesBlending ? AdditiveBlending : undefined}

_84

/>

_84

),

_84

[]

_84

)

_84

_84

return (

_84

<div style={{ width: 100vw, height: 100vh, background: "#000000" }}>

_84

<Canvas

_84

dpr={[1, 2]}

_84

camera={{ fov: 75, position: [0, 0, 500] }}

_84

>

_84

<ambientLight intensity={config.lightIntensity} />

_84

<group>

_84

{particles?.map((particle, index) => (

_84

<Particle

_84

key={particle.username}

_84

>

_84

<Geometry />

_84

<Material />

_84

</Particle>

_84

))}

_84

</group>

_84

</Canvas>

_84

</div>

_84

)

_84

}

_84

_84

const Particle = ({ children }: Props) => {

_84

const particle = useRef(null)

_84

_84

const pathOffset =

_84

Math.pow(

_84

Math.random() * config.xRandomnessShape,

_84

config.xRandomness - config.xRandomness / 2

_84

) * config.xThickness

_84

_84

const verticalRandomness = Math.random() * (config.yThickness - 1) + 1 - config.yThickness / 2

_84

_84

const speed = Math.random() * (config.min_speed - config.max_speed) + config.max_speed

_84

_84

const circumference = (config.widthRadius * Math.PI * 2) / 100

_84

const delayOffsetFactor = 100

_84

const delayOffset = Math.random() * delayOffsetFactor

_84

_84

useFrame(({ clock }) => {

_84

const timer = clock.getElapsedTime() * speed + delayOffset

_84

const isEven = Math.floor(timer / circumference) % 2 == 0

_84

_84

// When the loop count is even, draw bottom 8 shape

_84

// if odd, draw top 8 shape

_84

particle.current.position.x = isEven

_84

? Math.sin(timer) * config.widthRadius * config.widthRatio + pathOffset

_84

: Math.sin(timer) * config.widthRadius + pathOffset

_84

particle.current.position.y = isEven

_84

? Math.cos(timer) * config.bottomHeightRadius -

_84

config.bottomHeightRadius +

_84

verticalRandomness

_84

: -Math.cos(timer) * config.topHeightRadius + config.topHeightRadius + verticalRandomness

_84

})

_84

_84

return <mesh ref={particle}>{children}</mesh>

_84

}

_84

_84

export default Particle


Now THAT’S how you create a new constellation ✨. Feel free to use the code and learnings to build your own.

In this journey, you saw how to use Three.js and harness the power of React Three Fiber and creative experimentation to craft an animation. We leveraged trigonometry, animation hooks, and GUI playgrounds to build a "8" shape formed by user-generated stars.

If you loved this and the new Launch Week 8 branding, make sure to tune in on Monday at 09 AM PT as we unveil the full landing 💥