babylon.quarks-editor 0.28.0 → 0.29.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 +21 -0
- package/README.md +1 -1
- package/dist/core/defaultEffect.js +2 -0
- package/dist/core/emitterShapeWireframe.js +11 -4
- package/dist/core/loadEffect.js +2 -0
- package/dist/core/shapes.d.ts +7 -1
- package/dist/core/shapes.js +13 -3
- package/dist/core/systems.js +2 -0
- package/dist/core/values.d.ts +23 -6
- package/dist/core/values.js +47 -9
- package/dist/react/AxesCurveField.d.ts +21 -0
- package/dist/react/AxesCurveField.js +25 -0
- package/dist/react/LifeCurveField.d.ts +18 -0
- package/dist/react/LifeCurveField.js +26 -0
- package/dist/react/ValueField.d.ts +3 -2
- package/dist/react/ValueField.js +20 -6
- package/dist/react/behaviorModules.js +12 -8
- package/dist/react/inspectorHints.js +1 -1
- package/dist/react/modules.js +39 -14
- package/package.json +2 -2
- package/src/core/defaultEffect.ts +2 -0
- package/src/core/emitterShapeWireframe.ts +12 -4
- package/src/core/loadEffect.ts +2 -0
- package/src/core/shapes.ts +20 -2
- package/src/core/systems.ts +2 -0
- package/src/core/values.ts +73 -12
- package/src/react/AxesCurveField.tsx +72 -0
- package/src/react/LifeCurveField.tsx +57 -0
- package/src/react/ValueField.tsx +45 -5
- package/src/react/behaviorModules.tsx +31 -17
- package/src/react/inspectorHints.ts +1 -1
- package/src/react/modules.tsx +124 -29
package/dist/react/modules.js
CHANGED
|
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js';
|
|
3
3
|
import { Mesh } from '@babylonjs/core/Meshes/mesh.js';
|
|
4
4
|
import { PlusIcon, XMarkIcon } from '@heroicons/react/24/solid';
|
|
5
|
-
import { AxisAngleGenerator, BoxEmitFrom, ColorOverLife, ColorRange, ConstantColor, ConstantValue, EulerGenerator, Gradient, IntervalValue, RandomColor, RandomColorBetweenGradient, RandomQuatGenerator, RenderMode, SizeOverLife, Vector2, Vector3, Vector3Function, Vector4, normalizeTextureUV, } from 'babylon.quarks';
|
|
5
|
+
import { AxisAngleGenerator, BoxEmitFrom, ColorOverLife, ColorRange, ConeEmitFrom, ConstantColor, ConstantValue, EulerGenerator, Gradient, GridSampling, IntervalValue, RandomColor, RandomColorBetweenGradient, RandomQuatGenerator, RenderMode, SizeOverLife, Vector2, Vector3, Vector3Function, Vector4, normalizeTextureUV, } from 'babylon.quarks';
|
|
6
6
|
import React, { useMemo } from 'react';
|
|
7
7
|
import { DEFAULT_GRADIENT_STOPS, buildGradient, findBehavior, hexToRgb, readGradientStops, rgbToHex, } from '../core/colors.js';
|
|
8
8
|
import { applyRendererMaterial, getMaterialLabel } from '../core/material.js';
|
|
9
9
|
import { DEFAULT_SHAPE_PARAMS, SHAPE_PARAM_KEYS, SHAPE_TYPES, createShape, getShapeType, readShapeParams, } from '../core/shapes.js';
|
|
10
|
-
import { buildCurve, buildScalar,
|
|
11
|
-
import {
|
|
10
|
+
import { buildCurve, buildScalar, readScalar } from '../core/values.js';
|
|
11
|
+
import { AxesCurveField } from './AxesCurveField.js';
|
|
12
12
|
import { GradientEditor } from './GradientEditor.js';
|
|
13
13
|
import { InspectorInfoIcon } from './InspectorInfoIcon.js';
|
|
14
14
|
import { ModuleSection } from './ModuleSection.js';
|
|
@@ -60,6 +60,7 @@ const PARAM_LABELS = {
|
|
|
60
60
|
column: 'Columns',
|
|
61
61
|
row: 'Rows',
|
|
62
62
|
depth: 'Depth',
|
|
63
|
+
length: 'Length',
|
|
63
64
|
speed: 'Speed (lengths/s)',
|
|
64
65
|
};
|
|
65
66
|
/** Uppercase group title inside a module, as the Renderer's "Material" heading. */
|
|
@@ -143,14 +144,19 @@ export function MainModule({ binding }) {
|
|
|
143
144
|
: startColor instanceof Gradient
|
|
144
145
|
? 'gradient'
|
|
145
146
|
: colorMode;
|
|
146
|
-
return (_jsxs(ModuleSection, { title: "Main", children: [_jsx(Row, { label: "Duration", children: _jsx(NumberField, { value: system.duration, min: 0.05, onChange: (v) => binding.apply((s) => (s.duration = v)) }) }), _jsx(Row, { label: "Looping", children: _jsx(CheckboxField, { value: system.looping, onChange: (v) => binding.apply((s) => (s.looping = v)) }) }), _jsx(Row, { label: "World space", children: _jsx(CheckboxField, { value: system.worldSpace, onChange: (v) => binding.apply((s) => (s.worldSpace = v)) }) }),
|
|
147
|
-
{ value: '
|
|
148
|
-
{ value: 'hierarchy', label: 'Hierarchy (
|
|
149
|
-
|
|
147
|
+
return (_jsxs(ModuleSection, { title: "Main", children: [_jsx(Row, { label: "Duration", children: _jsx(NumberField, { value: system.duration, min: 0.05, onChange: (v) => binding.apply((s) => (s.duration = v)) }) }), _jsx(Row, { label: "Looping", children: _jsx(CheckboxField, { value: system.looping, onChange: (v) => binding.apply((s) => (s.looping = v)) }) }), _jsx(Row, { label: "World space", children: _jsx(CheckboxField, { value: system.worldSpace, onChange: (v) => binding.apply((s) => (s.worldSpace = v)) }) }), _jsx(Row, { label: "Scaling mode", children: _jsx(SelectField, { value: system.scalingMode, options: [
|
|
148
|
+
{ value: 'local', label: 'Local (own scale only)' },
|
|
149
|
+
{ value: 'hierarchy', label: 'Hierarchy (parents too)' },
|
|
150
|
+
{ value: 'shape', label: 'Shape (shape only)' },
|
|
151
|
+
], onChange: (mode) => binding.apply((s) => (s.scalingMode = mode)) }) }), _jsx(Row, { label: "Prewarm", children: _jsx(CheckboxField, { value: system.prewarm, onChange: (v) => binding.apply((s) => (s.prewarm = v)) }) }), system.prewarm && (_jsx(Row, { label: "Warm-up time", children: _jsx(NumberField, { value: system.prewarmTime, step: 0.1, min: 0, onChange: (v) => binding.apply((s) => (s.prewarmTime = Math.max(0, v))) }) })), _jsx(Row, { label: "Playback speed", children: _jsx(NumberField, { value: system.playbackSpeed, step: 0.1, min: 0, onChange: (v) => binding.apply((s) => (s.playbackSpeed = Math.max(0, v))) }) }), _jsxs(Row, { label: "Fixed seed", children: [_jsx(CheckboxField, { value: system.seed !== undefined, onChange: (v) => binding.apply((s) => (s.seed = v ? (s.seed ?? 1) : undefined)) }), system.seed !== undefined && (_jsx(NumberField, { value: system.seed, step: 1, min: 0, onChange: (v) => binding.apply((s) => (s.seed = Math.max(0, Math.floor(v)))) }))] }), _jsx(Row, { label: "Off screen", children: _jsx(SelectField, { value: system.cullingMode, options: [
|
|
150
152
|
{ value: 'always', label: 'Always simulate' },
|
|
151
153
|
{ value: 'pause', label: 'Pause' },
|
|
152
154
|
{ value: 'pauseAndCatchUp', label: 'Pause and catch up' },
|
|
153
|
-
], onChange: (mode) => binding.apply((s) => (s.cullingMode = mode)) }) }), _jsx(
|
|
155
|
+
], onChange: (mode) => binding.apply((s) => (s.cullingMode = mode)) }) }), _jsx(Row, { label: "Stop action", hint: "What the system does once it has finished, as Unity's Stop Action. The editor saves it but does not apply it, so previews replay.", children: _jsx(SelectField, { value: system.stopAction, options: [
|
|
156
|
+
{ value: 'none', label: 'None' },
|
|
157
|
+
{ value: 'disable', label: 'Disable' },
|
|
158
|
+
{ value: 'destroy', label: 'Destroy' },
|
|
159
|
+
], onChange: (v) => binding.apply((s) => (s.stopAction = v)) }) }), _jsx(ValueField, { label: "Start delay", generator: system.startDelay, min: 0, curveMax: 5, onChange: (g) => binding.apply((s) => {
|
|
154
160
|
s.startDelay = g;
|
|
155
161
|
s.restart();
|
|
156
162
|
}) }), _jsx(ValueField, { label: "Start lifetime", generator: system.startLife, min: 0.01, curveMax: 10, onChange: (g) => binding.apply((s) => (s.startLife = g)) }), _jsx(ValueField, { label: "Start speed", generator: system.startSpeed, min: 0, curveMax: 20, onChange: (g) => binding.apply((s) => (s.startSpeed = g)) }), _jsx(Row, { label: "Start size", children: _jsx(SelectField, { value: startSize3D ? '3d' : 'uniform', options: [
|
|
@@ -266,7 +272,10 @@ export function ShapeModule({ binding }) {
|
|
|
266
272
|
const scene = system.emitter.getScene();
|
|
267
273
|
// 'vfxBatch' is the renderer's own internal instancing mesh — never a valid emission source.
|
|
268
274
|
const meshOptions = scene.meshes.filter((m) => m instanceof Mesh && m.name !== 'vfxBatch' && m.getTotalVertices() > 0);
|
|
269
|
-
return (_jsxs(ModuleSection, { title: "Shape", children: [_jsx(Row, { label: "Shape", hintKey: "shape.type", children: _jsx(SelectField, { value: type, options: SHAPE_TYPES, onChange: (next) => binding.apply((s) => (s.emitterShape = createShape(next, params))) }) }), keys
|
|
275
|
+
return (_jsxs(ModuleSection, { title: "Shape", children: [_jsx(Row, { label: "Shape", hintKey: "shape.type", children: _jsx(SelectField, { value: type, options: SHAPE_TYPES, onChange: (next) => binding.apply((s) => (s.emitterShape = createShape(next, params))) }) }), keys
|
|
276
|
+
// A cone's Length only shapes Emit from Volume; Unity shows it for Cone Volume alone.
|
|
277
|
+
.filter((key) => key !== 'length' || params.coneEmitFrom === ConeEmitFrom.Volume)
|
|
278
|
+
.map((key) => (_jsx(Row, { label: PARAM_LABELS[key], children: _jsx(NumberField, { value: params[key], min: 0, onChange: (v) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, [key]: v }))) }) }, key))), type === 'mesh_surface' && (_jsx(Row, { label: "Source mesh", children: _jsx(SelectField, { value: params.mesh?.uniqueId != null ? String(params.mesh.uniqueId) : '__none', options: [
|
|
270
279
|
{ value: '__none', label: '(none)' },
|
|
271
280
|
...meshOptions.map((m) => ({ value: String(m.uniqueId), label: m.name })),
|
|
272
281
|
], onChange: (id) => {
|
|
@@ -276,7 +285,13 @@ export function ShapeModule({ binding }) {
|
|
|
276
285
|
{ value: BoxEmitFrom.Volume, label: 'Volume' },
|
|
277
286
|
{ value: BoxEmitFrom.Shell, label: 'Shell (faces)' },
|
|
278
287
|
{ value: BoxEmitFrom.Edge, label: 'Edges' },
|
|
279
|
-
], onChange: (emitFrom) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, emitFrom }))) }) })),
|
|
288
|
+
], onChange: (emitFrom) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, emitFrom }))) }) })), type === 'cone' && (_jsx(Row, { label: "Emit from", children: _jsx(SelectField, { value: params.coneEmitFrom, options: [
|
|
289
|
+
{ value: ConeEmitFrom.Base, label: 'Base' },
|
|
290
|
+
{ value: ConeEmitFrom.Volume, label: 'Volume' },
|
|
291
|
+
], onChange: (coneEmitFrom) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, coneEmitFrom }))) }) })), type === 'grid' && (_jsx(Row, { label: "Sampling", children: _jsx(SelectField, { value: params.sampling, options: [
|
|
292
|
+
{ value: GridSampling.Random, label: 'Random' },
|
|
293
|
+
{ value: GridSampling.Sequential, label: 'Sequential' },
|
|
294
|
+
], onChange: (sampling) => binding.apply((s) => (s.emitterShape = createShape(type, { ...params, sampling }))) }) })), !SHAPES_WITHOUT_MODE_SPREAD.has(type) && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Emit mode", children: _jsx(SelectField, { value: params.mode, options: [
|
|
280
295
|
{ value: 0, label: 'Random' },
|
|
281
296
|
{ value: 1, label: 'Loop' },
|
|
282
297
|
{ value: 2, label: 'Ping-pong' },
|
|
@@ -293,9 +308,9 @@ export function SizeOverLifeModule({ binding }) {
|
|
|
293
308
|
if (enabled) {
|
|
294
309
|
s.addBehavior(new SizeOverLife(buildScalar({ mode: 'curve', value: 1, min: 0, max: 1, curve })));
|
|
295
310
|
}
|
|
296
|
-
}), children: _jsx("div", { style: { marginTop: 6 }, children: _jsx(
|
|
311
|
+
}), children: _jsx("div", { style: { marginTop: 6 }, children: _jsx(AxesCurveField, { generator: behavior ? behavior.size : buildCurve([{ start: 0, p: curve }]), maxValue: 2, onChange: (size) => binding.apply((s) => {
|
|
297
312
|
removeBehavior(s, 'SizeOverLife');
|
|
298
|
-
s.addBehavior(new SizeOverLife(
|
|
313
|
+
s.addBehavior(new SizeOverLife(size));
|
|
299
314
|
}) }) }) }));
|
|
300
315
|
}
|
|
301
316
|
/** The Color over Lifetime module (Unity's Color over Lifetime): a gradient over the particle's life. */
|
|
@@ -554,7 +569,17 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
|
|
|
554
569
|
{ value: 'mix', label: 'Mix (outline)' },
|
|
555
570
|
{ value: 'fade', label: 'Fade (faces fade, edges stay)' },
|
|
556
571
|
], onChange: (mode) => patchRim({ mode }) }) }), _jsx(Row, { label: "Rim color", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: [0, 1, 2].map((channel) => (_jsx(NumberField, { value: rim.color[channel], min: 0, step: 0.05, onChange: (v) => setChannel(channel, v) }, channel))) }) }), _jsx(Row, { label: "Rim power", children: _jsx(NumberField, { value: rim.power, min: 0.01, step: 0.25, onChange: (power) => patchRim({ power }) }) }), _jsx(Row, { label: "Rim strength", children: _jsx(NumberField, { value: rim.strength, min: 0, step: 0.05, onChange: (strength) => patchRim({ strength }) }) })] }));
|
|
557
|
-
})(), _jsx(Row, { label: "Render order", children: _jsx(NumberField, { value: system.renderOrder, step: 1, onChange: (v) => binding.apply((s) => (s.renderOrder = Math.round(v))) }) }), _jsx(Row, { label: "Rendering group", children: _jsx(NumberField, { value: system.renderingGroupId, step: 1, min: 0, onChange: (v) => binding.apply((s) => (s.renderingGroupId = Math.max(0, Math.round(v)))) }) }), _jsx(Row, { label: "Layer mask", children: _jsx(NumberField, { value: system.layers.mask, min: 0, step: 1, onChange: (v) => binding.apply((s) => (s.layers.mask = Math.max(0, Math.floor(v)))) }) }), _jsxs(Row, { label: "Max particles", children: [_jsx(CheckboxField, { value: system.maxParticles !== undefined, onChange: (v) => binding.apply((s) => (s.maxParticles = v ? 1000 : undefined)) }), system.maxParticles !== undefined && (_jsx(NumberField, { value: system.maxParticles, min: 1, step: 10, onChange: (v) => binding.apply((s) => (s.maxParticles = Math.max(1, Math.floor(v)))) }))] }), system.
|
|
572
|
+
})(), _jsx(Row, { label: "Render order", children: _jsx(NumberField, { value: system.renderOrder, step: 1, onChange: (v) => binding.apply((s) => (s.renderOrder = Math.round(v))) }) }), _jsx(Row, { label: "Rendering group", children: _jsx(NumberField, { value: system.renderingGroupId, step: 1, min: 0, onChange: (v) => binding.apply((s) => (s.renderingGroupId = Math.max(0, Math.round(v)))) }) }), _jsx(Row, { label: "Layer mask", children: _jsx(NumberField, { value: system.layers.mask, min: 0, step: 1, onChange: (v) => binding.apply((s) => (s.layers.mask = Math.max(0, Math.floor(v)))) }) }), _jsxs(Row, { label: "Max particles", children: [_jsx(CheckboxField, { value: system.maxParticles !== undefined, onChange: (v) => binding.apply((s) => (s.maxParticles = v ? 1000 : undefined)) }), system.maxParticles !== undefined && (_jsx(NumberField, { value: system.maxParticles, min: 1, step: 10, onChange: (v) => binding.apply((s) => (s.maxParticles = Math.max(1, Math.floor(v)))) }))] }), system.maxParticles !== undefined && system.renderMode !== RenderMode.Trail && (_jsx(Row, { label: "Ring buffer", hint: "What a full system does with new particles, as Unity's Ring Buffer Mode.", children: _jsx(SelectField, { value: system.ringBufferMode, options: [
|
|
573
|
+
{ value: 'disabled', label: 'Disabled' },
|
|
574
|
+
{ value: 'pauseUntilReplaced', label: 'Pause until replaced' },
|
|
575
|
+
{ value: 'loopUntilReplaced', label: 'Loop until replaced' },
|
|
576
|
+
], onChange: (v) => binding.apply((s) => (s.ringBufferMode = v)) }) })), system.maxParticles !== undefined &&
|
|
577
|
+
system.renderMode !== RenderMode.Trail &&
|
|
578
|
+
system.ringBufferMode === 'loopUntilReplaced' && (_jsx(Row, { label: "Loop range", hint: "The part of its life a particle plays again, from 0 to 1.", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y'].map((end) => (_jsx(NumberField, { value: system.ringBufferLoopRange[end], min: 0, step: 0.05, onChange: (v) => binding.apply((s) => {
|
|
579
|
+
const next = s.ringBufferLoopRange.clone();
|
|
580
|
+
next[end] = Math.min(1, Math.max(0, v));
|
|
581
|
+
s.ringBufferLoopRange = new Vector2(Math.min(next.x, next.y), Math.max(next.x, next.y));
|
|
582
|
+
}) }, end))) }) })), system.renderMode !== RenderMode.Mesh && system.renderMode !== RenderMode.Trail && (_jsx(Row, { label: "Pivot", children: _jsx("div", { style: { display: 'flex', gap: 6 }, children: ['x', 'y', 'z'].map((axis) => (_jsx(NumberField, { value: system.pivot?.[axis] ?? 0, step: 0.05, onChange: (v) => binding.apply((s) => {
|
|
558
583
|
const next = (s.pivot ?? new Vector3()).clone();
|
|
559
584
|
next[axis] = v;
|
|
560
585
|
s.pivot = next;
|
|
@@ -573,7 +598,7 @@ export function RendererModule({ binding, textureOptions, geometryOptions, resol
|
|
|
573
598
|
], onChange: (mode) => binding.apply((s) => (s.sortMode = mode)) }) }), _jsx(Row, { label: "Limit screen size", children: _jsx(CheckboxField, { value: system.minParticleSize > 0 || system.maxParticleSize !== Infinity, onChange: (v) => binding.apply((s) => {
|
|
574
599
|
s.minParticleSize = 0;
|
|
575
600
|
s.maxParticleSize = v ? 0.5 : Infinity;
|
|
576
|
-
}) }) }), (system.minParticleSize > 0 || system.maxParticleSize !== Infinity) && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Min size (viewport)", children: _jsx(NumberField, { value: system.minParticleSize, step: 0.01, min: 0, onChange: (v) => binding.apply((s) => (s.minParticleSize = Math.max(0, v))) }) }), _jsx(Row, { label: "Max size (viewport)", children: _jsx(NumberField, { value: system.maxParticleSize === Infinity ? 0 : system.maxParticleSize, step: 0.05, min: 0, onChange: (v) => binding.apply((s) => (s.maxParticleSize = v > 0 ? v : Infinity)) }) })] })), _jsx(Row, { label: "Apply fog", children: _jsx(CheckboxField, { value: system.applyFog, onChange: (v) => binding.apply((s) => (s.applyFog = v)) }) }), _jsx(Row, { label: "Soft particles", children: _jsx(CheckboxField, { value: system.softParticles, onChange: (v) => binding.apply((s) => (s.softParticles = v)) }) }), system.softParticles && (_jsx(Row, { label: "Fade near/far", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: system.softNearFade, min: 0, onChange: (v) => binding.apply((s) => (s.softNearFade = v)) }), _jsx(NumberField, { value: system.softFarFade, min: 0, onChange: (v) => binding.apply((s) => (s.softFarFade = v)) })] }) }))] }));
|
|
601
|
+
}) }) }), (system.minParticleSize > 0 || system.maxParticleSize !== Infinity) && (_jsxs(_Fragment, { children: [_jsx(Row, { label: "Min size (viewport)", children: _jsx(NumberField, { value: system.minParticleSize, step: 0.01, min: 0, onChange: (v) => binding.apply((s) => (s.minParticleSize = Math.max(0, v))) }) }), _jsx(Row, { label: "Max size (viewport)", children: _jsx(NumberField, { value: system.maxParticleSize === Infinity ? 0 : system.maxParticleSize, step: 0.05, min: 0, onChange: (v) => binding.apply((s) => (s.maxParticleSize = v > 0 ? v : Infinity)) }) })] })), _jsx(Row, { label: "Apply fog", children: _jsx(CheckboxField, { value: system.applyFog, onChange: (v) => binding.apply((s) => (s.applyFog = v)) }) }), _jsx(Row, { label: "Image processing", children: _jsx(CheckboxField, { value: system.applyImageProcessing, onChange: (v) => binding.apply((s) => (s.applyImageProcessing = v)) }) }), _jsx(Row, { label: "Soft particles", children: _jsx(CheckboxField, { value: system.softParticles, onChange: (v) => binding.apply((s) => (s.softParticles = v)) }) }), system.softParticles && (_jsx(Row, { label: "Fade near/far", children: _jsxs("div", { style: { display: 'flex', gap: 6 }, children: [_jsx(NumberField, { value: system.softNearFade, min: 0, onChange: (v) => binding.apply((s) => (s.softNearFade = v)) }), _jsx(NumberField, { value: system.softFarFade, min: 0, onChange: (v) => binding.apply((s) => (s.softFarFade = v)) })] }) }))] }));
|
|
577
602
|
}
|
|
578
603
|
function removeBehavior(system, type) {
|
|
579
604
|
const behaviors = system.behaviors;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babylon.quarks-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Shuriken-style effect editor for babylon.quarks: headless edit-model core + React module-stack UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@babylonjs/materials": "^9.0.0",
|
|
62
62
|
"@heroicons/react": ">=2.0.0",
|
|
63
63
|
"@tanstack/react-virtual": ">=3.0.0",
|
|
64
|
-
"babylon.quarks": "~0.
|
|
64
|
+
"babylon.quarks": "~0.29.0",
|
|
65
65
|
"react": ">=18",
|
|
66
66
|
"react-dom": ">=18"
|
|
67
67
|
},
|
|
@@ -40,6 +40,8 @@ export function createDefaultEffect(
|
|
|
40
40
|
transparent: true,
|
|
41
41
|
blendMode: 1,
|
|
42
42
|
});
|
|
43
|
+
// The editor previews over and over; the stop action is saved, not applied.
|
|
44
|
+
system.ignoreStopAction = true;
|
|
43
45
|
system.addBehavior(new ColorOverLife(buildGradient(DEFAULT_GRADIENT_STOPS)));
|
|
44
46
|
system.emitter.rotation.x = -Math.PI / 2;
|
|
45
47
|
return system;
|
|
@@ -6,6 +6,7 @@ import {MeshBuilder} from '@babylonjs/core/Meshes/meshBuilder.js';
|
|
|
6
6
|
import {TransformNode} from '@babylonjs/core/Meshes/transformNode.js';
|
|
7
7
|
import type {Scene} from '@babylonjs/core/scene.js';
|
|
8
8
|
import type {ParticleSystem} from 'babylon.quarks';
|
|
9
|
+
import {ConeEmitFrom} from 'babylon.quarks';
|
|
9
10
|
import {EDITOR_SCENE_NODE_METADATA} from './editorScene.js';
|
|
10
11
|
import './lineShaders.js';
|
|
11
12
|
import {getShapeType, readShapeParams} from './shapes.js';
|
|
@@ -120,8 +121,16 @@ function buildEmitterWireframe(system: ParticleSystem, scene: Scene): Mesh[] {
|
|
|
120
121
|
lines('wf-cone-spoke-b', [Vector3.Zero(), arcPoint(radius, arc)]);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
// Emit from Volume draws the volume itself, out to its length; the base a stretch of the
|
|
125
|
+
// directions.
|
|
126
|
+
const preview =
|
|
127
|
+
params.coneEmitFrom === ConeEmitFrom.Volume
|
|
128
|
+
? Math.max(Number.isFinite(params.length) ? params.length : 0, 0.001)
|
|
129
|
+
: Math.max(radius * 2, 1.25);
|
|
130
|
+
// The rays and the far rim end at the same point, which a near-flat cone keeps in view.
|
|
131
|
+
const travel = preview / Math.max(Math.cos(angle), 0.05);
|
|
132
|
+
const farRadius = radius + Math.sin(angle) * travel;
|
|
133
|
+
const farZ = Math.cos(angle) * travel;
|
|
125
134
|
const spokeCount = Math.max(4, Math.ceil((arc / (Math.PI * 2)) * 12));
|
|
126
135
|
for (let i = 0; i <= spokeCount; i++) {
|
|
127
136
|
const theta = (i / spokeCount) * arc;
|
|
@@ -131,10 +140,9 @@ function buildEmitterWireframe(system: ParticleSystem, scene: Scene): Mesh[] {
|
|
|
131
140
|
Math.sin(angle) * Math.sin(theta),
|
|
132
141
|
Math.cos(angle)
|
|
133
142
|
);
|
|
134
|
-
const travel = preview / Math.max(Math.cos(angle), 0.05);
|
|
135
143
|
lines(`wf-cone-ray-${i}`, [rim, rim.add(dir.scale(travel))], DIRECTION_COLOR);
|
|
136
144
|
}
|
|
137
|
-
lines('wf-cone-far', xyArc(farRadius, arc, 32,
|
|
145
|
+
lines('wf-cone-far', xyArc(farRadius, arc, 32, farZ), DIRECTION_COLOR);
|
|
138
146
|
break;
|
|
139
147
|
}
|
|
140
148
|
case 'sphere': {
|
package/src/core/loadEffect.ts
CHANGED
|
@@ -110,6 +110,8 @@ export function parseEffectFromJson(
|
|
|
110
110
|
const systems: ParticleSystem[] = [];
|
|
111
111
|
QuarksUtil.runOnAllParticleEmitters(root, (emitter: ParticleEmitter) => {
|
|
112
112
|
const system = emitter.system as ParticleSystem;
|
|
113
|
+
// The editor previews over and over; the stop action is saved, not applied.
|
|
114
|
+
system.ignoreStopAction = true;
|
|
113
115
|
if (registerRenderer && renderer) {
|
|
114
116
|
renderer.addSystem(system);
|
|
115
117
|
}
|
package/src/core/shapes.ts
CHANGED
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
BoxEmitFrom,
|
|
5
5
|
BoxEmitter,
|
|
6
6
|
CircleEmitter,
|
|
7
|
+
ConeEmitFrom,
|
|
7
8
|
ConeEmitter,
|
|
8
9
|
ConstantValue,
|
|
9
10
|
DonutEmitter,
|
|
10
11
|
EdgeEmitter,
|
|
11
12
|
EmitterMode,
|
|
12
13
|
GridEmitter,
|
|
14
|
+
GridSampling,
|
|
13
15
|
HemisphereEmitter,
|
|
14
16
|
MeshSurfaceEmitter,
|
|
15
17
|
PointEmitter,
|
|
@@ -82,6 +84,12 @@ export interface ShapeParams {
|
|
|
82
84
|
depth: number;
|
|
83
85
|
/** Where a box emits from: its volume, its faces or its edges. */
|
|
84
86
|
emitFrom: BoxEmitFrom;
|
|
87
|
+
/** Where a cone emits from: its base or its volume. */
|
|
88
|
+
coneEmitFrom: ConeEmitFrom;
|
|
89
|
+
/** Height of a cone's volume (Unity's Length), for Emit from Volume. */
|
|
90
|
+
length: number;
|
|
91
|
+
/** How a grid picks its cells: at random or in turn. */
|
|
92
|
+
sampling: GridSampling;
|
|
85
93
|
/** How fast an edge emitter's loop / ping-pong point travels, in lengths per second. */
|
|
86
94
|
speed: number;
|
|
87
95
|
/** The mesh a mesh surface emitter emits from. */
|
|
@@ -103,6 +111,9 @@ export const DEFAULT_SHAPE_PARAMS: ShapeParams = {
|
|
|
103
111
|
row: 10,
|
|
104
112
|
depth: 1,
|
|
105
113
|
emitFrom: BoxEmitFrom.Volume,
|
|
114
|
+
coneEmitFrom: ConeEmitFrom.Base,
|
|
115
|
+
length: 5,
|
|
116
|
+
sampling: GridSampling.Random,
|
|
106
117
|
speed: 1,
|
|
107
118
|
mesh: undefined,
|
|
108
119
|
};
|
|
@@ -131,7 +142,11 @@ export function readShapeParams(shape: EmitterShape): ShapeParams {
|
|
|
131
142
|
column: num('column', DEFAULT_SHAPE_PARAMS.column),
|
|
132
143
|
row: num('row', DEFAULT_SHAPE_PARAMS.row),
|
|
133
144
|
depth: num('depth', DEFAULT_SHAPE_PARAMS.depth),
|
|
134
|
-
|
|
145
|
+
// A box and a cone both name it emitFrom, with different enums: each keeps its own.
|
|
146
|
+
emitFrom: shape instanceof BoxEmitter ? shape.emitFrom : DEFAULT_SHAPE_PARAMS.emitFrom,
|
|
147
|
+
coneEmitFrom: shape instanceof ConeEmitter ? shape.emitFrom : DEFAULT_SHAPE_PARAMS.coneEmitFrom,
|
|
148
|
+
length: num('length', DEFAULT_SHAPE_PARAMS.length),
|
|
149
|
+
sampling: num('sampling', DEFAULT_SHAPE_PARAMS.sampling) as GridSampling,
|
|
135
150
|
speed: anyShape.speed instanceof ConstantValue ? anyShape.speed.value : DEFAULT_SHAPE_PARAMS.speed,
|
|
136
151
|
mesh: anyShape.mesh as Mesh | undefined,
|
|
137
152
|
};
|
|
@@ -143,7 +158,7 @@ export const SHAPE_PARAM_KEYS: {[K in ShapeType]: Array<keyof ShapeParams>} = {
|
|
|
143
158
|
point: [],
|
|
144
159
|
sphere: ['radius', 'arc', 'thickness'],
|
|
145
160
|
hemisphere: ['radius', 'arc', 'thickness'],
|
|
146
|
-
cone: ['radius', 'arc', 'thickness', 'angle'],
|
|
161
|
+
cone: ['radius', 'arc', 'thickness', 'angle', 'length'],
|
|
147
162
|
circle: ['radius', 'arc', 'thickness'],
|
|
148
163
|
donut: ['radius', 'arc', 'thickness', 'donutRadius'],
|
|
149
164
|
rectangle: ['width', 'height'],
|
|
@@ -180,6 +195,8 @@ export function createShape(type: ShapeType, params: ShapeParams): EmitterShape
|
|
|
180
195
|
arc: params.arc,
|
|
181
196
|
thickness: params.thickness,
|
|
182
197
|
angle: params.angle,
|
|
198
|
+
emitFrom: params.coneEmitFrom,
|
|
199
|
+
length: params.length,
|
|
183
200
|
mode: params.mode,
|
|
184
201
|
spread: params.spread,
|
|
185
202
|
});
|
|
@@ -213,6 +230,7 @@ export function createShape(type: ShapeType, params: ShapeParams): EmitterShape
|
|
|
213
230
|
height: params.height,
|
|
214
231
|
column: params.column,
|
|
215
232
|
row: params.row,
|
|
233
|
+
sampling: params.sampling,
|
|
216
234
|
});
|
|
217
235
|
case 'box':
|
|
218
236
|
return new BoxEmitter({
|
package/src/core/systems.ts
CHANGED
|
@@ -49,6 +49,8 @@ export function createChildSystem(parent: ParticleSystem, options: ChildSystemOp
|
|
|
49
49
|
worldSpace: true,
|
|
50
50
|
});
|
|
51
51
|
child.emitter.name = options.name ?? (oneShot ? 'sub-emitter' : 'child-system');
|
|
52
|
+
// The editor previews over and over; the stop action is saved, not applied.
|
|
53
|
+
child.ignoreStopAction = true;
|
|
52
54
|
if (oneShot) {
|
|
53
55
|
child.onlyUsedByOther = true;
|
|
54
56
|
}
|
package/src/core/values.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type {FunctionValueGenerator, RotationGenerator, ValueGenerator, Vector3Generator} from 'babylon.quarks';
|
|
2
|
-
import {Bezier, ConstantValue, IntervalValue, PiecewiseBezier} from 'babylon.quarks';
|
|
2
|
+
import {Bezier, ConstantValue, IntervalValue, PiecewiseBezier, RandomBetweenCurves} from 'babylon.quarks';
|
|
3
3
|
|
|
4
4
|
/** A generator of one number: a constant or random value, or a curve over the particle's life. */
|
|
5
5
|
export type ScalarGenerator = ValueGenerator | FunctionValueGenerator;
|
|
6
6
|
/** Any generator a value field can be pointed at; ones that are not scalar read as the default. */
|
|
7
7
|
export type AnyGenerator = ScalarGenerator | Vector3Generator | RotationGenerator;
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* How a value field produces its number: Unity's Constant, Random Between Two Constants, Curve or
|
|
10
|
+
* Random Between Two Curves.
|
|
11
|
+
*/
|
|
12
|
+
export type ScalarMode = 'constant' | 'random' | 'curve' | 'curves';
|
|
10
13
|
|
|
11
14
|
/** Editable snapshot of a scalar value generator. */
|
|
12
15
|
export interface ScalarValueState {
|
|
@@ -18,8 +21,10 @@ export interface ScalarValueState {
|
|
|
18
21
|
min: number;
|
|
19
22
|
/** Upper end of the random range (mode: random). */
|
|
20
23
|
max: number;
|
|
21
|
-
/** Cubic bezier control values over normalized lifetime (mode: curve). */
|
|
24
|
+
/** Cubic bezier control values over normalized lifetime (mode: curve; the first curve of curves). */
|
|
22
25
|
curve: [number, number, number, number];
|
|
26
|
+
/** The second curve's control values (mode: curves); the first curve's when left out. */
|
|
27
|
+
curve2?: [number, number, number, number];
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
@@ -27,7 +32,14 @@ export interface ScalarValueState {
|
|
|
27
32
|
* Anything else, including a missing generator, reads as the constant 1.
|
|
28
33
|
*/
|
|
29
34
|
export function readScalar(generator: AnyGenerator | undefined): ScalarValueState {
|
|
30
|
-
const state: ScalarValueState = {
|
|
35
|
+
const state: ScalarValueState = {
|
|
36
|
+
mode: 'constant',
|
|
37
|
+
value: 1,
|
|
38
|
+
min: 0.5,
|
|
39
|
+
max: 1.5,
|
|
40
|
+
curve: [0, 1, 1, 0],
|
|
41
|
+
curve2: [0, 1, 1, 0],
|
|
42
|
+
};
|
|
31
43
|
if (generator instanceof ConstantValue) {
|
|
32
44
|
state.mode = 'constant';
|
|
33
45
|
state.value = generator.value;
|
|
@@ -40,15 +52,24 @@ export function readScalar(generator: AnyGenerator | undefined): ScalarValueStat
|
|
|
40
52
|
state.value = (generator.a + generator.b) / 2;
|
|
41
53
|
} else if (generator instanceof PiecewiseBezier) {
|
|
42
54
|
state.mode = 'curve';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
state.curve = firstSegment(generator) ?? state.curve;
|
|
56
|
+
state.curve2 = state.curve;
|
|
57
|
+
state.value = state.curve[0];
|
|
58
|
+
} else if (generator instanceof RandomBetweenCurves) {
|
|
59
|
+
state.mode = 'curves';
|
|
60
|
+
state.curve = firstSegment(generator.curve1) ?? state.curve;
|
|
61
|
+
state.curve2 = firstSegment(generator.curve2) ?? state.curve;
|
|
62
|
+
state.value = state.curve[0];
|
|
48
63
|
}
|
|
49
64
|
return state;
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
/** The control values of a curve's first segment, if it has one. */
|
|
68
|
+
function firstSegment(curve: PiecewiseBezier): [number, number, number, number] | undefined {
|
|
69
|
+
const first = curve.functions[0]?.[0];
|
|
70
|
+
return first ? [first.p[0], first.p[1], first.p[2], first.p[3]] : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
/** The generator a value field's state describes; a curve state builds a one-segment curve. */
|
|
53
74
|
export function buildScalar(state: ScalarValueState & {mode: 'curve'}): PiecewiseBezier;
|
|
54
75
|
export function buildScalar(state: ScalarValueState): ScalarGenerator;
|
|
@@ -60,6 +81,11 @@ export function buildScalar(state: ScalarValueState): ScalarGenerator {
|
|
|
60
81
|
return new IntervalValue(Math.min(state.min, state.max), Math.max(state.min, state.max));
|
|
61
82
|
case 'curve':
|
|
62
83
|
return new PiecewiseBezier([[new Bezier(...state.curve), 0]]);
|
|
84
|
+
case 'curves':
|
|
85
|
+
return new RandomBetweenCurves(
|
|
86
|
+
new PiecewiseBezier([[new Bezier(...state.curve), 0]]),
|
|
87
|
+
new PiecewiseBezier([[new Bezier(...(state.curve2 ?? state.curve)), 0]])
|
|
88
|
+
);
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
|
|
@@ -71,8 +97,20 @@ export interface CurvePiece {
|
|
|
71
97
|
p: [number, number, number, number];
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Reads pieces from a PiecewiseBezier, or from the first curve of a RandomBetweenCurves (the
|
|
102
|
+
* second with `second`). A constant reads as a flat curve, a random range as a flat curve at its
|
|
103
|
+
* lower end (its upper with `second`), and other generators as one default segment.
|
|
104
|
+
*/
|
|
105
|
+
export function readPieces(generator: AnyGenerator | undefined, second = false): CurvePiece[] {
|
|
106
|
+
if (generator instanceof RandomBetweenCurves) {
|
|
107
|
+
return readPieces(second ? generator.curve2 : generator.curve1);
|
|
108
|
+
}
|
|
109
|
+
// A constant is a flat curve, and a random range two flat curves at its ends.
|
|
110
|
+
if (generator instanceof ConstantValue || generator instanceof IntervalValue) {
|
|
111
|
+
const value = generator instanceof ConstantValue ? generator.value : second ? generator.b : generator.a;
|
|
112
|
+
return [{start: 0, p: [value, value, value, value]}];
|
|
113
|
+
}
|
|
76
114
|
if (generator instanceof PiecewiseBezier && generator.functions.length > 0) {
|
|
77
115
|
return generator.functions.map(([bezier, start]) => ({
|
|
78
116
|
start,
|
|
@@ -89,6 +127,29 @@ export function buildCurve(pieces: CurvePiece[]): PiecewiseBezier {
|
|
|
89
127
|
return new PiecewiseBezier(sorted.map((piece) => [new Bezier(...piece.p), piece.start] as [Bezier, number]));
|
|
90
128
|
}
|
|
91
129
|
|
|
130
|
+
/**
|
|
131
|
+
* `generator` as one curve (`'curve'`) or two (`'curves'`, Unity's Random Between Two Curves),
|
|
132
|
+
* keeping every segment: one curve becomes both bounds of two, and two keep their first as one.
|
|
133
|
+
* A constant is a flat curve, and a random range flat curves at its ends.
|
|
134
|
+
*/
|
|
135
|
+
export function withCurves(generator: AnyGenerator | undefined, mode: 'curve'): PiecewiseBezier;
|
|
136
|
+
export function withCurves(generator: AnyGenerator | undefined, mode: 'curves'): RandomBetweenCurves;
|
|
137
|
+
export function withCurves(
|
|
138
|
+
generator: AnyGenerator | undefined,
|
|
139
|
+
mode: 'curve' | 'curves'
|
|
140
|
+
): PiecewiseBezier | RandomBetweenCurves;
|
|
141
|
+
export function withCurves(
|
|
142
|
+
generator: AnyGenerator | undefined,
|
|
143
|
+
mode: 'curve' | 'curves'
|
|
144
|
+
): PiecewiseBezier | RandomBetweenCurves {
|
|
145
|
+
const first = buildCurve(readPieces(generator));
|
|
146
|
+
if (mode === 'curve') {
|
|
147
|
+
return first;
|
|
148
|
+
}
|
|
149
|
+
const second = generator instanceof RandomBetweenCurves || generator instanceof IntervalValue;
|
|
150
|
+
return new RandomBetweenCurves(first, buildCurve(readPieces(generator, second)));
|
|
151
|
+
}
|
|
152
|
+
|
|
92
153
|
/** Splits the piece containing x into two exact halves using Bezier.split. */
|
|
93
154
|
export function splitPieces(pieces: CurvePiece[], x: number): CurvePiece[] {
|
|
94
155
|
const sorted = [...pieces].sort((a, b) => a.start - b.start);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {IntervalValue, PiecewiseBezier, RandomBetweenCurves, Vector3Function} from 'babylon.quarks';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {AnyGenerator, withCurves} from '../core/values.js';
|
|
4
|
+
import {CheckboxField, Row} from './fields.js';
|
|
5
|
+
import {LifeCurveField} from './LifeCurveField.js';
|
|
6
|
+
|
|
7
|
+
const captionStyle: React.CSSProperties = {fontSize: 10.5, opacity: 0.6, marginTop: 6};
|
|
8
|
+
|
|
9
|
+
/** One curve or two, for one axis or for all of them. */
|
|
10
|
+
type AxisCurve = PiecewiseBezier | RandomBetweenCurves;
|
|
11
|
+
|
|
12
|
+
/** Props of an {@link AxesCurveField}. */
|
|
13
|
+
export interface AxesCurveFieldProps {
|
|
14
|
+
/** The generator shown: one curve (or two) for every axis, or a `Vector3Function` per axis. */
|
|
15
|
+
generator: AnyGenerator | undefined;
|
|
16
|
+
/** Top of the curve editors' value range. */
|
|
17
|
+
maxValue?: number;
|
|
18
|
+
/** Receives the edited generator. */
|
|
19
|
+
onChange: (generator: AxisCurve | Vector3Function) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The curve or curves `generator` holds, as a fresh copy: a constant as a flat curve, a random
|
|
24
|
+
* range as two flat ones.
|
|
25
|
+
*/
|
|
26
|
+
function curveOf(generator: AnyGenerator | undefined): AxisCurve {
|
|
27
|
+
const two = generator instanceof RandomBetweenCurves || generator instanceof IntervalValue;
|
|
28
|
+
return withCurves(generator, two ? 'curves' : 'curve');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A curve over life with Unity's Separate Axes switch: off, one curve (or two) for all axes; on, a
|
|
33
|
+
* {@link Vector3Function} with its own for X, Y and Z. Switching on gives every axis the shared
|
|
34
|
+
* curve; switching off keeps the X one.
|
|
35
|
+
*/
|
|
36
|
+
export function AxesCurveField(props: AxesCurveFieldProps): React.ReactElement {
|
|
37
|
+
const {generator} = props;
|
|
38
|
+
const axes = generator instanceof Vector3Function ? generator : undefined;
|
|
39
|
+
const toggle = (on: boolean) =>
|
|
40
|
+
props.onChange(
|
|
41
|
+
on ? new Vector3Function(curveOf(generator), curveOf(generator), curveOf(generator)) : curveOf(axes?.x)
|
|
42
|
+
);
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<Row label="Separate axes" hint="A curve for each of X, Y and Z, as Unity's Separate Axes.">
|
|
46
|
+
<CheckboxField value={axes !== undefined} onChange={toggle} />
|
|
47
|
+
</Row>
|
|
48
|
+
{axes === undefined ? (
|
|
49
|
+
<LifeCurveField generator={generator} maxValue={props.maxValue} onChange={props.onChange} />
|
|
50
|
+
) : (
|
|
51
|
+
(['x', 'y', 'z'] as const).map((axis) => (
|
|
52
|
+
<div key={axis}>
|
|
53
|
+
<div style={captionStyle}>{axis.toUpperCase()}</div>
|
|
54
|
+
<LifeCurveField
|
|
55
|
+
generator={axes[axis]}
|
|
56
|
+
maxValue={props.maxValue}
|
|
57
|
+
onChange={(curve) =>
|
|
58
|
+
props.onChange(
|
|
59
|
+
new Vector3Function(
|
|
60
|
+
axis === 'x' ? curve : axes.x,
|
|
61
|
+
axis === 'y' ? curve : axes.y,
|
|
62
|
+
axis === 'z' ? curve : axes.z
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
))
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {IntervalValue, PiecewiseBezier, RandomBetweenCurves} from 'babylon.quarks';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {AnyGenerator, buildCurve, CurvePiece, readPieces, withCurves} from '../core/values.js';
|
|
4
|
+
import {CurveEditor} from './CurveEditor.js';
|
|
5
|
+
import {CheckboxField, Row} from './fields.js';
|
|
6
|
+
|
|
7
|
+
const captionStyle: React.CSSProperties = {fontSize: 10.5, opacity: 0.6, flex: 'none'};
|
|
8
|
+
|
|
9
|
+
/** Props of a {@link LifeCurveField}. */
|
|
10
|
+
export interface LifeCurveFieldProps {
|
|
11
|
+
/** The generator shown: one curve, two curves, or anything else, read as one curve. */
|
|
12
|
+
generator: AnyGenerator | undefined;
|
|
13
|
+
/** Top of the curve editors' value range. */
|
|
14
|
+
maxValue?: number;
|
|
15
|
+
/** Receives the edited curve or curves. */
|
|
16
|
+
onChange: (generator: PiecewiseBezier | RandomBetweenCurves) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A setting that is a curve over the particle's life, or two with a "Two curves" switch (Unity's
|
|
21
|
+
* Random Between Two Curves): each particle then follows its own curve between the lower and the
|
|
22
|
+
* upper one. Every segment of both curves is kept through edits and switches.
|
|
23
|
+
*/
|
|
24
|
+
export function LifeCurveField(props: LifeCurveFieldProps): React.ReactElement {
|
|
25
|
+
const {generator} = props;
|
|
26
|
+
// A random range shows as two flat curves, and an edit turns it into them.
|
|
27
|
+
const two = generator instanceof RandomBetweenCurves || generator instanceof IntervalValue;
|
|
28
|
+
const edit = (pieces: CurvePiece[], upper: boolean) => {
|
|
29
|
+
if (!two) {
|
|
30
|
+
props.onChange(buildCurve(pieces));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const lower = upper ? buildCurve(readPieces(generator)) : buildCurve(pieces);
|
|
34
|
+
const higher = upper ? buildCurve(pieces) : buildCurve(readPieces(generator, true));
|
|
35
|
+
props.onChange(new RandomBetweenCurves(lower, higher));
|
|
36
|
+
};
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<Row label="Two curves" hint="Random between two curves: each particle follows its own curve between them.">
|
|
40
|
+
<CheckboxField
|
|
41
|
+
value={two}
|
|
42
|
+
onChange={(on) => props.onChange(withCurves(generator, on ? 'curves' : 'curve'))}
|
|
43
|
+
/>
|
|
44
|
+
</Row>
|
|
45
|
+
{(two ? (['lower', 'upper'] as const) : (['curve'] as const)).map((bound) => (
|
|
46
|
+
<div key={bound} style={{marginTop: 6}}>
|
|
47
|
+
{two && <span style={captionStyle}>{bound}</span>}
|
|
48
|
+
<CurveEditor
|
|
49
|
+
pieces={readPieces(generator, bound === 'upper')}
|
|
50
|
+
maxValue={props.maxValue}
|
|
51
|
+
onChange={(pieces) => edit(pieces, bound === 'upper')}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
}
|