@airisu-dev/react-native-live2d-skia 0.0.0-stage → 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/Live2dSkia.podspec +159 -0
  3. package/README.md +258 -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 +149 -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 +195 -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/decodeImage.ts +12 -0
  67. package/src/rendering/drawer.ts +256 -0
  68. package/src/rendering/skia.ts +26 -0
  69. package/src/rendering/skiaTypes.ts +99 -0
  70. package/src/rendering/ticker/addToTicker.ts +52 -0
  71. package/src/rendering/ticker/constants.ts +11 -0
  72. package/src/rendering/ticker/frames.ts +35 -0
  73. package/src/rendering/ticker/index.ts +5 -0
  74. package/src/rendering/ticker/loop.ts +115 -0
  75. package/src/rendering/ticker/state.ts +65 -0
  76. package/src/rendering/ticker/stats.ts +36 -0
  77. package/src/rendering/ticker/types.ts +57 -0
  78. package/src/specs/react-native-live2d-skia.nitro.ts +88 -0
  79. package/src/utils/clock.ts +10 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 airisu.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,159 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ # Cubism SDK for Native: proprietary, so each app downloads it from Live2D and
6
+ # sets its folder (app.plugin.js writes live2dSkia.cubismSdkPath to
7
+ # Podfile.properties.json, relative to ios/).
8
+ ios_dir = Pod::Config.instance.installation_root.to_s
9
+ podfile_properties_path = File.join(ios_dir, "Podfile.properties.json")
10
+ podfile_properties = File.exist?(podfile_properties_path) ? JSON.parse(File.read(podfile_properties_path)) : {}
11
+ configured_sdk_dir = ENV["LIVE2D_SKIA_CUBISM_SDK"] || podfile_properties["live2dSkia.cubismSdkPath"]
12
+ unless configured_sdk_dir
13
+ raise "[react-native-live2d-skia] Cubism SDK for Native isn't configured. Add the plugin to app.json: " \
14
+ "[\"@airisu-dev/react-native-live2d-skia\", { \"cubismSdkPath\": \"./vendor/CubismSdkForNative\" }], download the SDK with " \
15
+ "`@airisu-dev/react-native-live2d-skia fetch-sdk --accept-license` (through npx, bunx, yarn or pnpm), then prebuild."
16
+ end
17
+ cubism_sdk_dir = File.expand_path(configured_sdk_dir, ios_dir)
18
+ cubism_info = File.join(cubism_sdk_dir, "cubism-info.yml")
19
+ unless File.file?(cubism_info)
20
+ raise "[react-native-live2d-skia] Cubism SDK for Native not found at #{cubism_sdk_dir} (no cubism-info.yml). " \
21
+ "Download it with `@airisu-dev/react-native-live2d-skia fetch-sdk --accept-license` (through npx, bunx, yarn or pnpm)."
22
+ end
23
+ cubism_version = File.read(cubism_info)[/^version:\s*(\S+)/, 1] || "unknown"
24
+
25
+ # A Cubism SDK version as a number: major * 10000 + release * 100 + patch, so
26
+ # 5-r.4.1 is 50401 (a -beta suffix is ignored). Nil if it isn't one.
27
+ cubism_version_number = lambda do |version|
28
+ m = version.match(/^(\d+)-r\.(\d+)(?:\.(\d+))?/)
29
+ m && m[1].to_i * 10000 + m[2].to_i * 100 + m[3].to_i
30
+ end
31
+
32
+ # Listed versions (cubismSdk.versions in package.json) are supported. A newer
33
+ # release of a listed major only warns; anything else fails.
34
+ listed_versions = package["cubismSdk"]["versions"].keys
35
+ cubism_version_num = cubism_version_number.call(cubism_version)
36
+ unless listed_versions.include?(cubism_version)
37
+ newest_of_major = cubism_version_num && listed_versions.map { |v| cubism_version_number.call(v) }
38
+ .select { |n| n / 10000 == cubism_version_num / 10000 }.max
39
+ if newest_of_major.nil? || cubism_version_num < newest_of_major
40
+ raise "[react-native-live2d-skia] Cubism SDK for Native #{cubism_version} at #{cubism_sdk_dir} isn't supported: use #{listed_versions.join(', ')}."
41
+ end
42
+ Pod::UI.warn "[react-native-live2d-skia] Cubism SDK for Native #{cubism_version} found, tested with #{listed_versions.join(', ')}."
43
+ end
44
+
45
+ # CocoaPods only compiles files inside the pod, and the SDK ships one Core static
46
+ # library per slice. So copy the Framework source in, and package the Core
47
+ # slices as an xcframework (CocoaPods then links the right one for device or
48
+ # simulator). Redone when the SDK's folder or version changes. Gitignored, not published.
49
+ cubism_stage_dir = File.join(__dir__, "cubism-sdk")
50
+ cubism_stamp = "#{cubism_sdk_dir}\n#{cubism_version}\n"
51
+ cubism_stamp_path = File.join(cubism_stage_dir, ".stamp")
52
+ unless File.exist?(cubism_stamp_path) && File.read(cubism_stamp_path) == cubism_stamp
53
+ require "fileutils"
54
+ require "tmpdir"
55
+ FileUtils.rm_rf(cubism_stage_dir)
56
+ FileUtils.mkdir_p(cubism_stage_dir)
57
+ FileUtils.cp_r(File.join(cubism_sdk_dir, "Framework", "src"), File.join(cubism_stage_dir, "Framework"))
58
+ core = File.join(cubism_sdk_dir, "Core")
59
+ FileUtils.mkdir_p(File.join(cubism_stage_dir, "Core"))
60
+ FileUtils.cp_r(File.join(core, "include"), File.join(cubism_stage_dir, "Core", "include"))
61
+ lib = File.join(core, "lib", "ios")
62
+ Dir.mktmpdir do |tmp|
63
+ simulator_lib = File.join(tmp, "libLive2DCubismCore.a")
64
+ run = lambda do |*cmd|
65
+ system(*cmd) or raise "[react-native-live2d-skia] Failed to package Cubism Core: #{cmd.join(' ')}"
66
+ end
67
+ run.call("lipo", "-create",
68
+ File.join(lib, "Release-iphonesimulator-arm64", "libLive2DCubismCore.a"),
69
+ File.join(lib, "Release-iphonesimulator-x86_64", "libLive2DCubismCore.a"),
70
+ "-output", simulator_lib)
71
+ run.call("xcodebuild", "-create-xcframework",
72
+ "-library", File.join(lib, "Release-iphoneos", "libLive2DCubismCore.a"),
73
+ "-headers", File.join(core, "include"),
74
+ "-library", simulator_lib, "-headers", File.join(core, "include"),
75
+ "-output", File.join(cubism_stage_dir, "Live2DCubismCore.xcframework"))
76
+ end
77
+ File.write(cubism_stamp_path, cubism_stamp)
78
+ Pod::UI.puts "[react-native-live2d-skia] Using Cubism SDK for Native #{cubism_version} from #{cubism_sdk_dir}"
79
+ end
80
+
81
+ # Draw in C++ onto Skia canvases (cpp/drawing/SkiaDrawer.cpp). Skia is linked statically
82
+ # into the app by its pod (named react-native-skia in v3 and v2), so this only
83
+ # needs its headers and the defines it's built with. Which Skia: the one
84
+ # app.plugin.js found in the app's package.json (live2dSkia.skiaPackage), v3
85
+ # (Graphite) or v2 (Ganesh), resolved from the app since a monorepo can have
86
+ # both installed. Without it, models are drawn in JS.
87
+ rn_skia_package = podfile_properties["live2dSkia.skiaPackage"].to_s
88
+ rn_skia_dir = nil
89
+ unless rn_skia_package.empty?
90
+ app_dir = File.dirname(ios_dir)
91
+ resolved = IO.popen(["node", "-p",
92
+ "require('path').dirname(require.resolve(process.argv[1] + '/package.json', { paths: [process.argv[2]] }))",
93
+ rn_skia_package, app_dir], err: File::NULL, &:read).to_s.strip
94
+ rn_skia_dir = resolved if $?.success? && File.file?(File.join(resolved, "cpp", "api", "JsiSkCanvas.h"))
95
+ end
96
+ rn_skia_graphite = rn_skia_package == "react-native-skia"
97
+
98
+ Pod::Spec.new do |s|
99
+ s.name = "Live2dSkia"
100
+ s.version = package["version"]
101
+ s.summary = package["description"]
102
+ s.homepage = package["homepage"]
103
+ s.license = package["license"]
104
+ s.authors = package["author"]
105
+
106
+ s.platforms = { :ios => min_ios_version_supported }
107
+ s.source = { :git => "https://github.com/airisu-dev/react-native-live2d-skia.git", :tag => "#{s.version}" }
108
+
109
+ s.source_files = [
110
+ # Implementation (Swift)
111
+ "ios/**/*.{swift}",
112
+ # Autolinking/Registration (Objective-C++)
113
+ "ios/**/*.{m,mm}",
114
+ # Implementation (C++ objects)
115
+ "cpp/**/*.{hpp,cpp}",
116
+ # Cubism Native Framework (motions, physics, pose, ...)
117
+ "cubism-sdk/Framework/**/*.{hpp,cpp}",
118
+ ]
119
+ # Its Rendering/<backend> folders are left out: we draw with Skia
120
+ # (see cpp/cubism/CubismRendererStub.cpp).
121
+ s.exclude_files = "cubism-sdk/Framework/Rendering/*/**/*"
122
+
123
+ s.vendored_frameworks = "cubism-sdk/Live2DCubismCore.xcframework"
124
+ # Set before add_nitrogen_files, which merges its own settings into this.
125
+ header_search_paths = [
126
+ "\"$(PODS_TARGET_SRCROOT)/cubism-sdk/Core/include\"",
127
+ "\"$(PODS_TARGET_SRCROOT)/cubism-sdk/Framework\"",
128
+ "\"$(PODS_TARGET_SRCROOT)/cpp/hybrid\"",
129
+ "\"$(PODS_TARGET_SRCROOT)/cpp/cubism\"",
130
+ "\"$(PODS_TARGET_SRCROOT)/cpp/drawing\"",
131
+ ]
132
+ # The SDK's version, for cpp/cubism/CubismCompat.hpp.
133
+ preprocessor_definitions = ["$(inherited)", "LIVE2D_SKIA_CUBISM_SDK=#{cubism_version_num}"]
134
+ if rn_skia_dir
135
+ Pod::UI.puts "[Live2dSkia] Drawing with Skia in C++ (#{rn_skia_dir}, #{rn_skia_graphite ? 'Graphite' : 'Ganesh'})"
136
+ s.dependency "react-native-skia"
137
+ skia_headers = ["cpp", "cpp/skia", "cpp/api", "cpp/jsi", "cpp/rnskia", "cpp/rnskia/values", "cpp/utils"]
138
+ skia_headers << "cpp/dawn/include" if rn_skia_graphite
139
+ header_search_paths += skia_headers.map { |dir| "\"#{File.join(rn_skia_dir, dir)}\"" }
140
+ # Same as the Skia pod: struct layouts must match.
141
+ preprocessor_definitions += ["LIVE2D_SKIA=1",
142
+ "SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
143
+ "SK_DISABLE_LEGACY_SHAPER_FACTORY=1"]
144
+ preprocessor_definitions += rn_skia_graphite ? ["SK_GRAPHITE=1"] : ["SK_METAL=1", "SK_GANESH=1"]
145
+ else
146
+ Pod::UI.puts "[Live2dSkia] No Skia to draw with in C++, drawing in JS"
147
+ end
148
+ s.pod_target_xcconfig = {
149
+ "HEADER_SEARCH_PATHS" => header_search_paths.join(" "),
150
+ "GCC_PREPROCESSOR_DEFINITIONS" => preprocessor_definitions.join(" "),
151
+ }
152
+
153
+ load 'nitrogen/generated/ios/Live2dSkia+autolinking.rb'
154
+ add_nitrogen_files(s)
155
+
156
+ s.dependency 'React-jsi'
157
+ s.dependency 'React-callinvoker'
158
+ install_modules_dependencies(s)
159
+ end
package/README.md CHANGED
@@ -1,3 +1,259 @@
1
- # Temporary Holding Version
1
+ # react-native-live2d-skia
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Live2D Cubism models rendered natively in React Native with Skia. No WebView, a hooks API in the style of `pixi-live2d-display`, and one shared C++ codebase for iOS and Android.
4
+
5
+ [![Version](https://img.shields.io/npm/v/@airisu-dev/react-native-live2d-skia.svg)](https://www.npmjs.com/package/@airisu-dev/react-native-live2d-skia)
6
+ [![Downloads](https://img.shields.io/npm/dm/@airisu-dev/react-native-live2d-skia.svg)](https://www.npmjs.com/package/@airisu-dev/react-native-live2d-skia)
7
+ [![License](https://img.shields.io/npm/l/@airisu-dev/react-native-live2d-skia.svg)](https://github.com/airisu-dev/react-native-live2d-skia/blob/main/LICENSE)
8
+
9
+ Cubism Core and the Cubism Native Framework only run the model (motions, physics, expressions). Each drawable is drawn with Skia's `drawVertices`, so Skia picks the GPU backend: Metal on iOS, Vulkan or OpenGL on Android.
10
+
11
+ ## Requirements
12
+
13
+ - Expo with development builds (`expo prebuild`). The React Native CLI isn't supported.
14
+ - React Native 0.78 or higher (Nitro Views), Node 18 or higher.
15
+ - Skia, either version:
16
+
17
+ | Skia | Package | Renderer |
18
+ | ---- | ------------------------------------ | ------------------------ |
19
+ | v3 | `react-native-skia` (3.3+) | Graphite (Vulkan, Metal) |
20
+ | v2 | `@shopify/react-native-skia` (2.10+) | Ganesh (OpenGL, Metal) |
21
+
22
+ ## Installation
23
+
24
+ ### 1. Install the packages
25
+
26
+ ```bash
27
+ npx expo install @airisu-dev/react-native-live2d-skia react-native-nitro-modules react-native-skia react-native-reanimated react-native-worklets react-native-gesture-handler expo-asset expo-file-system
28
+ ```
29
+
30
+ Install one Skia package, not both. For v2, install `@shopify/react-native-skia` instead of `react-native-skia`, at 2.10 or later: `expo install` can pick an older version (2.6.2 for SDK 57), which hands Skia objects to C++ differently. Pin a newer one, e.g. `npx expo install @shopify/react-native-skia@2.14.0`. The config plugin reads which one the app depends on.
31
+
32
+ The app needs a `GestureHandlerRootView` at its root.
33
+
34
+ ### 2. Configure Metro
35
+
36
+ Models are bundled as assets. Add `moc3` to Metro's `assetExts` in `metro.config.js` (create it with `npx expo customize metro.config.js` if the app has none):
37
+
38
+ ```js
39
+ const { getDefaultConfig } = require('expo/metro-config')
40
+
41
+ const config = getDefaultConfig(__dirname)
42
+
43
+ // Bundle Live2D model data as assets.
44
+ config.resolver.assetExts.push('moc3')
45
+
46
+ module.exports = config
47
+ ```
48
+
49
+ The model's JSON files (`model3.json`, motions, physics, expressions) and textures bundle with Expo's defaults. Models loaded from a URL don't need this.
50
+
51
+ ### 3. Get the Cubism SDK for Native
52
+
53
+ The package doesn't include Live2D's Cubism SDK: its Core is proprietary, and each app downloads it from Live2D, accepting
54
+ [Live2D's license terms](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html).
55
+ 1. Download it (it's checked against a SHA-256):
56
+
57
+ ```bash
58
+ npx @airisu-dev/react-native-live2d-skia fetch-sdk --accept-license # or bunx, yarn, pnpm
59
+ ```
60
+
61
+ It extracts to `./vendor/CubismSdkForNative` (change it with `--out`). Add `/vendor/` to `.gitignore`, so it's never in a public repo. On EAS Build, run the same command in an `eas-build-post-install` script.
62
+
63
+ 2. Point the config plugin at it, in `app.json`:
64
+
65
+ ```json
66
+ "plugins": [["@airisu-dev/react-native-live2d-skia", { "cubismSdkPath": "./vendor/CubismSdkForNative" }]]
67
+ ```
68
+
69
+ 3. Run `npx expo prebuild`, then build. A missing or unsupported SDK stops the build with the path it looked at.
70
+
71
+ Publishing an app built on the SDK may also need Live2D's
72
+ [Cubism SDK Release License](https://www.live2d.com/en/sdk/license/) (businesses above 10M JPY a year).
73
+
74
+ ## Usage
75
+
76
+ ```tsx
77
+ import { Canvas } from '@shopify/react-native-skia' // or 'react-native-skia'
78
+ import { GestureDetector } from 'react-native-gesture-handler'
79
+ import { Live2D, useLive2DGesture, useLive2DModel } from '@airisu-dev/react-native-live2d-skia'
80
+
81
+ export function Mao() {
82
+ // A require.context of the model's folder: the files its model3.json lists are loaded from it.
83
+ const { model, error, loading, retry } = useLive2DModel(
84
+ require.context('./Mao', true, /\.(json|moc3|png)$/),
85
+ )
86
+ const gesture = useLive2DGesture(model, { x: 0, y: 0, scale: 0.4 })
87
+
88
+ return (
89
+ <GestureDetector gesture={gesture}>
90
+ <Canvas style={{ flex: 1 }}>
91
+ <Live2D model={model} x={0} y={0} scale={0.4} />
92
+ </Canvas>
93
+ </GestureDetector>
94
+ )
95
+ }
96
+ ```
97
+
98
+ `useLive2DModel` loads once on mount and doesn't throw. The source is read once, so remount the component (e.g. change its `key`) to switch models. The canvas is an ordinary view: size it with styles. Models are drawn at the `x`, `y` and `scale` they're given and clipped to the canvas, so fit them to the measured size.
99
+
100
+ A tap runs the model's reaction (a `Tap<area>` motion, or an expression for `Head`), and a drag makes it look at the finger.
101
+
102
+ ### Loading from a URL
103
+
104
+ ```tsx
105
+ const { model, error, retry } = useLive2DModel(
106
+ 'https://example.com/haru/haru_greeter_t03.model3.json',
107
+ )
108
+ ```
109
+
110
+ A string is the URL of a `model3.json`. The files it lists are resolved relative to it, and spaces in file names are escaped.
111
+ - Every file (JSON, moc, textures) is fetched in parallel with `expo/fetch`. The only caching is the platform's HTTP cache, so a persistent offline copy is up to the app.
112
+ - Motion sounds are ignored.
113
+ - A failed fetch ends up in `error` as `Live2D: failed to fetch <url> (<status>)`, and network errors (e.g. offline) come through the same way. `retry()` tries again. URLs in error messages never include their query, so tokens don't leak into the UI or logs.
114
+ - Cubism 2 models (`model.json`, `.moc`) can't be loaded: Cubism Core 5 only reads `.moc3`.
115
+
116
+ #### Protected models
117
+
118
+ ```tsx
119
+ // 1. A token in the query: reused for every file on the same host.
120
+ useLive2DModel('https://cdn.example.com/haru/haru.model3.json?token=abc')
121
+
122
+ // 2. Headers: sent with every request.
123
+ useLive2DModel(url, { headers: { Authorization: `Bearer ${token}` } })
124
+
125
+ // 3. A signature per file (e.g. S3 presigned URLs): sign each one yourself.
126
+ useLive2DModel(signedModel3Url, {
127
+ resolveUrl: (url, file) => api.sign(url), // may return a promise
128
+ })
129
+ ```
130
+
131
+ Options are read once, like the source, and `retry()` reuses them. To load with a fresh token, remount. `resolveUrl` runs on every load, so it can fetch fresh signatures.
132
+
133
+ ### Loading from files
134
+
135
+ Pass the files yourself as a `Live2DModelSource`:
136
+
137
+ ```tsx
138
+ useLive2DModel({
139
+ settings: require('./Mao/Mao.model3.json'),
140
+ moc: require('./Mao/Mao.moc3'),
141
+ textures: [require('./Mao/Mao.2048/texture_00.png')],
142
+ physics: require('./Mao/Mao.physics3.json'),
143
+ motions: { Idle: [require('./Mao/motions/idle.motion3.json')] },
144
+ })
145
+ ```
146
+
147
+ ### Several models in one canvas
148
+
149
+ ```tsx
150
+ const gesture = useLive2DGestures([
151
+ { model: hiyori, ...hiyoriPlace, onMove: (x, y) => moveHiyori(x, y) },
152
+ { model: mao, ...maoPlace, onMove: (x, y) => moveMao(x, y) },
153
+ ])
154
+
155
+ <GestureDetector gesture={gesture}>
156
+ <Canvas style={{ flex: 1 }}>
157
+ <Rect … /> {/* behind the models */}
158
+ <Live2D model={hiyori} {...hiyoriPlace} />
159
+ <Live2D model={mao} {...maoPlace} />
160
+ <RoundedRect … /> {/* over the models */}
161
+ </Canvas>
162
+ </GestureDetector>
163
+ ```
164
+
165
+ 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
+
167
+ ### Controlling the model
168
+
169
+ Points are in model pixels: the model's canvas at its native size (`model.width` x `model.height`), origin top-left.
170
+
171
+ ```ts
172
+ model.motion('TapBody') // random motion of a group
173
+ model.motion('TapBody', 0, 'force') // given motion and priority (idle / normal / force)
174
+ model.expression('exp_01') // expression; random without a name
175
+ model.resetExpression()
176
+ model.setParameter('ParamAngleX', 30) // held over motions until resetParameter
177
+ model.resetParameter('ParamAngleX')
178
+ model.focus(x, y) // look at a point, easing
179
+ model.resetFocus()
180
+ model.hitTest(x, y) // → ['Head']
181
+ ```
182
+
183
+ Methods can be called from the JS thread while the model animates on its background thread: the native model is locked.
184
+
185
+ ## API
186
+
187
+ ### `useLive2DModel(source, options?)`
188
+
189
+ `source` is a `require.context` of the model's folder, a `Live2DModelSource`, or the URL of a `model3.json`. It returns:
190
+
191
+ | Field | Description |
192
+ | --------- | ---------------------------------------------------------------- |
193
+ | `model` | The loaded model, `null` while loading or after an error |
194
+ | `error` | Why the last load failed (e.g. a network error for a URL source) |
195
+ | `loading` | `true` while a load is in progress |
196
+ | `retry()` | Loads again |
197
+
198
+ | Option | Description |
199
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
+ | `headers` | Sent with every request for a remote file |
201
+ | `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 |
202
+
203
+ ### `<Live2D />`
204
+
205
+ | Prop | Description |
206
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
207
+ | `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` |
210
+ | `paused` | Stops animating, keeps the current frame. Default `false` |
211
+ | `maxFps` | Caps the frame rate (e.g. `30` to save battery). Default: the display's refresh rate |
212
+ | `onFrameStats(stats)` | About once a second: `fps`, `targetFps`, per-frame times (`workMs`, `updateMs`, `drawablesMs`, `drawMs`, `meshMs`), `meshesBuilt`, `layers` (mask layers), `models` and `loopWorkMs` |
213
+
214
+ ### `useLive2DGestures(targets, options?)`
215
+
216
+ Returns one gesture for a `<GestureDetector>` around the `<Canvas>`. Gestures need a native view, so this can't be a prop of `<Live2D>`, which is drawn inside the canvas. List targets in drawing order.
217
+ - A drag makes every model look at the finger.
218
+ - A tap goes to the topmost model with a hit area under it.
219
+ - A long press (350 ms) on a model's hit area picks it up, if it has `onMove`.
220
+
221
+ | Target field | Description |
222
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
223
+ | `model` | `model` from `useLive2DModel` |
224
+ | `x`, `y`, `scale` | Same as its `<Live2D>` props, to map touches to the model |
225
+ | `onHit(hitAreas)` | Called on a tap on its hit areas. Return `true` to skip the default: a random `Tap<Area>` motion (e.g. `TapBody`), else a random expression for `Head`, else a random `Tap` motion (models with one tap group) |
226
+ | `onMove(x, y)` | Makes it draggable. Called with its new top-left position as the finger moves, keeping the point where it was grabbed; the app moves it |
227
+
228
+ | Option | Description |
229
+ | ------- | -------------------------------------------------------- |
230
+ | `focus` | Models follow the finger while it's down. Default `true` |
231
+
232
+ The gesture is built once; targets are read at touch time, so they can change on every render.
233
+
234
+ ### `useLive2DGesture(model, options?)`
235
+
236
+ The same for one model: `options` is a target (without `model`) plus `focus`.
237
+
238
+ ### `Live2DModel`
239
+
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 |
250
+
251
+ ## Contributing
252
+
253
+ Pull requests are welcome. For major changes, please open an issue first. To build and run the repo, see [DEVELOPMENT.md](https://github.com/airisu-dev/react-native-live2d-skia/blob/main/DEVELOPMENT.md).
254
+
255
+ ## License
256
+
257
+ MIT. The Cubism SDK is not part of this package; it's under [Live2D's license](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html).
258
+
259
+ Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).
@@ -0,0 +1,113 @@
1
+ project(Live2dSkia)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set (PACKAGE_NAME Live2dSkia)
5
+ set (CMAKE_VERBOSE_MAKEFILE ON)
6
+ set (CMAKE_CXX_STANDARD 20)
7
+
8
+ # Enable Raw Props parsing in react-native (for Nitro Views)
9
+ add_compile_options(-DRN_SERIALIZABLE_STATE=1)
10
+
11
+ # Cubism SDK for Native: proprietary, downloaded by each app. build.gradle
12
+ # passes its folder (and checks its version), see app.plugin.js.
13
+ if (NOT CUBISM_SDK_DIR OR NOT CUBISM_SDK_VERSION_NUMBER)
14
+ message(FATAL_ERROR "[react-native-live2d-skia] CUBISM_SDK_DIR isn't set: build through Gradle, "
15
+ "with live2dSkia.cubismSdkPath set by the react-native-live2d-skia config plugin.")
16
+ endif()
17
+ # The SDK's version, for cpp/cubism/CubismCompat.hpp.
18
+ add_compile_definitions(LIVE2D_SKIA_CUBISM_SDK=${CUBISM_SDK_VERSION_NUMBER})
19
+
20
+ # Cubism Core (closed source): the prebuilt static library.
21
+ set (CUBISM_CORE_DIR "${CUBISM_SDK_DIR}/Core")
22
+ set (CUBISM_CORE_LIB "${CUBISM_CORE_DIR}/lib/android/${ANDROID_ABI}/libLive2DCubismCore.a")
23
+ if (NOT EXISTS "${CUBISM_CORE_LIB}")
24
+ message(FATAL_ERROR "[react-native-live2d-skia] Cubism Core not found for ${ANDROID_ABI} at ${CUBISM_CORE_LIB}.")
25
+ endif()
26
+ add_library(Live2DCubismCore STATIC IMPORTED)
27
+ set_target_properties(Live2DCubismCore PROPERTIES IMPORTED_LOCATION "${CUBISM_CORE_LIB}")
28
+
29
+ # Cubism Native Framework (motions, physics, pose, ...): the source in the same
30
+ # SDK, so it always matches the Core.
31
+ # Its Rendering/<backend> folders are left out: we draw with Skia
32
+ # (see ../cpp/cubism/CubismRendererStub.cpp).
33
+ set (CUBISM_FRAMEWORK_DIR "${CUBISM_SDK_DIR}/Framework/src")
34
+ if (NOT EXISTS "${CUBISM_FRAMEWORK_DIR}/CubismFramework.cpp")
35
+ message(FATAL_ERROR "[react-native-live2d-skia] Cubism Native Framework not found at ${CUBISM_FRAMEWORK_DIR}.")
36
+ endif()
37
+ file(GLOB_RECURSE CUBISM_FRAMEWORK_SOURCES "${CUBISM_FRAMEWORK_DIR}/*.cpp")
38
+ list(FILTER CUBISM_FRAMEWORK_SOURCES EXCLUDE REGEX "/Rendering/[^/]+/")
39
+ add_library(CubismFramework STATIC ${CUBISM_FRAMEWORK_SOURCES})
40
+ target_include_directories(CubismFramework PUBLIC "${CUBISM_FRAMEWORK_DIR}" "${CUBISM_CORE_DIR}/include")
41
+ # Third-party code: don't flood the build log with its warnings.
42
+ target_compile_options(CubismFramework PRIVATE -w)
43
+
44
+ add_library(${PACKAGE_NAME} SHARED
45
+ src/main/cpp/cpp-adapter.cpp
46
+ ../cpp/hybrid/HybridLive2dSkia.cpp
47
+ ../cpp/hybrid/HybridLive2DModel.cpp
48
+ ../cpp/cubism/CubismSetup.cpp
49
+ ../cpp/cubism/CubismRendererStub.cpp
50
+ ../cpp/cubism/Live2DUserModel.cpp
51
+ ../cpp/cubism/Live2DUserModelAnimation.cpp
52
+ )
53
+
54
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/Live2dSkia+autolinking.cmake)
55
+
56
+ include_directories(
57
+ "src/main/cpp"
58
+ "../cpp/hybrid"
59
+ "../cpp/cubism"
60
+ "../cpp/drawing"
61
+ "${CUBISM_CORE_DIR}/include"
62
+ )
63
+
64
+ # Draw in C++ onto Skia canvases (cpp/drawing/SkiaDrawer.cpp). librnskia.so doesn't
65
+ # export Skia, so link the same prebuilt libskia.a it's built from and use its
66
+ # headers and defines; objects cross over through virtual calls. build.gradle
67
+ # passes the folders of the Skia the app uses, v3 (Graphite) or v2 (Ganesh).
68
+ # Without them, models are drawn in JS (src/drawer.ts).
69
+ set (RN_SKIA_LIBS_ABI "${RN_SKIA_LIBS}/${ANDROID_ABI}")
70
+ if (RN_SKIA_DIR AND EXISTS "${RN_SKIA_LIBS_ABI}/libskia.a" AND EXISTS "${RN_SKIA_DIR}/cpp/api/JsiSkCanvas.h")
71
+ message(STATUS "react-native-live2d-skia: drawing with Skia in C++ (${RN_SKIA_DIR}, Graphite: ${RN_SKIA_GRAPHITE})")
72
+ add_library(skia STATIC IMPORTED)
73
+ set_target_properties(skia PROPERTIES IMPORTED_LOCATION "${RN_SKIA_LIBS_ABI}/libskia.a")
74
+ target_sources(${PACKAGE_NAME} PRIVATE ../cpp/drawing/SkiaDrawer.cpp ../cpp/drawing/MeshCache.cpp)
75
+ target_include_directories(${PACKAGE_NAME} PRIVATE
76
+ "${RN_SKIA_DIR}/cpp/skia"
77
+ "${RN_SKIA_DIR}/cpp/api"
78
+ "${RN_SKIA_DIR}/cpp/jsi"
79
+ "${RN_SKIA_DIR}/cpp/rnskia"
80
+ "${RN_SKIA_DIR}/cpp/rnskia/values"
81
+ "${RN_SKIA_DIR}/cpp/utils"
82
+ "${RN_SKIA_DIR}/cpp"
83
+ "${RN_SKIA_DIR}/android/cpp/rnskia-android"
84
+ "${RN_SKIA_DIR}/android/cpp/jni/include"
85
+ "${REACT_NATIVE_DIR}/ReactCommon"
86
+ "${REACT_NATIVE_DIR}/ReactCommon/callinvoker")
87
+ # Same as the Skia package's build: struct layouts must match.
88
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE
89
+ LIVE2D_SKIA=1 SK_BUILD_FOR_ANDROID
90
+ SK_DISABLE_LEGACY_SHAPER_FACTORY SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API
91
+ FOLLY_NO_CONFIG=1 FOLLY_HAVE_CLOCK_GETTIME=1 FOLLY_HAVE_MEMRCHR=1
92
+ FOLLY_USE_LIBCPP=1 FOLLY_MOBILE=1 ON_ANDROID ONANDROID)
93
+ if (RN_SKIA_GRAPHITE)
94
+ target_include_directories(${PACKAGE_NAME} PRIVATE "${RN_SKIA_DIR}/cpp/dawn/include")
95
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE SK_GRAPHITE)
96
+ else()
97
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE SK_GL SK_GANESH)
98
+ endif()
99
+ set (LIVE2D_SKIA_LIBS skia)
100
+ else()
101
+ message(STATUS "react-native-live2d-skia: no Skia to draw with in C++, drawing in JS")
102
+ endif()
103
+
104
+ find_library(LOG_LIB log)
105
+
106
+ target_link_libraries(
107
+ ${PACKAGE_NAME}
108
+ ${LOG_LIB}
109
+ CubismFramework
110
+ Live2DCubismCore
111
+ ${LIVE2D_SKIA_LIBS}
112
+ android # <-- Android core
113
+ )