@toriistudio/shader-ui 0.0.9 → 0.0.10

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/dist/index.d.mts CHANGED
@@ -313,7 +313,10 @@ type DitherStreamProps = {
313
313
  height?: string | number;
314
314
  className?: string;
315
315
  style?: React.CSSProperties;
316
+ children?: React.ReactNode;
316
317
  imageTextureSrc?: string;
318
+ backgroundImageSrc?: string;
319
+ backgroundDithered?: boolean;
317
320
  projectionSpeed?: number;
318
321
  beamSpeed?: number;
319
322
  beamDirection?: "counterclockwise" | "clockwise";
@@ -321,11 +324,21 @@ type DitherStreamProps = {
321
324
  beamCenter?: [number, number];
322
325
  beamRadius?: number;
323
326
  beamScale?: number;
324
- beamPathShape?: "circle" | "square" | "diamond" | "triangle" | "oval";
327
+ beamPathShape?: "circle" | "square" | "diamond" | "triangle" | "oval" | "custom";
328
+ beamCustomPathPoints?: Array<[number, number]>;
329
+ beamEnabled?: boolean;
325
330
  pathPos?: [number, number];
326
331
  pathAngle?: number;
327
332
  godrayIntensity?: number;
328
333
  };
329
- declare function DitherStream({ width, height, className, style, imageTextureSrc, projectionSpeed, beamSpeed, beamDirection, beamColor, beamCenter, beamRadius, beamScale, beamPathShape, pathPos, pathAngle, godrayIntensity, }: DitherStreamProps): react_jsx_runtime.JSX.Element;
334
+ declare function DitherStream({ width, height, className, style, children, imageTextureSrc, backgroundImageSrc, backgroundDithered, projectionSpeed, beamSpeed, beamDirection, beamColor, beamCenter, beamRadius, beamScale, beamPathShape, beamCustomPathPoints, beamEnabled, pathPos, pathAngle, godrayIntensity, }: DitherStreamProps): react_jsx_runtime.JSX.Element;
330
335
 
331
- export { AnimatedDrawingSVG, type CombineShaderMode, DitherPulseRing, DitherStream, EFECTO_ASCII_COMPONENT_DEFAULTS, EFECTO_ASCII_POST_PROCESSING_DEFAULTS, Efecto, type EfectoAsciiColorPalette, type EfectoAsciiStyle, type PublicPostProcessingSettings as EfectoPublicAsciiPostProcessingSettings, FractalFlower, MenuGlitch, type MenuGlitchUniforms, OranoParticles, type OranoParticlesUniforms, RippleWave, ShaderArt, type ShaderArtUniforms, Snow, WANDY_HAND_DEFAULTS, WandyHand };
336
+ type DitherStreamPathDrawerProps = {
337
+ enabled: boolean;
338
+ beamColor?: string;
339
+ backgroundImageSrc?: string;
340
+ onCommit: (points: Array<[number, number]>) => void;
341
+ };
342
+ declare function DitherStreamPathDrawer({ enabled, beamColor, backgroundImageSrc, onCommit, }: DitherStreamPathDrawerProps): react_jsx_runtime.JSX.Element | null;
343
+
344
+ export { AnimatedDrawingSVG, type CombineShaderMode, DitherPulseRing, DitherStream, DitherStreamPathDrawer, EFECTO_ASCII_COMPONENT_DEFAULTS, EFECTO_ASCII_POST_PROCESSING_DEFAULTS, Efecto, type EfectoAsciiColorPalette, type EfectoAsciiStyle, type PublicPostProcessingSettings as EfectoPublicAsciiPostProcessingSettings, FractalFlower, MenuGlitch, type MenuGlitchUniforms, OranoParticles, type OranoParticlesUniforms, RippleWave, ShaderArt, type ShaderArtUniforms, Snow, WANDY_HAND_DEFAULTS, WandyHand };
package/dist/index.d.ts CHANGED
@@ -313,7 +313,10 @@ type DitherStreamProps = {
313
313
  height?: string | number;
314
314
  className?: string;
315
315
  style?: React.CSSProperties;
316
+ children?: React.ReactNode;
316
317
  imageTextureSrc?: string;
318
+ backgroundImageSrc?: string;
319
+ backgroundDithered?: boolean;
317
320
  projectionSpeed?: number;
318
321
  beamSpeed?: number;
319
322
  beamDirection?: "counterclockwise" | "clockwise";
@@ -321,11 +324,21 @@ type DitherStreamProps = {
321
324
  beamCenter?: [number, number];
322
325
  beamRadius?: number;
323
326
  beamScale?: number;
324
- beamPathShape?: "circle" | "square" | "diamond" | "triangle" | "oval";
327
+ beamPathShape?: "circle" | "square" | "diamond" | "triangle" | "oval" | "custom";
328
+ beamCustomPathPoints?: Array<[number, number]>;
329
+ beamEnabled?: boolean;
325
330
  pathPos?: [number, number];
326
331
  pathAngle?: number;
327
332
  godrayIntensity?: number;
328
333
  };
329
- declare function DitherStream({ width, height, className, style, imageTextureSrc, projectionSpeed, beamSpeed, beamDirection, beamColor, beamCenter, beamRadius, beamScale, beamPathShape, pathPos, pathAngle, godrayIntensity, }: DitherStreamProps): react_jsx_runtime.JSX.Element;
334
+ declare function DitherStream({ width, height, className, style, children, imageTextureSrc, backgroundImageSrc, backgroundDithered, projectionSpeed, beamSpeed, beamDirection, beamColor, beamCenter, beamRadius, beamScale, beamPathShape, beamCustomPathPoints, beamEnabled, pathPos, pathAngle, godrayIntensity, }: DitherStreamProps): react_jsx_runtime.JSX.Element;
330
335
 
331
- export { AnimatedDrawingSVG, type CombineShaderMode, DitherPulseRing, DitherStream, EFECTO_ASCII_COMPONENT_DEFAULTS, EFECTO_ASCII_POST_PROCESSING_DEFAULTS, Efecto, type EfectoAsciiColorPalette, type EfectoAsciiStyle, type PublicPostProcessingSettings as EfectoPublicAsciiPostProcessingSettings, FractalFlower, MenuGlitch, type MenuGlitchUniforms, OranoParticles, type OranoParticlesUniforms, RippleWave, ShaderArt, type ShaderArtUniforms, Snow, WANDY_HAND_DEFAULTS, WandyHand };
336
+ type DitherStreamPathDrawerProps = {
337
+ enabled: boolean;
338
+ beamColor?: string;
339
+ backgroundImageSrc?: string;
340
+ onCommit: (points: Array<[number, number]>) => void;
341
+ };
342
+ declare function DitherStreamPathDrawer({ enabled, beamColor, backgroundImageSrc, onCommit, }: DitherStreamPathDrawerProps): react_jsx_runtime.JSX.Element | null;
343
+
344
+ export { AnimatedDrawingSVG, type CombineShaderMode, DitherPulseRing, DitherStream, DitherStreamPathDrawer, EFECTO_ASCII_COMPONENT_DEFAULTS, EFECTO_ASCII_POST_PROCESSING_DEFAULTS, Efecto, type EfectoAsciiColorPalette, type EfectoAsciiStyle, type PublicPostProcessingSettings as EfectoPublicAsciiPostProcessingSettings, FractalFlower, MenuGlitch, type MenuGlitchUniforms, OranoParticles, type OranoParticlesUniforms, RippleWave, ShaderArt, type ShaderArtUniforms, Snow, WANDY_HAND_DEFAULTS, WandyHand };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(src_exports, {
33
33
  AnimatedDrawingSVG: () => AnimatedDrawingSVG,
34
34
  DitherPulseRing: () => DitherPulseRing,
35
35
  DitherStream: () => DitherStream,
36
+ DitherStreamPathDrawer: () => DitherStreamPathDrawer,
36
37
  EFECTO_ASCII_COMPONENT_DEFAULTS: () => EFECTO_ASCII_COMPONENT_DEFAULTS,
37
38
  EFECTO_ASCII_POST_PROCESSING_DEFAULTS: () => EFECTO_ASCII_POST_PROCESSING_DEFAULTS,
38
39
  Efecto: () => Efecto,
@@ -1804,9 +1805,9 @@ function AsciiEffect({
1804
1805
  let viewWidth = width2;
1805
1806
  let viewHeight = height2;
1806
1807
  if (camera instanceof THREE6.PerspectiveCamera) {
1807
- const distance = Math.abs(camera.position.z - assets.mesh.position.z);
1808
+ const distance2 = Math.abs(camera.position.z - assets.mesh.position.z);
1808
1809
  const verticalFov = THREE6.MathUtils.degToRad(camera.fov);
1809
- viewHeight = 2 * Math.tan(verticalFov / 2) * distance;
1810
+ viewHeight = 2 * Math.tan(verticalFov / 2) * distance2;
1810
1811
  viewWidth = viewHeight * camera.aspect;
1811
1812
  }
1812
1813
  const texture = loadedTextureRef.current;
@@ -2897,8 +2898,8 @@ function drawPolylineStamped(ctx, pts, visibleLen, totalLen, strokeWidth, stroke
2897
2898
  }
2898
2899
  };
2899
2900
  advanceSegment();
2900
- const stampAt = (distance) => {
2901
- const targetDistance = Math.min(distance, maxDistance);
2901
+ const stampAt = (distance2) => {
2902
+ const targetDistance = Math.min(distance2, maxDistance);
2902
2903
  while (segmentIndex < pts.length && targetDistance > segmentStartLen + segmentLength && segmentIndex < lastIndex) {
2903
2904
  segmentStartLen += segmentLength;
2904
2905
  segmentIndex++;
@@ -5154,7 +5155,7 @@ function createFallbackTexture() {
5154
5155
  }
5155
5156
 
5156
5157
  // src/shaders/dither-godray-beam-composite/fragment.glsl
5157
- var fragment_default15 = "precision highp float;\nprecision highp int;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uBeamSpeed;\nuniform float uBeamDirection;\nuniform vec3 uBeamColor;\nuniform vec2 uBeamCenter;\nuniform float uBeamRadius;\nuniform float uBeamScale;\nuniform int uPathShape;\nuniform vec2 uPathPos;\nuniform float uPathAngle;\n\nout vec4 fragColor;\n\nconst float PI = 3.14159265;\nconst float TWO_PI = 6.28318531;\n\nuvec2 hash2d(uvec2 v) {\n v = v * 1664525u + 1013904223u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n v ^= v >> 16u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n return v;\n}\n\nfloat randomFibo(vec2 p) {\n uvec2 v = floatBitsToUint(p);\n v = hash2d(v);\n return float(v.x ^ v.y) / float(0xffffffffu);\n}\n\nfloat calculateAngle(vec2 p, vec2 c) {\n float a = atan(p.y - c.y, p.x - c.x);\n return a < 0.0 ? a + TWO_PI : a;\n}\n\nfloat angularDiff(float a, float b) {\n float d = abs(a - b);\n return d > PI ? TWO_PI - d : d;\n}\n\nfloat angularFade(float pointAngle, float peakAngle, float fadeAmount) {\n return 1.04 - smoothstep(0.0, fadeAmount, angularDiff(pointAngle, peakAngle));\n}\n\nfloat sdEquilateralTriangle(vec2 p) {\n const float k = 1.7320508;\n p.x = abs(p.x) - 1.0;\n p.y = p.y + 1.0 / k;\n if (p.x + k * p.y > 0.0) {\n p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;\n }\n p.x -= clamp(p.x, -2.0, 0.0);\n return -length(p) * sign(p.y);\n}\n\nvec3 dodge(vec3 src, vec3 dst) {\n return vec3(\n src.x >= 1.0 ? 1.0 : min(1.0, dst.x / max(0.001, 1.0 - src.x)),\n src.y >= 1.0 ? 1.0 : min(1.0, dst.y / max(0.001, 1.0 - src.y)),\n src.z >= 1.0 ? 1.0 : min(1.0, dst.z / max(0.001, 1.0 - src.z))\n );\n}\n\nfloat easeExpoIn(float t) {\n return t <= 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0));\n}\n\nvec2 rot2(vec2 v, float a) {\n float c = cos(a);\n float s = sin(a);\n return vec2(v.x * c - v.y * s, v.x * s + v.y * c);\n}\n\nvec3 beamAt(vec2 uv) {\n float aspect = uResolution.x / uResolution.y;\n vec2 center = uBeamCenter;\n\n vec2 u2 = vec2(uv.x * aspect, uv.y);\n vec2 c2 = vec2(center.x * aspect, center.y);\n\n float ringRadius = uBeamRadius;\n vec2 delta = (u2 - c2) / max(uBeamScale, 0.0001);\n float circleDist = abs(length(delta) - ringRadius);\n float squareDist = abs(max(abs(delta.x), abs(delta.y)) - ringRadius);\n float diamondDist = abs(abs(delta.x) + abs(delta.y) - ringRadius);\n float ovalDist =\n abs(length(vec2(delta.x / 1.5, delta.y)) - ringRadius);\n float triangleDist =\n abs(sdEquilateralTriangle(delta / max(ringRadius, 0.0001))) * ringRadius;\n\n float ringDist = circleDist;\n if (uPathShape == 1) {\n ringDist = squareDist;\n } else if (uPathShape == 2) {\n ringDist = diamondDist;\n } else if (uPathShape == 3) {\n ringDist = triangleDist;\n } else if (uPathShape == 4) {\n ringDist = ovalDist;\n }\n\n float b = 0.25 / (1.0 - smoothstep(0.2, 0.002, ringDist + 0.02));\n float ang = fract(0.19 + uTime * uBeamSpeed * uBeamDirection) * TWO_PI;\n b *= angularFade(calculateAngle(u2, c2), ang, PI * 0.5);\n\n vec3 col = b * pow(max(0.0, 1.0 - ringDist), 3.0) * uBeamColor;\n col = tanh(clamp(col, -40.0, 40.0));\n col += (randomFibo(gl_FragCoord.xy) - 0.5) / 255.0;\n\n return col;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ang = uPathAngle;\n vec2 pos = uPathPos;\n vec2 off = uv - pos;\n vec2 ro = rot2(off, -ang);\n vec2 so = ro;\n\n if (ro.x > 0.0) {\n float e = easeExpoIn(ro.x);\n so.y = ro.y / (1.0 + 4.0 * e * e);\n }\n\n vec2 st = clamp(pos + rot2(so, ang), 0.0, 1.0);\n\n vec3 beam = beamAt(st);\n vec4 img = texture(uTexture, st);\n vec3 outColor = mix(beam, dodge(img.rgb, beam), img.a);\n\n fragColor = vec4(outColor, 1.0);\n}\n";
5158
+ var fragment_default15 = "precision highp float;\nprecision highp int;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uBeamSpeed;\nuniform float uBeamDirection;\nuniform vec3 uBeamColor;\nuniform vec2 uBeamCenter;\nuniform float uBeamRadius;\nuniform float uBeamScale;\nuniform int uPathShape;\nuniform int uCustomPointCount;\nuniform vec2 uCustomPoints[64];\nuniform vec2 uPathPos;\nuniform float uPathAngle;\n\nout vec4 fragColor;\n\nconst float PI = 3.14159265;\nconst float TWO_PI = 6.28318531;\nconst int MAX_CUSTOM_POINTS = 64;\n\nuvec2 hash2d(uvec2 v) {\n v = v * 1664525u + 1013904223u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n v ^= v >> 16u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n return v;\n}\n\nfloat randomFibo(vec2 p) {\n uvec2 v = floatBitsToUint(p);\n v = hash2d(v);\n return float(v.x ^ v.y) / float(0xffffffffu);\n}\n\nfloat calculateAngle(vec2 p, vec2 c) {\n float a = atan(p.y - c.y, p.x - c.x);\n return a < 0.0 ? a + TWO_PI : a;\n}\n\nfloat angularDiff(float a, float b) {\n float d = abs(a - b);\n return d > PI ? TWO_PI - d : d;\n}\n\nfloat angularFade(float pointAngle, float peakAngle, float fadeAmount) {\n return 1.04 - smoothstep(0.0, fadeAmount, angularDiff(pointAngle, peakAngle));\n}\n\nfloat sdEquilateralTriangle(vec2 p) {\n const float k = 1.7320508;\n p.x = abs(p.x) - 1.0;\n p.y = p.y + 1.0 / k;\n if (p.x + k * p.y > 0.0) {\n p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;\n }\n p.x -= clamp(p.x, -2.0, 0.0);\n return -length(p) * sign(p.y);\n}\n\nvec3 dodge(vec3 src, vec3 dst) {\n return vec3(\n src.x >= 1.0 ? 1.0 : min(1.0, dst.x / max(0.001, 1.0 - src.x)),\n src.y >= 1.0 ? 1.0 : min(1.0, dst.y / max(0.001, 1.0 - src.y)),\n src.z >= 1.0 ? 1.0 : min(1.0, dst.z / max(0.001, 1.0 - src.z))\n );\n}\n\nfloat easeExpoIn(float t) {\n return t <= 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0));\n}\n\nvec2 rot2(vec2 v, float a) {\n float c = cos(a);\n float s = sin(a);\n return vec2(v.x * c - v.y * s, v.x * s + v.y * c);\n}\n\nfloat segmentDistance(vec2 p, vec2 a, vec2 b) {\n vec2 pa = p - a;\n vec2 ba = b - a;\n float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.00001), 0.0, 1.0);\n return length(pa - ba * h);\n}\n\nvec3 beamAt(vec2 uv) {\n float aspect = uResolution.x / uResolution.y;\n vec2 center = uBeamCenter;\n\n vec2 u2 = vec2(uv.x * aspect, uv.y);\n vec2 c2 = vec2(center.x * aspect, center.y);\n\n float ringRadius = uBeamRadius;\n vec2 delta = (u2 - c2) / max(uBeamScale, 0.0001);\n float circleDist = abs(length(delta) - ringRadius);\n float squareDist = abs(max(abs(delta.x), abs(delta.y)) - ringRadius);\n float diamondDist = abs(abs(delta.x) + abs(delta.y) - ringRadius);\n float ovalDist =\n abs(length(vec2(delta.x / 1.5, delta.y)) - ringRadius);\n float triangleDist =\n abs(sdEquilateralTriangle(delta / max(ringRadius, 0.0001))) * ringRadius;\n float customDist = 10.0;\n\n float ringDist = circleDist;\n if (uPathShape == 1) {\n ringDist = squareDist;\n } else if (uPathShape == 2) {\n ringDist = diamondDist;\n } else if (uPathShape == 3) {\n ringDist = triangleDist;\n } else if (uPathShape == 4) {\n ringDist = ovalDist;\n } else if (uPathShape == 5 && uCustomPointCount > 1) {\n vec2 uva = vec2(uv.x * aspect, uv.y);\n for (int i = 0; i < MAX_CUSTOM_POINTS - 1; i++) {\n if (i >= uCustomPointCount - 1) {\n break;\n }\n vec2 a = vec2(uCustomPoints[i].x * aspect, uCustomPoints[i].y);\n vec2 b = vec2(uCustomPoints[i + 1].x * aspect, uCustomPoints[i + 1].y);\n customDist = min(customDist, segmentDistance(uva, a, b));\n }\n ringDist = customDist;\n }\n\n float b = 0.25 / (1.0 - smoothstep(0.2, 0.002, ringDist + 0.02));\n float ang = fract(0.19 + uTime * uBeamSpeed * uBeamDirection) * TWO_PI;\n b *= angularFade(calculateAngle(u2, c2), ang, PI * 0.5);\n\n vec3 col = b * pow(max(0.0, 1.0 - ringDist), 3.0) * uBeamColor;\n col = tanh(clamp(col, -40.0, 40.0));\n col += (randomFibo(gl_FragCoord.xy) - 0.5) / 255.0;\n\n return col;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ang = uPathAngle;\n vec2 pos = uPathPos;\n vec2 off = uv - pos;\n vec2 ro = rot2(off, -ang);\n vec2 so = ro;\n\n if (ro.x > 0.0) {\n float e = easeExpoIn(ro.x);\n so.y = ro.y / (1.0 + 4.0 * e * e);\n }\n\n vec2 st = clamp(pos + rot2(so, ang), 0.0, 1.0);\n\n vec3 beam = beamAt(st);\n vec4 img = texture(uTexture, st);\n vec3 outColor = mix(beam, dodge(img.rgb, beam), img.a);\n\n fragColor = vec4(outColor, 1.0);\n}\n";
5158
5159
 
5159
5160
  // src/shaders/dither-godray-beam-composite/vertex.glsl
5160
5161
  var vertex_default14 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoord = uv;\n gl_Position = vec4(position, 1.0);\n}\n";
@@ -5170,6 +5171,7 @@ function DitherStreamBeamCompositePass({
5170
5171
  beamRadius = 0.6,
5171
5172
  beamScale = 1,
5172
5173
  pathShape = "circle",
5174
+ customPathPoints = [],
5173
5175
  pathPos = [0.5009, 1.0473],
5174
5176
  pathAngle = (0.999 - 0.25) * -6.28318531,
5175
5177
  target = null,
@@ -5177,6 +5179,7 @@ function DitherStreamBeamCompositePass({
5177
5179
  enabled = true,
5178
5180
  priority = 0
5179
5181
  }) {
5182
+ const MAX_CUSTOM_POINTS = 64;
5180
5183
  const fallbackTexture = (0, import_react23.useMemo)(() => createFallbackTexture(), []);
5181
5184
  const uniforms = (0, import_react23.useMemo)(
5182
5185
  () => ({
@@ -5192,7 +5195,16 @@ function DitherStreamBeamCompositePass({
5192
5195
  uBeamRadius: { value: beamRadius },
5193
5196
  uBeamScale: { value: beamScale },
5194
5197
  uPathShape: {
5195
- value: pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : 0
5198
+ value: pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : pathShape === "custom" ? 5 : 0
5199
+ },
5200
+ uCustomPointCount: {
5201
+ value: Math.min(customPathPoints.length, MAX_CUSTOM_POINTS)
5202
+ },
5203
+ uCustomPoints: {
5204
+ value: Array.from(
5205
+ { length: MAX_CUSTOM_POINTS },
5206
+ () => new THREE21.Vector2(-1, -1)
5207
+ )
5196
5208
  },
5197
5209
  uPathPos: { value: new THREE21.Vector2(pathPos[0], pathPos[1]) },
5198
5210
  uPathAngle: { value: pathAngle }
@@ -5214,7 +5226,16 @@ function DitherStreamBeamCompositePass({
5214
5226
  );
5215
5227
  uniforms.uBeamRadius.value = beamRadius;
5216
5228
  uniforms.uBeamScale.value = beamScale;
5217
- uniforms.uPathShape.value = pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : 0;
5229
+ uniforms.uPathShape.value = pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : pathShape === "custom" ? 5 : 0;
5230
+ uniforms.uCustomPointCount.value = Math.min(
5231
+ customPathPoints.length,
5232
+ MAX_CUSTOM_POINTS
5233
+ );
5234
+ const uniformCustomPoints = uniforms.uCustomPoints.value;
5235
+ for (let index = 0; index < MAX_CUSTOM_POINTS; index += 1) {
5236
+ const point = customPathPoints[index];
5237
+ uniformCustomPoints[index].set(point?.[0] ?? -1, point?.[1] ?? -1);
5238
+ }
5218
5239
  uniforms.uPathPos.value.set(pathPos[0], pathPos[1]);
5219
5240
  uniforms.uPathAngle.value = pathAngle;
5220
5241
  return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
@@ -5240,7 +5261,7 @@ var import_react24 = require("react");
5240
5261
  var THREE22 = __toESM(require("three"));
5241
5262
 
5242
5263
  // src/shaders/dither-godray-dither/fragment.glsl
5243
- var fragment_default16 = "precision highp float;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform vec2 uResolution;\n\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ar = uResolution.x / uResolution.y;\n float ac = mix(ar, 1.0 / ar, 0.5);\n\n float gs = 0.005;\n float bg = 1.0 / gs;\n vec2 cellSize = vec2(1.0 / (bg * ar), 1.0 / bg) * ac;\n\n vec2 pos = vec2(0.5);\n vec2 off = uv - pos;\n vec2 cell = floor(off / cellSize);\n vec2 center = (cell + 0.5) * cellSize;\n vec2 pixelUv = center + pos;\n\n vec4 c = texture(uTexture, pixelUv);\n float lum = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));\n float gm = pow(mix(0.2, 2.2, 0.3), 2.2);\n\n vec2 local = mod(uv - pos, cellSize) / cellSize;\n vec2 ct = local * 2.0 - 1.0;\n float d = length(ct);\n\n float ns = 16.0;\n float si = clamp(floor(lum * ns * gm), 0.0, ns - 1.0);\n float rad = si / ns;\n float alpha = smoothstep(rad + 0.08, rad - 0.08, d);\n\n vec3 tint = (c.rgb - si * 0.04) * 1.4;\n fragColor = vec4(mix(vec3(0.0), tint, alpha), 1.0);\n}\n";
5264
+ var fragment_default16 = "precision highp float;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform sampler2D uBackgroundTexture;\nuniform float uHasBackground;\nuniform float uDitherBackground;\nuniform float uBackgroundAspect;\nuniform vec2 uResolution;\n\nout vec4 fragColor;\n\n// Object-cover UV: fills the canvas, cropping the image centered at (0.5, 0.5).\nvec2 coverUV(vec2 uv, float imageAspect, float canvasAspect) {\n vec2 offset = uv - 0.5;\n if (imageAspect > canvasAspect) {\n offset.x *= canvasAspect / imageAspect;\n } else {\n offset.y *= imageAspect / canvasAspect;\n }\n return offset + 0.5;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ar = uResolution.x / uResolution.y;\n float ac = mix(ar, 1.0 / ar, 0.5);\n\n float gs = 0.005;\n float bg = 1.0 / gs;\n vec2 cellSize = vec2(1.0 / (bg * ar), 1.0 / bg) * ac;\n\n vec2 pos = vec2(0.5);\n vec2 off = uv - pos;\n vec2 cell = floor(off / cellSize);\n vec2 center = (cell + 0.5) * cellSize;\n vec2 pixelUv = center + pos;\n\n // Foreground dither\n vec4 c = texture(uTexture, pixelUv);\n float lum = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));\n float gm = pow(mix(0.2, 2.2, 0.3), 2.2);\n\n vec2 local = mod(uv - pos, cellSize) / cellSize;\n vec2 ct = local * 2.0 - 1.0;\n float d = length(ct);\n\n float ns = 16.0;\n float si = clamp(floor(lum * ns * gm), 0.0, ns - 1.0);\n float rad = si / ns;\n float alpha = smoothstep(rad + 0.08, rad - 0.08, d);\n\n vec3 tint = (c.rgb - si * 0.04) * 1.4;\n\n // Background \u2014 sampled with cover UVs\n vec2 bgUV = coverUV(uv, uBackgroundAspect, ar);\n vec2 bgCellUV = coverUV(pixelUv, uBackgroundAspect, ar);\n\n float beamReveal = smoothstep(0.4, 0.75, lum);\n vec3 bgRaw = texture(uBackgroundTexture, bgUV).rgb;\n vec3 bgRawColor = mix(vec3(0.0), bgRaw, uHasBackground * beamReveal);\n\n // Background content at cell-snapped UV (pixelated)\n vec3 bgCell = texture(uBackgroundTexture, bgCellUV).rgb;\n\n // Dithered mode: background image multiplies the beam tint.\n // bgCell * 0.6 + 0.4 maps [0,1] \u2192 [0.4, 1.0], so:\n // bright bg \u2192 tint \xD7 1.0 (same intensity as raw mode)\n // dark bg \u2192 tint \xD7 0.4 (dark areas of image dim the beam)\n // The beam's hue is always preserved; the background shows as luminance texture.\n vec3 bgCell3 = bgCell * 0.6 + 0.4;\n vec3 bgFiltered = tint * bgCell3;\n vec3 bgDotContent = mix(tint, bgFiltered, uHasBackground);\n vec3 dotColor = mix(tint, bgDotContent, uDitherBackground);\n\n // Gap (between dots): raw bg reveal in raw mode, black in dithered mode.\n vec3 gapColor = bgRawColor * (1.0 - uDitherBackground);\n\n fragColor = vec4(mix(gapColor, dotColor, alpha), 1.0);\n}\n";
5244
5265
 
5245
5266
  // src/shaders/dither-godray-dither/vertex.glsl
5246
5267
  var vertex_default15 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoord = uv;\n gl_Position = vec4(position, 1.0);\n}\n";
@@ -5249,21 +5270,72 @@ var vertex_default15 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoor
5249
5270
  var import_jsx_runtime25 = require("react/jsx-runtime");
5250
5271
  function DitherStreamDitherPass({
5251
5272
  inputTexture = null,
5273
+ backgroundImageSrc,
5274
+ ditherBackground = true,
5252
5275
  target = null,
5253
5276
  clear = true,
5254
5277
  enabled = true,
5255
5278
  priority = 0
5256
5279
  }) {
5257
5280
  const fallbackTexture = (0, import_react24.useMemo)(() => createFallbackTexture(), []);
5281
+ const [backgroundTexture, setBackgroundTexture] = (0, import_react24.useState)(null);
5282
+ const [backgroundAspect, setBackgroundAspect] = (0, import_react24.useState)(1);
5283
+ (0, import_react24.useEffect)(() => {
5284
+ if (!backgroundImageSrc) {
5285
+ setBackgroundTexture(null);
5286
+ setBackgroundAspect(1);
5287
+ return;
5288
+ }
5289
+ let active = true;
5290
+ const loader = new THREE22.TextureLoader();
5291
+ loader.load(
5292
+ backgroundImageSrc,
5293
+ (texture) => {
5294
+ if (!active) {
5295
+ texture.dispose();
5296
+ return;
5297
+ }
5298
+ texture.minFilter = THREE22.LinearFilter;
5299
+ texture.magFilter = THREE22.LinearFilter;
5300
+ texture.needsUpdate = true;
5301
+ const img = texture.image;
5302
+ if (img && img.naturalWidth && img.naturalHeight) {
5303
+ setBackgroundAspect(img.naturalWidth / img.naturalHeight);
5304
+ }
5305
+ setBackgroundTexture(texture);
5306
+ },
5307
+ void 0,
5308
+ (error) => {
5309
+ if (!active) return;
5310
+ console.error(
5311
+ "[DitherStream] Failed to load backgroundImageSrc \u2014 likely a CORS issue. The image server must include Access-Control-Allow-Origin headers.",
5312
+ backgroundImageSrc,
5313
+ error
5314
+ );
5315
+ setBackgroundTexture(null);
5316
+ }
5317
+ );
5318
+ return () => {
5319
+ active = false;
5320
+ };
5321
+ }, [backgroundImageSrc]);
5258
5322
  const uniforms = (0, import_react24.useMemo)(
5259
5323
  () => ({
5260
5324
  uTexture: { value: inputTexture ?? fallbackTexture },
5325
+ uBackgroundTexture: { value: fallbackTexture },
5326
+ uHasBackground: { value: 0 },
5327
+ uDitherBackground: { value: 1 },
5328
+ uBackgroundAspect: { value: 1 },
5261
5329
  uResolution: { value: new THREE22.Vector2(1, 1) }
5262
5330
  }),
5263
5331
  // eslint-disable-next-line react-hooks/exhaustive-deps
5264
5332
  []
5265
5333
  );
5266
5334
  uniforms.uTexture.value = inputTexture ?? fallbackTexture;
5335
+ uniforms.uBackgroundTexture.value = backgroundTexture ?? fallbackTexture;
5336
+ uniforms.uHasBackground.value = backgroundTexture ? 1 : 0;
5337
+ uniforms.uDitherBackground.value = ditherBackground ? 1 : 0;
5338
+ uniforms.uBackgroundAspect.value = backgroundAspect;
5267
5339
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5268
5340
  ShaderPass,
5269
5341
  {
@@ -5522,7 +5594,10 @@ function DitherStream({
5522
5594
  height = "100%",
5523
5595
  className = "relative h-full w-full",
5524
5596
  style,
5597
+ children,
5525
5598
  imageTextureSrc,
5599
+ backgroundImageSrc,
5600
+ backgroundDithered = true,
5526
5601
  projectionSpeed = 0.05,
5527
5602
  beamSpeed = 0.1,
5528
5603
  beamDirection = "counterclockwise",
@@ -5531,6 +5606,8 @@ function DitherStream({
5531
5606
  beamRadius = 0.6,
5532
5607
  beamScale = 1,
5533
5608
  beamPathShape = "circle",
5609
+ beamCustomPathPoints = [],
5610
+ beamEnabled = true,
5534
5611
  pathPos = [0.5009, 1.0473],
5535
5612
  pathAngle = (0.999 - 0.25) * -6.28318531,
5536
5613
  godrayIntensity = 2.9
@@ -5567,6 +5644,7 @@ function DitherStream({
5567
5644
  },
5568
5645
  {
5569
5646
  component: DitherStreamBeamCompositePass,
5647
+ enabled: beamEnabled,
5570
5648
  props: {
5571
5649
  beamSpeed,
5572
5650
  beamDirection,
@@ -5575,12 +5653,14 @@ function DitherStream({
5575
5653
  beamRadius,
5576
5654
  beamScale,
5577
5655
  pathShape: beamPathShape,
5656
+ customPathPoints: beamCustomPathPoints,
5578
5657
  pathPos,
5579
5658
  pathAngle
5580
5659
  }
5581
5660
  },
5582
5661
  {
5583
- component: DitherStreamDitherPass
5662
+ component: DitherStreamDitherPass,
5663
+ props: { backgroundImageSrc, ditherBackground: backgroundDithered }
5584
5664
  },
5585
5665
  {
5586
5666
  component: DitherStreamGodRaysPass,
@@ -5588,6 +5668,345 @@ function DitherStream({
5588
5668
  }
5589
5669
  ]
5590
5670
  }
5671
+ ),
5672
+ children
5673
+ ]
5674
+ }
5675
+ );
5676
+ }
5677
+
5678
+ // src/components/DitherStreamPathDrawer.tsx
5679
+ var import_react28 = require("react");
5680
+ var import_jsx_runtime29 = require("react/jsx-runtime");
5681
+ var MIN_POINT_DISTANCE = 6e-3;
5682
+ var MAX_CAPTURED_POINTS = 512;
5683
+ var MAX_OUTPUT_POINTS = 64;
5684
+ var BRUSH_STEP_PX = 2;
5685
+ var BRUSH_RADIUS_PX = 8;
5686
+ var COMMIT_IDLE_MS = 2500;
5687
+ var DITHER_CELL = 3;
5688
+ var DITHER_HALO_FACTOR = 3.8;
5689
+ var BAYER_4X4 = [
5690
+ [0, 8, 2, 10],
5691
+ [12, 4, 14, 6],
5692
+ [3, 11, 1, 9],
5693
+ [15, 7, 13, 5]
5694
+ ].map((row) => row.map((v) => v / 16));
5695
+ var bayerThreshold = (px, py) => {
5696
+ const bx = (Math.floor(px / DITHER_CELL) % 4 + 4) % 4;
5697
+ const by = (Math.floor(py / DITHER_CELL) % 4 + 4) % 4;
5698
+ return BAYER_4X4[by][bx];
5699
+ };
5700
+ var clamp01 = (value) => Math.min(1, Math.max(0, value));
5701
+ var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
5702
+ var samplePath = (points, count) => {
5703
+ if (points.length <= count) return points;
5704
+ const sampled = [];
5705
+ for (let index = 0; index < count; index += 1) {
5706
+ const t = index / Math.max(1, count - 1);
5707
+ const sourceIndex = Math.round(t * (points.length - 1));
5708
+ sampled.push(points[sourceIndex]);
5709
+ }
5710
+ return sampled;
5711
+ };
5712
+ var hexToRgb = (hex) => {
5713
+ const normalized = hex.replace("#", "");
5714
+ if (normalized.length !== 6) {
5715
+ return { r: 170, g: 176, b: 240 };
5716
+ }
5717
+ return {
5718
+ r: parseInt(normalized.slice(0, 2), 16),
5719
+ g: parseInt(normalized.slice(2, 4), 16),
5720
+ b: parseInt(normalized.slice(4, 6), 16)
5721
+ };
5722
+ };
5723
+ var hash2 = (x, y) => {
5724
+ const n = x * 15731 + y * 789221 + 1376312589;
5725
+ const masked = n & 2147483647;
5726
+ return masked % 997 / 997;
5727
+ };
5728
+ function DitherStreamPathDrawer({
5729
+ enabled,
5730
+ beamColor = "#aab0f0",
5731
+ backgroundImageSrc,
5732
+ onCommit
5733
+ }) {
5734
+ const overlayRef = (0, import_react28.useRef)(null);
5735
+ const canvasRef = (0, import_react28.useRef)(null);
5736
+ const drawingRef = (0, import_react28.useRef)(false);
5737
+ const pointerIdRef = (0, import_react28.useRef)(null);
5738
+ const commitTimeoutRef = (0, import_react28.useRef)(null);
5739
+ const pointsRef = (0, import_react28.useRef)([]);
5740
+ const lastPixelPointRef = (0, import_react28.useRef)(null);
5741
+ const [cursor, setCursor] = (0, import_react28.useState)(null);
5742
+ const updatePoints = (0, import_react28.useCallback)((nextPoints) => {
5743
+ pointsRef.current = nextPoints;
5744
+ }, []);
5745
+ const getRelativePoint = (0, import_react28.useCallback)(
5746
+ (event) => {
5747
+ const rect = overlayRef.current?.getBoundingClientRect();
5748
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
5749
+ const x = clamp01((event.clientX - rect.left) / rect.width);
5750
+ const y = clamp01(1 - (event.clientY - rect.top) / rect.height);
5751
+ return {
5752
+ normalized: { x, y },
5753
+ pixels: {
5754
+ x: x * rect.width,
5755
+ y: (1 - y) * rect.height
5756
+ }
5757
+ };
5758
+ },
5759
+ []
5760
+ );
5761
+ const clearCanvas = (0, import_react28.useCallback)(() => {
5762
+ const canvas = canvasRef.current;
5763
+ if (!canvas) return;
5764
+ const ctx = canvas.getContext("2d");
5765
+ if (!ctx) return;
5766
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
5767
+ }, []);
5768
+ const stampBrush = (0, import_react28.useCallback)(
5769
+ (ctx, point, strength = 1) => {
5770
+ const color = hexToRgb(beamColor);
5771
+ const haloRadius = BRUSH_RADIUS_PX * DITHER_HALO_FACTOR;
5772
+ const glow = ctx.createRadialGradient(
5773
+ point.x,
5774
+ point.y,
5775
+ 0,
5776
+ point.x,
5777
+ point.y,
5778
+ haloRadius
5779
+ );
5780
+ glow.addColorStop(
5781
+ 0,
5782
+ `rgba(${color.r}, ${color.g}, ${color.b}, ${0.09 * strength})`
5783
+ );
5784
+ glow.addColorStop(1, "rgba(0,0,0,0)");
5785
+ ctx.fillStyle = glow;
5786
+ ctx.beginPath();
5787
+ ctx.arc(point.x, point.y, haloRadius, 0, Math.PI * 2);
5788
+ ctx.fill();
5789
+ const step = DITHER_CELL;
5790
+ const startX = Math.floor((point.x - haloRadius) / step) * step;
5791
+ const endX = Math.ceil((point.x + haloRadius) / step) * step;
5792
+ const startY = Math.floor((point.y - haloRadius) / step) * step;
5793
+ const endY = Math.ceil((point.y + haloRadius) / step) * step;
5794
+ for (let py = startY; py <= endY; py += step) {
5795
+ for (let px = startX; px <= endX; px += step) {
5796
+ const radial = Math.hypot(
5797
+ px + step * 0.5 - point.x,
5798
+ py + step * 0.5 - point.y
5799
+ );
5800
+ if (radial > haloRadius) continue;
5801
+ const normalizedDist = radial / haloRadius;
5802
+ const density = 1 - normalizedDist;
5803
+ if (bayerThreshold(px, py) >= density) continue;
5804
+ const alpha = density * 0.9 * strength;
5805
+ const isHot = normalizedDist < 0.28 && hash2(Math.floor(px) + 11, Math.floor(py) - 7) > 0.68;
5806
+ ctx.fillStyle = isHot ? `rgba(255,255,255,${alpha})` : `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
5807
+ ctx.fillRect(px, py, step, step);
5808
+ }
5809
+ }
5810
+ ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${0.88 * strength})`;
5811
+ ctx.beginPath();
5812
+ ctx.arc(point.x, point.y, BRUSH_RADIUS_PX * 0.55, 0, Math.PI * 2);
5813
+ ctx.fill();
5814
+ ctx.fillStyle = `rgba(255,255,255,${0.92 * strength})`;
5815
+ ctx.beginPath();
5816
+ ctx.arc(point.x, point.y, BRUSH_RADIUS_PX * 0.28, 0, Math.PI * 2);
5817
+ ctx.fill();
5818
+ },
5819
+ [beamColor]
5820
+ );
5821
+ const drawSegment = (0, import_react28.useCallback)(
5822
+ (from, to) => {
5823
+ const canvas = canvasRef.current;
5824
+ if (!canvas) return;
5825
+ const ctx = canvas.getContext("2d");
5826
+ if (!ctx) return;
5827
+ const dx = to.x - from.x;
5828
+ const dy = to.y - from.y;
5829
+ const distancePx = Math.hypot(dx, dy);
5830
+ const steps = Math.max(1, Math.ceil(distancePx / BRUSH_STEP_PX));
5831
+ for (let index = 0; index <= steps; index += 1) {
5832
+ const t = index / steps;
5833
+ stampBrush(
5834
+ ctx,
5835
+ { x: from.x + dx * t, y: from.y + dy * t },
5836
+ drawingRef.current ? 1 : 0.92
5837
+ );
5838
+ }
5839
+ },
5840
+ [stampBrush]
5841
+ );
5842
+ const finalizePath = (0, import_react28.useCallback)(() => {
5843
+ if (commitTimeoutRef.current) {
5844
+ clearTimeout(commitTimeoutRef.current);
5845
+ commitTimeoutRef.current = null;
5846
+ }
5847
+ const capturedPoints = pointsRef.current;
5848
+ if (capturedPoints.length < 2) {
5849
+ updatePoints([]);
5850
+ return;
5851
+ }
5852
+ const sampled = samplePath(capturedPoints, MAX_OUTPUT_POINTS);
5853
+ onCommit(sampled.map((point) => [point.x, point.y]));
5854
+ updatePoints([]);
5855
+ }, [onCommit, updatePoints]);
5856
+ const queueCommit = (0, import_react28.useCallback)(() => {
5857
+ if (commitTimeoutRef.current) {
5858
+ clearTimeout(commitTimeoutRef.current);
5859
+ }
5860
+ commitTimeoutRef.current = setTimeout(() => {
5861
+ finalizePath();
5862
+ }, COMMIT_IDLE_MS);
5863
+ }, [finalizePath]);
5864
+ const addPoint = (0, import_react28.useCallback)(
5865
+ (point) => {
5866
+ const previousPoints = pointsRef.current;
5867
+ if (!previousPoints.length) {
5868
+ updatePoints([point]);
5869
+ return;
5870
+ }
5871
+ const lastPoint = previousPoints[previousPoints.length - 1];
5872
+ if (previousPoints.length < MAX_CAPTURED_POINTS && distance(lastPoint, point) >= MIN_POINT_DISTANCE) {
5873
+ updatePoints([...previousPoints, point]);
5874
+ }
5875
+ },
5876
+ [updatePoints]
5877
+ );
5878
+ (0, import_react28.useEffect)(() => {
5879
+ if (enabled) return;
5880
+ drawingRef.current = false;
5881
+ pointerIdRef.current = null;
5882
+ if (commitTimeoutRef.current) {
5883
+ clearTimeout(commitTimeoutRef.current);
5884
+ commitTimeoutRef.current = null;
5885
+ }
5886
+ pointsRef.current = [];
5887
+ lastPixelPointRef.current = null;
5888
+ setCursor(null);
5889
+ clearCanvas();
5890
+ }, [clearCanvas, enabled]);
5891
+ (0, import_react28.useEffect)(() => {
5892
+ if (!enabled) return;
5893
+ const canvas = canvasRef.current;
5894
+ const container = overlayRef.current;
5895
+ if (!canvas || !container) return;
5896
+ const resize = () => {
5897
+ const rect = container.getBoundingClientRect();
5898
+ const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
5899
+ canvas.width = Math.max(1, Math.floor(rect.width * dpr));
5900
+ canvas.height = Math.max(1, Math.floor(rect.height * dpr));
5901
+ canvas.style.width = `${rect.width}px`;
5902
+ canvas.style.height = `${rect.height}px`;
5903
+ const ctx = canvas.getContext("2d");
5904
+ if (!ctx) return;
5905
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5906
+ ctx.globalCompositeOperation = "source-over";
5907
+ ctx.clearRect(0, 0, rect.width, rect.height);
5908
+ };
5909
+ resize();
5910
+ const observer = new ResizeObserver(resize);
5911
+ observer.observe(container);
5912
+ return () => observer.disconnect();
5913
+ }, [enabled]);
5914
+ if (!enabled) return null;
5915
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
5916
+ "div",
5917
+ {
5918
+ ref: overlayRef,
5919
+ className: "absolute inset-0 z-20 cursor-crosshair touch-none",
5920
+ style: {
5921
+ background: backgroundImageSrc ? void 0 : "radial-gradient(circle at center, rgba(102,110,164,0.10), rgba(7,9,17,0.72))",
5922
+ border: "1px dashed rgba(188,196,246,0.35)"
5923
+ },
5924
+ onPointerDown: (event) => {
5925
+ const point = getRelativePoint(event);
5926
+ if (!point) return;
5927
+ drawingRef.current = true;
5928
+ pointerIdRef.current = event.pointerId;
5929
+ setCursor(point.normalized);
5930
+ const existingPoints = pointsRef.current;
5931
+ if (existingPoints.length === 0) {
5932
+ updatePoints([point.normalized]);
5933
+ } else {
5934
+ updatePoints([...existingPoints, point.normalized]);
5935
+ }
5936
+ lastPixelPointRef.current = point.pixels;
5937
+ const canvas = canvasRef.current;
5938
+ const ctx = canvas?.getContext("2d");
5939
+ if (ctx) {
5940
+ stampBrush(ctx, point.pixels);
5941
+ }
5942
+ queueCommit();
5943
+ event.currentTarget.setPointerCapture(event.pointerId);
5944
+ },
5945
+ onPointerMove: (event) => {
5946
+ const point = getRelativePoint(event);
5947
+ if (!point) return;
5948
+ setCursor(point.normalized);
5949
+ if (!drawingRef.current) return;
5950
+ addPoint(point.normalized);
5951
+ const lastPoint = lastPixelPointRef.current;
5952
+ if (lastPoint) {
5953
+ drawSegment(lastPoint, point.pixels);
5954
+ }
5955
+ lastPixelPointRef.current = point.pixels;
5956
+ queueCommit();
5957
+ },
5958
+ onPointerUp: (event) => {
5959
+ if (pointerIdRef.current === event.pointerId) {
5960
+ event.currentTarget.releasePointerCapture(event.pointerId);
5961
+ }
5962
+ drawingRef.current = false;
5963
+ pointerIdRef.current = null;
5964
+ lastPixelPointRef.current = null;
5965
+ },
5966
+ onPointerCancel: (event) => {
5967
+ if (pointerIdRef.current === event.pointerId) {
5968
+ event.currentTarget.releasePointerCapture(event.pointerId);
5969
+ }
5970
+ drawingRef.current = false;
5971
+ pointerIdRef.current = null;
5972
+ lastPixelPointRef.current = null;
5973
+ if (commitTimeoutRef.current) {
5974
+ clearTimeout(commitTimeoutRef.current);
5975
+ commitTimeoutRef.current = null;
5976
+ }
5977
+ updatePoints([]);
5978
+ clearCanvas();
5979
+ },
5980
+ children: [
5981
+ backgroundImageSrc && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
5982
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5983
+ "img",
5984
+ {
5985
+ src: backgroundImageSrc,
5986
+ className: "pointer-events-none absolute inset-0 h-full w-full object-cover",
5987
+ alt: "",
5988
+ "aria-hidden": true
5989
+ }
5990
+ ),
5991
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-black/50" })
5992
+ ] }),
5993
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "pointer-events-none absolute left-1/2 top-3 -translate-x-1/2 rounded-full border border-white/30 bg-black/40 px-3 py-1 text-[11px] uppercase tracking-[0.14em] text-white/80", children: "Click and drag to draw your beam path" }),
5994
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
5995
+ "canvas",
5996
+ {
5997
+ ref: canvasRef,
5998
+ className: "pointer-events-none absolute inset-0"
5999
+ }
6000
+ ),
6001
+ cursor && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
6002
+ "div",
6003
+ {
6004
+ className: "pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/70 bg-white/15",
6005
+ style: {
6006
+ left: `${cursor.x * 100}%`,
6007
+ top: `${(1 - cursor.y) * 100}%`
6008
+ }
6009
+ }
5591
6010
  )
5592
6011
  ]
5593
6012
  }
@@ -5598,6 +6017,7 @@ function DitherStream({
5598
6017
  AnimatedDrawingSVG,
5599
6018
  DitherPulseRing,
5600
6019
  DitherStream,
6020
+ DitherStreamPathDrawer,
5601
6021
  EFECTO_ASCII_COMPONENT_DEFAULTS,
5602
6022
  EFECTO_ASCII_POST_PROCESSING_DEFAULTS,
5603
6023
  Efecto,
package/dist/index.mjs CHANGED
@@ -1781,9 +1781,9 @@ function AsciiEffect({
1781
1781
  let viewWidth = width2;
1782
1782
  let viewHeight = height2;
1783
1783
  if (camera instanceof THREE6.PerspectiveCamera) {
1784
- const distance = Math.abs(camera.position.z - assets.mesh.position.z);
1784
+ const distance2 = Math.abs(camera.position.z - assets.mesh.position.z);
1785
1785
  const verticalFov = THREE6.MathUtils.degToRad(camera.fov);
1786
- viewHeight = 2 * Math.tan(verticalFov / 2) * distance;
1786
+ viewHeight = 2 * Math.tan(verticalFov / 2) * distance2;
1787
1787
  viewWidth = viewHeight * camera.aspect;
1788
1788
  }
1789
1789
  const texture = loadedTextureRef.current;
@@ -2874,8 +2874,8 @@ function drawPolylineStamped(ctx, pts, visibleLen, totalLen, strokeWidth, stroke
2874
2874
  }
2875
2875
  };
2876
2876
  advanceSegment();
2877
- const stampAt = (distance) => {
2878
- const targetDistance = Math.min(distance, maxDistance);
2877
+ const stampAt = (distance2) => {
2878
+ const targetDistance = Math.min(distance2, maxDistance);
2879
2879
  while (segmentIndex < pts.length && targetDistance > segmentStartLen + segmentLength && segmentIndex < lastIndex) {
2880
2880
  segmentStartLen += segmentLength;
2881
2881
  segmentIndex++;
@@ -5140,7 +5140,7 @@ function createFallbackTexture() {
5140
5140
  }
5141
5141
 
5142
5142
  // src/shaders/dither-godray-beam-composite/fragment.glsl
5143
- var fragment_default15 = "precision highp float;\nprecision highp int;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uBeamSpeed;\nuniform float uBeamDirection;\nuniform vec3 uBeamColor;\nuniform vec2 uBeamCenter;\nuniform float uBeamRadius;\nuniform float uBeamScale;\nuniform int uPathShape;\nuniform vec2 uPathPos;\nuniform float uPathAngle;\n\nout vec4 fragColor;\n\nconst float PI = 3.14159265;\nconst float TWO_PI = 6.28318531;\n\nuvec2 hash2d(uvec2 v) {\n v = v * 1664525u + 1013904223u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n v ^= v >> 16u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n return v;\n}\n\nfloat randomFibo(vec2 p) {\n uvec2 v = floatBitsToUint(p);\n v = hash2d(v);\n return float(v.x ^ v.y) / float(0xffffffffu);\n}\n\nfloat calculateAngle(vec2 p, vec2 c) {\n float a = atan(p.y - c.y, p.x - c.x);\n return a < 0.0 ? a + TWO_PI : a;\n}\n\nfloat angularDiff(float a, float b) {\n float d = abs(a - b);\n return d > PI ? TWO_PI - d : d;\n}\n\nfloat angularFade(float pointAngle, float peakAngle, float fadeAmount) {\n return 1.04 - smoothstep(0.0, fadeAmount, angularDiff(pointAngle, peakAngle));\n}\n\nfloat sdEquilateralTriangle(vec2 p) {\n const float k = 1.7320508;\n p.x = abs(p.x) - 1.0;\n p.y = p.y + 1.0 / k;\n if (p.x + k * p.y > 0.0) {\n p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;\n }\n p.x -= clamp(p.x, -2.0, 0.0);\n return -length(p) * sign(p.y);\n}\n\nvec3 dodge(vec3 src, vec3 dst) {\n return vec3(\n src.x >= 1.0 ? 1.0 : min(1.0, dst.x / max(0.001, 1.0 - src.x)),\n src.y >= 1.0 ? 1.0 : min(1.0, dst.y / max(0.001, 1.0 - src.y)),\n src.z >= 1.0 ? 1.0 : min(1.0, dst.z / max(0.001, 1.0 - src.z))\n );\n}\n\nfloat easeExpoIn(float t) {\n return t <= 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0));\n}\n\nvec2 rot2(vec2 v, float a) {\n float c = cos(a);\n float s = sin(a);\n return vec2(v.x * c - v.y * s, v.x * s + v.y * c);\n}\n\nvec3 beamAt(vec2 uv) {\n float aspect = uResolution.x / uResolution.y;\n vec2 center = uBeamCenter;\n\n vec2 u2 = vec2(uv.x * aspect, uv.y);\n vec2 c2 = vec2(center.x * aspect, center.y);\n\n float ringRadius = uBeamRadius;\n vec2 delta = (u2 - c2) / max(uBeamScale, 0.0001);\n float circleDist = abs(length(delta) - ringRadius);\n float squareDist = abs(max(abs(delta.x), abs(delta.y)) - ringRadius);\n float diamondDist = abs(abs(delta.x) + abs(delta.y) - ringRadius);\n float ovalDist =\n abs(length(vec2(delta.x / 1.5, delta.y)) - ringRadius);\n float triangleDist =\n abs(sdEquilateralTriangle(delta / max(ringRadius, 0.0001))) * ringRadius;\n\n float ringDist = circleDist;\n if (uPathShape == 1) {\n ringDist = squareDist;\n } else if (uPathShape == 2) {\n ringDist = diamondDist;\n } else if (uPathShape == 3) {\n ringDist = triangleDist;\n } else if (uPathShape == 4) {\n ringDist = ovalDist;\n }\n\n float b = 0.25 / (1.0 - smoothstep(0.2, 0.002, ringDist + 0.02));\n float ang = fract(0.19 + uTime * uBeamSpeed * uBeamDirection) * TWO_PI;\n b *= angularFade(calculateAngle(u2, c2), ang, PI * 0.5);\n\n vec3 col = b * pow(max(0.0, 1.0 - ringDist), 3.0) * uBeamColor;\n col = tanh(clamp(col, -40.0, 40.0));\n col += (randomFibo(gl_FragCoord.xy) - 0.5) / 255.0;\n\n return col;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ang = uPathAngle;\n vec2 pos = uPathPos;\n vec2 off = uv - pos;\n vec2 ro = rot2(off, -ang);\n vec2 so = ro;\n\n if (ro.x > 0.0) {\n float e = easeExpoIn(ro.x);\n so.y = ro.y / (1.0 + 4.0 * e * e);\n }\n\n vec2 st = clamp(pos + rot2(so, ang), 0.0, 1.0);\n\n vec3 beam = beamAt(st);\n vec4 img = texture(uTexture, st);\n vec3 outColor = mix(beam, dodge(img.rgb, beam), img.a);\n\n fragColor = vec4(outColor, 1.0);\n}\n";
5143
+ var fragment_default15 = "precision highp float;\nprecision highp int;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform float uTime;\nuniform vec2 uResolution;\nuniform float uBeamSpeed;\nuniform float uBeamDirection;\nuniform vec3 uBeamColor;\nuniform vec2 uBeamCenter;\nuniform float uBeamRadius;\nuniform float uBeamScale;\nuniform int uPathShape;\nuniform int uCustomPointCount;\nuniform vec2 uCustomPoints[64];\nuniform vec2 uPathPos;\nuniform float uPathAngle;\n\nout vec4 fragColor;\n\nconst float PI = 3.14159265;\nconst float TWO_PI = 6.28318531;\nconst int MAX_CUSTOM_POINTS = 64;\n\nuvec2 hash2d(uvec2 v) {\n v = v * 1664525u + 1013904223u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n v ^= v >> 16u;\n v.x += v.y * v.y * 1664525u + 1013904223u;\n v.y += v.x * v.x * 1664525u + 1013904223u;\n return v;\n}\n\nfloat randomFibo(vec2 p) {\n uvec2 v = floatBitsToUint(p);\n v = hash2d(v);\n return float(v.x ^ v.y) / float(0xffffffffu);\n}\n\nfloat calculateAngle(vec2 p, vec2 c) {\n float a = atan(p.y - c.y, p.x - c.x);\n return a < 0.0 ? a + TWO_PI : a;\n}\n\nfloat angularDiff(float a, float b) {\n float d = abs(a - b);\n return d > PI ? TWO_PI - d : d;\n}\n\nfloat angularFade(float pointAngle, float peakAngle, float fadeAmount) {\n return 1.04 - smoothstep(0.0, fadeAmount, angularDiff(pointAngle, peakAngle));\n}\n\nfloat sdEquilateralTriangle(vec2 p) {\n const float k = 1.7320508;\n p.x = abs(p.x) - 1.0;\n p.y = p.y + 1.0 / k;\n if (p.x + k * p.y > 0.0) {\n p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;\n }\n p.x -= clamp(p.x, -2.0, 0.0);\n return -length(p) * sign(p.y);\n}\n\nvec3 dodge(vec3 src, vec3 dst) {\n return vec3(\n src.x >= 1.0 ? 1.0 : min(1.0, dst.x / max(0.001, 1.0 - src.x)),\n src.y >= 1.0 ? 1.0 : min(1.0, dst.y / max(0.001, 1.0 - src.y)),\n src.z >= 1.0 ? 1.0 : min(1.0, dst.z / max(0.001, 1.0 - src.z))\n );\n}\n\nfloat easeExpoIn(float t) {\n return t <= 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0));\n}\n\nvec2 rot2(vec2 v, float a) {\n float c = cos(a);\n float s = sin(a);\n return vec2(v.x * c - v.y * s, v.x * s + v.y * c);\n}\n\nfloat segmentDistance(vec2 p, vec2 a, vec2 b) {\n vec2 pa = p - a;\n vec2 ba = b - a;\n float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.00001), 0.0, 1.0);\n return length(pa - ba * h);\n}\n\nvec3 beamAt(vec2 uv) {\n float aspect = uResolution.x / uResolution.y;\n vec2 center = uBeamCenter;\n\n vec2 u2 = vec2(uv.x * aspect, uv.y);\n vec2 c2 = vec2(center.x * aspect, center.y);\n\n float ringRadius = uBeamRadius;\n vec2 delta = (u2 - c2) / max(uBeamScale, 0.0001);\n float circleDist = abs(length(delta) - ringRadius);\n float squareDist = abs(max(abs(delta.x), abs(delta.y)) - ringRadius);\n float diamondDist = abs(abs(delta.x) + abs(delta.y) - ringRadius);\n float ovalDist =\n abs(length(vec2(delta.x / 1.5, delta.y)) - ringRadius);\n float triangleDist =\n abs(sdEquilateralTriangle(delta / max(ringRadius, 0.0001))) * ringRadius;\n float customDist = 10.0;\n\n float ringDist = circleDist;\n if (uPathShape == 1) {\n ringDist = squareDist;\n } else if (uPathShape == 2) {\n ringDist = diamondDist;\n } else if (uPathShape == 3) {\n ringDist = triangleDist;\n } else if (uPathShape == 4) {\n ringDist = ovalDist;\n } else if (uPathShape == 5 && uCustomPointCount > 1) {\n vec2 uva = vec2(uv.x * aspect, uv.y);\n for (int i = 0; i < MAX_CUSTOM_POINTS - 1; i++) {\n if (i >= uCustomPointCount - 1) {\n break;\n }\n vec2 a = vec2(uCustomPoints[i].x * aspect, uCustomPoints[i].y);\n vec2 b = vec2(uCustomPoints[i + 1].x * aspect, uCustomPoints[i + 1].y);\n customDist = min(customDist, segmentDistance(uva, a, b));\n }\n ringDist = customDist;\n }\n\n float b = 0.25 / (1.0 - smoothstep(0.2, 0.002, ringDist + 0.02));\n float ang = fract(0.19 + uTime * uBeamSpeed * uBeamDirection) * TWO_PI;\n b *= angularFade(calculateAngle(u2, c2), ang, PI * 0.5);\n\n vec3 col = b * pow(max(0.0, 1.0 - ringDist), 3.0) * uBeamColor;\n col = tanh(clamp(col, -40.0, 40.0));\n col += (randomFibo(gl_FragCoord.xy) - 0.5) / 255.0;\n\n return col;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ang = uPathAngle;\n vec2 pos = uPathPos;\n vec2 off = uv - pos;\n vec2 ro = rot2(off, -ang);\n vec2 so = ro;\n\n if (ro.x > 0.0) {\n float e = easeExpoIn(ro.x);\n so.y = ro.y / (1.0 + 4.0 * e * e);\n }\n\n vec2 st = clamp(pos + rot2(so, ang), 0.0, 1.0);\n\n vec3 beam = beamAt(st);\n vec4 img = texture(uTexture, st);\n vec3 outColor = mix(beam, dodge(img.rgb, beam), img.a);\n\n fragColor = vec4(outColor, 1.0);\n}\n";
5144
5144
 
5145
5145
  // src/shaders/dither-godray-beam-composite/vertex.glsl
5146
5146
  var vertex_default14 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoord = uv;\n gl_Position = vec4(position, 1.0);\n}\n";
@@ -5156,6 +5156,7 @@ function DitherStreamBeamCompositePass({
5156
5156
  beamRadius = 0.6,
5157
5157
  beamScale = 1,
5158
5158
  pathShape = "circle",
5159
+ customPathPoints = [],
5159
5160
  pathPos = [0.5009, 1.0473],
5160
5161
  pathAngle = (0.999 - 0.25) * -6.28318531,
5161
5162
  target = null,
@@ -5163,6 +5164,7 @@ function DitherStreamBeamCompositePass({
5163
5164
  enabled = true,
5164
5165
  priority = 0
5165
5166
  }) {
5167
+ const MAX_CUSTOM_POINTS = 64;
5166
5168
  const fallbackTexture = useMemo16(() => createFallbackTexture(), []);
5167
5169
  const uniforms = useMemo16(
5168
5170
  () => ({
@@ -5178,7 +5180,16 @@ function DitherStreamBeamCompositePass({
5178
5180
  uBeamRadius: { value: beamRadius },
5179
5181
  uBeamScale: { value: beamScale },
5180
5182
  uPathShape: {
5181
- value: pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : 0
5183
+ value: pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : pathShape === "custom" ? 5 : 0
5184
+ },
5185
+ uCustomPointCount: {
5186
+ value: Math.min(customPathPoints.length, MAX_CUSTOM_POINTS)
5187
+ },
5188
+ uCustomPoints: {
5189
+ value: Array.from(
5190
+ { length: MAX_CUSTOM_POINTS },
5191
+ () => new THREE21.Vector2(-1, -1)
5192
+ )
5182
5193
  },
5183
5194
  uPathPos: { value: new THREE21.Vector2(pathPos[0], pathPos[1]) },
5184
5195
  uPathAngle: { value: pathAngle }
@@ -5200,7 +5211,16 @@ function DitherStreamBeamCompositePass({
5200
5211
  );
5201
5212
  uniforms.uBeamRadius.value = beamRadius;
5202
5213
  uniforms.uBeamScale.value = beamScale;
5203
- uniforms.uPathShape.value = pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : 0;
5214
+ uniforms.uPathShape.value = pathShape === "square" ? 1 : pathShape === "diamond" ? 2 : pathShape === "triangle" ? 3 : pathShape === "oval" ? 4 : pathShape === "custom" ? 5 : 0;
5215
+ uniforms.uCustomPointCount.value = Math.min(
5216
+ customPathPoints.length,
5217
+ MAX_CUSTOM_POINTS
5218
+ );
5219
+ const uniformCustomPoints = uniforms.uCustomPoints.value;
5220
+ for (let index = 0; index < MAX_CUSTOM_POINTS; index += 1) {
5221
+ const point = customPathPoints[index];
5222
+ uniformCustomPoints[index].set(point?.[0] ?? -1, point?.[1] ?? -1);
5223
+ }
5204
5224
  uniforms.uPathPos.value.set(pathPos[0], pathPos[1]);
5205
5225
  uniforms.uPathAngle.value = pathAngle;
5206
5226
  return /* @__PURE__ */ jsx24(
@@ -5222,11 +5242,11 @@ function DitherStreamBeamCompositePass({
5222
5242
  }
5223
5243
 
5224
5244
  // src/components/DitherStreamDitherPass.tsx
5225
- import { useMemo as useMemo17 } from "react";
5245
+ import { useEffect as useEffect17, useMemo as useMemo17, useState as useState4 } from "react";
5226
5246
  import * as THREE22 from "three";
5227
5247
 
5228
5248
  // src/shaders/dither-godray-dither/fragment.glsl
5229
- var fragment_default16 = "precision highp float;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform vec2 uResolution;\n\nout vec4 fragColor;\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ar = uResolution.x / uResolution.y;\n float ac = mix(ar, 1.0 / ar, 0.5);\n\n float gs = 0.005;\n float bg = 1.0 / gs;\n vec2 cellSize = vec2(1.0 / (bg * ar), 1.0 / bg) * ac;\n\n vec2 pos = vec2(0.5);\n vec2 off = uv - pos;\n vec2 cell = floor(off / cellSize);\n vec2 center = (cell + 0.5) * cellSize;\n vec2 pixelUv = center + pos;\n\n vec4 c = texture(uTexture, pixelUv);\n float lum = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));\n float gm = pow(mix(0.2, 2.2, 0.3), 2.2);\n\n vec2 local = mod(uv - pos, cellSize) / cellSize;\n vec2 ct = local * 2.0 - 1.0;\n float d = length(ct);\n\n float ns = 16.0;\n float si = clamp(floor(lum * ns * gm), 0.0, ns - 1.0);\n float rad = si / ns;\n float alpha = smoothstep(rad + 0.08, rad - 0.08, d);\n\n vec3 tint = (c.rgb - si * 0.04) * 1.4;\n fragColor = vec4(mix(vec3(0.0), tint, alpha), 1.0);\n}\n";
5249
+ var fragment_default16 = "precision highp float;\n\nin vec2 vTextureCoord;\n\nuniform sampler2D uTexture;\nuniform sampler2D uBackgroundTexture;\nuniform float uHasBackground;\nuniform float uDitherBackground;\nuniform float uBackgroundAspect;\nuniform vec2 uResolution;\n\nout vec4 fragColor;\n\n// Object-cover UV: fills the canvas, cropping the image centered at (0.5, 0.5).\nvec2 coverUV(vec2 uv, float imageAspect, float canvasAspect) {\n vec2 offset = uv - 0.5;\n if (imageAspect > canvasAspect) {\n offset.x *= canvasAspect / imageAspect;\n } else {\n offset.y *= imageAspect / canvasAspect;\n }\n return offset + 0.5;\n}\n\nvoid main() {\n vec2 uv = vTextureCoord;\n\n float ar = uResolution.x / uResolution.y;\n float ac = mix(ar, 1.0 / ar, 0.5);\n\n float gs = 0.005;\n float bg = 1.0 / gs;\n vec2 cellSize = vec2(1.0 / (bg * ar), 1.0 / bg) * ac;\n\n vec2 pos = vec2(0.5);\n vec2 off = uv - pos;\n vec2 cell = floor(off / cellSize);\n vec2 center = (cell + 0.5) * cellSize;\n vec2 pixelUv = center + pos;\n\n // Foreground dither\n vec4 c = texture(uTexture, pixelUv);\n float lum = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));\n float gm = pow(mix(0.2, 2.2, 0.3), 2.2);\n\n vec2 local = mod(uv - pos, cellSize) / cellSize;\n vec2 ct = local * 2.0 - 1.0;\n float d = length(ct);\n\n float ns = 16.0;\n float si = clamp(floor(lum * ns * gm), 0.0, ns - 1.0);\n float rad = si / ns;\n float alpha = smoothstep(rad + 0.08, rad - 0.08, d);\n\n vec3 tint = (c.rgb - si * 0.04) * 1.4;\n\n // Background \u2014 sampled with cover UVs\n vec2 bgUV = coverUV(uv, uBackgroundAspect, ar);\n vec2 bgCellUV = coverUV(pixelUv, uBackgroundAspect, ar);\n\n float beamReveal = smoothstep(0.4, 0.75, lum);\n vec3 bgRaw = texture(uBackgroundTexture, bgUV).rgb;\n vec3 bgRawColor = mix(vec3(0.0), bgRaw, uHasBackground * beamReveal);\n\n // Background content at cell-snapped UV (pixelated)\n vec3 bgCell = texture(uBackgroundTexture, bgCellUV).rgb;\n\n // Dithered mode: background image multiplies the beam tint.\n // bgCell * 0.6 + 0.4 maps [0,1] \u2192 [0.4, 1.0], so:\n // bright bg \u2192 tint \xD7 1.0 (same intensity as raw mode)\n // dark bg \u2192 tint \xD7 0.4 (dark areas of image dim the beam)\n // The beam's hue is always preserved; the background shows as luminance texture.\n vec3 bgCell3 = bgCell * 0.6 + 0.4;\n vec3 bgFiltered = tint * bgCell3;\n vec3 bgDotContent = mix(tint, bgFiltered, uHasBackground);\n vec3 dotColor = mix(tint, bgDotContent, uDitherBackground);\n\n // Gap (between dots): raw bg reveal in raw mode, black in dithered mode.\n vec3 gapColor = bgRawColor * (1.0 - uDitherBackground);\n\n fragColor = vec4(mix(gapColor, dotColor, alpha), 1.0);\n}\n";
5230
5250
 
5231
5251
  // src/shaders/dither-godray-dither/vertex.glsl
5232
5252
  var vertex_default15 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoord = uv;\n gl_Position = vec4(position, 1.0);\n}\n";
@@ -5235,21 +5255,72 @@ var vertex_default15 = "out vec2 vTextureCoord;\n\nvoid main() {\n vTextureCoor
5235
5255
  import { jsx as jsx25 } from "react/jsx-runtime";
5236
5256
  function DitherStreamDitherPass({
5237
5257
  inputTexture = null,
5258
+ backgroundImageSrc,
5259
+ ditherBackground = true,
5238
5260
  target = null,
5239
5261
  clear = true,
5240
5262
  enabled = true,
5241
5263
  priority = 0
5242
5264
  }) {
5243
5265
  const fallbackTexture = useMemo17(() => createFallbackTexture(), []);
5266
+ const [backgroundTexture, setBackgroundTexture] = useState4(null);
5267
+ const [backgroundAspect, setBackgroundAspect] = useState4(1);
5268
+ useEffect17(() => {
5269
+ if (!backgroundImageSrc) {
5270
+ setBackgroundTexture(null);
5271
+ setBackgroundAspect(1);
5272
+ return;
5273
+ }
5274
+ let active = true;
5275
+ const loader = new THREE22.TextureLoader();
5276
+ loader.load(
5277
+ backgroundImageSrc,
5278
+ (texture) => {
5279
+ if (!active) {
5280
+ texture.dispose();
5281
+ return;
5282
+ }
5283
+ texture.minFilter = THREE22.LinearFilter;
5284
+ texture.magFilter = THREE22.LinearFilter;
5285
+ texture.needsUpdate = true;
5286
+ const img = texture.image;
5287
+ if (img && img.naturalWidth && img.naturalHeight) {
5288
+ setBackgroundAspect(img.naturalWidth / img.naturalHeight);
5289
+ }
5290
+ setBackgroundTexture(texture);
5291
+ },
5292
+ void 0,
5293
+ (error) => {
5294
+ if (!active) return;
5295
+ console.error(
5296
+ "[DitherStream] Failed to load backgroundImageSrc \u2014 likely a CORS issue. The image server must include Access-Control-Allow-Origin headers.",
5297
+ backgroundImageSrc,
5298
+ error
5299
+ );
5300
+ setBackgroundTexture(null);
5301
+ }
5302
+ );
5303
+ return () => {
5304
+ active = false;
5305
+ };
5306
+ }, [backgroundImageSrc]);
5244
5307
  const uniforms = useMemo17(
5245
5308
  () => ({
5246
5309
  uTexture: { value: inputTexture ?? fallbackTexture },
5310
+ uBackgroundTexture: { value: fallbackTexture },
5311
+ uHasBackground: { value: 0 },
5312
+ uDitherBackground: { value: 1 },
5313
+ uBackgroundAspect: { value: 1 },
5247
5314
  uResolution: { value: new THREE22.Vector2(1, 1) }
5248
5315
  }),
5249
5316
  // eslint-disable-next-line react-hooks/exhaustive-deps
5250
5317
  []
5251
5318
  );
5252
5319
  uniforms.uTexture.value = inputTexture ?? fallbackTexture;
5320
+ uniforms.uBackgroundTexture.value = backgroundTexture ?? fallbackTexture;
5321
+ uniforms.uHasBackground.value = backgroundTexture ? 1 : 0;
5322
+ uniforms.uDitherBackground.value = ditherBackground ? 1 : 0;
5323
+ uniforms.uBackgroundAspect.value = backgroundAspect;
5253
5324
  return /* @__PURE__ */ jsx25(
5254
5325
  ShaderPass,
5255
5326
  {
@@ -5268,7 +5339,7 @@ function DitherStreamDitherPass({
5268
5339
  }
5269
5340
 
5270
5341
  // src/components/DitherStreamGodRaysPass.tsx
5271
- import { useEffect as useEffect17, useMemo as useMemo18 } from "react";
5342
+ import { useEffect as useEffect18, useMemo as useMemo18 } from "react";
5272
5343
  import * as THREE23 from "three";
5273
5344
 
5274
5345
  // src/shaders/dither-godray-extract/fragment.glsl
@@ -5313,13 +5384,13 @@ function DitherStreamGodRaysPass({
5313
5384
  () => new THREE23.WebGLRenderTarget(1, 1, TARGET_OPTIONS),
5314
5385
  []
5315
5386
  );
5316
- useEffect17(() => {
5387
+ useEffect18(() => {
5317
5388
  return () => {
5318
5389
  extractTarget.dispose();
5319
5390
  marchTarget.dispose();
5320
5391
  };
5321
5392
  }, [extractTarget, marchTarget]);
5322
- useEffect17(() => {
5393
+ useEffect18(() => {
5323
5394
  if (size.width <= 1 || size.height <= 1) return;
5324
5395
  extractTarget.setSize(size.width, size.height);
5325
5396
  marchTarget.setSize(
@@ -5402,7 +5473,7 @@ function DitherStreamGodRaysPass({
5402
5473
  }
5403
5474
 
5404
5475
  // src/components/DitherStreamProjectionPass.tsx
5405
- import { useEffect as useEffect18, useMemo as useMemo19, useState as useState4 } from "react";
5476
+ import { useEffect as useEffect19, useMemo as useMemo19, useState as useState5 } from "react";
5406
5477
  import * as THREE24 from "three";
5407
5478
 
5408
5479
  // src/shaders/dither-godray-projection/fragment.glsl
@@ -5422,9 +5493,9 @@ function DitherStreamProjectionPass({
5422
5493
  enabled = true,
5423
5494
  priority = 0
5424
5495
  }) {
5425
- const [imageTexture, setImageTexture] = useState4(null);
5496
+ const [imageTexture, setImageTexture] = useState5(null);
5426
5497
  const fallbackTexture = useMemo19(() => createFallbackTexture(), []);
5427
- useEffect18(() => {
5498
+ useEffect19(() => {
5428
5499
  let active = true;
5429
5500
  const loader = new THREE24.TextureLoader();
5430
5501
  loader.load(
@@ -5481,11 +5552,11 @@ function DitherStreamProjectionPass({
5481
5552
  }
5482
5553
 
5483
5554
  // src/components/DitherStreamRendererConfig.tsx
5484
- import { useEffect as useEffect19 } from "react";
5555
+ import { useEffect as useEffect20 } from "react";
5485
5556
  import * as THREE25 from "three";
5486
5557
  function DitherStreamRendererConfig() {
5487
5558
  const sharedScene = useSceneContext();
5488
- useEffect19(() => {
5559
+ useEffect20(() => {
5489
5560
  const context = sharedScene?.contextRef.current;
5490
5561
  if (!context) return;
5491
5562
  const renderer = context.renderer;
@@ -5508,7 +5579,10 @@ function DitherStream({
5508
5579
  height = "100%",
5509
5580
  className = "relative h-full w-full",
5510
5581
  style,
5582
+ children,
5511
5583
  imageTextureSrc,
5584
+ backgroundImageSrc,
5585
+ backgroundDithered = true,
5512
5586
  projectionSpeed = 0.05,
5513
5587
  beamSpeed = 0.1,
5514
5588
  beamDirection = "counterclockwise",
@@ -5517,6 +5591,8 @@ function DitherStream({
5517
5591
  beamRadius = 0.6,
5518
5592
  beamScale = 1,
5519
5593
  beamPathShape = "circle",
5594
+ beamCustomPathPoints = [],
5595
+ beamEnabled = true,
5520
5596
  pathPos = [0.5009, 1.0473],
5521
5597
  pathAngle = (0.999 - 0.25) * -6.28318531,
5522
5598
  godrayIntensity = 2.9
@@ -5553,6 +5629,7 @@ function DitherStream({
5553
5629
  },
5554
5630
  {
5555
5631
  component: DitherStreamBeamCompositePass,
5632
+ enabled: beamEnabled,
5556
5633
  props: {
5557
5634
  beamSpeed,
5558
5635
  beamDirection,
@@ -5561,12 +5638,14 @@ function DitherStream({
5561
5638
  beamRadius,
5562
5639
  beamScale,
5563
5640
  pathShape: beamPathShape,
5641
+ customPathPoints: beamCustomPathPoints,
5564
5642
  pathPos,
5565
5643
  pathAngle
5566
5644
  }
5567
5645
  },
5568
5646
  {
5569
- component: DitherStreamDitherPass
5647
+ component: DitherStreamDitherPass,
5648
+ props: { backgroundImageSrc, ditherBackground: backgroundDithered }
5570
5649
  },
5571
5650
  {
5572
5651
  component: DitherStreamGodRaysPass,
@@ -5574,6 +5653,350 @@ function DitherStream({
5574
5653
  }
5575
5654
  ]
5576
5655
  }
5656
+ ),
5657
+ children
5658
+ ]
5659
+ }
5660
+ );
5661
+ }
5662
+
5663
+ // src/components/DitherStreamPathDrawer.tsx
5664
+ import {
5665
+ useCallback as useCallback9,
5666
+ useEffect as useEffect21,
5667
+ useRef as useRef13,
5668
+ useState as useState6
5669
+ } from "react";
5670
+ import { Fragment as Fragment5, jsx as jsx29, jsxs as jsxs5 } from "react/jsx-runtime";
5671
+ var MIN_POINT_DISTANCE = 6e-3;
5672
+ var MAX_CAPTURED_POINTS = 512;
5673
+ var MAX_OUTPUT_POINTS = 64;
5674
+ var BRUSH_STEP_PX = 2;
5675
+ var BRUSH_RADIUS_PX = 8;
5676
+ var COMMIT_IDLE_MS = 2500;
5677
+ var DITHER_CELL = 3;
5678
+ var DITHER_HALO_FACTOR = 3.8;
5679
+ var BAYER_4X4 = [
5680
+ [0, 8, 2, 10],
5681
+ [12, 4, 14, 6],
5682
+ [3, 11, 1, 9],
5683
+ [15, 7, 13, 5]
5684
+ ].map((row) => row.map((v) => v / 16));
5685
+ var bayerThreshold = (px, py) => {
5686
+ const bx = (Math.floor(px / DITHER_CELL) % 4 + 4) % 4;
5687
+ const by = (Math.floor(py / DITHER_CELL) % 4 + 4) % 4;
5688
+ return BAYER_4X4[by][bx];
5689
+ };
5690
+ var clamp01 = (value) => Math.min(1, Math.max(0, value));
5691
+ var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
5692
+ var samplePath = (points, count) => {
5693
+ if (points.length <= count) return points;
5694
+ const sampled = [];
5695
+ for (let index = 0; index < count; index += 1) {
5696
+ const t = index / Math.max(1, count - 1);
5697
+ const sourceIndex = Math.round(t * (points.length - 1));
5698
+ sampled.push(points[sourceIndex]);
5699
+ }
5700
+ return sampled;
5701
+ };
5702
+ var hexToRgb = (hex) => {
5703
+ const normalized = hex.replace("#", "");
5704
+ if (normalized.length !== 6) {
5705
+ return { r: 170, g: 176, b: 240 };
5706
+ }
5707
+ return {
5708
+ r: parseInt(normalized.slice(0, 2), 16),
5709
+ g: parseInt(normalized.slice(2, 4), 16),
5710
+ b: parseInt(normalized.slice(4, 6), 16)
5711
+ };
5712
+ };
5713
+ var hash2 = (x, y) => {
5714
+ const n = x * 15731 + y * 789221 + 1376312589;
5715
+ const masked = n & 2147483647;
5716
+ return masked % 997 / 997;
5717
+ };
5718
+ function DitherStreamPathDrawer({
5719
+ enabled,
5720
+ beamColor = "#aab0f0",
5721
+ backgroundImageSrc,
5722
+ onCommit
5723
+ }) {
5724
+ const overlayRef = useRef13(null);
5725
+ const canvasRef = useRef13(null);
5726
+ const drawingRef = useRef13(false);
5727
+ const pointerIdRef = useRef13(null);
5728
+ const commitTimeoutRef = useRef13(null);
5729
+ const pointsRef = useRef13([]);
5730
+ const lastPixelPointRef = useRef13(null);
5731
+ const [cursor, setCursor] = useState6(null);
5732
+ const updatePoints = useCallback9((nextPoints) => {
5733
+ pointsRef.current = nextPoints;
5734
+ }, []);
5735
+ const getRelativePoint = useCallback9(
5736
+ (event) => {
5737
+ const rect = overlayRef.current?.getBoundingClientRect();
5738
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
5739
+ const x = clamp01((event.clientX - rect.left) / rect.width);
5740
+ const y = clamp01(1 - (event.clientY - rect.top) / rect.height);
5741
+ return {
5742
+ normalized: { x, y },
5743
+ pixels: {
5744
+ x: x * rect.width,
5745
+ y: (1 - y) * rect.height
5746
+ }
5747
+ };
5748
+ },
5749
+ []
5750
+ );
5751
+ const clearCanvas = useCallback9(() => {
5752
+ const canvas = canvasRef.current;
5753
+ if (!canvas) return;
5754
+ const ctx = canvas.getContext("2d");
5755
+ if (!ctx) return;
5756
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
5757
+ }, []);
5758
+ const stampBrush = useCallback9(
5759
+ (ctx, point, strength = 1) => {
5760
+ const color = hexToRgb(beamColor);
5761
+ const haloRadius = BRUSH_RADIUS_PX * DITHER_HALO_FACTOR;
5762
+ const glow = ctx.createRadialGradient(
5763
+ point.x,
5764
+ point.y,
5765
+ 0,
5766
+ point.x,
5767
+ point.y,
5768
+ haloRadius
5769
+ );
5770
+ glow.addColorStop(
5771
+ 0,
5772
+ `rgba(${color.r}, ${color.g}, ${color.b}, ${0.09 * strength})`
5773
+ );
5774
+ glow.addColorStop(1, "rgba(0,0,0,0)");
5775
+ ctx.fillStyle = glow;
5776
+ ctx.beginPath();
5777
+ ctx.arc(point.x, point.y, haloRadius, 0, Math.PI * 2);
5778
+ ctx.fill();
5779
+ const step = DITHER_CELL;
5780
+ const startX = Math.floor((point.x - haloRadius) / step) * step;
5781
+ const endX = Math.ceil((point.x + haloRadius) / step) * step;
5782
+ const startY = Math.floor((point.y - haloRadius) / step) * step;
5783
+ const endY = Math.ceil((point.y + haloRadius) / step) * step;
5784
+ for (let py = startY; py <= endY; py += step) {
5785
+ for (let px = startX; px <= endX; px += step) {
5786
+ const radial = Math.hypot(
5787
+ px + step * 0.5 - point.x,
5788
+ py + step * 0.5 - point.y
5789
+ );
5790
+ if (radial > haloRadius) continue;
5791
+ const normalizedDist = radial / haloRadius;
5792
+ const density = 1 - normalizedDist;
5793
+ if (bayerThreshold(px, py) >= density) continue;
5794
+ const alpha = density * 0.9 * strength;
5795
+ const isHot = normalizedDist < 0.28 && hash2(Math.floor(px) + 11, Math.floor(py) - 7) > 0.68;
5796
+ ctx.fillStyle = isHot ? `rgba(255,255,255,${alpha})` : `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`;
5797
+ ctx.fillRect(px, py, step, step);
5798
+ }
5799
+ }
5800
+ ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${0.88 * strength})`;
5801
+ ctx.beginPath();
5802
+ ctx.arc(point.x, point.y, BRUSH_RADIUS_PX * 0.55, 0, Math.PI * 2);
5803
+ ctx.fill();
5804
+ ctx.fillStyle = `rgba(255,255,255,${0.92 * strength})`;
5805
+ ctx.beginPath();
5806
+ ctx.arc(point.x, point.y, BRUSH_RADIUS_PX * 0.28, 0, Math.PI * 2);
5807
+ ctx.fill();
5808
+ },
5809
+ [beamColor]
5810
+ );
5811
+ const drawSegment = useCallback9(
5812
+ (from, to) => {
5813
+ const canvas = canvasRef.current;
5814
+ if (!canvas) return;
5815
+ const ctx = canvas.getContext("2d");
5816
+ if (!ctx) return;
5817
+ const dx = to.x - from.x;
5818
+ const dy = to.y - from.y;
5819
+ const distancePx = Math.hypot(dx, dy);
5820
+ const steps = Math.max(1, Math.ceil(distancePx / BRUSH_STEP_PX));
5821
+ for (let index = 0; index <= steps; index += 1) {
5822
+ const t = index / steps;
5823
+ stampBrush(
5824
+ ctx,
5825
+ { x: from.x + dx * t, y: from.y + dy * t },
5826
+ drawingRef.current ? 1 : 0.92
5827
+ );
5828
+ }
5829
+ },
5830
+ [stampBrush]
5831
+ );
5832
+ const finalizePath = useCallback9(() => {
5833
+ if (commitTimeoutRef.current) {
5834
+ clearTimeout(commitTimeoutRef.current);
5835
+ commitTimeoutRef.current = null;
5836
+ }
5837
+ const capturedPoints = pointsRef.current;
5838
+ if (capturedPoints.length < 2) {
5839
+ updatePoints([]);
5840
+ return;
5841
+ }
5842
+ const sampled = samplePath(capturedPoints, MAX_OUTPUT_POINTS);
5843
+ onCommit(sampled.map((point) => [point.x, point.y]));
5844
+ updatePoints([]);
5845
+ }, [onCommit, updatePoints]);
5846
+ const queueCommit = useCallback9(() => {
5847
+ if (commitTimeoutRef.current) {
5848
+ clearTimeout(commitTimeoutRef.current);
5849
+ }
5850
+ commitTimeoutRef.current = setTimeout(() => {
5851
+ finalizePath();
5852
+ }, COMMIT_IDLE_MS);
5853
+ }, [finalizePath]);
5854
+ const addPoint = useCallback9(
5855
+ (point) => {
5856
+ const previousPoints = pointsRef.current;
5857
+ if (!previousPoints.length) {
5858
+ updatePoints([point]);
5859
+ return;
5860
+ }
5861
+ const lastPoint = previousPoints[previousPoints.length - 1];
5862
+ if (previousPoints.length < MAX_CAPTURED_POINTS && distance(lastPoint, point) >= MIN_POINT_DISTANCE) {
5863
+ updatePoints([...previousPoints, point]);
5864
+ }
5865
+ },
5866
+ [updatePoints]
5867
+ );
5868
+ useEffect21(() => {
5869
+ if (enabled) return;
5870
+ drawingRef.current = false;
5871
+ pointerIdRef.current = null;
5872
+ if (commitTimeoutRef.current) {
5873
+ clearTimeout(commitTimeoutRef.current);
5874
+ commitTimeoutRef.current = null;
5875
+ }
5876
+ pointsRef.current = [];
5877
+ lastPixelPointRef.current = null;
5878
+ setCursor(null);
5879
+ clearCanvas();
5880
+ }, [clearCanvas, enabled]);
5881
+ useEffect21(() => {
5882
+ if (!enabled) return;
5883
+ const canvas = canvasRef.current;
5884
+ const container = overlayRef.current;
5885
+ if (!canvas || !container) return;
5886
+ const resize = () => {
5887
+ const rect = container.getBoundingClientRect();
5888
+ const dpr = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
5889
+ canvas.width = Math.max(1, Math.floor(rect.width * dpr));
5890
+ canvas.height = Math.max(1, Math.floor(rect.height * dpr));
5891
+ canvas.style.width = `${rect.width}px`;
5892
+ canvas.style.height = `${rect.height}px`;
5893
+ const ctx = canvas.getContext("2d");
5894
+ if (!ctx) return;
5895
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
5896
+ ctx.globalCompositeOperation = "source-over";
5897
+ ctx.clearRect(0, 0, rect.width, rect.height);
5898
+ };
5899
+ resize();
5900
+ const observer = new ResizeObserver(resize);
5901
+ observer.observe(container);
5902
+ return () => observer.disconnect();
5903
+ }, [enabled]);
5904
+ if (!enabled) return null;
5905
+ return /* @__PURE__ */ jsxs5(
5906
+ "div",
5907
+ {
5908
+ ref: overlayRef,
5909
+ className: "absolute inset-0 z-20 cursor-crosshair touch-none",
5910
+ style: {
5911
+ background: backgroundImageSrc ? void 0 : "radial-gradient(circle at center, rgba(102,110,164,0.10), rgba(7,9,17,0.72))",
5912
+ border: "1px dashed rgba(188,196,246,0.35)"
5913
+ },
5914
+ onPointerDown: (event) => {
5915
+ const point = getRelativePoint(event);
5916
+ if (!point) return;
5917
+ drawingRef.current = true;
5918
+ pointerIdRef.current = event.pointerId;
5919
+ setCursor(point.normalized);
5920
+ const existingPoints = pointsRef.current;
5921
+ if (existingPoints.length === 0) {
5922
+ updatePoints([point.normalized]);
5923
+ } else {
5924
+ updatePoints([...existingPoints, point.normalized]);
5925
+ }
5926
+ lastPixelPointRef.current = point.pixels;
5927
+ const canvas = canvasRef.current;
5928
+ const ctx = canvas?.getContext("2d");
5929
+ if (ctx) {
5930
+ stampBrush(ctx, point.pixels);
5931
+ }
5932
+ queueCommit();
5933
+ event.currentTarget.setPointerCapture(event.pointerId);
5934
+ },
5935
+ onPointerMove: (event) => {
5936
+ const point = getRelativePoint(event);
5937
+ if (!point) return;
5938
+ setCursor(point.normalized);
5939
+ if (!drawingRef.current) return;
5940
+ addPoint(point.normalized);
5941
+ const lastPoint = lastPixelPointRef.current;
5942
+ if (lastPoint) {
5943
+ drawSegment(lastPoint, point.pixels);
5944
+ }
5945
+ lastPixelPointRef.current = point.pixels;
5946
+ queueCommit();
5947
+ },
5948
+ onPointerUp: (event) => {
5949
+ if (pointerIdRef.current === event.pointerId) {
5950
+ event.currentTarget.releasePointerCapture(event.pointerId);
5951
+ }
5952
+ drawingRef.current = false;
5953
+ pointerIdRef.current = null;
5954
+ lastPixelPointRef.current = null;
5955
+ },
5956
+ onPointerCancel: (event) => {
5957
+ if (pointerIdRef.current === event.pointerId) {
5958
+ event.currentTarget.releasePointerCapture(event.pointerId);
5959
+ }
5960
+ drawingRef.current = false;
5961
+ pointerIdRef.current = null;
5962
+ lastPixelPointRef.current = null;
5963
+ if (commitTimeoutRef.current) {
5964
+ clearTimeout(commitTimeoutRef.current);
5965
+ commitTimeoutRef.current = null;
5966
+ }
5967
+ updatePoints([]);
5968
+ clearCanvas();
5969
+ },
5970
+ children: [
5971
+ backgroundImageSrc && /* @__PURE__ */ jsxs5(Fragment5, { children: [
5972
+ /* @__PURE__ */ jsx29(
5973
+ "img",
5974
+ {
5975
+ src: backgroundImageSrc,
5976
+ className: "pointer-events-none absolute inset-0 h-full w-full object-cover",
5977
+ alt: "",
5978
+ "aria-hidden": true
5979
+ }
5980
+ ),
5981
+ /* @__PURE__ */ jsx29("div", { className: "pointer-events-none absolute inset-0 bg-black/50" })
5982
+ ] }),
5983
+ /* @__PURE__ */ jsx29("div", { className: "pointer-events-none absolute left-1/2 top-3 -translate-x-1/2 rounded-full border border-white/30 bg-black/40 px-3 py-1 text-[11px] uppercase tracking-[0.14em] text-white/80", children: "Click and drag to draw your beam path" }),
5984
+ /* @__PURE__ */ jsx29(
5985
+ "canvas",
5986
+ {
5987
+ ref: canvasRef,
5988
+ className: "pointer-events-none absolute inset-0"
5989
+ }
5990
+ ),
5991
+ cursor && /* @__PURE__ */ jsx29(
5992
+ "div",
5993
+ {
5994
+ className: "pointer-events-none absolute h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/70 bg-white/15",
5995
+ style: {
5996
+ left: `${cursor.x * 100}%`,
5997
+ top: `${(1 - cursor.y) * 100}%`
5998
+ }
5999
+ }
5577
6000
  )
5578
6001
  ]
5579
6002
  }
@@ -5583,6 +6006,7 @@ export {
5583
6006
  AnimatedDrawingSVG,
5584
6007
  DitherPulseRing,
5585
6008
  DitherStream,
6009
+ DitherStreamPathDrawer,
5586
6010
  EFECTO_ASCII_COMPONENT_DEFAULTS,
5587
6011
  EFECTO_ASCII_POST_PROCESSING_DEFAULTS,
5588
6012
  Efecto,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toriistudio/shader-ui",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Shader components",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",