File indexing completed on 2025-01-19 03:59:53
0001 import re 0002 import math 0003 from xml.etree import ElementTree 0004 0005 from .handler import SvgHandler, NameMode 0006 from ... import objects 0007 from ...nvector import NVector 0008 from ...utils import restructure 0009 from ...utils.transform import TransformMatrix 0010 try: 0011 from ...utils import font 0012 has_font = True 0013 except ImportError: 0014 has_font = False 0015 0016 0017 class PrecompTime: 0018 def __init__(self, pcl: objects.PreCompLayer): 0019 self.pcl = pcl 0020 0021 def get_time_offset(self, time, lot): 0022 remap = time 0023 if self.pcl.time_remapping: 0024 remapf = self.pcl.time_remapping.get_value(time) 0025 remap = lot.in_point * (1-remapf) + lot.out_point * remapf 0026 0027 return remap - self.pcl.start_time 0028 0029 0030 class SvgBuilder(SvgHandler, restructure.AbstractBuilder): 0031 merge_paths = True 0032 namestart = ( 0033 r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" + 0034 r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + 0035 r"\uFDF0-\uFFFD\U00010000-\U000EFFFF" 0036 ) 0037 namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040" 0038 id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart)) 0039 0040 def __init__(self, time=0): 0041 super().__init__() 0042 self.svg = ElementTree.Element("svg") 0043 self.dom = ElementTree.ElementTree(self.svg) 0044 self.svg.attrib["xmlns"] = self.ns_map["svg"] 0045 self.ids = set() 0046 self.idc = 0 0047 self.name_mode = NameMode.Inkscape 0048 self.actual_time = time 0049 self.precomp_times = [] 0050 self._precomps = {} 0051 self._assets = {} 0052 self._current_layer = [] 0053 0054 @property 0055 def time(self): 0056 time = self.actual_time 0057 if self.precomp_times: 0058 for pct in self.precomp_times: 0059 time = pct.get_time_offset(time, self._current_layer[-1]) 0060 return time 0061 0062 def gen_id(self, prefix="id"): 0063 while True: 0064 self.idc += 1 0065 id = "%s_%s" % (prefix, self.idc) 0066 if id not in self.ids: 0067 break 0068 self.ids.add(id) 0069 return id 0070 0071 def set_clean_id(self, dom, n): 0072 idn = n.replace(" ", "_") 0073 if self.id_re.match(idn) and idn not in self.ids: 0074 self.ids.add(idn) 0075 else: 0076 idn = self.gen_id(dom.tag) 0077 0078 dom.attrib["id"] = idn 0079 return idn 0080 0081 def set_id(self, dom, lottieobj, inkscape_qual=None, force=False): 0082 n = getattr(lottieobj, "name", None) 0083 if n is None or self.name_mode == NameMode.NoName: 0084 if force: 0085 id = self.gen_id(dom.tag) 0086 dom.attrib["id"] = id 0087 return id 0088 return None 0089 0090 idn = self.set_clean_id(dom, n) 0091 if inkscape_qual is None: 0092 inkscape_qual = self.qualified("inkscape", "label") 0093 if inkscape_qual: 0094 dom.attrib[inkscape_qual] = n 0095 return idn 0096 0097 def _on_animation(self, animation: objects.Animation): 0098 self.svg.attrib["width"] = str(animation.width) 0099 self.svg.attrib["height"] = str(animation.height) 0100 self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height) 0101 self.svg.attrib["version"] = "1.1" 0102 self.set_id(self.svg, animation, self.qualified("sodipodi", "docname")) 0103 self.defs = ElementTree.SubElement(self.svg, "defs") 0104 if self.name_mode == NameMode.Inkscape: 0105 self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96" 0106 self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96" 0107 namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview")) 0108 namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true" 0109 namedview.attrib["borderlayer"] = "true" 0110 namedview.attrib["bordercolor"] = "#666666" 0111 namedview.attrib["pagecolor"] = "#ffffff" 0112 self.svg.attrib["style"] = "fill: none; stroke: none" 0113 0114 self._current_layer = [animation] 0115 return self.svg 0116 0117 def _mask_to_def(self, mask): 0118 svgmask = ElementTree.SubElement(self.defs, "mask") 0119 mask_id = self.gen_id() 0120 svgmask.attrib["id"] = mask_id 0121 svgmask.attrib["mask-type"] = "alpha" 0122 path = ElementTree.SubElement(svgmask, "path") 0123 path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time)) 0124 path.attrib["fill"] = "#fff" 0125 path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100) 0126 return mask_id 0127 0128 def _matte_source_to_def(self, layer_builder): 0129 svgmask = ElementTree.SubElement(self.defs, "mask") 0130 if not layer_builder.matte_id: 0131 layer_builder.matte_id = self.gen_id() 0132 svgmask.attrib["id"] = layer_builder.matte_id 0133 matte_mode = layer_builder.matte_target.lottie.matte_mode 0134 0135 mask_type = "alpha" 0136 if matte_mode == objects.MatteMode.Luma: 0137 mask_type = "luminance" 0138 svgmask.attrib["mask-type"] = mask_type 0139 return svgmask 0140 0141 def _on_masks(self, masks): 0142 if len(masks) == 1: 0143 return self._mask_to_def(masks[0]) 0144 mask_ids = list(map(self._mask_to_def, masks)) 0145 mask_def = ElementTree.SubElement(self.defs, "mask") 0146 mask_id = self.gen_id() 0147 mask_def.attrib["id"] = mask_id 0148 g = mask_def 0149 for mid in mask_ids: 0150 g = ElementTree.SubElement(g, "g") 0151 g.attrib["mask"] = "url(#%s)" % mid 0152 full = ElementTree.SubElement(g, "rect") 0153 full.attrib["fill"] = "#fff" 0154 full.attrib["width"] = self.svg.attrib["width"] 0155 full.attrib["height"] = self.svg.attrib["height"] 0156 full.attrib["x"] = "0" 0157 full.attrib["y"] = "0" 0158 return mask_id 0159 0160 def _on_layer(self, layer_builder, dom_parent): 0161 lot = layer_builder.lottie 0162 self._current_layer.append(lot) 0163 0164 if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time): 0165 self._current_layer.pop() 0166 return None 0167 0168 if layer_builder.matte_target: 0169 dom_parent = self._matte_source_to_def(layer_builder) 0170 0171 g = self.group_from_lottie(lot, dom_parent, True) 0172 0173 if lot.masks: 0174 g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks) 0175 elif layer_builder.matte_source: 0176 matte_id = layer_builder.matte_source.matte_id 0177 if not matte_id: 0178 matte_id = layer_builder.matte_source.matte_id = self.gen_id() 0179 g.attrib["mask"] = "url(#%s)" % matte_id 0180 0181 if isinstance(lot, objects.PreCompLayer): 0182 self.precomp_times.append(PrecompTime(lot)) 0183 0184 for layer in self._precomps.get(lot.reference_id, []): 0185 self.process_layer(layer, g) 0186 0187 self.precomp_times.pop() 0188 elif isinstance(lot, objects.NullLayer): 0189 g.attrib["opacity"] = "1" 0190 elif isinstance(lot, objects.ImageLayer): 0191 use = ElementTree.SubElement(g, "use") 0192 use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id] 0193 elif isinstance(lot, objects.TextLayer): 0194 self._on_text_layer(g, lot) 0195 elif isinstance(lot, objects.SolidColorLayer): 0196 rect = ElementTree.SubElement(g, "rect") 0197 rect.attrib["width"] = str(lot.width) 0198 rect.attrib["height"] = str(lot.height) 0199 rect.attrib["fill"] = lot.color 0200 0201 if not lot.name: 0202 g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__ 0203 if layer_builder.shapegroup: 0204 g.attrib["style"] = self.group_to_style(layer_builder.shapegroup) 0205 self._split_stroke(layer_builder.shapegroup, g, dom_parent) 0206 #if lot.hidden: 0207 #g.attrib.setdefault("style", "") 0208 #g.attrib["style"] += "display: none;" 0209 0210 return g 0211 0212 def _on_text_layer(self, g, lot): 0213 text = ElementTree.SubElement(g, "text") 0214 doc = lot.data.get_value(self.time) 0215 if doc: 0216 text.attrib["font-family"] = doc.font_family 0217 text.attrib["font-size"] = str(doc.font_size) 0218 if doc.line_height: 0219 text.attrib["line-height"] = "%s%%" % doc.line_height 0220 if doc.justify == objects.text.TextJustify.Left: 0221 text.attrib["text-align"] = "start" 0222 elif doc.justify == objects.text.TextJustify.Center: 0223 text.attrib["text-align"] = "center" 0224 elif doc.justify == objects.text.TextJustify.Right: 0225 text.attrib["text-align"] = "end" 0226 0227 text.attrib["fill"] = color_to_css(doc.color) 0228 text.text = doc.text 0229 0230 def _on_layer_end(self, out_layer): 0231 self._current_layer.pop() 0232 0233 def _on_precomp(self, id, dom_parent, layers): 0234 self._precomps[id] = layers 0235 0236 def _on_asset(self, asset): 0237 if isinstance(asset, objects.assets.Image): 0238 img = ElementTree.SubElement(self.defs, "image") 0239 xmlid = self.set_clean_id(img, asset.id) 0240 self._assets[asset.id] = xmlid 0241 if asset.is_embedded: 0242 url = asset.image 0243 else: 0244 url = asset.image_path + asset.image 0245 img.attrib[self.qualified("xlink", "href")] = url 0246 img.attrib["width"] = str(asset.width) 0247 img.attrib["height"] = str(asset.height) 0248 0249 def _get_value(self, prop, default=NVector(0, 0)): 0250 if prop: 0251 v = prop.get_value(self.time) 0252 else: 0253 v = default 0254 0255 if v is None: 0256 return default 0257 if isinstance(v, NVector): 0258 return v.clone() 0259 return v 0260 0261 def set_transform(self, dom, transform, auto_orient=False): 0262 mat = transform.to_matrix(self.time, auto_orient) 0263 dom.attrib["transform"] = mat.to_css_2d() 0264 0265 if transform.opacity is not None: 0266 op = transform.opacity.get_value(self.time) 0267 if op != 100: 0268 dom.attrib["opacity"] = str(op/100) 0269 0270 def _get_group_stroke(self, group): 0271 style = {} 0272 if group.stroke: 0273 if isinstance(group.stroke, objects.GradientStroke): 0274 style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke) 0275 else: 0276 style["stroke"] = color_to_css(group.stroke.color.get_value(self.time)) 0277 0278 style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100 0279 style["stroke-width"] = group.stroke.width.get_value(self.time) 0280 if group.stroke.miter_limit is not None: 0281 style["stroke-miterlimit"] = group.stroke.miter_limit 0282 0283 if group.stroke.line_cap == objects.LineCap.Round: 0284 style["stroke-linecap"] = "round" 0285 elif group.stroke.line_cap == objects.LineCap.Butt: 0286 style["stroke-linecap"] = "butt" 0287 elif group.stroke.line_cap == objects.LineCap.Square: 0288 style["stroke-linecap"] = "square" 0289 0290 if group.stroke.line_join == objects.LineJoin.Round: 0291 style["stroke-linejoin"] = "round" 0292 elif group.stroke.line_join == objects.LineJoin.Bevel: 0293 style["stroke-linejoin"] = "bevel" 0294 elif group.stroke.line_join == objects.LineJoin.Miter: 0295 style["stroke-linejoin"] = "miter" 0296 0297 if group.stroke.dashes: 0298 dasharray = [] 0299 last = 0 0300 last_mode = objects.StrokeDashType.Dash 0301 for dash in group.stroke.dashes: 0302 if last_mode == dash.type: 0303 last += dash.length.get_value(self.time) 0304 else: 0305 if last_mode != objects.StrokeDashType.Offset: 0306 dasharray.append(str(last)) 0307 last = 0 0308 last_mode = dash.type 0309 style["stroke-dasharray"] = " ".join(dasharray) 0310 return style 0311 0312 def _style_to_css(self, style): 0313 return ";".join(map( 0314 lambda x: ":".join(map(str, x)), 0315 style.items() 0316 )) 0317 0318 def _split_stroke(self, group, fill_layer, out_parent): 0319 if not group.stroke:# or group.stroke_above: 0320 return 0321 0322 style = self._get_group_stroke(group) 0323 if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0: 0324 return 0325 0326 if group.stroke_above: 0327 if fill_layer.attrib.get("style", ""): 0328 fill_layer.attrib["style"] += ";" 0329 else: 0330 fill_layer.attrib["style"] = "" 0331 fill_layer.attrib["style"] += self._style_to_css(style) 0332 return fill_layer 0333 0334 g = ElementTree.Element("g") 0335 self.set_clean_id(g, "stroke") 0336 use = ElementTree.Element("use") 0337 for i, e in enumerate(out_parent): 0338 if e is fill_layer: 0339 out_parent.insert(i, g) 0340 out_parent.remove(fill_layer) 0341 break 0342 else: 0343 return 0344 0345 g.append(use) 0346 g.append(fill_layer) 0347 0348 use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"] 0349 use.attrib["style"] = self._style_to_css(style) 0350 return g 0351 0352 def group_to_style(self, group): 0353 style = {} 0354 if group.fill: 0355 style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100 0356 if isinstance(group.fill, objects.GradientFill): 0357 style["fill"] = "url(#%s)" % self.process_gradient(group.fill) 0358 else: 0359 style["fill"] = color_to_css(group.fill.color.get_value(self.time)) 0360 0361 if group.fill.fill_rule: 0362 style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero" 0363 0364 if group.lottie.hidden: 0365 style["display"] = "none" 0366 #if group.stroke_above: 0367 #style.update(self._get_group_stroke(group)) 0368 0369 return self._style_to_css(style) 0370 0371 def process_gradient(self, gradient): 0372 spos = gradient.start_point.get_value(self.time) 0373 epos = gradient.end_point.get_value(self.time) 0374 0375 if gradient.gradient_type == objects.GradientType.Linear: 0376 dom = ElementTree.SubElement(self.defs, "linearGradient") 0377 dom.attrib["x1"] = str(spos[0]) 0378 dom.attrib["y1"] = str(spos[1]) 0379 dom.attrib["x2"] = str(epos[0]) 0380 dom.attrib["y2"] = str(epos[1]) 0381 elif gradient.gradient_type == objects.GradientType.Radial: 0382 dom = ElementTree.SubElement(self.defs, "radialGradient") 0383 dom.attrib["cx"] = str(spos[0]) 0384 dom.attrib["cy"] = str(spos[1]) 0385 dom.attrib["r"] = str((epos-spos).length) 0386 a = gradient.highlight_angle.get_value(self.time) * math.pi / 180 0387 l = gradient.highlight_length.get_value(self.time) 0388 dom.attrib["fx"] = str(spos[0] + math.cos(a) * l) 0389 dom.attrib["fy"] = str(spos[1] + math.sin(a) * l) 0390 0391 id = self.set_id(dom, gradient, force=True) 0392 dom.attrib["gradientUnits"] = "userSpaceOnUse" 0393 0394 for off, color in gradient.colors.stops_at(self.time): 0395 stop = ElementTree.SubElement(dom, "stop") 0396 stop.attrib["offset"] = "%s%%" % (off * 100) 0397 stop.attrib["stop-color"] = color_to_css(color[:3]) 0398 if len(color) > 3: 0399 stop.attrib["stop-opacity"] = str(color[3]) 0400 0401 return id 0402 0403 def group_from_lottie(self, lottie, dom_parent, layer): 0404 g = ElementTree.SubElement(dom_parent, "g") 0405 if layer and self.name_mode == NameMode.Inkscape: 0406 g.attrib[self.qualified("inkscape", "groupmode")] = "layer" 0407 self.set_id(g, lottie, force=True) 0408 self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False)) 0409 return g 0410 0411 def _on_shapegroup(self, group, dom_parent): 0412 if group.empty(): 0413 return 0414 0415 if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger): 0416 path = self.build_path(group.paths.paths, dom_parent) 0417 self.set_id(path, group.paths.paths[0], force=True) 0418 path.attrib["style"] = self.group_to_style(group) 0419 self.set_transform(path, group.lottie.transform) 0420 return self._split_stroke(group, path, dom_parent) 0421 0422 g = self.group_from_lottie(group.lottie, dom_parent, group.layer) 0423 g.attrib["style"] = self.group_to_style(group) 0424 self.shapegroup_process_children(group, g) 0425 return self._split_stroke(group, g, dom_parent) 0426 0427 def _on_merged_path(self, shape, shapegroup, out_parent): 0428 path = self.build_path(shape.paths, out_parent) 0429 self.set_id(path, shape.paths[0]) 0430 path.attrib["style"] = self.group_to_style(shapegroup) 0431 #self._split_stroke(shapegroup, path, out_parent) 0432 return path 0433 0434 def _on_shape(self, shape, shapegroup, out_parent): 0435 if isinstance(shape, objects.Rect): 0436 svgshape = self.build_rect(shape, out_parent) 0437 elif isinstance(shape, objects.Ellipse): 0438 svgshape = self.build_ellipse(shape, out_parent) 0439 elif isinstance(shape, objects.Star): 0440 svgshape = self.build_path([shape.to_bezier()], out_parent) 0441 elif isinstance(shape, objects.Path): 0442 svgshape = self.build_path([shape], out_parent) 0443 elif has_font and isinstance(shape, font.FontShape): 0444 svgshape = self.build_text(shape, out_parent) 0445 else: 0446 return 0447 self.set_id(svgshape, shape, force=True) 0448 if "style" not in svgshape.attrib: 0449 svgshape.attrib["style"] = "" 0450 svgshape.attrib["style"] += self.group_to_style(shapegroup) 0451 #self._split_stroke(shapegroup, svgshape, out_parent) 0452 0453 if shape.hidden: 0454 svgshape.attrib["style"] += "display: none;" 0455 return svgshape 0456 0457 def build_rect(self, shape, parent): 0458 rect = ElementTree.SubElement(parent, "rect") 0459 size = shape.size.get_value(self.time) 0460 pos = shape.position.get_value(self.time) 0461 rect.attrib["width"] = str(size[0]) 0462 rect.attrib["height"] = str(size[1]) 0463 rect.attrib["x"] = str(pos[0] - size[0] / 2) 0464 rect.attrib["y"] = str(pos[1] - size[1] / 2) 0465 rect.attrib["rx"] = str(shape.rounded.get_value(self.time)) 0466 return rect 0467 0468 def build_ellipse(self, shape, parent): 0469 ellipse = ElementTree.SubElement(parent, "ellipse") 0470 size = shape.size.get_value(self.time) 0471 pos = shape.position.get_value(self.time) 0472 ellipse.attrib["rx"] = str(size[0] / 2) 0473 ellipse.attrib["ry"] = str(size[1] / 2) 0474 ellipse.attrib["cx"] = str(pos[0]) 0475 ellipse.attrib["cy"] = str(pos[1]) 0476 return ellipse 0477 0478 def build_path(self, shapes, parent): 0479 path = ElementTree.SubElement(parent, "path") 0480 d = "" 0481 for shape in shapes: 0482 bez = shape.shape.get_value(self.time) 0483 if isinstance(bez, list): 0484 bez = bez[0] 0485 if not bez.vertices: 0486 continue 0487 if d: 0488 d += "\n" 0489 d += self._bezier_to_d(bez) 0490 0491 path.attrib["d"] = d 0492 return path 0493 0494 def _bezier_tangent(self, tangent): 0495 _tangent_threshold = 0.5 0496 if tangent.length < _tangent_threshold: 0497 return NVector(0, 0) 0498 return tangent 0499 0500 def _bezier_to_d(self, bez): 0501 d = "M %s,%s " % tuple(bez.vertices[0].components[:2]) 0502 for i in range(1, len(bez.vertices)): 0503 qfrom = bez.vertices[i-1] 0504 h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom 0505 qto = bez.vertices[i] 0506 h2 = self._bezier_tangent(bez.in_tangents[i]) + qto 0507 0508 d += "C %s,%s %s,%s %s,%s " % ( 0509 h1[0], h1[1], 0510 h2[0], h2[1], 0511 qto[0], qto[1], 0512 ) 0513 if bez.closed: 0514 qfrom = bez.vertices[-1] 0515 h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom 0516 qto = bez.vertices[0] 0517 h2 = self._bezier_tangent(bez.in_tangents[0]) + qto 0518 d += "C %s,%s %s,%s %s,%s Z" % ( 0519 h1[0], h1[1], 0520 h2[0], h2[1], 0521 qto[0], qto[1], 0522 ) 0523 0524 return d 0525 0526 def _on_shape_modifier(self, shape, shapegroup, out_parent): 0527 if isinstance(shape.lottie, objects.Repeater): 0528 svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent) 0529 elif isinstance(shape.lottie, objects.RoundedCorners): 0530 svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent) 0531 elif isinstance(shape.lottie, objects.Trim): 0532 svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent) 0533 else: 0534 return self.shapegroup_process_child(shape.child, shapegroup, out_parent) 0535 return svgshape 0536 0537 def build_repeater(self, shape, child, shapegroup, out_parent): 0538 original = self.shapegroup_process_child(child, shapegroup, out_parent) 0539 if not original: 0540 return 0541 0542 ncopies = int(round(shape.copies.get_value(self.time))) 0543 if ncopies == 1: 0544 return 0545 0546 out_parent.remove(original) 0547 0548 g = ElementTree.SubElement(out_parent, "g") 0549 self.set_clean_id(g, "repeater") 0550 0551 for copy in range(ncopies-1): 0552 use = ElementTree.SubElement(g, "use") 0553 use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"] 0554 0555 orig_wrapper = ElementTree.SubElement(g, "g") 0556 orig_wrapper.append(original) 0557 0558 transform = objects.Transform() 0559 so = shape.transform.start_opacity.get_value(self.time) 0560 eo = shape.transform.end_opacity.get_value(self.time) 0561 position = shape.transform.position.get_value(self.time) 0562 rotation = shape.transform.rotation.get_value(self.time) 0563 anchor_point = shape.transform.anchor_point.get_value(self.time) 0564 for i in range(ncopies-1, -1, -1): 0565 of = i / (ncopies-1) 0566 transform.opacity.value = so * of + eo * (1 - of) 0567 self.set_transform(g[i], transform) 0568 transform.position.value += position 0569 transform.rotation.value += rotation 0570 transform.anchor_point.value += anchor_point 0571 0572 return g 0573 0574 def build_rouded_corners(self, shape, child, shapegroup, out_parent): 0575 round_amount = shape.radius.get_value(self.time) 0576 return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount) 0577 0578 def _build_rouded_corners_shape(self, shape, round_amount): 0579 if not isinstance(shape, objects.Shape): 0580 return [shape] 0581 path = shape.to_bezier() 0582 bezier = path.shape.get_value(self.time).rounded(round_amount) 0583 path.shape.clear_animation(bezier) 0584 return [path] 0585 0586 def build_trim_path(self, shape, child, shapegroup, out_parent): 0587 start = max(0, min(1, shape.start.get_value(self.time) / 100)) 0588 end = max(0, min(1, shape.end.get_value(self.time) / 100)) 0589 offset = shape.offset.get_value(self.time) / 360 % 1 0590 0591 multidata = {} 0592 length = 0 0593 0594 if shape.multiple == objects.TrimMultipleShapes.Individually: 0595 for visishape in reversed(list(self._modifier_foreach_shape(child))): 0596 bez = visishape.to_bezier().shape.get_value(self.time) 0597 local_length = bez.rough_length() 0598 multidata[visishape] = (bez, length, local_length) 0599 length += local_length 0600 0601 return self._modifier_process( 0602 child, shapegroup, out_parent, self._build_trim_path_shape, 0603 start+offset, end+offset, multidata, length 0604 ) 0605 0606 def _modifier_foreach_shape(self, shape): 0607 if isinstance(shape, restructure.RestructuredShapeGroup): 0608 for child in shape.children: 0609 for chsh in self._modifier_foreach_shape(child): 0610 yield chsh 0611 elif isinstance(shape, restructure.RestructuredPathMerger): 0612 for p in shape.paths: 0613 yield p 0614 elif isinstance(shape, objects.Shape): 0615 yield shape 0616 0617 def _modifier_process(self, child, shapegroup, out_parent, callback, *args): 0618 children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args) 0619 return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children] 0620 0621 def _trim_offlocal(self, t, local_start, local_length, total_length): 0622 gt = (t * total_length - local_start) / local_length 0623 return max(0, min(1, gt)) 0624 0625 def _build_trim_path_shape(self, shape, start, end, multidata, total_length): 0626 if not isinstance(shape, objects.Shape): 0627 return [shape] 0628 0629 if multidata: 0630 bezier, local_start, local_length = multidata[shape] 0631 if end > 1: 0632 lstart = self._trim_offlocal(start, local_start, local_length, total_length) 0633 lend = self._trim_offlocal(end-1, local_start, local_length, total_length) 0634 out = [] 0635 if lstart < 1: 0636 out.append(objects.Path(bezier.segment(lstart, 1))) 0637 if lend > 0: 0638 out.append(objects.Path(bezier.segment(0, lend))) 0639 return out 0640 0641 lstart = self._trim_offlocal(start, local_start, local_length, total_length) 0642 lend = self._trim_offlocal(end, local_start, local_length, total_length) 0643 if lend <= 0 or lstart >= 1: 0644 return [] 0645 if lstart <= 0 and lend >= 1: 0646 return [objects.Path(bezier)] 0647 seg = bezier.segment(lstart, lend) 0648 return [objects.Path(seg)] 0649 0650 path = shape.to_bezier() 0651 bezier = path.shape.get_value(self.time) 0652 if end > 1: 0653 bez1 = bezier.segment(start, 1) 0654 bez2 = bezier.segment(0, end-1) 0655 return [objects.Path(bez1), objects.Path(bez2)] 0656 else: 0657 seg = bezier.segment(start, end) 0658 return [objects.Path(seg)] 0659 0660 def _modifier_process_children(self, shapegroup, out_parent, callback, *args): 0661 children = [] 0662 for shape in shapegroup.children: 0663 children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args)) 0664 shapegroup.children = children 0665 0666 def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args): 0667 if isinstance(shape, restructure.RestructuredShapeGroup): 0668 self._modifier_process_children(shape, out_parent, callback, *args) 0669 return [shape] 0670 elif isinstance(shape, restructure.RestructuredPathMerger): 0671 paths = [] 0672 for p in shape.paths: 0673 paths.extend(callback(p, *args)) 0674 shape.paths = paths 0675 if paths: 0676 return [shape] 0677 return [] 0678 else: 0679 return callback(shape, *args) 0680 0681 def _custom_object_supported(self, shape): 0682 if has_font and isinstance(shape, font.FontShape): 0683 return True 0684 return False 0685 0686 def build_text(self, shape, parent): 0687 text = ElementTree.SubElement(parent, "text") 0688 if "family" in shape.query: 0689 text.attrib["font-family"] = shape.query["family"] 0690 if "weight" in shape.query: 0691 text.attrib["font-weight"] = str(shape.query.weight_to_css()) 0692 slant = int(shape.query.get("slant", 0)) 0693 if slant > 0 and slant < 110: 0694 text.attrib["font-style"] = "italic" 0695 elif slant >= 110: 0696 text.attrib["font-style"] = "oblique" 0697 0698 text.attrib["font-size"] = str(shape.size) 0699 0700 text.attrib["white-space"] = "pre" 0701 0702 pos = shape.style.position 0703 text.attrib["x"] = str(pos.x) 0704 text.attrib["y"] = str(pos.y) 0705 text.text = shape.text 0706 0707 return text 0708 0709 0710 def color_to_css(color): 0711 #if len(color) == 4: 0712 #return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3] 0713 return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3])) 0714 0715 0716 def to_svg(animation, time): 0717 builder = SvgBuilder(time) 0718 builder.process(animation) 0719 return builder.dom