Rocket Starfield Pro
Rocket is currently in beta.
A customizable starfield effect using HTML5 Canvas and reactive signals. Move your mouse over the starfield to control the center position.
Explanation #
The outer demo owns the interactive controls and passes the current values in as props. The component itself mounts the canvas from refs.canvas in onFirstRender(), then keeps the animation loop and resize observer internal.
Usage Example #
Rocket Component #
1import { rocket } from 'datastar'
2
3rocket('star-field', {
4 mode: 'light',
5 props: ({ number }) => ({
6 starCount: number.min(1).default(500),
7 speed: number.clamp(1, 100).default(50),
8 centerX: number.clamp(0, 100).default(50),
9 centerY: number.clamp(0, 100).default(50),
10 }),
11 onFirstRender: ({ refs, cleanup, host, observeProps, props }) => {
12 let aid = 0
13 let ro
14 let stars = []
15 let last = 0
16 let count = props.starCount
17 let canvas
18 let ctx
19 let bg = '#000'
20 let strokeStyles = []
21
22 const reset = () => {
23 if (!canvas) return
24 const {
25 clientWidth: width = innerWidth,
26 clientHeight: height = innerHeight,
27 } = canvas.parentElement ?? {}
28 canvas.width = width
29 canvas.height = height
30 stars = Array.from({ length: props.starCount }, () => {
31 const x = Math.random() * width,
32 y = Math.random() * height
33 return { fx: x, fy: y, tx: x, ty: y, b: (Math.random() * 100) | 0 }
34 })
35 }
36
37 const animate = (now = 0) => {
38 if (!canvas || !ctx) return
39 const { width, height } = canvas
40 const centerX = (width * props.centerX) / 100
41 const centerY = (height * props.centerY) / 100
42 const dt = last ? (now - last) / 1000 : 0
43 const velocity = (1 + (8 * (props.speed - 1)) / 99) * dt * 0.5
44 const max = Math.min(width, height) * 0.1
45 const buckets = {}
46
47 last = now
48 ctx.clearRect(0, 0, width, height)
49 ctx.fillStyle = bg
50 ctx.fillRect(0, 0, width, height)
51
52 for (const star of stars) {
53 star.fx = star.tx
54 star.fy = star.ty
55 star.tx += Math.max(-max, Math.min(max, (star.tx - centerX) * velocity))
56 star.ty += Math.max(-max, Math.min(max, (star.ty - centerY) * velocity))
57 star.b = Math.min(100, star.b + 1)
58
59 if (star.fx < 0 || star.fx > width || star.fy < 0 || star.fy > height) {
60 star.tx = star.fx = Math.random() * width
61 star.ty = star.fy = Math.random() * height
62 star.b = (Math.random() * 100) | 0
63 continue
64 }
65
66 const bucket = (star.b / 10) | 0
67 ;(buckets[bucket] ??= new Path2D()).moveTo(star.fx, star.fy)
68 buckets[bucket].lineTo(star.tx, star.ty)
69 }
70
71 ctx.lineWidth = 1
72 for (const bucket in buckets) {
73 ctx.strokeStyle = strokeStyles[bucket]
74 ctx.stroke(buckets[bucket])
75 }
76
77 aid = requestAnimationFrame(animate)
78 }
79
80 canvas = refs.canvas
81 ctx = canvas?.getContext('2d', {
82 alpha: true,
83 antialias: true,
84 optimizeSpeed: true,
85 willReadFrequently: false,
86 })
87 if (!canvas || !ctx) return
88
89 canvas.removeAttribute('width')
90 canvas.removeAttribute('height')
91
92 const style = getComputedStyle(host)
93 bg = style.backgroundColor
94 strokeStyles = Array.from(
95 { length: 10 },
96 (_, i) => `color-mix(in srgb, ${style.color}, ${bg} ${i * 10}%)`,
97 )
98
99 reset()
100 animate()
101 ro = new ResizeObserver(reset)
102 ro.observe(canvas.parentElement || canvas)
103
104 observeProps(() => {
105 if (props.starCount === count) return
106 count = props.starCount
107 reset()
108 }, 'starCount')
109
110 cleanup(() => {
111 cancelAnimationFrame(aid)
112 ro?.disconnect()
113 })
114 },
115 render: ({ html }) => html`
116 <style>
117 .rocket-starfield {
118 display: block;
119 width: 100%;
120 height: 100%;
121 }
122
123 canvas {
124 display: block;
125 width: 100%;
126 height: 100%;
127 }
128 </style>
129
130 <div class="rocket-starfield">
131 <canvas data-ref:canvas data-ignore-morph></canvas>
132 </div>
133 `,
134})