react-native-figma-shadow 0.1.0-alpha.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/android/CMakeLists.txt +30 -0
  4. package/android/build.gradle +76 -0
  5. package/android/gradle.properties +3 -0
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/com/figmashadow/FigmaShadowView.kt +224 -0
  8. package/android/src/main/java/com/figmashadow/FigmaShadowViewManager.kt +71 -0
  9. package/android/src/main/java/com/figmashadow/FigmaShadowViewPackage.kt +30 -0
  10. package/android/src/main/jni/cpp-adapter.cpp +77 -0
  11. package/cpp/figmashadow/Color.cpp +225 -0
  12. package/cpp/figmashadow/Color.h +14 -0
  13. package/cpp/figmashadow/FigmaShadow.cpp +114 -0
  14. package/cpp/figmashadow/FigmaShadow.h +30 -0
  15. package/cpp/figmashadow/Parser.cpp +150 -0
  16. package/cpp/figmashadow/Parser.h +16 -0
  17. package/cpp/figmashadow/Rasterizer.cpp +276 -0
  18. package/cpp/figmashadow/Rasterizer.h +21 -0
  19. package/cpp/figmashadow/Types.h +66 -0
  20. package/docs/ARCHITECTURE.md +67 -0
  21. package/docs/images/fs_drop.png +0 -0
  22. package/docs/images/fs_inset.png +0 -0
  23. package/docs/images/fs_offset.png +0 -0
  24. package/ios/FigmaShadowView.h +9 -0
  25. package/ios/FigmaShadowView.mm +207 -0
  26. package/lib/commonjs/FigmaShadowViewNativeComponent.js +10 -0
  27. package/lib/commonjs/FigmaShadowViewNativeComponent.js.map +1 -0
  28. package/lib/commonjs/index.js +106 -0
  29. package/lib/commonjs/index.js.map +1 -0
  30. package/lib/commonjs/package.json +1 -0
  31. package/lib/commonjs/parseShadow.js +145 -0
  32. package/lib/commonjs/parseShadow.js.map +1 -0
  33. package/lib/module/FigmaShadowViewNativeComponent.js +5 -0
  34. package/lib/module/FigmaShadowViewNativeComponent.js.map +1 -0
  35. package/lib/module/index.js +88 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/parseShadow.js +139 -0
  39. package/lib/module/parseShadow.js.map +1 -0
  40. package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts +24 -0
  41. package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts.map +1 -0
  42. package/lib/typescript/src/index.d.ts +40 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -0
  44. package/lib/typescript/src/parseShadow.d.ts +22 -0
  45. package/lib/typescript/src/parseShadow.d.ts.map +1 -0
  46. package/package.json +122 -0
  47. package/react-native-figma-shadow.podspec +25 -0
  48. package/react-native.config.js +19 -0
  49. package/src/FigmaShadowViewNativeComponent.ts +30 -0
  50. package/src/index.tsx +134 -0
  51. package/src/parseShadow.ts +134 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RaoMK
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.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # react-native-figma-shadow
2
+
3
+ > **Status: alpha (`0.1.0-alpha.x`).** The C++ core is tested and the package
4
+ > builds, but the native iOS/Android layers have not yet been run through a full
5
+ > app build. Install with `react-native-figma-shadow@alpha` and expect rough
6
+ > edges. Feedback welcome.
7
+
8
+ Paste a CSS `box-shadow` — the exact string Figma's **Copy as CSS** produces, or
9
+ what you already use on the web — and get a **pixel-identical** shadow on iOS and
10
+ Android.
11
+
12
+ ```tsx
13
+ import { Shadow } from 'react-native-figma-shadow';
14
+
15
+ <Shadow shadow="0px 4px 20px 0px rgba(0, 0, 0, 0.15)" borderRadius={16}>
16
+ <Card />
17
+ </Shadow>;
18
+ ```
19
+
20
+ - **One renderer, both platforms.** The shadow is rasterized by a shared C++
21
+ core (an analytic Gaussian convolution of a rounded rectangle). iOS and Android
22
+ run the same code and produce the same bytes — no `CALayer` blur vs `elevation`
23
+ mismatch.
24
+ - **Zero peer dependencies.** No Skia, no `react-native-svg`. Adds roughly
25
+ 50–100 KB to your app download.
26
+ - **New Architecture native component** (Fabric). Requires React Native 0.76+
27
+ with the New Architecture enabled.
28
+ - **Real CSS semantics:** multiple comma-separated shadows, `inset`, spread,
29
+ negative spread, and any CSS color (`#rgb`, `#rrggbbaa`, `rgb()/rgba()`,
30
+ `hsl()/hsla()`, named colors).
31
+
32
+ | `0 0 20px rgba(0,0,0,.5)` | `8px 10px 16px rgba(0,0,0,.6)` | `inset 0 8px 12px rgba(0,0,0,.7)` |
33
+ | :---: | :---: | :---: |
34
+ | ![drop](docs/images/fs_drop.png) | ![offset](docs/images/fs_offset.png) | ![inset](docs/images/fs_inset.png) |
35
+
36
+ <sub>Rendered by the shared C++ core — byte-for-byte identical output feeds both the iOS `CALayer` and the Android `Canvas`.</sub>
37
+
38
+ ## Why the numbers finally match
39
+
40
+ "Blur" means different things across systems:
41
+
42
+ | System | `blur` value is… |
43
+ | ------------------------- | --------------------------- |
44
+ | Figma / CSS `box-shadow` | ≈ 2× the Gaussian σ |
45
+ | iOS `shadowRadius` | ≈ the Gaussian σ |
46
+ | Android `elevation` | not configurable |
47
+
48
+ This library normalizes everything to the CSS/Figma definition (`σ = blur / 2`),
49
+ so the number you paste is the number you get.
50
+
51
+ ## Install
52
+
53
+ ```sh
54
+ npm install react-native-figma-shadow
55
+ # or: yarn add react-native-figma-shadow (Yarn 1 or Yarn 3+/Berry)
56
+ cd ios && pod install
57
+ ```
58
+
59
+ The C++ core is compiled into your app automatically by the New Architecture
60
+ build (CocoaPods on iOS, CMake/`externalNativeBuild` on Android). There is no
61
+ extra setup step, no postinstall script, and no peer dependency beyond
62
+ `react` / `react-native` themselves.
63
+
64
+ ### Yarn 3+ (Berry)
65
+
66
+ The package ships a modern `exports` map and installs cleanly under Yarn Berry.
67
+ As with every React Native native module, use the **`node-modules` linker** —
68
+ Metro, CocoaPods, and Gradle autolinking all expect a real `node_modules` tree,
69
+ so Yarn PnP is not supported by the RN toolchain:
70
+
71
+ ```yaml
72
+ # .yarnrc.yml
73
+ nodeLinker: node-modules
74
+ ```
75
+
76
+ ## API
77
+
78
+ ### `<Shadow>`
79
+
80
+ | Prop | Type | Notes |
81
+ | -------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------- |
82
+ | `shadow` | `string` | A CSS `box-shadow` value. `box-shadow:` prefix and trailing `;` are tolerated. |
83
+ | `borderRadius` | `number` | Uniform corner radius of the shadow shape. Match your child. |
84
+ | `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomRightRadius` / `borderBottomLeftRadius` | `number` | Per-corner override. |
85
+ | `backgroundColor` | `string` | Fill painted inside the content box, **below** an inset shadow and below children (use this instead of a background on the child for CSS-correct inset shadows). |
86
+ | `highQuality` | `boolean` | Opt into the slower exact rounded-corner quadrature. Default `false`. |
87
+ | `style` | `ViewStyle` | Applied to the outer layout box. |
88
+
89
+ ```tsx
90
+ // multiple shadows — comma-separated, straight from Figma / CSS
91
+ <Shadow
92
+ shadow="0 2px 4px rgba(0,0,0,.10), 0 12px 32px rgba(0,0,0,.14)"
93
+ borderRadius={16}
94
+ >
95
+ <Card />
96
+ </Shadow>
97
+
98
+ // inner shadow (Figma "Inner shadow")
99
+ <Shadow shadow="inset 0 2px 8px rgba(0,0,0,0.25)" borderRadius={12} backgroundColor="#fff">
100
+ <TextInput />
101
+ </Shadow>
102
+
103
+ // per-corner radius
104
+ <Shadow
105
+ shadow="0 8px 24px rgba(0,0,0,.12)"
106
+ borderTopLeftRadius={16}
107
+ borderTopRightRadius={16}
108
+ >
109
+ <Sheet />
110
+ </Shadow>
111
+ ```
112
+
113
+ ### Helpers
114
+
115
+ ```ts
116
+ import { parseBoxShadow, computeBleed } from 'react-native-figma-shadow';
117
+
118
+ parseBoxShadow('0 4px 20px rgba(0,0,0,.15)');
119
+ // [{ offsetX: 0, offsetY: 4, blur: 20, spread: 0, inset: false }]
120
+
121
+ computeBleed(parseBoxShadow('0 4px 20px rgba(0,0,0,.15)'));
122
+ // { left: 30, top: 30, right: 30, bottom: 34 }
123
+ ```
124
+
125
+ ## How it works
126
+
127
+ 1. `<Shadow>` parses the value on the JS side only to compute the **bleed** — how
128
+ far the shadow extends past the element box — and reserves that space with
129
+ negative margins so layout stays neutral.
130
+ 2. The native Fabric view (`FigmaShadowView`) hands the raw `box-shadow` string,
131
+ the content size, corner radii, bleed and pixel ratio to the shared C++
132
+ `render()`.
133
+ 3. C++ parses the declaration, rasterizes every layer into one premultiplied
134
+ RGBA8888 bitmap, and memoizes it (identical requests — e.g. every row of a
135
+ list — reuse one bitmap).
136
+ 4. iOS sets the bitmap as a `CALayer`'s `contents`; Android blits it in
137
+ `dispatchDraw` behind the children. Neither platform does any blur math.
138
+
139
+ ## Limitations
140
+
141
+ - **Shape shadows only.** The shadow follows the `borderRadius` you give it, not
142
+ the actual alpha of arbitrary content (text, transparent PNGs). `drop-shadow`
143
+ that hugs content alpha is not in this version.
144
+ - **New Architecture required** (RN 0.76+). No legacy-architecture fallback.
145
+ - First render of a given shadow happens on a background thread and appears a
146
+ frame or two later; after that it is cached.
147
+ - The `backgroundColor` fill uses the top-left radius for all four corners when
148
+ per-corner radii differ (the shadow itself is always exact).
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,30 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(FigmaShadow)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_VERBOSE_MAKEFILE ON)
7
+
8
+ set(SHARED_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cpp")
9
+
10
+ file(GLOB CORE_SOURCES CONFIGURE_DEPENDS "${SHARED_CORE_DIR}/figmashadow/*.cpp")
11
+
12
+ add_library(react-native-figma-shadow
13
+ SHARED
14
+ ${CORE_SOURCES}
15
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/main/jni/cpp-adapter.cpp"
16
+ )
17
+
18
+ target_include_directories(react-native-figma-shadow
19
+ PUBLIC
20
+ "${SHARED_CORE_DIR}"
21
+ "${SHARED_CORE_DIR}/figmashadow"
22
+ )
23
+
24
+ find_library(log-lib log)
25
+ find_library(jnigraphics-lib jnigraphics)
26
+
27
+ target_link_libraries(react-native-figma-shadow
28
+ ${log-lib}
29
+ ${jnigraphics-lib}
30
+ )
@@ -0,0 +1,76 @@
1
+ buildscript {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ dependencies {
7
+ classpath "com.android.tools.build:gradle:8.7.2"
8
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"
9
+ classpath "com.facebook.react:react-native-gradle-plugin"
10
+ }
11
+ }
12
+
13
+ def getExtOrDefault = { name, fallback ->
14
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
15
+ }
16
+
17
+ def reactNativeArchitectures() {
18
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
19
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
20
+ }
21
+
22
+ apply plugin: "com.android.library"
23
+ apply plugin: "org.jetbrains.kotlin.android"
24
+ apply plugin: "com.facebook.react"
25
+
26
+ android {
27
+ namespace "com.figmashadow"
28
+
29
+ compileSdkVersion getExtOrDefault("compileSdkVersion", 35)
30
+
31
+ defaultConfig {
32
+ minSdkVersion getExtOrDefault("minSdkVersion", 24)
33
+ targetSdkVersion getExtOrDefault("targetSdkVersion", 34)
34
+
35
+ externalNativeBuild {
36
+ cmake {
37
+ cppFlags "-O2", "-frtti", "-fexceptions", "-Wall"
38
+ arguments "-DANDROID_STL=c++_shared"
39
+ abiFilters(*reactNativeArchitectures())
40
+ }
41
+ }
42
+ }
43
+
44
+ externalNativeBuild {
45
+ cmake {
46
+ path "CMakeLists.txt"
47
+ }
48
+ }
49
+
50
+ buildTypes {
51
+ release {
52
+ minifyEnabled false
53
+ }
54
+ }
55
+
56
+ compileOptions {
57
+ sourceCompatibility JavaVersion.VERSION_17
58
+ targetCompatibility JavaVersion.VERSION_17
59
+ }
60
+ }
61
+
62
+ repositories {
63
+ google()
64
+ mavenCentral()
65
+ }
66
+
67
+ dependencies {
68
+ implementation "com.facebook.react:react-android"
69
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.24"
70
+ }
71
+
72
+ react {
73
+ jsRootDir = file("../src/")
74
+ libraryName = "RNFigmaShadowSpec"
75
+ codegenJavaPackageName = "com.figmashadow"
76
+ }
@@ -0,0 +1,3 @@
1
+ FigmaShadow_compileSdkVersion=35
2
+ FigmaShadow_targetSdkVersion=34
3
+ FigmaShadow_minSdkVersion=24
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,224 @@
1
+ package com.figmashadow
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.graphics.Paint
7
+ import android.graphics.Path
8
+ import android.graphics.RectF
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import android.util.DisplayMetrics
12
+ import com.facebook.react.views.view.ReactViewGroup
13
+ import java.util.concurrent.Executors
14
+
15
+ /**
16
+ * Container that paints a CSS-style shadow behind its children. All shadow
17
+ * geometry is produced by the shared C++ core (see [nativeRenderShadow]); this
18
+ * view only blits the resulting bitmap.
19
+ */
20
+ class FigmaShadowView(context: Context) : ReactViewGroup(context) {
21
+
22
+ private val density: Float =
23
+ context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
24
+
25
+ private var shadowSpec: String = ""
26
+ private var backgroundSpec: String? = null
27
+ private var radiusTopLeft = 0f
28
+ private var radiusTopRight = 0f
29
+ private var radiusBottomRight = 0f
30
+ private var radiusBottomLeft = 0f
31
+ private var bleedLeft = 0f
32
+ private var bleedTop = 0f
33
+ private var bleedRight = 0f
34
+ private var bleedBottom = 0f
35
+ private var highQuality = false
36
+
37
+ private var shadowBitmap: Bitmap? = null
38
+ private var dirty = true
39
+ private var renderGeneration = 0
40
+ private var renderInFlight = -1
41
+
42
+ private val bitmapPaint = Paint(Paint.FILTER_BITMAP_FLAG)
43
+ private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG)
44
+ private var hasFill = false
45
+ private val fillPath = Path()
46
+ private val fillRect = RectF()
47
+ private val fillRadii = FloatArray(8)
48
+
49
+ init {
50
+ setWillNotDraw(false)
51
+ // The shadow is drawn outside the padded content box.
52
+ clipChildren = false
53
+ }
54
+
55
+ fun setShadowSpec(value: String?) {
56
+ val next = value ?: ""
57
+ if (next != shadowSpec) {
58
+ shadowSpec = next
59
+ dirty = true
60
+ invalidate()
61
+ }
62
+ }
63
+
64
+ fun setFillColor(value: String?) {
65
+ if (value != backgroundSpec) {
66
+ backgroundSpec = value
67
+ resolveFill()
68
+ invalidate()
69
+ }
70
+ }
71
+
72
+ fun setHighQuality(value: Boolean) {
73
+ if (value != highQuality) {
74
+ highQuality = value
75
+ dirty = true
76
+ invalidate()
77
+ }
78
+ }
79
+
80
+ private inline fun updateRadius(current: Float, next: Float, apply: (Float) -> Unit) {
81
+ if (current != next) {
82
+ apply(next)
83
+ dirty = true
84
+ invalidate()
85
+ }
86
+ }
87
+
88
+ fun setRadiusTopLeft(v: Float) = updateRadius(radiusTopLeft, v) { radiusTopLeft = it }
89
+ fun setRadiusTopRight(v: Float) = updateRadius(radiusTopRight, v) { radiusTopRight = it }
90
+ fun setRadiusBottomRight(v: Float) = updateRadius(radiusBottomRight, v) { radiusBottomRight = it }
91
+ fun setRadiusBottomLeft(v: Float) = updateRadius(radiusBottomLeft, v) { radiusBottomLeft = it }
92
+
93
+ fun setBleedLeft(v: Float) = updateRadius(bleedLeft, v) { bleedLeft = it }
94
+ fun setBleedTop(v: Float) = updateRadius(bleedTop, v) { bleedTop = it }
95
+ fun setBleedRight(v: Float) = updateRadius(bleedRight, v) { bleedRight = it }
96
+ fun setBleedBottom(v: Float) = updateRadius(bleedBottom, v) { bleedBottom = it }
97
+
98
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
99
+ super.onSizeChanged(w, h, oldw, oldh)
100
+ dirty = true
101
+ }
102
+
103
+ private fun contentWidthPx(): Float = width - (bleedLeft + bleedRight) * density
104
+ private fun contentHeightPx(): Float = height - (bleedTop + bleedBottom) * density
105
+
106
+ /** Kicks off an off-thread raster; the current bitmap keeps showing until it lands. */
107
+ private fun rebuildShadow() {
108
+ dirty = false
109
+
110
+ val contentW = contentWidthPx() / density
111
+ val contentH = contentHeightPx() / density
112
+ if (contentW <= 0f || contentH <= 0f || shadowSpec.isEmpty()) {
113
+ shadowBitmap = null
114
+ return
115
+ }
116
+
117
+ val generation = ++renderGeneration
118
+ if (generation == renderInFlight) return
119
+ renderInFlight = generation
120
+
121
+ val spec = shadowSpec
122
+ val rtl = radiusTopLeft
123
+ val rtr = radiusTopRight
124
+ val rbr = radiusBottomRight
125
+ val rbl = radiusBottomLeft
126
+ val bl = bleedLeft
127
+ val bt = bleedTop
128
+ val br = bleedRight
129
+ val bb = bleedBottom
130
+ val hq = highQuality
131
+
132
+ executor.execute {
133
+ val bitmap = nativeRenderShadow(
134
+ contentW, contentH, rtl, rtr, rbr, rbl, spec, bl, bt, br, bb, density, hq,
135
+ )
136
+ mainHandler.post {
137
+ renderInFlight = -1
138
+ if (generation == renderGeneration) {
139
+ shadowBitmap = bitmap
140
+ invalidate()
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ private fun resolveFill() {
147
+ val spec = backgroundSpec
148
+ val rgba = FloatArray(4)
149
+ hasFill = spec != null && nativeParseColor(spec, rgba) && rgba[3] > 0f
150
+ if (hasFill) {
151
+ fillPaint.setARGB(
152
+ (rgba[3] * 255).toInt(),
153
+ (rgba[0] * 255).toInt(),
154
+ (rgba[1] * 255).toInt(),
155
+ (rgba[2] * 255).toInt(),
156
+ )
157
+ }
158
+ }
159
+
160
+ private fun updateFillPath() {
161
+ val left = bleedLeft * density
162
+ val top = bleedTop * density
163
+ fillRect.set(left, top, left + contentWidthPx(), top + contentHeightPx())
164
+ val tl = radiusTopLeft * density
165
+ val tr = radiusTopRight * density
166
+ val br = radiusBottomRight * density
167
+ val bl = radiusBottomLeft * density
168
+ fillRadii[0] = tl; fillRadii[1] = tl
169
+ fillRadii[2] = tr; fillRadii[3] = tr
170
+ fillRadii[4] = br; fillRadii[5] = br
171
+ fillRadii[6] = bl; fillRadii[7] = bl
172
+ fillPath.reset()
173
+ fillPath.addRoundRect(fillRect, fillRadii, Path.Direction.CW)
174
+ }
175
+
176
+ override fun dispatchDraw(canvas: Canvas) {
177
+ if (dirty) rebuildShadow()
178
+
179
+ if (hasFill) {
180
+ updateFillPath()
181
+ canvas.drawPath(fillPath, fillPaint)
182
+ }
183
+
184
+ shadowBitmap?.let { bmp ->
185
+ canvas.drawBitmap(bmp, null, RectF(0f, 0f, width.toFloat(), height.toFloat()), bitmapPaint)
186
+ }
187
+
188
+ super.dispatchDraw(canvas)
189
+ }
190
+
191
+ companion object {
192
+ init {
193
+ System.loadLibrary("react-native-figma-shadow")
194
+ }
195
+
196
+ private val executor = Executors.newSingleThreadExecutor { r ->
197
+ Thread(r, "figma-shadow-raster").apply { priority = Thread.NORM_PRIORITY - 1 }
198
+ }
199
+ private val mainHandler = Handler(Looper.getMainLooper())
200
+
201
+ @JvmStatic
202
+ external fun nativeRenderShadow(
203
+ contentWidth: Float,
204
+ contentHeight: Float,
205
+ radiusTopLeft: Float,
206
+ radiusTopRight: Float,
207
+ radiusBottomRight: Float,
208
+ radiusBottomLeft: Float,
209
+ boxShadow: String,
210
+ bleedLeft: Float,
211
+ bleedTop: Float,
212
+ bleedRight: Float,
213
+ bleedBottom: Float,
214
+ scale: Float,
215
+ highQuality: Boolean,
216
+ ): Bitmap?
217
+
218
+ @JvmStatic
219
+ external fun nativeParseColor(token: String, out: FloatArray): Boolean
220
+
221
+ @JvmStatic
222
+ external fun nativeClearCache()
223
+ }
224
+ }
@@ -0,0 +1,71 @@
1
+ package com.figmashadow
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+ import com.facebook.react.viewmanagers.FigmaShadowViewManagerDelegate
9
+ import com.facebook.react.viewmanagers.FigmaShadowViewManagerInterface
10
+
11
+ @ReactModule(name = FigmaShadowViewManager.NAME)
12
+ class FigmaShadowViewManager :
13
+ SimpleViewManager<FigmaShadowView>(),
14
+ FigmaShadowViewManagerInterface<FigmaShadowView> {
15
+
16
+ private val delegate: ViewManagerDelegate<FigmaShadowView> =
17
+ FigmaShadowViewManagerDelegate(this)
18
+
19
+ override fun getDelegate(): ViewManagerDelegate<FigmaShadowView> = delegate
20
+
21
+ override fun getName(): String = NAME
22
+
23
+ override fun createViewInstance(context: ThemedReactContext): FigmaShadowView =
24
+ FigmaShadowView(context)
25
+
26
+ @ReactProp(name = "shadow")
27
+ override fun setShadow(view: FigmaShadowView, value: String?) = view.setShadowSpec(value)
28
+
29
+ @ReactProp(name = "fillColor")
30
+ override fun setFillColor(view: FigmaShadowView, value: String?) = view.setFillColor(value)
31
+
32
+ @ReactProp(name = "highQuality")
33
+ override fun setHighQuality(view: FigmaShadowView, value: Boolean) = view.setHighQuality(value)
34
+
35
+ @ReactProp(name = "pixelRatio")
36
+ override fun setPixelRatio(view: FigmaShadowView, value: Float) {
37
+ // Android derives density from resources; the JS-supplied value is unused.
38
+ }
39
+
40
+ @ReactProp(name = "borderTopLeftRadius", defaultFloat = 0f)
41
+ override fun setBorderTopLeftRadius(view: FigmaShadowView, value: Float) =
42
+ view.setRadiusTopLeft(value)
43
+
44
+ @ReactProp(name = "borderTopRightRadius", defaultFloat = 0f)
45
+ override fun setBorderTopRightRadius(view: FigmaShadowView, value: Float) =
46
+ view.setRadiusTopRight(value)
47
+
48
+ @ReactProp(name = "borderBottomRightRadius", defaultFloat = 0f)
49
+ override fun setBorderBottomRightRadius(view: FigmaShadowView, value: Float) =
50
+ view.setRadiusBottomRight(value)
51
+
52
+ @ReactProp(name = "borderBottomLeftRadius", defaultFloat = 0f)
53
+ override fun setBorderBottomLeftRadius(view: FigmaShadowView, value: Float) =
54
+ view.setRadiusBottomLeft(value)
55
+
56
+ @ReactProp(name = "bleedLeft", defaultFloat = 0f)
57
+ override fun setBleedLeft(view: FigmaShadowView, value: Float) = view.setBleedLeft(value)
58
+
59
+ @ReactProp(name = "bleedTop", defaultFloat = 0f)
60
+ override fun setBleedTop(view: FigmaShadowView, value: Float) = view.setBleedTop(value)
61
+
62
+ @ReactProp(name = "bleedRight", defaultFloat = 0f)
63
+ override fun setBleedRight(view: FigmaShadowView, value: Float) = view.setBleedRight(value)
64
+
65
+ @ReactProp(name = "bleedBottom", defaultFloat = 0f)
66
+ override fun setBleedBottom(view: FigmaShadowView, value: Float) = view.setBleedBottom(value)
67
+
68
+ companion object {
69
+ const val NAME = "FigmaShadowView"
70
+ }
71
+ }
@@ -0,0 +1,30 @@
1
+ package com.figmashadow
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import com.facebook.react.uimanager.ViewManager
9
+
10
+ class FigmaShadowViewPackage : BaseReactPackage() {
11
+
12
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
13
+
14
+ override fun createViewManagers(
15
+ reactContext: ReactApplicationContext,
16
+ ): List<ViewManager<*, *>> = listOf(FigmaShadowViewManager())
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
19
+ mapOf(
20
+ FigmaShadowViewManager.NAME to ReactModuleInfo(
21
+ FigmaShadowViewManager.NAME,
22
+ FigmaShadowViewManager.NAME,
23
+ false, // canOverrideExistingModule
24
+ false, // needsEagerInit
25
+ false, // isCxxModule
26
+ true, // isTurboModule
27
+ ),
28
+ )
29
+ }
30
+ }
@@ -0,0 +1,77 @@
1
+ #include <android/bitmap.h>
2
+ #include <jni.h>
3
+
4
+ #include <cstring>
5
+ #include <string>
6
+
7
+ #include "Color.h"
8
+ #include "FigmaShadow.h"
9
+
10
+ namespace {
11
+
12
+ std::string jstringToStd(JNIEnv *env, jstring value) {
13
+ if (value == nullptr) return {};
14
+ const char *chars = env->GetStringUTFChars(value, nullptr);
15
+ std::string out(chars ? chars : "");
16
+ if (chars) env->ReleaseStringUTFChars(value, chars);
17
+ return out;
18
+ }
19
+
20
+ jobject createArgb8888Bitmap(JNIEnv *env, int width, int height) {
21
+ jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
22
+ jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
23
+ jfieldID argb8888Field = env->GetStaticFieldID(
24
+ configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
25
+ jobject config = env->GetStaticObjectField(configClass, argb8888Field);
26
+ jmethodID createBitmap = env->GetStaticMethodID(
27
+ bitmapClass, "createBitmap",
28
+ "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
29
+ return env->CallStaticObjectMethod(bitmapClass, createBitmap, width, height, config);
30
+ }
31
+
32
+ } // namespace
33
+
34
+ extern "C" JNIEXPORT jobject JNICALL
35
+ Java_com_figmashadow_FigmaShadowView_nativeRenderShadow(
36
+ JNIEnv *env, jclass /*clazz*/, jfloat contentWidth, jfloat contentHeight,
37
+ jfloat radiusTopLeft, jfloat radiusTopRight, jfloat radiusBottomRight,
38
+ jfloat radiusBottomLeft, jstring boxShadow, jfloat bleedLeft, jfloat bleedTop,
39
+ jfloat bleedRight, jfloat bleedBottom, jfloat scale, jboolean highQuality) {
40
+ const std::string shadow = jstringToStd(env, boxShadow);
41
+
42
+ figmashadow::Bitmap bmp = figmashadow::render(
43
+ contentWidth, contentHeight, radiusTopLeft, radiusTopRight,
44
+ radiusBottomRight, radiusBottomLeft, shadow, bleedLeft, bleedTop, bleedRight,
45
+ bleedBottom, scale, highQuality == JNI_TRUE);
46
+
47
+ if (bmp.empty()) return nullptr;
48
+
49
+ jobject bitmap = createArgb8888Bitmap(env, bmp.width, bmp.height);
50
+ if (bitmap == nullptr) return nullptr;
51
+
52
+ void *pixels = nullptr;
53
+ if (AndroidBitmap_lockPixels(env, bitmap, &pixels) != ANDROID_BITMAP_RESULT_SUCCESS) {
54
+ return nullptr;
55
+ }
56
+ // figmashadow::Bitmap is premultiplied RGBA8888; Android's ARGB_8888 is stored
57
+ // in the same byte order (R,G,B,A) and premultiplied by default.
58
+ std::memcpy(pixels, bmp.pixels.data(), bmp.pixels.size());
59
+ AndroidBitmap_unlockPixels(env, bitmap);
60
+
61
+ return bitmap;
62
+ }
63
+
64
+ extern "C" JNIEXPORT jboolean JNICALL
65
+ Java_com_figmashadow_FigmaShadowView_nativeParseColor(JNIEnv *env, jclass /*clazz*/,
66
+ jstring token, jfloatArray out) {
67
+ figmashadow::Color color;
68
+ if (!figmashadow::parseColor(jstringToStd(env, token), color)) return JNI_FALSE;
69
+ jfloat values[4] = {color.r, color.g, color.b, color.a};
70
+ env->SetFloatArrayRegion(out, 0, 4, values);
71
+ return JNI_TRUE;
72
+ }
73
+
74
+ extern "C" JNIEXPORT void JNICALL
75
+ Java_com_figmashadow_FigmaShadowView_nativeClearCache(JNIEnv * /*env*/, jclass /*clazz*/) {
76
+ figmashadow::clearCache();
77
+ }