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