@jonsoc/console-app 1.1.34

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.
Files changed (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. package/vite.config.ts +25 -0
@@ -0,0 +1,820 @@
1
+ import { createSignal, createEffect, onMount, onCleanup, Accessor } from "solid-js"
2
+ import "./spotlight.css"
3
+
4
+ export interface ParticlesConfig {
5
+ enabled: boolean
6
+ amount: number
7
+ size: [number, number]
8
+ speed: number
9
+ opacity: number
10
+ drift: number
11
+ }
12
+
13
+ export interface SpotlightConfig {
14
+ placement: [number, number]
15
+ color: string
16
+ speed: number
17
+ spread: number
18
+ length: number
19
+ width: number
20
+ pulsating: false | [number, number]
21
+ distance: number
22
+ saturation: number
23
+ noiseAmount: number
24
+ distortion: number
25
+ opacity: number
26
+ particles: ParticlesConfig
27
+ }
28
+
29
+ export const defaultConfig: SpotlightConfig = {
30
+ placement: [0.5, -0.15],
31
+ color: "#ffffff",
32
+ speed: 0.8,
33
+ spread: 0.5,
34
+ length: 4.0,
35
+ width: 0.15,
36
+ pulsating: [0.95, 1.1],
37
+ distance: 3.5,
38
+ saturation: 0.35,
39
+ noiseAmount: 0.15,
40
+ distortion: 0.05,
41
+ opacity: 0.325,
42
+ particles: {
43
+ enabled: true,
44
+ amount: 70,
45
+ size: [1.25, 1.5],
46
+ speed: 0.75,
47
+ opacity: 0.9,
48
+ drift: 1.5,
49
+ },
50
+ }
51
+
52
+ export interface SpotlightAnimationState {
53
+ time: number
54
+ intensity: number
55
+ pulseValue: number
56
+ }
57
+
58
+ interface SpotlightProps {
59
+ config: Accessor<SpotlightConfig>
60
+ class?: string
61
+ onAnimationFrame?: (state: SpotlightAnimationState) => void
62
+ }
63
+
64
+ const hexToRgb = (hex: string): [number, number, number] => {
65
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
66
+ return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1]
67
+ }
68
+
69
+ const getAnchorAndDir = (
70
+ placement: [number, number],
71
+ w: number,
72
+ h: number,
73
+ ): { anchor: [number, number]; dir: [number, number] } => {
74
+ const [px, py] = placement
75
+ const outside = 0.2
76
+
77
+ let anchorX = px * w
78
+ let anchorY = py * h
79
+ let dirX = 0
80
+ let dirY = 0
81
+
82
+ const centerX = 0.5
83
+ const centerY = 0.5
84
+
85
+ if (py <= 0.25) {
86
+ anchorY = -outside * h + py * h
87
+ dirY = 1
88
+ dirX = (centerX - px) * 0.5
89
+ } else if (py >= 0.75) {
90
+ anchorY = (1 + outside) * h - (1 - py) * h
91
+ dirY = -1
92
+ dirX = (centerX - px) * 0.5
93
+ } else if (px <= 0.25) {
94
+ anchorX = -outside * w + px * w
95
+ dirX = 1
96
+ dirY = (centerY - py) * 0.5
97
+ } else if (px >= 0.75) {
98
+ anchorX = (1 + outside) * w - (1 - px) * w
99
+ dirX = -1
100
+ dirY = (centerY - py) * 0.5
101
+ } else {
102
+ dirY = 1
103
+ }
104
+
105
+ const len = Math.sqrt(dirX * dirX + dirY * dirY)
106
+ if (len > 0) {
107
+ dirX /= len
108
+ dirY /= len
109
+ }
110
+
111
+ return { anchor: [anchorX, anchorY], dir: [dirX, dirY] }
112
+ }
113
+
114
+ interface UniformData {
115
+ iTime: number
116
+ iResolution: [number, number]
117
+ lightPos: [number, number]
118
+ lightDir: [number, number]
119
+ color: [number, number, number]
120
+ speed: number
121
+ lightSpread: number
122
+ lightLength: number
123
+ sourceWidth: number
124
+ pulsating: number
125
+ pulsatingMin: number
126
+ pulsatingMax: number
127
+ fadeDistance: number
128
+ saturation: number
129
+ noiseAmount: number
130
+ distortion: number
131
+ particlesEnabled: number
132
+ particleAmount: number
133
+ particleSizeMin: number
134
+ particleSizeMax: number
135
+ particleSpeed: number
136
+ particleOpacity: number
137
+ particleDrift: number
138
+ }
139
+
140
+ const WGSL_SHADER = `
141
+ struct Uniforms {
142
+ iTime: f32,
143
+ _pad0: f32,
144
+ iResolution: vec2<f32>,
145
+ lightPos: vec2<f32>,
146
+ lightDir: vec2<f32>,
147
+ color: vec3<f32>,
148
+ speed: f32,
149
+ lightSpread: f32,
150
+ lightLength: f32,
151
+ sourceWidth: f32,
152
+ pulsating: f32,
153
+ pulsatingMin: f32,
154
+ pulsatingMax: f32,
155
+ fadeDistance: f32,
156
+ saturation: f32,
157
+ noiseAmount: f32,
158
+ distortion: f32,
159
+ particlesEnabled: f32,
160
+ particleAmount: f32,
161
+ particleSizeMin: f32,
162
+ particleSizeMax: f32,
163
+ particleSpeed: f32,
164
+ particleOpacity: f32,
165
+ particleDrift: f32,
166
+ _pad1: f32,
167
+ _pad2: f32,
168
+ };
169
+
170
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
171
+
172
+ struct VertexOutput {
173
+ @builtin(position) position: vec4<f32>,
174
+ @location(0) vUv: vec2<f32>,
175
+ };
176
+
177
+ @vertex
178
+ fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
179
+ var positions = array<vec2<f32>, 3>(
180
+ vec2<f32>(-1.0, -1.0),
181
+ vec2<f32>(3.0, -1.0),
182
+ vec2<f32>(-1.0, 3.0)
183
+ );
184
+
185
+ var output: VertexOutput;
186
+ let pos = positions[vertexIndex];
187
+ output.position = vec4<f32>(pos, 0.0, 1.0);
188
+ output.vUv = pos * 0.5 + 0.5;
189
+ return output;
190
+ }
191
+
192
+ fn hash(p: vec2<f32>) -> f32 {
193
+ let p3 = fract(p.xyx * 0.1031);
194
+ return fract((p3.x + p3.y) * p3.z + dot(p3, p3.yzx + 33.33));
195
+ }
196
+
197
+ fn hash2(p: vec2<f32>) -> vec2<f32> {
198
+ let n = sin(dot(p, vec2<f32>(41.0, 289.0)));
199
+ return fract(vec2<f32>(n * 262144.0, n * 32768.0));
200
+ }
201
+
202
+ fn fastNoise(st: vec2<f32>) -> f32 {
203
+ return fract(sin(dot(st, vec2<f32>(12.9898, 78.233))) * 43758.5453);
204
+ }
205
+
206
+ fn lightStrengthCombined(lightSource: vec2<f32>, lightRefDirection: vec2<f32>, coord: vec2<f32>) -> f32 {
207
+ let sourceToCoord = coord - lightSource;
208
+ let distSq = dot(sourceToCoord, sourceToCoord);
209
+ let distance = sqrt(distSq);
210
+
211
+ let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
212
+ let maxDistance = max(baseSize * uniforms.lightLength, 0.001);
213
+ if (distance > maxDistance) {
214
+ return 0.0;
215
+ }
216
+
217
+ let invDist = 1.0 / max(distance, 0.001);
218
+ let dirNorm = sourceToCoord * invDist;
219
+ let cosAngle = dot(dirNorm, lightRefDirection);
220
+
221
+ if (cosAngle < 0.0) {
222
+ return 0.0;
223
+ }
224
+
225
+ let side = dot(dirNorm, vec2<f32>(-lightRefDirection.y, lightRefDirection.x));
226
+ let time = uniforms.iTime;
227
+ let speed = uniforms.speed;
228
+
229
+ let asymNoise = fastNoise(vec2<f32>(side * 6.0 + time * 0.12, distance * 0.004 + cosAngle * 2.0));
230
+ let asymShift = (asymNoise - 0.5) * uniforms.distortion * 0.6;
231
+
232
+ let distortPhase = time * 1.4 + distance * 0.006 + cosAngle * 4.5 + side * 1.7;
233
+ let distortedAngle = cosAngle + uniforms.distortion * sin(distortPhase) * 0.22 + asymShift;
234
+
235
+ let flickerSeed = cosAngle * 9.0 + side * 4.0 + time * speed * 0.35;
236
+ let flicker = 0.86 + fastNoise(vec2<f32>(flickerSeed, distance * 0.01)) * 0.28;
237
+
238
+ let asymSpread = max(uniforms.lightSpread * (0.9 + (asymNoise - 0.5) * 0.25), 0.001);
239
+ let spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / asymSpread);
240
+ let lengthFalloff = clamp(1.0 - distance / maxDistance, 0.0, 1.0);
241
+
242
+ let fadeMaxDist = max(baseSize * uniforms.fadeDistance, 0.001);
243
+ let fadeFalloff = clamp((fadeMaxDist - distance) / fadeMaxDist, 0.0, 1.0);
244
+
245
+ var pulse: f32 = 1.0;
246
+ if (uniforms.pulsating > 0.5) {
247
+ let pulseCenter = (uniforms.pulsatingMin + uniforms.pulsatingMax) * 0.5;
248
+ let pulseAmplitude = (uniforms.pulsatingMax - uniforms.pulsatingMin) * 0.5;
249
+ pulse = pulseCenter + pulseAmplitude * sin(time * speed * 3.0);
250
+ }
251
+
252
+ let timeSpeed = time * speed;
253
+ let wave = 0.5
254
+ + 0.25 * sin(cosAngle * 28.0 + side * 8.0 + timeSpeed * 1.2)
255
+ + 0.18 * cos(cosAngle * 22.0 - timeSpeed * 0.95 + side * 6.0)
256
+ + 0.12 * sin(cosAngle * 35.0 + timeSpeed * 1.6 + asymNoise * 3.0);
257
+ let minStrength = 0.14 + asymNoise * 0.06;
258
+ let baseStrength = max(clamp(wave * (0.85 + asymNoise * 0.3), 0.0, 1.0), minStrength);
259
+
260
+ let lightStrength = baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse * flicker;
261
+ let ambientLight = (0.06 + asymNoise * 0.04) * lengthFalloff * fadeFalloff * spreadFactor;
262
+
263
+ return max(lightStrength, ambientLight);
264
+ }
265
+
266
+ fn particle(coord: vec2<f32>, particlePos: vec2<f32>, size: f32) -> f32 {
267
+ let delta = coord - particlePos;
268
+ let distSq = dot(delta, delta);
269
+ let sizeSq = size * size;
270
+
271
+ if (distSq > sizeSq * 9.0) {
272
+ return 0.0;
273
+ }
274
+
275
+ let d = sqrt(distSq);
276
+ let core = smoothstep(size, size * 0.35, d);
277
+ let glow = smoothstep(size * 3.0, 0.0, d) * 0.55;
278
+ return core + glow;
279
+ }
280
+
281
+ fn renderParticles(coord: vec2<f32>, lightSource: vec2<f32>, lightDir: vec2<f32>) -> f32 {
282
+ if (uniforms.particlesEnabled < 0.5 || uniforms.particleAmount < 1.0) {
283
+ return 0.0;
284
+ }
285
+
286
+ var particleSum: f32 = 0.0;
287
+ let particleCount = i32(uniforms.particleAmount);
288
+ let time = uniforms.iTime * uniforms.particleSpeed;
289
+ let perpDir = vec2<f32>(-lightDir.y, lightDir.x);
290
+ let baseSize = min(uniforms.iResolution.x, uniforms.iResolution.y);
291
+ let maxDist = max(baseSize * uniforms.lightLength, 1.0);
292
+ let spreadScale = uniforms.lightSpread * baseSize * 0.65;
293
+ let coneHalfWidth = uniforms.lightSpread * baseSize * 0.55;
294
+
295
+ for (var i: i32 = 0; i < particleCount; i = i + 1) {
296
+ let fi = f32(i);
297
+ let seed = vec2<f32>(fi * 127.1, fi * 311.7);
298
+ let rnd = hash2(seed);
299
+
300
+ let lifeDuration = 2.0 + hash(seed + vec2<f32>(19.0, 73.0)) * 3.0;
301
+ let lifeOffset = hash(seed + vec2<f32>(91.0, 37.0)) * lifeDuration;
302
+ let lifeProgress = fract((time + lifeOffset) / lifeDuration);
303
+
304
+ let fadeIn = smoothstep(0.0, 0.2, lifeProgress);
305
+ let fadeOut = 1.0 - smoothstep(0.8, 1.0, lifeProgress);
306
+ let lifeFade = fadeIn * fadeOut;
307
+ if (lifeFade < 0.01) {
308
+ continue;
309
+ }
310
+
311
+ let alongLight = rnd.x * maxDist * 0.8;
312
+ let perpOffset = (rnd.y - 0.5) * spreadScale;
313
+
314
+ let floatPhase = rnd.y * 6.28318 + fi * 0.37;
315
+ let floatSpeed = 0.35 + rnd.x * 0.9;
316
+ let drift = vec2<f32>(
317
+ sin(time * floatSpeed + floatPhase),
318
+ cos(time * floatSpeed * 0.85 + floatPhase * 1.3)
319
+ ) * uniforms.particleDrift * baseSize * 0.08;
320
+
321
+ let wobble = vec2<f32>(
322
+ sin(time * 1.4 + floatPhase * 2.1),
323
+ cos(time * 1.1 + floatPhase * 1.6)
324
+ ) * uniforms.particleDrift * baseSize * 0.03;
325
+
326
+ let flowOffset = (rnd.x - 0.5) * baseSize * 0.12 + fract(time * 0.06 + rnd.y) * baseSize * 0.1;
327
+
328
+ let basePos = lightSource + lightDir * (alongLight + flowOffset) + perpDir * perpOffset + drift + wobble;
329
+
330
+ let toParticle = basePos - lightSource;
331
+ let projLen = dot(toParticle, lightDir);
332
+ if (projLen < 0.0 || projLen > maxDist) {
333
+ continue;
334
+ }
335
+
336
+ let sideDist = abs(dot(toParticle, perpDir));
337
+ if (sideDist > coneHalfWidth) {
338
+ continue;
339
+ }
340
+
341
+ let size = mix(uniforms.particleSizeMin, uniforms.particleSizeMax, rnd.x);
342
+ let twinkle = 0.7 + 0.3 * sin(time * (1.5 + rnd.y * 2.0) + floatPhase);
343
+ let distFade = 1.0 - smoothstep(maxDist * 0.2, maxDist * 0.95, projLen);
344
+ if (distFade < 0.01) {
345
+ continue;
346
+ }
347
+
348
+ let p = particle(coord, basePos, size);
349
+ if (p > 0.0) {
350
+ particleSum = particleSum + p * lifeFade * twinkle * distFade * uniforms.particleOpacity;
351
+ if (particleSum >= 1.0) {
352
+ break;
353
+ }
354
+ }
355
+ }
356
+
357
+ return min(particleSum, 1.0);
358
+ }
359
+
360
+ @fragment
361
+ fn fragmentMain(@builtin(position) fragCoord: vec4<f32>, @location(0) vUv: vec2<f32>) -> @location(0) vec4<f32> {
362
+ let coord = vec2<f32>(fragCoord.x, fragCoord.y);
363
+
364
+ let normalizedX = (coord.x / uniforms.iResolution.x) - 0.5;
365
+ let widthOffset = -normalizedX * uniforms.sourceWidth * uniforms.iResolution.x;
366
+
367
+ let perpDir = vec2<f32>(-uniforms.lightDir.y, uniforms.lightDir.x);
368
+ let adjustedLightPos = uniforms.lightPos + perpDir * widthOffset;
369
+
370
+ let lightValue = lightStrengthCombined(adjustedLightPos, uniforms.lightDir, coord);
371
+
372
+ if (lightValue < 0.001) {
373
+ let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
374
+ if (particles < 0.001) {
375
+ return vec4<f32>(0.0, 0.0, 0.0, 0.0);
376
+ }
377
+ let particleBrightness = particles * 1.8;
378
+ return vec4<f32>(uniforms.color * particleBrightness, particles * 0.9);
379
+ }
380
+
381
+ var fragColor = vec4<f32>(lightValue, lightValue, lightValue, lightValue);
382
+
383
+ if (uniforms.noiseAmount > 0.01) {
384
+ let n = fastNoise(coord * 0.5 + uniforms.iTime * 0.5);
385
+ let grain = mix(1.0, n, uniforms.noiseAmount * 0.5);
386
+ fragColor = vec4<f32>(fragColor.rgb * grain, fragColor.a);
387
+ }
388
+
389
+ let brightness = 1.0 - (coord.y / uniforms.iResolution.y);
390
+ fragColor = vec4<f32>(
391
+ fragColor.x * (0.15 + brightness * 0.85),
392
+ fragColor.y * (0.35 + brightness * 0.65),
393
+ fragColor.z * (0.55 + brightness * 0.45),
394
+ fragColor.a
395
+ );
396
+
397
+ if (abs(uniforms.saturation - 1.0) > 0.01) {
398
+ let gray = dot(fragColor.rgb, vec3<f32>(0.299, 0.587, 0.114));
399
+ fragColor = vec4<f32>(mix(vec3<f32>(gray), fragColor.rgb, uniforms.saturation), fragColor.a);
400
+ }
401
+
402
+ fragColor = vec4<f32>(fragColor.rgb * uniforms.color, fragColor.a);
403
+
404
+ let particles = renderParticles(coord, adjustedLightPos, uniforms.lightDir);
405
+ if (particles > 0.001) {
406
+ let particleBrightness = particles * 1.8;
407
+ fragColor = vec4<f32>(fragColor.rgb + uniforms.color * particleBrightness, max(fragColor.a, particles * 0.9));
408
+ }
409
+
410
+ return fragColor;
411
+ }
412
+ `
413
+
414
+ const UNIFORM_BUFFER_SIZE = 144
415
+
416
+ function updateUniformBuffer(buffer: Float32Array, data: UniformData): void {
417
+ buffer[0] = data.iTime
418
+ buffer[2] = data.iResolution[0]
419
+ buffer[3] = data.iResolution[1]
420
+ buffer[4] = data.lightPos[0]
421
+ buffer[5] = data.lightPos[1]
422
+ buffer[6] = data.lightDir[0]
423
+ buffer[7] = data.lightDir[1]
424
+ buffer[8] = data.color[0]
425
+ buffer[9] = data.color[1]
426
+ buffer[10] = data.color[2]
427
+ buffer[11] = data.speed
428
+ buffer[12] = data.lightSpread
429
+ buffer[13] = data.lightLength
430
+ buffer[14] = data.sourceWidth
431
+ buffer[15] = data.pulsating
432
+ buffer[16] = data.pulsatingMin
433
+ buffer[17] = data.pulsatingMax
434
+ buffer[18] = data.fadeDistance
435
+ buffer[19] = data.saturation
436
+ buffer[20] = data.noiseAmount
437
+ buffer[21] = data.distortion
438
+ buffer[22] = data.particlesEnabled
439
+ buffer[23] = data.particleAmount
440
+ buffer[24] = data.particleSizeMin
441
+ buffer[25] = data.particleSizeMax
442
+ buffer[26] = data.particleSpeed
443
+ buffer[27] = data.particleOpacity
444
+ buffer[28] = data.particleDrift
445
+ }
446
+
447
+ export default function Spotlight(props: SpotlightProps) {
448
+ let containerRef: HTMLDivElement | undefined
449
+ let canvasRef: HTMLCanvasElement | null = null
450
+ let deviceRef: GPUDevice | null = null
451
+ let contextRef: GPUCanvasContext | null = null
452
+ let pipelineRef: GPURenderPipeline | null = null
453
+ let uniformBufferRef: GPUBuffer | null = null
454
+ let bindGroupRef: GPUBindGroup | null = null
455
+ let animationIdRef: number | null = null
456
+ let cleanupFunctionRef: (() => void) | null = null
457
+ let uniformDataRef: UniformData | null = null
458
+ let uniformArrayRef: Float32Array | null = null
459
+ let configRef: SpotlightConfig = props.config()
460
+ let frameCount = 0
461
+
462
+ const [isVisible, setIsVisible] = createSignal(false)
463
+
464
+ createEffect(() => {
465
+ configRef = props.config()
466
+ })
467
+
468
+ onMount(() => {
469
+ if (!containerRef) return
470
+
471
+ const observer = new IntersectionObserver(
472
+ (entries) => {
473
+ const entry = entries[0]
474
+ setIsVisible(entry.isIntersecting)
475
+ },
476
+ { threshold: 0.1 },
477
+ )
478
+
479
+ observer.observe(containerRef)
480
+
481
+ onCleanup(() => {
482
+ observer.disconnect()
483
+ })
484
+ })
485
+
486
+ createEffect(() => {
487
+ const visible = isVisible()
488
+ const config = props.config()
489
+ if (!visible || !containerRef) {
490
+ return
491
+ }
492
+
493
+ if (cleanupFunctionRef) {
494
+ cleanupFunctionRef()
495
+ cleanupFunctionRef = null
496
+ }
497
+
498
+ const initializeWebGPU = async () => {
499
+ if (!containerRef) {
500
+ return
501
+ }
502
+
503
+ await new Promise((resolve) => setTimeout(resolve, 10))
504
+
505
+ if (!containerRef) {
506
+ return
507
+ }
508
+
509
+ if (!navigator.gpu) {
510
+ console.warn("WebGPU is not supported in this browser")
511
+ return
512
+ }
513
+
514
+ const adapter = await navigator.gpu.requestAdapter({
515
+ powerPreference: "high-performance",
516
+ })
517
+ if (!adapter) {
518
+ console.warn("Failed to get WebGPU adapter")
519
+ return
520
+ }
521
+
522
+ const device = await adapter.requestDevice()
523
+ deviceRef = device
524
+
525
+ const canvas = document.createElement("canvas")
526
+ canvas.style.width = "100%"
527
+ canvas.style.height = "100%"
528
+ canvasRef = canvas
529
+
530
+ while (containerRef.firstChild) {
531
+ containerRef.removeChild(containerRef.firstChild)
532
+ }
533
+ containerRef.appendChild(canvas)
534
+
535
+ const context = canvas.getContext("webgpu")
536
+ if (!context) {
537
+ console.warn("Failed to get WebGPU context")
538
+ return
539
+ }
540
+ contextRef = context
541
+
542
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
543
+ context.configure({
544
+ device,
545
+ format: presentationFormat,
546
+ alphaMode: "premultiplied",
547
+ })
548
+
549
+ const shaderModule = device.createShaderModule({
550
+ code: WGSL_SHADER,
551
+ })
552
+
553
+ const uniformBuffer = device.createBuffer({
554
+ size: UNIFORM_BUFFER_SIZE,
555
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
556
+ })
557
+ uniformBufferRef = uniformBuffer
558
+
559
+ const bindGroupLayout = device.createBindGroupLayout({
560
+ entries: [
561
+ {
562
+ binding: 0,
563
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
564
+ buffer: { type: "uniform" },
565
+ },
566
+ ],
567
+ })
568
+
569
+ const bindGroup = device.createBindGroup({
570
+ layout: bindGroupLayout,
571
+ entries: [
572
+ {
573
+ binding: 0,
574
+ resource: { buffer: uniformBuffer },
575
+ },
576
+ ],
577
+ })
578
+ bindGroupRef = bindGroup
579
+
580
+ const pipelineLayout = device.createPipelineLayout({
581
+ bindGroupLayouts: [bindGroupLayout],
582
+ })
583
+
584
+ const pipeline = device.createRenderPipeline({
585
+ layout: pipelineLayout,
586
+ vertex: {
587
+ module: shaderModule,
588
+ entryPoint: "vertexMain",
589
+ },
590
+ fragment: {
591
+ module: shaderModule,
592
+ entryPoint: "fragmentMain",
593
+ targets: [
594
+ {
595
+ format: presentationFormat,
596
+ blend: {
597
+ color: {
598
+ srcFactor: "src-alpha",
599
+ dstFactor: "one-minus-src-alpha",
600
+ operation: "add",
601
+ },
602
+ alpha: {
603
+ srcFactor: "one",
604
+ dstFactor: "one-minus-src-alpha",
605
+ operation: "add",
606
+ },
607
+ },
608
+ },
609
+ ],
610
+ },
611
+ primitive: {
612
+ topology: "triangle-list",
613
+ },
614
+ })
615
+ pipelineRef = pipeline
616
+
617
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
618
+ const dpr = Math.min(window.devicePixelRatio, 2)
619
+ const w = wCSS * dpr
620
+ const h = hCSS * dpr
621
+ const { anchor, dir } = getAnchorAndDir(config.placement, w, h)
622
+
623
+ uniformDataRef = {
624
+ iTime: 0,
625
+ iResolution: [w, h],
626
+ lightPos: anchor,
627
+ lightDir: dir,
628
+ color: hexToRgb(config.color),
629
+ speed: config.speed,
630
+ lightSpread: config.spread,
631
+ lightLength: config.length,
632
+ sourceWidth: config.width,
633
+ pulsating: config.pulsating !== false ? 1.0 : 0.0,
634
+ pulsatingMin: config.pulsating !== false ? config.pulsating[0] : 1.0,
635
+ pulsatingMax: config.pulsating !== false ? config.pulsating[1] : 1.0,
636
+ fadeDistance: config.distance,
637
+ saturation: config.saturation,
638
+ noiseAmount: config.noiseAmount,
639
+ distortion: config.distortion,
640
+ particlesEnabled: config.particles.enabled ? 1.0 : 0.0,
641
+ particleAmount: config.particles.amount,
642
+ particleSizeMin: config.particles.size[0],
643
+ particleSizeMax: config.particles.size[1],
644
+ particleSpeed: config.particles.speed,
645
+ particleOpacity: config.particles.opacity,
646
+ particleDrift: config.particles.drift,
647
+ }
648
+
649
+ const updatePlacement = () => {
650
+ if (!containerRef || !canvasRef || !uniformDataRef) {
651
+ return
652
+ }
653
+
654
+ const dpr = Math.min(window.devicePixelRatio, 2)
655
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
656
+ const w = Math.floor(wCSS * dpr)
657
+ const h = Math.floor(hCSS * dpr)
658
+
659
+ canvasRef.width = w
660
+ canvasRef.height = h
661
+
662
+ uniformDataRef.iResolution = [w, h]
663
+
664
+ const { anchor, dir } = getAnchorAndDir(configRef.placement, w, h)
665
+ uniformDataRef.lightPos = anchor
666
+ uniformDataRef.lightDir = dir
667
+ }
668
+
669
+ const loop = (t: number) => {
670
+ if (!deviceRef || !contextRef || !pipelineRef || !uniformBufferRef || !bindGroupRef || !uniformDataRef) {
671
+ return
672
+ }
673
+
674
+ const timeSeconds = t * 0.001
675
+ uniformDataRef.iTime = timeSeconds
676
+ frameCount++
677
+
678
+ if (props.onAnimationFrame && frameCount % 2 === 0) {
679
+ const pulsatingMin = configRef.pulsating !== false ? configRef.pulsating[0] : 1.0
680
+ const pulsatingMax = configRef.pulsating !== false ? configRef.pulsating[1] : 1.0
681
+ const pulseCenter = (pulsatingMin + pulsatingMax) * 0.5
682
+ const pulseAmplitude = (pulsatingMax - pulsatingMin) * 0.5
683
+ const pulseValue =
684
+ configRef.pulsating !== false
685
+ ? pulseCenter + pulseAmplitude * Math.sin(timeSeconds * configRef.speed * 3.0)
686
+ : 1.0
687
+
688
+ const baseIntensity1 = 0.45 + 0.15 * Math.sin(timeSeconds * configRef.speed * 1.5)
689
+ const baseIntensity2 = 0.3 + 0.2 * Math.cos(timeSeconds * configRef.speed * 1.1)
690
+ const intensity = Math.max((baseIntensity1 + baseIntensity2) * pulseValue, 0.55)
691
+
692
+ props.onAnimationFrame({
693
+ time: timeSeconds,
694
+ intensity,
695
+ pulseValue: Math.max(pulseValue, 0.9),
696
+ })
697
+ }
698
+
699
+ try {
700
+ if (!uniformArrayRef) {
701
+ uniformArrayRef = new Float32Array(36)
702
+ }
703
+ updateUniformBuffer(uniformArrayRef, uniformDataRef)
704
+ deviceRef.queue.writeBuffer(uniformBufferRef, 0, uniformArrayRef.buffer)
705
+
706
+ const commandEncoder = deviceRef.createCommandEncoder()
707
+
708
+ const textureView = contextRef.getCurrentTexture().createView()
709
+
710
+ const renderPass = commandEncoder.beginRenderPass({
711
+ colorAttachments: [
712
+ {
713
+ view: textureView,
714
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
715
+ loadOp: "clear",
716
+ storeOp: "store",
717
+ },
718
+ ],
719
+ })
720
+
721
+ renderPass.setPipeline(pipelineRef)
722
+ renderPass.setBindGroup(0, bindGroupRef)
723
+ renderPass.draw(3)
724
+ renderPass.end()
725
+
726
+ deviceRef.queue.submit([commandEncoder.finish()])
727
+
728
+ animationIdRef = requestAnimationFrame(loop)
729
+ } catch (error) {
730
+ console.warn("WebGPU rendering error:", error)
731
+ return
732
+ }
733
+ }
734
+
735
+ window.addEventListener("resize", updatePlacement)
736
+ updatePlacement()
737
+ animationIdRef = requestAnimationFrame(loop)
738
+
739
+ cleanupFunctionRef = () => {
740
+ if (animationIdRef) {
741
+ cancelAnimationFrame(animationIdRef)
742
+ animationIdRef = null
743
+ }
744
+
745
+ window.removeEventListener("resize", updatePlacement)
746
+
747
+ if (uniformBufferRef) {
748
+ uniformBufferRef.destroy()
749
+ uniformBufferRef = null
750
+ }
751
+
752
+ if (deviceRef) {
753
+ deviceRef.destroy()
754
+ deviceRef = null
755
+ }
756
+
757
+ if (canvasRef && canvasRef.parentNode) {
758
+ canvasRef.parentNode.removeChild(canvasRef)
759
+ }
760
+
761
+ canvasRef = null
762
+ contextRef = null
763
+ pipelineRef = null
764
+ bindGroupRef = null
765
+ uniformDataRef = null
766
+ }
767
+ }
768
+
769
+ initializeWebGPU()
770
+
771
+ onCleanup(() => {
772
+ if (cleanupFunctionRef) {
773
+ cleanupFunctionRef()
774
+ cleanupFunctionRef = null
775
+ }
776
+ })
777
+ })
778
+
779
+ createEffect(() => {
780
+ if (!uniformDataRef || !containerRef) {
781
+ return
782
+ }
783
+
784
+ const config = props.config()
785
+
786
+ uniformDataRef.color = hexToRgb(config.color)
787
+ uniformDataRef.speed = config.speed
788
+ uniformDataRef.lightSpread = config.spread
789
+ uniformDataRef.lightLength = config.length
790
+ uniformDataRef.sourceWidth = config.width
791
+ uniformDataRef.pulsating = config.pulsating !== false ? 1.0 : 0.0
792
+ uniformDataRef.pulsatingMin = config.pulsating !== false ? config.pulsating[0] : 1.0
793
+ uniformDataRef.pulsatingMax = config.pulsating !== false ? config.pulsating[1] : 1.0
794
+ uniformDataRef.fadeDistance = config.distance
795
+ uniformDataRef.saturation = config.saturation
796
+ uniformDataRef.noiseAmount = config.noiseAmount
797
+ uniformDataRef.distortion = config.distortion
798
+ uniformDataRef.particlesEnabled = config.particles.enabled ? 1.0 : 0.0
799
+ uniformDataRef.particleAmount = config.particles.amount
800
+ uniformDataRef.particleSizeMin = config.particles.size[0]
801
+ uniformDataRef.particleSizeMax = config.particles.size[1]
802
+ uniformDataRef.particleSpeed = config.particles.speed
803
+ uniformDataRef.particleOpacity = config.particles.opacity
804
+ uniformDataRef.particleDrift = config.particles.drift
805
+
806
+ const dpr = Math.min(window.devicePixelRatio, 2)
807
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef
808
+ const { anchor, dir } = getAnchorAndDir(config.placement, wCSS * dpr, hCSS * dpr)
809
+ uniformDataRef.lightPos = anchor
810
+ uniformDataRef.lightDir = dir
811
+ })
812
+
813
+ return (
814
+ <div
815
+ ref={containerRef}
816
+ class={`spotlight-container ${props.class ?? ""}`.trim()}
817
+ style={{ opacity: props.config().opacity }}
818
+ />
819
+ )
820
+ }