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
@@ -0,0 +1,276 @@
1
+ #include "Rasterizer.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+ #include <cstdint>
6
+
7
+ namespace figmashadow {
8
+
9
+ namespace {
10
+
11
+ constexpr float kSqrt2 = 1.41421356237f;
12
+ constexpr float kInvSqrt2Pi = 0.39894228040f;
13
+ constexpr int kMaxDimension = 8192;
14
+
15
+ inline float clamp01(float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); }
16
+ inline float clampf(float v, float lo, float hi) {
17
+ return v < lo ? lo : (v > hi ? hi : v);
18
+ }
19
+ inline float gauss1d(float d, float sigma) {
20
+ return std::exp(-(d * d) / (2.0f * sigma * sigma)) * kInvSqrt2Pi / sigma;
21
+ }
22
+
23
+ struct Radii4 {
24
+ float tl, tr, br, bl;
25
+ };
26
+
27
+ // Signed distance to a rounded rectangle centred at the origin with half-extents
28
+ // (hx, hy). Negative inside.
29
+ float sdRoundRect(float x, float y, float hx, float hy, const Radii4& r) {
30
+ float rr = (x >= 0.0f) ? ((y >= 0.0f) ? r.br : r.tr) : ((y >= 0.0f) ? r.bl : r.tl);
31
+ rr = clampf(rr, 0.0f, std::min(hx, hy));
32
+ float qx = std::fabs(x) - (hx - rr);
33
+ float qy = std::fabs(y) - (hy - rr);
34
+ float ax = std::max(qx, 0.0f);
35
+ float ay = std::max(qy, 0.0f);
36
+ float outside = std::sqrt(ax * ax + ay * ay);
37
+ float inside = std::min(std::max(qx, qy), 0.0f);
38
+ return outside + inside - rr;
39
+ }
40
+
41
+ // Antialiased hard coverage of the rounded rectangle (used for clipping and for
42
+ // the element knock-out).
43
+ float hardCoverage(float x, float y, float hx, float hy, const Radii4& r, float aa) {
44
+ float sd = sdRoundRect(x, y, hx, hy, r);
45
+ return clamp01(0.5f - sd / std::max(aa, 1e-4f));
46
+ }
47
+
48
+ // Exact Gaussian convolution of a sharp-cornered rectangle: the 2D integral is
49
+ // separable into a product of error functions.
50
+ float rectCoverage(float x, float y, float hx, float hy, float sigma) {
51
+ float s2 = kSqrt2 * sigma;
52
+ float gx = 0.5f * (std::erf((hx - x) / s2) + std::erf((hx + x) / s2));
53
+ float gy = 0.5f * (std::erf((hy - y) / s2) + std::erf((hy + y) / s2));
54
+ return clamp01(gx * gy);
55
+ }
56
+
57
+ // Half-width of the shape, measured from the vertical centre line, at signed
58
+ // vertical coordinate `t`. `rNeg` is the corner radius used for t <= 0, `rPos`
59
+ // for t > 0.
60
+ float sideExtent(float t, float hx, float hy, float rNeg, float rPos) {
61
+ float r = (t <= 0.0f) ? rNeg : rPos;
62
+ r = clampf(r, 0.0f, std::min(hx, hy));
63
+ float at = std::fabs(t);
64
+ if (at >= hy) return 0.0f;
65
+ float straight = hy - r;
66
+ if (at <= straight) return hx;
67
+ float dy = at - straight;
68
+ return (hx - r) + std::sqrt(std::max(0.0f, r * r - dy * dy));
69
+ }
70
+
71
+ // Exact-ish Gaussian convolution of a rounded rectangle via bounded vertical
72
+ // quadrature. Slower; used only when `highQuality` is requested.
73
+ float quadratureCoverage(float x, float y, float hx, float hy, const Radii4& r,
74
+ float sigma) {
75
+ float s2 = kSqrt2 * sigma;
76
+ float R = 3.5f * sigma;
77
+ int n = static_cast<int>(std::ceil(9.5f * sigma / 0.75f));
78
+ n = std::min(std::max(n, 16), 160);
79
+ float step = (2.0f * R) / n;
80
+ float lo = y - R;
81
+ float acc = 0.0f;
82
+ float wsum = 0.0f;
83
+ for (int i = 0; i < n; ++i) {
84
+ float t = lo + (i + 0.5f) * step;
85
+ float w = gauss1d(y - t, sigma);
86
+ float re = sideExtent(t, hx, hy, r.tr, r.br);
87
+ float le = sideExtent(t, hx, hy, r.tl, r.bl);
88
+ float horiz = 0.0f;
89
+ if (re > 0.0f || le > 0.0f) {
90
+ horiz = clamp01(0.5f * (std::erf((re - x) / s2) + std::erf((le + x) / s2)));
91
+ }
92
+ acc += w * horiz;
93
+ wsum += w;
94
+ }
95
+ return (wsum > 1e-6f) ? clamp01(acc / wsum) : 0.0f;
96
+ }
97
+
98
+ // Gaussian convolution of a rounded rectangle. Fast path: exact for sharp
99
+ // corners, an SDF/error-function approximation for rounded corners (visually
100
+ // indistinguishable for shadows and, crucially, identical on every platform).
101
+ float blurredCoverage(float x, float y, float hx, float hy, const Radii4& r,
102
+ float sigma, float aa, bool highQuality) {
103
+ if (hx <= 0.0f || hy <= 0.0f) return 0.0f;
104
+ if (sigma < 0.6f) return hardCoverage(x, y, hx, hy, r, std::max(aa, 0.75f));
105
+ if (r.tl == 0.0f && r.tr == 0.0f && r.br == 0.0f && r.bl == 0.0f) {
106
+ return rectCoverage(x, y, hx, hy, sigma);
107
+ }
108
+ if (highQuality) return quadratureCoverage(x, y, hx, hy, r, sigma);
109
+ float sd = sdRoundRect(x, y, hx, hy, r);
110
+ return clamp01(0.5f * (1.0f + std::erf(-sd / (kSqrt2 * sigma))));
111
+ }
112
+
113
+ Radii4 spreadRadii(const CornerRadii& r, float spread, float hx, float hy) {
114
+ auto grow = [&](float v) {
115
+ float out = (v > 0.0f) ? std::max(0.0f, v + spread) : 0.0f;
116
+ return clampf(out, 0.0f, std::min(hx, hy));
117
+ };
118
+ return {grow(r.topLeft), grow(r.topRight), grow(r.bottomRight), grow(r.bottomLeft)};
119
+ }
120
+
121
+ Radii4 shrinkRadii(const CornerRadii& r, float spread, float hx, float hy) {
122
+ auto shrink = [&](float v) {
123
+ float out = (v > 0.0f) ? std::max(0.0f, v - spread) : 0.0f;
124
+ return clampf(out, 0.0f, std::min(std::max(hx, 0.0f), std::max(hy, 0.0f)));
125
+ };
126
+ return {shrink(r.topLeft), shrink(r.topRight), shrink(r.bottomRight), shrink(r.bottomLeft)};
127
+ }
128
+
129
+ // Premultiplied float RGBA accumulation buffer.
130
+ struct FImage {
131
+ int w = 0;
132
+ int h = 0;
133
+ std::vector<float> px; // w*h*4
134
+ };
135
+
136
+ inline void compositeOver(float* dst, float sr, float sg, float sb, float sa) {
137
+ float inv = 1.0f - sa;
138
+ dst[0] = sr + dst[0] * inv;
139
+ dst[1] = sg + dst[1] * inv;
140
+ dst[2] = sb + dst[2] * inv;
141
+ dst[3] = sa + dst[3] * inv;
142
+ }
143
+
144
+ } // namespace
145
+
146
+ Bitmap renderShadow(const RenderRequest& req) {
147
+ Bitmap result;
148
+
149
+ const float logicalW = req.bleed.left + req.contentWidth + req.bleed.right;
150
+ const float logicalH = req.bleed.top + req.contentHeight + req.bleed.bottom;
151
+ if (logicalW <= 0.0f || logicalH <= 0.0f || req.layers.empty()) return result;
152
+
153
+ const float scale = std::max(req.scale, 0.1f);
154
+ int outW = std::min(kMaxDimension,
155
+ std::max(1, static_cast<int>(std::lround(logicalW * scale))));
156
+ int outH = std::min(kMaxDimension,
157
+ std::max(1, static_cast<int>(std::lround(logicalH * scale))));
158
+
159
+ // Drop to a lower internal resolution for pathologically large surfaces; the
160
+ // factor is derived purely from the inputs, so both platforms pick the same
161
+ // one.
162
+ double outPx = static_cast<double>(outW) * outH;
163
+ float internalScale = scale;
164
+ if (outPx > kMaxRenderPixels) {
165
+ internalScale = scale * static_cast<float>(std::sqrt(kMaxRenderPixels / outPx));
166
+ }
167
+ int rw = std::max(1, static_cast<int>(std::lround(logicalW * internalScale)));
168
+ int rh = std::max(1, static_cast<int>(std::lround(logicalH * internalScale)));
169
+ const float aa = 1.0f / internalScale;
170
+
171
+ FImage acc;
172
+ acc.w = rw;
173
+ acc.h = rh;
174
+ acc.px.assign(static_cast<size_t>(rw) * rh * 4, 0.0f);
175
+
176
+ const float hx = req.contentWidth * 0.5f;
177
+ const float hy = req.contentHeight * 0.5f;
178
+ const float ecx = req.bleed.left + hx;
179
+ const float ecy = req.bleed.top + hy;
180
+ const Radii4 elementRadii{req.radii.topLeft, req.radii.topRight,
181
+ req.radii.bottomRight, req.radii.bottomLeft};
182
+
183
+ // Paint order: the first layer in the list ends up on top, so composite from
184
+ // the last layer to the first.
185
+ for (auto it = req.layers.rbegin(); it != req.layers.rend(); ++it) {
186
+ const ShadowLayer& layer = *it;
187
+ if (layer.color.a <= 0.0f) continue;
188
+ const float sigma = layer.blur * 0.5f;
189
+
190
+ if (!layer.inset) {
191
+ const float shx = hx + layer.spread;
192
+ const float shy = hy + layer.spread;
193
+ if (shx <= 0.0f || shy <= 0.0f) continue;
194
+ const Radii4 sr = spreadRadii(req.radii, layer.spread, shx, shy);
195
+ const float cxr = ecx + layer.offsetX;
196
+ const float cyr = ecy + layer.offsetY;
197
+
198
+ for (int py = 0; py < rh; ++py) {
199
+ const float ly = (py + 0.5f) / internalScale;
200
+ float* row = acc.px.data() + static_cast<size_t>(py) * rw * 4;
201
+ for (int px = 0; px < rw; ++px) {
202
+ const float lx = (px + 0.5f) / internalScale;
203
+ float cov = blurredCoverage(lx - cxr, ly - cyr, shx, shy, sr, sigma, aa,
204
+ req.highQuality);
205
+ if (cov <= 0.001f) continue;
206
+ float knock = hardCoverage(lx - ecx, ly - ecy, hx, hy, elementRadii, aa);
207
+ float a = layer.color.a * cov * (1.0f - knock);
208
+ if (a <= 0.001f) continue;
209
+ compositeOver(row + px * 4, layer.color.r * a, layer.color.g * a,
210
+ layer.color.b * a, a);
211
+ }
212
+ }
213
+ } else {
214
+ const float ihx = hx - layer.spread;
215
+ const float ihy = hy - layer.spread;
216
+ const Radii4 ir = shrinkRadii(req.radii, layer.spread, ihx, ihy);
217
+ const float cxr = ecx + layer.offsetX;
218
+ const float cyr = ecy + layer.offsetY;
219
+ const bool collapsed = ihx <= 0.0f || ihy <= 0.0f;
220
+
221
+ for (int py = 0; py < rh; ++py) {
222
+ const float ly = (py + 0.5f) / internalScale;
223
+ float* row = acc.px.data() + static_cast<size_t>(py) * rw * 4;
224
+ for (int px = 0; px < rw; ++px) {
225
+ const float lx = (px + 0.5f) / internalScale;
226
+ float inside = hardCoverage(lx - ecx, ly - ecy, hx, hy, elementRadii, aa);
227
+ if (inside <= 0.001f) continue;
228
+ float innerCov =
229
+ collapsed ? 0.0f
230
+ : blurredCoverage(lx - cxr, ly - cyr, ihx, ihy, ir, sigma,
231
+ aa, req.highQuality);
232
+ float a = layer.color.a * (1.0f - innerCov) * inside;
233
+ if (a <= 0.001f) continue;
234
+ compositeOver(row + px * 4, layer.color.r * a, layer.color.g * a,
235
+ layer.color.b * a, a);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // Resample (identity when rw==outW && rh==outH) and pack to premultiplied
242
+ // RGBA8888.
243
+ result.width = outW;
244
+ result.height = outH;
245
+ result.pixels.assign(static_cast<size_t>(outW) * outH * 4, 0);
246
+ for (int oy = 0; oy < outH; ++oy) {
247
+ float fy = (oy + 0.5f) * rh / outH - 0.5f;
248
+ int y0 = static_cast<int>(std::floor(fy));
249
+ float wy = fy - y0;
250
+ int y0c = std::min(std::max(y0, 0), rh - 1);
251
+ int y1c = std::min(std::max(y0 + 1, 0), rh - 1);
252
+ for (int ox = 0; ox < outW; ++ox) {
253
+ float fx = (ox + 0.5f) * rw / outW - 0.5f;
254
+ int x0 = static_cast<int>(std::floor(fx));
255
+ float wx = fx - x0;
256
+ int x0c = std::min(std::max(x0, 0), rw - 1);
257
+ int x1c = std::min(std::max(x0 + 1, 0), rw - 1);
258
+
259
+ const float* p00 = acc.px.data() + (static_cast<size_t>(y0c) * rw + x0c) * 4;
260
+ const float* p10 = acc.px.data() + (static_cast<size_t>(y0c) * rw + x1c) * 4;
261
+ const float* p01 = acc.px.data() + (static_cast<size_t>(y1c) * rw + x0c) * 4;
262
+ const float* p11 = acc.px.data() + (static_cast<size_t>(y1c) * rw + x1c) * 4;
263
+
264
+ uint8_t* out = result.pixels.data() + (static_cast<size_t>(oy) * outW + ox) * 4;
265
+ for (int c = 0; c < 4; ++c) {
266
+ float top = p00[c] * (1.0f - wx) + p10[c] * wx;
267
+ float bot = p01[c] * (1.0f - wx) + p11[c] * wx;
268
+ float v = top * (1.0f - wy) + bot * wy;
269
+ out[c] = static_cast<uint8_t>(std::lround(clamp01(v) * 255.0f));
270
+ }
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+
276
+ } // namespace figmashadow
@@ -0,0 +1,21 @@
1
+ #pragma once
2
+
3
+ #include "Types.h"
4
+
5
+ namespace figmashadow {
6
+
7
+ // Renders every layer of a box-shadow into a single premultiplied RGBA8888
8
+ // bitmap sized to (content + bleed) * scale device pixels.
9
+ //
10
+ // The math is identical on every platform: an analytic Gaussian convolution of a
11
+ // rounded rectangle (exact error-function form for the sharp-cornered case, a
12
+ // bounded vertical quadrature for rounded corners). No platform 2D library is
13
+ // involved, so iOS and Android produce the same bytes.
14
+ Bitmap renderShadow(const RenderRequest& req);
15
+
16
+ // Largest render buffer, in device pixels, before the rasterizer transparently
17
+ // drops to a lower internal resolution (the result is still returned at full
18
+ // size via nearest sizing metadata on the caller side — here we simply cap).
19
+ constexpr double kMaxRenderPixels = 4.0e6;
20
+
21
+ } // namespace figmashadow
@@ -0,0 +1,66 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <vector>
5
+
6
+ namespace figmashadow {
7
+
8
+ // Straight (non-premultiplied) RGBA, components in [0, 1].
9
+ struct Color {
10
+ float r = 0.0f;
11
+ float g = 0.0f;
12
+ float b = 0.0f;
13
+ float a = 0.0f;
14
+ };
15
+
16
+ // A single CSS `box-shadow` layer.
17
+ struct ShadowLayer {
18
+ float offsetX = 0.0f; // logical px
19
+ float offsetY = 0.0f; // logical px
20
+ float blur = 0.0f; // CSS blur radius, logical px, always >= 0
21
+ float spread = 0.0f; // logical px, may be negative
22
+ Color color;
23
+ bool inset = false;
24
+ };
25
+
26
+ // Per-corner border radii, logical px.
27
+ struct CornerRadii {
28
+ float topLeft = 0.0f;
29
+ float topRight = 0.0f;
30
+ float bottomRight = 0.0f;
31
+ float bottomLeft = 0.0f;
32
+ };
33
+
34
+ // Extra space, in logical px, that the shadow bleeds past each edge of the
35
+ // element box. Computed on the JS side and passed in verbatim so the native
36
+ // view size and the rasterized buffer size always agree.
37
+ struct Bleed {
38
+ float left = 0.0f;
39
+ float top = 0.0f;
40
+ float right = 0.0f;
41
+ float bottom = 0.0f;
42
+ };
43
+
44
+ // RGBA8888, premultiplied alpha, row-major, tightly packed (stride = width * 4).
45
+ struct Bitmap {
46
+ int width = 0;
47
+ int height = 0;
48
+ std::vector<uint8_t> pixels;
49
+
50
+ bool empty() const {
51
+ return width <= 0 || height <= 0 ||
52
+ pixels.size() != static_cast<size_t>(width) * height * 4;
53
+ }
54
+ };
55
+
56
+ struct RenderRequest {
57
+ float contentWidth = 0.0f; // logical px, the element box
58
+ float contentHeight = 0.0f; // logical px
59
+ CornerRadii radii;
60
+ Bleed bleed;
61
+ float scale = 1.0f; // device pixel ratio
62
+ bool highQuality = false; // use the slower exact rounded-corner quadrature
63
+ std::vector<ShadowLayer> layers;
64
+ };
65
+
66
+ } // namespace figmashadow
@@ -0,0 +1,67 @@
1
+ # Architecture
2
+
3
+ ## The parity guarantee
4
+
5
+ Every pixel of every shadow is produced by one function — `figmashadow::renderShadow`
6
+ in [`cpp/figmashadow/Rasterizer.cpp`](../cpp/figmashadow/Rasterizer.cpp) — compiled
7
+ once from the same source into both the iOS pod and the Android `.so`. Neither
8
+ platform runs any blur code of its own (`CALayer.shadowRadius`, `elevation`,
9
+ `RenderEffect`, SVG `feGaussianBlur`, …), so there is nothing to diverge.
10
+
11
+ The math is an analytic Gaussian convolution of a rounded rectangle:
12
+
13
+ - **Sharp corners** (all radii 0): the 2D integral is separable into a product
14
+ of error functions — exact and cheap.
15
+ - **Rounded corners** (fast path, default): an SDF/error-function approximation.
16
+ Visually indistinguishable for shadows; ~1 `erf` per pixel.
17
+ - **Rounded corners** (`highQuality`): a bounded vertical quadrature of the
18
+ horizontal error-function term. Slower, closer to a reference `box-shadow`.
19
+
20
+ All three are pure `float`/`double` arithmetic — deterministic under IEEE-754,
21
+ so the same inputs give the same bytes on every device.
22
+
23
+ ## Data flow
24
+
25
+ ```
26
+ <Shadow shadow="0 4px 20px rgba(0,0,0,.15)" borderRadius={16}>
27
+
28
+ │ src/parseShadow.ts — JS parses only enough to size the bleed
29
+ │ src/index.tsx — reserves the bleed with negative margins
30
+
31
+ FigmaShadowView (Fabric native component, codegen spec in
32
+ src/FigmaShadowViewNativeComponent.ts)
33
+ │ props: shadow string, content size, radii, bleed, pixelRatio
34
+
35
+ figmashadow::render(...) cpp/figmashadow/FigmaShadow.cpp
36
+ │ parse (Parser.cpp) → rasterize (Rasterizer.cpp) → LRU memoize
37
+
38
+ premultiplied RGBA8888 bitmap, (content + bleed) * scale device px
39
+
40
+ ┌────┴─────────────────────────────┐
41
+ ▼ ▼
42
+ iOS: CGImage → CALayer.contents Android: Bitmap → Canvas.drawBitmap
43
+ (ios/FigmaShadowView.mm) (android/.../FigmaShadowView.kt)
44
+ ```
45
+
46
+ Rasterization runs on a background thread on both platforms; the memo cache
47
+ (keyed on the quantised request) makes every repeat — e.g. every row of a list —
48
+ effectively free.
49
+
50
+ ## Layout model
51
+
52
+ `<Shadow>` renders an outer `View` (your layout box) wrapping the native view.
53
+ The native view's border-box is `content + 2 × bleed`; equal **negative margins**
54
+ pull its frame back so siblings lay out as if only the children were there, and
55
+ equal **padding** pushes the children into the centre region. The shadow paints
56
+ into the bleed band, which is inside the native view's own bounds — so nothing
57
+ depends on `overflow` except an ancestor that sets `overflow: hidden`, which
58
+ clips the shadow exactly as CSS would.
59
+
60
+ ## What is not done yet
61
+
62
+ - No runnable example app in this repo (planned).
63
+ - `drop-shadow` that follows arbitrary content alpha (text, transparent PNGs) —
64
+ only shape shadows derived from `borderRadius`.
65
+ - No animated/interpolatable shadow props.
66
+ - The `backgroundColor` fill uses one corner radius for all four when per-corner
67
+ radii differ (the shadow itself is always exact).
Binary file
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @interface FigmaShadowView : RCTViewComponentView
7
+ @end
8
+
9
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,207 @@
1
+ #import "FigmaShadowView.h"
2
+
3
+ #import <react/renderer/components/RNFigmaShadowSpec/ComponentDescriptors.h>
4
+ #import <react/renderer/components/RNFigmaShadowSpec/EventEmitters.h>
5
+ #import <react/renderer/components/RNFigmaShadowSpec/Props.h>
6
+ #import <react/renderer/components/RNFigmaShadowSpec/RCTComponentViewHelpers.h>
7
+
8
+ #import "Color.h"
9
+ #import "FigmaShadow.h"
10
+
11
+ using namespace facebook::react;
12
+
13
+ @interface FigmaShadowView () <RCTFigmaShadowViewViewProtocol>
14
+ @end
15
+
16
+ @implementation FigmaShadowView {
17
+ CALayer *_shadowLayer;
18
+ CALayer *_fillLayer;
19
+
20
+ std::string _shadow;
21
+ std::string _backgroundColor;
22
+ float _radiusTL;
23
+ float _radiusTR;
24
+ float _radiusBR;
25
+ float _radiusBL;
26
+ float _bleedLeft;
27
+ float _bleedTop;
28
+ float _bleedRight;
29
+ float _bleedBottom;
30
+ float _pixelRatio;
31
+ bool _highQuality;
32
+ uint64_t _renderGeneration;
33
+ }
34
+
35
+ + (ComponentDescriptorProvider)componentDescriptorProvider
36
+ {
37
+ return concreteComponentDescriptorProvider<FigmaShadowViewComponentDescriptor>();
38
+ }
39
+
40
+ - (instancetype)initWithFrame:(CGRect)frame
41
+ {
42
+ if (self = [super initWithFrame:frame]) {
43
+ static const auto defaultProps = std::make_shared<const FigmaShadowViewProps>();
44
+ _props = defaultProps;
45
+ _pixelRatio = (float)[UIScreen mainScreen].scale;
46
+
47
+ _fillLayer = [CALayer layer];
48
+ _shadowLayer = [CALayer layer];
49
+ for (CALayer *layer in @[ _fillLayer, _shadowLayer ]) {
50
+ layer.actions = @{
51
+ @"contents" : [NSNull null],
52
+ @"position" : [NSNull null],
53
+ @"bounds" : [NSNull null],
54
+ @"backgroundColor" : [NSNull null],
55
+ };
56
+ }
57
+ // Fill sits under content; shadow (which includes inset darkening) over the
58
+ // fill but under content.
59
+ [self.layer insertSublayer:_fillLayer atIndex:0];
60
+ [self.layer insertSublayer:_shadowLayer atIndex:1];
61
+ }
62
+ return self;
63
+ }
64
+
65
+ #pragma mark - Props
66
+
67
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
68
+ {
69
+ const auto &newProps = *std::static_pointer_cast<FigmaShadowViewProps const>(props);
70
+
71
+ _shadow = newProps.shadow;
72
+ _backgroundColor = newProps.fillColor;
73
+ _radiusTL = newProps.borderTopLeftRadius;
74
+ _radiusTR = newProps.borderTopRightRadius;
75
+ _radiusBR = newProps.borderBottomRightRadius;
76
+ _radiusBL = newProps.borderBottomLeftRadius;
77
+ _bleedLeft = newProps.bleedLeft;
78
+ _bleedTop = newProps.bleedTop;
79
+ _bleedRight = newProps.bleedRight;
80
+ _bleedBottom = newProps.bleedBottom;
81
+ _highQuality = newProps.highQuality;
82
+ if (newProps.pixelRatio > 0) {
83
+ _pixelRatio = newProps.pixelRatio;
84
+ }
85
+
86
+ [super updateProps:props oldProps:oldProps];
87
+ [self setNeedsShadowRender];
88
+ }
89
+
90
+ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
91
+ oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
92
+ {
93
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
94
+ [self setNeedsShadowRender];
95
+ }
96
+
97
+ #pragma mark - Rendering
98
+
99
+ - (void)setNeedsShadowRender
100
+ {
101
+ // Coalesce prop + layout updates that arrive in the same commit.
102
+ [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(renderShadow) object:nil];
103
+ [self performSelector:@selector(renderShadow) withObject:nil afterDelay:0];
104
+ }
105
+
106
+ + (dispatch_queue_t)renderQueue
107
+ {
108
+ static dispatch_queue_t queue;
109
+ static dispatch_once_t once;
110
+ dispatch_once(&once, ^{
111
+ queue = dispatch_queue_create("com.figmashadow.raster", DISPATCH_QUEUE_SERIAL);
112
+ });
113
+ return queue;
114
+ }
115
+
116
+ - (void)renderShadow
117
+ {
118
+ const CGRect bounds = self.bounds;
119
+ const CGFloat viewW = CGRectGetWidth(bounds);
120
+ const CGFloat viewH = CGRectGetHeight(bounds);
121
+ if (viewW <= 0 || viewH <= 0) {
122
+ return;
123
+ }
124
+
125
+ const float contentW = (float)viewW - _bleedLeft - _bleedRight;
126
+ const float contentH = (float)viewH - _bleedTop - _bleedBottom;
127
+ if (contentW <= 0 || contentH <= 0) {
128
+ _shadowLayer.contents = nil;
129
+ _fillLayer.backgroundColor = nil;
130
+ return;
131
+ }
132
+
133
+ // --- background fill (cheap; stays on the main thread) ---
134
+ const CGRect contentRect = CGRectMake(_bleedLeft, _bleedTop, contentW, contentH);
135
+ figmashadow::Color fill;
136
+ const bool hasFill =
137
+ !_backgroundColor.empty() && figmashadow::parseColor(_backgroundColor, fill);
138
+ _fillLayer.frame = contentRect;
139
+ if (hasFill && fill.a > 0) {
140
+ _fillLayer.backgroundColor =
141
+ [UIColor colorWithRed:fill.r green:fill.g blue:fill.b alpha:fill.a].CGColor;
142
+ // Uniform-radius fast path; differing per-corner radii degrade to the
143
+ // top-left value for the fill only (the shadow itself is always exact).
144
+ _fillLayer.cornerRadius = _radiusTL;
145
+ } else {
146
+ _fillLayer.backgroundColor = nil;
147
+ }
148
+
149
+ // --- shadow raster (off the main thread; the cache makes repeats instant) ---
150
+ const uint64_t generation = ++_renderGeneration;
151
+ const std::string shadow = _shadow;
152
+ const float radiusTL = _radiusTL, radiusTR = _radiusTR, radiusBR = _radiusBR, radiusBL = _radiusBL;
153
+ const float bleedLeft = _bleedLeft, bleedTop = _bleedTop, bleedRight = _bleedRight, bleedBottom = _bleedBottom;
154
+ const float pixelRatio = _pixelRatio;
155
+ const bool highQuality = _highQuality;
156
+
157
+ __weak FigmaShadowView *weakSelf = self;
158
+ dispatch_async([FigmaShadowView renderQueue], ^{
159
+ figmashadow::Bitmap bmp = figmashadow::render(
160
+ contentW, contentH, radiusTL, radiusTR, radiusBR, radiusBL, shadow,
161
+ bleedLeft, bleedTop, bleedRight, bleedBottom, pixelRatio, highQuality);
162
+
163
+ CGImageRef image = bmp.empty() ? nullptr : [FigmaShadowView makeImageFromBitmap:bmp];
164
+
165
+ dispatch_async(dispatch_get_main_queue(), ^{
166
+ FigmaShadowView *strongSelf = weakSelf;
167
+ if (strongSelf == nil || generation != strongSelf->_renderGeneration) {
168
+ if (image) CGImageRelease(image);
169
+ return;
170
+ }
171
+ strongSelf->_shadowLayer.frame = strongSelf.bounds;
172
+ strongSelf->_shadowLayer.contentsScale = 1.0;
173
+ strongSelf->_shadowLayer.contentsGravity = kCAGravityResize;
174
+ strongSelf->_shadowLayer.contents = (__bridge_transfer id)image;
175
+ });
176
+ });
177
+ }
178
+
179
+ + (CGImageRef)makeImageFromBitmap:(const figmashadow::Bitmap &)bmp CF_RETURNS_RETAINED
180
+ {
181
+ const size_t bytesPerRow = (size_t)bmp.width * 4;
182
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
183
+ CGContextRef ctx = CGBitmapContextCreate(
184
+ (void *)bmp.pixels.data(), bmp.width, bmp.height, 8, bytesPerRow, colorSpace,
185
+ kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
186
+ CGImageRef image = CGBitmapContextCreateImage(ctx);
187
+ CGContextRelease(ctx);
188
+ CGColorSpaceRelease(colorSpace);
189
+ return image;
190
+ }
191
+
192
+ - (void)prepareForRecycle
193
+ {
194
+ [super prepareForRecycle];
195
+ _shadowLayer.contents = nil;
196
+ _fillLayer.contents = nil;
197
+ _fillLayer.backgroundColor = nil;
198
+ _shadow.clear();
199
+ _backgroundColor.clear();
200
+ }
201
+
202
+ @end
203
+
204
+ Class<RCTComponentViewProtocol> FigmaShadowViewCls(void)
205
+ {
206
+ return FigmaShadowView.class;
207
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _codegenNativeComponent = _interopRequireDefault(require("react-native/Libraries/Utilities/codegenNativeComponent"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ var _default = exports.default = (0, _codegenNativeComponent.default)('FigmaShadowView');
10
+ //# sourceMappingURL=FigmaShadowViewNativeComponent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_codegenNativeComponent","_interopRequireDefault","require","e","__esModule","default","_default","exports","codegenNativeComponent"],"sourceRoot":"../../src","sources":["FigmaShadowViewNativeComponent.ts"],"mappings":";;;;;;AAEA,IAAAA,uBAAA,GAAAC,sBAAA,CAAAC,OAAA;AAA6F,SAAAD,uBAAAE,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAAA,IAAAG,QAAA,GAAAC,OAAA,CAAAF,OAAA,GA2B9E,IAAAG,+BAAsB,EAAc,iBAAiB,CAAC","ignoreList":[]}