@3driseai/3drise-viewer 0.1.22 → 0.1.23
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/components/CreatedObject.d.ts.map +1 -1
- package/dist/components/CreatedObject.js +6 -1
- package/dist/components/LightObject.d.ts.map +1 -1
- package/dist/components/LightObject.js +20 -13
- package/dist/components/ModelPrimitive.d.ts.map +1 -1
- package/dist/components/ModelPrimitive.js +4 -6
- package/dist/components/ObjectNode.js +1 -1
- package/dist/components/ParticlesGenerator.d.ts +3 -1
- package/dist/components/ParticlesGenerator.d.ts.map +1 -1
- package/dist/components/ParticlesGenerator.js +2 -2
- package/dist/components/ProjectLoader.js +1 -1
- package/dist/components/SceneBuilder.d.ts.map +1 -1
- package/dist/components/SceneBuilder.js +5 -1
- package/dist/components/fluorescentTube/FluorescentTube.d.ts +14 -0
- package/dist/components/fluorescentTube/FluorescentTube.d.ts.map +1 -0
- package/dist/components/fluorescentTube/FluorescentTube.js +262 -0
- package/dist/components/fluorescentTube/index.d.ts +5 -0
- package/dist/components/fluorescentTube/index.d.ts.map +1 -0
- package/dist/components/fluorescentTube/index.js +3 -0
- package/dist/components/fluorescentTube/tubeMaterials.d.ts +34 -0
- package/dist/components/fluorescentTube/tubeMaterials.d.ts.map +1 -0
- package/dist/components/fluorescentTube/tubeMaterials.js +185 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -0
- package/dist/components/ledArray/LedArray.d.ts +14 -0
- package/dist/components/ledArray/LedArray.d.ts.map +1 -0
- package/dist/components/ledArray/LedArray.js +243 -0
- package/dist/components/ledArray/index.d.ts +7 -0
- package/dist/components/ledArray/index.d.ts.map +1 -0
- package/dist/components/ledArray/index.js +4 -0
- package/dist/components/ledArray/ledLayout.d.ts +22 -0
- package/dist/components/ledArray/ledLayout.d.ts.map +1 -0
- package/dist/components/ledArray/ledLayout.js +59 -0
- package/dist/components/ledArray/ledMaterials.d.ts +14 -0
- package/dist/components/ledArray/ledMaterials.d.ts.map +1 -0
- package/dist/components/ledArray/ledMaterials.js +151 -0
- package/dist/components/lightBulb/LightBulb.d.ts.map +1 -1
- package/dist/components/lightBulb/LightBulb.js +10 -13
- package/dist/components/particles/BioluminescentWakeParticles.d.ts +3 -1
- package/dist/components/particles/BioluminescentWakeParticles.d.ts.map +1 -1
- package/dist/components/particles/BioluminescentWakeParticles.js +7 -3
- package/dist/components/particles/BrownianDustParticles.d.ts +3 -1
- package/dist/components/particles/BrownianDustParticles.d.ts.map +1 -1
- package/dist/components/particles/BrownianDustParticles.js +7 -3
- package/dist/components/particles/ChladniSandParticles.d.ts +3 -1
- package/dist/components/particles/ChladniSandParticles.d.ts.map +1 -1
- package/dist/components/particles/ChladniSandParticles.js +7 -3
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts +3 -1
- package/dist/components/particles/ElectronOrbitalsParticles.d.ts.map +1 -1
- package/dist/components/particles/ElectronOrbitalsParticles.js +7 -3
- package/dist/components/particles/FaradayPowderParticles.d.ts +3 -1
- package/dist/components/particles/FaradayPowderParticles.d.ts.map +1 -1
- package/dist/components/particles/FaradayPowderParticles.js +7 -3
- package/dist/components/particles/GrinderSparksParticles.d.ts +3 -1
- package/dist/components/particles/GrinderSparksParticles.d.ts.map +1 -1
- package/dist/components/particles/GrinderSparksParticles.js +7 -3
- package/dist/components/particles/NeonRainParticles.d.ts +3 -1
- package/dist/components/particles/NeonRainParticles.d.ts.map +1 -1
- package/dist/components/particles/NeonRainParticles.js +7 -3
- package/dist/components/particles/TidalStreamParticles.d.ts +3 -1
- package/dist/components/particles/TidalStreamParticles.d.ts.map +1 -1
- package/dist/components/particles/TidalStreamParticles.js +7 -3
- package/dist/components/spotBeam/SpotBeam.d.ts +14 -0
- package/dist/components/spotBeam/SpotBeam.d.ts.map +1 -0
- package/dist/components/spotBeam/SpotBeam.js +244 -0
- package/dist/components/spotBeam/beamPatterns.d.ts +11 -0
- package/dist/components/spotBeam/beamPatterns.d.ts.map +1 -0
- package/dist/components/spotBeam/beamPatterns.js +106 -0
- package/dist/components/spotBeam/index.d.ts +7 -0
- package/dist/components/spotBeam/index.d.ts.map +1 -0
- package/dist/components/spotBeam/index.js +4 -0
- package/dist/components/spotBeam/spotBeamMaterials.d.ts +16 -0
- package/dist/components/spotBeam/spotBeamMaterials.d.ts.map +1 -0
- package/dist/components/spotBeam/spotBeamMaterials.js +307 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useLightConfig.d.ts +25 -0
- package/dist/hooks/useLightConfig.d.ts.map +1 -0
- package/dist/hooks/useLightConfig.js +125 -0
- package/dist/hooks/useLightLive.d.ts.map +1 -1
- package/dist/hooks/useLightLive.js +9 -2
- package/dist/hooks/useShadowApplication.d.ts +4 -4
- package/dist/hooks/useShadowApplication.d.ts.map +1 -1
- package/dist/hooks/useShadowApplication.js +25 -10
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { useFrame } from '@react-three/fiber';
|
|
5
5
|
import { GENERATIVE_PARTICLES_ANIMATABLE } from '@3driseai/3drise-core';
|
|
6
|
-
import { applyConfigKeyframeAnimations } from '../../utils/configAnimations.js';
|
|
6
|
+
import { applyConfigKeyframeAnimations, applyConfigMouseMove } from '../../utils/configAnimations.js';
|
|
7
7
|
import { createPointerUniforms, stepPointerField, usePointerField } from '../../utils/pointerField.js';
|
|
8
8
|
import { createTrailUniforms, applyTrailConfig, createCpuTrail } from '../../utils/particleTrails.js';
|
|
9
9
|
import { useObjectReady } from '../../hooks/useObjectReady.js';
|
|
@@ -768,7 +768,7 @@ const DEFAULTS = {
|
|
|
768
768
|
const ANIMATABLE = new Set(GENERATIVE_PARTICLES_ANIMATABLE.tidalStream.map((p) => p.value));
|
|
769
769
|
/** Knobs that rebuild the simulation: they change on commit, never through the live path. */
|
|
770
770
|
const STRUCTURAL = ['particleCount', 'satelliteCount', 'orbitEccentricity', 'streamAge', 'discStars'];
|
|
771
|
-
const TidalStreamParticles = ({ id, size, config, animations, onObjectReady, onObjectDispose }) => {
|
|
771
|
+
const TidalStreamParticles = ({ id, size, config, animations, mouseMove, onObjectReady, onObjectDispose }) => {
|
|
772
772
|
const rootRef = useRef(null);
|
|
773
773
|
useObjectReady(rootRef, config, onObjectReady, onObjectDispose);
|
|
774
774
|
const s = size > 0 ? size : 1;
|
|
@@ -800,8 +800,12 @@ const TidalStreamParticles = ({ id, size, config, animations, onObjectReady, onO
|
|
|
800
800
|
const pointer = usePointerField();
|
|
801
801
|
useFrame((state, delta) => {
|
|
802
802
|
instance.update(Math.min(delta, 0.1), state.camera, pointer);
|
|
803
|
-
|
|
803
|
+
// Keyframes first, pointer on top: a knob bound to both follows the cursor.
|
|
804
|
+
const pointerBound = !!mouseMove?.enabled && !!mouseMove.properties?.length;
|
|
805
|
+
if (keyframes?.length || pointerBound) {
|
|
804
806
|
applyConfigKeyframeAnimations(keyframes, animFrom.current, cfg, animOverride.current, performance.now() - animStart.current, ANIMATABLE);
|
|
807
|
+
if (pointerBound)
|
|
808
|
+
applyConfigMouseMove(mouseMove.properties, cfg, animOverride.current, state.pointer, ANIMATABLE);
|
|
805
809
|
push(live.current);
|
|
806
810
|
}
|
|
807
811
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AnimationOptions, CreatedObjectSettings, MouseMoveDomain } from '@3driseai/3drise-core';
|
|
3
|
+
import type { ObjectReadyProps } from '../../types/objectReady';
|
|
4
|
+
export type SpotBeamProps = ObjectReadyProps & {
|
|
5
|
+
/** Scene object id: the light controller finds the live-edit handle by it. */
|
|
6
|
+
id?: string;
|
|
7
|
+
config: Record<string, any>;
|
|
8
|
+
animations?: AnimationOptions[];
|
|
9
|
+
mouseMove?: MouseMoveDomain;
|
|
10
|
+
settings: CreatedObjectSettings | Record<string, any>;
|
|
11
|
+
};
|
|
12
|
+
declare const SpotBeam: React.FC<SpotBeamProps>;
|
|
13
|
+
export default SpotBeam;
|
|
14
|
+
//# sourceMappingURL=SpotBeam.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpotBeam.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/SpotBeam.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAK1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAItG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG;IAC3C,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACzD,CAAC;AAuBF,QAAA,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,CAwOrC,CAAC;AACF,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// SpotBeam — a spot light you can see: pattern, beam and pool
|
|
4
|
+
// =============================================================================
|
|
5
|
+
//
|
|
6
|
+
// Rendered by LightObject for `config.type === 'spotBeam'`. This is a light, not
|
|
7
|
+
// a lamp: the geometry is a small emitter you can grab in the editor (and can
|
|
8
|
+
// switch off entirely), because the object is meant to be dropped inside whatever
|
|
9
|
+
// fixture the scene already has.
|
|
10
|
+
//
|
|
11
|
+
// The work is in the optics:
|
|
12
|
+
// - a procedural mask on SpotLight.map shapes the pool (slot, bars, blinds,
|
|
13
|
+
// window, leaves) — see beamPatterns.ts;
|
|
14
|
+
// - the beam samples that same mask, so the haze in the air and the light on
|
|
15
|
+
// the floor are the same shape;
|
|
16
|
+
// - the beam is nested cone shells in one InstancedMesh, dense on the axis.
|
|
17
|
+
//
|
|
18
|
+
// The object's origin is the light. It aims at `targetPosition` in world space,
|
|
19
|
+
// the same contract the plain spot light uses.
|
|
20
|
+
//
|
|
21
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
22
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
23
|
+
import { CircleGeometry, Color, ConeGeometry, InstancedBufferAttribute, Matrix4, Object3D, SphereGeometry, Vector3 } from 'three';
|
|
24
|
+
import { SELF_OWNED_MATERIAL_KEY } from '@3driseai/3drise-core';
|
|
25
|
+
import { useObjectReady } from '../../hooks/useObjectReady.js';
|
|
26
|
+
import { useLightShadow } from '../../hooks/useLightShadow.js';
|
|
27
|
+
import { useLightConfig } from '../../hooks/useLightConfig.js';
|
|
28
|
+
import { createBeamPattern } from './beamPatterns.js';
|
|
29
|
+
import { createSpotBeamMaterials, disposeSpotBeamMaterials } from './spotBeamMaterials.js';
|
|
30
|
+
import { kelvinToColor } from '../lightBulb/kelvin.js';
|
|
31
|
+
/** Cone shells: more is smoother haze, and each one is a draw. */
|
|
32
|
+
const LAYERS = 10;
|
|
33
|
+
const num = (v, fallback) => (typeof v === 'number' ? v : fallback);
|
|
34
|
+
const hash = (n) => {
|
|
35
|
+
const s = Math.sin(n * 127.1) * 43758.5453;
|
|
36
|
+
return s - Math.floor(s);
|
|
37
|
+
};
|
|
38
|
+
const noise1 = (x) => {
|
|
39
|
+
const i = Math.floor(x), f = x - i, u = f * f * (3 - 2 * f);
|
|
40
|
+
return hash(i) * (1 - u) + hash(i + 1) * u;
|
|
41
|
+
};
|
|
42
|
+
const COLD = new Color(0.06, 0.02, 0.0);
|
|
43
|
+
const EMBER = new Color(1.0, 0.4, 0.08);
|
|
44
|
+
const WHITE = new Color(1, 1, 1);
|
|
45
|
+
const scratchVec = new Vector3();
|
|
46
|
+
const scratchScale = new Vector3();
|
|
47
|
+
const scratchMatrix = new Matrix4();
|
|
48
|
+
const SpotBeam = ({ id, config, animations, mouseMove, settings, onObjectReady, onObjectDispose }) => {
|
|
49
|
+
const scene = useThree((s) => s.scene);
|
|
50
|
+
const groupRef = useRef(null);
|
|
51
|
+
const lightRef = useRef(null);
|
|
52
|
+
const targetRef = useRef(new Object3D());
|
|
53
|
+
const beamRef = useRef(null);
|
|
54
|
+
const soloRef = useRef(null);
|
|
55
|
+
useObjectReady(groupRef, settings, onObjectReady, onObjectDispose);
|
|
56
|
+
useLightShadow(lightRef, config.shadow);
|
|
57
|
+
const { merged } = useLightConfig(groupRef, lightRef, id, config, animations, mouseMove);
|
|
58
|
+
// --- geometry: a unit cone for the beam, a small emitter ---
|
|
59
|
+
const geometry = useMemo(() => {
|
|
60
|
+
// apex at the origin, opening along +Y, rim radius == height so the
|
|
61
|
+
// shader can read x/y as the projector's coordinate
|
|
62
|
+
const cone = new ConeGeometry(1, 1, 56, 18, true);
|
|
63
|
+
cone.rotateX(Math.PI);
|
|
64
|
+
cone.translate(0, 0.5, 0);
|
|
65
|
+
const lens = new CircleGeometry(0.16, 40);
|
|
66
|
+
lens.rotateX(-Math.PI / 2);
|
|
67
|
+
const dot = new SphereGeometry(0.05, 20, 12);
|
|
68
|
+
return { cone, lens, dot };
|
|
69
|
+
}, []);
|
|
70
|
+
useEffect(() => () => {
|
|
71
|
+
geometry.cone.dispose();
|
|
72
|
+
geometry.lens.dispose();
|
|
73
|
+
geometry.dot.dispose();
|
|
74
|
+
}, [geometry]);
|
|
75
|
+
const mats = useMemo(() => createSpotBeamMaterials(), []);
|
|
76
|
+
useEffect(() => () => disposeSpotBeamMaterials(mats), [mats]);
|
|
77
|
+
// --- the projected mask: shapes the pool and the haze together ---
|
|
78
|
+
const pattern = (config.pattern ?? 'round');
|
|
79
|
+
const patternSoftness = Math.min(Math.max(num(config.patternSoftness, 0.25), 0), 1);
|
|
80
|
+
const patternRotation = num(config.patternRotation, 0);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const texture = createBeamPattern(pattern, patternSoftness, patternRotation);
|
|
83
|
+
mats.layered.uniforms.uPattern.value = texture;
|
|
84
|
+
mats.volumetric.uniforms.uPattern.value = texture;
|
|
85
|
+
if (lightRef.current)
|
|
86
|
+
lightRef.current.map = texture;
|
|
87
|
+
return () => {
|
|
88
|
+
if (lightRef.current?.map === texture)
|
|
89
|
+
lightRef.current.map = null;
|
|
90
|
+
if (mats.layered.uniforms.uPattern.value === texture)
|
|
91
|
+
mats.layered.uniforms.uPattern.value = null;
|
|
92
|
+
if (mats.volumetric.uniforms.uPattern.value === texture)
|
|
93
|
+
mats.volumetric.uniforms.uPattern.value = null;
|
|
94
|
+
texture.dispose();
|
|
95
|
+
};
|
|
96
|
+
}, [mats, pattern, patternSoftness, patternRotation]);
|
|
97
|
+
// One shell per layer, fraction 0 (axis) → 1 (rim).
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const beam = beamRef.current;
|
|
100
|
+
if (!beam)
|
|
101
|
+
return;
|
|
102
|
+
const fracs = new Float32Array(LAYERS);
|
|
103
|
+
for (let i = 0; i < LAYERS; i++)
|
|
104
|
+
fracs[i] = (i + 1) / LAYERS;
|
|
105
|
+
beam.geometry.setAttribute('aFrac', new InstancedBufferAttribute(fracs, 1));
|
|
106
|
+
mats.layered.uniforms.uLayers.value = LAYERS;
|
|
107
|
+
}, [mats]);
|
|
108
|
+
const lightColor = useMemo(() => ((config.colorMode ?? 'temperature') === 'color'
|
|
109
|
+
? new Color(config.color || '#ffffff')
|
|
110
|
+
: kelvinToColor(config.temperature ?? 3600)), [config.colorMode, config.color, config.temperature]);
|
|
111
|
+
// targetPosition is world space, like the plain spot light's.
|
|
112
|
+
const [tx, ty, tz] = (config.targetPosition ?? [0, 0, 0]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const light = lightRef.current;
|
|
115
|
+
if (!light)
|
|
116
|
+
return;
|
|
117
|
+
const target = targetRef.current;
|
|
118
|
+
target.position.set(tx, ty, tz);
|
|
119
|
+
target.updateMatrixWorld();
|
|
120
|
+
light.target = target;
|
|
121
|
+
scene.add(target);
|
|
122
|
+
return () => { scene.remove(target); };
|
|
123
|
+
}, [scene, tx, ty, tz]);
|
|
124
|
+
// The light owns its materials and its shadow flags.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const group = groupRef.current;
|
|
127
|
+
if (!group)
|
|
128
|
+
return;
|
|
129
|
+
group.userData[SELF_OWNED_MATERIAL_KEY] = true;
|
|
130
|
+
return () => { delete group.userData[SELF_OWNED_MATERIAL_KEY]; };
|
|
131
|
+
}, []);
|
|
132
|
+
// Anything the user puts in front of this light is usually centimetres away.
|
|
133
|
+
const shadowNear = config.shadow?.cameraNear;
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const light = lightRef.current;
|
|
136
|
+
if (!light || shadowNear !== undefined)
|
|
137
|
+
return;
|
|
138
|
+
light.shadow.camera.near = 0.05;
|
|
139
|
+
light.shadow.camera.updateProjectionMatrix();
|
|
140
|
+
}, [shadowNear]);
|
|
141
|
+
const emitter = (config.emitter ?? 'lens');
|
|
142
|
+
// Which beam the user picked. Each mode is a different mesh, so this rebuilds
|
|
143
|
+
// rather than tweens: off, one shell, nested shells, or a raymarched volume.
|
|
144
|
+
const beamMode = (config.beamMode ?? 'soft');
|
|
145
|
+
const power = useRef({ level: (config.on ?? true) ? 1 : 0, base: config.intensity ?? 1, written: Number.NaN, seed: Math.random() * 1000 });
|
|
146
|
+
const scratch = useMemo(() => ({ heat: new Color(), glow: new Color() }), []);
|
|
147
|
+
useFrame(({ clock, camera }, delta) => {
|
|
148
|
+
const light = lightRef.current;
|
|
149
|
+
const group = groupRef.current;
|
|
150
|
+
if (!light || !group)
|
|
151
|
+
return;
|
|
152
|
+
const cfg = merged.current;
|
|
153
|
+
const p = power.current;
|
|
154
|
+
const on = cfg.on ?? true;
|
|
155
|
+
const warmup = Math.max(num(cfg.warmup, 0.3), 0);
|
|
156
|
+
const flicker = Math.min(Math.max(num(cfg.flicker, 0), 0), 1);
|
|
157
|
+
const beamAmount = Math.min(Math.max(num(cfg.beam, 1), 0), 3);
|
|
158
|
+
const haloAmount = Math.min(Math.max(num(cfg.halo, 1), 0), 3);
|
|
159
|
+
const baseIntensity = Math.max(num(config.intensity, 1), 1e-4);
|
|
160
|
+
if (light.intensity !== p.written)
|
|
161
|
+
p.base = light.intensity;
|
|
162
|
+
const target = on ? 1 : 0;
|
|
163
|
+
const tau = target > p.level ? warmup : warmup * 2.2;
|
|
164
|
+
p.level = tau <= 0 ? target : p.level + (target - p.level) * (1 - Math.exp(-Math.min(delta, 0.1) * 3 / tau));
|
|
165
|
+
if (Math.abs(target - p.level) < 1e-4)
|
|
166
|
+
p.level = target;
|
|
167
|
+
let flick = 1;
|
|
168
|
+
if (flicker > 0) {
|
|
169
|
+
const t = clock.elapsedTime + p.seed;
|
|
170
|
+
flick = 1 - flicker * (0.12 * noise1(t * 11) + 0.55 * Math.pow(noise1(t * 2.7), 4));
|
|
171
|
+
}
|
|
172
|
+
const output = Math.pow(p.level, 2.0) * flick;
|
|
173
|
+
light.intensity = p.base * output;
|
|
174
|
+
p.written = light.intensity;
|
|
175
|
+
const glowK = Math.min(output * (p.base / baseIntensity), 3);
|
|
176
|
+
const k = p.level * flick;
|
|
177
|
+
// Emitter: ember while it warms, then the light's own colour.
|
|
178
|
+
const heat = scratch.heat;
|
|
179
|
+
if (k < 0.5)
|
|
180
|
+
heat.copy(COLD).lerp(EMBER, k / 0.5);
|
|
181
|
+
else
|
|
182
|
+
heat.copy(EMBER).lerp(scratch.glow.copy(light.color).lerp(WHITE, 0.55), (k - 0.5) / 0.5);
|
|
183
|
+
mats.lens.color.copy(heat).multiplyScalar(0.05 + glowK * 2.2);
|
|
184
|
+
const glow = scratch.glow.copy(light.color);
|
|
185
|
+
mats.halo.uniforms.uColor.value.copy(glow);
|
|
186
|
+
mats.halo.uniforms.uStrength.value = haloAmount * glowK * 0.55;
|
|
187
|
+
const penumbra = num(cfg.penumbra, 0.3);
|
|
188
|
+
const noise = Math.min(Math.max(num(cfg.beamNoise, 0.35), 0), 1);
|
|
189
|
+
const active = beamMode === 'layered' ? mats.layered : beamMode === 'volumetric' ? mats.volumetric : mats.soft;
|
|
190
|
+
active.uniforms.uColor.value.copy(glow);
|
|
191
|
+
active.uniforms.uPenumbra.value = penumbra;
|
|
192
|
+
if (beamMode === 'layered') {
|
|
193
|
+
mats.layered.uniforms.uNoise.value = noise;
|
|
194
|
+
mats.layered.uniforms.uTime.value = clock.elapsedTime;
|
|
195
|
+
mats.layered.uniforms.uStrength.value = beamAmount * glowK * 0.45;
|
|
196
|
+
}
|
|
197
|
+
else if (beamMode === 'volumetric') {
|
|
198
|
+
mats.volumetric.uniforms.uNoise.value = noise;
|
|
199
|
+
mats.volumetric.uniforms.uTime.value = clock.elapsedTime;
|
|
200
|
+
mats.volumetric.uniforms.uSteps.value = num(cfg.beamSteps, 28);
|
|
201
|
+
mats.volumetric.uniforms.uStrength.value = beamAmount * glowK * 0.9;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
mats.soft.uniforms.uStrength.value = beamAmount * glowK * 0.5;
|
|
205
|
+
}
|
|
206
|
+
// Size and aim whichever beam mesh is mounted: as wide as the cone, as
|
|
207
|
+
// long as the throw, pointing down the light's own axis.
|
|
208
|
+
const mesh = beamMode === 'layered' ? beamRef.current : soloRef.current;
|
|
209
|
+
if (mesh) {
|
|
210
|
+
const reach = targetRef.current.position.distanceTo(group.getWorldPosition(scratchVec)) || 6;
|
|
211
|
+
const worldScale = group.getWorldScale(scratchScale).x || 1;
|
|
212
|
+
const length = Math.max((reach / worldScale) * num(cfg.beamLength, 1), 0.1);
|
|
213
|
+
const radius = Math.tan(Math.min(num(cfg.angle, Math.PI / 6), 1.45)) * length;
|
|
214
|
+
const visible = beamAmount > 0 && glowK > 0.01;
|
|
215
|
+
mesh.visible = visible;
|
|
216
|
+
group.worldToLocal(scratchVec.copy(targetRef.current.position));
|
|
217
|
+
mesh.quaternion.identity();
|
|
218
|
+
mesh.up.set(0, 0, 1);
|
|
219
|
+
mesh.lookAt(scratchVec);
|
|
220
|
+
mesh.rotateX(Math.PI / 2); // the cone opens along +Y, lookAt aims +Z
|
|
221
|
+
if (beamMode === 'layered') {
|
|
222
|
+
for (let i = 0; i < LAYERS; i++) {
|
|
223
|
+
scratchMatrix.makeScale(radius, length, radius);
|
|
224
|
+
mesh.setMatrixAt(i, scratchMatrix);
|
|
225
|
+
}
|
|
226
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
mesh.scale.set(radius, length, radius);
|
|
230
|
+
}
|
|
231
|
+
if (beamMode === 'volumetric' && visible) {
|
|
232
|
+
// The raymarch runs in the cone's own space, so the camera goes there too.
|
|
233
|
+
mesh.updateMatrixWorld();
|
|
234
|
+
mats.volumetric.uniforms.uCamera.value.copy(camera.position);
|
|
235
|
+
mesh.worldToLocal(mats.volumetric.uniforms.uCamera.value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return (_jsxs("group", { ref: groupRef, children: [_jsx("spotLight", { ref: lightRef,
|
|
240
|
+
// THREE.SpotLight's constructor parks itself at (0, 1, 0); left alone
|
|
241
|
+
// the light sits a unit away from the object it belongs to.
|
|
242
|
+
position: [0, 0, 0], color: lightColor, intensity: config.intensity ?? 1, angle: config.angle ?? Math.PI / 6, penumbra: config.penumbra ?? 0.3, decay: config.decay ?? 1, distance: config.distance ?? 0 }), emitter === 'lens' && _jsx("mesh", { geometry: geometry.lens, material: mats.lens, renderOrder: 2 }), emitter === 'dot' && _jsx("mesh", { geometry: geometry.dot, material: mats.lens, renderOrder: 2 }), beamMode === 'layered' && (_jsx("instancedMesh", { ref: beamRef, args: [geometry.cone, mats.layered, LAYERS], renderOrder: 3, frustumCulled: false })), (beamMode === 'soft' || beamMode === 'volumetric') && (_jsx("mesh", { ref: soloRef, geometry: geometry.cone, material: beamMode === 'soft' ? mats.soft : mats.volumetric, renderOrder: 3, frustumCulled: false })), _jsx("mesh", { material: mats.halo, renderOrder: 4, frustumCulled: false, children: _jsx("planeGeometry", { args: [2, 2] }) })] }));
|
|
243
|
+
};
|
|
244
|
+
export default SpotBeam;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Texture } from 'three';
|
|
2
|
+
export type BeamPattern = 'round' | 'slot' | 'bars' | 'blinds' | 'window' | 'leaves';
|
|
3
|
+
export declare const BEAM_PATTERNS: BeamPattern[];
|
|
4
|
+
/**
|
|
5
|
+
* Build the mask. `rotation` is baked in rather than applied as a uv transform:
|
|
6
|
+
* three samples a spot light's map through the light frustum and ignores the
|
|
7
|
+
* texture matrix, so rotating it any other way would turn the haze without
|
|
8
|
+
* turning the pool.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createBeamPattern(pattern: BeamPattern, softness: number, rotationDeg: number): Texture;
|
|
11
|
+
//# sourceMappingURL=beamPatterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beamPatterns.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/beamPatterns.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErF,eAAO,MAAM,aAAa,EAAE,WAAW,EAA4D,CAAC;AA+DpG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAyBtG"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// beamPatterns — the shape the light throws
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// A spot light's beam is shaped by what sits in front of it, so this generates
|
|
6
|
+
// that mask once, as a texture: three projects it through the light's own
|
|
7
|
+
// frustum (SpotLight.map), and the beam shader samples the same texture, so the
|
|
8
|
+
// haze in the air and the pool on the floor always agree.
|
|
9
|
+
//
|
|
10
|
+
// Everything is masked by the cone's own circle, so the beam edge stays round
|
|
11
|
+
// whatever the pattern does inside it.
|
|
12
|
+
//
|
|
13
|
+
import { CanvasTexture, LinearFilter, SRGBColorSpace } from 'three';
|
|
14
|
+
export const BEAM_PATTERNS = ['round', 'slot', 'bars', 'blinds', 'window', 'leaves'];
|
|
15
|
+
const SIZE = 512;
|
|
16
|
+
/** Soft-edged mask of the cone itself; everything else multiplies into it. */
|
|
17
|
+
function coneMask(g, softness) {
|
|
18
|
+
const r = SIZE / 2;
|
|
19
|
+
const inner = Math.max(0.02, 1 - softness) * r;
|
|
20
|
+
const grad = g.createRadialGradient(r, r, inner, r, r, r);
|
|
21
|
+
grad.addColorStop(0, 'rgba(255,255,255,1)');
|
|
22
|
+
grad.addColorStop(1, 'rgba(255,255,255,0)');
|
|
23
|
+
g.globalCompositeOperation = 'destination-in';
|
|
24
|
+
g.fillStyle = grad;
|
|
25
|
+
g.fillRect(0, 0, SIZE, SIZE);
|
|
26
|
+
g.globalCompositeOperation = 'source-over';
|
|
27
|
+
}
|
|
28
|
+
function drawPattern(g, pattern, softness) {
|
|
29
|
+
const s = SIZE;
|
|
30
|
+
g.fillStyle = '#ffffff';
|
|
31
|
+
if (pattern === 'round') {
|
|
32
|
+
g.fillRect(0, 0, s, s);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (pattern === 'slot') {
|
|
36
|
+
const h = s * 0.3;
|
|
37
|
+
g.fillRect(0, (s - h) / 2, s, h);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (pattern === 'bars') {
|
|
41
|
+
const n = 7, w = s / n;
|
|
42
|
+
for (let i = 0; i < n; i++)
|
|
43
|
+
if (i % 2 === 0)
|
|
44
|
+
g.fillRect(i * w, 0, w * 0.62, s);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (pattern === 'blinds') {
|
|
48
|
+
const n = 9, h = s / n;
|
|
49
|
+
for (let i = 0; i < n; i++)
|
|
50
|
+
g.fillRect(0, i * h, s, h * 0.5);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (pattern === 'window') {
|
|
54
|
+
const bar = s * 0.045;
|
|
55
|
+
g.fillRect(0, 0, s, s);
|
|
56
|
+
g.fillStyle = '#000000';
|
|
57
|
+
g.fillRect(s / 2 - bar / 2, 0, bar, s);
|
|
58
|
+
g.fillRect(0, s / 2 - bar / 2, s, bar);
|
|
59
|
+
g.fillRect(0, 0, s, bar * 0.8);
|
|
60
|
+
g.fillRect(0, s - bar * 0.8, s, bar * 0.8);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// leaves: overlapping blobs, deterministic so the same config is the same dapple
|
|
64
|
+
let seed = 9;
|
|
65
|
+
const rnd = () => (seed = (seed * 1664525 + 1013904223) % 4294967296) / 4294967296;
|
|
66
|
+
g.fillRect(0, 0, s, s);
|
|
67
|
+
g.fillStyle = '#000000';
|
|
68
|
+
for (let i = 0; i < 34; i++) {
|
|
69
|
+
const x = rnd() * s, y = rnd() * s, r = s * (0.05 + rnd() * 0.11);
|
|
70
|
+
g.beginPath();
|
|
71
|
+
g.ellipse(x, y, r, r * (0.55 + rnd() * 0.7), rnd() * Math.PI, 0, Math.PI * 2);
|
|
72
|
+
g.fill();
|
|
73
|
+
}
|
|
74
|
+
void softness;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the mask. `rotation` is baked in rather than applied as a uv transform:
|
|
78
|
+
* three samples a spot light's map through the light frustum and ignores the
|
|
79
|
+
* texture matrix, so rotating it any other way would turn the haze without
|
|
80
|
+
* turning the pool.
|
|
81
|
+
*/
|
|
82
|
+
export function createBeamPattern(pattern, softness, rotationDeg) {
|
|
83
|
+
const canvas = document.createElement('canvas');
|
|
84
|
+
canvas.width = canvas.height = SIZE;
|
|
85
|
+
const g = canvas.getContext('2d');
|
|
86
|
+
g.clearRect(0, 0, SIZE, SIZE);
|
|
87
|
+
g.save();
|
|
88
|
+
g.translate(SIZE / 2, SIZE / 2);
|
|
89
|
+
g.rotate((rotationDeg * Math.PI) / 180);
|
|
90
|
+
g.translate(-SIZE / 2, -SIZE / 2);
|
|
91
|
+
drawPattern(g, pattern, softness);
|
|
92
|
+
g.restore();
|
|
93
|
+
// Soften the pattern edges as well as the cone rim.
|
|
94
|
+
if (softness > 0.02 && pattern !== 'round') {
|
|
95
|
+
g.filter = `blur(${Math.round(softness * 26)}px)`;
|
|
96
|
+
g.drawImage(canvas, 0, 0);
|
|
97
|
+
g.filter = 'none';
|
|
98
|
+
}
|
|
99
|
+
coneMask(g, Math.max(softness, 0.06));
|
|
100
|
+
const texture = new CanvasTexture(canvas);
|
|
101
|
+
texture.colorSpace = SRGBColorSpace;
|
|
102
|
+
texture.minFilter = LinearFilter;
|
|
103
|
+
texture.magFilter = LinearFilter;
|
|
104
|
+
texture.needsUpdate = true;
|
|
105
|
+
return texture;
|
|
106
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as SpotBeam } from './SpotBeam';
|
|
2
|
+
export type { SpotBeamProps } from './SpotBeam';
|
|
3
|
+
export { BEAM_PATTERNS, createBeamPattern } from './beamPatterns';
|
|
4
|
+
export type { BeamPattern } from './beamPatterns';
|
|
5
|
+
export { createSpotBeamMaterials, disposeSpotBeamMaterials } from './spotBeamMaterials';
|
|
6
|
+
export type { SpotBeamMaterials } from './spotBeamMaterials';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AACxF,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// A spot light with a visible, pattern-shaped beam (config.type 'spotBeam').
|
|
2
|
+
export { default as SpotBeam } from './SpotBeam.js';
|
|
3
|
+
export { BEAM_PATTERNS, createBeamPattern } from './beamPatterns.js';
|
|
4
|
+
export { createSpotBeamMaterials, disposeSpotBeamMaterials } from './spotBeamMaterials.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MeshBasicMaterial, ShaderMaterial } from 'three';
|
|
2
|
+
export type BeamMode = 'none' | 'soft' | 'layered' | 'volumetric';
|
|
3
|
+
export declare const BEAM_MODES: BeamMode[];
|
|
4
|
+
export interface SpotBeamMaterials {
|
|
5
|
+
lens: MeshBasicMaterial;
|
|
6
|
+
halo: ShaderMaterial;
|
|
7
|
+
/** One cone shell, lit by how obliquely you look through it. One draw. */
|
|
8
|
+
soft: ShaderMaterial;
|
|
9
|
+
/** Nested shells: cheap depth, approximates the pattern. */
|
|
10
|
+
layered: ShaderMaterial;
|
|
11
|
+
/** Raymarched cone interior: the only mode where the pattern exists in the air. */
|
|
12
|
+
volumetric: ShaderMaterial;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSpotBeamMaterials(): SpotBeamMaterials;
|
|
15
|
+
export declare function disposeSpotBeamMaterials(m: SpotBeamMaterials): void;
|
|
16
|
+
//# sourceMappingURL=spotBeamMaterials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spotBeamMaterials.d.ts","sourceRoot":"","sources":["../../../src/components/spotBeam/spotBeamMaterials.ts"],"names":[],"mappings":"AAcA,OAAO,EAAiD,iBAAiB,EAAE,cAAc,EAAW,MAAM,OAAO,CAAC;AAGlH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAElE,eAAO,MAAM,UAAU,EAAE,QAAQ,EAA8C,CAAC;AAEhF,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,0EAA0E;IAC1E,IAAI,EAAE,cAAc,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,cAAc,CAAC;IACxB,mFAAmF;IACnF,UAAU,EAAE,cAAc,CAAC;CAC9B;AA+RD,wBAAgB,uBAAuB,IAAI,iBAAiB,CAQ3D;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAEnE"}
|