@airisu-dev/react-native-live2d-skia 0.1.2-nightly.20261025.2b7609a → 0.2.0-rc.1

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/README.md CHANGED
@@ -162,6 +162,17 @@ const gesture = useLive2DGestures([
162
162
  </GestureDetector>
163
163
  ```
164
164
 
165
+ To animate a model, pass Reanimated shared values as `x`, `y` or `scale` (the gesture hooks take the same). It moves on the UI thread, with no React render per frame:
166
+
167
+ ```tsx
168
+ const x = useSharedValue(0)
169
+ useEffect(() => {
170
+ x.set(withRepeat(withTiming(80, { duration: 1500 }), -1, true))
171
+ }, [x])
172
+ // …
173
+ <Live2D model={model} x={x} y={0} scale={0.4} />
174
+ ```
175
+
165
176
  Inside a `<Canvas>`, JSX order is drawing order: a `<Live2D>` is one layer among ordinary Skia shapes. One animation loop updates and draws every shown model each frame, and two `<Live2D>`s showing the same model share its frames.
166
177
 
167
178
  ### Controlling the model
@@ -175,6 +186,10 @@ model.expression('exp_01') // expression; random without a name
175
186
  model.resetExpression()
176
187
  model.setParameter('ParamAngleX', 30) // held over motions until resetParameter
177
188
  model.resetParameter('ParamAngleX')
189
+ model.resetParameters() // release every held parameter
190
+ model.getParameter('ParamAngleX') // current value, motions and physics applied
191
+ model.stopMotion() // stops it where it is
192
+ model.resetPose() // stops it and eases back to the default pose
178
193
  model.focus(x, y) // look at a point, easing
179
194
  model.resetFocus()
180
195
  model.hitTest(x, y) // → ['Head']
@@ -182,6 +197,28 @@ model.hitTest(x, y) // → ['Head']
182
197
 
183
198
  Methods can be called from the JS thread while the model animates on its background thread: the native model is locked.
184
199
 
200
+ ### Driving the model from a camera or an animation
201
+
202
+ To drive parameters at a high rate, e.g. from face tracking, keep the JS thread out of it. `useLive2DParameters` holds parameters at the values of a Reanimated shared value, passing them on from the UI thread. Write the shared value from any worklet:
203
+
204
+ ```tsx
205
+ const values = useSharedValue<Record<string, number>>({})
206
+ useLive2DParameters(model, values, { smoothing: 0.08 })
207
+ // in a worklet: values.value = { ParamAngleX: yaw * 30, ParamEyeLOpen: leftEyeOpen }
208
+ ```
209
+
210
+ A camera frame processor (e.g. react-native-vision-camera with ML Kit or `react-native-fast-tflite`) can also call the native model directly, in one locked call per frame:
211
+
212
+ ```ts
213
+ model.native.setParameters(['ParamAngleX', 'ParamEyeLOpen'], [yaw * 30, leftEyeOpen])
214
+ ```
215
+
216
+ - Values are clamped to each parameter's range, and ids the model doesn't have are ignored.
217
+ - Trackers deliver 15-30 values a second and the display shows 60 frames: `smoothing` (a time constant in seconds, `model.setParameterSmoothing(0.08)`) eases held parameters towards their values in between.
218
+ - Typical mapping: head yaw, pitch and roll to `ParamAngleX`, `ParamAngleY` and `ParamAngleZ` (about ±30°, mirrored for a front camera), eye openness to `ParamEyeLOpen` and `ParamEyeROpen`, mouth openness to `ParamMouthOpenY`, smile to `ParamMouthForm`.
219
+ - `useLive2DModel(source, { idleMotion: false })` (or `model.setIdleMotionEnabled(false)` later) stops the random `Idle` motions that start whenever nothing plays, so only your values drive the model. Call `model.resetParameters()` when the face is lost, and the model's own motions take over again.
220
+ - `model.resetPose()` eases what motions moved back to the default pose and leaves held parameters alone, so it can be called mid-motion while tracking runs.
221
+
185
222
  ## API
186
223
 
187
224
  ### `useLive2DModel(source, options?)`
@@ -199,14 +236,16 @@ Methods can be called from the JS thread while the model animates on its backgro
199
236
  | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
237
  | `headers` | Sent with every request for a remote file |
201
238
  | `resolveUrl` | `(url, file) => string \| Promise<string>`: the URL to request for each file the `model3.json` lists. `url` is resolved next to it without a query, `file` is the path as listed. Replaces the default, which reuses the `model3.json` URL's query |
239
+ | `idleMotion` | Whether a random `Idle` motion starts whenever nothing plays, from the first frame. Default `true`; `model.setIdleMotionEnabled` changes it later |
240
+ | `eyeBlink` | Whether the eyes blink, for models with eye blink parameters. Default `true`; `model.setEyeBlinkEnabled` changes it later |
202
241
 
203
242
  ### `<Live2D />`
204
243
 
205
244
  | Prop | Description |
206
245
  | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
207
246
  | `model` | `model` from `useLive2DModel` (renders nothing while `null`) |
208
- | `x`, `y` | Top-left position on the canvas. Default `0` |
209
- | `scale` | Uniform scale of the model's native canvas size. Default `1` |
247
+ | `x`, `y` | Top-left position on the canvas: a number, or a Reanimated shared value. Default `0` |
248
+ | `scale` | Uniform scale of the model's native canvas size: a number, or a shared value. Default `1` |
210
249
  | `paused` | Stops animating, keeps the current frame. Default `false` |
211
250
  | `maxFps` | Caps the frame rate (e.g. `30` to save battery). Default: the display's refresh rate |
212
251
  | `onFrameStats(stats)` | About once a second: `fps`, `targetFps`, per-frame times (`workMs`, `updateMs`, `drawablesMs`, `drawMs`, `meshMs`), `meshesBuilt`, `layers` (mask layers), `models` and `loopWorkMs` |
@@ -235,18 +274,32 @@ The gesture is built once; targets are read at touch time, so they can change on
235
274
 
236
275
  The same for one model: `options` is a target (without `model`) plus `focus`.
237
276
 
277
+ ### `useLive2DParameters(model, values, options?)`
278
+
279
+ Holds parameters at the values of a `SharedValue<Record<string, number>>` as it changes, from the UI thread, and releases them on unmount. `options.smoothing` is the easing time constant in seconds (default `0`). `useLive2DParameter(model, id, value)` is the same for one parameter and a `SharedValue<number>`.
280
+
238
281
  ### `Live2DModel`
239
282
 
240
- | Member | Description |
241
- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
242
- | `motion(group, index?, priority?)` | Plays a motion; `priority` is `'idle'`, `'normal'` or `'force'`. `false` if there is none or a higher priority one is playing |
243
- | `expression(name?)` | Fades to an expression, random without a name. `false` if there is none |
244
- | `resetExpression()` | Removes the current expression |
245
- | `setParameter(id, value)` / `resetParameter(id)` | Holds a parameter over motions until reset |
246
- | `focus(x, y)` / `resetFocus()` | Looks at a point, easing / looks straight ahead |
247
- | `hitTest(x, y)` | Names of the `model3.json` hit areas at a point |
248
- | `motionGroups`, `expressionNames` | What the model has |
249
- | `width`, `height` | The model's native canvas size |
283
+ | Member | Description |
284
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | `motion(group, index?, priority?)` | Plays a motion; `priority` is `'idle'`, `'normal'` or `'force'`. `false` if there is none or a higher priority one is playing |
286
+ | `expression(name?)` | Fades to an expression, random without a name. `false` if there is none |
287
+ | `resetExpression()` | Removes the current expression |
288
+ | `setParameter(id, value)` / `resetParameter(id)` | Holds a parameter over motions until reset |
289
+ | `focus(x, y)` / `resetFocus()` | Looks at a point, easing / looks straight ahead |
290
+ | `hitTest(x, y)` | Names of the `model3.json` hit areas at a point |
291
+ | `setParameters(values)` | `setParameter` for each entry of a `{ id: value }` object, in one locked call |
292
+ | `setParameterSmoothing(seconds)` | Eases held parameters towards their values; `0` (default) applies them as they are |
293
+ | `setIdleMotionEnabled(enabled)` | Whether a random `Idle` motion starts when nothing plays. Default `true` |
294
+ | `setEyeBlinkEnabled(enabled)` | Whether the eyes blink, for models with eye blink parameters. Default `true` |
295
+ | `resetParameters()` / `getParameter(id)` | Releases every held parameter / reads a parameter's current value |
296
+ | `stopMotion()` / `isMotionPlaying()` | Stops the playing motion, leaving the model where it was (an `Idle` one starts again if enabled) / whether one plays |
297
+ | `resetPose(fadeSeconds?)` | Stops the playing motion and eases every parameter and switched pose part back to its default (0.5 s by default, `0` snaps); held ones stay |
298
+ | `motions` | `Record<group, MotionInfo[]>`: file, duration, loop and fades of each motion, from the model's files |
299
+ | `expressions` | `ExpressionInfo[]`: name, file and the parameters each expression sets |
300
+ | `parameters` | `ParameterInfo[]`: id, min, max, default, and the name and group from the model's `cdi3.json` if it has one. `setParameter` clamps to the range |
301
+ | `motionGroups`, `expressionNames` | The names in `motions` and `expressions` |
302
+ | `width`, `height` | The model's native canvas size |
250
303
 
251
304
  ## Contributing
252
305
 
@@ -1,25 +1,45 @@
1
1
  #pragma once
2
+ #include <cmath>
2
3
  #include <map>
3
4
  #include "CubismModelSettingJson.hpp"
4
5
  #include "Motion/ICubismUpdater.hpp"
5
6
 
6
7
  namespace margelo::nitro::live2dskia {
7
8
 
8
- // Applies setParameter values every update, after look and breath (so they
9
+ // A parameter held at `target` by setParameter(s). `current` is what the model gets: the
10
+ // target, or eased towards it when smoothing is on.
11
+ struct HeldParameter {
12
+ float target = 0;
13
+ float current = 0;
14
+ // Set until the first update after holding: `current` starts from the model's own value.
15
+ bool fresh = true;
16
+ };
17
+
18
+ // Applies the held parameters every update, after look and breath (so they
9
19
  // can be overridden) and before physics (so hair and clothes follow).
10
20
  class HeldParameterUpdater : public Live2D::Cubism::Framework::ICubismUpdater {
11
21
  public:
12
- explicit HeldParameterUpdater(const std::map<Live2D::Cubism::Framework::CubismIdHandle, float>& values)
13
- : ICubismUpdater(Live2D::Cubism::Framework::CubismUpdateOrder_Breath + 50), _values(values) {}
22
+ // `smoothing` is the time constant in seconds of the easing, 0 for none.
23
+ HeldParameterUpdater(std::map<Live2D::Cubism::Framework::CubismIdHandle, HeldParameter>& values, const float& smoothing)
24
+ : ICubismUpdater(Live2D::Cubism::Framework::CubismUpdateOrder_Breath + 50), _values(values), _smoothing(smoothing) {}
14
25
 
15
- void OnLateUpdate(Live2D::Cubism::Framework::CubismModel* model, Live2D::Cubism::Framework::csmFloat32) override {
16
- for (const auto& [id, value] : _values) {
17
- model->SetParameterValue(id, value);
26
+ void OnLateUpdate(Live2D::Cubism::Framework::CubismModel* model, Live2D::Cubism::Framework::csmFloat32 deltaSeconds) override {
27
+ const float tau = _smoothing;
28
+ for (auto& [id, held] : _values) {
29
+ if (tau <= 0) {
30
+ held.current = held.target;
31
+ } else {
32
+ if (held.fresh) held.current = model->GetParameterValue(id);
33
+ held.current += (held.target - held.current) * (1.0f - std::exp(-deltaSeconds / tau));
34
+ }
35
+ held.fresh = false;
36
+ model->SetParameterValue(id, held.current);
18
37
  }
19
38
  }
20
39
 
21
40
  private:
22
- const std::map<Live2D::Cubism::Framework::CubismIdHandle, float>& _values;
41
+ std::map<Live2D::Cubism::Framework::CubismIdHandle, HeldParameter>& _values;
42
+ const float& _smoothing;
23
43
  };
24
44
 
25
45
  } // namespace margelo::nitro::live2dskia
@@ -1,4 +1,5 @@
1
1
  #include "Live2DUserModel.hpp"
2
+ #include <algorithm>
2
3
  #include <stdexcept>
3
4
  #include "CubismDefaultParameterId.hpp"
4
5
  #include "CubismHelpers.hpp"
@@ -36,7 +37,7 @@ Live2DUserModel::Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::
36
37
 
37
38
  if (_eyeBlinkIds.GetSize() > 0) {
38
39
  _eyeBlink = CubismEyeBlink::Create(_setting);
39
- _updateScheduler.AddUpdatableList(CSM_NEW CubismEyeBlinkUpdater(_motionUpdated, *_eyeBlink));
40
+ _updateScheduler.AddUpdatableList(CSM_NEW CubismEyeBlinkUpdater(_skipEyeBlink, *_eyeBlink));
40
41
  }
41
42
 
42
43
  _breath = CubismBreath::Create();
@@ -62,7 +63,7 @@ Live2DUserModel::Live2DUserModel(const uint8_t* moc, size_t mocSize, const std::
62
63
  _look->SetParameters(look);
63
64
  _updateScheduler.AddUpdatableList(CSM_NEW CubismLookUpdater(*_look, *_dragManager));
64
65
 
65
- _updateScheduler.AddUpdatableList(CSM_NEW HeldParameterUpdater(_heldParameters));
66
+ _updateScheduler.AddUpdatableList(CSM_NEW HeldParameterUpdater(_heldParameters, _parameterSmoothing));
66
67
 
67
68
  _updateScheduler.SortUpdatableList();
68
69
  _model->SaveParameters();
@@ -82,13 +83,29 @@ Live2DUserModel::~Live2DUserModel() {
82
83
  }
83
84
 
84
85
  void Live2DUserModel::setParameter(const std::string& id, float value) {
85
- _heldParameters[idOf(id.c_str())] = value;
86
+ const CubismIdHandle handle = idOf(id.c_str());
87
+ const csmInt32 index = _model->GetParameterIndex(handle);
88
+ if (index < 0) return;
89
+ const float clamped = std::min(_model->GetParameterMaximumValue(index), std::max(_model->GetParameterMinimumValue(index), value));
90
+ // Holding again keeps `current`, so a changing target is eased towards, not restarted.
91
+ _heldParameters[handle].target = clamped;
92
+ }
93
+
94
+ void Live2DUserModel::setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) {
95
+ const size_t count = std::min(ids.size(), values.size());
96
+ for (size_t i = 0; i < count; ++i) {
97
+ setParameter(ids[i], static_cast<float>(values[i]));
98
+ }
86
99
  }
87
100
 
88
101
  void Live2DUserModel::clearParameter(const std::string& id) {
89
102
  _heldParameters.erase(idOf(id.c_str()));
90
103
  }
91
104
 
105
+ void Live2DUserModel::clearParameters() {
106
+ _heldParameters.clear();
107
+ }
108
+
92
109
  void Live2DUserModel::focus(float x, float y) {
93
110
  SetDragging(x, y);
94
111
  }
@@ -109,8 +126,10 @@ void Live2DUserModel::update(float deltaSeconds) {
109
126
  // Same order as the SDK sample's LAppModel::Update.
110
127
  _motionUpdated = false;
111
128
  _model->LoadParameters();
129
+ // Before motions, so one started during the fade blends in from the easing pose.
130
+ if (_poseResetRemaining > 0) easeToDefaultPose(deltaSeconds);
112
131
  if (_motionManager->IsFinished()) {
113
- startRandomMotion("Idle", PriorityIdle);
132
+ if (_idleMotionEnabled) startRandomMotion("Idle", PriorityIdle);
114
133
  } else {
115
134
  _motionUpdated = _motionManager->UpdateMotion(_model, deltaSeconds);
116
135
  }
@@ -118,6 +137,7 @@ void Live2DUserModel::update(float deltaSeconds) {
118
137
 
119
138
  // Eye blink (unless a motion moved the eyes), expression, look, breath,
120
139
  // held parameters, physics, pose.
140
+ _skipEyeBlink = _motionUpdated || !_eyeBlinkEnabled;
121
141
  _updateScheduler.OnLateUpdate(_model, deltaSeconds);
122
142
  _model->Update();
123
143
  }
@@ -3,6 +3,7 @@
3
3
  #include <string>
4
4
  #include <vector>
5
5
  #include "CubismModelSettingJson.hpp"
6
+ #include "HeldParameterUpdater.hpp"
6
7
  #include "Model/CubismUserModel.hpp"
7
8
 
8
9
  namespace margelo::nitro::live2dskia {
@@ -28,12 +29,28 @@ class Live2DUserModel : public csm::CubismUserModel {
28
29
  void loadMotion(const std::string& group, int index, const std::string& json);
29
30
  bool startMotion(const std::string& group, int index, Priority priority);
30
31
  bool startRandomMotion(const std::string& group, Priority priority);
32
+ void stopMotion();
33
+ bool isMotionPlaying();
34
+ // Stops motions and eases every parameter to its default over `fadeSeconds` (0 snaps).
35
+ void resetPose(float fadeSeconds);
31
36
  void loadExpression(const std::string& name, const std::string& json);
32
37
  bool setExpression(const std::string& name);
33
38
  bool setRandomExpression();
34
39
  void clearExpression();
40
+ // Clamped to the parameter's range; ids the model doesn't have are ignored.
35
41
  void setParameter(const std::string& id, float value);
42
+ void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values);
36
43
  void clearParameter(const std::string& id);
44
+ void clearParameters();
45
+ // Time constant in seconds of the easing of held parameters towards their value, 0 for none.
46
+ float parameterSmoothing() const { return _parameterSmoothing; }
47
+ void setParameterSmoothing(float seconds) { _parameterSmoothing = seconds < 0 ? 0 : seconds; }
48
+ // Whether a random Idle motion starts whenever no motion plays.
49
+ bool idleMotionEnabled() const { return _idleMotionEnabled; }
50
+ void setIdleMotionEnabled(bool enabled) { _idleMotionEnabled = enabled; }
51
+ // Whether the eyes blink (models with eye blink parameters only).
52
+ bool eyeBlinkEnabled() const { return _eyeBlinkEnabled; }
53
+ void setEyeBlinkEnabled(bool enabled) { _eyeBlinkEnabled = enabled; }
37
54
  // -1..1 on each axis, y-up.
38
55
  void focus(float x, float y);
39
56
  // Hit area names containing the point (model units, y-up).
@@ -49,9 +66,17 @@ class Live2DUserModel : public csm::CubismUserModel {
49
66
  std::map<std::string, csm::ACubismMotion*> _motions;
50
67
  std::map<std::string, csm::ACubismMotion*> _expressions;
51
68
  // Set by setParameter, applied every update.
52
- std::map<csm::CubismIdHandle, float> _heldParameters;
69
+ std::map<csm::CubismIdHandle, HeldParameter> _heldParameters;
70
+ float _parameterSmoothing = 0;
71
+ bool _idleMotionEnabled = true;
72
+ bool _eyeBlinkEnabled = true;
73
+ // Seconds left of a resetPose fade, 0 when none runs.
74
+ float _poseResetRemaining = 0;
75
+ void easeToDefaultPose(float deltaSeconds);
53
76
  // Read by the eye blink updater: no blinking while a motion moves the eyes.
54
77
  csm::csmBool _motionUpdated = false;
78
+ // Read by the eye blink updater: set while a motion moves the eyes or blinking is off.
79
+ csm::csmBool _skipEyeBlink = false;
55
80
  };
56
81
 
57
82
  } // namespace margelo::nitro::live2dskia
@@ -3,6 +3,7 @@
3
3
  #include <cstdlib>
4
4
  #include <iterator>
5
5
  #include <stdexcept>
6
+ #include <vector>
6
7
  #include "CubismHelpers.hpp"
7
8
  #include "Motion/CubismMotion.hpp"
8
9
  #include "Motion/CubismPhysicsUpdater.hpp"
@@ -68,6 +69,51 @@ bool Live2DUserModel::startRandomMotion(const std::string& group, Priority prior
68
69
  return startMotion(group, std::rand() % count, priority);
69
70
  }
70
71
 
72
+ void Live2DUserModel::stopMotion() {
73
+ _motionManager->StopAllMotions();
74
+ }
75
+
76
+ bool Live2DUserModel::isMotionPlaying() {
77
+ return !_motionManager->IsFinished();
78
+ }
79
+
80
+ void Live2DUserModel::resetPose(float fadeSeconds) {
81
+ _motionManager->StopAllMotions();
82
+ // Motions also switch pose parts (Hiyori's raised arm is PartArmB) through per-part values
83
+ // that aren't model parameters. Reset puts back the first part of each group; restoring the
84
+ // opacities it set lets the pose fade to that part instead of popping.
85
+ if (_pose) {
86
+ const csmInt32 partCount = _model->GetPartCount();
87
+ std::vector<csmFloat32> opacities(static_cast<size_t>(partCount));
88
+ for (csmInt32 i = 0; i < partCount; ++i) opacities[i] = _model->GetPartOpacity(i);
89
+ _pose->Reset(_model);
90
+ if (fadeSeconds > 0) {
91
+ for (csmInt32 i = 0; i < partCount; ++i) _model->SetPartOpacity(i, opacities[i]);
92
+ }
93
+ }
94
+ // update() starts each frame from the saved parameters, so a stopped motion alone
95
+ // leaves the model frozen in its last pose: move those towards the defaults instead.
96
+ if (fadeSeconds > 0) {
97
+ _poseResetRemaining = fadeSeconds;
98
+ return;
99
+ }
100
+ _poseResetRemaining = 0;
101
+ for (csmInt32 i = 0; i < _model->GetParameterCount(); ++i) {
102
+ _model->SetParameterValue(i, _model->GetParameterDefaultValue(i));
103
+ }
104
+ _model->SaveParameters();
105
+ }
106
+
107
+ void Live2DUserModel::easeToDefaultPose(float deltaSeconds) {
108
+ // Linear in time: each frame covers its share of what is left, so it lands at the end.
109
+ const float t = deltaSeconds >= _poseResetRemaining ? 1.0f : deltaSeconds / _poseResetRemaining;
110
+ for (csmInt32 i = 0; i < _model->GetParameterCount(); ++i) {
111
+ const float value = _model->GetParameterValue(i);
112
+ _model->SetParameterValue(i, value + (_model->GetParameterDefaultValue(i) - value) * t);
113
+ }
114
+ _poseResetRemaining = t >= 1.0f ? 0 : _poseResetRemaining - deltaSeconds;
115
+ }
116
+
71
117
  void Live2DUserModel::loadExpression(const std::string& name, const std::string& json) {
72
118
  requireValidJson(json, "exp3.json for " + name);
73
119
  ACubismMotion* expression = LoadExpression(bytesOf(json), static_cast<csmSizeInt>(json.size()), name.c_str());
@@ -3,6 +3,7 @@
3
3
  #include <numeric>
4
4
  #include "CubismSetup.hpp"
5
5
  #include "CubismCompat.hpp"
6
+ #include "CubismHelpers.hpp"
6
7
 
7
8
  namespace margelo::nitro::live2dskia {
8
9
 
@@ -106,6 +107,21 @@ bool HybridLive2DModel::startMotion(const std::string& group, std::optional<doub
106
107
  return _userModel->startMotion(group, static_cast<int>(*index), toPriority(priority));
107
108
  }
108
109
 
110
+ void HybridLive2DModel::stopMotion() {
111
+ std::lock_guard lock(_mutex);
112
+ _userModel->stopMotion();
113
+ }
114
+
115
+ bool HybridLive2DModel::isMotionPlaying() {
116
+ std::lock_guard lock(_mutex);
117
+ return _userModel->isMotionPlaying();
118
+ }
119
+
120
+ void HybridLive2DModel::resetPose(std::optional<double> fadeSeconds) {
121
+ std::lock_guard lock(_mutex);
122
+ _userModel->resetPose(static_cast<float>(fadeSeconds.value_or(0.5)));
123
+ }
124
+
109
125
  void HybridLive2DModel::loadExpression(const std::string& name, const std::string& json) {
110
126
  std::lock_guard lock(_mutex);
111
127
  _userModel->loadExpression(name, json);
@@ -131,6 +147,69 @@ void HybridLive2DModel::clearParameter(const std::string& id) {
131
147
  _userModel->clearParameter(id);
132
148
  }
133
149
 
150
+ std::vector<ParameterData> HybridLive2DModel::getParameters() {
151
+ std::lock_guard lock(_mutex);
152
+ auto* model = _userModel->model();
153
+ const int count = model->GetParameterCount();
154
+ std::vector<ParameterData> result;
155
+ result.reserve(count);
156
+ for (int i = 0; i < count; ++i) {
157
+ result.emplace_back(std::string(model->GetParameterId(i)->GetString().GetRawString()),
158
+ model->GetParameterMinimumValue(i),
159
+ model->GetParameterMaximumValue(i),
160
+ model->GetParameterDefaultValue(i),
161
+ model->GetParameterValue(i));
162
+ }
163
+ return result;
164
+ }
165
+
166
+ double HybridLive2DModel::getParameterValue(const std::string& id) {
167
+ std::lock_guard lock(_mutex);
168
+ auto* model = _userModel->model();
169
+ const int index = model->GetParameterIndex(idOf(id.c_str()));
170
+ return index < 0 ? 0.0 : model->GetParameterValue(index);
171
+ }
172
+
173
+ void HybridLive2DModel::setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) {
174
+ std::lock_guard lock(_mutex);
175
+ _userModel->setParameters(ids, values);
176
+ }
177
+
178
+ void HybridLive2DModel::clearParameters() {
179
+ std::lock_guard lock(_mutex);
180
+ _userModel->clearParameters();
181
+ }
182
+
183
+ double HybridLive2DModel::getParameterSmoothing() {
184
+ std::lock_guard lock(_mutex);
185
+ return _userModel->parameterSmoothing();
186
+ }
187
+
188
+ void HybridLive2DModel::setParameterSmoothing(double seconds) {
189
+ std::lock_guard lock(_mutex);
190
+ _userModel->setParameterSmoothing(static_cast<float>(seconds));
191
+ }
192
+
193
+ bool HybridLive2DModel::getIdleMotionEnabled() {
194
+ std::lock_guard lock(_mutex);
195
+ return _userModel->idleMotionEnabled();
196
+ }
197
+
198
+ void HybridLive2DModel::setIdleMotionEnabled(bool enabled) {
199
+ std::lock_guard lock(_mutex);
200
+ _userModel->setIdleMotionEnabled(enabled);
201
+ }
202
+
203
+ bool HybridLive2DModel::getEyeBlinkEnabled() {
204
+ std::lock_guard lock(_mutex);
205
+ return _userModel->eyeBlinkEnabled();
206
+ }
207
+
208
+ void HybridLive2DModel::setEyeBlinkEnabled(bool enabled) {
209
+ std::lock_guard lock(_mutex);
210
+ _userModel->setEyeBlinkEnabled(enabled);
211
+ }
212
+
134
213
  void HybridLive2DModel::focus(double x, double y) {
135
214
  std::lock_guard lock(_mutex);
136
215
  _userModel->focus(static_cast<float>(std::clamp(x, -1.0, 1.0)), static_cast<float>(std::clamp(y, -1.0, 1.0)));
@@ -26,11 +26,24 @@ class HybridLive2DModel : public HybridLive2DModelSpec {
26
26
  void loadPose(const std::string& json) override;
27
27
  void loadMotion(const std::string& group, double index, const std::string& json) override;
28
28
  bool startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) override;
29
+ void stopMotion() override;
30
+ bool isMotionPlaying() override;
31
+ void resetPose(std::optional<double> fadeSeconds) override;
29
32
  void loadExpression(const std::string& name, const std::string& json) override;
30
33
  bool setExpression(const std::optional<std::string>& name) override;
31
34
  void clearExpression() override;
32
35
  void setParameter(const std::string& id, double value) override;
33
36
  void clearParameter(const std::string& id) override;
37
+ void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) override;
38
+ void clearParameters() override;
39
+ double getParameterSmoothing() override;
40
+ void setParameterSmoothing(double seconds) override;
41
+ bool getIdleMotionEnabled() override;
42
+ void setIdleMotionEnabled(bool enabled) override;
43
+ bool getEyeBlinkEnabled() override;
44
+ void setEyeBlinkEnabled(bool enabled) override;
45
+ std::vector<ParameterData> getParameters() override;
46
+ double getParameterValue(const std::string& id) override;
34
47
  void focus(double x, double y) override;
35
48
  std::vector<std::string> hitTest(double x, double y) override;
36
49
  void update(double deltaSeconds) override;
@@ -20,16 +20,29 @@ namespace margelo::nitro::live2dskia {
20
20
  prototype.registerHybridGetter("originY", &HybridLive2DModelSpec::getOriginY);
21
21
  prototype.registerHybridGetter("pixelsPerUnit", &HybridLive2DModelSpec::getPixelsPerUnit);
22
22
  prototype.registerHybridGetter("drawableCount", &HybridLive2DModelSpec::getDrawableCount);
23
+ prototype.registerHybridGetter("parameterSmoothing", &HybridLive2DModelSpec::getParameterSmoothing);
24
+ prototype.registerHybridSetter("parameterSmoothing", &HybridLive2DModelSpec::setParameterSmoothing);
25
+ prototype.registerHybridGetter("idleMotionEnabled", &HybridLive2DModelSpec::getIdleMotionEnabled);
26
+ prototype.registerHybridSetter("idleMotionEnabled", &HybridLive2DModelSpec::setIdleMotionEnabled);
27
+ prototype.registerHybridGetter("eyeBlinkEnabled", &HybridLive2DModelSpec::getEyeBlinkEnabled);
28
+ prototype.registerHybridSetter("eyeBlinkEnabled", &HybridLive2DModelSpec::setEyeBlinkEnabled);
23
29
  prototype.registerHybridMethod("getDrawables", &HybridLive2DModelSpec::getDrawables);
24
30
  prototype.registerHybridMethod("loadPhysics", &HybridLive2DModelSpec::loadPhysics);
25
31
  prototype.registerHybridMethod("loadPose", &HybridLive2DModelSpec::loadPose);
26
32
  prototype.registerHybridMethod("loadMotion", &HybridLive2DModelSpec::loadMotion);
27
33
  prototype.registerHybridMethod("startMotion", &HybridLive2DModelSpec::startMotion);
34
+ prototype.registerHybridMethod("stopMotion", &HybridLive2DModelSpec::stopMotion);
35
+ prototype.registerHybridMethod("isMotionPlaying", &HybridLive2DModelSpec::isMotionPlaying);
36
+ prototype.registerHybridMethod("resetPose", &HybridLive2DModelSpec::resetPose);
28
37
  prototype.registerHybridMethod("loadExpression", &HybridLive2DModelSpec::loadExpression);
29
38
  prototype.registerHybridMethod("setExpression", &HybridLive2DModelSpec::setExpression);
30
39
  prototype.registerHybridMethod("clearExpression", &HybridLive2DModelSpec::clearExpression);
31
40
  prototype.registerHybridMethod("setParameter", &HybridLive2DModelSpec::setParameter);
32
41
  prototype.registerHybridMethod("clearParameter", &HybridLive2DModelSpec::clearParameter);
42
+ prototype.registerHybridMethod("setParameters", &HybridLive2DModelSpec::setParameters);
43
+ prototype.registerHybridMethod("clearParameters", &HybridLive2DModelSpec::clearParameters);
44
+ prototype.registerHybridMethod("getParameters", &HybridLive2DModelSpec::getParameters);
45
+ prototype.registerHybridMethod("getParameterValue", &HybridLive2DModelSpec::getParameterValue);
33
46
  prototype.registerHybridMethod("focus", &HybridLive2DModelSpec::focus);
34
47
  prototype.registerHybridMethod("hitTest", &HybridLive2DModelSpec::hitTest);
35
48
  prototype.registerHybridMethod("update", &HybridLive2DModelSpec::update);
@@ -17,12 +17,15 @@
17
17
  namespace margelo::nitro::live2dskia { struct DrawableData; }
18
18
  // Forward declaration of `MotionPriority` to properly resolve imports.
19
19
  namespace margelo::nitro::live2dskia { enum class MotionPriority; }
20
+ // Forward declaration of `ParameterData` to properly resolve imports.
21
+ namespace margelo::nitro::live2dskia { struct ParameterData; }
20
22
 
21
23
  #include "DrawableData.hpp"
22
24
  #include <vector>
23
25
  #include <string>
24
26
  #include <optional>
25
27
  #include "MotionPriority.hpp"
28
+ #include "ParameterData.hpp"
26
29
 
27
30
  namespace margelo::nitro::live2dskia {
28
31
 
@@ -57,6 +60,12 @@ namespace margelo::nitro::live2dskia {
57
60
  virtual double getOriginY() = 0;
58
61
  virtual double getPixelsPerUnit() = 0;
59
62
  virtual double getDrawableCount() = 0;
63
+ virtual double getParameterSmoothing() = 0;
64
+ virtual void setParameterSmoothing(double parameterSmoothing) = 0;
65
+ virtual bool getIdleMotionEnabled() = 0;
66
+ virtual void setIdleMotionEnabled(bool idleMotionEnabled) = 0;
67
+ virtual bool getEyeBlinkEnabled() = 0;
68
+ virtual void setEyeBlinkEnabled(bool eyeBlinkEnabled) = 0;
60
69
 
61
70
  public:
62
71
  // Methods
@@ -65,11 +74,18 @@ namespace margelo::nitro::live2dskia {
65
74
  virtual void loadPose(const std::string& json) = 0;
66
75
  virtual void loadMotion(const std::string& group, double index, const std::string& json) = 0;
67
76
  virtual bool startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) = 0;
77
+ virtual void stopMotion() = 0;
78
+ virtual bool isMotionPlaying() = 0;
79
+ virtual void resetPose(std::optional<double> fadeSeconds) = 0;
68
80
  virtual void loadExpression(const std::string& name, const std::string& json) = 0;
69
81
  virtual bool setExpression(const std::optional<std::string>& name) = 0;
70
82
  virtual void clearExpression() = 0;
71
83
  virtual void setParameter(const std::string& id, double value) = 0;
72
84
  virtual void clearParameter(const std::string& id) = 0;
85
+ virtual void setParameters(const std::vector<std::string>& ids, const std::vector<double>& values) = 0;
86
+ virtual void clearParameters() = 0;
87
+ virtual std::vector<ParameterData> getParameters() = 0;
88
+ virtual double getParameterValue(const std::string& id) = 0;
73
89
  virtual void focus(double x, double y) = 0;
74
90
  virtual std::vector<std::string> hitTest(double x, double y) = 0;
75
91
  virtual void update(double deltaSeconds) = 0;