godlights 0.1.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.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # godlights
2
+
3
+ Animated god-ray / light-beam effects for React. Render stunning volumetric light scenes on a `<canvas>`, fully configurable and animatable.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install godlights
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```tsx
14
+ import { GodLights } from "godlights";
15
+ import type { SceneConfig } from "godlights";
16
+
17
+ const scene: SceneConfig = {
18
+ width: 1920,
19
+ height: 1080,
20
+ noise: 8,
21
+ grainSize: 1,
22
+ layers: [
23
+ {
24
+ id: "bg",
25
+ type: "background",
26
+ bgType: "solid",
27
+ bgColor: "#000000",
28
+ bgColor2: "#000000",
29
+ bgGradientAngle: 180,
30
+ },
31
+ {
32
+ id: "rays-1",
33
+ name: "Rays 1",
34
+ type: "rays",
35
+ direction: 180,
36
+ spread: 120,
37
+ originX: 50,
38
+ originY: -20,
39
+ rayCount: 40,
40
+ rayWidth: 87,
41
+ divergence: 0.4,
42
+ rayLength: 0.55,
43
+ colorStart: "#ffffff",
44
+ colorEnd: "#ffffff",
45
+ opacity: 0.42,
46
+ blendMode: "screen",
47
+ fadeToTransparent: true,
48
+ blur: 17.5,
49
+ randomnessWidth: 100,
50
+ randomnessLength: 24,
51
+ randomnessAngle: 0,
52
+ seed: 1337,
53
+ },
54
+ {
55
+ id: "halo-1",
56
+ name: "Halo 1",
57
+ type: "halo",
58
+ originX: 50,
59
+ originY: 0,
60
+ color: "#ffffff",
61
+ intensity: 0.25,
62
+ size: 0.5,
63
+ blendMode: "lighter",
64
+ },
65
+ ],
66
+ };
67
+
68
+ export default function App() {
69
+ return (
70
+ <GodLights
71
+ scene={scene}
72
+ animate
73
+ className="absolute inset-0 w-full h-full"
74
+ />
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## `<GodLights>` props
80
+
81
+ | Prop | Type | Default | Description |
82
+ |------|------|---------|-------------|
83
+ | `scene` | `SceneConfig` | required | Full scene configuration |
84
+ | `animate` | `boolean` | `false` | Enable animation loop |
85
+ | `animParams` | `AnimParams` | `DEFAULT_ANIM_PARAMS` | Animation parameters (speed, amplitudes) |
86
+ | `showFps` | `boolean` | `false` | Show FPS counter overlay |
87
+ | `className` | `string` | — | CSS class for the wrapper `<div>` |
88
+ | `style` | `CSSProperties` | — | Inline style for the wrapper `<div>` |
89
+
90
+ ## `SceneConfig`
91
+
92
+ ```ts
93
+ interface SceneConfig {
94
+ width: number; // Canvas width in px (used for aspect ratio)
95
+ height: number; // Canvas height in px
96
+ noise: number; // Film grain intensity (0–100)
97
+ grainSize: number; // Film grain pixel size (1–4)
98
+ layers: Layer[]; // Ordered list of layers (bottom to top)
99
+ }
100
+ ```
101
+
102
+ ## Layer types
103
+
104
+ ### `BackgroundLayer`
105
+
106
+ ```ts
107
+ {
108
+ id: string;
109
+ type: "background";
110
+ bgType: "solid" | "linear" | "radial";
111
+ bgColor: string; // Hex color (primary)
112
+ bgColor2: string; // Hex color (secondary, for gradients)
113
+ bgGradientAngle: number; // Gradient angle in degrees
114
+ }
115
+ ```
116
+
117
+ ### `RayLayer`
118
+
119
+ ```ts
120
+ {
121
+ id: string;
122
+ name: string;
123
+ type: "rays";
124
+ direction: number; // Angle the rays point (degrees)
125
+ spread: number; // Angular spread of ray fan (degrees)
126
+ originX: number; // Origin X (% of canvas width, can be < 0 or > 100)
127
+ originY: number; // Origin Y (% of canvas height, can be < 0 or > 100)
128
+ rayCount: number; // Number of rays
129
+ rayWidth: number; // Ray width (1–200)
130
+ divergence: number; // How much rays splay out (0.1–5)
131
+ rayLength: number; // Ray length as fraction of canvas diagonal (0.1–3)
132
+ colorStart: string; // Hex color at origin
133
+ colorEnd: string; // Hex color at tip
134
+ opacity: number; // Overall opacity (0–1)
135
+ blendMode: BlendMode; // CSS blend mode
136
+ fadeToTransparent: boolean; // Fade tips to transparent
137
+ blur: number; // Gaussian blur in px (0–100)
138
+ randomnessWidth: number; // Width randomness (0–100)
139
+ randomnessLength: number; // Length randomness (0–100)
140
+ randomnessAngle: number; // Angle randomness (0–100)
141
+ seed: number; // RNG seed for reproducible randomness
142
+ }
143
+ ```
144
+
145
+ ### `HaloLayer`
146
+
147
+ ```ts
148
+ {
149
+ id: string;
150
+ name: string;
151
+ type: "halo";
152
+ originX: number; // Center X (% of canvas width)
153
+ originY: number; // Center Y (% of canvas height)
154
+ color: string; // Hex color
155
+ intensity: number; // Brightness (0–1)
156
+ size: number; // Radius as fraction of canvas diagonal (0.01–2)
157
+ blendMode: BlendMode;
158
+ }
159
+ ```
160
+
161
+ ## `AnimParams`
162
+
163
+ ```ts
164
+ interface AnimParams {
165
+ speed: number; // Animation speed (0–10)
166
+ angleAmp: number; // Ray angle oscillation amplitude (0–100)
167
+ lengthAmp: number; // Ray length oscillation amplitude (0–100)
168
+ widthAmp: number; // Ray width oscillation amplitude (0–100)
169
+ haloAmp: number; // Halo intensity oscillation amplitude (0–100)
170
+ }
171
+ ```
172
+
173
+ ## Default values
174
+
175
+ ```ts
176
+ import {
177
+ DEFAULT_SCENE,
178
+ DEFAULT_RAY_LAYER,
179
+ DEFAULT_HALO_LAYER,
180
+ DEFAULT_BACKGROUND_LAYER,
181
+ DEFAULT_ANIM_PARAMS,
182
+ } from "godlights";
183
+ ```
184
+
185
+ ## Utility exports
186
+
187
+ ```ts
188
+ import {
189
+ drawScene, // (canvas, scene, t?) => void — draw one frame
190
+ exportScene, // (scene) => Promise<Blob> — export PNG blob
191
+ exportDataURL, // (scene) => Promise<string> — export PNG data URL
192
+ BLEND_MODES, // { value, label }[] — available blend modes
193
+ } from "godlights";
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,8 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const G=require("react/jsx-runtime"),E=require("react"),K=[{label:"Normal",value:"source-over"},{label:"Lighter (additive)",value:"lighter"},{label:"Screen",value:"screen"},{label:"Overlay",value:"overlay"},{label:"Soft light",value:"soft-light"},{label:"Hard light",value:"hard-light"}],J={speed:1,angleAmp:50,lengthAmp:50,widthAmp:50,haloAmp:50},j={type:"rays",direction:200,spread:60,originX:50,originY:0,rayCount:24,rayWidth:60,divergence:1.6,rayLength:1.4,opacity:.6,blendMode:"lighter",colorStart:"#ffffff",colorEnd:"#ffffff",fadeToTransparent:!0,blur:8,randomnessWidth:30,randomnessLength:18,randomnessAngle:30,seed:1337},x={type:"halo",originX:50,originY:0,intensity:.5,size:.25,color:"#ffffff",blendMode:"lighter"},H={id:"background",type:"background",bgType:"solid",bgColor:"#000000",bgColor2:"#000000",bgGradientAngle:180},Q={width:1920,height:1080,noise:8,grainSize:1,layers:[{...H},{id:"halo-1",name:"Halo",...x},{id:"rays-1",name:"Rays",...j}]},Z={width:1920,height:1080,rayCount:24,rayWidth:60,divergence:1.6,rayLength:1.4,opacity:.6,blendMode:"lighter",haloBlendMode:"lighter",direction:200,spread:60,originX:50,originY:0,haloOriginX:50,haloOriginY:0,colorStart:"#ffd28a",colorEnd:"#ffd28a",fadeToTransparent:!0,bgType:"gradient",bgColor:"#0b1024",bgColor2:"#1a1340",bgGradientAngle:180,halo:.5,haloSize:.25,haloColor:"#ffd28a",blur:8,noise:8,grainSize:1,randomness:30,randomnessWidth:30,randomnessLength:18,randomnessAngle:30,seed:1337};function $(e){let t=e>>>0;return function(){t=t+1831565813|0;let o=t;return o=Math.imul(o^o>>>15,o|1),o^=o+Math.imul(o^o>>>7,o|61),((o^o>>>14)>>>0)/4294967296}}function Y(e){const t=e.replace("#","").trim(),o=t.length===3?t.split("").map(i=>i+i).join(""):t.padEnd(6,"0"),n=parseInt(o.substring(0,6),16);return{r:n>>16&255,g:n>>8&255,b:n&255}}function k(e,t){return`rgba(${e.r},${e.g},${e.b},${t})`}function ee(e){return(e-90)*Math.PI/180}function te(e,t,o,n){if(e.save(),e.globalCompositeOperation="source-over",n.bgType==="solid")e.fillStyle=n.bgColor,e.fillRect(0,0,t,o);else if(n.bgType==="gradient"){const i=n.bgGradientAngle*Math.PI/180,a=t/2,s=o/2,r=Math.hypot(t,o)/2,l=Math.cos(i)*r,d=Math.sin(i)*r,c=e.createLinearGradient(a-l,s-d,a+l,s+d);c.addColorStop(0,n.bgColor),c.addColorStop(1,n.bgColor2),e.fillStyle=c,e.fillRect(0,0,t,o)}e.restore()}function ne(e,t,o,n,i=0,a){if(n.intensity<=0)return;e.save(),e.globalCompositeOperation=n.blendMode;const s=n.originX/100*t,r=n.originY/100*o,l=a?a.haloAmp/50:1,d=i!==0?1+Math.sin(i*.4)*.04*l:1,c=Math.hypot(t,o)*n.size*d,m=e.createRadialGradient(s,r,0,s,r,c),f=Y(n.color);m.addColorStop(0,k(f,n.intensity)),m.addColorStop(.5,k(f,n.intensity*.4)),m.addColorStop(1,k(f,0)),e.fillStyle=m,e.beginPath(),e.arc(s,r,c,0,Math.PI*2),e.fill(),e.restore()}function P(e,t,o,n,i=0,a){const s=n.originX/100*t,r=n.originY/100*o,l=ee(n.direction),d=n.spread*Math.PI/180,c=Y(n.colorStart),m=Y(n.colorEnd),y=Math.hypot(t,o)*n.rayLength,b=$(n.seed),R=n.randomnessWidth??n.randomness??0,T=n.randomnessLength??n.randomness??0,O=n.randomnessAngle??n.randomness??0;for(let h=0;h<n.rayCount;h++){const C=n.rayCount===1?.5:h/(n.rayCount-1),M=1-b()*(R/100),L=1-b()*(T/100)*.6,v=n.rayCount>1?d/(n.rayCount-1):d,u=(b()-.5)*(O/100)*v,p=h*2.399,g=a?a.angleAmp/50:1,A=a?a.lengthAmp/50:1,_=a?a.widthAmp/50:1,S=i!==0?Math.sin(i*.6+p)*(Math.max(O,12)/100)*v*.55*g:0,w=i!==0?1+Math.sin(i*.45+p+1.2)*(R/400)*_:1,D=i!==0?1+Math.sin(i*.35+p+2.5)*(T/400)*A:1,V=l-d/2+d*C+u+S,I=Math.max(1,n.rayWidth*M*w),N=Math.max(1,I*n.divergence),z=Math.max(50,y*L*D);e.save(),e.translate(s,r),e.rotate(V);const W=e.createLinearGradient(0,0,z,0);W.addColorStop(0,k(c,n.opacity)),W.addColorStop(1,k(m,n.fadeToTransparent?0:n.opacity)),e.fillStyle=W,e.beginPath(),e.moveTo(0,-I/2),e.lineTo(z,-N/2),e.lineTo(z,N/2),e.lineTo(0,I/2),e.closePath(),e.fill(),e.restore()}}function oe(e,t,o,n,i=0,a){if(n.blur>0){const s=new OffscreenCanvas(t,o),r=s.getContext("2d");if(!r)return;P(r,t,o,n,i,a),e.save(),e.globalCompositeOperation=n.blendMode,e.filter=`blur(${n.blur}px)`,e.drawImage(s,0,0),e.restore()}else e.save(),e.globalCompositeOperation=n.blendMode,P(e,t,o,n,i,a),e.restore()}function U(e,t,o=0,n,i=!1){const a=e.getContext("2d");if(!a)return;const{width:s,height:r}=e;a.clearRect(0,0,s,r);for(const l of t.layers)l.type==="background"?te(a,s,r,l):l.type==="halo"?ne(a,s,r,l,o,n):l.type==="rays"&&oe(a,s,r,l,o,n);!i&&t.noise>0&&re(a,s,r,t.noise,t.grainSize)}function B(e,t){U(e,{width:t.width,height:t.height,noise:t.noise,grainSize:t.grainSize,layers:[{id:"background",type:"background",bgType:t.bgType,bgColor:t.bgColor,bgColor2:t.bgColor2,bgGradientAngle:t.bgGradientAngle},{id:"halo-legacy",type:"halo",name:"Halo",originX:t.haloOriginX,originY:t.haloOriginY,intensity:t.halo,size:t.haloSize,color:t.haloColor,blendMode:t.haloBlendMode},{id:"rays-legacy",type:"rays",name:"Rays",direction:t.direction,spread:t.spread,originX:t.originX,originY:t.originY,rayCount:t.rayCount,rayWidth:t.rayWidth,divergence:t.divergence,rayLength:t.rayLength,opacity:t.opacity,blendMode:t.blendMode,colorStart:t.colorStart,colorEnd:t.colorEnd,fadeToTransparent:t.fadeToTransparent,blur:t.blur,randomness:t.randomness,randomnessWidth:t.randomnessWidth,randomnessLength:t.randomnessLength,randomnessAngle:t.randomnessAngle,seed:t.seed}]})}function re(e,t,o,n,i){const a=Math.max(1,Math.floor(i)),s=e.getImageData(0,0,t,o),r=s.data,l=n/100*60;if(a===1)for(let d=0;d<r.length;d+=4){if(r[d+3]===0)continue;const c=(Math.random()-.5)*2*l;r[d]=F(r[d]+c),r[d+1]=F(r[d+1]+c),r[d+2]=F(r[d+2]+c)}else for(let d=0;d<o;d+=a)for(let c=0;c<t;c+=a){const m=(Math.random()-.5)*2*l;for(let f=0;f<a&&d+f<o;f++)for(let y=0;y<a&&c+y<t;y++){const b=((d+f)*t+(c+y))*4;r[b+3]!==0&&(r[b]=F(r[b]+m),r[b+1]=F(r[b+1]+m),r[b+2]=F(r[b+2]+m))}}e.putImageData(s,0,0)}function F(e){return e<0?0:e>255?255:e}async function ae(e,t,o=.95){const n=document.createElement("canvas");return n.width=e.width,n.height=e.height,U(n,e),new Promise((i,a)=>{n.toBlob(s=>{s?i(s):a(new Error("Failed to generate image"))},t,o)})}async function se(e){const t=document.createElement("canvas");return t.width=e.width,t.height=e.height,U(t,e),`background-image: url("${t.toDataURL("image/png")}");
2
+ background-size: cover;
3
+ background-position: center;
4
+ background-repeat: no-repeat;`}async function ie(e,t,o=.95){const n=document.createElement("canvas");return n.width=e.width,n.height=e.height,B(n,e),new Promise((i,a)=>{n.toBlob(s=>{s?i(s):a(new Error("Failed to generate image"))},t,o)})}async function q(e,t,o=.95){const n=document.createElement("canvas");return n.width=e.width,n.height=e.height,B(n,e),n.toDataURL(t,o)}async function de(e){return`background-image: url("${await q(e,"image/png")}");
5
+ background-size: cover;
6
+ background-position: center;
7
+ background-repeat: no-repeat;`}const X={position:"absolute",top:0,right:0,bottom:0,left:0,width:"100%",height:"100%",display:"block"};function le({scene:e,animate:t=!1,animParams:o,showFps:n=!1,className:i,style:a}){const s=E.useRef(null),r=E.useRef(null),l=E.useRef(e),d=E.useRef(o),[c,m]=E.useState(0),f=E.useRef([]);E.useEffect(()=>{l.current=e},[e]),E.useEffect(()=>{d.current=o},[o]),E.useEffect(()=>{if(t)return;const h=s.current;if(!h)return;const C=requestAnimationFrame(()=>{h.width=e.width,h.height=e.height,U(h,e)});return()=>cancelAnimationFrame(C)},[e,t]);const{noise:y,grainSize:b,width:R,height:T}=e;E.useEffect(()=>{if(!t)return;const h=r.current;if(!h||y<=0)return;const C=h.getContext("2d");if(!C)return;const M=R,L=T,v=C.createImageData(M,L),u=v.data,p=Math.max(1,Math.floor(b));if(p===1)for(let g=0;g<u.length;g+=4){const A=Math.random()*255|0;u[g]=u[g+1]=u[g+2]=A,u[g+3]=255}else for(let g=0;g<L;g+=p)for(let A=0;A<M;A+=p){const _=Math.random()*255|0;for(let S=0;S<p&&g+S<L;S++)for(let w=0;w<p&&A+w<M;w++){const D=((g+S)*M+(A+w))*4;u[D]=u[D+1]=u[D+2]=_,u[D+3]=255}}C.putImageData(v,0,0)},[t,y,b,R,T]),E.useEffect(()=>{if(!t){f.current=[],m(0);return}const h=s.current;if(!h)return;let C,M=0,L=null;const v=u=>{var g;if(L!==null&&(M+=(u-L)/1e3*(((g=d.current)==null?void 0:g.speed)??1)),L=u,n){const A=f.current;A.push(u);const _=u-1e3;let S=0;for(;S<A.length&&A[S]<_;)S++;f.current=A.slice(S),m(f.current.length)}const p=l.current;(h.width!==p.width||h.height!==p.height)&&(h.width=p.width,h.height=p.height),U(h,p,M,d.current,!0),C=requestAnimationFrame(v)};return C=requestAnimationFrame(v),()=>cancelAnimationFrame(C)},[t,n]);const O=t&&y>0?y/100*.35:0;return G.jsxs("div",{className:i,style:{position:"relative",overflow:"hidden",...a},children:[G.jsx("canvas",{ref:s,width:R,height:T,style:X}),t&&G.jsx("canvas",{ref:r,width:R,height:T,style:{...X,pointerEvents:"none",mixBlendMode:"overlay",opacity:O,display:y>0?"block":"none"}}),n&&t&&G.jsxs("span",{style:{position:"absolute",top:12,right:12,borderRadius:6,background:"rgba(0,0,0,0.6)",padding:"2px 8px",fontFamily:"monospace",fontSize:12,color:"rgba(255,255,255,0.7)",pointerEvents:"none",backdropFilter:"blur(4px)",WebkitBackdropFilter:"blur(4px)"},children:[c," fps"]})]})}exports.BLEND_MODES=K;exports.DEFAULT_ANIM_PARAMS=J;exports.DEFAULT_BACKGROUND_LAYER=H;exports.DEFAULT_CONFIG=Z;exports.DEFAULT_HALO_LAYER=x;exports.DEFAULT_RAY_LAYER=j;exports.DEFAULT_SCENE=Q;exports.GodLights=le;exports.buildCssSnippet=de;exports.buildSceneCssSnippet=se;exports.drawGodRays=B;exports.drawScene=U;exports.exportDataURL=q;exports.exportImage=ie;exports.exportScene=ae;exports.hexToRgb=Y;exports.mulberry32=$;
8
+ //# sourceMappingURL=godlights.js.map
@@ -0,0 +1,435 @@
1
+ import { jsxs as X, jsx as B } from "react/jsx-runtime";
2
+ import M from "react";
3
+ const re = [
4
+ { label: "Normal", value: "source-over" },
5
+ { label: "Lighter (additive)", value: "lighter" },
6
+ { label: "Screen", value: "screen" },
7
+ { label: "Overlay", value: "overlay" },
8
+ { label: "Soft light", value: "soft-light" },
9
+ { label: "Hard light", value: "hard-light" }
10
+ ], ae = {
11
+ speed: 1,
12
+ angleAmp: 50,
13
+ lengthAmp: 50,
14
+ widthAmp: 50,
15
+ haloAmp: 50
16
+ }, j = {
17
+ type: "rays",
18
+ direction: 200,
19
+ spread: 60,
20
+ originX: 50,
21
+ originY: 0,
22
+ rayCount: 24,
23
+ rayWidth: 60,
24
+ divergence: 1.6,
25
+ rayLength: 1.4,
26
+ opacity: 0.6,
27
+ blendMode: "lighter",
28
+ colorStart: "#ffffff",
29
+ colorEnd: "#ffffff",
30
+ fadeToTransparent: !0,
31
+ blur: 8,
32
+ randomnessWidth: 30,
33
+ randomnessLength: 18,
34
+ randomnessAngle: 30,
35
+ seed: 1337
36
+ }, V = {
37
+ type: "halo",
38
+ originX: 50,
39
+ originY: 0,
40
+ intensity: 0.5,
41
+ size: 0.25,
42
+ color: "#ffffff",
43
+ blendMode: "lighter"
44
+ }, q = {
45
+ id: "background",
46
+ type: "background",
47
+ bgType: "solid",
48
+ bgColor: "#000000",
49
+ bgColor2: "#000000",
50
+ bgGradientAngle: 180
51
+ }, se = {
52
+ width: 1920,
53
+ height: 1080,
54
+ noise: 8,
55
+ grainSize: 1,
56
+ layers: [
57
+ { ...q },
58
+ { id: "halo-1", name: "Halo", ...V },
59
+ { id: "rays-1", name: "Rays", ...j }
60
+ ]
61
+ }, ie = {
62
+ width: 1920,
63
+ height: 1080,
64
+ rayCount: 24,
65
+ rayWidth: 60,
66
+ divergence: 1.6,
67
+ rayLength: 1.4,
68
+ opacity: 0.6,
69
+ blendMode: "lighter",
70
+ haloBlendMode: "lighter",
71
+ direction: 200,
72
+ spread: 60,
73
+ originX: 50,
74
+ originY: 0,
75
+ haloOriginX: 50,
76
+ haloOriginY: 0,
77
+ colorStart: "#ffd28a",
78
+ colorEnd: "#ffd28a",
79
+ fadeToTransparent: !0,
80
+ bgType: "gradient",
81
+ bgColor: "#0b1024",
82
+ bgColor2: "#1a1340",
83
+ bgGradientAngle: 180,
84
+ halo: 0.5,
85
+ haloSize: 0.25,
86
+ haloColor: "#ffd28a",
87
+ blur: 8,
88
+ noise: 8,
89
+ grainSize: 1,
90
+ randomness: 30,
91
+ randomnessWidth: 30,
92
+ randomnessLength: 18,
93
+ randomnessAngle: 30,
94
+ seed: 1337
95
+ };
96
+ function x(e) {
97
+ let t = e >>> 0;
98
+ return function() {
99
+ t = t + 1831565813 | 0;
100
+ let o = t;
101
+ return o = Math.imul(o ^ o >>> 15, o | 1), o ^= o + Math.imul(o ^ o >>> 7, o | 61), ((o ^ o >>> 14) >>> 0) / 4294967296;
102
+ };
103
+ }
104
+ function Y(e) {
105
+ const t = e.replace("#", "").trim(), o = t.length === 3 ? t.split("").map((i) => i + i).join("") : t.padEnd(6, "0"), n = parseInt(o.substring(0, 6), 16);
106
+ return {
107
+ r: n >> 16 & 255,
108
+ g: n >> 8 & 255,
109
+ b: n & 255
110
+ };
111
+ }
112
+ function O(e, t) {
113
+ return `rgba(${e.r},${e.g},${e.b},${t})`;
114
+ }
115
+ function K(e) {
116
+ return (e - 90) * Math.PI / 180;
117
+ }
118
+ function J(e, t, o, n) {
119
+ if (e.save(), e.globalCompositeOperation = "source-over", n.bgType === "solid")
120
+ e.fillStyle = n.bgColor, e.fillRect(0, 0, t, o);
121
+ else if (n.bgType === "gradient") {
122
+ const i = n.bgGradientAngle * Math.PI / 180, a = t / 2, s = o / 2, r = Math.hypot(t, o) / 2, d = Math.cos(i) * r, l = Math.sin(i) * r, c = e.createLinearGradient(a - d, s - l, a + d, s + l);
123
+ c.addColorStop(0, n.bgColor), c.addColorStop(1, n.bgColor2), e.fillStyle = c, e.fillRect(0, 0, t, o);
124
+ }
125
+ e.restore();
126
+ }
127
+ function Q(e, t, o, n, i = 0, a) {
128
+ if (n.intensity <= 0) return;
129
+ e.save(), e.globalCompositeOperation = n.blendMode;
130
+ const s = n.originX / 100 * t, r = n.originY / 100 * o, d = a ? a.haloAmp / 50 : 1, l = i !== 0 ? 1 + Math.sin(i * 0.4) * 0.04 * d : 1, c = Math.hypot(t, o) * n.size * l, m = e.createRadialGradient(s, r, 0, s, r, c), p = Y(n.color);
131
+ m.addColorStop(0, O(p, n.intensity)), m.addColorStop(0.5, O(p, n.intensity * 0.4)), m.addColorStop(1, O(p, 0)), e.fillStyle = m, e.beginPath(), e.arc(s, r, c, 0, Math.PI * 2), e.fill(), e.restore();
132
+ }
133
+ function P(e, t, o, n, i = 0, a) {
134
+ const s = n.originX / 100 * t, r = n.originY / 100 * o, d = K(n.direction), l = n.spread * Math.PI / 180, c = Y(n.colorStart), m = Y(n.colorEnd), A = Math.hypot(t, o) * n.rayLength, b = x(n.seed), L = n.randomnessWidth ?? n.randomness ?? 0, w = n.randomnessLength ?? n.randomness ?? 0, G = n.randomnessAngle ?? n.randomness ?? 0;
135
+ for (let h = 0; h < n.rayCount; h++) {
136
+ const C = n.rayCount === 1 ? 0.5 : h / (n.rayCount - 1), S = 1 - b() * (L / 100), E = 1 - b() * (w / 100) * 0.6, R = n.rayCount > 1 ? l / (n.rayCount - 1) : l, g = (b() - 0.5) * (G / 100) * R, f = h * 2.399, u = a ? a.angleAmp / 50 : 1, y = a ? a.lengthAmp / 50 : 1, D = a ? a.widthAmp / 50 : 1, v = i !== 0 ? Math.sin(i * 0.6 + f) * (Math.max(G, 12) / 100) * R * 0.55 * u : 0, T = i !== 0 ? 1 + Math.sin(i * 0.45 + f + 1.2) * (L / 400) * D : 1, k = i !== 0 ? 1 + Math.sin(i * 0.35 + f + 2.5) * (w / 400) * y : 1, N = d - l / 2 + l * C + g + v, I = Math.max(1, n.rayWidth * S * T), _ = Math.max(1, I * n.divergence), U = Math.max(50, A * E * k);
137
+ e.save(), e.translate(s, r), e.rotate(N);
138
+ const W = e.createLinearGradient(0, 0, U, 0);
139
+ W.addColorStop(0, O(c, n.opacity)), W.addColorStop(
140
+ 1,
141
+ O(m, n.fadeToTransparent ? 0 : n.opacity)
142
+ ), e.fillStyle = W, e.beginPath(), e.moveTo(0, -I / 2), e.lineTo(U, -_ / 2), e.lineTo(U, _ / 2), e.lineTo(0, I / 2), e.closePath(), e.fill(), e.restore();
143
+ }
144
+ }
145
+ function Z(e, t, o, n, i = 0, a) {
146
+ if (n.blur > 0) {
147
+ const s = new OffscreenCanvas(t, o), r = s.getContext("2d");
148
+ if (!r) return;
149
+ P(r, t, o, n, i, a), e.save(), e.globalCompositeOperation = n.blendMode, e.filter = `blur(${n.blur}px)`, e.drawImage(s, 0, 0), e.restore();
150
+ } else
151
+ e.save(), e.globalCompositeOperation = n.blendMode, P(e, t, o, n, i, a), e.restore();
152
+ }
153
+ function z(e, t, o = 0, n, i = !1) {
154
+ const a = e.getContext("2d");
155
+ if (!a) return;
156
+ const { width: s, height: r } = e;
157
+ a.clearRect(0, 0, s, r);
158
+ for (const d of t.layers)
159
+ d.type === "background" ? J(a, s, r, d) : d.type === "halo" ? Q(a, s, r, d, o, n) : d.type === "rays" && Z(a, s, r, d, o, n);
160
+ !i && t.noise > 0 && ee(a, s, r, t.noise, t.grainSize);
161
+ }
162
+ function H(e, t) {
163
+ z(e, {
164
+ width: t.width,
165
+ height: t.height,
166
+ noise: t.noise,
167
+ grainSize: t.grainSize,
168
+ layers: [
169
+ {
170
+ id: "background",
171
+ type: "background",
172
+ bgType: t.bgType,
173
+ bgColor: t.bgColor,
174
+ bgColor2: t.bgColor2,
175
+ bgGradientAngle: t.bgGradientAngle
176
+ },
177
+ {
178
+ id: "halo-legacy",
179
+ type: "halo",
180
+ name: "Halo",
181
+ originX: t.haloOriginX,
182
+ originY: t.haloOriginY,
183
+ intensity: t.halo,
184
+ size: t.haloSize,
185
+ color: t.haloColor,
186
+ blendMode: t.haloBlendMode
187
+ },
188
+ {
189
+ id: "rays-legacy",
190
+ type: "rays",
191
+ name: "Rays",
192
+ direction: t.direction,
193
+ spread: t.spread,
194
+ originX: t.originX,
195
+ originY: t.originY,
196
+ rayCount: t.rayCount,
197
+ rayWidth: t.rayWidth,
198
+ divergence: t.divergence,
199
+ rayLength: t.rayLength,
200
+ opacity: t.opacity,
201
+ blendMode: t.blendMode,
202
+ colorStart: t.colorStart,
203
+ colorEnd: t.colorEnd,
204
+ fadeToTransparent: t.fadeToTransparent,
205
+ blur: t.blur,
206
+ randomness: t.randomness,
207
+ randomnessWidth: t.randomnessWidth,
208
+ randomnessLength: t.randomnessLength,
209
+ randomnessAngle: t.randomnessAngle,
210
+ seed: t.seed
211
+ }
212
+ ]
213
+ });
214
+ }
215
+ function ee(e, t, o, n, i) {
216
+ const a = Math.max(1, Math.floor(i)), s = e.getImageData(0, 0, t, o), r = s.data, d = n / 100 * 60;
217
+ if (a === 1)
218
+ for (let l = 0; l < r.length; l += 4) {
219
+ if (r[l + 3] === 0) continue;
220
+ const c = (Math.random() - 0.5) * 2 * d;
221
+ r[l] = F(r[l] + c), r[l + 1] = F(r[l + 1] + c), r[l + 2] = F(r[l + 2] + c);
222
+ }
223
+ else
224
+ for (let l = 0; l < o; l += a)
225
+ for (let c = 0; c < t; c += a) {
226
+ const m = (Math.random() - 0.5) * 2 * d;
227
+ for (let p = 0; p < a && l + p < o; p++)
228
+ for (let A = 0; A < a && c + A < t; A++) {
229
+ const b = ((l + p) * t + (c + A)) * 4;
230
+ r[b + 3] !== 0 && (r[b] = F(r[b] + m), r[b + 1] = F(r[b + 1] + m), r[b + 2] = F(r[b + 2] + m));
231
+ }
232
+ }
233
+ e.putImageData(s, 0, 0);
234
+ }
235
+ function F(e) {
236
+ return e < 0 ? 0 : e > 255 ? 255 : e;
237
+ }
238
+ async function le(e, t, o = 0.95) {
239
+ const n = document.createElement("canvas");
240
+ return n.width = e.width, n.height = e.height, z(n, e), new Promise((i, a) => {
241
+ n.toBlob(
242
+ (s) => {
243
+ s ? i(s) : a(new Error("Failed to generate image"));
244
+ },
245
+ t,
246
+ o
247
+ );
248
+ });
249
+ }
250
+ async function de(e) {
251
+ const t = document.createElement("canvas");
252
+ return t.width = e.width, t.height = e.height, z(t, e), `background-image: url("${t.toDataURL("image/png")}");
253
+ background-size: cover;
254
+ background-position: center;
255
+ background-repeat: no-repeat;`;
256
+ }
257
+ async function ce(e, t, o = 0.95) {
258
+ const n = document.createElement("canvas");
259
+ return n.width = e.width, n.height = e.height, H(n, e), new Promise((i, a) => {
260
+ n.toBlob(
261
+ (s) => {
262
+ s ? i(s) : a(new Error("Failed to generate image"));
263
+ },
264
+ t,
265
+ o
266
+ );
267
+ });
268
+ }
269
+ async function te(e, t, o = 0.95) {
270
+ const n = document.createElement("canvas");
271
+ return n.width = e.width, n.height = e.height, H(n, e), n.toDataURL(t, o);
272
+ }
273
+ async function he(e) {
274
+ return `background-image: url("${await te(e, "image/png")}");
275
+ background-size: cover;
276
+ background-position: center;
277
+ background-repeat: no-repeat;`;
278
+ }
279
+ const $ = {
280
+ position: "absolute",
281
+ top: 0,
282
+ right: 0,
283
+ bottom: 0,
284
+ left: 0,
285
+ width: "100%",
286
+ height: "100%",
287
+ display: "block"
288
+ };
289
+ function ge({
290
+ scene: e,
291
+ animate: t = !1,
292
+ animParams: o,
293
+ showFps: n = !1,
294
+ className: i,
295
+ style: a
296
+ }) {
297
+ const s = M.useRef(null), r = M.useRef(null), d = M.useRef(e), l = M.useRef(o), [c, m] = M.useState(0), p = M.useRef([]);
298
+ M.useEffect(() => {
299
+ d.current = e;
300
+ }, [e]), M.useEffect(() => {
301
+ l.current = o;
302
+ }, [o]), M.useEffect(() => {
303
+ if (t) return;
304
+ const h = s.current;
305
+ if (!h) return;
306
+ const C = requestAnimationFrame(() => {
307
+ h.width = e.width, h.height = e.height, z(h, e);
308
+ });
309
+ return () => cancelAnimationFrame(C);
310
+ }, [e, t]);
311
+ const { noise: A, grainSize: b, width: L, height: w } = e;
312
+ M.useEffect(() => {
313
+ if (!t) return;
314
+ const h = r.current;
315
+ if (!h || A <= 0) return;
316
+ const C = h.getContext("2d");
317
+ if (!C) return;
318
+ const S = L, E = w, R = C.createImageData(S, E), g = R.data, f = Math.max(1, Math.floor(b));
319
+ if (f === 1)
320
+ for (let u = 0; u < g.length; u += 4) {
321
+ const y = Math.random() * 255 | 0;
322
+ g[u] = g[u + 1] = g[u + 2] = y, g[u + 3] = 255;
323
+ }
324
+ else
325
+ for (let u = 0; u < E; u += f)
326
+ for (let y = 0; y < S; y += f) {
327
+ const D = Math.random() * 255 | 0;
328
+ for (let v = 0; v < f && u + v < E; v++)
329
+ for (let T = 0; T < f && y + T < S; T++) {
330
+ const k = ((u + v) * S + (y + T)) * 4;
331
+ g[k] = g[k + 1] = g[k + 2] = D, g[k + 3] = 255;
332
+ }
333
+ }
334
+ C.putImageData(R, 0, 0);
335
+ }, [t, A, b, L, w]), M.useEffect(() => {
336
+ if (!t) {
337
+ p.current = [], m(0);
338
+ return;
339
+ }
340
+ const h = s.current;
341
+ if (!h) return;
342
+ let C, S = 0, E = null;
343
+ const R = (g) => {
344
+ var u;
345
+ if (E !== null && (S += (g - E) / 1e3 * (((u = l.current) == null ? void 0 : u.speed) ?? 1)), E = g, n) {
346
+ const y = p.current;
347
+ y.push(g);
348
+ const D = g - 1e3;
349
+ let v = 0;
350
+ for (; v < y.length && y[v] < D; ) v++;
351
+ p.current = y.slice(v), m(p.current.length);
352
+ }
353
+ const f = d.current;
354
+ (h.width !== f.width || h.height !== f.height) && (h.width = f.width, h.height = f.height), z(h, f, S, l.current, !0), C = requestAnimationFrame(R);
355
+ };
356
+ return C = requestAnimationFrame(R), () => cancelAnimationFrame(C);
357
+ }, [t, n]);
358
+ const G = t && A > 0 ? A / 100 * 0.35 : 0;
359
+ return /* @__PURE__ */ X(
360
+ "div",
361
+ {
362
+ className: i,
363
+ style: { position: "relative", overflow: "hidden", ...a },
364
+ children: [
365
+ /* @__PURE__ */ B(
366
+ "canvas",
367
+ {
368
+ ref: s,
369
+ width: L,
370
+ height: w,
371
+ style: $
372
+ }
373
+ ),
374
+ t && /* @__PURE__ */ B(
375
+ "canvas",
376
+ {
377
+ ref: r,
378
+ width: L,
379
+ height: w,
380
+ style: {
381
+ ...$,
382
+ pointerEvents: "none",
383
+ mixBlendMode: "overlay",
384
+ opacity: G,
385
+ display: A > 0 ? "block" : "none"
386
+ }
387
+ }
388
+ ),
389
+ n && t && /* @__PURE__ */ X(
390
+ "span",
391
+ {
392
+ style: {
393
+ position: "absolute",
394
+ top: 12,
395
+ right: 12,
396
+ borderRadius: 6,
397
+ background: "rgba(0,0,0,0.6)",
398
+ padding: "2px 8px",
399
+ fontFamily: "monospace",
400
+ fontSize: 12,
401
+ color: "rgba(255,255,255,0.7)",
402
+ pointerEvents: "none",
403
+ backdropFilter: "blur(4px)",
404
+ WebkitBackdropFilter: "blur(4px)"
405
+ },
406
+ children: [
407
+ c,
408
+ " fps"
409
+ ]
410
+ }
411
+ )
412
+ ]
413
+ }
414
+ );
415
+ }
416
+ export {
417
+ re as BLEND_MODES,
418
+ ae as DEFAULT_ANIM_PARAMS,
419
+ q as DEFAULT_BACKGROUND_LAYER,
420
+ ie as DEFAULT_CONFIG,
421
+ V as DEFAULT_HALO_LAYER,
422
+ j as DEFAULT_RAY_LAYER,
423
+ se as DEFAULT_SCENE,
424
+ ge as GodLights,
425
+ he as buildCssSnippet,
426
+ de as buildSceneCssSnippet,
427
+ H as drawGodRays,
428
+ z as drawScene,
429
+ te as exportDataURL,
430
+ ce as exportImage,
431
+ le as exportScene,
432
+ Y as hexToRgb,
433
+ x as mulberry32
434
+ };
435
+ //# sourceMappingURL=godlights.mjs.map
@@ -0,0 +1,181 @@
1
+ import { default as default_2 } from 'react';
2
+ import { JSX as JSX_2 } from 'react/jsx-runtime';
3
+
4
+ /** Parameters that control the animation loop — not persisted in scene. */
5
+ export declare interface AnimParams {
6
+ /** Time multiplier — higher = faster (default 1). */
7
+ speed: number;
8
+ /** Angle swing intensity 0–100 (default 50). */
9
+ angleAmp: number;
10
+ /** Ray length oscillation intensity 0–100 (default 50). */
11
+ lengthAmp: number;
12
+ /** Ray width oscillation intensity 0–100 (default 50). */
13
+ widthAmp: number;
14
+ /** Halo pulse intensity 0–100 (default 50). */
15
+ haloAmp: number;
16
+ }
17
+
18
+ export declare interface BackgroundLayer {
19
+ id: "background";
20
+ type: "background";
21
+ bgType: BackgroundType;
22
+ bgColor: string;
23
+ bgColor2: string;
24
+ bgGradientAngle: number;
25
+ }
26
+
27
+ export declare type BackgroundType = "transparent" | "solid" | "gradient";
28
+
29
+ export declare const BLEND_MODES: {
30
+ label: string;
31
+ value: BlendMode;
32
+ }[];
33
+
34
+ /**
35
+ * God Rays / Light Rays rendering engine
36
+ * Supports multiple independent ray and halo layers per scene.
37
+ */
38
+ export declare type BlendMode = "source-over" | "lighter" | "screen" | "overlay" | "soft-light" | "hard-light";
39
+
40
+ export declare function buildCssSnippet(config: GodRaysConfig): Promise<string>;
41
+
42
+ export declare function buildSceneCssSnippet(scene: SceneConfig): Promise<string>;
43
+
44
+ export declare const DEFAULT_ANIM_PARAMS: AnimParams;
45
+
46
+ export declare const DEFAULT_BACKGROUND_LAYER: BackgroundLayer;
47
+
48
+ export declare const DEFAULT_CONFIG: GodRaysConfig;
49
+
50
+ export declare const DEFAULT_HALO_LAYER: Omit<HaloLayer, "id" | "name">;
51
+
52
+ export declare const DEFAULT_RAY_LAYER: Omit<RayLayer, "id" | "name">;
53
+
54
+ export declare const DEFAULT_SCENE: SceneConfig;
55
+
56
+ export declare function drawGodRays(canvas: HTMLCanvasElement, config: GodRaysConfig): void;
57
+
58
+ export declare function drawScene(canvas: HTMLCanvasElement, scene: SceneConfig, time?: number, anim?: AnimParams, skipGrain?: boolean): void;
59
+
60
+ export declare function exportDataURL(config: GodRaysConfig, type: "image/png" | "image/jpeg", quality?: number): Promise<string>;
61
+
62
+ export declare function exportImage(config: GodRaysConfig, type: "image/png" | "image/jpeg", quality?: number): Promise<Blob>;
63
+
64
+ export declare function exportScene(scene: SceneConfig, type: "image/png" | "image/jpeg", quality?: number): Promise<Blob>;
65
+
66
+ /**
67
+ * Standalone Godlights canvas component.
68
+ *
69
+ * @example
70
+ * <GodLights scene={myScene} className="w-full h-full" />
71
+ * <GodLights scene={myScene} animate animParams={{ speed: 1, angleAmp: 50, lengthAmp: 50, widthAmp: 50, haloAmp: 50 }} />
72
+ */
73
+ export declare function GodLights({ scene, animate, animParams, showFps, className, style, }: GodLightsProps): JSX_2.Element;
74
+
75
+ export declare interface GodLightsProps {
76
+ /** Full scene configuration — use the Godlights editor to build this. */
77
+ scene: SceneConfig;
78
+ /** Enable animation loop. */
79
+ animate?: boolean;
80
+ /** Animation parameters (speed, amplitudes). Only used when animate=true. */
81
+ animParams?: AnimParams;
82
+ /** Show FPS counter overlay. Only visible when animate=true. */
83
+ showFps?: boolean;
84
+ className?: string;
85
+ style?: default_2.CSSProperties;
86
+ }
87
+
88
+ export declare interface GodRaysConfig {
89
+ width: number;
90
+ height: number;
91
+ rayCount: number;
92
+ rayWidth: number;
93
+ divergence: number;
94
+ rayLength: number;
95
+ opacity: number;
96
+ blendMode: BlendMode;
97
+ haloBlendMode: BlendMode;
98
+ direction: number;
99
+ spread: number;
100
+ originX: number;
101
+ originY: number;
102
+ haloOriginX: number;
103
+ haloOriginY: number;
104
+ colorStart: string;
105
+ colorEnd: string;
106
+ fadeToTransparent: boolean;
107
+ bgType: BackgroundType;
108
+ bgColor: string;
109
+ bgColor2: string;
110
+ bgGradientAngle: number;
111
+ halo: number;
112
+ haloSize: number;
113
+ haloColor: string;
114
+ blur: number;
115
+ noise: number;
116
+ grainSize: number;
117
+ randomness: number;
118
+ randomnessWidth: number;
119
+ randomnessLength: number;
120
+ randomnessAngle: number;
121
+ seed: number;
122
+ }
123
+
124
+ export declare interface HaloLayer {
125
+ id: string;
126
+ type: "halo";
127
+ name: string;
128
+ originX: number;
129
+ originY: number;
130
+ intensity: number;
131
+ size: number;
132
+ color: string;
133
+ blendMode: BlendMode;
134
+ }
135
+
136
+ export declare function hexToRgb(hex: string): {
137
+ r: number;
138
+ g: number;
139
+ b: number;
140
+ };
141
+
142
+ export declare type Layer = RayLayer | HaloLayer | BackgroundLayer;
143
+
144
+ export declare function mulberry32(seed: number): () => number;
145
+
146
+ export declare interface RayLayer {
147
+ id: string;
148
+ type: "rays";
149
+ name: string;
150
+ direction: number;
151
+ spread: number;
152
+ originX: number;
153
+ originY: number;
154
+ rayCount: number;
155
+ rayWidth: number;
156
+ divergence: number;
157
+ rayLength: number;
158
+ opacity: number;
159
+ blendMode: BlendMode;
160
+ colorStart: string;
161
+ colorEnd: string;
162
+ fadeToTransparent: boolean;
163
+ blur: number;
164
+ /** @deprecated use randomnessWidth/Length/Angle */
165
+ randomness?: number;
166
+ randomnessWidth: number;
167
+ randomnessLength: number;
168
+ randomnessAngle: number;
169
+ seed: number;
170
+ }
171
+
172
+ export declare interface SceneConfig {
173
+ width: number;
174
+ height: number;
175
+ noise: number;
176
+ grainSize: number;
177
+ /** Ordered back-to-front. BackgroundLayer is always at index 0. */
178
+ layers: Layer[];
179
+ }
180
+
181
+ export { }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "godlights",
3
+ "version": "0.1.0",
4
+ "description": "Animated god ray / light beam effects for React — zero dependencies beyond React itself.",
5
+ "type": "module",
6
+ "main": "./dist/godlights.js",
7
+ "module": "./dist/godlights.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/godlights.mjs",
12
+ "require": "./dist/godlights.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist/*.js",
18
+ "dist/*.mjs",
19
+ "dist/*.d.ts",
20
+ "README.md"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/gustavoquinalha/rays-generator.git",
25
+ "directory": "packages/godlights"
26
+ },
27
+ "homepage": "https://github.com/gustavoquinalha/rays-generator/tree/main/packages/godlights#readme",
28
+ "sideEffects": false,
29
+ "scripts": {
30
+ "build": "vite build && tsc --emitDeclarationOnly --declaration --outDir dist",
31
+ "dev": "vite build --watch"
32
+ },
33
+ "peerDependencies": {
34
+ "react": ">=18",
35
+ "react-dom": ">=18"
36
+ },
37
+ "devDependencies": {
38
+ "@types/react": "^18.3.12",
39
+ "@types/react-dom": "^18.3.1",
40
+ "@vitejs/plugin-react": "^4.3.3",
41
+ "typescript": "^5.6.3",
42
+ "vite": "^5.4.10",
43
+ "vite-plugin-dts": "^4.5.4"
44
+ },
45
+ "keywords": [
46
+ "god-rays",
47
+ "light-rays",
48
+ "canvas",
49
+ "react",
50
+ "animation",
51
+ "visual-effects"
52
+ ],
53
+ "license": "MIT"
54
+ }