1 /*
2 * Copyright (c) 2024 the ThorVG project. All rights reserved.
3
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10
11 * The above copyright notice and this permission notice shall be included in all
12 * copies or substantial portions of the Software.
13
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 * SOFTWARE.
21 */
22
23 #include "../../lv_conf_internal.h"
24 #if LV_USE_THORVG_INTERNAL
25
26 #include "tvgLottieModifier.h"
27
28
29 /************************************************************************/
30 /* Internal Class Implementation */
31 /************************************************************************/
32
_roundCorner(Array<PathCommand> & cmds,Array<Point> & pts,const Point & prev,const Point & curr,const Point & next,float r)33 static void _roundCorner(Array<PathCommand>& cmds, Array<Point>& pts, const Point& prev, const Point& curr, const Point& next, float r)
34 {
35 auto lenPrev = length(prev - curr);
36 auto rPrev = lenPrev > 0.0f ? 0.5f * std::min(lenPrev * 0.5f, r) / lenPrev : 0.0f;
37 auto lenNext = length(next - curr);
38 auto rNext = lenNext > 0.0f ? 0.5f * std::min(lenNext * 0.5f, r) / lenNext : 0.0f;
39
40 auto dPrev = rPrev * (curr - prev);
41 auto dNext = rNext * (curr - next);
42
43 pts.push(curr - 2.0f * dPrev);
44 pts.push(curr - dPrev);
45 pts.push(curr - dNext);
46 pts.push(curr - 2.0f * dNext);
47 cmds.push(PathCommand::LineTo);
48 cmds.push(PathCommand::CubicTo);
49 }
50
51
_zero(const Point & p1,const Point & p2)52 static bool _zero(const Point& p1, const Point& p2)
53 {
54 constexpr float epsilon = 1e-3f;
55 return fabsf(p1.x / p2.x - 1.0f) < epsilon && fabsf(p1.y / p2.y - 1.0f) < epsilon;
56 }
57
58
_intersect(const Line & line1,const Line & line2,Point & intersection,bool & inside)59 static bool _intersect(const Line& line1, const Line& line2, Point& intersection, bool& inside)
60 {
61 if (_zero(line1.pt2, line2.pt1)) {
62 intersection = line1.pt2;
63 inside = true;
64 return true;
65 }
66
67 constexpr float epsilon = 1e-3f;
68 float denom = (line1.pt2.x - line1.pt1.x) * (line2.pt2.y - line2.pt1.y) - (line1.pt2.y - line1.pt1.y) * (line2.pt2.x - line2.pt1.x);
69 if (fabsf(denom) < epsilon) return false;
70
71 float t = ((line2.pt1.x - line1.pt1.x) * (line2.pt2.y - line2.pt1.y) - (line2.pt1.y - line1.pt1.y) * (line2.pt2.x - line2.pt1.x)) / denom;
72 float u = ((line2.pt1.x - line1.pt1.x) * (line1.pt2.y - line1.pt1.y) - (line2.pt1.y - line1.pt1.y) * (line1.pt2.x - line1.pt1.x)) / denom;
73
74 intersection.x = line1.pt1.x + t * (line1.pt2.x - line1.pt1.x);
75 intersection.y = line1.pt1.y + t * (line1.pt2.y - line1.pt1.y);
76 inside = t >= -epsilon && t <= 1.0f + epsilon && u >= -epsilon && u <= 1.0f + epsilon;
77
78 return true;
79 }
80
81
_offset(const Point & p1,const Point & p2,float offset)82 static Line _offset(const Point& p1, const Point& p2, float offset)
83 {
84 auto scaledNormal = normal(p1, p2) * offset;
85 return {p1 + scaledNormal, p2 + scaledNormal};
86 }
87
88
_clockwise(const Point * pts,uint32_t n)89 static bool _clockwise(const Point* pts, uint32_t n)
90 {
91 auto area = 0.0f;
92
93 for (uint32_t i = 0; i < n - 1; i++) {
94 area += cross(pts[i], pts[i + 1]);
95 }
96 area += cross(pts[n - 1], pts[0]);;
97
98 return area < 0.0f;
99 }
100
101
corner(const Line & line,const Line & nextLine,uint32_t movetoOutIndex,bool nextClose,Array<PathCommand> & outCmds,Array<Point> & outPts) const102 void LottieOffsetModifier::corner(const Line& line, const Line& nextLine, uint32_t movetoOutIndex, bool nextClose, Array<PathCommand>& outCmds, Array<Point>& outPts) const
103 {
104 bool inside{};
105 Point intersect{};
106 if (_intersect(line, nextLine, intersect, inside)) {
107 if (inside) {
108 if (nextClose) outPts[movetoOutIndex] = intersect;
109 outPts.push(intersect);
110 } else {
111 outPts.push(line.pt2);
112 if (join == StrokeJoin::Round) {
113 outCmds.push(PathCommand::CubicTo);
114 outPts.push((line.pt2 + intersect) * 0.5f);
115 outPts.push((nextLine.pt1 + intersect) * 0.5f);
116 outPts.push(nextLine.pt1);
117 } else if (join == StrokeJoin::Miter) {
118 auto norm = normal(line.pt1, line.pt2);
119 auto nextNorm = normal(nextLine.pt1, nextLine.pt2);
120 auto miterDirection = (norm + nextNorm) / length(norm + nextNorm);
121 outCmds.push(PathCommand::LineTo);
122 if (1.0f <= miterLimit * fabsf(miterDirection.x * norm.x + miterDirection.y * norm.y)) outPts.push(intersect);
123 else outPts.push(nextLine.pt1);
124 } else {
125 outCmds.push(PathCommand::LineTo);
126 outPts.push(nextLine.pt1);
127 }
128 }
129 } else outPts.push(line.pt2);
130 }
131
132
line(const PathCommand * inCmds,uint32_t inCmdsCnt,const Point * inPts,uint32_t & currentPt,uint32_t currentCmd,State & state,bool degenerated,Array<PathCommand> & outCmds,Array<Point> & outPts,float offset) const133 void LottieOffsetModifier::line(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t& currentPt, uint32_t currentCmd, State& state, bool degenerated, Array<PathCommand>& outCmds, Array<Point>& outPts, float offset) const
134 {
135 if (tvg::zero(inPts[currentPt - 1] - inPts[currentPt])) {
136 ++currentPt;
137 return;
138 }
139
140 if (inCmds[currentCmd - 1] != PathCommand::LineTo) state.line = _offset(inPts[currentPt - 1], inPts[currentPt], offset);
141
142 if (state.moveto) {
143 outCmds.push(PathCommand::MoveTo);
144 state.movetoOutIndex = outPts.count;
145 outPts.push(state.line.pt1);
146 state.firstLine = state.line;
147 state.moveto = false;
148 }
149
150 auto nonDegeneratedCubic = [&](uint32_t cmd, uint32_t pt) {
151 return inCmds[cmd] == PathCommand::CubicTo && !tvg::zero(inPts[pt] - inPts[pt + 1]) && !tvg::zero(inPts[pt + 2] - inPts[pt + 3]);
152 };
153
154 outCmds.push(PathCommand::LineTo);
155
156 if (currentCmd + 1 == inCmdsCnt || inCmds[currentCmd + 1] == PathCommand::MoveTo || nonDegeneratedCubic(currentCmd + 1, currentPt + degenerated)) {
157 outPts.push(state.line.pt2);
158 ++currentPt;
159 return;
160 }
161
162 Line nextLine = state.firstLine;
163 if (inCmds[currentCmd + 1] == PathCommand::LineTo) nextLine = _offset(inPts[currentPt + degenerated], inPts[currentPt + 1 + degenerated], offset);
164 else if (inCmds[currentCmd + 1] == PathCommand::CubicTo) nextLine = _offset(inPts[currentPt + 1 + degenerated], inPts[currentPt + 2 + degenerated], offset);
165 else if (inCmds[currentCmd + 1] == PathCommand::Close && !_zero(inPts[currentPt + degenerated], inPts[state.movetoInIndex + degenerated]))
166 nextLine = _offset(inPts[currentPt + degenerated], inPts[state.movetoInIndex + degenerated], offset);
167
168 corner(state.line, nextLine, state.movetoOutIndex, inCmds[currentCmd + 1] == PathCommand::Close, outCmds, outPts);
169
170 state.line = nextLine;
171 ++currentPt;
172 }
173
174 /************************************************************************/
175 /* External Class Implementation */
176 /************************************************************************/
177
modifyPath(const PathCommand * inCmds,uint32_t inCmdsCnt,const Point * inPts,uint32_t inPtsCnt,Array<PathCommand> & outCmds,Array<Point> & outPts,Matrix * transform) const178 bool LottieRoundnessModifier::modifyPath(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array<PathCommand>& outCmds, Array<Point>& outPts, Matrix* transform) const
179 {
180 outCmds.reserve(inCmdsCnt * 2);
181 outPts.reserve((uint32_t)(inPtsCnt * 1.5));
182 auto ptsCnt = outPts.count;
183
184 uint32_t startIndex = 0;
185 for (uint32_t iCmds = 0, iPts = 0; iCmds < inCmdsCnt; ++iCmds) {
186 switch (inCmds[iCmds]) {
187 case PathCommand::MoveTo: {
188 startIndex = outPts.count;
189 outCmds.push(PathCommand::MoveTo);
190 outPts.push(inPts[iPts++]);
191 break;
192 }
193 case PathCommand::CubicTo: {
194 auto& prev = inPts[iPts - 1];
195 auto& curr = inPts[iPts + 2];
196 if (iCmds < inCmdsCnt - 1 &&
197 tvg::zero(inPts[iPts - 1] - inPts[iPts]) &&
198 tvg::zero(inPts[iPts + 1] - inPts[iPts + 2])) {
199 if (inCmds[iCmds + 1] == PathCommand::CubicTo &&
200 tvg::zero(inPts[iPts + 2] - inPts[iPts + 3]) &&
201 tvg::zero(inPts[iPts + 4] - inPts[iPts + 5])) {
202 _roundCorner(outCmds, outPts, prev, curr, inPts[iPts + 5], r);
203 iPts += 3;
204 break;
205 } else if (inCmds[iCmds + 1] == PathCommand::Close) {
206 _roundCorner(outCmds, outPts, prev, curr, inPts[2], r);
207 outPts[startIndex] = outPts.last();
208 iPts += 3;
209 break;
210 }
211 }
212 outCmds.push(PathCommand::CubicTo);
213 outPts.push(inPts[iPts++]);
214 outPts.push(inPts[iPts++]);
215 outPts.push(inPts[iPts++]);
216 break;
217 }
218 case PathCommand::Close: {
219 outCmds.push(PathCommand::Close);
220 break;
221 }
222 default: break;
223 }
224 }
225 if (transform) {
226 for (auto i = ptsCnt; i < outPts.count; ++i) {
227 outPts[i] *= *transform;
228 }
229 }
230 return true;
231 }
232
233
modifyPolystar(TVG_UNUSED const Array<PathCommand> & inCmds,const Array<Point> & inPts,Array<PathCommand> & outCmds,Array<Point> & outPts,float outerRoundness,bool hasRoundness) const234 bool LottieRoundnessModifier::modifyPolystar(TVG_UNUSED const Array<PathCommand>& inCmds, const Array<Point>& inPts, Array<PathCommand>& outCmds, Array<Point>& outPts, float outerRoundness, bool hasRoundness) const
235 {
236 static constexpr auto ROUNDED_POLYSTAR_MAGIC_NUMBER = 0.47829f;
237
238 auto len = length(inPts[1] - inPts[2]);
239 auto r = len > 0.0f ? ROUNDED_POLYSTAR_MAGIC_NUMBER * std::min(len * 0.5f, this->r) / len : 0.0f;
240
241 if (hasRoundness) {
242 outCmds.grow((uint32_t)(1.5 * inCmds.count));
243 outPts.grow((uint32_t)(4.5 * inCmds.count));
244
245 int start = 3 * tvg::zero(outerRoundness);
246 outCmds.push(PathCommand::MoveTo);
247 outPts.push(inPts[start]);
248
249 for (uint32_t i = 1 + start; i < inPts.count; i += 6) {
250 auto& prev = inPts[i];
251 auto& curr = inPts[i + 2];
252 auto& next = (i < inPts.count - start) ? inPts[i + 4] : inPts[2];
253 auto& nextCtrl = (i < inPts.count - start) ? inPts[i + 5] : inPts[3];
254 auto dNext = r * (curr - next);
255 auto dPrev = r * (curr - prev);
256
257 auto p0 = curr - 2.0f * dPrev;
258 auto p1 = curr - dPrev;
259 auto p2 = curr - dNext;
260 auto p3 = curr - 2.0f * dNext;
261
262 outCmds.push(PathCommand::CubicTo);
263 outPts.push(prev); outPts.push(p0); outPts.push(p0);
264 outCmds.push(PathCommand::CubicTo);
265 outPts.push(p1); outPts.push(p2); outPts.push(p3);
266 outCmds.push(PathCommand::CubicTo);
267 outPts.push(p3); outPts.push(next); outPts.push(nextCtrl);
268 }
269 } else {
270 outCmds.grow(2 * inCmds.count);
271 outPts.grow(4 * inCmds.count);
272
273 auto dPrev = r * (inPts[1] - inPts[0]);
274 auto p = inPts[0] + 2.0f * dPrev;
275 outCmds.push(PathCommand::MoveTo);
276 outPts.push(p);
277
278 for (uint32_t i = 1; i < inPts.count; ++i) {
279 auto& curr = inPts[i];
280 auto& next = (i == inPts.count - 1) ? inPts[1] : inPts[i + 1];
281 auto dNext = r * (curr - next);
282
283 auto p0 = curr - 2.0f * dPrev;
284 auto p1 = curr - dPrev;
285 auto p2 = curr - dNext;
286 auto p3 = curr - 2.0f * dNext;
287
288 outCmds.push(PathCommand::LineTo);
289 outPts.push(p0);
290 outCmds.push(PathCommand::CubicTo);
291 outPts.push(p1); outPts.push(p2); outPts.push(p3);
292
293 dPrev = -1.0f * dNext;
294 }
295 }
296 outCmds.push(PathCommand::Close);
297 return true;
298 }
299
300
modifyRect(const Point & size,float & r) const301 bool LottieRoundnessModifier::modifyRect(const Point& size, float& r) const
302 {
303 r = std::min(this->r, std::max(size.x, size.y) * 0.5f);
304 return true;
305 }
306
307
modifyPath(const PathCommand * inCmds,uint32_t inCmdsCnt,const Point * inPts,uint32_t inPtsCnt,Array<PathCommand> & outCmds,Array<Point> & outPts) const308 bool LottieOffsetModifier::modifyPath(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array<PathCommand>& outCmds, Array<Point>& outPts) const
309 {
310 outCmds.reserve(inCmdsCnt * 2);
311 outPts.reserve(inPtsCnt * (join == StrokeJoin::Round ? 4 : 2));
312
313 Array<Bezier> stack{5};
314 State state;
315 auto offset = _clockwise(inPts, inPtsCnt) ? this->offset : -this->offset;
316 auto threshold = 1.0f / fabsf(offset) + 1.0f;
317
318 for (uint32_t iCmd = 0, iPt = 0; iCmd < inCmdsCnt; ++iCmd) {
319 if (inCmds[iCmd] == PathCommand::MoveTo) {
320 state.moveto = true;
321 state.movetoInIndex = iPt++;
322 } else if (inCmds[iCmd] == PathCommand::LineTo) {
323 line(inCmds, inCmdsCnt, inPts, iPt, iCmd, state, false, outCmds, outPts, offset);
324 } else if (inCmds[iCmd] == PathCommand::CubicTo) {
325 //cubic degenerated to a line
326 if (tvg::zero(inPts[iPt - 1] - inPts[iPt]) || tvg::zero(inPts[iPt + 1] - inPts[iPt + 2])) {
327 ++iPt;
328 line(inCmds, inCmdsCnt, inPts, iPt, iCmd, state, true, outCmds, outPts, offset);
329 ++iPt;
330 continue;
331 }
332
333 stack.push({inPts[iPt - 1], inPts[iPt], inPts[iPt + 1], inPts[iPt + 2]});
334 while (!stack.empty()) {
335 auto& bezier = stack.last();
336 auto len = tvg::length(bezier.start - bezier.ctrl1) + tvg::length(bezier.ctrl1 - bezier.ctrl2) + tvg::length(bezier.ctrl2 - bezier.end);
337
338 if (len > threshold * bezier.length()) {
339 Bezier next;
340 bezier.split(0.5f, next);
341 stack.push(next);
342 continue;
343 }
344 stack.pop();
345
346 auto line1 = _offset(bezier.start, bezier.ctrl1, offset);
347 auto line2 = _offset(bezier.ctrl1, bezier.ctrl2, offset);
348 auto line3 = _offset(bezier.ctrl2, bezier.end, offset);
349
350 if (state.moveto) {
351 outCmds.push(PathCommand::MoveTo);
352 state.movetoOutIndex = outPts.count;
353 outPts.push(line1.pt1);
354 state.firstLine = line1;
355 state.moveto = false;
356 }
357
358 bool inside{};
359 Point intersect{};
360 _intersect(line1, line2, intersect, inside);
361 outPts.push(intersect);
362 _intersect(line2, line3, intersect, inside);
363 outPts.push(intersect);
364 outPts.push(line3.pt2);
365 outCmds.push(PathCommand::CubicTo);
366 }
367
368 iPt += 3;
369 }
370 else {
371 if (!_zero(inPts[iPt - 1], inPts[state.movetoInIndex])) {
372 outCmds.push(PathCommand::LineTo);
373 corner(state.line, state.firstLine, state.movetoOutIndex, true, outCmds, outPts);
374 }
375 outCmds.push(PathCommand::Close);
376 }
377 }
378 return true;
379 }
380
381
modifyPolystar(const Array<PathCommand> & inCmds,const Array<Point> & inPts,Array<PathCommand> & outCmds,Array<Point> & outPts) const382 bool LottieOffsetModifier::modifyPolystar(const Array<PathCommand>& inCmds, const Array<Point>& inPts, Array<PathCommand>& outCmds, Array<Point>& outPts) const {
383 return modifyPath(inCmds.data, inCmds.count, inPts.data, inPts.count, outCmds, outPts);
384 }
385
386
modifyRect(const PathCommand * inCmds,uint32_t inCmdsCnt,const Point * inPts,uint32_t inPtsCnt,Array<PathCommand> & outCmds,Array<Point> & outPts) const387 bool LottieOffsetModifier::modifyRect(const PathCommand* inCmds, uint32_t inCmdsCnt, const Point* inPts, uint32_t inPtsCnt, Array<PathCommand>& outCmds, Array<Point>& outPts) const
388 {
389 return modifyPath(inCmds, inCmdsCnt, inPts, inPtsCnt, outCmds, outPts);
390 }
391
392
modifyEllipse(float & rx,float & ry) const393 bool LottieOffsetModifier::modifyEllipse(float& rx, float& ry) const
394 {
395 rx += offset;
396 ry += offset;
397 return true;
398 }
399
400 #endif /* LV_USE_THORVG_INTERNAL */
401
402