@playfast/reform-better-hub-ui-primitives 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
- // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
- // (apps/web/src/components/ui/halftone-background.tsx). Full credit to the Better Auth team.
3
-
4
- import type * as React from "react";
5
- import { useEffect, useRef } from "react";
6
-
7
- const VERTEX_SHADER = `
8
- attribute vec2 a_position;
9
- void main() {
10
- gl_Position = vec4(a_position, 0.0, 1.0);
11
- }`;
1
+ import type * as React from 'react'
2
+ import { useEffect, useRef } from 'react'
3
+ import {
4
+ CLICK_DECAY,
5
+ FULLSCREEN_VERTEX_COUNT,
6
+ MOUSE_LERP,
7
+ MOUSE_LERP_IDLE,
8
+ MS_PER_SECOND,
9
+ RENDER_SCALE,
10
+ type ShaderSpec,
11
+ VERTEX_SHADER,
12
+ } from './halftone-config'
12
13
 
13
14
  const FRAGMENT_SHADER = `
14
15
  precision highp float;
@@ -77,7 +78,6 @@ void main() {
77
78
 
78
79
  vec2 ap = vec2(uv.x * aspect, uv.y);
79
80
 
80
- // ═══ SHARED MOUSE WARP ═══
81
81
  vec2 scaledP = ap * 2.5;
82
82
  vec2 mUV = u_mouse;
83
83
  mUV.x *= aspect;
@@ -97,7 +97,6 @@ void main() {
97
97
  warp += normalize(cD + 0.001) * sin(cDist * 12.0 - time * 8.0) * u_click * 0.4 * exp(-cDist * 2.0);
98
98
  }
99
99
 
100
- // ═══ NOISE LAYER ═══
101
100
  vec2 np = scaledP + warp;
102
101
  vec2 q = vec2(fbm(np + t * 0.3), fbm(np + vec2(5.2, 1.3) + t * 0.2));
103
102
  vec2 r = vec2(
@@ -107,7 +106,6 @@ void main() {
107
106
  float f = fbm(np + 4.0 * r);
108
107
  float noise = clamp(f*f*f + 0.6*f*f + 0.5*f, 0.0, 1.0) * 0.15;
109
108
 
110
- // ═══ CONTRIBUTION GRID ═══
111
109
  vec2 gp = ap;
112
110
  gp += vec2(snoise(ap * 3.0 + t), snoise(ap * 3.0 + t + 50.0)) * 0.004;
113
111
  gp += warp * 0.04;
@@ -135,173 +133,163 @@ void main() {
135
133
  float gridReveal = smoothstep(0.5, 3.0, time);
136
134
  float grid = inSquare * level * gFade * gridReveal * 0.10;
137
135
 
138
- // ═══ COMPOSE ═══
139
136
  vec2 vc = uv - 0.5;
140
137
  float vignette = 1.0 - dot(vc, vc) * 0.3;
141
138
 
142
139
  float col = noise * vignette + grid;
143
140
  gl_FragColor = vec4(vec3(col), 1.0);
144
- }`;
145
-
146
- const RENDER_SCALE = 0.5;
147
- const MS_PER_SECOND = 1000;
148
- const MOUSE_LERP = 0.08;
149
- const MOUSE_LERP_IDLE = 0.02;
150
- const CLICK_DECAY = 0.96;
151
- const FULLSCREEN_VERTEX_COUNT = 6;
152
-
153
- type ShaderSpec = { readonly type: number; readonly source: string };
141
+ }`
154
142
 
155
143
  export function HalftoneBackground(): React.JSX.Element {
156
- const canvasRef = useRef<HTMLCanvasElement>(null);
157
- const wrapperRef = useRef<HTMLDivElement>(null);
158
- const frameRef = useRef<number>(0);
159
- const mouseRef = useRef({ x: -1, y: -1 });
160
- const smoothMouseRef = useRef({ x: -1, y: -1 });
161
- const clickRef = useRef({ strength: 0, x: 0, y: 0 });
162
-
163
- useEffect(() => {
164
- const canvas = canvasRef.current;
165
- const wrapper = wrapperRef.current;
166
- if (!canvas || !wrapper) {
167
- return;
168
- }
169
-
170
- const glContext = canvas.getContext("webgl", {
171
- alpha: false,
172
- antialias: false,
173
- preserveDrawingBuffer: false,
174
- });
175
- if (!glContext) {
176
- return;
177
- }
178
-
179
- const createShader = ({ type, source }: ShaderSpec) => {
180
- const shader = glContext!.createShader(type)!;
181
- glContext!.shaderSource(shader, source);
182
- glContext!.compileShader(shader);
183
- return shader;
184
- };
185
-
186
- const vertexShader = createShader({
187
- type: glContext.VERTEX_SHADER,
188
- source: VERTEX_SHADER,
189
- });
190
- const fragmentShader = createShader({
191
- type: glContext.FRAGMENT_SHADER,
192
- source: FRAGMENT_SHADER,
193
- });
194
-
195
- const program = glContext.createProgram()!;
196
- glContext.attachShader(program, vertexShader);
197
- glContext.attachShader(program, fragmentShader);
198
- glContext.linkProgram(program);
199
- glContext.useProgram(program);
200
-
201
- const buffer = glContext.createBuffer();
202
- glContext.bindBuffer(glContext.ARRAY_BUFFER, buffer);
203
- glContext.bufferData(
204
- glContext.ARRAY_BUFFER,
205
- new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
206
- glContext.STATIC_DRAW,
207
- );
208
-
209
- const aPosition = glContext.getAttribLocation(program, "a_position");
210
- glContext.enableVertexAttribArray(aPosition);
211
- glContext.vertexAttribPointer(aPosition, 2, glContext.FLOAT, false, 0, 0);
212
-
213
- const uTime = glContext.getUniformLocation(program, "u_time");
214
- const uResolution = glContext.getUniformLocation(program, "u_resolution");
215
- const uMouse = glContext.getUniformLocation(program, "u_mouse");
216
- const uClick = glContext.getUniformLocation(program, "u_click");
217
- const uClickPos = glContext.getUniformLocation(program, "u_clickPos");
218
-
219
- const startTime = performance.now();
220
-
221
- const resize = () => {
222
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
223
- const scale = RENDER_SCALE * dpr;
224
- canvas.width = canvas.offsetWidth * scale;
225
- canvas.height = canvas.offsetHeight * scale;
226
- glContext.viewport(0, 0, canvas.width, canvas.height);
227
- };
228
-
229
- resize();
230
- window.addEventListener("resize", resize);
231
-
232
- const onMouseMove = (event: MouseEvent) => {
233
- const rect = wrapper.getBoundingClientRect();
234
- mouseRef.current.x = (event.clientX - rect.left) / rect.width;
235
- mouseRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height;
236
- };
237
-
238
- const onMouseLeave = () => {
239
- mouseRef.current.x = -1;
240
- mouseRef.current.y = -1;
241
- };
242
-
243
- const onClick = (event: MouseEvent) => {
244
- const rect = wrapper.getBoundingClientRect();
245
- clickRef.current.x = (event.clientX - rect.left) / rect.width;
246
- clickRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height;
247
- clickRef.current.strength = 1.0;
248
- };
249
-
250
- window.addEventListener("mousemove", onMouseMove, { passive: true });
251
- wrapper.addEventListener("mouseleave", onMouseLeave);
252
- wrapper.addEventListener("click", onClick);
253
-
254
- const draw = () => {
255
- const elapsed = (performance.now() - startTime) / MS_PER_SECOND;
256
-
257
- const target = mouseRef.current;
258
- const smooth = smoothMouseRef.current;
259
- if (target.x < 0) {
260
- smooth.x += (target.x - smooth.x) * MOUSE_LERP_IDLE;
261
- smooth.y += (target.y - smooth.y) * MOUSE_LERP_IDLE;
262
- } else {
263
- smooth.x += (target.x - smooth.x) * MOUSE_LERP;
264
- smooth.y += (target.y - smooth.y) * MOUSE_LERP;
265
- }
266
-
267
- clickRef.current.strength *= CLICK_DECAY;
268
-
269
- glContext.uniform1f(uTime, elapsed);
270
- glContext.uniform2f(uResolution, canvas.width, canvas.height);
271
- glContext.uniform2f(uMouse, smooth.x, smooth.y);
272
- glContext.uniform1f(uClick, clickRef.current.strength);
273
- glContext.uniform2f(uClickPos, clickRef.current.x, clickRef.current.y);
274
- glContext.drawArrays(glContext.TRIANGLES, 0, FULLSCREEN_VERTEX_COUNT);
275
- frameRef.current = requestAnimationFrame(draw);
276
- };
277
-
278
- frameRef.current = requestAnimationFrame(draw);
279
-
280
- return () => {
281
- cancelAnimationFrame(frameRef.current);
282
- window.removeEventListener("resize", resize);
283
- window.removeEventListener("mousemove", onMouseMove);
284
- wrapper.removeEventListener("mouseleave", onMouseLeave);
285
- wrapper.removeEventListener("click", onClick);
286
- glContext.deleteProgram(program);
287
- glContext.deleteShader(vertexShader);
288
- glContext.deleteShader(fragmentShader);
289
- glContext.deleteBuffer(buffer);
290
- };
291
- }, []);
292
-
293
- return (
294
- <div
295
- ref={wrapperRef}
296
- className="absolute inset-0 overflow-hidden"
297
- style={{ background: "var(--shader-bg)" }}
298
- aria-hidden="true"
299
- >
300
- <canvas
301
- ref={canvasRef}
302
- className="w-full h-full"
303
- style={{ filter: "var(--shader-filter)" }}
304
- />
305
- </div>
306
- );
144
+ const canvasRef = useRef<HTMLCanvasElement>(null)
145
+ const wrapperRef = useRef<HTMLDivElement>(null)
146
+ const frameRef = useRef<number>(0)
147
+ const mouseRef = useRef({ x: -1, y: -1 })
148
+ const smoothMouseRef = useRef({ x: -1, y: -1 })
149
+ const clickRef = useRef({ strength: 0, x: 0, y: 0 })
150
+
151
+ useEffect(() => {
152
+ const canvas = canvasRef.current
153
+ const wrapper = wrapperRef.current
154
+ if (!canvas || !wrapper) {
155
+ return
156
+ }
157
+
158
+ const glContext = canvas.getContext('webgl', {
159
+ alpha: false,
160
+ antialias: false,
161
+ preserveDrawingBuffer: false,
162
+ })
163
+ if (!glContext) {
164
+ return
165
+ }
166
+
167
+ const createShader = ({ type, source }: ShaderSpec) => {
168
+ const shader = glContext!.createShader(type)!
169
+ glContext!.shaderSource(shader, source)
170
+ glContext!.compileShader(shader)
171
+ return shader
172
+ }
173
+
174
+ const vertexShader = createShader({
175
+ type: glContext.VERTEX_SHADER,
176
+ source: VERTEX_SHADER,
177
+ })
178
+ const fragmentShader = createShader({
179
+ type: glContext.FRAGMENT_SHADER,
180
+ source: FRAGMENT_SHADER,
181
+ })
182
+
183
+ const program = glContext.createProgram()!
184
+ glContext.attachShader(program, vertexShader)
185
+ glContext.attachShader(program, fragmentShader)
186
+ glContext.linkProgram(program)
187
+ glContext.useProgram(program)
188
+
189
+ const buffer = glContext.createBuffer()
190
+ glContext.bindBuffer(glContext.ARRAY_BUFFER, buffer)
191
+ glContext.bufferData(
192
+ glContext.ARRAY_BUFFER,
193
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
194
+ glContext.STATIC_DRAW,
195
+ )
196
+
197
+ const aPosition = glContext.getAttribLocation(program, 'a_position')
198
+ glContext.enableVertexAttribArray(aPosition)
199
+ glContext.vertexAttribPointer(aPosition, 2, glContext.FLOAT, false, 0, 0)
200
+
201
+ const uTime = glContext.getUniformLocation(program, 'u_time')
202
+ const uResolution = glContext.getUniformLocation(program, 'u_resolution')
203
+ const uMouse = glContext.getUniformLocation(program, 'u_mouse')
204
+ const uClick = glContext.getUniformLocation(program, 'u_click')
205
+ const uClickPos = glContext.getUniformLocation(program, 'u_clickPos')
206
+
207
+ const startTime = performance.now()
208
+
209
+ const resize = () => {
210
+ const dpr = Math.min(window.devicePixelRatio || 1, 2)
211
+ const scale = RENDER_SCALE * dpr
212
+ canvas.width = canvas.offsetWidth * scale
213
+ canvas.height = canvas.offsetHeight * scale
214
+ glContext.viewport(0, 0, canvas.width, canvas.height)
215
+ }
216
+
217
+ resize()
218
+ window.addEventListener('resize', resize)
219
+
220
+ const onMouseMove = (event: MouseEvent) => {
221
+ const rect = wrapper.getBoundingClientRect()
222
+ mouseRef.current.x = (event.clientX - rect.left) / rect.width
223
+ mouseRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height
224
+ }
225
+
226
+ const onMouseLeave = () => {
227
+ mouseRef.current.x = -1
228
+ mouseRef.current.y = -1
229
+ }
230
+
231
+ const onClick = (event: MouseEvent) => {
232
+ const rect = wrapper.getBoundingClientRect()
233
+ clickRef.current.x = (event.clientX - rect.left) / rect.width
234
+ clickRef.current.y = 1.0 - (event.clientY - rect.top) / rect.height
235
+ clickRef.current.strength = 1.0
236
+ }
237
+
238
+ window.addEventListener('mousemove', onMouseMove, { passive: true })
239
+ wrapper.addEventListener('mouseleave', onMouseLeave)
240
+ wrapper.addEventListener('click', onClick)
241
+
242
+ const draw = () => {
243
+ const elapsed = (performance.now() - startTime) / MS_PER_SECOND
244
+
245
+ const target = mouseRef.current
246
+ const smooth = smoothMouseRef.current
247
+ if (target.x < 0) {
248
+ smooth.x += (target.x - smooth.x) * MOUSE_LERP_IDLE
249
+ smooth.y += (target.y - smooth.y) * MOUSE_LERP_IDLE
250
+ } else {
251
+ smooth.x += (target.x - smooth.x) * MOUSE_LERP
252
+ smooth.y += (target.y - smooth.y) * MOUSE_LERP
253
+ }
254
+
255
+ clickRef.current.strength *= CLICK_DECAY
256
+
257
+ glContext.uniform1f(uTime, elapsed)
258
+ glContext.uniform2f(uResolution, canvas.width, canvas.height)
259
+ glContext.uniform2f(uMouse, smooth.x, smooth.y)
260
+ glContext.uniform1f(uClick, clickRef.current.strength)
261
+ glContext.uniform2f(uClickPos, clickRef.current.x, clickRef.current.y)
262
+ glContext.drawArrays(glContext.TRIANGLES, 0, FULLSCREEN_VERTEX_COUNT)
263
+ frameRef.current = requestAnimationFrame(draw)
264
+ }
265
+
266
+ frameRef.current = requestAnimationFrame(draw)
267
+
268
+ return () => {
269
+ cancelAnimationFrame(frameRef.current)
270
+ window.removeEventListener('resize', resize)
271
+ window.removeEventListener('mousemove', onMouseMove)
272
+ wrapper.removeEventListener('mouseleave', onMouseLeave)
273
+ wrapper.removeEventListener('click', onClick)
274
+ glContext.deleteProgram(program)
275
+ glContext.deleteShader(vertexShader)
276
+ glContext.deleteShader(fragmentShader)
277
+ glContext.deleteBuffer(buffer)
278
+ }
279
+ }, [])
280
+
281
+ return (
282
+ <div
283
+ ref={wrapperRef}
284
+ className="absolute inset-0 overflow-hidden"
285
+ style={{ background: 'var(--shader-bg)' }}
286
+ aria-hidden="true"
287
+ >
288
+ <canvas
289
+ ref={canvasRef}
290
+ className="w-full h-full"
291
+ style={{ filter: 'var(--shader-filter)' }}
292
+ />
293
+ </div>
294
+ )
307
295
  }
@@ -0,0 +1,14 @@
1
+ export const VERTEX_SHADER = `
2
+ attribute vec2 a_position;
3
+ void main() {
4
+ gl_Position = vec4(a_position, 0.0, 1.0);
5
+ }`
6
+
7
+ export const RENDER_SCALE = 0.5
8
+ export const MS_PER_SECOND = 1000
9
+ export const MOUSE_LERP = 0.08
10
+ export const MOUSE_LERP_IDLE = 0.02
11
+ export const CLICK_DECAY = 0.96
12
+ export const FULLSCREEN_VERTEX_COUNT = 6
13
+
14
+ export type ShaderSpec = { readonly type: number; readonly source: string }