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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +76 -0
- package/android/gradle.properties +3 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/figmashadow/FigmaShadowView.kt +224 -0
- package/android/src/main/java/com/figmashadow/FigmaShadowViewManager.kt +71 -0
- package/android/src/main/java/com/figmashadow/FigmaShadowViewPackage.kt +30 -0
- package/android/src/main/jni/cpp-adapter.cpp +77 -0
- package/cpp/figmashadow/Color.cpp +225 -0
- package/cpp/figmashadow/Color.h +14 -0
- package/cpp/figmashadow/FigmaShadow.cpp +114 -0
- package/cpp/figmashadow/FigmaShadow.h +30 -0
- package/cpp/figmashadow/Parser.cpp +150 -0
- package/cpp/figmashadow/Parser.h +16 -0
- package/cpp/figmashadow/Rasterizer.cpp +276 -0
- package/cpp/figmashadow/Rasterizer.h +21 -0
- package/cpp/figmashadow/Types.h +66 -0
- package/docs/ARCHITECTURE.md +67 -0
- package/docs/images/fs_drop.png +0 -0
- package/docs/images/fs_inset.png +0 -0
- package/docs/images/fs_offset.png +0 -0
- package/ios/FigmaShadowView.h +9 -0
- package/ios/FigmaShadowView.mm +207 -0
- package/lib/commonjs/FigmaShadowViewNativeComponent.js +10 -0
- package/lib/commonjs/FigmaShadowViewNativeComponent.js.map +1 -0
- package/lib/commonjs/index.js +106 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/parseShadow.js +145 -0
- package/lib/commonjs/parseShadow.js.map +1 -0
- package/lib/module/FigmaShadowViewNativeComponent.js +5 -0
- package/lib/module/FigmaShadowViewNativeComponent.js.map +1 -0
- package/lib/module/index.js +88 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/parseShadow.js +139 -0
- package/lib/module/parseShadow.js.map +1 -0
- package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts +24 -0
- package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +40 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/parseShadow.d.ts +22 -0
- package/lib/typescript/src/parseShadow.d.ts.map +1 -0
- package/package.json +122 -0
- package/react-native-figma-shadow.podspec +25 -0
- package/react-native.config.js +19 -0
- package/src/FigmaShadowViewNativeComponent.ts +30 -0
- package/src/index.tsx +134 -0
- package/src/parseShadow.ts +134 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#include "Color.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <array>
|
|
5
|
+
#include <cctype>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <cstdlib>
|
|
9
|
+
#include <string>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
12
|
+
namespace figmashadow {
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
std::string toLower(std::string s) {
|
|
17
|
+
std::transform(s.begin(), s.end(), s.begin(),
|
|
18
|
+
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
std::string trim(const std::string& s) {
|
|
23
|
+
size_t a = s.find_first_not_of(" \t\n\r\f\v");
|
|
24
|
+
if (a == std::string::npos) return "";
|
|
25
|
+
size_t b = s.find_last_not_of(" \t\n\r\f\v");
|
|
26
|
+
return s.substr(a, b - a + 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
float clamp01(float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); }
|
|
30
|
+
|
|
31
|
+
bool hexNibble(char c, int& out) {
|
|
32
|
+
if (c >= '0' && c <= '9') { out = c - '0'; return true; }
|
|
33
|
+
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
34
|
+
if (c >= 'a' && c <= 'f') { out = c - 'a' + 10; return true; }
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
bool parseHex(const std::string& tok, Color& out) {
|
|
39
|
+
if (tok.empty() || tok[0] != '#') return false;
|
|
40
|
+
std::string h = tok.substr(1);
|
|
41
|
+
const size_t n = h.size();
|
|
42
|
+
if (n != 3 && n != 4 && n != 6 && n != 8) return false;
|
|
43
|
+
|
|
44
|
+
std::vector<int> nibbles;
|
|
45
|
+
nibbles.reserve(n);
|
|
46
|
+
for (char c : h) {
|
|
47
|
+
int v;
|
|
48
|
+
if (!hexNibble(c, v)) return false;
|
|
49
|
+
nibbles.push_back(v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
int r, g, b, a = 255;
|
|
53
|
+
if (n == 3 || n == 4) {
|
|
54
|
+
r = nibbles[0] * 17;
|
|
55
|
+
g = nibbles[1] * 17;
|
|
56
|
+
b = nibbles[2] * 17;
|
|
57
|
+
if (n == 4) a = nibbles[3] * 17;
|
|
58
|
+
} else {
|
|
59
|
+
r = nibbles[0] * 16 + nibbles[1];
|
|
60
|
+
g = nibbles[2] * 16 + nibbles[3];
|
|
61
|
+
b = nibbles[4] * 16 + nibbles[5];
|
|
62
|
+
if (n == 8) a = nibbles[6] * 16 + nibbles[7];
|
|
63
|
+
}
|
|
64
|
+
out.r = r / 255.0f;
|
|
65
|
+
out.g = g / 255.0f;
|
|
66
|
+
out.b = b / 255.0f;
|
|
67
|
+
out.a = a / 255.0f;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Splits the inside of `rgb( ... )` on commas and/or whitespace (and an
|
|
72
|
+
// optional `/` alpha separator), tolerating the modern and legacy syntaxes.
|
|
73
|
+
std::vector<std::string> splitArgs(const std::string& inner) {
|
|
74
|
+
std::vector<std::string> parts;
|
|
75
|
+
std::string cur;
|
|
76
|
+
for (char c : inner) {
|
|
77
|
+
if (c == ',' || c == '/' || std::isspace(static_cast<unsigned char>(c))) {
|
|
78
|
+
if (!cur.empty()) { parts.push_back(cur); cur.clear(); }
|
|
79
|
+
} else {
|
|
80
|
+
cur.push_back(c);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!cur.empty()) parts.push_back(cur);
|
|
84
|
+
return parts;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parses a channel that is either `0..255` or a percentage `0..100%`.
|
|
88
|
+
bool parseChannel(const std::string& s, float& out) {
|
|
89
|
+
if (s.empty()) return false;
|
|
90
|
+
char* end = nullptr;
|
|
91
|
+
double v = std::strtod(s.c_str(), &end);
|
|
92
|
+
if (end == s.c_str()) return false;
|
|
93
|
+
if (*end == '%') {
|
|
94
|
+
out = clamp01(static_cast<float>(v / 100.0));
|
|
95
|
+
} else {
|
|
96
|
+
out = clamp01(static_cast<float>(v / 255.0));
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bool parseAlpha(const std::string& s, float& out) {
|
|
102
|
+
if (s.empty()) return false;
|
|
103
|
+
char* end = nullptr;
|
|
104
|
+
double v = std::strtod(s.c_str(), &end);
|
|
105
|
+
if (end == s.c_str()) return false;
|
|
106
|
+
if (*end == '%') v /= 100.0;
|
|
107
|
+
out = clamp01(static_cast<float>(v));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void hslToRgb(float h, float s, float l, Color& out) {
|
|
112
|
+
h = std::fmod(std::fmod(h, 360.0f) + 360.0f, 360.0f) / 360.0f;
|
|
113
|
+
auto hue = [](float p, float q, float t) {
|
|
114
|
+
if (t < 0.0f) t += 1.0f;
|
|
115
|
+
if (t > 1.0f) t -= 1.0f;
|
|
116
|
+
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
|
|
117
|
+
if (t < 1.0f / 2.0f) return q;
|
|
118
|
+
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
|
|
119
|
+
return p;
|
|
120
|
+
};
|
|
121
|
+
if (s <= 0.0f) {
|
|
122
|
+
out.r = out.g = out.b = l;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
|
|
126
|
+
float p = 2.0f * l - q;
|
|
127
|
+
out.r = hue(p, q, h + 1.0f / 3.0f);
|
|
128
|
+
out.g = hue(p, q, h);
|
|
129
|
+
out.b = hue(p, q, h - 1.0f / 3.0f);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
bool parseFunctional(const std::string& tok, Color& out) {
|
|
133
|
+
size_t open = tok.find('(');
|
|
134
|
+
if (open == std::string::npos || tok.back() != ')') return false;
|
|
135
|
+
std::string fn = toLower(tok.substr(0, open));
|
|
136
|
+
std::string inner = tok.substr(open + 1, tok.size() - open - 2);
|
|
137
|
+
auto args = splitArgs(inner);
|
|
138
|
+
|
|
139
|
+
if (fn == "rgb" || fn == "rgba") {
|
|
140
|
+
if (args.size() < 3) return false;
|
|
141
|
+
float r, g, b, a = 1.0f;
|
|
142
|
+
if (!parseChannel(args[0], r) || !parseChannel(args[1], g) ||
|
|
143
|
+
!parseChannel(args[2], b))
|
|
144
|
+
return false;
|
|
145
|
+
if (args.size() >= 4 && !parseAlpha(args[3], a)) return false;
|
|
146
|
+
out = {r, g, b, a};
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (fn == "hsl" || fn == "hsla") {
|
|
150
|
+
if (args.size() < 3) return false;
|
|
151
|
+
char* e = nullptr;
|
|
152
|
+
float h = static_cast<float>(std::strtod(args[0].c_str(), &e));
|
|
153
|
+
if (e == args[0].c_str()) return false;
|
|
154
|
+
auto pct = [](const std::string& s, float& v) {
|
|
155
|
+
char* end = nullptr;
|
|
156
|
+
double d = std::strtod(s.c_str(), &end);
|
|
157
|
+
if (end == s.c_str()) return false;
|
|
158
|
+
v = static_cast<float>(d / 100.0);
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
float s, l, a = 1.0f;
|
|
162
|
+
if (!pct(args[1], s) || !pct(args[2], l)) return false;
|
|
163
|
+
s = clamp01(s);
|
|
164
|
+
l = clamp01(l);
|
|
165
|
+
if (args.size() >= 4 && !parseAlpha(args[3], a)) return false;
|
|
166
|
+
hslToRgb(h, s, l, out);
|
|
167
|
+
out.a = a;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
struct NamedColor {
|
|
174
|
+
const char* name;
|
|
175
|
+
uint32_t rgb; // 0xRRGGBB
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// The most common CSS named colors. rgb()/rgba()/hex cover the rest and are what
|
|
179
|
+
// Figma and design tools actually emit.
|
|
180
|
+
constexpr std::array<NamedColor, 34> kNamed = {{
|
|
181
|
+
{"black", 0x000000}, {"white", 0xffffff}, {"red", 0xff0000},
|
|
182
|
+
{"green", 0x008000}, {"blue", 0x0000ff}, {"yellow", 0xffff00},
|
|
183
|
+
{"cyan", 0x00ffff}, {"magenta", 0xff00ff}, {"gray", 0x808080},
|
|
184
|
+
{"grey", 0x808080}, {"silver", 0xc0c0c0}, {"maroon", 0x800000},
|
|
185
|
+
{"olive", 0x808000}, {"lime", 0x00ff00}, {"aqua", 0x00ffff},
|
|
186
|
+
{"teal", 0x008080}, {"navy", 0x000080}, {"fuchsia", 0xff00ff},
|
|
187
|
+
{"purple", 0x800080}, {"orange", 0xffa500}, {"pink", 0xffc0cb},
|
|
188
|
+
{"brown", 0xa52a2a}, {"gold", 0xffd700}, {"indigo", 0x4b0082},
|
|
189
|
+
{"violet", 0xee82ee}, {"crimson", 0xdc143c}, {"coral", 0xff7f50},
|
|
190
|
+
{"salmon", 0xfa8072}, {"khaki", 0xf0e68c}, {"lavender", 0xe6e6fa},
|
|
191
|
+
{"plum", 0xdda0dd}, {"orchid", 0xda70d6}, {"tan", 0xd2b48c},
|
|
192
|
+
{"turquoise", 0x40e0d0},
|
|
193
|
+
}};
|
|
194
|
+
|
|
195
|
+
} // namespace
|
|
196
|
+
|
|
197
|
+
bool parseColor(const std::string& raw, Color& out) {
|
|
198
|
+
std::string tok = trim(raw);
|
|
199
|
+
if (tok.empty()) return false;
|
|
200
|
+
|
|
201
|
+
if (tok[0] == '#') return parseHex(tok, out);
|
|
202
|
+
if (tok.find('(') != std::string::npos) return parseFunctional(tok, out);
|
|
203
|
+
|
|
204
|
+
std::string lower = toLower(tok);
|
|
205
|
+
if (lower == "transparent") {
|
|
206
|
+
out = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (lower == "currentcolor") {
|
|
210
|
+
out = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
for (const auto& nc : kNamed) {
|
|
214
|
+
if (lower == nc.name) {
|
|
215
|
+
out.r = ((nc.rgb >> 16) & 0xff) / 255.0f;
|
|
216
|
+
out.g = ((nc.rgb >> 8) & 0xff) / 255.0f;
|
|
217
|
+
out.b = (nc.rgb & 0xff) / 255.0f;
|
|
218
|
+
out.a = 1.0f;
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} // namespace figmashadow
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
#include "Types.h"
|
|
6
|
+
|
|
7
|
+
namespace figmashadow {
|
|
8
|
+
|
|
9
|
+
// Parses a CSS color token: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba(),
|
|
10
|
+
// hsl()/hsla(), `transparent`, `currentColor` (treated as opaque black) and the
|
|
11
|
+
// common CSS named colors. Returns false if `token` is not a color.
|
|
12
|
+
bool parseColor(const std::string& token, Color& out);
|
|
13
|
+
|
|
14
|
+
} // namespace figmashadow
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#include "FigmaShadow.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
#include <list>
|
|
6
|
+
#include <mutex>
|
|
7
|
+
#include <sstream>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <unordered_map>
|
|
10
|
+
|
|
11
|
+
#include "Parser.h"
|
|
12
|
+
#include "Rasterizer.h"
|
|
13
|
+
|
|
14
|
+
namespace figmashadow {
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
// Quantise floats to 1/8 px before hashing so sub-pixel layout jitter still
|
|
19
|
+
// hits the cache.
|
|
20
|
+
long quant(float v) { return std::lround(v * 8.0f); }
|
|
21
|
+
|
|
22
|
+
std::string makeKey(float cw, float ch, float rtl, float rtr, float rbr, float rbl,
|
|
23
|
+
const std::string& boxShadow, float bl, float bt, float br,
|
|
24
|
+
float bb, float scale, bool hq) {
|
|
25
|
+
std::ostringstream os;
|
|
26
|
+
os << quant(cw) << '|' << quant(ch) << '|' << quant(rtl) << '|' << quant(rtr)
|
|
27
|
+
<< '|' << quant(rbr) << '|' << quant(rbl) << '|' << quant(bl) << '|'
|
|
28
|
+
<< quant(bt) << '|' << quant(br) << '|' << quant(bb) << '|' << quant(scale)
|
|
29
|
+
<< '|' << (hq ? '1' : '0') << '|' << boxShadow;
|
|
30
|
+
return os.str();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
constexpr size_t kMaxCacheBytes = 6 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
struct Cache {
|
|
36
|
+
std::mutex mutex;
|
|
37
|
+
std::list<std::pair<std::string, Bitmap>> items; // front = most recent
|
|
38
|
+
std::unordered_map<std::string, decltype(items)::iterator> index;
|
|
39
|
+
size_t bytes = 0;
|
|
40
|
+
|
|
41
|
+
bool get(const std::string& key, Bitmap& out) {
|
|
42
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
43
|
+
auto it = index.find(key);
|
|
44
|
+
if (it == index.end()) return false;
|
|
45
|
+
items.splice(items.begin(), items, it->second);
|
|
46
|
+
out = it->second->second;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void put(const std::string& key, const Bitmap& bmp) {
|
|
51
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
52
|
+
if (index.count(key)) return;
|
|
53
|
+
items.emplace_front(key, bmp);
|
|
54
|
+
index[key] = items.begin();
|
|
55
|
+
bytes += bmp.pixels.size();
|
|
56
|
+
while (bytes > kMaxCacheBytes && items.size() > 1) {
|
|
57
|
+
auto& back = items.back();
|
|
58
|
+
bytes -= back.second.pixels.size();
|
|
59
|
+
index.erase(back.first);
|
|
60
|
+
items.pop_back();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
void clear() {
|
|
65
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
66
|
+
items.clear();
|
|
67
|
+
index.clear();
|
|
68
|
+
bytes = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
size_t size() {
|
|
72
|
+
std::lock_guard<std::mutex> lock(mutex);
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
Cache& cache() {
|
|
78
|
+
static Cache instance;
|
|
79
|
+
return instance;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
} // namespace
|
|
83
|
+
|
|
84
|
+
Bitmap render(float contentWidth, float contentHeight, float radiusTopLeft,
|
|
85
|
+
float radiusTopRight, float radiusBottomRight, float radiusBottomLeft,
|
|
86
|
+
const std::string& boxShadow, float bleedLeft, float bleedTop,
|
|
87
|
+
float bleedRight, float bleedBottom, float scale, bool highQuality) {
|
|
88
|
+
const std::string key =
|
|
89
|
+
makeKey(contentWidth, contentHeight, radiusTopLeft, radiusTopRight,
|
|
90
|
+
radiusBottomRight, radiusBottomLeft, boxShadow, bleedLeft, bleedTop,
|
|
91
|
+
bleedRight, bleedBottom, scale, highQuality);
|
|
92
|
+
|
|
93
|
+
Bitmap cached;
|
|
94
|
+
if (cache().get(key, cached)) return cached;
|
|
95
|
+
|
|
96
|
+
RenderRequest req;
|
|
97
|
+
req.contentWidth = contentWidth;
|
|
98
|
+
req.contentHeight = contentHeight;
|
|
99
|
+
req.radii = {radiusTopLeft, radiusTopRight, radiusBottomRight, radiusBottomLeft};
|
|
100
|
+
req.bleed = {bleedLeft, bleedTop, bleedRight, bleedBottom};
|
|
101
|
+
req.scale = scale;
|
|
102
|
+
req.highQuality = highQuality;
|
|
103
|
+
req.layers = parseBoxShadow(boxShadow);
|
|
104
|
+
|
|
105
|
+
Bitmap bmp = renderShadow(req);
|
|
106
|
+
if (!bmp.empty()) cache().put(key, bmp);
|
|
107
|
+
return bmp;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void clearCache() { cache().clear(); }
|
|
111
|
+
|
|
112
|
+
size_t cacheSizeBytes() { return cache().size(); }
|
|
113
|
+
|
|
114
|
+
} // namespace figmashadow
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
#include "Types.h"
|
|
6
|
+
|
|
7
|
+
namespace figmashadow {
|
|
8
|
+
|
|
9
|
+
// High-level entry point used by both platform glue layers.
|
|
10
|
+
//
|
|
11
|
+
// Parses `boxShadow`, rasterizes every layer into one premultiplied RGBA8888
|
|
12
|
+
// bitmap sized to (content + bleed) * scale device pixels, and memoizes the
|
|
13
|
+
// result: identical requests (e.g. every row of a list) reuse one bitmap.
|
|
14
|
+
//
|
|
15
|
+
// `bleed*` must be the same values the JS layer used to inflate the native
|
|
16
|
+
// view, so the bitmap lines up 1:1 with the view's pixels.
|
|
17
|
+
Bitmap render(float contentWidth, float contentHeight,
|
|
18
|
+
float radiusTopLeft, float radiusTopRight,
|
|
19
|
+
float radiusBottomRight, float radiusBottomLeft,
|
|
20
|
+
const std::string& boxShadow,
|
|
21
|
+
float bleedLeft, float bleedTop, float bleedRight, float bleedBottom,
|
|
22
|
+
float scale, bool highQuality = false);
|
|
23
|
+
|
|
24
|
+
// Drops every memoized bitmap. Call on a memory warning.
|
|
25
|
+
void clearCache();
|
|
26
|
+
|
|
27
|
+
// Approximate number of bytes held by the cache.
|
|
28
|
+
size_t cacheSizeBytes();
|
|
29
|
+
|
|
30
|
+
} // namespace figmashadow
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#include "Parser.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cctype>
|
|
5
|
+
#include <cstdlib>
|
|
6
|
+
|
|
7
|
+
#include "Color.h"
|
|
8
|
+
|
|
9
|
+
namespace figmashadow {
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
std::string trim(const std::string& s) {
|
|
14
|
+
size_t a = s.find_first_not_of(" \t\n\r\f\v");
|
|
15
|
+
if (a == std::string::npos) return "";
|
|
16
|
+
size_t b = s.find_last_not_of(" \t\n\r\f\v");
|
|
17
|
+
return s.substr(a, b - a + 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
std::string toLower(std::string s) {
|
|
21
|
+
std::transform(s.begin(), s.end(), s.begin(),
|
|
22
|
+
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Splits on the given delimiter, ignoring delimiters nested inside parentheses
|
|
27
|
+
// (so `rgba(0, 0, 0, .5)` survives a comma split).
|
|
28
|
+
std::vector<std::string> splitTopLevel(const std::string& s, char delim) {
|
|
29
|
+
std::vector<std::string> out;
|
|
30
|
+
std::string cur;
|
|
31
|
+
int depth = 0;
|
|
32
|
+
for (char c : s) {
|
|
33
|
+
if (c == '(') depth++;
|
|
34
|
+
else if (c == ')') depth = std::max(0, depth - 1);
|
|
35
|
+
|
|
36
|
+
if (c == delim && depth == 0) {
|
|
37
|
+
out.push_back(cur);
|
|
38
|
+
cur.clear();
|
|
39
|
+
} else {
|
|
40
|
+
cur.push_back(c);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
out.push_back(cur);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Whitespace tokenizer that keeps `fn( ... )` groups intact.
|
|
48
|
+
std::vector<std::string> tokenize(const std::string& s) {
|
|
49
|
+
std::vector<std::string> out;
|
|
50
|
+
std::string cur;
|
|
51
|
+
int depth = 0;
|
|
52
|
+
for (char c : s) {
|
|
53
|
+
if (c == '(') depth++;
|
|
54
|
+
else if (c == ')') depth = std::max(0, depth - 1);
|
|
55
|
+
|
|
56
|
+
if (std::isspace(static_cast<unsigned char>(c)) && depth == 0) {
|
|
57
|
+
if (!cur.empty()) { out.push_back(cur); cur.clear(); }
|
|
58
|
+
} else {
|
|
59
|
+
cur.push_back(c);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!cur.empty()) out.push_back(cur);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parses a CSS length. Accepts a bare number or one suffixed with `px`/`dp`/`pt`
|
|
67
|
+
// (all treated as logical px). Rejects other units so they don't get mistaken
|
|
68
|
+
// for offsets.
|
|
69
|
+
bool parseLength(const std::string& tok, float& out) {
|
|
70
|
+
if (tok.empty()) return false;
|
|
71
|
+
char* end = nullptr;
|
|
72
|
+
double v = std::strtod(tok.c_str(), &end);
|
|
73
|
+
if (end == tok.c_str()) return false;
|
|
74
|
+
std::string unit = toLower(trim(std::string(end)));
|
|
75
|
+
if (unit.empty() || unit == "px" || unit == "dp" || unit == "dip" || unit == "pt") {
|
|
76
|
+
out = static_cast<float>(v);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ShadowLayer parseLayer(const std::string& raw, bool& ok) {
|
|
83
|
+
ShadowLayer layer;
|
|
84
|
+
ok = false;
|
|
85
|
+
|
|
86
|
+
std::vector<float> lengths;
|
|
87
|
+
bool hasColor = false;
|
|
88
|
+
|
|
89
|
+
for (const auto& tok : tokenize(raw)) {
|
|
90
|
+
std::string lower = toLower(tok);
|
|
91
|
+
if (lower == "inset") {
|
|
92
|
+
layer.inset = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
float len;
|
|
96
|
+
Color color;
|
|
97
|
+
// A bare number is always a length; try length before color so tokens like
|
|
98
|
+
// `0` aren't swallowed by a lenient color parser.
|
|
99
|
+
if (parseLength(tok, len)) {
|
|
100
|
+
lengths.push_back(len);
|
|
101
|
+
} else if (!hasColor && parseColor(tok, color)) {
|
|
102
|
+
layer.color = color;
|
|
103
|
+
hasColor = true;
|
|
104
|
+
} else {
|
|
105
|
+
// Unknown token: ignore it rather than dropping the whole layer.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (lengths.size() < 2) return layer;
|
|
110
|
+
|
|
111
|
+
layer.offsetX = lengths[0];
|
|
112
|
+
layer.offsetY = lengths[1];
|
|
113
|
+
if (lengths.size() >= 3) layer.blur = std::max(0.0f, lengths[2]);
|
|
114
|
+
if (lengths.size() >= 4) layer.spread = lengths[3];
|
|
115
|
+
if (!hasColor) layer.color = {0.0f, 0.0f, 0.0f, 1.0f}; // CSS default is currentColor
|
|
116
|
+
|
|
117
|
+
ok = true;
|
|
118
|
+
return layer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
} // namespace
|
|
122
|
+
|
|
123
|
+
std::vector<ShadowLayer> parseBoxShadow(const std::string& input) {
|
|
124
|
+
std::string s = trim(input);
|
|
125
|
+
|
|
126
|
+
// Strip an optional `box-shadow:` / `boxShadow:` prefix.
|
|
127
|
+
size_t colon = s.find(':');
|
|
128
|
+
if (colon != std::string::npos) {
|
|
129
|
+
std::string head = toLower(trim(s.substr(0, colon)));
|
|
130
|
+
if (head == "box-shadow" || head == "boxshadow") {
|
|
131
|
+
s = trim(s.substr(colon + 1));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!s.empty() && s.back() == ';') s.pop_back();
|
|
135
|
+
s = trim(s);
|
|
136
|
+
|
|
137
|
+
std::vector<ShadowLayer> layers;
|
|
138
|
+
if (s.empty() || toLower(s) == "none") return layers;
|
|
139
|
+
|
|
140
|
+
for (const auto& part : splitTopLevel(s, ',')) {
|
|
141
|
+
std::string layerStr = trim(part);
|
|
142
|
+
if (layerStr.empty()) continue;
|
|
143
|
+
bool ok = false;
|
|
144
|
+
ShadowLayer layer = parseLayer(layerStr, ok);
|
|
145
|
+
if (ok) layers.push_back(layer);
|
|
146
|
+
}
|
|
147
|
+
return layers;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} // namespace figmashadow
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
#include "Types.h"
|
|
7
|
+
|
|
8
|
+
namespace figmashadow {
|
|
9
|
+
|
|
10
|
+
// Parses a CSS `box-shadow` value into layers, in source order (the first layer
|
|
11
|
+
// paints on top). Tolerates a leading `box-shadow:` and a trailing `;`, the
|
|
12
|
+
// `inset` keyword in any position, 2-4 lengths, and a color token anywhere in
|
|
13
|
+
// the declaration. Unparseable layers are skipped.
|
|
14
|
+
std::vector<ShadowLayer> parseBoxShadow(const std::string& input);
|
|
15
|
+
|
|
16
|
+
} // namespace figmashadow
|