Coverage for lib/lottie/utils/funky_parser.py: 0%
964 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-20 16:17 +0100
1import re
2import sys
3import enum
4import math
5import typing
7from ..parsers.svg.svgdata import color_table
8from ..parsers.svg.importer import parse_color, parse_svg_file
9from ..objects.animation import Animation
10from ..objects import layers
11from ..objects import shapes
12from ..nvector import NVector
13from .font import FontStyle
16color_words = {
17 "alice": {"blue": {"_": 1}},
18 "antique": {"white": {"_": 1}},
19 "aqua": {
20 "_": 1,
21 "marine": {"_": 1}
22 },
23 "azure": {"_": 1},
24 "beige": {"_": 1},
25 "bisque": {"_": 1},
26 "black": {"_": 1},
27 "blanched": {"almond": {"_": 1}},
28 "blue": {
29 "_": 1,
30 "navy": "navy",
31 "violet": {"_": 1},
32 },
33 "brown": {"_": 1},
34 "burly": {"wood": {"_": 1}},
35 "cadet": {"blue": {"_": 1}},
36 "chartreuse": {"_": 1},
37 "chocolate": {"_": 1},
38 "coral": {"_": 1},
39 "cornflower": {"blue": {"_": 1}},
40 "corn": {
41 "silk": {"_": 1},
42 "flower": {"blue": {"_": 1}},
43 },
44 "crimson": {"_": 1},
45 "cyan": {"_": 1},
46 "dark": {
47 "blue": {"_": 1},
48 "cyan": {"_": 1},
49 "golden": {"rod": {"_": 1}},
50 "gray": {"_": 1},
51 "green": {"_": 1},
52 "grey": {"_": 1},
53 "khaki": {"_": 1},
54 "magenta": {"_": 1},
55 "olive": {"green": {"_": 1}},
56 "orange": {"_": 1},
57 "orchid": {"_": 1},
58 "red": {"_": 1},
59 "salmon": {"_": 1},
60 "sea": {"green": {"_": 1}},
61 "slate": {
62 "blue": {"_": 1},
63 "gray": {"_": 1},
64 "grey": {"_": 1}
65 },
66 "turquoise": {"_": 1},
67 "violet": {"_": 1}
68 },
69 "deep": {
70 "pink": {"_": 1},
71 "sky": {"blue": {"_": 1}},
72 },
73 "dim": {
74 "gray": {"_": 1},
75 "grey": {"_": 1}
76 },
77 "dodger": {"blue": {"_": 1}},
78 "fire": {"brick": {"_": 1}},
79 "floral": {"white": {"_": 1}},
80 "forest": {"green": {"_": 1}},
81 "fuchsia": {"_": 1},
82 "gainsboro": {"_": 1},
83 "ghost": {"white": {"_": 1}},
84 "gold": {"_": 1},
85 "golden": {"rod": {"_": 1}},
86 "gray": {"_": 1},
87 "green": {
88 "_": 1,
89 "yellow": {"_": 1}
90 },
91 "grey": {"_": 1},
92 "honeydew": {"_": 1},
93 "hotpink": {"_": 1},
94 "indian": {"red": {"_": 1}},
95 "indigo": {"_": 1},
96 "ivory": {"_": 1},
97 "khaki": {"_": 1},
98 "lavender": {
99 "_": 1,
100 "blush": {"_": 1},
101 },
102 "lawn": {"green": {"_": 1}},
103 "lemon": {"chiffon": {"_": 1}},
104 "light": {
105 "blue": {"_": 1},
106 "coral": {"_": 1},
107 "cyan": {"_": 1},
108 "golden": {
109 "rod": {
110 "_": 1,
111 "yellow": {"_": 1},
112 }
113 },
114 "gray": {"_": 1},
115 "green": {"_": 1},
116 "grey": {"_": 1},
117 "pink": {"_": 1},
118 "salmon": {"_": 1},
119 "sea": {"green": {"_": 1}},
120 "sky": {"blue": {"_": 1}},
121 "slate": {
122 "gray": {"_": 1},
123 "grey": {"_": 1}
124 },
125 "steel": {"blue": {"_": 1}},
126 "yellow": {"_": 1},
127 },
128 "lime": {
129 "_": 1,
130 "green": {"_": 1},
131 },
132 "linen": {"_": 1},
133 "magenta": {"_": 1},
134 "maroon": {"_": 1},
135 "medium": {
136 "aquamarine": {"_": 1},
137 "blue": {"_": 1},
138 "orchid": {"_": 1},
139 "purple": {"_": 1},
140 "sea": {"green": {"_": 1}},
141 "slate": {"blue": {"_": 1}},
142 "spring": {"green": {"_": 1}},
143 "turquoise": {"_": 1},
144 "violet": {"red": {"_": 1}},
145 },
146 "midnight": {"blue": {"_": 1}},
147 "mint": {"cream": {"_": 1}},
148 "misty": {"rose": {"_": 1}},
149 "moccasin": {"_": 1},
150 "navajo": {"white": {"_": 1}},
151 "navy": {
152 "_": 1,
153 "blue": "navy",
154 },
155 "old": {"lace": {"_": 1}},
156 "olive": {
157 "_": 1,
158 "drab": {"_": 1},
159 },
160 "orange": {
161 "_": 1,
162 "red": {"_": 1},
163 },
164 "orchid": {"_": 1},
165 "pale": {
166 "golden": {"rod": {"_": 1}},
167 "green": {"_": 1},
168 "turquoise": {"_": 1},
169 "violet": {"red": {"_": 1}},
170 },
171 "papaya": {"whip": {"_": 1}},
172 "peach": {"puff": {"_": 1}},
173 "peru": {"_": 1},
174 "pink": {"_": 1},
175 "plum": {"_": 1},
176 "powder": {"blue": {"_": 1}},
177 "purple": {"_": 1},
178 "red": {"_": 1},
179 "rosy": {"brown": {"_": 1}},
180 "royal": {"blue": {"_": 1}},
181 "saddle": {"brown": {"_": 1}},
182 "salmon": {"_": 1},
183 "sandy": {"brown": {"_": 1}},
184 "sea": {
185 "green": {"_": 1},
186 "shell": {"_": 1},
187 },
188 "seashell": {"_": 1},
189 "sienna": {"_": 1},
190 "silver": {"_": 1},
191 "sky": {"blue": {"_": 1}},
192 "slate": {
193 "blue": {"_": 1},
194 "gray": {"_": 1},
195 "grey": {"_": 1},
196 },
197 "snow": {"_": 1},
198 "spring": {"green": {"_": 1}},
199 "steel": {"blue": {"_": 1}},
200 "tan": {"_": 1},
201 "teal": {"_": 1},
202 "thistle": {"_": 1},
203 "tomato": {"_": 1},
204 "turquoise": {"_": 1},
205 "violet": {"_": 1},
206 "wheat": {"_": 1},
207 "white": {
208 "_": 1,
209 "smoke": {"_": 1},
210 },
211 "yellow": {
212 "_": 1,
213 "green": {"_": 1},
214 },
215}
218class TokenType(enum.Enum):
219 Word = enum.auto()
220 Number = enum.auto()
221 Eof = enum.auto()
222 String = enum.auto()
223 Punctuation = enum.auto()
224 Color = enum.auto()
227class Token:
228 def __init__(
229 self,
230 type: TokenType,
231 line: int,
232 col: int,
233 start: int,
234 end: int,
235 value: typing.Any = None
236 ):
237 self.col = col
238 self.line = line
239 self.end = end
240 self.start = start
241 self.type = type
242 self.value = value
244 def __repr__(self):
245 if self.type == TokenType.Eof:
246 return "End of file"
247 return repr(self.value)
250class AnimatableType:
251 Position = enum.auto()
252 Color = enum.auto()
253 Angle = enum.auto()
254 Size = enum.auto()
255 Number = enum.auto()
256 Integer = enum.auto()
259class AnimatableProperty:
260 def __init__(self, name, phrase, props, type: AnimatableType):
261 self.name = name.split()
262 self.phrase = phrase
263 self.props = props
264 self.type = type
266 def set_initial(self, value):
267 for prop in self.props:
268 prop.value = value
270 def get_initial(self):
271 return self.props[0].get_value(0)
273 def add_keyframe(self, time, value):
274 for prop in self.props:
275 if not prop.animated and time > 0:
276 prop.add_keyframe(0, prop.value)
277 prop.add_keyframe(time, value)
279 def loop(self, time):
280 for prop in self.props:
281 if prop.animated:
282 prop.add_keyframe(time, prop.get_value(0))
284 def get_value(self, time):
285 return self.props[0].get_value(time)
288class ShapeData:
289 def __init__(self, extent):
290 self.color = [0, 0, 0]
291 self.color_explicit = False
292 self.extent = extent
293 self.size_multiplitier = 1
294 self.portrait = False
295 self.roundness = 0
296 self.opacity = 1
297 self.count = 1
298 self.stroke = None
299 self.stroke_on_top = True
300 self.properties = {}
302 def scale(self, multiplier):
303 self.size_multiplitier *= multiplier
304 self.extent *= multiplier
306 def define_property(self, name, type, props, phrase=None):
307 self.properties[name] = AnimatableProperty(name, phrase, props, type)
309 def add_property(self, name, prop):
310 self.properties[name].props.append(prop)
313class Lexer:
314 expression = re.compile(
315 r'[\r\t ]*(?P<token>(?P<punc>[:,;.])|(?P<word>[a-zA-Z\']+)|(?P<color>#(?:[a-fA-F0-9]{3}){1,2})|' +
316 r'(?P<number>-?[0-9]+(?P<fraction>\.[0-9]+)?)|(?P<string>"(?:[^"\\]|\\["\\nbt])+"))[\r\t ]*'
317 )
319 def __init__(self, text):
320 self.text = text
321 self.pos = 0
322 self.token = None
323 self.line = 1
324 self.line_pos = 0
325 self.tokens = []
326 self.token_index = 0
328 def _new_token(self, token: Token):
329 self.token = token
330 self.tokens.append(token)
331 self.token_index = len(self.tokens)
332 return token
334 def next(self):
335 if self.token_index < len(self.tokens):
336 self.token = self.tokens[self.token_index]
337 self.token_index += 1
338 return self.token
340 while True:
341 if self.pos >= len(self.text):
342 return self._new_token(Token(TokenType.Eof, self.line, self.pos - self.line_pos, self.pos, self.pos))
344 if self.text[self.pos] == '\n':
345 self.line += 1
346 self.pos += 1
347 self.line_pos = self.pos
348 else:
349 match = self.expression.match(self.text, self.pos)
350 if match:
351 break
353 self.pos += 1
355 if match.group("word"):
356 type = TokenType.Word
357 value = match.group("word").lower()
358 elif match.group("number"):
359 type = TokenType.Number
360 if match.group("fraction"):
361 value = float(match.group("number"))
362 else:
363 value = int(match.group("number"))
364 elif match.group("string"):
365 type = TokenType.String
366 value = match.group("string")[1:-1]
367 elif match.group("punc"):
368 value = match.group("punc")
369 type = TokenType.Punctuation
370 elif match.group("color"):
371 value = match.group("color")
372 type = TokenType.Color
374 self.pos = match.end("token")
375 return self._new_token(Token(type, self.line, self.pos - self.line_pos, match.start("token"), self.pos, value))
377 def back(self):
378 if self.token_index > 0:
379 self.restore(self.token_index - 1)
381 def save(self):
382 return self.token_index
384 def restore(self, index):
385 self.token_index = index
386 if index > 0:
387 self.token = self.tokens[self.token_index-1]
388 else:
389 self.token = self.tokens[0]
392class Logger:
393 def warn(self, message):
394 sys.stderr.write(message)
395 sys.stderr.write("\n")
398class DummyLogger(Logger):
399 def warn(self, message):
400 pass
403class StorageLogger(Logger):
404 def __init__(self):
405 self.messages = []
407 def warn(self, message):
408 self.messages.append(message)
411class Parser:
412 sides = {
413 "penta": 5,
414 "hexa": 6,
415 "hepta": 7,
416 "octa": 8,
417 "ennea": 9,
418 "deca": 10,
419 }
421 def __init__(self, text, logger: Logger):
422 self.lexer = Lexer(text)
423 self.lexer.next()
424 self.logger = logger
425 self.allow_resize = True
426 self.max_duration = None
427 self.svg_shapes = []
428 self.font = FontStyle("Ubuntu", 80)
430 def next(self):
431 return self.lexer.next()
433 @property
434 def token(self):
435 return self.lexer.token
437 def color(self):
438 return self.get_color(color_words, "")
440 def get_color_value(self, value, complete_word: str):
441 if isinstance(value, str):
442 return NVector(*color_table[value])
443 elif isinstance(value, (list, tuple)):
444 return NVector(*value)
445 elif isinstance(value, NVector):
446 return value
447 else:
448 return NVector(*color_table[complete_word])
450 def complete_color(self, word_dict: dict, complete_word: str):
451 if "_" in word_dict:
452 return self.get_color_value(complete_word)
453 else:
454 next_item = next(iter(word_dict.item()))
455 return self.complete_color(next_item[1], complete_word + next_item[0])
457 def get_color(self, word_dict: dict, complete_word: str):
458 if self.token.type == TokenType.Color:
459 color = parse_color(self.token.value)
460 self.next()
461 return color
463 if self.token.type != TokenType.Word:
464 return None
466 value = word_dict.get(self.token.value, None)
467 if not value:
468 return None
470 if isinstance(value, dict):
471 next_word = complete_word + self.token.value
472 self.next()
473 color = self.get_color(value, next_word)
474 if color is not None:
475 return color
477 if "_" in value:
478 return self.get_color_value(value["_"], next_word)
480 self.warn("Incomplete color name")
481 else:
482 return self.get_color_value(value, complete_word)
484 def warn(self, message):
485 token = self.token
486 self.logger.warn("At line %s column %s, near %r: %s" % (token.line, token.col, token, message))
488 def parse(self):
489 self.lottie = Animation(180, 60)
490 if self.article():
491 if self.check_words("animation", "composition"):
492 self.next()
493 self.animation()
494 else:
495 self.lexer.back()
496 self.layers(self.lottie)
497 if self.token.type != TokenType.Eof:
498 self.warn("Extra tokens")
499 return self.lottie
501 def article(self):
502 if self.check_words("a", "an", "the"):
503 self.next()
504 return True
505 return False
507 def check_words(self, *words):
508 if self.token.type != TokenType.Word:
509 return False
511 return self.token.value in words
513 def skip_words(self, *words):
514 if self.check_words(*words):
515 self.next()
516 return True
517 return False
519 def require_one_of(self, *words):
520 if self.check_words(*words):
521 return True
523 self.warn("Expected " + repr(words[0]))
524 return False
526 def check_word_sequence(self, words):
527 token = self.token
528 index = self.lexer.save()
529 for word in words:
530 if token.type != TokenType.Word or token.value != word:
531 break
532 token = self.next()
533 else:
534 return True
536 self.lexer.restore(index)
537 return False
539 def possesive(self):
540 return self.skip_words("its", "his", "her", "their")
542 def properties(self, shape_data, callback, callback_args=[], words=["with"]):
543 lexind_and = -1
544 while True:
545 must_have_property = self.skip_words(*words)
547 self.article() or self.possesive()
549 lexind = self.lexer.save()
551 if not callback(*callback_args):
552 self.lexer.restore(lexind)
554 if shape_data and not self.shape_common_property(shape_data):
555 if must_have_property:
556 self.warn("Unknown property")
557 break
558 if lexind_and != -1:
559 self.lexer.restore(lexind_and)
560 break
562 lexind_and = self.lexer.save()
563 if not self.skip_and():
564 break
566 def simple_properties_callback(self, object, properties):
567 if self.check_words(*properties.keys()):
568 prop = self.token.value
569 self.next()
570 if self.check_words("of"):
571 self.next()
573 value = properties[prop](getattr(object, prop))
574 setattr(object, prop, value)
575 return True
576 return False
578 def animation(self):
579 while True:
580 if self.check_words("lasts", "lasting"):
581 self.next()
582 if self.check_words("for"):
583 self.next()
584 self.lottie.out_point = self.time(self.lottie.out_point)
585 elif self.check_words("stops", "stopping", "loops", "looping"):
586 if self.check_words("for", "after"):
587 self.next()
588 self.lottie.out_point = self.time(self.lottie.out_point)
589 if self.max_duration and self.lottie.out_point > self.max_duration:
590 self.lottie.out_point = self.max_duration
591 elif self.check_words("with", "has"):
592 props = {
593 "width": self.integer,
594 "height": self.integer,
595 "name": self.string
596 }
597 if not self.allow_resize:
598 props.pop("width")
599 props.pop("height")
600 self.properties(None, self.simple_properties_callback, [self.lottie, props], ["with", "has"])
601 elif self.skip_and():
602 pass
603 else:
604 return
606 def time(self, default):
607 if self.token.type != TokenType.Number:
608 self.warn("Expected time")
609 return default
611 amount = self.token.value
613 self.next()
614 if self.check_words("seconds", "second"):
615 amount *= self.lottie.frame_rate
616 elif self.check_words("milliseconds", "millisecond"):
617 amount *= self.lottie.frame_rate / 1000
618 elif not self.check_words("frames", "frame"):
619 self.warn("Missing time unit")
620 return amount
622 self.next()
623 return amount
625 def integer(self, default, warn=True):
626 if self.token.type != TokenType.Number or not isinstance(self.token.value, int):
627 if warn:
628 self.warn("Expected integer")
629 return default
631 val = self.token.value
632 self.next()
633 return val
635 def number(self, default):
636 if self.token.type != TokenType.Number:
637 self.warn("Expected number")
638 return default
640 val = self.token.value
641 self.next()
643 return val
645 def string(self, default):
646 if self.token.type != TokenType.String:
647 self.warn("Expected string")
648 return default
650 val = self.token.value
651 self.next()
652 return val
654 def layers(self, composition):
655 while True:
656 if self.token.type == TokenType.Punctuation and self.token.value in ";.":
657 self.next()
658 self.skip_and()
659 self.skip_words("then") or self.skip_words("finally")
661 if self.check_words("there's"):
662 self.next()
663 self.layer(composition)
664 elif self.check_words("there"):
665 self.next()
666 if self.check_words("is", "are"):
667 self.next()
668 self.layer(composition)
669 elif self.article():
670 self.lexer.back()
671 self.layer(composition)
672 elif self.token.type == TokenType.Number and isinstance(self.token.value, int):
673 self.layer(composition)
674 else:
675 break
677 def count(self, default=1):
678 if self.article():
679 return 1
680 return self.integer(default)
682 def layer(self, composition):
683 if self.token.type != TokenType.Word:
684 self.warn("Expected shape")
685 return
687 layer = layers.ShapeLayer()
688 layer.in_point = self.lottie.in_point
689 layer.out_point = self.lottie.out_point
690 composition.insert_layer(0, layer)
692 self.shape_list(layer)
694 if self.token.type == TokenType.Punctuation and self.token.value in ",;.":
695 self.next()
697 def skip_and(self):
698 if self.token.type == TokenType.Punctuation and self.token.value == ",":
699 self.next()
700 self.skip_words("and")
701 return True
702 return self.skip_words("and")
704 def shape_list(self, parent):
705 extent = min(self.lottie.width, self.lottie.height) * 0.4
706 shape = ShapeData(extent)
707 shape.count = self.count()
709 while True:
710 ok = False
712 color = self.color()
713 if color:
714 ok = True
715 shape.color = color
716 shape.color_explicit = True
718 if self.check_words("transparent", "invisible"):
719 self.next()
720 shape.color = None
721 ok = True
723 size_mult = self.size_multiplitier()
724 if size_mult:
725 ok = True
726 shape.scale(size_mult)
728 if self.check_words("portrait"):
729 self.next()
730 shape.portrait = True
731 ok = True
733 if self.check_words("landscape"):
734 self.next()
735 shape.portrait = False
736 ok = True
738 lexind = self.lexer.save()
739 qualifier = self.size_qualifier()
740 if self.check_words("rounded"):
741 shape.roundness = qualifier
742 self.next()
743 ok = True
744 elif self.check_words("transparent"):
745 shape.opacity = (1 / qualifier)
746 self.next()
747 ok = True
748 elif lexind < self.lexer.token_index:
749 self.lexer.restore(lexind)
751 if self.check_words("star", "polygon", "ellipse", "rectangle", "circle", "square", "text"):
752 shape_type = self.token.value
753 function = getattr(self, "shape_" + shape_type)
754 self.next()
755 shape_object = function(shape)
756 self.add_shape(parent, shape_object, shape)
757 return
759 if self.token.type == TokenType.Word:
760 for name, sides in self.sides.items():
761 if self.token.value.startswith(name):
762 if self.token.value.endswith("gon"):
763 self.next()
764 shape_object = self.shape_polygon(shape, sides)
765 self.add_shape(parent, shape_object, shape)
766 return
767 elif self.token.value.endswith("gram"):
768 self.next()
769 shape_object = self.shape_star(shape, sides)
770 self.add_shape(parent, shape_object, shape)
771 return
773 if self.check_words("triangle"):
774 self.next()
775 shape_object = self.shape_polygon(shape, 3)
776 self.add_shape(parent, shape_object, shape)
777 return
779 sides = self.integer(None, False)
780 if sides is not None:
781 if self.check_words("sided", "pointed"):
782 self.next()
783 if self.check_words("polygon", "star"):
784 shape_type = self.token.value
785 function = getattr(self, "shape_" + shape_type)
786 self.next()
787 shape_object = function(shape, sides)
788 self.add_shape(parent, shape_object, shape)
789 return
790 else:
791 self.warn("Expected 'star' or 'polygon'")
792 return
793 else:
794 continue
796 for svg_shape in self.svg_shapes:
797 if svg_shape.match(parent, self, shape):
798 return
800 if not ok:
801 self.next()
802 break
804 self.warn("Expected shape")
806 def size_qualifier(self):
807 base = 1
809 while True:
810 if self.check_words("very", "much"):
811 self.next()
812 base *= 1.33
813 elif self.check_words("extremely"):
814 self.next()
815 base *= 1.5
816 elif self.check_words("incredibly"):
817 self.next()
818 base *= 2
819 else:
820 break
822 return base
824 def size_multiplitier(self):
825 lexind = self.lexer.save()
826 base = self.size_qualifier()
828 if self.check_words("small"):
829 self.next()
830 return 0.8 / base
831 elif self.check_words("large", "big"):
832 self.next()
833 return 1.2 * base
834 elif self.check_words("tiny"):
835 self.next()
836 return 0.5 / base
837 elif self.check_words("huge"):
838 self.next()
839 return 1.6 * base
840 else:
841 self.lexer.restore(lexind)
842 return None
844 def add_shape(self, parent, shape_object, shape_data):
845 g = shapes.Group()
846 g.add_shape(shape_object)
848 if shape_data.stroke and shape_data.stroke_on_top:
849 g.add_shape(shape_data.stroke)
851 if shape_data.color:
852 fill = shapes.Fill(shape_data.color)
853 g.add_shape(fill)
854 shape_data.define_property("color", AnimatableType.Color, [fill.color])
856 if shape_data.stroke and not shape_data.stroke_on_top:
857 g.add_shape(shape_data.stroke)
859 if shape_data.opacity != 1:
860 g.transform.opacity.value = 100 * shape_data.opacity
862 if "position" in shape_data.properties:
863 center = shape_data.properties["position"].get_initial()
864 else:
865 center = g.bounding_box(0).center()
866 g.transform.position.value = self.position(g, 0) + center
867 g.transform.anchor_point.value = NVector(*center)
868 shape_data.define_property("position", AnimatableType.Position, [g.transform.position], ["moves"])
869 shape_data.define_property("rotation", AnimatableType.Angle, [g.transform.rotation], ["rotates"])
871 if self.check_words("rotated"):
872 self.next()
873 g.transform.rotation.value = self.angle(0)
875 parent.insert_shape(0, g)
877 if self.skip_words("that"):
878 self.animated_properties(shape_data)
880 return g
882 def position(self, shape: shapes.Group, time: float):
883 px = 0
884 py = 0
886 qual = self.size_qualifier()
888 if self.check_words("to", "in", "towards", "on", "at"):
889 self.next()
890 if self.check_words("the"):
891 self.next()
892 if self.token.type != TokenType.Word:
893 self.warn("Expected position")
894 return
896 while True:
897 if self.check_words("left"):
898 px = -1
899 elif self.check_words("right"):
900 px = 1
901 elif self.check_words("top"):
902 py = -1
903 elif self.check_words("bottom"):
904 py = 1
905 elif self.check_words("center", "middle"):
906 pass
907 else:
908 break
910 self.next()
912 if self.check_words("side", "corner"):
913 self.next()
915 if px == 0 and py == 0:
916 return shape.transform.position.get_value(time)
918 box = shape.bounding_box(time)
919 center = box.center()
920 left = box.width / 2
921 right = self.lottie.width - box.width / 2
922 top = box.height / 2
923 bottom = self.lottie.height - box.height / 2
925 pos = shape.transform.position.get_value(time)
926 x = pos.x
927 y = pos.y
928 dx = dy = 0
930 if px < 0:
931 dx = left - center.x
932 elif px > 0:
933 dx = right - center.y
935 if py < 0:
936 dy = top - center.y
937 elif py > 0:
938 dy = bottom - center.y
940 return NVector(
941 x + dx * qual,
942 y + dy * qual,
943 )
945 def animation_time(self, time, required):
946 if self.skip_words("at"):
947 if self.skip_words("the"):
948 self.require_one_of("end")
949 self.next()
950 return self.lottie.out_point
951 return self.time(time)
952 if self.skip_words("after"):
953 return self.time(0) + time
954 if required:
955 return time
956 return None
958 def animated_properties(self, shape_data: ShapeData):
959 time = 0
961 while True:
962 prop_time = self.animation_time(time, False)
963 changing = self.skip_words("changing", "changes")
964 possesive = self.possesive()
965 found_property = None
966 value = None
967 loop = False
969 if possesive or changing:
970 for property in shape_data.properties.values():
971 if self.check_word_sequence(property.name):
972 found_property = property
973 if possesive and not changing:
974 if self.skip_words("loops"):
975 self.skip_words("back")
976 loop = True
977 break
978 else:
979 self.skip_words("changes")
980 value = self.animated_property(shape_data, property, time)
981 break
982 else:
983 for property in shape_data.properties.values():
984 if property.phrase and self.check_word_sequence(property.phrase):
985 found_property = property
986 value = self.animated_property(shape_data, property, time)
987 break
989 if not found_property:
990 self.warn("Unknown property")
991 break
993 if prop_time is None:
994 self.prop_time = self.animation_time(time, True)
995 time = prop_time
997 if loop:
998 found_property.loop(time)
999 else:
1000 if value is None:
1001 break
1003 found_property.add_keyframe(time, value)
1005 cont = self.skip_and()
1006 cont = self.skip_words("then") or cont
1007 if not cont:
1008 break
1010 def animated_property_value(self, property: AnimatableProperty, time):
1011 if property.type == AnimatableType.Angle:
1012 return self.angle(None)
1013 elif property.type == AnimatableType.Number:
1014 return self.number(None)
1015 elif property.type == AnimatableType.Integer:
1016 return self.integer(None)
1017 elif property.type == AnimatableType.Color:
1018 value = self.color()
1019 if not value:
1020 self.warn("Expected color")
1021 return value
1022 elif property.type == AnimatableType.Position:
1023 return self.position_value(property.get_value(time))
1024 elif property.type == AnimatableType.Size:
1025 return self.vector_value()
1026 return None
1028 def animated_property(self, shape_data: ShapeData, property: AnimatableProperty, time):
1029 relative = False
1030 if not self.skip_words("to"):
1031 relative = self.skip_words("by")
1033 value = self.animated_property_value(property, time)
1035 if value is not None and relative:
1036 value += property.get_value(time)
1038 return value
1040 def vector_value(self):
1041 x = self.number(0)
1042 if self.token.type == TokenType.Punctuation and self.token.value == ",":
1043 self.next()
1044 y = self.number(0)
1046 return NVector(x, y)
1048 def position_value(self, start: NVector):
1050 if self.token.type == TokenType.Word:
1051 direction = None
1053 if self.check_words("left") or self.check_word_sequence("the", "left"):
1054 direction = NVector(-1, 0)
1055 elif self.check_words("right") or self.check_word_sequence("the", "right"):
1056 direction = NVector(1, 0)
1057 elif self.check_words("up", "upwards", "upward"):
1058 direction = NVector(0, -1)
1059 elif self.check_words("down", "downwards", "downward"):
1060 direction = NVector(0, 1)
1062 if direction is None:
1063 self.warn("Expected direction or position")
1064 return start
1066 self.skip_words("by")
1068 return start + direction * self.number()
1070 return self.vector_value()
1072 def shape_common_property(self, shape_data: ShapeData):
1073 color = NVector(0, 0, 0)
1074 width = 4
1076 while True:
1077 got_color = self.color()
1078 if got_color:
1079 color = got_color
1080 continue
1082 quant = self.size_qualifier()
1083 if self.check_words("thick"):
1084 self.next()
1085 width *= 1.5 * quant
1086 continue
1087 elif self.check_words("thin"):
1088 self.next()
1089 width *= 0.6 / quant
1090 continue
1091 else:
1092 break
1094 if self.check_words("stroke", "border", "outline", "edge", "borders", "edges"):
1095 self.next()
1096 shape_data.stroke = shapes.Stroke(color, width)
1097 return True
1099 return False
1101 def animated_properties_callback(self, shape_data: ShapeData):
1102 for property in shape_data.properties.values():
1103 if self.check_word_sequence(property.name):
1104 self.skip_words("to", "is", "are")
1105 property.set_initial(self.animated_property_value(property, 0))
1106 return True
1107 return False
1109 def shape_square(self, shape_data: ShapeData):
1110 pos = NVector(self.lottie.width / 2, self.lottie.height / 2)
1111 size = NVector(shape_data.extent, shape_data.extent)
1112 round_base = shape_data.extent / 5
1113 shape = shapes.Rect(pos, size, shape_data.roundness * round_base)
1114 shape_data.define_property("position", AnimatableType.Position, [shape.position])
1115 shape_data.define_property("size", AnimatableType.Size, [shape.position])
1116 self.properties(shape_data, self.animated_properties_callback, [shape_data], ["with", "of"])
1117 return shape
1119 def shape_circle(self, shape_data: ShapeData):
1120 pos = NVector(self.lottie.width / 2, self.lottie.height / 2)
1121 size = NVector(shape_data.extent, shape_data.extent)
1122 shape = shapes.Ellipse(pos, size)
1123 shape_data.define_property("position", AnimatableType.Position, [shape.position])
1124 shape_data.define_property("size", AnimatableType.Size, [shape.position])
1125 self.properties(shape_data, self.animated_properties_callback, [shape_data], ["with", "of"])
1126 return shape
1128 def shape_star(self, shape_data: ShapeData, sides: int = None):
1129 pos = NVector(self.lottie.width / 2, self.lottie.height / 2)
1130 round_base = shape_data.extent / 5
1131 roundness = shape_data.roundness * round_base
1132 shape = shapes.Star()
1133 shape.position.value = pos
1134 shape.inner_roundness.value = roundness
1135 shape.outer_radius.value = shape_data.extent / 2
1136 shape.outer_roundness.value = roundness
1137 shape.star_type = shapes.StarType.Star
1138 shape.points.value = sides or 5
1140 def callback():
1141 if self.animated_properties_callback(shape_data):
1142 return True
1144 if self.check_words("diameter"):
1145 shape.outer_radius.value = self.number(shape.outer_radius.value) / 2
1146 return True
1147 elif self.token.type == TokenType.Number:
1148 if sides:
1149 self.warn("Number of sides already specified")
1150 shape.points.value = self.integer(shape.points.value)
1151 if self.require_one_of("points", "sides", "point", "side"):
1152 self.next()
1153 return True
1154 return False
1156 shape.inner_radius.value = shape.outer_radius.value / 2
1158 shape_data.define_property("position", AnimatableType.Position, [shape.position])
1159 shape_data.define_property("outer radius", AnimatableType.Number, [shape.outer_radius])
1160 shape_data.define_property("radius", AnimatableType.Number, [shape.outer_radius])
1161 shape_data.define_property("inner radius", AnimatableType.Number, [shape.inner_radius])
1162 self.properties(shape_data, callback, [], ["with", "of", "has"])
1164 return shape
1166 def shape_polygon(self, shape_data: ShapeData, sides: int = None):
1167 shape = self.shape_star(shape_data, sides)
1168 shape.inner_radius = None
1169 shape.inner_roundness = None
1170 shape.star_type = shapes.StarType.Polygon
1171 return shape
1173 def angle_direction(self):
1174 if self.check_words("clockwise"):
1175 self.next()
1176 return 1
1177 elif self.check_words("counter"):
1178 self.next()
1179 if self.check_words("clockwise"):
1180 self.next()
1181 return -1
1182 return 0
1184 def fraction(self):
1185 if self.article():
1186 amount = 1
1187 else:
1188 amount = self.number(1)
1190 if self.check_words("full", "entire"):
1191 self.next()
1192 return amount, True
1193 elif self.check_words("half", "halfs"):
1194 self.next()
1195 return amount / 2, True
1196 elif self.check_words("third", "thirds"):
1197 self.next()
1198 return amount / 3, True
1199 elif self.check_words("quarter", "quarters"):
1200 self.next()
1201 return amount / 3, True
1203 return amount, False
1205 def angle(self, default):
1206 direction = self.angle_direction()
1208 amount, has_fraction = self.fraction()
1210 if self.skip_and():
1211 more_frac = self.fraction()[0]
1212 if has_fraction:
1213 amount += amount * more_frac
1214 else:
1215 amount += more_frac
1217 if self.check_words("turns"):
1218 self.next()
1219 amount *= 360
1220 elif self.require_one_of("degrees"):
1221 self.next()
1222 elif self.check_word_sequence(["pi", "radians"]):
1223 amount *= 180
1224 if direction == 0:
1225 direction = -1
1227 if direction == 0:
1228 direction = 1
1229 return amount * direction
1231 def rect_properties(self, shape_data: ShapeData, shape):
1232 extent = shape_data.extent
1233 parse_data = {
1234 "width": None,
1235 "height": None,
1236 "ratio": math.sqrt(2),
1237 "size_specified": False
1238 }
1239 handle_orientation = True
1241 shape_data.define_property("position", AnimatableType.Position, [shape.position])
1242 shape_data.define_property("size", AnimatableType.Size, [shape.size])
1244 def callback():
1245 if self.animated_properties_callback(shape_data):
1246 return True
1248 if self.check_words("ratio"):
1249 self.next()
1250 ratio = self.fraction()[0]
1251 if ratio <= 0:
1252 self.warn("Ratio must be positive")
1253 else:
1254 parse_data["ratio"] = ratio
1255 return True
1256 elif self.check_words("width"):
1257 self.next()
1258 parse_data["width"] = self.number(0)
1259 return True
1260 elif self.check_words("height"):
1261 self.next()
1262 parse_data["height"] = self.number(0)
1263 return True
1264 elif self.check_words("size"):
1265 parse_data["size_specified"] = True
1267 return False
1269 self.properties(shape_data, callback, [], ["with", "of", "has"])
1271 if not parse_data["size_specified"]:
1272 width = parse_data["width"]
1273 height = parse_data["height"]
1274 ratio = parse_data["ratio"]
1276 if width is None and height is None:
1277 width = extent
1278 height = width / ratio
1279 elif width is None:
1280 width = height * ratio
1281 elif height is None:
1282 height = width / ratio
1283 else:
1284 handle_orientation = False
1286 if handle_orientation:
1287 if (width > height and shape_data.portrait) or (width < height and not shape_data.portrait):
1288 width, height = height, width
1290 shape.size.value = NVector(width, height)
1292 def shape_rectangle(self, shape_data: ShapeData):
1293 pos = NVector(self.lottie.width / 2, self.lottie.height / 2)
1294 round_base = shape_data.extent / 5
1296 shape = shapes.Rect(pos, NVector(0, 0), shape_data.roundness * round_base)
1297 shape_data.define_property("roundness", AnimatableType.Number, [shape.rounded])
1298 self.rect_properties(shape_data, shape)
1299 return shape
1301 def shape_ellipse(self, shape_data: ShapeData):
1302 pos = NVector(self.lottie.width / 2, self.lottie.height / 2)
1303 shape = shapes.Ellipse(pos, NVector(0, 0))
1304 self.rect_properties(shape_data, shape)
1305 return shape
1307 def shape_text(self, shape_data: ShapeData):
1308 text = self.string("")
1309 font = self.font.clone()
1310 shape = font.render(text)
1312 box = shape.bounding_box()
1314 center = box.center()
1315 anim_center = NVector(self.lottie.width / 2, self.lottie.height / 2)
1316 shape.transform.anchor_point.value = anim_center
1317 shape.transform.position.value.y = self.lottie.height - center.y
1318 shape.transform.position.value.x = self.lottie.width - center.x
1320 scale = shape_data.size_multiplitier
1321 if box.width > self.lottie.width > 0:
1322 scale *= self.lottie.width / box.width
1324 if scale != 1:
1325 wrapper = shapes.Group()
1326 wrapper.transform.position.value = wrapper.transform.anchor_point.value = anim_center
1327 wrapper.transform.scale.value *= scale
1328 wrapper.add_shape(shape)
1329 return wrapper
1331 return shape
1334class SvgLoader:
1335 def __init__(self):
1336 self.cache = {}
1338 def load(self, filename):
1339 if filename in self.cache:
1340 return self.cache[filename]
1342 anim = parse_svg_file(filename)
1343 self.cache[filename] = anim
1344 return anim
1347class SvgFeature:
1348 def __init__(self, layer_names, colors):
1349 self.layer_names = layer_names
1350 self.colors = [parse_color(color) for color in colors]
1352 def iter_stylers(self, lottie_object):
1353 objects = []
1354 if self.layer_names:
1355 for layer_name in self.layer_names:
1356 found = lottie_object.find(layer_name)
1357 if found:
1358 objects.append(found)
1359 else:
1360 objects = [lottie_object]
1362 for object in objects:
1363 for styler in object.find_all((shapes.Fill, shapes.Stroke)):
1364 if len(self.colors) == 0 or styler.color.value in self.colors:
1365 yield styler
1367 def process(self, lottie_object, new_color):
1368 for styler in self.iter_stylers(lottie_object):
1369 styler.color.value = new_color
1372class SvgShape:
1373 def __init__(self, file, phrase, feature_map, main_feature, facing_direction, svg_loader: SvgLoader):
1374 self.file = file
1375 self.phrase = phrase
1376 self.feature_map = feature_map
1377 if isinstance(main_feature, str):
1378 self.main_feature = feature_map[main_feature]
1379 else:
1380 self.main_feature = main_feature
1381 self.facing_direction = facing_direction
1382 self.svg_loader = svg_loader
1384 def callback(self, parser: Parser, shape_data: ShapeData):
1385 lexind = parser.lexer.save()
1386 color = parser.color()
1387 if color:
1388 if parser.check_words(*self.feature_map.keys()):
1389 feature_name = parser.lexer.token.value
1390 shape_data.properties[feature_name].set_initial(color)
1391 parser.next()
1392 return True
1393 else:
1394 parser.lexer.restore(lexind)
1396 return False
1398 def match(self, parent, parser: Parser, shape_data: ShapeData):
1399 if not parser.check_word_sequence(self.phrase):
1400 return False
1402 shape_data.stroke_on_top = False
1403 svg_anim = parse_svg_file(self.file, 0, parser.lottie.out_point, parser.lottie.frame_rate)
1404 layer = svg_anim.layers[0].clone()
1405 group = shapes.Group()
1406 group.name = layer.name
1407 group.shapes = layer.shapes + group.shapes
1408 layer.transform.clone_into(group.transform)
1410 wrapper = shapes.Group()
1411 wrapper.add_shape(group)
1413 delta_ap = group.bounding_box(0).center()
1414 wrapper.transform.anchor_point.value += delta_ap
1415 wrapper.transform.position.value += delta_ap
1416 wrapper.transform.scale.value *= shape_data.size_multiplitier
1418 for name, feature in self.feature_map.items():
1419 singular = name[:-1] if name.endswith("s") else name
1420 shape_data.properties[name] = AnimatableProperty(singular + " color", None, [], AnimatableType.Color)
1421 for styler in feature.iter_stylers(group):
1422 shape_data.add_property(name, styler.color)
1424 if self.main_feature:
1425 shape_data.define_property("color", AnimatableType.Color, [])
1427 for styler in self.main_feature.iter_stylers(group):
1428 shape_data.add_property("color", styler.color)
1430 if shape_data.color and shape_data.color_explicit:
1431 shape_data.properties["color"].set_initial(shape_data.color)
1433 parser.properties(shape_data, self.callback, [parser, shape_data], ["with"])
1435 if self.facing_direction != 0 and parser.check_words("facing", "looking"):
1436 parser.next()
1437 if parser.skip_words("to"):
1438 parser.skip_words("the")
1440 if parser.check_words("left", "right"):
1441 direction = -1 if parser.token.value == "left" else 1
1442 if direction != self.facing_direction:
1443 wrapper.transform.scale.value.x *= -1
1444 else:
1445 parser.warn("Missing facing direction")
1447 parser.next()
1449 shape_data.color = None
1450 parser.add_shape(parent, wrapper, shape_data)
1451 return True