babylon.quarks-editor 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/core/material.d.ts +3 -0
- package/dist/core/material.js +12 -0
- package/dist/core/shapes.d.ts +8 -2
- package/dist/core/shapes.js +25 -1
- package/dist/react/EffectEditor.js +2 -2
- package/dist/react/EffectEditorHost.js +14 -2
- package/dist/react/SubEmittersModule.js +3 -1
- package/dist/react/ValueField.d.ts +2 -5
- package/dist/react/ValueField.js +2 -1
- package/dist/react/behaviorModules.d.ts +11 -0
- package/dist/react/behaviorModules.js +81 -3
- package/dist/react/modules.js +74 -23
- package/package.json +4 -3
- package/src/core/material.ts +15 -0
- package/src/core/shapes.ts +36 -0
- package/src/react/EffectEditor.tsx +4 -0
- package/src/react/EffectEditorHost.tsx +17 -2
- package/src/react/SubEmittersModule.tsx +15 -0
- package/src/react/ValueField.tsx +6 -1
- package/src/react/behaviorModules.tsx +315 -0
- package/src/react/modules.tsx +282 -28
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to the `babylon.quarks-editor` package are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and the project follows the `quarks.core` 0.x version line.
|
|
6
6
|
|
|
7
|
+
## [0.24.0] — 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Main module: "Playback speed", "Fixed seed" (the system's random seed, for an effect that plays
|
|
12
|
+
out identically every time) and "Off screen" (the system's culling mode: always simulate, pause,
|
|
13
|
+
pause and catch up). Renderer module: "Rendering group", "Apply fog", "Sort mode" and "Limit
|
|
14
|
+
screen size" (min / max particle size as fractions of the viewport). Shape module: "Align to
|
|
15
|
+
direction". Noise module: "Scroll speed" and "Remap" (with its curve). Velocity module: "Orbital
|
|
16
|
+
offset" and "Radial". Sub Emitters module: "Inherit color / size / rotation / lifetime".
|
|
17
|
+
- The editor now reaches everything the runtime can do. Shape module: Box (with emit from
|
|
18
|
+
volume / shell / edges) and Edge shapes. Main module: "Blend between two" start colour
|
|
19
|
+
(`ColorRange`) and "3D (axis + angle)" start rotation (`AxisAngleGenerator`). New modules:
|
|
20
|
+
"Rotation over Lifetime (3D)" for mesh particles (axis + angle, per axis, random) and
|
|
21
|
+
"Sequences" (`ApplySequences`: sequences read from an image's opaque pixels, with time window,
|
|
22
|
+
scale and position). Renderer module: "Layer mask", "Max particles", billboard "Pivot", the
|
|
23
|
+
trail flags (record in world space, UV from head, color from particle, die with particle, head
|
|
24
|
+
width scale) and, for meshes, an environment-map atlas picked from the host's textures with its
|
|
25
|
+
reflection level.
|
|
26
|
+
- "Random between two" fields label their min and max inputs. Export and Save warn when the
|
|
27
|
+
effect references `blob:` textures, which do not survive a reload.
|
|
28
|
+
|
|
7
29
|
## [0.23.0] — 2026-10-18
|
|
8
30
|
|
|
9
31
|
### Added
|
package/dist/core/material.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export interface RendererMaterialPatch {
|
|
|
6
6
|
depthWrite?: boolean;
|
|
7
7
|
alphaTest?: number;
|
|
8
8
|
texture?: unknown;
|
|
9
|
+
/** A 3×2 cube-face atlas mesh particles reflect, or null for none. */
|
|
10
|
+
reflectionAtlas?: unknown;
|
|
11
|
+
reflectionLevel?: number;
|
|
9
12
|
}
|
|
10
13
|
/** Applies renderer material settings and keeps imported QuarksMaterial JSON in sync for export. */
|
|
11
14
|
export declare function applyRendererMaterial(system: ParticleSystem, patch: RendererMaterialPatch): void;
|
package/dist/core/material.js
CHANGED
|
@@ -22,6 +22,12 @@ export function applyRendererMaterial(system, patch) {
|
|
|
22
22
|
if (patch.texture !== undefined) {
|
|
23
23
|
system.texture = patch.texture;
|
|
24
24
|
}
|
|
25
|
+
if (patch.reflectionAtlas !== undefined) {
|
|
26
|
+
settings.reflectionAtlas = patch.reflectionAtlas;
|
|
27
|
+
}
|
|
28
|
+
if (patch.reflectionLevel !== undefined) {
|
|
29
|
+
settings.reflectionLevel = patch.reflectionLevel;
|
|
30
|
+
}
|
|
25
31
|
const material = system.material;
|
|
26
32
|
if (isEditableMaterialRecord(material)) {
|
|
27
33
|
if (patch.blendMode !== undefined) {
|
|
@@ -44,6 +50,12 @@ export function applyRendererMaterial(system, patch) {
|
|
|
44
50
|
material.texture = patch.texture;
|
|
45
51
|
material.map = patch.texture;
|
|
46
52
|
}
|
|
53
|
+
if (patch.reflectionAtlas !== undefined) {
|
|
54
|
+
material.reflectionAtlas = patch.reflectionAtlas;
|
|
55
|
+
}
|
|
56
|
+
if (patch.reflectionLevel !== undefined) {
|
|
57
|
+
material.reflectionLevel = patch.reflectionLevel;
|
|
58
|
+
}
|
|
47
59
|
}
|
|
48
60
|
system.neededToUpdateRender = true;
|
|
49
61
|
}
|
package/dist/core/shapes.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
2
2
|
import type { EmitterShape } from 'babylon.quarks';
|
|
3
|
-
import { EmitterMode } from 'babylon.quarks';
|
|
4
|
-
export type ShapeType = 'point' | 'sphere' | 'hemisphere' | 'cone' | 'circle' | 'donut' | 'rectangle' | 'grid' | 'mesh_surface';
|
|
3
|
+
import { BoxEmitFrom, EmitterMode } from 'babylon.quarks';
|
|
4
|
+
export type ShapeType = 'point' | 'sphere' | 'hemisphere' | 'cone' | 'circle' | 'donut' | 'rectangle' | 'grid' | 'box' | 'edge' | 'mesh_surface';
|
|
5
5
|
export declare const SHAPE_TYPES: Array<{
|
|
6
6
|
value: ShapeType;
|
|
7
7
|
label: string;
|
|
@@ -18,6 +18,12 @@ export interface ShapeParams {
|
|
|
18
18
|
spread: number;
|
|
19
19
|
column: number;
|
|
20
20
|
row: number;
|
|
21
|
+
/** Box depth (Z). */
|
|
22
|
+
depth: number;
|
|
23
|
+
/** Where a box emits from: its volume, its faces or its edges. */
|
|
24
|
+
emitFrom: BoxEmitFrom;
|
|
25
|
+
/** How fast an edge emitter's loop / ping-pong point travels, in lengths per second. */
|
|
26
|
+
speed: number;
|
|
21
27
|
mesh?: Mesh;
|
|
22
28
|
}
|
|
23
29
|
export declare const DEFAULT_SHAPE_PARAMS: ShapeParams;
|
package/dist/core/shapes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CircleEmitter, ConeEmitter, DonutEmitter, EmitterMode, GridEmitter, HemisphereEmitter, MeshSurfaceEmitter, PointEmitter, RectangleEmitter, SphereEmitter, } from 'babylon.quarks';
|
|
1
|
+
import { BoxEmitFrom, BoxEmitter, CircleEmitter, ConeEmitter, ConstantValue, DonutEmitter, EdgeEmitter, EmitterMode, GridEmitter, HemisphereEmitter, MeshSurfaceEmitter, PointEmitter, RectangleEmitter, SphereEmitter, } from 'babylon.quarks';
|
|
2
2
|
export const SHAPE_TYPES = [
|
|
3
3
|
{ value: 'cone', label: 'Cone' },
|
|
4
4
|
{ value: 'sphere', label: 'Sphere' },
|
|
@@ -7,6 +7,8 @@ export const SHAPE_TYPES = [
|
|
|
7
7
|
{ value: 'donut', label: 'Donut' },
|
|
8
8
|
{ value: 'rectangle', label: 'Rectangle' },
|
|
9
9
|
{ value: 'grid', label: 'Grid' },
|
|
10
|
+
{ value: 'box', label: 'Box' },
|
|
11
|
+
{ value: 'edge', label: 'Edge' },
|
|
10
12
|
{ value: 'mesh_surface', label: 'Mesh surface' },
|
|
11
13
|
{ value: 'point', label: 'Point' },
|
|
12
14
|
];
|
|
@@ -22,6 +24,9 @@ export const DEFAULT_SHAPE_PARAMS = {
|
|
|
22
24
|
spread: 0,
|
|
23
25
|
column: 10,
|
|
24
26
|
row: 10,
|
|
27
|
+
depth: 1,
|
|
28
|
+
emitFrom: BoxEmitFrom.Volume,
|
|
29
|
+
speed: 1,
|
|
25
30
|
mesh: undefined,
|
|
26
31
|
};
|
|
27
32
|
export function getShapeType(shape) {
|
|
@@ -43,6 +48,9 @@ export function readShapeParams(shape) {
|
|
|
43
48
|
height: num('height', DEFAULT_SHAPE_PARAMS.height),
|
|
44
49
|
column: num('column', DEFAULT_SHAPE_PARAMS.column),
|
|
45
50
|
row: num('row', DEFAULT_SHAPE_PARAMS.row),
|
|
51
|
+
depth: num('depth', DEFAULT_SHAPE_PARAMS.depth),
|
|
52
|
+
emitFrom: num('emitFrom', DEFAULT_SHAPE_PARAMS.emitFrom),
|
|
53
|
+
speed: anyShape.speed instanceof ConstantValue ? anyShape.speed.value : DEFAULT_SHAPE_PARAMS.speed,
|
|
46
54
|
mesh: anyShape.mesh,
|
|
47
55
|
};
|
|
48
56
|
}
|
|
@@ -57,6 +65,8 @@ export const SHAPE_PARAM_KEYS = {
|
|
|
57
65
|
donut: ['radius', 'arc', 'thickness', 'donutRadius'],
|
|
58
66
|
rectangle: ['width', 'height'],
|
|
59
67
|
grid: ['width', 'height', 'column', 'row'],
|
|
68
|
+
box: ['width', 'height', 'depth'],
|
|
69
|
+
edge: ['radius', 'speed'],
|
|
60
70
|
mesh_surface: [],
|
|
61
71
|
};
|
|
62
72
|
export function createShape(type, params) {
|
|
@@ -119,6 +129,20 @@ export function createShape(type, params) {
|
|
|
119
129
|
column: params.column,
|
|
120
130
|
row: params.row,
|
|
121
131
|
});
|
|
132
|
+
case 'box':
|
|
133
|
+
return new BoxEmitter({
|
|
134
|
+
width: params.width,
|
|
135
|
+
height: params.height,
|
|
136
|
+
depth: params.depth,
|
|
137
|
+
emitFrom: params.emitFrom,
|
|
138
|
+
});
|
|
139
|
+
case 'edge':
|
|
140
|
+
return new EdgeEmitter({
|
|
141
|
+
radius: params.radius,
|
|
142
|
+
mode: params.mode,
|
|
143
|
+
spread: params.spread,
|
|
144
|
+
speed: new ConstantValue(params.speed),
|
|
145
|
+
});
|
|
122
146
|
case 'mesh_surface':
|
|
123
147
|
return new MeshSurfaceEmitter(params.mesh);
|
|
124
148
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useSyncExternalStore } from 'react';
|
|
3
3
|
import { EffectBinding } from '../core/binding';
|
|
4
|
-
import { CollisionModule, ColorBySpeedModule, EmitDirectionModule, ForceOverLifeModule, GravityModule, InheritVelocityModule, LimitSpeedOverLifeModule, NoiseModule, OrbitOverLifeModule, RotationBySpeedModule, RotationOverLifeModule, SizeBySpeedModule, SpeedOverLifeModule, TextureSheetModule, TurbulenceModule, VelocityOverLifeModule, WidthOverTrailModule, } from './behaviorModules';
|
|
4
|
+
import { CollisionModule, ColorBySpeedModule, EmitDirectionModule, ForceOverLifeModule, GravityModule, InheritVelocityModule, LimitSpeedOverLifeModule, NoiseModule, OrbitOverLifeModule, Rotation3DOverLifeModule, RotationBySpeedModule, RotationOverLifeModule, SequencesModule, SizeBySpeedModule, SpeedOverLifeModule, TextureSheetModule, TurbulenceModule, VelocityOverLifeModule, WidthOverTrailModule, } from './behaviorModules';
|
|
5
5
|
import { ColorOverLifeModule, EmissionModule, MainModule, RendererModule, ShapeModule, SizeOverLifeModule, } from './modules';
|
|
6
6
|
import { ObjectModule } from './ObjectModule';
|
|
7
7
|
import { SubEmittersModule } from './SubEmittersModule';
|
|
@@ -31,5 +31,5 @@ export const EffectEditor = React.memo(function EffectEditor(props) {
|
|
|
31
31
|
useEffect(() => (binding === props.binding ? undefined : binding.subscribe(() => props.binding.markChanged())), [binding, props.binding]);
|
|
32
32
|
useSyncExternalStore((onStoreChange) => binding.subscribe(onStoreChange), () => binding.getRevision());
|
|
33
33
|
const gizmoTargetIsEmitter = allSystems.some((s) => s.emitter === props.gizmoTargetNode);
|
|
34
|
-
return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel, onBeforeChange: () => props.binding.onBeforeChange?.(), onChanged: () => props.binding.markChanged() }), !gizmoTargetIsEmitter && (_jsxs("div", { style: { fontSize: 11, color: theme.textDim, margin: '0 0 10px', lineHeight: 1.45 }, children: ["Modules below edit ", _jsx("strong", { children: activeSystem.emitter.name || 'particle system' }), ". Select its emitter in the timeline to move it with the gizmo."] })), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
|
|
34
|
+
return (_jsxs("div", { style: { fontFamily: theme.font, color: theme.text }, children: [_jsx(ObjectModule, { node: props.gizmoTargetNode, label: props.gizmoTargetLabel, onBeforeChange: () => props.binding.onBeforeChange?.(), onChanged: () => props.binding.markChanged() }), !gizmoTargetIsEmitter && (_jsxs("div", { style: { fontSize: 11, color: theme.textDim, margin: '0 0 10px', lineHeight: 1.45 }, children: ["Modules below edit ", _jsx("strong", { children: activeSystem.emitter.name || 'particle system' }), ". Select its emitter in the timeline to move it with the gizmo."] })), _jsx(MainModule, { binding: binding }), _jsx(EmissionModule, { binding: binding }), _jsx(ShapeModule, { binding: binding }), _jsx(EmitDirectionModule, { binding: binding }), _jsx(VelocityOverLifeModule, { binding: binding }), _jsx(SpeedOverLifeModule, { binding: binding }), _jsx(LimitSpeedOverLifeModule, { binding: binding }), _jsx(InheritVelocityModule, { binding: binding }), _jsx(ForceOverLifeModule, { binding: binding }), _jsx(GravityModule, { binding: binding }), _jsx(OrbitOverLifeModule, { binding: binding }), _jsx(ColorOverLifeModule, { binding: binding }), _jsx(ColorBySpeedModule, { binding: binding }), _jsx(SizeOverLifeModule, { binding: binding }), _jsx(SizeBySpeedModule, { binding: binding }), _jsx(RotationOverLifeModule, { binding: binding }), _jsx(Rotation3DOverLifeModule, { binding: binding }), _jsx(RotationBySpeedModule, { binding: binding }), _jsx(NoiseModule, { binding: binding }), _jsx(TurbulenceModule, { binding: binding }), _jsx(CollisionModule, { binding: binding }), _jsx(WidthOverTrailModule, { binding: binding }), _jsx(SubEmittersModule, { binding: binding, rootBinding: props.binding }), _jsx(TextureSheetModule, { binding: binding }), _jsx(SequencesModule, { binding: binding, textureOptions: props.textureOptions }), _jsx(RendererModule, { binding: binding, textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture, resolveGeometry: props.resolveGeometry })] }));
|
|
35
35
|
});
|
|
@@ -48,6 +48,18 @@ import { TimelinePanel } from './TimelinePanel';
|
|
|
48
48
|
import { TransportBar } from './TransportBar';
|
|
49
49
|
import { iconStyle } from './icons';
|
|
50
50
|
import { buttonStyle, globalEditorStyles, theme } from './theme';
|
|
51
|
+
/**
|
|
52
|
+
* The effect's JSON, with a warning when it points at object URLs: a texture
|
|
53
|
+
* dropped in from disk lives at a `blob:` URL that dies with the page, so the
|
|
54
|
+
* exported effect would load without it.
|
|
55
|
+
*/
|
|
56
|
+
function exportEffectJSON(binding, warn) {
|
|
57
|
+
const json = binding.exportJSON('EditorEffect');
|
|
58
|
+
if (json.includes('"blob:')) {
|
|
59
|
+
warn('This effect references blob: textures, which will not load after a reload. Re-pick them from a hosted URL.');
|
|
60
|
+
}
|
|
61
|
+
return json;
|
|
62
|
+
}
|
|
51
63
|
const SCENE_SAVE_INFO_KEY = 'babylon.quarks-editor:scene-save-info-seen';
|
|
52
64
|
function toStoredAsset(asset) {
|
|
53
65
|
return { path: asset.path, name: asset.name, json: asset.json, missingFiles: asset.missingFiles };
|
|
@@ -1619,7 +1631,7 @@ export function EffectEditorHost(props) {
|
|
|
1619
1631
|
}, onCancel: () => setRenamingRoot(false) })), _jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }, children: [_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => {
|
|
1620
1632
|
if (!binding)
|
|
1621
1633
|
return;
|
|
1622
|
-
const blob = new Blob([binding
|
|
1634
|
+
const blob = new Blob([exportEffectJSON(binding, showToast)], {
|
|
1623
1635
|
type: 'application/json',
|
|
1624
1636
|
});
|
|
1625
1637
|
const link = document.createElement('a');
|
|
@@ -1627,7 +1639,7 @@ export function EffectEditorHost(props) {
|
|
|
1627
1639
|
link.download = `${binding.root.name || 'effect'}.json`;
|
|
1628
1640
|
link.click();
|
|
1629
1641
|
URL.revokeObjectURL(link.href);
|
|
1630
|
-
}, children: "Export" }), props.onSave && (_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => binding && props.onSave(binding
|
|
1642
|
+
}, children: "Export" }), props.onSave && (_jsx("button", { className: "qe-hover", style: buttonStyle, disabled: !binding, onClick: () => binding && props.onSave(exportEffectJSON(binding, showToast)), children: "Save" }))] })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '6px 14px 20px' }, children: inspectorBinding && inspectorSystem && inspectorNode ? (_jsx(EffectEditor, { binding: inspectorBinding, selectedSystem: inspectorSystem, gizmoTargetNode: inspectorNode, gizmoTargetLabel: inspectorNode.name || 'Node', textureOptions: props.textureOptions, geometryOptions: props.geometryOptions, resolveTexture: props.resolveTexture ? resolveTextureForEditor : undefined, resolveGeometry: props.resolveGeometry ? resolveGeometryForEditor : undefined })) : (_jsx("div", { style: { fontSize: 13, color: theme.textDim, lineHeight: 1.5 }, children: "No effect selected." })) })] })] }), _jsxs("div", { "aria-hidden": !drawerOpen && !drawerPinned, style: {
|
|
1631
1643
|
height: DRAWER_HEIGHT,
|
|
1632
1644
|
display: 'flex',
|
|
1633
1645
|
flexDirection: 'column',
|
|
@@ -35,5 +35,7 @@ export function SubEmittersModule(props) {
|
|
|
35
35
|
{ value: SubParticleEmitMode.Death, label: 'On death' },
|
|
36
36
|
{ value: SubParticleEmitMode.Birth, label: 'On birth' },
|
|
37
37
|
{ value: SubParticleEmitMode.Frame, label: 'Every frame' },
|
|
38
|
-
], onChange: (mode) => binding.apply(() => (behavior.mode = mode)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: behavior.emitProbability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.emitProbability = Math.min(1, v))) }) }), _jsx(Row, { label: "Use velocity", children: _jsx(CheckboxField, { value: behavior.useVelocityAsBasis, onChange: (v) => binding.apply(() => (behavior.useVelocityAsBasis = v)) }) }),
|
|
38
|
+
], onChange: (mode) => binding.apply(() => (behavior.mode = mode)) }) }), _jsx(Row, { label: "Probability", children: _jsx(NumberField, { value: behavior.emitProbability, min: 0, step: 0.05, onChange: (v) => binding.apply(() => (behavior.emitProbability = Math.min(1, v))) }) }), _jsx(Row, { label: "Use velocity", children: _jsx(CheckboxField, { value: behavior.useVelocityAsBasis, onChange: (v) => binding.apply(() => (behavior.useVelocityAsBasis = v)) }) }), ['color', 'size', 'rotation', 'lifetime'].map((property) => (_jsx(Row, { label: `Inherit ${property}`, children: _jsx(CheckboxField, { value: behavior.inherit.includes(property), onChange: (v) => binding.apply(() => (behavior.inherit = v
|
|
39
|
+
? [...behavior.inherit.filter((p) => p !== property), property]
|
|
40
|
+
: behavior.inherit.filter((p) => p !== property))) }) }, property))), _jsxs("p", { style: { margin: '8px 0 2px', color: theme.textDim, fontSize: 11.5 }, children: ["Spawns \u201C", target?.emitter.name ?? 'sub-emitter', "\u201D \u2014 select it in the hierarchy above to edit it."] })] })) }));
|
|
39
41
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { ScalarGenerator } from '../core/values';
|
|
2
|
-
/**
|
|
3
|
-
* Unity-like value field: Constant | Random Between Two Constants | Curve,
|
|
4
|
-
* bound to a quarks value generator.
|
|
5
|
-
*/
|
|
6
3
|
export declare function ValueField(props: {
|
|
7
4
|
label: string;
|
|
8
5
|
hint?: string;
|
|
@@ -11,4 +8,4 @@ export declare function ValueField(props: {
|
|
|
11
8
|
onChange: (generator: ScalarGenerator) => void;
|
|
12
9
|
min?: number;
|
|
13
10
|
curveMax?: number;
|
|
14
|
-
}):
|
|
11
|
+
}): React.JSX.Element;
|
package/dist/react/ValueField.js
CHANGED
|
@@ -6,6 +6,7 @@ import { NumberField, Row, SelectField } from './fields';
|
|
|
6
6
|
* Unity-like value field: Constant | Random Between Two Constants | Curve,
|
|
7
7
|
* bound to a quarks value generator.
|
|
8
8
|
*/
|
|
9
|
+
const captionStyle = { fontSize: 10.5, opacity: 0.6, flex: 'none' };
|
|
9
10
|
export function ValueField(props) {
|
|
10
11
|
const state = readScalar(props.generator);
|
|
11
12
|
const commit = (patch) => {
|
|
@@ -15,5 +16,5 @@ export function ValueField(props) {
|
|
|
15
16
|
{ value: 'constant', label: 'Constant' },
|
|
16
17
|
{ value: 'random', label: 'Random between two' },
|
|
17
18
|
{ value: 'curve', label: 'Curve' },
|
|
18
|
-
], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => props.onChange(buildCurve(pieces)) }) }))] }));
|
|
19
|
+
], onChange: (mode) => commit({ mode }) }) }), state.mode === 'constant' && (_jsx(Row, { label: "", children: _jsx(NumberField, { value: state.value, min: props.min, onChange: (value) => commit({ value }) }) })), state.mode === 'random' && (_jsx(Row, { label: "", children: _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [_jsx("span", { style: captionStyle, children: "min" }), _jsx(NumberField, { value: state.min, min: props.min, onChange: (min) => commit({ min }) }), _jsx("span", { style: captionStyle, children: "max" }), _jsx(NumberField, { value: state.max, min: props.min, onChange: (max) => commit({ max }) })] }) })), state.mode === 'curve' && (_jsx("div", { style: { marginTop: 6 }, children: _jsx(CurveEditor, { pieces: readPieces(props.generator), maxValue: props.curveMax, onChange: (pieces) => props.onChange(buildCurve(pieces)) }) }))] }));
|
|
19
20
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { EffectBinding } from '../core/binding';
|
|
3
|
+
import type { TextureOption } from './modules';
|
|
3
4
|
interface ModuleProps {
|
|
4
5
|
binding: EffectBinding;
|
|
5
6
|
}
|
|
@@ -10,6 +11,16 @@ export declare function VelocityOverLifeModule({ binding }: ModuleProps): React.
|
|
|
10
11
|
export declare function InheritVelocityModule({ binding }: ModuleProps): React.JSX.Element;
|
|
11
12
|
export declare function GravityModule({ binding }: ModuleProps): React.JSX.Element;
|
|
12
13
|
export declare function RotationOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
14
|
+
/** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
|
|
15
|
+
export declare function Rotation3DOverLifeModule({ binding }: ModuleProps): React.JSX.Element;
|
|
16
|
+
/**
|
|
17
|
+
* Moves particles, one after another, onto the opaque pixels of an image (a
|
|
18
|
+
* logo or a word drawn from sparks). Each sequence is read from an image once;
|
|
19
|
+
* its points, scale, position and time window are what gets saved.
|
|
20
|
+
*/
|
|
21
|
+
export declare function SequencesModule({ binding, textureOptions }: ModuleProps & {
|
|
22
|
+
textureOptions?: TextureOption[];
|
|
23
|
+
}): React.JSX.Element;
|
|
13
24
|
export declare function NoiseModule({ binding }: ModuleProps): React.JSX.Element;
|
|
14
25
|
export declare function TurbulenceModule({ binding }: ModuleProps): React.JSX.Element;
|
|
15
26
|
export declare function EmitDirectionModule({ binding }: ModuleProps): React.JSX.Element;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
|
|
3
|
+
import { ApplyCollision, ApplyForce, ApplySequences, AxisAngleGenerator, Bezier, ChangeEmitDirection, ColorBySpeed, ConstantValue, EulerGenerator, ForceOverLife, FrameOverLife, getPhysicsResolver, Gradient, InheritVelocity, IntervalValue, LimitSpeedOverLife, Noise, OrbitOverLife, PiecewiseBezier, RandomQuatGenerator, Rotation3DOverLife, RotationBySpeed, RotationOverLife, SizeBySpeed, SpeedOverLife, TextureSequencer, TurbulenceField, Vector3, VelocityOverLife, WidthOverLength, } from 'babylon.quarks';
|
|
4
|
+
import React from 'react';
|
|
3
5
|
import { buildGradient, DEFAULT_GRADIENT_STOPS, findBehavior, readGradientStops } from '../core/colors';
|
|
4
6
|
import { buildCurve, buildScalar, readPieces, readScalar } from '../core/values';
|
|
5
7
|
import { CurveEditor } from './CurveEditor';
|
|
6
8
|
import { CheckboxField, NumberField, Row, SelectField } from './fields';
|
|
7
9
|
import { GradientEditor } from './GradientEditor';
|
|
10
|
+
import { iconStyle } from './icons';
|
|
8
11
|
import { ModuleSection } from './ModuleSection';
|
|
12
|
+
import { theme } from './theme';
|
|
9
13
|
import { ValueField } from './ValueField';
|
|
10
14
|
function removeBehaviorOfType(behaviors, type) {
|
|
11
15
|
for (let i = behaviors.length - 1; i >= 0; i--) {
|
|
@@ -37,7 +41,7 @@ export function VelocityOverLifeModule({ binding }) {
|
|
|
37
41
|
return (_jsx(BehaviorModule, { binding: binding, title: "Velocity over Lifetime", type: "VelocityOverLife", create: () => new VelocityOverLife(new ConstantValue(0), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Space", children: _jsx(SelectField, { value: behavior.space, options: [
|
|
38
42
|
{ value: 'local', label: 'Local' },
|
|
39
43
|
{ value: 'world', label: 'World' },
|
|
40
|
-
], onChange: (v) => binding.apply(() => (behavior.space = v)) }) }), _jsx(ValueField, { label: "Linear X", generator: behavior.linearX, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearX = g)) }), _jsx(ValueField, { label: "Linear Y", generator: behavior.linearY, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearY = g)) }), _jsx(ValueField, { label: "Linear Z", generator: behavior.linearZ, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearZ = g)) }), _jsx(ValueField, { label: "Orbital X", generator: behavior.orbitalX, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalX = g)) }), _jsx(ValueField, { label: "Orbital Y", generator: behavior.orbitalY, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalY = g)) }), _jsx(ValueField, { label: "Orbital Z", generator: behavior.orbitalZ, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalZ = g)) })] })) }));
|
|
44
|
+
], onChange: (v) => binding.apply(() => (behavior.space = v)) }) }), _jsx(ValueField, { label: "Linear X", generator: behavior.linearX, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearX = g)) }), _jsx(ValueField, { label: "Linear Y", generator: behavior.linearY, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearY = g)) }), _jsx(ValueField, { label: "Linear Z", generator: behavior.linearZ, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.linearZ = g)) }), _jsx(ValueField, { label: "Orbital X", generator: behavior.orbitalX, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalX = g)) }), _jsx(ValueField, { label: "Orbital Y", generator: behavior.orbitalY, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalY = g)) }), _jsx(ValueField, { label: "Orbital Z", generator: behavior.orbitalZ, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.orbitalZ = g)) }), _jsx(Vector3Row, { label: "Orbital offset", value: behavior.orbitalOffset, onChange: (v) => binding.apply(() => (behavior.orbitalOffset = v)) }), _jsx(ValueField, { label: "Radial", generator: behavior.radial, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.radial = g)) })] })) }));
|
|
41
45
|
}
|
|
42
46
|
export function InheritVelocityModule({ binding }) {
|
|
43
47
|
return (_jsx(BehaviorModule, { binding: binding, title: "Inherit Velocity", type: "InheritVelocity", create: () => new InheritVelocity(new ConstantValue(1), 'initial'), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Mode", hintKey: "inheritVelocity.mode", children: _jsx(SelectField, { value: behavior.mode, options: [
|
|
@@ -51,8 +55,82 @@ export function GravityModule({ binding }) {
|
|
|
51
55
|
export function RotationOverLifeModule({ binding }) {
|
|
52
56
|
return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime", type: "RotationOverLife", create: () => new RotationOverLife(new ConstantValue(Math.PI)), children: (behavior) => (_jsx(ValueField, { hintKey: "rotationOverLife.velocity", label: "Velocity (rad/s)", generator: behavior.angularVelocity, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = g)) })) }));
|
|
53
57
|
}
|
|
58
|
+
/** Turns mesh particles by a 3D angular velocity; billboards turn through Rotation over Lifetime. */
|
|
59
|
+
export function Rotation3DOverLifeModule({ binding }) {
|
|
60
|
+
return (_jsx(BehaviorModule, { binding: binding, title: "Rotation over Lifetime (3D)", type: "Rotation3DOverLife", create: () => new Rotation3DOverLife(new AxisAngleGenerator(new Vector3(0, 1, 0), new ConstantValue(Math.PI))), children: (behavior) => {
|
|
61
|
+
const generator = behavior.angularVelocity;
|
|
62
|
+
const mode = generator instanceof AxisAngleGenerator
|
|
63
|
+
? 'axisAngle'
|
|
64
|
+
: generator instanceof EulerGenerator
|
|
65
|
+
? 'euler'
|
|
66
|
+
: 'random';
|
|
67
|
+
const axisAngle = generator instanceof AxisAngleGenerator ? generator : undefined;
|
|
68
|
+
const euler = generator instanceof EulerGenerator ? generator : undefined;
|
|
69
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { style: { margin: '2px 0 6px', color: theme.textDim, fontSize: 11.5 }, children: "Mesh particles only; billboards spin with Rotation over Lifetime." }), _jsx(Row, { label: "Mode", children: _jsx(SelectField, { value: mode, options: [
|
|
70
|
+
{ value: 'axisAngle', label: 'Axis + angle' },
|
|
71
|
+
{ value: 'euler', label: 'Per axis' },
|
|
72
|
+
{ value: 'random', label: 'Random' },
|
|
73
|
+
], onChange: (next) => binding.apply(() => {
|
|
74
|
+
behavior.angularVelocity =
|
|
75
|
+
next === 'axisAngle'
|
|
76
|
+
? new AxisAngleGenerator(new Vector3(0, 1, 0), new ConstantValue(Math.PI))
|
|
77
|
+
: next === 'euler'
|
|
78
|
+
? new EulerGenerator(new ConstantValue(0), new ConstantValue(Math.PI), new ConstantValue(0))
|
|
79
|
+
: new RandomQuatGenerator();
|
|
80
|
+
}) }) }), axisAngle && (_jsxs(_Fragment, { children: [_jsx(Vector3Row, { label: "Axis", value: axisAngle.axis, step: 0.1, onChange: (v) => binding.apply(() => (behavior.angularVelocity = new AxisAngleGenerator(v, axisAngle.angle))) }), _jsx(ValueField, { label: "Angle (rad/s)", generator: axisAngle.angle, curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = new AxisAngleGenerator(axisAngle.axis.clone(), g))) })] })), euler &&
|
|
81
|
+
['angleX', 'angleY', 'angleZ'].map((key) => (_jsx(ValueField, { label: `${key.slice(-1)} (rad/s)`, generator: euler[key], curveMax: Math.PI * 4, onChange: (g) => binding.apply(() => (behavior.angularVelocity = new EulerGenerator(key === 'angleX' ? g : euler.angleX, key === 'angleY' ? g : euler.angleY, key === 'angleZ' ? g : euler.angleZ))) }, key)))] }));
|
|
82
|
+
} }));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Moves particles, one after another, onto the opaque pixels of an image (a
|
|
86
|
+
* logo or a word drawn from sparks). Each sequence is read from an image once;
|
|
87
|
+
* its points, scale, position and time window are what gets saved.
|
|
88
|
+
*/
|
|
89
|
+
export function SequencesModule({ binding, textureOptions }) {
|
|
90
|
+
const [imageUrl, setImageUrl] = React.useState(textureOptions?.[0]?.url ?? '');
|
|
91
|
+
const [threshold, setThreshold] = React.useState(0.5);
|
|
92
|
+
const [status, setStatus] = React.useState(null);
|
|
93
|
+
const addFromImage = async (behavior) => {
|
|
94
|
+
if (!imageUrl) {
|
|
95
|
+
setStatus('Pick an image first.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const image = new Image();
|
|
100
|
+
image.crossOrigin = 'anonymous';
|
|
101
|
+
image.src = imageUrl;
|
|
102
|
+
await image.decode();
|
|
103
|
+
const sequencer = new TextureSequencer(0.05, 0.05, new Vector3());
|
|
104
|
+
sequencer.fromImage(image, threshold);
|
|
105
|
+
const last = behavior.sequencers[behavior.sequencers.length - 1];
|
|
106
|
+
const start = last ? last[0].b : 0;
|
|
107
|
+
binding.apply(() => behavior.appendSequencer(new IntervalValue(start, start + 2), sequencer));
|
|
108
|
+
setStatus(`${sequencer.locations.length} points read.`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
setStatus(`Could not read the image: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const pair = (a, b, onA, onB, step) => (_jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: a, step: step, onChange: onA }), _jsx(NumberField, { value: b, step: step, onChange: onB })] }));
|
|
115
|
+
return (_jsx(BehaviorModule, { binding: binding, title: "Sequences", type: "ApplySequences", create: () => new ApplySequences(0.001), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx("p", { style: { margin: '2px 0 6px', color: theme.textDim, fontSize: 11.5 }, children: "Particles fly, one after another, onto the opaque pixels of an image during each sequence's time window." }), _jsx(Row, { label: "Delay per particle", children: _jsx(NumberField, { value: behavior.delay, min: 0, step: 0.001, onChange: (v) => binding.apply(() => (behavior.delay = Math.max(0, v))) }) }), behavior.sequencers.map(([range, sequencer], index) => {
|
|
116
|
+
const texture = sequencer;
|
|
117
|
+
return (_jsxs("div", { style: {
|
|
118
|
+
border: `1px solid ${theme.border}`,
|
|
119
|
+
borderRadius: 6,
|
|
120
|
+
padding: '4px 6px',
|
|
121
|
+
marginTop: 6,
|
|
122
|
+
}, children: [_jsxs(Row, { label: `Sequence ${index + 1}`, children: [_jsxs("span", { style: { fontSize: 11.5, color: theme.textDim }, children: [texture.locations?.length ?? 0, " points"] }), _jsx("button", { className: "qe-hover", style: {
|
|
123
|
+
marginLeft: 'auto',
|
|
124
|
+
background: 'none',
|
|
125
|
+
border: 'none',
|
|
126
|
+
cursor: 'pointer',
|
|
127
|
+
}, title: "Remove sequence", onClick: () => binding.apply(() => behavior.sequencers.splice(index, 1)), children: _jsx(XMarkIcon, { style: iconStyle(13) }) })] }), _jsx(Row, { label: "Time from / to (s)", children: pair(range.a, range.b, (v) => binding.apply(() => (range.a = v)), (v) => binding.apply(() => (range.b = v)), 0.1) }), texture.locations !== undefined && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Scale X / Y", children: pair(texture.scaleX, texture.scaleY, (v) => binding.apply(() => (texture.scaleX = v)), (v) => binding.apply(() => (texture.scaleY = v)), 0.01) }), _jsx(Vector3Row, { label: "Position", value: texture.position, step: 0.1, onChange: (v) => binding.apply(() => texture.position.copy(v)) })] }))] }, index));
|
|
128
|
+
}), _jsx(Row, { label: "Image", children: textureOptions && textureOptions.length > 0 ? (_jsx(SelectField, { value: imageUrl, options: textureOptions.map((option) => ({ value: option.url, label: option.label })), onChange: (url) => setImageUrl(url) })) : (_jsx("input", { className: "qe-hover", style: { flex: 1, minWidth: 0, fontSize: 12 }, placeholder: "Image URL", value: imageUrl, onChange: (event) => setImageUrl(event.target.value) })) }), _jsx(Row, { label: "Alpha threshold", children: _jsx(NumberField, { value: threshold, min: 0, step: 0.05, onChange: (v) => setThreshold(Math.min(1, v)) }) }), _jsxs("button", { className: "qe-hover", style: { display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, fontSize: 12 }, onClick: () => void addFromImage(behavior), children: [_jsx(PlusIcon, { style: iconStyle(13) }), "Add sequence from image"] }), status && _jsx("p", { style: { margin: '4px 0 0', color: theme.textDim, fontSize: 11.5 }, children: status })] })) }));
|
|
129
|
+
}
|
|
54
130
|
export function NoiseModule({ binding }) {
|
|
55
|
-
return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "noise.frequency", label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { hintKey: "noise.strength", label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) })
|
|
131
|
+
return (_jsx(BehaviorModule, { binding: binding, title: "Noise", type: "Noise", create: () => new Noise(new ConstantValue(1), new ConstantValue(1), new ConstantValue(1), new ConstantValue(0)), children: (behavior) => (_jsxs(_Fragment, { children: [_jsx(ValueField, { hintKey: "noise.frequency", label: "Frequency", generator: behavior.frequency, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.frequency = g)) }), _jsx(ValueField, { hintKey: "noise.strength", label: "Strength", generator: behavior.power, min: 0, curveMax: 10, onChange: (g) => binding.apply(() => (behavior.power = g)) }), _jsx(ValueField, { label: "Position amount", generator: behavior.positionAmount, min: 0, curveMax: 2, onChange: (g) => binding.apply(() => (behavior.positionAmount = g)) }), _jsx(ValueField, { label: "Rotation amount", generator: behavior.rotationAmount, min: 0, curveMax: Math.PI, onChange: (g) => binding.apply(() => (behavior.rotationAmount = g)) }), _jsx(ValueField, { label: "Scroll speed", generator: behavior.scrollSpeed, curveMax: 5, onChange: (g) => binding.apply(() => (behavior.scrollSpeed = g)) }), _jsx(Row, { label: "Remap", children: _jsx(CheckboxField, { value: behavior.remap !== undefined, onChange: (v) => binding.apply(() => (behavior.remap = v
|
|
132
|
+
? new PiecewiseBezier([[new Bezier(-1, -1 / 3, 1 / 3, 1), 0]])
|
|
133
|
+
: undefined)) }) }), behavior.remap !== undefined && (_jsx(ValueField, { label: "Remap curve", generator: behavior.remap, curveMax: 1, onChange: (g) => binding.apply(() => (behavior.remap = g)) }))] })) }));
|
|
56
134
|
}
|
|
57
135
|
/** Three numeric inputs for a Vector3 field, replacing the whole vector on any edit. */
|
|
58
136
|
function Vector3Row(props) {
|