physics-animator 0.2.0 → 0.9.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.
@@ -1,8 +1,44 @@
1
- /**
2
- * Spring
3
- *
4
- * @author George Corney (haxiomic)
5
- */
1
+ import { StepResult } from "../IFieldAnimator.js";
2
+ const defaultSpringParameters = {
3
+ duration_s: 0.5,
4
+ };
5
+ export const SpringAnimator = {
6
+ createState(obj, field, target, params) {
7
+ return {
8
+ x: obj[field],
9
+ targetX: target,
10
+ v: 0,
11
+ physicsParameters: Spring.getPhysicsParameters(params ?? defaultSpringParameters),
12
+ };
13
+ },
14
+ updateState(state, object, field, target, params) {
15
+ state.x = object[field];
16
+ state.targetX = target;
17
+ state.physicsParameters = Spring.getPhysicsParameters(params ?? defaultSpringParameters);
18
+ },
19
+ step(state, object, field, params, dt_s) {
20
+ let physicsParameters = state.physicsParameters;
21
+ // step the spring
22
+ if (physicsParameters != null && isFinite(physicsParameters.strength) && isFinite(physicsParameters.damping)) {
23
+ Spring.stepSpring(dt_s, state, physicsParameters);
24
+ }
25
+ else {
26
+ // instant transition: set to the target
27
+ state.x = state.targetX;
28
+ state.v = 0;
29
+ }
30
+ // update the object
31
+ object[field] = state.x;
32
+ // complete the animation if it's close enough to the target and velocity is close to 0
33
+ if (Math.abs(state.x - state.targetX) < 0.0001 && Math.abs(state.v) < 0.0001) {
34
+ object[field] = state.targetX;
35
+ return StepResult.Complete;
36
+ }
37
+ else {
38
+ return StepResult.Continue;
39
+ }
40
+ }
41
+ };
6
42
  export var Spring;
7
43
  (function (Spring) {
8
44
  /**
@@ -13,7 +49,7 @@ export var Spring;
13
49
  * `strength = damping * damping / 4`
14
50
  */
15
51
  function Exponential(options) {
16
- // solved numerically
52
+ // found numerically
17
53
  const halfLifeConstant = 3.356694; // from solve (1+u)*exp(-u)=0.5 for u, and constant = 2u
18
54
  const pointOnePercentConstant = 18.46682; // from solve (1+u)*exp(-u)=0.001 for u, and constant = 2u
19
55
  const damping = pointOnePercentConstant / options.duration_s;
@@ -26,11 +62,12 @@ export var Spring;
26
62
  // -2ln(0.001) = b t
27
63
  const durationTarget = 0.001; // 0.1% of target
28
64
  let damping = -2 * Math.log(durationTarget) / duration_s;
29
- // 4k - b^2 > 0
30
- let bSq = damping * damping;
31
- const criticalStrength = bSq / 4;
32
- let strength = criticalStrength + (bounce * bounce + 1);
33
- return { damping, strength };
65
+ // see https://www.desmos.com/calculator/h43ylohte7
66
+ const strength = 0.25 * (((2 * bounce * Math.PI) / duration_s) ** 2 + damping ** 2);
67
+ return {
68
+ damping,
69
+ strength,
70
+ };
34
71
  }
35
72
  Spring.Underdamped = Underdamped;
36
73
  function getPhysicsParameters(parameters) {
@@ -53,6 +90,8 @@ export var Spring;
53
90
  * @param dt_s
54
91
  * @param state
55
92
  * @param parameters
93
+ *
94
+ * If parameters are NaN or infinite, the spring will skip to the target
56
95
  */
57
96
  function stepSpring(dt_s, state, parameters) {
58
97
  // analytic integration (unconditionally stable)
@@ -71,6 +110,12 @@ export var Spring;
71
110
  return;
72
111
  if (dt_s === 0)
73
112
  return;
113
+ if (!isFinite(k) || !isFinite(b) || !isFinite(v0) || !isFinite(dx0)) {
114
+ // skip to target
115
+ state.x = state.targetX;
116
+ state.v = 0;
117
+ return 0; // no energy
118
+ }
74
119
  let critical = k * 4 - b * b;
75
120
  if (critical > 0) {
76
121
  // under damped
@@ -0,0 +1,68 @@
1
+ import { StepResult } from "../IFieldAnimator.js";
2
+ const defaultParams = {
3
+ duration_s: 0.5, // default duration of the tween in seconds
4
+ easingFn: linearStep, // default easing function is linear
5
+ };
6
+ export const TweenAnimator = {
7
+ createState(obj, field, target, params) {
8
+ return {
9
+ x0: obj[field], // initial value of the field
10
+ t0_ms: performance.now(), // time when the tween started in milliseconds
11
+ target: target,
12
+ velocity: 0,
13
+ ...(params ?? defaultParams),
14
+ };
15
+ },
16
+ updateState(state, object, field, target, params) {
17
+ state.target = target;
18
+ state.x0 = object[field]; // update the initial value of the field
19
+ state.t0_ms = performance.now(); // reset the start time of the tween
20
+ state.duration_s = params?.duration_s ?? defaultParams.duration_s; // update the duration of the tween
21
+ state.easingFn = params?.easingFn ?? defaultParams.easingFn; //
22
+ },
23
+ step(state, object, field, params, dt_s) {
24
+ // step the tween
25
+ let x = object[field];
26
+ params.easingFn(object, field, state.target, state, dt_s);
27
+ let x_new = object[field];
28
+ state.velocity = (x_new - x) / dt_s;
29
+ // remove the tween if it's complete
30
+ let deltaTime_s = (performance.now() - state.t0_ms) / 1000;
31
+ if (deltaTime_s >= state.duration_s) {
32
+ object[field] = state.target;
33
+ return StepResult.Complete;
34
+ }
35
+ else {
36
+ return StepResult.Continue;
37
+ }
38
+ }
39
+ };
40
+ export function linearStep(object, field, target, params, dt_s) {
41
+ let dx = target - params.x0;
42
+ let t = (performance.now() - params.t0_ms) / 1000;
43
+ let u = t / params.duration_s;
44
+ let x_new = params.x0 + dx * u;
45
+ object[field] = x_new;
46
+ }
47
+ // cubic ease in out
48
+ export function easeInOutStep(object, field, target, params, dt_s) {
49
+ let dx = target - params.x0;
50
+ let t = (performance.now() - params.t0_ms) / 1000;
51
+ let u = t / params.duration_s;
52
+ let x_new = params.x0 + dx * u * u * (3 - 2 * u);
53
+ object[field] = x_new;
54
+ }
55
+ export function easeInStep(object, field, target, params, dt_s) {
56
+ let dx = target - params.x0;
57
+ let t = (performance.now() - params.t0_ms) / 1000;
58
+ let u = t / params.duration_s;
59
+ let x_new = params.x0 + dx * u * u * u;
60
+ object[field] = x_new;
61
+ }
62
+ export function easeOutStep(object, field, target, params, dt_s) {
63
+ let dx = target - params.x0;
64
+ let t = (performance.now() - params.t0_ms) / 1000;
65
+ let u = t / params.duration_s;
66
+ let x_new = params.x0 + dx * (1 - Math.pow(1 - u, 3));
67
+ object[field] = x_new;
68
+ }
package/dist/esm/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export * from './AnimationSequencer.js';
2
2
  export * from './Animator.js';
3
- export * from './Spring.js';
3
+ export * from './IFieldAnimator.js';
@@ -1,6 +1,6 @@
1
- import { useRef } from "react";
2
- import { useAnimator } from "./useAnimator.js";
3
1
  import { useInitializer } from "use-initializer";
2
+ import { useAnimator } from "./useAnimator.js";
3
+ import { useEffect } from "react";
4
4
  /**
5
5
  * A value that animates to a target value using a spring animation.
6
6
  * This will **not** cause a re-render when the value changes.
@@ -10,47 +10,17 @@ import { useInitializer } from "use-initializer";
10
10
  export function useSpringValue(options, onChange) {
11
11
  const animator = useAnimator(options.animator);
12
12
  const springValue = useInitializer(() => {
13
- let value = structuredClone(options.initial);
14
13
  return {
15
- get value() {
16
- return value;
17
- },
18
- set value(newValue) {
19
- value = newValue;
20
- onChange(value);
21
- },
14
+ value: structuredClone(options.initial),
22
15
  };
23
16
  });
24
- const afterStepListener = useRef(null);
25
- switch (typeof options.initial) {
26
- case 'number':
27
- {
28
- animator.springTo(springValue, 'value', options.target, options);
29
- }
30
- break;
31
- default:
32
- {
33
- if (Array.isArray(options.initial)) {
34
- for (let i = 0; i < options.initial.length; i++) {
35
- animator.springTo(springValue.value, i, options.target[i], options);
36
- }
37
- }
38
- else {
39
- // assume object, iterate over keys
40
- for (const key in options.initial) {
41
- animator.springTo(springValue.value, key, options.target[key], options);
42
- }
43
- }
44
- if (!afterStepListener.current) {
45
- afterStepListener.current = animator.onAfterStep.addListener(() => {
46
- onChange(springValue.value);
47
- });
48
- animator.onAllComplete(springValue.value, () => {
49
- afterStepListener.current?.remove();
50
- afterStepListener.current = null;
51
- }, 'once');
52
- }
53
- }
54
- break;
55
- }
17
+ useEffect(() => {
18
+ let remove = animator.onChange(springValue, () => {
19
+ onChange(springValue.value);
20
+ }).remove;
21
+ return () => {
22
+ remove();
23
+ };
24
+ }, [springValue, onChange]);
25
+ animator.springTo(springValue, { value: options.target }, options);
56
26
  }
@@ -1,72 +1,92 @@
1
1
  import { EventSignal } from "@haxiomic/event-signal";
2
- import { Spring, SpringParameters } from "./Spring.js";
3
- declare enum AnimationType {
4
- Spring = 0,
5
- Tween = 1
6
- }
2
+ import { IFieldAnimator } from "./IFieldAnimator.js";
3
+ import { SpringParameters } from "./animators/SpringAnimator.js";
4
+ import { EasingStepFn } from "./animators/TweenAnimator.js";
7
5
  /**
8
6
  * Physically based animation of numeric properties of objects
9
7
  *
10
8
  * Designed to avoid discontinuities for smooth animation in all conditions
11
9
  */
12
10
  export declare class Animator {
13
- onBeforeStep: EventSignal<{
14
- dt_s: number;
15
- }, {
16
- dt_s: number;
17
- }>;
18
- onAfterStep: EventSignal<{
19
- dt_s: number;
20
- }, {
21
- dt_s: number;
22
- }>;
23
- protected _onAnimationComplete: EventSignal<{
24
- object: any;
25
- field: string | number | symbol;
26
- }, {
27
- object: any;
28
- field: string | number | symbol;
29
- }>;
30
- protected _onObjectAnimationsComplete: EventSignal<{
11
+ animations: Map<any, Map<FieldKey, FieldAnimation<any, any>>>;
12
+ protected readonly events: {
13
+ beforeStep: EventSignal<{
14
+ dt_s: number;
15
+ }, {
16
+ dt_s: number;
17
+ }>;
18
+ afterStep: EventSignal<{
19
+ dt_s: number;
20
+ }, {
21
+ dt_s: number;
22
+ }>;
23
+ completeField: EventSignal<{
24
+ object: any;
25
+ field: FieldKey;
26
+ }, {
27
+ object: any;
28
+ field: FieldKey;
29
+ }>;
30
+ completeObject: EventSignal<{
31
+ object: any;
32
+ }, {
33
+ object: any;
34
+ }>;
35
+ };
36
+ protected changeObjectEvents: Map<any, EventSignal<any, any>>;
37
+ protected changeFieldEvents: Map<any, Map<FieldKey, EventSignal<{
31
38
  object: any;
39
+ field: any;
32
40
  }, {
33
41
  object: any;
34
- }>;
35
- animations: Map<any, Map<string | number | symbol, {
36
- target: number;
37
- type: AnimationType;
38
- springParams: Spring.PhysicsParameters | null;
39
- tweenParams: Tween.Parameters | null;
40
- step: TweenStepFn | null;
41
- velocity: number;
42
- }>>;
42
+ field: any;
43
+ }>>>;
44
+ protected beforeChange: EventSignal<void, void>;
45
+ protected afterChange: EventSignal<void, void>;
43
46
  constructor(onBeforeStep?: (dt_s: number) => void, onAfterStep?: (dt_s: number) => void);
44
- springTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, params?: SpringParameters | null): void;
45
- customTweenTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number, step: TweenStepFn): void;
46
- linearTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
47
- easeInOutTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
48
- easeInTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
49
- easeOutTo<Obj, Name extends keyof Obj>(object: Obj, field: Name, target: Obj[Name] & number, duration_s: number): void;
50
- /**
51
- * Remove animation from the object and set the field to the target value
52
- */
53
- setTo<Obj, Name extends keyof Obj, T extends Obj[Name]>(object: Obj, field: Name, target: T): void;
54
- onComplete<Obj, Name extends keyof Obj>(object: Obj, field: Name, callback: (object: Obj, field: Name) => void): {
47
+ setTo<Obj>(object: Obj, target: Partial<Obj>): void;
48
+ animateTo<Obj, Parameters, State, FieldType>(object: Obj, target: Partial<Obj>, animator?: IFieldAnimator<Parameters, State, FieldType>, params?: Parameters | null): void;
49
+ springTo<Obj>(object: Obj, target: Partial<Obj>, params: SpringParameters | null): void;
50
+ customTweenTo<Obj>(object: Obj, target: Partial<Obj>, duration_s: number, easingFn: EasingStepFn): void;
51
+ linearTo<Obj>(object: Obj, target: Partial<Obj>, duration_s: number): void;
52
+ easeInOutTo<Obj>(object: Obj, target: Partial<Obj>, duration_s: number): void;
53
+ easeInTo<Obj>(object: Obj, target: Partial<Obj>, duration_s: number): void;
54
+ easeOutTo<Obj>(object: Obj, target: Partial<Obj>, duration_s: number): void;
55
+ onCompleteField<Obj, Name extends keyof Obj>(object: Obj, field: Name, callback: (object: Obj, field: Name) => void, once?: 'once'): {
55
56
  priority: number;
56
57
  listener: (event: {
57
58
  object: any;
58
- field: string | number | symbol;
59
+ field: FieldKey;
59
60
  }) => void;
60
61
  remove: () => void;
61
62
  };
62
- onAllComplete<Obj>(object: Obj, callback: (object: Obj) => void, once?: 'once'): {
63
+ onComplete<Obj>(object: Obj, callback: (object: Obj) => void, once?: 'once'): {
63
64
  priority: number;
64
65
  listener: (event: {
65
66
  object: any;
66
67
  }) => void;
67
68
  remove: () => void;
68
69
  };
69
- private _springState;
70
+ onChangeField<Obj, Name extends keyof Obj>(object: Obj, field: Name, callback: (object: Obj, field: Name) => void): {
71
+ remove: () => void;
72
+ };
73
+ onChange<Obj>(object: Obj, callback: (object: Obj) => void): {
74
+ remove: () => void;
75
+ };
76
+ onBeforeStep(callback: (dt_s: number) => void): {
77
+ priority: number;
78
+ listener: (event: {
79
+ dt_s: number;
80
+ }) => void;
81
+ remove: () => void;
82
+ };
83
+ onAfterStep(callback: (dt_s: number) => void): {
84
+ priority: number;
85
+ listener: (event: {
86
+ dt_s: number;
87
+ }) => void;
88
+ remove: () => void;
89
+ };
70
90
  step(dt_s: number): void;
71
91
  private t_last;
72
92
  tick(): number;
@@ -91,16 +111,27 @@ export declare class Animator {
91
111
  * Remove animation for this object and field if it exists
92
112
  * Does not change the value of the field
93
113
  */
94
- remove<T>(object: T, field: keyof T): void;
114
+ remove<T>(object: T, field: keyof T, dispatchComplete?: boolean): void;
95
115
  /**
96
116
  * Remove all animations for this object
97
117
  */
98
- removeObject(object: any): void;
118
+ removeObject(object: any, dispatchComplete?: boolean): void;
99
119
  /**
100
120
  * Remove all animations
101
121
  */
102
- removeAll(): void;
103
- getVelocity<Obj, Name extends keyof Obj>(object: Obj, field: Name): number;
122
+ removeAll(dispatchComplete?: boolean): void;
123
+ getState<Obj, Name extends keyof Obj>(object: Obj, field: Name): any;
124
+ protected dispatchChangeObjectEvent(object: any): void;
125
+ protected dispatchChangeFieldEvent<Obj, Name extends keyof Obj>(object: Obj, field: Name): void;
126
+ protected addChangeFieldListener<Obj, Name extends keyof Obj>(object: Obj, field: Name, callback: (object: Obj, field: Name) => void): {
127
+ remove: () => void;
128
+ };
129
+ /**
130
+ * Remove animation from the object and set the field to the target value
131
+ *
132
+ * This completes the animation immediately and dispatches the onComplete event
133
+ */
134
+ protected setFieldTo<Obj, Name extends keyof Obj, V extends Obj[Name]>(object: Obj, field: Name, targetValue: V): void;
104
135
  /**
105
136
  * Creates a new map if one doesn't already exist for the given object
106
137
  */
@@ -108,18 +139,12 @@ export declare class Animator {
108
139
  /**
109
140
  * Creates a new spring if one doesn't already exist for the given object and field
110
141
  */
111
- private getAnimationOrCreate;
112
- }
113
- export type TweenStepFn = (object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number) => void;
114
- export declare namespace Tween {
115
- type Parameters = {
116
- x0: number;
117
- t0_ms: number;
118
- duration_s: number;
119
- };
120
- function linearStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
121
- function easeInOutStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
122
- function easeInStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
123
- function easeOutStep(object: any, field: string | number | symbol, target: number, params: Tween.Parameters, dt_s: number): void;
142
+ private syncAnimation;
124
143
  }
144
+ type FieldAnimation<Params, State> = {
145
+ animator: IFieldAnimator<Params, State, any>;
146
+ state: State;
147
+ params: Params | null;
148
+ };
149
+ type FieldKey = string | number | symbol;
125
150
  export {};
@@ -0,0 +1,9 @@
1
+ export declare enum StepResult {
2
+ Continue = 0,// continue stepping
3
+ Complete = 1
4
+ }
5
+ export interface IFieldAnimator<Params, State, FieldType> {
6
+ createState<Name extends keyof Obj, Obj extends Record<Name, FieldType>>(object: Obj, field: Name, target: Obj[Name], params: Params | null): State;
7
+ updateState<Name extends keyof Obj, Obj extends Record<Name, FieldType>>(state: State, object: Obj, field: Name, target: Obj[Name], params: Params | null): void;
8
+ step<Name extends keyof Obj, Obj extends Record<Name, FieldType>>(state: State, object: Obj, field: Name, params: Params, dt_s: number): StepResult;
9
+ }
@@ -1,3 +1,5 @@
1
+ import { IFieldAnimator } from "../IFieldAnimator.js";
2
+ export declare const SpringAnimator: IFieldAnimator<SpringParameters, SpringState, number>;
1
3
  /**
2
4
  * Spring
3
5
  *
@@ -12,10 +14,16 @@ type UnderdampedParameters = {
12
14
  duration_s: number;
13
15
  /**
14
16
  * How soft / bouncy the spring, at 0 there is no bounce and decay is exponential, from 0 to infinity the spring will overshoot its target while decaying
15
- * It can be loosely through of roughly the number of oscillations it will take to reach the target
17
+ * It can be thought of roughly the number of oscillations it will take to reach the target
16
18
  */
17
19
  bounce: number;
18
20
  };
21
+ type SpringState = {
22
+ x: number;
23
+ targetX: number;
24
+ v: number;
25
+ physicsParameters: Spring.PhysicsParameters;
26
+ };
19
27
  export type SpringParameters = ExponentialParameters | UnderdampedParameters | Spring.PhysicsParameters;
20
28
  export declare namespace Spring {
21
29
  type PhysicsParameters = {
@@ -37,6 +45,8 @@ export declare namespace Spring {
37
45
  * @param dt_s
38
46
  * @param state
39
47
  * @param parameters
48
+ *
49
+ * If parameters are NaN or infinite, the spring will skip to the target
40
50
  */
41
51
  function stepSpring(dt_s: number, state: {
42
52
  x: number;
@@ -0,0 +1,21 @@
1
+ import { IFieldAnimator } from "../IFieldAnimator.js";
2
+ export type TweenParameters = {
3
+ duration_s: number;
4
+ easingFn: EasingStepFn;
5
+ };
6
+ export declare const TweenAnimator: IFieldAnimator<TweenParameters, TweenState, number>;
7
+ type TweenState = {
8
+ x0: number;
9
+ t0_ms: number;
10
+ target: number;
11
+ duration_s: number;
12
+ easingFn: EasingStepFn;
13
+ velocity: number;
14
+ };
15
+ type FieldKey = string | number | symbol;
16
+ export type EasingStepFn = (object: any, field: FieldKey, target: number, state: TweenState, dt_s: number) => void;
17
+ export declare function linearStep(object: any, field: FieldKey, target: number, params: TweenState, dt_s: number): void;
18
+ export declare function easeInOutStep(object: any, field: FieldKey, target: number, params: TweenState, dt_s: number): void;
19
+ export declare function easeInStep(object: any, field: FieldKey, target: number, params: TweenState, dt_s: number): void;
20
+ export declare function easeOutStep(object: any, field: FieldKey, target: number, params: TweenState, dt_s: number): void;
21
+ export {};
@@ -1,3 +1,3 @@
1
1
  export * from './AnimationSequencer.js';
2
2
  export * from './Animator.js';
3
- export * from './Spring.js';
3
+ export * from './IFieldAnimator.js';
@@ -1,5 +1,5 @@
1
1
  import { Animator } from "../Animator.js";
2
- import { SpringParameters } from "../Spring.js";
2
+ import { SpringParameters } from "src/animators/SpringAnimator.js";
3
3
  /**
4
4
  * A value that animates to a target value using a spring animation.
5
5
  * This **will** cause a re-render when the value changes.
@@ -1,5 +1,5 @@
1
1
  import { Animator } from "../Animator.js";
2
- import { SpringParameters } from "../Spring.js";
2
+ import { SpringParameters } from "src/animators/SpringAnimator.js";
3
3
  /**
4
4
  * A value that animates to a target value using a spring animation.
5
5
  * This will **not** cause a re-render when the value changes.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "physics-animator",
3
- "version": "0.2.0",
3
+ "version": "0.9.0",
4
4
  "author": "haxiomic (George Corney)",
5
5
  "license": "MIT",
6
- "type": "module",
7
6
  "description": "A TypeScript animation system grounded in physics with three.js and react support.",
7
+ "type": "module",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",
10
10
  "types": "dist/types/index.d.ts",
@@ -35,21 +35,13 @@
35
35
  "typescript": "^5.0.0"
36
36
  },
37
37
  "dependencies": {
38
- "@haxiomic/event-signal": "^1.0.0",
39
- "use-initializer": "^1.0.2"
38
+ "@haxiomic/event-signal": "^1.1.0",
39
+ "use-initializer": "^1.1.0"
40
40
  },
41
41
  "peerDependencies": {
42
- "@types/three": "x.x.x",
43
- "react": ">=18.2.0",
44
- "three": "x.x.x"
42
+ "react": ">=18.2.0"
45
43
  },
46
44
  "peerDependenciesMeta": {
47
- "@types/three": {
48
- "optional": true
49
- },
50
- "three": {
51
- "optional": true
52
- },
53
45
  "react": {
54
46
  "optional": true
55
47
  }