@woosh/meep-engine 3.18.0 → 3.20.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/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleCurveEditorView.js +303 -120
- package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleGradientEditorView.js +108 -63
- package/editor/view/particles/effect/ParticleGraphEditorView.js +2 -2
- package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
- package/editor/view/particles/effect/ParticleNodeParametersView.js +7 -46
- package/editor/view/particles/effect/particle-editor.css +60 -30
- package/package.json +1 -2
- package/samples/engine/README.md +1 -1
- package/src/core/binary/compression/decompress_bytes.d.ts +13 -0
- package/src/core/binary/compression/decompress_bytes.d.ts.map +1 -0
- package/src/core/binary/compression/decompress_bytes.js +28 -0
- package/src/core/collection/array/array_group_by.d.ts.map +1 -1
- package/src/core/collection/array/array_group_by.js +2 -11
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +4 -9
- package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +67 -22
- package/src/core/model/object/optional_clone.d.ts +7 -0
- package/src/core/model/object/optional_clone.d.ts.map +1 -0
- package/src/core/model/object/optional_equality.d.ts +10 -0
- package/src/core/model/object/optional_equality.d.ts.map +1 -0
- package/src/{shade/descriptor/util → core/model/object}/optional_equality.js +2 -2
- package/src/core/model/object/optional_hash.d.ts +9 -0
- package/src/core/model/object/optional_hash.d.ts.map +1 -0
- package/src/{shade/descriptor/util → core/model/object}/optional_hash.js +3 -3
- package/src/engine/asset/Asset.d.ts +1 -1
- package/src/engine/asset/Asset.d.ts.map +1 -1
- package/src/engine/asset/Asset.js +3 -1
- package/src/engine/asset/loaders/image/ImageDecoderWorker.js +12 -27
- package/src/engine/asset/loaders/image/prototypePNG.js +8 -7
- package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.d.ts.map +1 -1
- package/src/engine/ecs/gui/hud/HeadsUpDisplaySystem.js +9 -10
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +266 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -0
- package/src/engine/graphics/ecs/particles/ParticleEffect.js +439 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +58 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -0
- package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +219 -0
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +178 -25
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.js +809 -310
- package/src/format/image/png/PNGReader.d.ts +7 -6
- package/src/format/image/png/PNGReader.d.ts.map +1 -1
- package/src/format/image/png/PNGReader.js +13 -12
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts +3 -2
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +5 -4
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts +3 -2
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -1
- package/src/format/image/png/chunk/png_chunk_decode_zTXt.js +5 -4
- package/src/format/image/png/png_inflate.d.ts +3 -3
- package/src/format/image/png/png_inflate.d.ts.map +1 -1
- package/src/format/image/png/png_inflate.js +29 -39
- package/src/format/texture/ktx2/ktx2_read.d.ts +4 -4
- package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -1
- package/src/format/texture/ktx2/ktx2_read.js +18 -21
- package/src/shade/descriptor/binding/BindGroupLayoutDescriptor.js +1 -1
- package/src/shade/descriptor/binding/BindGroupLayoutEntry.js +3 -3
- package/src/shade/descriptor/pipeline/PipelineDescriptorBase.js +1 -1
- package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +1 -1
- package/src/shade/descriptor/pipeline/render/ColorTargetState.js +3 -3
- package/src/shade/descriptor/pipeline/render/FragmentState.js +1 -1
- package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +3 -3
- package/src/shade/playground/particle_ecs/README.md +203 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.d.ts +39 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_editor.js +315 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +145 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_effects.js +202 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts +23 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_sprites.js +315 -0
- package/src/shade/playground/particle_ecs/bonfire_world.d.ts +86 -0
- package/src/shade/playground/particle_ecs/bonfire_world.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/bonfire_world.js +303 -0
- package/src/shade/playground/particle_ecs/effects/embers.json +1634 -0
- package/src/shade/playground/particle_ecs/effects/flame.json +1882 -0
- package/src/shade/playground/particle_ecs/effects/smoke.json +1860 -0
- package/src/shade/playground/particle_ecs/effects/soot.json +1606 -0
- package/src/shade/playground/particle_ecs/index.html +330 -0
- package/src/shade/playground/particle_ecs/main.d.ts +2 -0
- package/src/shade/playground/particle_ecs/main.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/main.js +566 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.d.ts +16 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.d.ts.map +1 -0
- package/src/shade/playground/particle_ecs/moonlit_environment.js +144 -0
- package/src/shade/playground/particle_editor/README.md +5 -4
- package/src/shade/playground/particle_system/particle_prototype.d.ts +7 -9
- package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
- package/src/shade/playground/particle_system/particle_prototype.js +7 -9
- package/src/shade/playground/profile_hotkey.d.ts +58 -0
- package/src/shade/playground/profile_hotkey.d.ts.map +1 -0
- package/src/shade/playground/profile_hotkey.js +325 -0
- package/src/shade/playground/ssr_motion/README.md +22 -0
- package/src/shade/playground/ssr_motion/index.html +18 -0
- package/src/shade/playground/ssr_motion/main.d.ts +2 -0
- package/src/shade/playground/ssr_motion/main.d.ts.map +1 -0
- package/src/shade/playground/ssr_motion/main.js +149 -0
- package/src/shade/playground/ssr_variance/README.md +105 -0
- package/src/shade/playground/ssr_variance/capture.d.ts +16 -0
- package/src/shade/playground/ssr_variance/capture.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/capture.js +96 -0
- package/src/shade/playground/ssr_variance/index.html +23 -0
- package/src/shade/playground/ssr_variance/main.d.ts +2 -0
- package/src/shade/playground/ssr_variance/main.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/main.js +246 -0
- package/src/shade/playground/ssr_variance/reference.d.ts +18 -0
- package/src/shade/playground/ssr_variance/reference.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/reference.js +78 -0
- package/src/shade/playground/ssr_variance/scene.d.ts +11 -0
- package/src/shade/playground/ssr_variance/scene.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/scene.js +61 -0
- package/src/shade/playground/ssr_variance/statistics.d.ts +44 -0
- package/src/shade/playground/ssr_variance/statistics.d.ts.map +1 -0
- package/src/shade/playground/ssr_variance/statistics.js +51 -0
- package/src/shade/renderer/geometry/Geometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/Geometry.js +4 -7
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
- package/src/shade/renderer/loader/gltf/tiny-gltf.js +9 -3
- package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +4 -4
- package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
- package/src/shade/renderer/loader/usd/usd_decode_image.js +4 -4
- package/src/shade/renderer/material/StandardShadeMaterial.js +2 -2
- package/src/shade/renderer/particles/DESIGN.md +745 -702
- package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
- package/src/shade/renderer/particles/GPUParticleSystem.js +3 -1
- package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +1 -1
- package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +9 -15
- package/src/shade/renderer/particles/graph/particle_group_library.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/particle_group_library.js +8 -3
- package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +0 -2
- package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/particle_node_parameters.js +0 -9
- package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/particle_node_presentation.js +0 -10
- package/src/shade/renderer/particles/graph/particle_ramp.d.ts +2 -8
- package/src/shade/renderer/particles/graph/particle_ramp.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/particle_ramp.js +2 -8
- package/src/shade/renderer/particles/graph/validate_particle_graph.js +0 -6
- package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph_particles_avboit.js +13 -4
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +1 -12
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/InstructionStream.js +1 -12
- package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +54 -60
- package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleVMISA.js +9 -17
- package/src/shade/renderer/particles/isa/particle_assembly.d.ts +0 -1
- package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/particle_assembly.js +2 -16
- package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts +1 -1
- package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.js +1 -1
- package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +0 -14
- package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +3 -17
- package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +15 -36
- package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +1 -5
- package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +1 -8
- package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +2 -11
- package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
- package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +2 -11
- package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +11 -4
- package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/ParticleEmitter.js +431 -424
- package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.js +13 -0
- package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts +3 -6
- package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +49 -17
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +0 -2
- package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.js +0 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +1 -5
- package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.js +0 -2
- package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +1 -3
- package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
- package/src/shade/renderer/particles/vm/ParticleVMReference.js +4 -20
- package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +2 -5
- package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance.d.ts.map +1 -1
- package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance.js +0 -2
- package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance_wide.d.ts.map +1 -1
- package/src/shade/renderer/particles/warmup/shader_particle_warmup_advance_wide.js +0 -2
- package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/SSR.js +2 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_reprojection.js +20 -5
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_sample_history.js +19 -5
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts +4 -0
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.d.ts.map +1 -0
- package/src/shade/renderer/postprocess/ssr/reproject/chunk_ssr_temporal_accumulate.js +137 -0
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts +0 -8
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/ssr/ssr_reproject_shader.js +18 -114
- package/src/shade/renderer/scene/Mesh.js +1 -1
- package/src/shade/renderer/texture/ShadeTexture.js +1 -1
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts +2 -2
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts.map +1 -1
- package/src/shade/renderer/texture/source/texel_data_from_ktx2.js +3 -3
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts +0 -19
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +0 -30
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts +0 -11
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_disabled.js +0 -13
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts +0 -20
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_warmup.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts +0 -14
- package/src/shade/renderer/particles/shaders/chunk_particle_emitter_world_sphere.d.ts.map +0 -1
- package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts +0 -50
- package/src/shade/renderer/particles/shaders/shader_particle_reclaim.d.ts.map +0 -1
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts +0 -17
- package/src/shade/renderer/postprocess/ssr/reproject/shader_ffx_denoiser_reflections_reproject.d.ts.map +0 -1
- /package/src/{shade/descriptor/util → core/model/object}/optional_clone.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnimationCurve } from "../../../../src/engine/animation/curve/AnimationCurve.js";
|
|
2
2
|
import { Keyframe } from "../../../../src/engine/animation/curve/Keyframe.js";
|
|
3
|
-
import { clamp01 } from "../../../../src/core/math/clamp01.js";
|
|
3
|
+
import { clamp01 } from "../../../../src/core/math/clamp01.js";
|
|
4
|
+
import { spline3_hermite_bounds } from "../../../../src/core/math/spline/spline3_hermite_bounds.js";
|
|
4
5
|
import {
|
|
5
6
|
particle_curve_animation,
|
|
6
7
|
particle_curve_from_animation,
|
|
@@ -8,12 +9,12 @@ import {
|
|
|
8
9
|
import View from "../../../../src/view/View.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* A curve
|
|
12
|
+
* A curve with normalized time across and an unrestricted value up.
|
|
12
13
|
*
|
|
13
14
|
* Drag a key to move it. Click where there is no key and one is added there, with tangents that
|
|
14
15
|
* leave the curve looking like it did — so adding a key is somewhere to make an edit *from* rather
|
|
15
|
-
* than an edit in itself. Middle-click a key, or press the remove button, and it goes. The
|
|
16
|
-
* below
|
|
16
|
+
* than an edit in itself. Middle-click a key, or press the remove button, and it goes. The fields
|
|
17
|
+
* below edit its exact time, value and tangents.
|
|
17
18
|
*
|
|
18
19
|
* ## It is an AnimationCurve
|
|
19
20
|
*
|
|
@@ -26,27 +27,26 @@ import View from "../../../../src/view/View.js";
|
|
|
26
27
|
* {@link ../../../../src/shade/renderer/particles/graph/particle_ramp.js} — and its agreeing with
|
|
27
28
|
* this one is what `particle_ramp.spec.js` checks.
|
|
28
29
|
*
|
|
29
|
-
* ## The
|
|
30
|
+
* ## The viewport
|
|
30
31
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* control would not be.
|
|
32
|
+
* Time spans 0 to 1. The value axis fits the curve, including extrema between keys. Its scale stays
|
|
33
|
+
* fixed throughout a pointer gesture so the mapping under the pointer cannot shift during a drag.
|
|
34
|
+
* Dragging beyond the plot continues along that scale; releasing refits the viewport.
|
|
35
35
|
*
|
|
36
36
|
* ## When an edit is committed
|
|
37
37
|
*
|
|
38
38
|
* On pointer up, for the reason set out in {@link ./ParticleGradientEditorView.js}: a key dragged
|
|
39
|
-
* across the
|
|
39
|
+
* across the plot is one thing to undo, not the hundred pointer events it was made of.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
/** @type {number} */
|
|
43
|
-
const WIDTH =
|
|
43
|
+
const WIDTH = 240;
|
|
44
44
|
|
|
45
45
|
/** @type {number} */
|
|
46
|
-
const HEIGHT =
|
|
46
|
+
const HEIGHT = 132;
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
const
|
|
48
|
+
/** Separate gutters for axis labels and complete endpoint handles. Used by drawing and picking. */
|
|
49
|
+
const PLOT = Object.freeze({ left: 52, top: 10, right: WIDTH - 12, bottom: HEIGHT - 20 });
|
|
50
50
|
|
|
51
51
|
/** How near a key a press has to be to take hold of it rather than make a new one. */
|
|
52
52
|
const GRAB_RADIUS = 8;
|
|
@@ -59,7 +59,8 @@ const GRAB_RADIUS = 8;
|
|
|
59
59
|
const TANGENT_MODES = Object.freeze({
|
|
60
60
|
Smooth: "smooth",
|
|
61
61
|
Linear: "linear",
|
|
62
|
-
Flat: "flat",
|
|
62
|
+
Flat: "flat",
|
|
63
|
+
Custom: "custom",
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
export class ParticleCurveEditorView extends View {
|
|
@@ -67,7 +68,12 @@ export class ParticleCurveEditorView extends View {
|
|
|
67
68
|
* The curve being edited — a working copy, drawn continuously and committed on release.
|
|
68
69
|
* @type {AnimationCurve}
|
|
69
70
|
*/
|
|
70
|
-
#curve = new AnimationCurve();
|
|
71
|
+
#curve = new AnimationCurve();
|
|
72
|
+
|
|
73
|
+
/** Value bounds and tick spacing of the viewport, never limits on the authored data.
|
|
74
|
+
* @type {{min: number, max: number, step: number}}
|
|
75
|
+
*/
|
|
76
|
+
#range = { min: 0, max: 1, step: 0.25 };
|
|
71
77
|
|
|
72
78
|
/** @type {HTMLCanvasElement} */
|
|
73
79
|
#canvas;
|
|
@@ -78,14 +84,17 @@ export class ParticleCurveEditorView extends View {
|
|
|
78
84
|
/** @type {number} */
|
|
79
85
|
#selected = 0;
|
|
80
86
|
|
|
81
|
-
/** @type {
|
|
82
|
-
#
|
|
87
|
+
/** @type {number|null} */
|
|
88
|
+
#pointerId = null;
|
|
89
|
+
|
|
90
|
+
/** @type {boolean} ignore our commit's synchronous parameter-change notification */
|
|
91
|
+
#writing = false;
|
|
83
92
|
|
|
84
93
|
/** @type {function():object} */
|
|
85
94
|
#read;
|
|
86
95
|
|
|
87
96
|
/** @type {function(object):void} */
|
|
88
|
-
#
|
|
97
|
+
#write;
|
|
89
98
|
|
|
90
99
|
/**
|
|
91
100
|
* Which rule each key's tangents came from, by key.
|
|
@@ -105,7 +114,13 @@ export class ParticleCurveEditorView extends View {
|
|
|
105
114
|
#modes = new WeakMap();
|
|
106
115
|
|
|
107
116
|
/** @type {HTMLSelectElement} */
|
|
108
|
-
#tangents;
|
|
117
|
+
#tangents;
|
|
118
|
+
|
|
119
|
+
/** @type {HTMLInputElement} */
|
|
120
|
+
#time;
|
|
121
|
+
|
|
122
|
+
/** @type {HTMLInputElement} */
|
|
123
|
+
#value;
|
|
109
124
|
|
|
110
125
|
/**
|
|
111
126
|
* @param {object} options
|
|
@@ -116,7 +131,7 @@ export class ParticleCurveEditorView extends View {
|
|
|
116
131
|
super();
|
|
117
132
|
|
|
118
133
|
this.#read = read;
|
|
119
|
-
this.#
|
|
134
|
+
this.#write = write;
|
|
120
135
|
|
|
121
136
|
this.el = document.createElement('div');
|
|
122
137
|
|
|
@@ -126,7 +141,9 @@ export class ParticleCurveEditorView extends View {
|
|
|
126
141
|
|
|
127
142
|
canvas.width = WIDTH;
|
|
128
143
|
canvas.height = HEIGHT;
|
|
129
|
-
canvas.classList.add('plot');
|
|
144
|
+
canvas.classList.add('plot');
|
|
145
|
+
canvas.tabIndex = 0;
|
|
146
|
+
canvas.setAttribute('aria-label', 'Curve: normalized time t and value. Drag keys; click to add; Escape to cancel.');
|
|
130
147
|
|
|
131
148
|
this.el.appendChild(canvas);
|
|
132
149
|
|
|
@@ -134,7 +151,16 @@ export class ParticleCurveEditorView extends View {
|
|
|
134
151
|
|
|
135
152
|
fields.classList.add('key-fields');
|
|
136
153
|
|
|
137
|
-
this.el.appendChild(fields);
|
|
154
|
+
this.el.appendChild(fields);
|
|
155
|
+
|
|
156
|
+
this.#time = coordinate_field(fields, 'time', 'Time (t)');
|
|
157
|
+
this.#time.min = '0';
|
|
158
|
+
this.#time.max = '1';
|
|
159
|
+
this.#value = coordinate_field(fields, 'value', 'Value');
|
|
160
|
+
|
|
161
|
+
const tangentLabel = document.createElement('label');
|
|
162
|
+
tangentLabel.classList.add('ramp-field', 'field-tangents');
|
|
163
|
+
tangentLabel.appendChild(document.createTextNode('Tangents'));
|
|
138
164
|
|
|
139
165
|
const tangents = this.#tangents = document.createElement('select');
|
|
140
166
|
|
|
@@ -145,12 +171,15 @@ export class ParticleCurveEditorView extends View {
|
|
|
145
171
|
const option = document.createElement('option');
|
|
146
172
|
|
|
147
173
|
option.value = mode;
|
|
148
|
-
option.textContent = label;
|
|
174
|
+
option.textContent = label;
|
|
175
|
+
// Authored slopes are preserved, but this picker cannot manufacture custom tangents.
|
|
176
|
+
option.disabled = mode === TANGENT_MODES.Custom;
|
|
149
177
|
|
|
150
178
|
tangents.appendChild(option);
|
|
151
179
|
}
|
|
152
180
|
|
|
153
|
-
|
|
181
|
+
tangentLabel.appendChild(tangents);
|
|
182
|
+
fields.appendChild(tangentLabel);
|
|
154
183
|
|
|
155
184
|
const remove = document.createElement('button');
|
|
156
185
|
|
|
@@ -164,7 +193,27 @@ export class ParticleCurveEditorView extends View {
|
|
|
164
193
|
canvas.addEventListener('pointerdown', event => this.#press(event));
|
|
165
194
|
canvas.addEventListener('pointermove', event => this.#move(event));
|
|
166
195
|
canvas.addEventListener('pointerup', event => this.#release(event));
|
|
167
|
-
canvas.addEventListener('pointercancel', event => this.#
|
|
196
|
+
canvas.addEventListener('pointercancel', event => this.#cancel(event));
|
|
197
|
+
canvas.addEventListener('lostpointercapture', event => this.#cancel(event));
|
|
198
|
+
canvas.addEventListener('keydown', event => {
|
|
199
|
+
if (event.key === 'Escape' && this.#pointerId !== null) {
|
|
200
|
+
event.stopPropagation();
|
|
201
|
+
this.refresh();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
this.on.unlinked.add(() => this.refresh());
|
|
205
|
+
|
|
206
|
+
for (const input of [this.#time, this.#value]) {
|
|
207
|
+
input.addEventListener('change', () => {
|
|
208
|
+
const key = this.#curve.keys[this.#selected];
|
|
209
|
+
if (key === undefined) return;
|
|
210
|
+
const time = input === this.#time ? Number.parseFloat(input.value) : key.time;
|
|
211
|
+
const value = input === this.#value ? Number.parseFloat(input.value) : key.value;
|
|
212
|
+
this.#moveSelected(Number.isFinite(time) ? clamp01(time) : key.time,
|
|
213
|
+
Number.isFinite(value) ? value : key.value);
|
|
214
|
+
this.#commit();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
168
217
|
|
|
169
218
|
tangents.addEventListener('change', () => {
|
|
170
219
|
const key = this.#curve.keys[this.#selected];
|
|
@@ -176,7 +225,7 @@ export class ParticleCurveEditorView extends View {
|
|
|
176
225
|
this.#modes.set(key, tangents.value);
|
|
177
226
|
this.#applyTangents(this.#selected, tangents.value);
|
|
178
227
|
|
|
179
|
-
this.#commit(
|
|
228
|
+
this.#commit();
|
|
180
229
|
this.render();
|
|
181
230
|
});
|
|
182
231
|
|
|
@@ -196,8 +245,10 @@ export class ParticleCurveEditorView extends View {
|
|
|
196
245
|
/**
|
|
197
246
|
* Re-read the parameter and redraw.
|
|
198
247
|
*/
|
|
199
|
-
refresh() {
|
|
200
|
-
|
|
248
|
+
refresh() {
|
|
249
|
+
if (this.#writing) return;
|
|
250
|
+
this.#endDrag();
|
|
251
|
+
const value = this.#read();
|
|
201
252
|
|
|
202
253
|
this.#curve = particle_curve_animation(
|
|
203
254
|
value !== null && typeof value === "object" && Array.isArray(value.keys) ? value : { keys: [] }
|
|
@@ -209,25 +260,36 @@ export class ParticleCurveEditorView extends View {
|
|
|
209
260
|
this.#curve.keys.forEach((key, index) => this.#modes.set(key, this.#modeOf(index)));
|
|
210
261
|
|
|
211
262
|
this.#writeFields();
|
|
212
|
-
this.render();
|
|
213
|
-
}
|
|
263
|
+
this.render();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#commit() {
|
|
267
|
+
const value = particle_curve_from_animation(this.#curve);
|
|
268
|
+
if (JSON.stringify(value) === JSON.stringify(this.#read())) return;
|
|
269
|
+
this.#writing = true;
|
|
270
|
+
try {
|
|
271
|
+
this.#write(value);
|
|
272
|
+
} finally {
|
|
273
|
+
this.#writing = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
214
276
|
|
|
215
277
|
/**
|
|
216
278
|
* @param {PointerEvent} event
|
|
217
|
-
* @returns {{time: number, value: number}}
|
|
279
|
+
* @returns {{time: number, value: number}} the pointer in curve coordinates
|
|
218
280
|
*/
|
|
219
281
|
#pointOf(event) {
|
|
220
282
|
const rect = this.#canvas.getBoundingClientRect();
|
|
221
283
|
|
|
222
284
|
const scale = rect.width / WIDTH;
|
|
223
285
|
|
|
224
|
-
const x = (event.clientX - rect.left) / scale -
|
|
225
|
-
const y = (event.clientY - rect.top) / (rect.height / HEIGHT) -
|
|
286
|
+
const x = (event.clientX - rect.left) / scale - PLOT.left;
|
|
287
|
+
const y = (event.clientY - rect.top) / (rect.height / HEIGHT) - PLOT.top;
|
|
226
288
|
|
|
227
289
|
return {
|
|
228
|
-
time: clamp01(x / (
|
|
290
|
+
time: clamp01(x / (PLOT.right - PLOT.left)),
|
|
229
291
|
// the plot runs upward and the canvas runs downward
|
|
230
|
-
value:
|
|
292
|
+
value: this.#range.min + (1 - y / (PLOT.bottom - PLOT.top)) * (this.#range.max - this.#range.min),
|
|
231
293
|
};
|
|
232
294
|
}
|
|
233
295
|
|
|
@@ -238,8 +300,8 @@ export class ParticleCurveEditorView extends View {
|
|
|
238
300
|
*/
|
|
239
301
|
#plot(time, value) {
|
|
240
302
|
return {
|
|
241
|
-
x:
|
|
242
|
-
y:
|
|
303
|
+
x: PLOT.left + time * (PLOT.right - PLOT.left),
|
|
304
|
+
y: PLOT.top + (this.#range.max - value) / (this.#range.max - this.#range.min) * (PLOT.bottom - PLOT.top),
|
|
243
305
|
};
|
|
244
306
|
}
|
|
245
307
|
|
|
@@ -263,7 +325,7 @@ export class ParticleCurveEditorView extends View {
|
|
|
263
325
|
|
|
264
326
|
const away = Math.hypot(point.x - x, point.y - y);
|
|
265
327
|
|
|
266
|
-
if (away <= GRAB_RADIUS && away < distance) {
|
|
328
|
+
if (away <= GRAB_RADIUS && (away < distance || (away === distance && index === this.#selected))) {
|
|
267
329
|
nearest = index;
|
|
268
330
|
distance = away;
|
|
269
331
|
}
|
|
@@ -275,8 +337,9 @@ export class ParticleCurveEditorView extends View {
|
|
|
275
337
|
/**
|
|
276
338
|
* @param {PointerEvent} event
|
|
277
339
|
*/
|
|
278
|
-
#press(event) {
|
|
279
|
-
|
|
340
|
+
#press(event) {
|
|
341
|
+
if (this.#pointerId !== null) return;
|
|
342
|
+
const key = this.#keyUnder(event);
|
|
280
343
|
|
|
281
344
|
if (event.button === 1) {
|
|
282
345
|
if (key !== -1) {
|
|
@@ -294,20 +357,20 @@ export class ParticleCurveEditorView extends View {
|
|
|
294
357
|
|
|
295
358
|
if (key !== -1) {
|
|
296
359
|
this.#selected = key;
|
|
297
|
-
} else {
|
|
298
|
-
const { time } = this.#pointOf(event);
|
|
299
|
-
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
this.#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.#
|
|
360
|
+
} else {
|
|
361
|
+
const { time } = this.#pointOf(event);
|
|
362
|
+
|
|
363
|
+
// Labels and gutters are not an insertion surface.
|
|
364
|
+
const rect = this.#canvas.getBoundingClientRect();
|
|
365
|
+
const x = (event.clientX - rect.left) / rect.width * WIDTH;
|
|
366
|
+
const y = (event.clientY - rect.top) / rect.height * HEIGHT;
|
|
367
|
+
if (x < PLOT.left || x > PLOT.right || y < PLOT.top || y > PLOT.bottom) return;
|
|
368
|
+
this.#insertAt(time);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
this.#canvas.focus({ preventScroll: true });
|
|
373
|
+
this.#pointerId = event.pointerId ?? 0;
|
|
311
374
|
|
|
312
375
|
if (typeof this.#canvas.setPointerCapture === "function") {
|
|
313
376
|
this.#canvas.setPointerCapture(event.pointerId);
|
|
@@ -320,18 +383,24 @@ export class ParticleCurveEditorView extends View {
|
|
|
320
383
|
/**
|
|
321
384
|
* @param {PointerEvent} event
|
|
322
385
|
*/
|
|
323
|
-
#move(event) {
|
|
324
|
-
if (
|
|
386
|
+
#move(event) {
|
|
387
|
+
if (this.#pointerId !== (event.pointerId ?? 0)) {
|
|
325
388
|
return;
|
|
326
389
|
}
|
|
327
390
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
391
|
+
const { time, value } = this.#pointOf(event);
|
|
392
|
+
this.#moveSelected(time, value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** @param {number} time @param {number} value */
|
|
396
|
+
#moveSelected(time, value) {
|
|
397
|
+
const key = this.#curve.keys[this.#selected];
|
|
398
|
+
if (key === undefined) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Crossing several keys changes neighbours at BOTH the old and new locations.
|
|
403
|
+
const affected = new Set(this.#curve.keys.slice(Math.max(0, this.#selected - 1), this.#selected + 2));
|
|
335
404
|
|
|
336
405
|
key.time = time;
|
|
337
406
|
key.value = value;
|
|
@@ -339,35 +408,73 @@ export class ParticleCurveEditorView extends View {
|
|
|
339
408
|
// The curve keeps its keys in time order and this one may have crossed a neighbour. Sorting
|
|
340
409
|
// by hand rather than removing and re-adding, so that the key object — and the selection
|
|
341
410
|
// pointing at it — survive the move.
|
|
342
|
-
this.#selected = resort(this.#curve.keys, this.#selected);
|
|
343
|
-
|
|
344
|
-
this.
|
|
411
|
+
this.#selected = resort(this.#curve.keys, this.#selected);
|
|
412
|
+
|
|
413
|
+
this.#curve.keys.slice(Math.max(0, this.#selected - 1), this.#selected + 2).forEach(other => affected.add(other));
|
|
414
|
+
for (const other of affected) {
|
|
415
|
+
this.#applyTangents(this.#curve.keys.indexOf(other), this.#modes.get(other) ?? TANGENT_MODES.Custom);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.#writeFields();
|
|
419
|
+
this.render();
|
|
345
420
|
}
|
|
346
421
|
|
|
347
422
|
/**
|
|
348
423
|
* @param {PointerEvent} event
|
|
349
424
|
*/
|
|
350
425
|
#release(event) {
|
|
351
|
-
if (
|
|
426
|
+
if (this.#pointerId !== (event.pointerId ?? 0)) {
|
|
352
427
|
return;
|
|
353
428
|
}
|
|
354
429
|
|
|
355
|
-
this.#
|
|
356
|
-
|
|
357
|
-
if (typeof this.#canvas.releasePointerCapture === "function" && this.#canvas.hasPointerCapture?.(event.pointerId)) {
|
|
358
|
-
this.#canvas.releasePointerCapture(event.pointerId);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// A key that moved changed the slope on both sides of it, so whichever rule its neighbours
|
|
362
|
-
// were following has to be applied again — otherwise a "Linear" segment stops being straight
|
|
363
|
-
// the moment either of its ends is dragged.
|
|
364
|
-
this.#reapplyAround(this.#selected);
|
|
365
|
-
|
|
366
|
-
this.#commit(particle_curve_from_animation(this.#curve));
|
|
430
|
+
this.#endDrag();
|
|
431
|
+
this.#commit();
|
|
367
432
|
|
|
368
433
|
this.#writeFields();
|
|
369
|
-
this.render();
|
|
370
|
-
}
|
|
434
|
+
this.render();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#endDrag() {
|
|
438
|
+
const pointerId = this.#pointerId;
|
|
439
|
+
this.#pointerId = null;
|
|
440
|
+
if (pointerId !== null && this.#canvas.hasPointerCapture?.(pointerId)) {
|
|
441
|
+
this.#canvas.releasePointerCapture(pointerId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @param {PointerEvent} event */
|
|
446
|
+
#cancel(event) {
|
|
447
|
+
if (this.#pointerId === (event.pointerId ?? 0)) this.refresh();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Split a Hermite segment using its exact derivative, preserving even custom curves.
|
|
451
|
+
* @param {number} time
|
|
452
|
+
*/
|
|
453
|
+
#insertAt(time) {
|
|
454
|
+
const keys = this.#curve.keys;
|
|
455
|
+
const existing = keys.findIndex(key => Math.abs(key.time - time) < 1e-9);
|
|
456
|
+
if (existing !== -1) {
|
|
457
|
+
this.#selected = existing;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const previous = keys[this.#curve.getKeyIndexLow(time)];
|
|
461
|
+
const next = keys[this.#curve.getKeyIndexLow(time) + 1];
|
|
462
|
+
let slope = 0;
|
|
463
|
+
if (previous !== undefined && next !== undefined && time > previous.time && time < next.time) {
|
|
464
|
+
const span = next.time - previous.time;
|
|
465
|
+
const u = (time - previous.time) / span;
|
|
466
|
+
const rise = next.value - previous.value;
|
|
467
|
+
const m0 = previous.outTangent * span;
|
|
468
|
+
const m1 = next.inTangent * span;
|
|
469
|
+
slope = ((3 * (m0 + m1 - 2 * rise) * u + 2 * (3 * rise - 2 * m0 - m1)) * u + m0) / span;
|
|
470
|
+
} else if (keys.length > 0) {
|
|
471
|
+
// The engine holds the endpoint value outside the keyed interval.
|
|
472
|
+
if (time < keys[0].time) keys[0].inTangent = 0;
|
|
473
|
+
if (time > keys[keys.length - 1].time) keys[keys.length - 1].outTangent = 0;
|
|
474
|
+
}
|
|
475
|
+
this.#selected = this.#curve.add(Keyframe.from(time, this.#curve.evaluate(time), slope, slope));
|
|
476
|
+
this.#modes.set(keys[this.#selected], this.#modeOf(this.#selected));
|
|
477
|
+
}
|
|
371
478
|
|
|
372
479
|
#removeSelected() {
|
|
373
480
|
// A curve with one key is a constant, which is a thing to be; a curve with none is nothing,
|
|
@@ -382,7 +489,7 @@ export class ParticleCurveEditorView extends View {
|
|
|
382
489
|
|
|
383
490
|
this.#reapplyAround(this.#selected);
|
|
384
491
|
|
|
385
|
-
this.#commit(
|
|
492
|
+
this.#commit();
|
|
386
493
|
this.#writeFields();
|
|
387
494
|
this.render();
|
|
388
495
|
}
|
|
@@ -401,7 +508,7 @@ export class ParticleCurveEditorView extends View {
|
|
|
401
508
|
const key = this.#curve.keys[neighbour];
|
|
402
509
|
|
|
403
510
|
if (key !== undefined) {
|
|
404
|
-
this.#applyTangents(neighbour, this.#modes.get(key) ?? TANGENT_MODES.
|
|
511
|
+
this.#applyTangents(neighbour, this.#modes.get(key) ?? TANGENT_MODES.Custom);
|
|
405
512
|
}
|
|
406
513
|
}
|
|
407
514
|
}
|
|
@@ -415,9 +522,11 @@ export class ParticleCurveEditorView extends View {
|
|
|
415
522
|
|
|
416
523
|
const key = keys[index];
|
|
417
524
|
|
|
418
|
-
if (key === undefined) {
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
525
|
+
if (key === undefined) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (mode === TANGENT_MODES.Custom) return;
|
|
421
530
|
|
|
422
531
|
if (mode === TANGENT_MODES.Flat) {
|
|
423
532
|
key.inTangent = 0;
|
|
@@ -449,8 +558,8 @@ export class ParticleCurveEditorView extends View {
|
|
|
449
558
|
/**
|
|
450
559
|
* Which rule a key's tangents look like they came from. There is nowhere to *store* the answer —
|
|
451
560
|
* a curve is keys and tangents, and adding a mode to it would be inventing a format the rest of
|
|
452
|
-
* the engine does not read — so it is recovered from the numbers
|
|
453
|
-
*
|
|
561
|
+
* the engine does not read — so it is recovered from the numbers for the preset rules.
|
|
562
|
+
* Slopes that do not match a rule stay Custom until explicitly changed.
|
|
454
563
|
*
|
|
455
564
|
* @param {number} index
|
|
456
565
|
* @returns {string} a {@link TANGENT_MODES}
|
|
@@ -471,19 +580,28 @@ export class ParticleCurveEditorView extends View {
|
|
|
471
580
|
linear.keys = this.#curve.keys.map(other => other.clone());
|
|
472
581
|
linear.alignTangents(index);
|
|
473
582
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
583
|
+
const close = (a, b) => Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
584
|
+
if (close(linear.keys[index].inTangent, key.inTangent) && close(linear.keys[index].outTangent, key.outTangent)) {
|
|
585
|
+
return TANGENT_MODES.Linear;
|
|
586
|
+
}
|
|
587
|
+
const previous = this.#curve.keys[index - 1] ?? key;
|
|
588
|
+
const next = this.#curve.keys[index + 1] ?? key;
|
|
589
|
+
const slope = next.time === previous.time ? 0 : (next.value - previous.value) / (next.time - previous.time);
|
|
590
|
+
return close(slope, key.inTangent) && close(slope, key.outTangent) ? TANGENT_MODES.Smooth : TANGENT_MODES.Custom;
|
|
477
591
|
}
|
|
478
592
|
|
|
479
593
|
#writeFields() {
|
|
480
594
|
const key = this.#curve.keys[this.#selected];
|
|
481
595
|
|
|
482
|
-
this.#tangents.value = (key === undefined ? undefined : this.#modes.get(key)) ?? TANGENT_MODES.Smooth;
|
|
596
|
+
this.#tangents.value = (key === undefined ? undefined : this.#modes.get(key)) ?? TANGENT_MODES.Smooth;
|
|
597
|
+
this.#time.value = key === undefined ? '' : String(Number(key.time.toFixed(6)));
|
|
598
|
+
this.#value.value = key === undefined ? '' : String(key.value);
|
|
599
|
+
for (const field of [this.#time, this.#value, this.#tangents]) field.disabled = key === undefined;
|
|
483
600
|
}
|
|
484
601
|
|
|
485
|
-
render() {
|
|
486
|
-
|
|
602
|
+
render() {
|
|
603
|
+
if (this.#pointerId === null) this.#range = value_range(this.#curve);
|
|
604
|
+
const context = this.#context;
|
|
487
605
|
|
|
488
606
|
if (context === null) {
|
|
489
607
|
return;
|
|
@@ -496,45 +614,62 @@ export class ParticleCurveEditorView extends View {
|
|
|
496
614
|
context.fillStyle = '#171b21';
|
|
497
615
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
498
616
|
|
|
499
|
-
//
|
|
617
|
+
// Numeric values on Y, normalized time on X. Both use the same mapping as the handles.
|
|
500
618
|
context.strokeStyle = '#2c323b';
|
|
501
619
|
context.lineWidth = 1;
|
|
502
620
|
context.beginPath();
|
|
503
621
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
context.moveTo(
|
|
512
|
-
context.lineTo(
|
|
622
|
+
context.font = '10px sans-serif';
|
|
623
|
+
context.fillStyle = '#9da0a3';
|
|
624
|
+
const { min, max, step } = this.#range;
|
|
625
|
+
const ticks = Math.round((max - min) / step);
|
|
626
|
+
for (let i = 0; i <= ticks; i++) {
|
|
627
|
+
const value = min + i * step;
|
|
628
|
+
const horizontal = this.#plot(0, value);
|
|
629
|
+
context.moveTo(PLOT.left, horizontal.y + 0.5);
|
|
630
|
+
context.lineTo(PLOT.right, horizontal.y + 0.5);
|
|
631
|
+
context.textAlign = 'right';
|
|
632
|
+
context.fillText(String(Number(value.toPrecision(12))), PLOT.left - 9, horizontal.y + 3, PLOT.left - 12);
|
|
633
|
+
}
|
|
634
|
+
for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
|
|
635
|
+
const vertical = this.#plot(fraction, min);
|
|
636
|
+
context.moveTo(vertical.x + 0.5, PLOT.top);
|
|
637
|
+
context.lineTo(vertical.x + 0.5, PLOT.bottom);
|
|
638
|
+
context.textAlign = 'center';
|
|
639
|
+
context.fillText(String(fraction), vertical.x, PLOT.bottom + 15);
|
|
513
640
|
}
|
|
514
641
|
|
|
515
|
-
context.stroke();
|
|
642
|
+
context.stroke();
|
|
516
643
|
|
|
517
644
|
// The curve itself, asked of the AnimationCurve a pixel at a time — the same evaluation the
|
|
518
645
|
// rest of the engine does, so what is drawn is what the curve is.
|
|
519
|
-
context.strokeStyle = '#7fb5ff';
|
|
520
|
-
context.lineWidth = 1.5;
|
|
521
|
-
context.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
646
|
+
context.strokeStyle = '#7fb5ff';
|
|
647
|
+
context.lineWidth = 1.5;
|
|
648
|
+
context.save();
|
|
649
|
+
context.beginPath();
|
|
650
|
+
context.rect(PLOT.left, PLOT.top, PLOT.right - PLOT.left, PLOT.bottom - PLOT.top);
|
|
651
|
+
context.clip();
|
|
652
|
+
context.beginPath();
|
|
653
|
+
|
|
654
|
+
for (let x = PLOT.left; x <= PLOT.right; x++) {
|
|
655
|
+
const time = (x - PLOT.left) / (PLOT.right - PLOT.left);
|
|
656
|
+
|
|
657
|
+
const value = this.#curve.evaluate(time);
|
|
658
|
+
const point = this.#plot(time, value);
|
|
659
|
+
|
|
660
|
+
if (x === PLOT.left) {
|
|
529
661
|
context.moveTo(point.x, point.y);
|
|
530
662
|
} else {
|
|
531
663
|
context.lineTo(point.x, point.y);
|
|
532
664
|
}
|
|
533
665
|
}
|
|
534
666
|
|
|
535
|
-
context.stroke();
|
|
536
|
-
|
|
537
|
-
|
|
667
|
+
context.stroke();
|
|
668
|
+
context.restore();
|
|
669
|
+
|
|
670
|
+
const ordered = this.#curve.keys.map((key, index) => ({ key, index }))
|
|
671
|
+
.sort((a, b) => Number(a.index === this.#selected) - Number(b.index === this.#selected));
|
|
672
|
+
ordered.forEach(({ key, index }) => {
|
|
538
673
|
const point = this.#plot(key.time, key.value);
|
|
539
674
|
|
|
540
675
|
context.beginPath();
|
|
@@ -548,7 +683,55 @@ export class ParticleCurveEditorView extends View {
|
|
|
548
683
|
context.stroke();
|
|
549
684
|
});
|
|
550
685
|
}
|
|
551
|
-
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** @param {HTMLElement} parent @param {string} name @param {string} text
|
|
689
|
+
* @returns {HTMLInputElement}
|
|
690
|
+
*/
|
|
691
|
+
function coordinate_field(parent, name, text) {
|
|
692
|
+
const label = document.createElement('label');
|
|
693
|
+
label.classList.add('ramp-field', `field-${name}`);
|
|
694
|
+
label.appendChild(document.createTextNode(text));
|
|
695
|
+
const input = document.createElement('input');
|
|
696
|
+
input.type = 'number';
|
|
697
|
+
input.classList.add(name);
|
|
698
|
+
input.step = 'any';
|
|
699
|
+
label.appendChild(input);
|
|
700
|
+
parent.appendChild(label);
|
|
701
|
+
return input;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Fit rounded value ticks to the entire spline, rather than just its keys or pixel samples.
|
|
705
|
+
* @param {AnimationCurve} curve
|
|
706
|
+
* @returns {{min: number, max: number, step: number}}
|
|
707
|
+
*/
|
|
708
|
+
function value_range(curve) {
|
|
709
|
+
const keys = curve.keys;
|
|
710
|
+
if (keys.length === 0) return { min: 0, max: 1, step: 0.25 };
|
|
711
|
+
|
|
712
|
+
let min = keys[0].value;
|
|
713
|
+
let max = min;
|
|
714
|
+
const bounds = [0, 0];
|
|
715
|
+
for (let i = 1; i < keys.length; i++) {
|
|
716
|
+
const previous = keys[i - 1];
|
|
717
|
+
const key = keys[i];
|
|
718
|
+
const span = key.time - previous.time;
|
|
719
|
+
spline3_hermite_bounds(bounds, 0, 1, previous.value, key.value,
|
|
720
|
+
previous.outTangent * span, key.inTangent * span);
|
|
721
|
+
min = Math.min(min, bounds[0]);
|
|
722
|
+
max = Math.max(max, bounds[1]);
|
|
723
|
+
}
|
|
724
|
+
if (min === max) {
|
|
725
|
+
const padding = min === 0 ? 0.5 : Math.abs(min) * 0.1;
|
|
726
|
+
min -= padding;
|
|
727
|
+
max += padding;
|
|
728
|
+
}
|
|
729
|
+
const roughStep = (max - min) / 4;
|
|
730
|
+
const magnitude = 10 ** Math.floor(Math.log10(roughStep));
|
|
731
|
+
const multiple = [1, 2, 2.5, 5, 10].find(value => value * magnitude >= roughStep) ?? 10;
|
|
732
|
+
const step = multiple * magnitude;
|
|
733
|
+
return { min: Math.floor(min / step) * step, max: Math.ceil(max / step) * step, step };
|
|
734
|
+
}
|
|
552
735
|
|
|
553
736
|
/**
|
|
554
737
|
* Move one element of an already-sorted-but-for-it array back into place, and say where it went.
|