@airisu-dev/react-native-live2d-skia 0.0.0-stage → 0.1.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/Live2dSkia.podspec +159 -0
  3. package/README.md +64 -2
  4. package/android/CMakeLists.txt +113 -0
  5. package/android/build.gradle +223 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/live2dskia/Live2dSkiaPackage.kt +20 -0
  11. package/app.plugin.js +2 -0
  12. package/cli/build/index.js +157 -0
  13. package/cpp/cubism/CubismCompat.hpp +26 -0
  14. package/cpp/cubism/CubismHelpers.hpp +27 -0
  15. package/cpp/cubism/CubismRendererStub.cpp +14 -0
  16. package/cpp/cubism/CubismSetup.cpp +54 -0
  17. package/cpp/cubism/CubismSetup.hpp +9 -0
  18. package/cpp/cubism/HeldParameterUpdater.hpp +25 -0
  19. package/cpp/cubism/Live2DUserModel.cpp +125 -0
  20. package/cpp/cubism/Live2DUserModel.hpp +57 -0
  21. package/cpp/cubism/Live2DUserModelAnimation.cpp +107 -0
  22. package/cpp/drawing/JsiUnwrap.hpp +25 -0
  23. package/cpp/drawing/MeshCache.cpp +75 -0
  24. package/cpp/drawing/MeshCache.hpp +45 -0
  25. package/cpp/drawing/SkiaDrawer.cpp +138 -0
  26. package/cpp/drawing/SkiaDrawer.hpp +42 -0
  27. package/cpp/hybrid/HybridLive2DModel.cpp +174 -0
  28. package/cpp/hybrid/HybridLive2DModel.hpp +64 -0
  29. package/cpp/hybrid/HybridLive2dSkia.cpp +10 -0
  30. package/cpp/hybrid/HybridLive2dSkia.hpp +11 -0
  31. package/ios/Bridge.h +8 -0
  32. package/nitro.json +20 -0
  33. package/nitrogen/generated/.gitattributes +1 -0
  34. package/nitrogen/generated/android/Live2dSkia+autolinking.cmake +82 -0
  35. package/nitrogen/generated/android/Live2dSkia+autolinking.gradle +27 -0
  36. package/nitrogen/generated/android/Live2dSkiaOnLoad.cpp +49 -0
  37. package/nitrogen/generated/android/Live2dSkiaOnLoad.hpp +34 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/live2dskia/Live2dSkiaOnLoad.kt +35 -0
  39. package/nitrogen/generated/ios/Live2dSkia+autolinking.rb +62 -0
  40. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.cpp +17 -0
  41. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Bridge.hpp +27 -0
  42. package/nitrogen/generated/ios/Live2dSkia-Swift-Cxx-Umbrella.hpp +38 -0
  43. package/nitrogen/generated/ios/Live2dSkiaAutolinking.mm +35 -0
  44. package/nitrogen/generated/ios/Live2dSkiaAutolinking.swift +16 -0
  45. package/nitrogen/generated/shared/c++/DrawableData.hpp +116 -0
  46. package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.cpp +39 -0
  47. package/nitrogen/generated/shared/c++/HybridLive2DModelSpec.hpp +86 -0
  48. package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.cpp +21 -0
  49. package/nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.hpp +66 -0
  50. package/nitrogen/generated/shared/c++/MotionPriority.hpp +80 -0
  51. package/package.json +146 -4
  52. package/plugin/build/index.d.ts +14 -0
  53. package/plugin/build/index.js +81 -0
  54. package/src/components/Live2D.tsx +131 -0
  55. package/src/components/index.ts +2 -0
  56. package/src/hooks/index.ts +8 -0
  57. package/src/hooks/useLive2DGesture.ts +140 -0
  58. package/src/hooks/useLive2DModel.ts +47 -0
  59. package/src/index.ts +5 -0
  60. package/src/loader/cubismJson.ts +23 -0
  61. package/src/loader/index.ts +9 -0
  62. package/src/loader/loadModel.ts +194 -0
  63. package/src/loader/modelContext.ts +80 -0
  64. package/src/loader/modelUrl.ts +112 -0
  65. package/src/native.ts +4 -0
  66. package/src/rendering/drawer.ts +256 -0
  67. package/src/rendering/skia.ts +114 -0
  68. package/src/rendering/ticker/addToTicker.ts +52 -0
  69. package/src/rendering/ticker/constants.ts +11 -0
  70. package/src/rendering/ticker/frames.ts +35 -0
  71. package/src/rendering/ticker/index.ts +5 -0
  72. package/src/rendering/ticker/loop.ts +115 -0
  73. package/src/rendering/ticker/state.ts +65 -0
  74. package/src/rendering/ticker/stats.ts +36 -0
  75. package/src/rendering/ticker/types.ts +57 -0
  76. package/src/specs/react-native-live2d-skia.nitro.ts +88 -0
  77. package/src/utils/clock.ts +10 -0
@@ -0,0 +1,75 @@
1
+ #if LIVE2D_SKIA
2
+
3
+ #include "MeshCache.hpp"
4
+ #include <chrono>
5
+ #include <cstring>
6
+
7
+ namespace margelo::nitro::live2dskia {
8
+
9
+ using namespace Live2D::Cubism::Core;
10
+
11
+ namespace {
12
+
13
+ double millisecondsSince(std::chrono::steady_clock::time_point start) {
14
+ return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
15
+ }
16
+
17
+ } // namespace
18
+
19
+ void MeshCache::setTextureSizes(std::vector<SkISize> sizes) {
20
+ _textureSizes = std::move(sizes);
21
+ _meshes.clear();
22
+ }
23
+
24
+ void MeshCache::resize(size_t drawableCount) {
25
+ if (_meshes.size() != drawableCount) _meshes.resize(drawableCount);
26
+ }
27
+
28
+ void MeshCache::resetStats() {
29
+ _meshesBuilt = 0;
30
+ _meshMs = 0;
31
+ }
32
+
33
+ const SkVertices* MeshCache::meshOf(const csmModel* model, int i, const CanvasInfo& info) {
34
+ const int vertexCount = csmGetDrawableVertexCounts(model)[i];
35
+ const int indexCount = csmGetDrawableIndexCounts(model)[i];
36
+ const int texture = csmGetDrawableTextureIndices(model)[i];
37
+ if (vertexCount == 0 || indexCount == 0 || texture < 0 ||
38
+ texture >= static_cast<int>(_textureSizes.size())) {
39
+ return nullptr;
40
+ }
41
+ Mesh& mesh = _meshes[i];
42
+ const csmVector2* positions = csmGetDrawableVertexPositions(model)[i];
43
+ if (mesh.vertices && mesh.positions.size() == static_cast<size_t>(vertexCount) &&
44
+ std::memcmp(mesh.positions.data(), positions, vertexCount * sizeof(csmVector2)) == 0) {
45
+ return mesh.vertices.get();
46
+ }
47
+
48
+ const auto start = std::chrono::steady_clock::now();
49
+ if (mesh.texCoords.size() != static_cast<size_t>(vertexCount)) {
50
+ const csmVector2* uvs = csmGetDrawableVertexUvs(model)[i];
51
+ const SkISize size = _textureSizes[texture];
52
+ mesh.texCoords.resize(vertexCount);
53
+ for (int v = 0; v < vertexCount; ++v) {
54
+ // Cubism UVs are v-up.
55
+ mesh.texCoords[v] = {uvs[v].X * size.width(), (1 - uvs[v].Y) * size.height()};
56
+ }
57
+ }
58
+ _points.resize(vertexCount);
59
+ for (int v = 0; v < vertexCount; ++v) {
60
+ // Cubism is y-up in model units; Skia is y-down in pixels.
61
+ _points[v] = {info.originX + positions[v].X * info.pixelsPerUnit,
62
+ info.originY - positions[v].Y * info.pixelsPerUnit};
63
+ }
64
+ mesh.vertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, vertexCount, _points.data(),
65
+ mesh.texCoords.data(), nullptr, indexCount,
66
+ csmGetDrawableIndices(model)[i]);
67
+ mesh.positions.assign(positions, positions + vertexCount);
68
+ _meshesBuilt++;
69
+ _meshMs += millisecondsSince(start);
70
+ return mesh.vertices.get();
71
+ }
72
+
73
+ } // namespace margelo::nitro::live2dskia
74
+
75
+ #endif // LIVE2D_SKIA
@@ -0,0 +1,45 @@
1
+ #pragma once
2
+ #if LIVE2D_SKIA
3
+
4
+ #include <vector>
5
+ #include "SkiaDrawer.hpp"
6
+ #include "include/core/SkPoint.h"
7
+ #include "include/core/SkRefCnt.h"
8
+ #include "include/core/SkSize.h"
9
+ #include "include/core/SkVertices.h"
10
+
11
+ namespace margelo::nitro::live2dskia {
12
+
13
+ // One Skia mesh per drawable, rebuilt only when the drawable's vertices moved.
14
+ class MeshCache {
15
+ public:
16
+ // Texture coordinates depend on the texture sizes, so this drops every mesh.
17
+ void setTextureSizes(std::vector<SkISize> sizes);
18
+ void resize(size_t drawableCount);
19
+ void resetStats();
20
+
21
+ // Returns the drawable's mesh, rebuilt if its vertices moved; null if it has none.
22
+ const SkVertices* meshOf(const Live2D::Cubism::Core::csmModel* model, int i, const CanvasInfo& info);
23
+
24
+ int meshesBuilt() const { return _meshesBuilt; }
25
+ double meshMs() const { return _meshMs; }
26
+
27
+ private:
28
+ struct Mesh {
29
+ sk_sp<SkVertices> vertices;
30
+ // Model-space positions the mesh was built from, to detect changes.
31
+ std::vector<Live2D::Cubism::Core::csmVector2> positions;
32
+ // Texture coordinates in pixels, built once.
33
+ std::vector<SkPoint> texCoords;
34
+ };
35
+
36
+ std::vector<SkISize> _textureSizes;
37
+ std::vector<Mesh> _meshes;
38
+ std::vector<SkPoint> _points; // scratch
39
+ int _meshesBuilt = 0;
40
+ double _meshMs = 0;
41
+ };
42
+
43
+ } // namespace margelo::nitro::live2dskia
44
+
45
+ #endif // LIVE2D_SKIA
@@ -0,0 +1,138 @@
1
+ #if LIVE2D_SKIA
2
+
3
+ #include "SkiaDrawer.hpp"
4
+ #include "CubismCompat.hpp"
5
+ #include <algorithm>
6
+ #include <numeric>
7
+ #include "JsiSkCanvas.h"
8
+ #include "JsiSkShader.h"
9
+ #include "JsiUnwrap.hpp"
10
+ #include "MeshCache.hpp"
11
+ #include "include/core/SkCanvas.h"
12
+ #include "include/core/SkPaint.h"
13
+ #include "include/core/SkShader.h"
14
+ #include "include/core/SkVertices.h"
15
+
16
+ namespace margelo::nitro::live2dskia {
17
+
18
+ using namespace Live2D::Cubism::Core;
19
+
20
+ namespace {
21
+
22
+ SkBlendMode blendModeOf(csmFlags constantFlags) {
23
+ if (constantFlags & csmBlendAdditive) return SkBlendMode::kPlus;
24
+ if (constantFlags & csmBlendMultiplicative) return SkBlendMode::kMultiply;
25
+ return SkBlendMode::kSrcOver;
26
+ }
27
+
28
+ } // namespace
29
+
30
+ struct SkiaDrawer::Impl {
31
+ std::vector<sk_sp<SkShader>> shaders;
32
+ MeshCache meshes;
33
+ std::vector<int> order; // scratch
34
+ SkPaint paint;
35
+ SkPaint layerPaint;
36
+
37
+ void drawMesh(SkCanvas* canvas, const SkVertices* vertices, int texture, SkPaint& p) {
38
+ p.setShader(shaders[texture]);
39
+ // No per-vertex colors, so the vertex blend mode is unused; the shader alone colors the mesh.
40
+ canvas->drawVertices(vertices, SkBlendMode::kDst, p);
41
+ }
42
+ };
43
+
44
+ SkiaDrawer::SkiaDrawer() : _impl(std::make_unique<Impl>()) {
45
+ _impl->paint.setAntiAlias(true);
46
+ }
47
+
48
+ SkiaDrawer::~SkiaDrawer() = default;
49
+
50
+ void SkiaDrawer::setTextures(jsi::Runtime& runtime, const jsi::Value& shaders, const jsi::Value& sizes) {
51
+ jsi::Array shaderArray = shaders.asObject(runtime).asArray(runtime);
52
+ jsi::Array sizeArray = sizes.asObject(runtime).asArray(runtime);
53
+ const size_t count = shaderArray.size(runtime);
54
+ if (sizeArray.size(runtime) != count * 2) {
55
+ throw jsi::JSError(runtime, "Live2D: expected a width and height per texture");
56
+ }
57
+ _impl->shaders.clear();
58
+ std::vector<SkISize> textureSizes;
59
+ for (size_t i = 0; i < count; ++i) {
60
+ auto shader = unwrap<RNSkia::JsiSkShader>(runtime, shaderArray.getValueAtIndex(runtime, i), "an SkShader");
61
+ _impl->shaders.push_back(shader->getObject());
62
+ textureSizes.push_back(SkISize::Make(
63
+ static_cast<int>(sizeArray.getValueAtIndex(runtime, i * 2).asNumber()),
64
+ static_cast<int>(sizeArray.getValueAtIndex(runtime, i * 2 + 1).asNumber())));
65
+ }
66
+ _impl->meshes.setTextureSizes(std::move(textureSizes));
67
+ }
68
+
69
+ jsi::Value SkiaDrawer::draw(jsi::Runtime& runtime, const jsi::Value& canvasValue, bool wantStats,
70
+ const csmModel* model, const CanvasInfo& info) {
71
+ if (_impl->shaders.empty()) throw jsi::JSError(runtime, "Live2D: call setTextures before draw");
72
+ SkCanvas* canvas = unwrap<RNSkia::JsiSkCanvas>(runtime, canvasValue, "an SkCanvas")->getCanvas();
73
+ Impl& d = *_impl;
74
+
75
+ const int count = csmGetDrawableCount(model);
76
+ d.meshes.resize(count);
77
+ d.meshes.resetStats();
78
+ int layers = 0;
79
+
80
+ const int* renderOrders = cubism::renderOrders(model);
81
+ const csmFlags* dynamicFlags = csmGetDrawableDynamicFlags(model);
82
+ const csmFlags* constantFlags = csmGetDrawableConstantFlags(model);
83
+ const int* textureIndices = csmGetDrawableTextureIndices(model);
84
+ const float* opacities = csmGetDrawableOpacities(model);
85
+ const int* maskCounts = csmGetDrawableMaskCounts(model);
86
+ const int** masks = csmGetDrawableMasks(model);
87
+
88
+ // Draw order: drawable indices sorted by render order.
89
+ std::vector<int>& order = d.order;
90
+ order.resize(count);
91
+ std::iota(order.begin(), order.end(), 0);
92
+ std::sort(order.begin(), order.end(), [&](int a, int b) { return renderOrders[a] < renderOrders[b]; });
93
+
94
+ for (int i : order) {
95
+ if (!(dynamicFlags[i] & csmIsVisible) || opacities[i] <= 0) continue;
96
+ const SkVertices* vertices = d.meshes.meshOf(model, i, info);
97
+ if (!vertices) continue;
98
+
99
+ if (maskCounts[i] == 0) {
100
+ d.paint.setAlphaf(opacities[i]);
101
+ d.paint.setBlendMode(blendModeOf(constantFlags[i]));
102
+ d.drawMesh(canvas, vertices, textureIndices[i], d.paint);
103
+ continue;
104
+ }
105
+
106
+ // Draw into a layer, cut it with the union of the masks, then composite
107
+ // the layer with the drawable's blend mode.
108
+ const SkRect& bounds = vertices->bounds();
109
+ d.layerPaint.setBlendMode(blendModeOf(constantFlags[i]));
110
+ canvas->saveLayer(&bounds, &d.layerPaint);
111
+ d.paint.setAlphaf(opacities[i]);
112
+ d.paint.setBlendMode(SkBlendMode::kSrcOver);
113
+ d.drawMesh(canvas, vertices, textureIndices[i], d.paint);
114
+
115
+ d.layerPaint.setBlendMode((constantFlags[i] & csmIsInvertedMask) ? SkBlendMode::kDstOut : SkBlendMode::kDstIn);
116
+ canvas->saveLayer(&bounds, &d.layerPaint);
117
+ d.paint.setAlphaf(1);
118
+ for (int m = 0; m < maskCounts[i]; ++m) {
119
+ const int maskIndex = masks[i][m];
120
+ const SkVertices* maskVertices = d.meshes.meshOf(model, maskIndex, info);
121
+ if (maskVertices) d.drawMesh(canvas, maskVertices, textureIndices[maskIndex], d.paint);
122
+ }
123
+ canvas->restore();
124
+ canvas->restore();
125
+ layers += 2;
126
+ }
127
+
128
+ if (!wantStats) return jsi::Value::undefined();
129
+ jsi::Object stats(runtime);
130
+ stats.setProperty(runtime, "meshesBuilt", d.meshes.meshesBuilt());
131
+ stats.setProperty(runtime, "layers", layers);
132
+ stats.setProperty(runtime, "meshMs", d.meshes.meshMs());
133
+ return stats;
134
+ }
135
+
136
+ } // namespace margelo::nitro::live2dskia
137
+
138
+ #endif // LIVE2D_SKIA
@@ -0,0 +1,42 @@
1
+ #pragma once
2
+ #if LIVE2D_SKIA
3
+
4
+ #include <jsi/jsi.h>
5
+ #include <memory>
6
+ #include <vector>
7
+ #include "Live2DCubismCore.hpp"
8
+
9
+ namespace margelo::nitro::live2dskia {
10
+
11
+ namespace jsi = facebook::jsi;
12
+
13
+ struct CanvasInfo {
14
+ float originX;
15
+ float originY;
16
+ float pixelsPerUnit;
17
+ };
18
+
19
+ // Draws a model's drawables onto a react-native-skia canvas in C++, the same
20
+ // way src/rendering/drawer.ts does in JS, without building meshes through JSI.
21
+ // Not thread-safe: HybridLive2DModel serializes calls.
22
+ class SkiaDrawer {
23
+ public:
24
+ SkiaDrawer();
25
+ ~SkiaDrawer();
26
+
27
+ // shaders: SkShader[] (one per texture); sizes: [width0, height0, width1, ...].
28
+ void setTextures(jsi::Runtime& runtime, const jsi::Value& shaders, const jsi::Value& sizes);
29
+
30
+ // Draws the model's current state onto a JS SkCanvas. Returns
31
+ // { meshesBuilt, layers, meshMs } when wantStats, else undefined.
32
+ jsi::Value draw(jsi::Runtime& runtime, const jsi::Value& canvas, bool wantStats,
33
+ const Live2D::Cubism::Core::csmModel* model, const CanvasInfo& info);
34
+
35
+ private:
36
+ struct Impl;
37
+ std::unique_ptr<Impl> _impl;
38
+ };
39
+
40
+ } // namespace margelo::nitro::live2dskia
41
+
42
+ #endif // LIVE2D_SKIA
@@ -0,0 +1,174 @@
1
+ #include "HybridLive2DModel.hpp"
2
+ #include <algorithm>
3
+ #include <numeric>
4
+ #include "CubismSetup.hpp"
5
+ #include "CubismCompat.hpp"
6
+
7
+ namespace margelo::nitro::live2dskia {
8
+
9
+ using namespace Live2D::Cubism::Core;
10
+
11
+ namespace {
12
+
13
+ template <typename T>
14
+ std::shared_ptr<ArrayBuffer> copyToArrayBuffer(const T* data, size_t count) {
15
+ return ArrayBuffer::copy(reinterpret_cast<const uint8_t*>(data), count * sizeof(T));
16
+ }
17
+
18
+ Live2DUserModel::Priority toPriority(std::optional<MotionPriority> priority) {
19
+ switch (priority.value_or(MotionPriority::NORMAL)) {
20
+ case MotionPriority::IDLE: return Live2DUserModel::PriorityIdle;
21
+ case MotionPriority::FORCE: return Live2DUserModel::PriorityForce;
22
+ default: return Live2DUserModel::PriorityNormal;
23
+ }
24
+ }
25
+
26
+ } // namespace
27
+
28
+ HybridLive2DModel::HybridLive2DModel(const uint8_t* mocData, size_t mocSize, const std::string& settingsJson)
29
+ : HybridObject(TAG), HybridLive2DModelSpec() {
30
+ // The Framework must be started before any of its objects are created.
31
+ ensureCubismFramework();
32
+ _userModel = std::make_unique<Live2DUserModel>(mocData, mocSize, settingsJson);
33
+
34
+ csmVector2 size{}, origin{};
35
+ csmReadCanvasInfo(_userModel->model()->GetModel(), &size, &origin, &_pixelsPerUnit);
36
+ _canvasWidth = size.X;
37
+ _canvasHeight = size.Y;
38
+ _originX = origin.X;
39
+ _originY = origin.Y;
40
+
41
+ // Compute vertex positions, opacities and render orders for the default parameters.
42
+ _userModel->update(0);
43
+ }
44
+
45
+ double HybridLive2DModel::getDrawableCount() {
46
+ std::lock_guard lock(_mutex);
47
+ return csmGetDrawableCount(_userModel->model()->GetModel());
48
+ }
49
+
50
+ std::vector<DrawableData> HybridLive2DModel::getDrawables() {
51
+ std::lock_guard lock(_mutex);
52
+ const csmModel* model = _userModel->model()->GetModel();
53
+ const int count = csmGetDrawableCount(model);
54
+ const int* renderOrders = cubism::renderOrders(model);
55
+ const csmFlags* dynamicFlags = csmGetDrawableDynamicFlags(model);
56
+ const csmFlags* constantFlags = csmGetDrawableConstantFlags(model);
57
+ const int* textureIndices = csmGetDrawableTextureIndices(model);
58
+ const float* opacities = csmGetDrawableOpacities(model);
59
+ const int* vertexCounts = csmGetDrawableVertexCounts(model);
60
+ const csmVector2** positions = csmGetDrawableVertexPositions(model);
61
+ const csmVector2** uvs = csmGetDrawableVertexUvs(model);
62
+ const int* indexCounts = csmGetDrawableIndexCounts(model);
63
+ const unsigned short** indices = csmGetDrawableIndices(model);
64
+ const int* maskCounts = csmGetDrawableMaskCounts(model);
65
+ const int** masks = csmGetDrawableMasks(model);
66
+
67
+ std::vector<int> order(count);
68
+ std::iota(order.begin(), order.end(), 0);
69
+ std::sort(order.begin(), order.end(),
70
+ [&](int a, int b) { return renderOrders[a] < renderOrders[b]; });
71
+
72
+ std::vector<DrawableData> result;
73
+ result.reserve(count);
74
+ for (int i : order) {
75
+ result.emplace_back(i,
76
+ (dynamicFlags[i] & csmIsVisible) != 0,
77
+ textureIndices[i],
78
+ opacities[i],
79
+ constantFlags[i],
80
+ std::vector<double>(masks[i], masks[i] + maskCounts[i]),
81
+ copyToArrayBuffer(positions[i], vertexCounts[i]),
82
+ copyToArrayBuffer(uvs[i], vertexCounts[i]),
83
+ copyToArrayBuffer(indices[i], indexCounts[i]));
84
+ }
85
+ return result;
86
+ }
87
+
88
+ void HybridLive2DModel::loadPhysics(const std::string& json) {
89
+ std::lock_guard lock(_mutex);
90
+ _userModel->loadPhysics(json);
91
+ }
92
+
93
+ void HybridLive2DModel::loadPose(const std::string& json) {
94
+ std::lock_guard lock(_mutex);
95
+ _userModel->loadPose(json);
96
+ }
97
+
98
+ void HybridLive2DModel::loadMotion(const std::string& group, double index, const std::string& json) {
99
+ std::lock_guard lock(_mutex);
100
+ _userModel->loadMotion(group, static_cast<int>(index), json);
101
+ }
102
+
103
+ bool HybridLive2DModel::startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) {
104
+ std::lock_guard lock(_mutex);
105
+ if (!index) return _userModel->startRandomMotion(group, toPriority(priority));
106
+ return _userModel->startMotion(group, static_cast<int>(*index), toPriority(priority));
107
+ }
108
+
109
+ void HybridLive2DModel::loadExpression(const std::string& name, const std::string& json) {
110
+ std::lock_guard lock(_mutex);
111
+ _userModel->loadExpression(name, json);
112
+ }
113
+
114
+ bool HybridLive2DModel::setExpression(const std::optional<std::string>& name) {
115
+ std::lock_guard lock(_mutex);
116
+ return name ? _userModel->setExpression(*name) : _userModel->setRandomExpression();
117
+ }
118
+
119
+ void HybridLive2DModel::clearExpression() {
120
+ std::lock_guard lock(_mutex);
121
+ _userModel->clearExpression();
122
+ }
123
+
124
+ void HybridLive2DModel::setParameter(const std::string& id, double value) {
125
+ std::lock_guard lock(_mutex);
126
+ _userModel->setParameter(id, static_cast<float>(value));
127
+ }
128
+
129
+ void HybridLive2DModel::clearParameter(const std::string& id) {
130
+ std::lock_guard lock(_mutex);
131
+ _userModel->clearParameter(id);
132
+ }
133
+
134
+ void HybridLive2DModel::focus(double x, double y) {
135
+ std::lock_guard lock(_mutex);
136
+ _userModel->focus(static_cast<float>(std::clamp(x, -1.0, 1.0)), static_cast<float>(std::clamp(y, -1.0, 1.0)));
137
+ }
138
+
139
+ std::vector<std::string> HybridLive2DModel::hitTest(double x, double y) {
140
+ std::lock_guard lock(_mutex);
141
+ return _userModel->hitTest(static_cast<float>(x), static_cast<float>(y));
142
+ }
143
+
144
+ void HybridLive2DModel::update(double deltaSeconds) {
145
+ std::lock_guard lock(_mutex);
146
+ _userModel->update(static_cast<float>(deltaSeconds));
147
+ }
148
+
149
+ void HybridLive2DModel::loadHybridMethods() {
150
+ HybridLive2DModelSpec::loadHybridMethods();
151
+ #if LIVE2D_SKIA
152
+ registerHybrids(this, [](Prototype& prototype) {
153
+ prototype.registerRawHybridMethod("setTextures", 2, &HybridLive2DModel::setTexturesRaw);
154
+ prototype.registerRawHybridMethod("drawTo", 2, &HybridLive2DModel::drawToRaw);
155
+ });
156
+ #endif
157
+ }
158
+
159
+ #if LIVE2D_SKIA
160
+ jsi::Value HybridLive2DModel::setTexturesRaw(jsi::Runtime& runtime, const jsi::Value&, const jsi::Value* args, size_t) {
161
+ std::lock_guard lock(_mutex);
162
+ _drawer.setTextures(runtime, args[0], args[1]);
163
+ return jsi::Value::undefined();
164
+ }
165
+
166
+ jsi::Value HybridLive2DModel::drawToRaw(jsi::Runtime& runtime, const jsi::Value&, const jsi::Value* args, size_t) {
167
+ std::lock_guard lock(_mutex);
168
+ const CanvasInfo info{_originX, _originY, _pixelsPerUnit};
169
+ return _drawer.draw(runtime, args[0], args[1].isBool() && args[1].getBool(),
170
+ _userModel->model()->GetModel(), info);
171
+ }
172
+ #endif
173
+
174
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,64 @@
1
+ #pragma once
2
+ #include <memory>
3
+ #include <mutex>
4
+ #include <string>
5
+ #include <vector>
6
+ #include "HybridLive2DModelSpec.hpp"
7
+ #include "Live2DUserModel.hpp"
8
+ #include "SkiaDrawer.hpp"
9
+
10
+ namespace margelo::nitro::live2dskia {
11
+
12
+ class HybridLive2DModel : public HybridLive2DModelSpec {
13
+ public:
14
+ // Takes a .moc3 file's bytes and its model3.json. Throws std::runtime_error if they can't be loaded.
15
+ HybridLive2DModel(const uint8_t* mocData, size_t mocSize, const std::string& settingsJson);
16
+
17
+ double getCanvasWidth() override { return _canvasWidth; }
18
+ double getCanvasHeight() override { return _canvasHeight; }
19
+ double getOriginX() override { return _originX; }
20
+ double getOriginY() override { return _originY; }
21
+ double getPixelsPerUnit() override { return _pixelsPerUnit; }
22
+ double getDrawableCount() override;
23
+
24
+ std::vector<DrawableData> getDrawables() override;
25
+ void loadPhysics(const std::string& json) override;
26
+ void loadPose(const std::string& json) override;
27
+ void loadMotion(const std::string& group, double index, const std::string& json) override;
28
+ bool startMotion(const std::string& group, std::optional<double> index, std::optional<MotionPriority> priority) override;
29
+ void loadExpression(const std::string& name, const std::string& json) override;
30
+ bool setExpression(const std::optional<std::string>& name) override;
31
+ void clearExpression() override;
32
+ void setParameter(const std::string& id, double value) override;
33
+ void clearParameter(const std::string& id) override;
34
+ void focus(double x, double y) override;
35
+ std::vector<std::string> hitTest(double x, double y) override;
36
+ void update(double deltaSeconds) override;
37
+
38
+ protected:
39
+ // Adds the raw JSI methods that take react-native-skia objects, when built with Skia.
40
+ void loadHybridMethods() override;
41
+
42
+ #if LIVE2D_SKIA
43
+ private:
44
+ // setTextures(shaders: SkShader[], sizes: number[]): void
45
+ jsi::Value setTexturesRaw(jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* args, size_t count);
46
+ // drawTo(canvas: SkCanvas, wantStats: boolean): DrawStats | undefined
47
+ jsi::Value drawToRaw(jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* args, size_t count);
48
+ #endif
49
+
50
+ private:
51
+ // The model is updated on the animation thread and controlled from the JS thread.
52
+ std::mutex _mutex;
53
+ std::unique_ptr<Live2DUserModel> _userModel;
54
+ #if LIVE2D_SKIA
55
+ SkiaDrawer _drawer;
56
+ #endif
57
+ float _canvasWidth = 0;
58
+ float _canvasHeight = 0;
59
+ float _originX = 0;
60
+ float _originY = 0;
61
+ float _pixelsPerUnit = 0;
62
+ };
63
+
64
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,10 @@
1
+ #include "HybridLive2dSkia.hpp"
2
+ #include "HybridLive2DModel.hpp"
3
+
4
+ namespace margelo::nitro::live2dskia {
5
+
6
+ std::shared_ptr<HybridLive2DModelSpec> HybridLive2dSkia::loadModel(const std::shared_ptr<ArrayBuffer>& moc, const std::string& settingsJson) {
7
+ return std::make_shared<HybridLive2DModel>(moc->data(), moc->size(), settingsJson);
8
+ }
9
+
10
+ } // namespace margelo::nitro::live2dskia
@@ -0,0 +1,11 @@
1
+ #pragma once
2
+ #include "HybridLive2dSkiaSpec.hpp"
3
+
4
+ namespace margelo::nitro::live2dskia {
5
+ class HybridLive2dSkia : public HybridLive2dSkiaSpec {
6
+ public:
7
+ HybridLive2dSkia() : HybridObject(TAG), HybridLive2dSkiaSpec() {}
8
+
9
+ std::shared_ptr<HybridLive2DModelSpec> loadModel(const std::shared_ptr<ArrayBuffer>& moc, const std::string& settingsJson) override;
10
+ };
11
+ } // namespace margelo::nitro::live2dskia
package/ios/Bridge.h ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Bridge.h
3
+ // react-native-live2d-skia
4
+ //
5
+ // Created by airisu.dev on 10/10/2026
6
+ //
7
+
8
+ #pragma once
package/nitro.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://nitro.margelo.com/nitro.schema.json",
3
+ "cxxNamespace": ["live2dskia"],
4
+ "ios": {
5
+ "iosModuleName": "Live2dSkia"
6
+ },
7
+ "android": {
8
+ "androidNamespace": ["live2dskia"],
9
+ "androidCxxLibName": "Live2dSkia"
10
+ },
11
+ "autolinking": {
12
+ "Live2dSkia": {
13
+ "all": {
14
+ "language": "c++",
15
+ "implementationClassName": "HybridLive2dSkia"
16
+ }
17
+ }
18
+ },
19
+ "ignorePaths": ["**/node_modules"]
20
+ }
@@ -0,0 +1 @@
1
+ ** linguist-generated=true
@@ -0,0 +1,82 @@
1
+ #
2
+ # Live2dSkia+autolinking.cmake
3
+ # This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ # https://github.com/mrousavy/nitro
5
+ # Copyright © Marc Rousavy @ Margelo
6
+ #
7
+
8
+ # This is a CMake file that adds all files generated by Nitrogen
9
+ # to the current CMake project.
10
+ #
11
+ # To use it, add this to your CMakeLists.txt:
12
+ # ```cmake
13
+ # include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/Live2dSkia+autolinking.cmake)
14
+ # ```
15
+
16
+ # Define a flag to check if we are building properly
17
+ add_definitions(-DBUILDING_LIVE2DSKIA_WITH_GENERATED_CMAKE_PROJECT)
18
+
19
+ # Enable Raw Props parsing in react-native (for Nitro Views)
20
+ add_definitions(-DRN_SERIALIZABLE_STATE)
21
+
22
+ # Add all headers that were generated by Nitrogen
23
+ include_directories(
24
+ "../nitrogen/generated/shared/c++"
25
+ "../nitrogen/generated/android/c++"
26
+ "../nitrogen/generated/android/"
27
+ )
28
+
29
+ # Add all .cpp sources that were generated by Nitrogen
30
+ target_sources(
31
+ # CMake project name (Android C++ library name)
32
+ Live2dSkia PRIVATE
33
+ # Autolinking Setup
34
+ ../nitrogen/generated/android/Live2dSkiaOnLoad.cpp
35
+ # Shared Nitrogen C++ sources
36
+ ../nitrogen/generated/shared/c++/HybridLive2DModelSpec.cpp
37
+ ../nitrogen/generated/shared/c++/HybridLive2dSkiaSpec.cpp
38
+ # Android-specific Nitrogen C++ sources
39
+
40
+ )
41
+
42
+ # From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake
43
+ # Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake
44
+ target_compile_definitions(
45
+ Live2dSkia PRIVATE
46
+ -DFOLLY_NO_CONFIG=1
47
+ -DFOLLY_HAVE_CLOCK_GETTIME=1
48
+ -DFOLLY_USE_LIBCPP=1
49
+ -DFOLLY_CFG_NO_COROUTINES=1
50
+ -DFOLLY_MOBILE=1
51
+ -DFOLLY_HAVE_RECVMMSG=1
52
+ -DFOLLY_HAVE_PTHREAD=1
53
+ # Once we target android-23 above, we can comment
54
+ # the following line. NDK uses GNU style stderror_r() after API 23.
55
+ -DFOLLY_HAVE_XSI_STRERROR_R=1
56
+ )
57
+
58
+ # Add all libraries required by the generated specs
59
+ find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++
60
+ find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule)
61
+ find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all HybridObjects and use the Nitro core library
62
+
63
+ # Link all libraries together
64
+ target_link_libraries(
65
+ Live2dSkia
66
+ fbjni::fbjni # <-- Facebook C++ JNI helpers
67
+ ReactAndroid::jsi # <-- RN: JSI
68
+ react-native-nitro-modules::NitroModules # <-- NitroModules Core :)
69
+ )
70
+
71
+ # Link react-native (different prefab between RN 0.75 and RN 0.76)
72
+ if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
73
+ target_link_libraries(
74
+ Live2dSkia
75
+ ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab
76
+ )
77
+ else()
78
+ target_link_libraries(
79
+ Live2dSkia
80
+ ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core
81
+ )
82
+ endif()