Programmatically generate SVG (vector) images, animations, and interactive Jupyter widgets

Add invert_y support for add_key_frame and add_attribute_key_sequence

Changed files
+301 -104
drawsvg
+83 -41
drawsvg/drawing.py
···
self.width = width
self.height = height
if isinstance(origin, str):
-
top, bottom = 0, -height
-
if context.invert_y:
-
top, bottom = bottom, top
self.view_box = {
'center': (-width/2, -height/2, width, height),
-
'top-left': (0, top, width, height),
-
'top-right': (-width, top, width, height),
-
'bottom-left': (0, bottom, width, height),
-
'bottom-right': (-width, bottom, width, height),
}[origin]
else:
origin = tuple(origin)
···
self.context = context
self.id_prefix = str(id_prefix)
self.svg_args = {}
for k, v in svg_args.items():
k = k.replace('__', ':')
k = k.replace('_', '-')
if k[-1] == '-':
k = k[:-1]
self.svg_args[k] = v
-
self.context.drawing_creation_hook(self)
def set_render_size(self, w=None, h=None):
self.render_width = w
self.render_height = h
···
else:
self.svg_args['onload'] = onload
self.js_list.append(js_text)
-
def all_elements(self):
-
'''Return self.elements and self.ordered_elements as a single list.'''
-
output = list(self.elements)
for z in sorted(self.ordered_elements):
output.extend(self.ordered_elements[z])
return output
def as_svg(self, output_file=None, randomize_ids=False, header=XML_HEADER,
-
skip_js=False, skip_css=False):
if output_file is None:
with StringIO() as f:
-
self.as_svg(f, randomize_ids=randomize_ids, header=header)
return f.getvalue()
output_file.write(header)
img_width, img_height = self.calc_render_size()
svg_args = dict(
···
viewBox=' '.join(map(str, self.view_box)))
svg_args.update(self.svg_args)
output_file.write(SVG_START)
-
self.context.write_svg_document_args(svg_args, output_file)
output_file.write('>\n')
-
if self.css_list and not skip_css:
output_file.write(SVG_CSS_FMT.format(elements_module.escape_cdata(
-
'\n'.join(self.css_list))))
output_file.write('\n')
output_file.write('<defs>\n')
# Write definition elements
···
return dup
for element in self.other_defs:
if hasattr(element, 'write_svg_element'):
element.write_svg_element(
-
id_map, is_duplicate, output_file, self.context, False)
output_file.write('\n')
-
all_elements = self.all_elements()
for element in all_elements:
if hasattr(element, 'write_svg_defs'):
element.write_svg_defs(
-
id_map, is_duplicate, output_file, self.context, False)
output_file.write('</defs>\n')
# Generate ids for normal elements
prev_def_set = set(prev_set)
for element in all_elements:
if hasattr(element, 'write_svg_element'):
element.write_svg_element(
-
id_map, is_duplicate, output_file, self.context, True)
prev_set = prev_def_set
# Write normal elements
for element in all_elements:
if hasattr(element, 'write_svg_element'):
element.write_svg_element(
-
id_map, is_duplicate, output_file, self.context, False)
output_file.write('\n')
-
if self.js_list and not skip_js:
output_file.write(SVG_JS_FMT.format(elements_module.escape_cdata(
-
'\n'.join(self.js_list))))
output_file.write('\n')
output_file.write(SVG_END)
def as_html(self, output_file=None, title=None, randomize_ids=False,
-
fix_embed_iframe=False):
if output_file is None:
with StringIO() as f:
self.as_html(
f, title=title, randomize_ids=randomize_ids,
-
fix_embed_iframe=fix_embed_iframe)
return f.getvalue()
output_file.write('<!DOCTYPE html>\n')
output_file.write('<head>\n')
···
output_file.write('</head>\n<body>\n')
self.as_svg(
output_file, randomize_ids=randomize_ids, header="",
-
skip_css=False, skip_js=False)
output_file.write('\n</body>\n</html>\n')
@staticmethod
def _random_id(length=8):
return (random.choice(string.ascii_letters)
+ ''.join(random.choices(
string.ascii_letters+string.digits, k=length-1)))
-
def save_svg(self, fname, encoding='utf-8'):
with open(fname, 'w', encoding=encoding) as f:
-
self.as_svg(output_file=f)
-
def save_html(self, fname, title=None, encoding='utf-8'):
with open(fname, 'w', encoding=encoding) as f:
-
self.as_html(output_file=f, title=title)
-
def save_png(self, fname):
-
self.rasterize(to_file=fname)
-
def rasterize(self, to_file=None):
if to_file is not None:
-
return Raster.from_svg_to_file(self.as_svg(), to_file)
else:
-
return Raster.from_svg(self.as_svg())
def _repr_svg_(self):
'''Display in Jupyter notebook.'''
return self.as_svg(randomize_ids=True)
-
def display_inline(self):
'''Display inline in the Jupyter web page.'''
-
return jupyter.JupyterSvgInline(self.as_svg(randomize_ids=True))
-
def display_iframe(self):
'''Display within an iframe the Jupyter web page.'''
w, h = self.calc_render_size()
-
html = self.as_html(fix_embed_iframe=True)
return jupyter.JupyterSvgFrame(html, w, h, mime='text/html')
-
def display_image(self):
'''Display within an img in the Jupyter web page.'''
-
return jupyter.JupyterSvgImage(self.as_svg())
···
self.width = width
self.height = height
if isinstance(origin, str):
+
if context.invert_y and origin.startswith('bottom-'):
+
origin = origin.replace('bottom-', 'top-')
+
elif context.invert_y and origin.startswith('top-'):
+
origin = origin.replace('top-', 'bottom-')
self.view_box = {
'center': (-width/2, -height/2, width, height),
+
'top-left': (0, 0, width, height),
+
'top-right': (-width, 0, width, height),
+
'bottom-left': (0, -height, width, height),
+
'bottom-right': (-width, -height, width, height),
}[origin]
else:
origin = tuple(origin)
···
self.context = context
self.id_prefix = str(id_prefix)
self.svg_args = {}
+
self._cached_context = None
+
self._cached_extra_prepost_with_context = None
for k, v in svg_args.items():
k = k.replace('__', ':')
k = k.replace('_', '-')
if k[-1] == '-':
k = k[:-1]
self.svg_args[k] = v
def set_render_size(self, w=None, h=None):
self.render_width = w
self.render_height = h
···
else:
self.svg_args['onload'] = onload
self.js_list.append(js_text)
+
def all_elements(self, context=None):
+
'''Return self.elements, self.ordered_elements, and extras as a single
+
list.
+
'''
+
extra_pre, extra_post = (
+
self._extra_prepost_with_context_avoid_recompute(
+
context=context))
+
output = list(extra_pre)
+
output.extend(self.elements)
for z in sorted(self.ordered_elements):
output.extend(self.ordered_elements[z])
+
output.extend(extra_post)
return output
+
def _extra_prepost_with_context_avoid_recompute(self, context=None):
+
if (self._cached_extra_prepost_with_context is not None
+
and self._cached_context == context):
+
return self._cached_extra_prepost_with_context
+
self._cached_context = context
+
self._cached_extra_prepost_with_context = (
+
self._extra_prepost_children_with_context(context))
+
return self._cached_extra_prepost_with_context
+
def _extra_prepost_children_with_context(self, context=None):
+
if context is None:
+
context = self.context
+
return context.extra_prepost_drawing_elements(self)
+
def all_css(self, context=None):
+
if context is None:
+
context = self.context
+
return list(context.extra_css(self)) + self.css_list
+
def all_javascript(self, context=None):
+
if context is None:
+
context = self.context
+
return list(context.extra_javascript(self)) + self.js_list
def as_svg(self, output_file=None, randomize_ids=False, header=XML_HEADER,
+
skip_js=False, skip_css=False, context=None):
if output_file is None:
with StringIO() as f:
+
self.as_svg(
+
f, randomize_ids=randomize_ids, header=header,
+
skip_js=skip_js, skip_css=skip_css, context=context)
return f.getvalue()
+
if context is None:
+
context = self.context
output_file.write(header)
img_width, img_height = self.calc_render_size()
svg_args = dict(
···
viewBox=' '.join(map(str, self.view_box)))
svg_args.update(self.svg_args)
output_file.write(SVG_START)
+
context.write_svg_document_args(self, svg_args, output_file)
output_file.write('>\n')
+
css_list = self.all_css(context)
+
if css_list and not skip_css:
output_file.write(SVG_CSS_FMT.format(elements_module.escape_cdata(
+
'\n'.join(css_list))))
output_file.write('\n')
output_file.write('<defs>\n')
# Write definition elements
···
return dup
for element in self.other_defs:
if hasattr(element, 'write_svg_element'):
+
local = types.LocalContext(
+
context, element, self, self.other_defs)
element.write_svg_element(
+
id_map, is_duplicate, output_file, local, False)
output_file.write('\n')
+
all_elements = self.all_elements(context=context)
for element in all_elements:
if hasattr(element, 'write_svg_defs'):
+
local = types.LocalContext(context, element, self, all_elements)
element.write_svg_defs(
+
id_map, is_duplicate, output_file, local, False)
output_file.write('</defs>\n')
# Generate ids for normal elements
prev_def_set = set(prev_set)
for element in all_elements:
if hasattr(element, 'write_svg_element'):
+
local = types.LocalContext(context, element, self, all_elements)
element.write_svg_element(
+
id_map, is_duplicate, output_file, local, True)
prev_set = prev_def_set
# Write normal elements
for element in all_elements:
if hasattr(element, 'write_svg_element'):
+
local = types.LocalContext(context, element, self, all_elements)
element.write_svg_element(
+
id_map, is_duplicate, output_file, local, False)
output_file.write('\n')
+
js_list = self.all_javascript(context)
+
if js_list and not skip_js:
output_file.write(SVG_JS_FMT.format(elements_module.escape_cdata(
+
'\n'.join(js_list))))
output_file.write('\n')
output_file.write(SVG_END)
def as_html(self, output_file=None, title=None, randomize_ids=False,
+
context=None, fix_embed_iframe=False):
if output_file is None:
with StringIO() as f:
self.as_html(
f, title=title, randomize_ids=randomize_ids,
+
context=context, fix_embed_iframe=fix_embed_iframe)
return f.getvalue()
output_file.write('<!DOCTYPE html>\n')
output_file.write('<head>\n')
···
output_file.write('</head>\n<body>\n')
self.as_svg(
output_file, randomize_ids=randomize_ids, header="",
+
skip_css=False, skip_js=False, context=context)
output_file.write('\n</body>\n</html>\n')
@staticmethod
def _random_id(length=8):
return (random.choice(string.ascii_letters)
+ ''.join(random.choices(
string.ascii_letters+string.digits, k=length-1)))
+
def save_svg(self, fname, encoding='utf-8', context=None):
with open(fname, 'w', encoding=encoding) as f:
+
self.as_svg(output_file=f, context=context)
+
def save_html(self, fname, title=None, encoding='utf-8', context=None):
with open(fname, 'w', encoding=encoding) as f:
+
self.as_html(output_file=f, title=title, context=context)
+
def save_png(self, fname, context=None):
+
self.rasterize(to_file=fname, context=context)
+
def rasterize(self, to_file=None, context=None):
if to_file is not None:
+
return Raster.from_svg_to_file(
+
self.as_svg(context=context), to_file)
else:
+
return Raster.from_svg(self.as_svg(context=context))
def _repr_svg_(self):
'''Display in Jupyter notebook.'''
return self.as_svg(randomize_ids=True)
+
def display_inline(self, context=None):
'''Display inline in the Jupyter web page.'''
+
return jupyter.JupyterSvgInline(self.as_svg(
+
randomize_ids=True, context=context))
+
def display_iframe(self, context=None):
'''Display within an iframe the Jupyter web page.'''
w, h = self.calc_render_size()
+
html = self.as_html(fix_embed_iframe=True, context=context)
return jupyter.JupyterSvgFrame(html, w, h, mime='text/html')
+
def display_image(self, context=None):
'''Display within an img in the Jupyter web page.'''
+
return jupyter.JupyterSvgImage(self.as_svg(context=context))
+12 -9
drawsvg/elements.py
···
import xml.sax.saxutils as xml
from . import url_encode
-
from .types import DrawingElement, DrawingBasicElement, DrawingParentElement
def escape_cdata(content):
···
def __init__(self):
pass
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
-
context, force_dup=False):
pass
def __eq__(self, other):
if isinstance(other, type(self)):
···
super().__init__()
self.content = content
self.defs = defs
-
def write_svg_element(self, id_map, is_duplicate, output_file, context,
dry_run, force_dup=False):
if dry_run:
return
···
single_line = False
text = tuple(text)
return text, single_line
-
def write_content(self, id_map, is_duplicate, output_file, context,
dry_run):
if dry_run:
return
output_file.write(self.escaped_text)
-
def write_children_content(self, id_map, is_duplicate, output_file, context,
-
dry_run):
-
children = self.all_children(context=context)
for child in children:
child.write_svg_element(
-
id_map, is_duplicate, output_file, context, dry_run)
def append_line(self, line, **kwargs):
if self._text_path is not None:
raise ValueError('appendLine is not supported for text on a path')
···
def __init__(self, text, **kwargs):
super().__init__(**kwargs)
self.escaped_text = xml.escape(text)
-
def write_content(self, id_map, is_duplicate, output_file, context,
dry_run):
if dry_run:
return
···
import xml.sax.saxutils as xml
from . import url_encode
+
from .types import (
+
DrawingElement, DrawingBasicElement, DrawingParentElement, LocalContext
+
)
def escape_cdata(content):
···
def __init__(self):
pass
def write_svg_element(self, id_map, is_duplicate, output_file, dry_run,
+
lcontext, force_dup=False):
pass
def __eq__(self, other):
if isinstance(other, type(self)):
···
super().__init__()
self.content = content
self.defs = defs
+
def write_svg_element(self, id_map, is_duplicate, output_file, lcontext,
dry_run, force_dup=False):
if dry_run:
return
···
single_line = False
text = tuple(text)
return text, single_line
+
def write_content(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
if dry_run:
return
output_file.write(self.escaped_text)
+
def write_children_content(self, id_map, is_duplicate, output_file,
+
lcontext, dry_run):
+
children = self.all_children(lcontext=lcontext)
for child in children:
+
local = LocalContext(lcontext.context, child, self, children)
child.write_svg_element(
+
id_map, is_duplicate, output_file, local, dry_run)
def append_line(self, line, **kwargs):
if self._text_path is not None:
raise ValueError('appendLine is not supported for text on a path')
···
def __init__(self, text, **kwargs):
super().__init__(**kwargs)
self.escaped_text = xml.escape(text)
+
def write_content(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
if dry_run:
return
+128 -15
drawsvg/native_animation/synced_animation.py
···
def total_duration(self):
return self.start_delay + self.duration + self.end_delay
-
def drawing_creation_hook(self, d, context):
-
'''Called by Drawing on initialization.'''
config = self._with_filled_defaults(d, context)
if self.show_playback_progress or self.show_playback_controls:
-
# Append control UI
controls = config.controls_draw_function(
config, hidden=not self.show_playback_progress)
-
d.append(controls, z=float('inf'))
-
if self.show_playback_controls:
-
# Add control JavaScript
-
d.append_javascript(config.controls_js,
-
onload=config.controls_js_onload)
def _with_filled_defaults(self, d, context):
# By default place the controls along the bottom edge
···
repeat_count = 1
fill = 'freeze'
dur_str = f'{total_duration}s'
key_times = ';'.join(
str(max(0, min(1, round(
(start_delay + t) / total_duration, 3))))
-
for t in self.times
)
-
values_str = ';'.join(map(str, self.values))
if not key_times.startswith('0;'):
key_times = '0;' + key_times
-
values_str = f'{self.values[0]};' + values_str
if not key_times.endswith(';1'):
key_times = key_times + ';1'
-
values_str = values_str + f';{self.values[-1]}'
attrs = dict(
dur=dur_str,
values=values_str,
···
self.attr_timelines[attr] = timeline
timeline.extend(times, values)
-
def children_with_context(self, context=None):
return [
-
timeline.as_animate_element(context.animation_config)
-
for timeline in self.attr_timelines.values()
]
def animate_element_sequence(times, element_sequence):
'''Animate a list of elements to appear one-at-a-time in sequence.
···
def total_duration(self):
return self.start_delay + self.duration + self.end_delay
+
def extra_css(self, d, context):
+
return []
+
+
def extra_javascript(self, d, context):
+
config = self._with_filled_defaults(d, context)
+
if self.show_playback_controls:
+
return [config.controls_js]
+
return []
+
+
def extra_onload_js(self, d, context):
+
config = self._with_filled_defaults(d, context)
+
if self.show_playback_controls:
+
return [config.controls_js_onload]
+
return []
+
+
def extra_drawing_elements(self, d, context):
config = self._with_filled_defaults(d, context)
if self.show_playback_progress or self.show_playback_controls:
+
# Control UI
controls = config.controls_draw_function(
config, hidden=not self.show_playback_progress)
+
return [controls]
+
return []
def _with_filled_defaults(self, d, context):
# By default place the controls along the bottom edge
···
repeat_count = 1
fill = 'freeze'
dur_str = f'{total_duration}s'
+
values = self.values
+
times = self.times
key_times = ';'.join(
str(max(0, min(1, round(
(start_delay + t) / total_duration, 3))))
+
for t in times
)
+
values_str = ';'.join(map(str, values))
if not key_times.startswith('0;'):
key_times = '0;' + key_times
+
values_str = f'{values[0]};' + values_str
if not key_times.endswith(';1'):
key_times = key_times + ';1'
+
values_str = values_str + f';{values[-1]}'
attrs = dict(
dur=dur_str,
values=values_str,
···
self.attr_timelines[attr] = timeline
timeline.extend(times, values)
+
def _timelines_adjusted_for_context(self, lcontext=None):
+
all_timelines = dict(self.attr_timelines)
+
if lcontext is not None and lcontext.context.invert_y:
+
# Invert cy, y1, y2, ...
+
for name, timeline in self.attr_timelines.items():
+
if name != 'y' and lcontext.context.is_attr_inverted(name):
+
inv_timeline = AnimatedAttributeTimeline(
+
timeline.name, timeline.animate_attrs,
+
timeline.times, [-v for v in timeline.values])
+
all_timelines[name] = inv_timeline
+
# Invert -y - height
+
y_attrs = None
+
if 'height' in all_timelines.keys():
+
height_timeline = all_timelines['height']
+
htimes = height_timeline.times
+
hvalues = height_timeline.values
+
y_attrs = height_timeline.animate_attrs
+
else:
+
height_timeline = None
+
htimes = [0]
+
hvalues = [lcontext.element.args.get('height', 0)]
+
if 'y' in all_timelines.keys():
+
y_timeline = all_timelines['y']
+
ytimes = y_timeline.times
+
yvalues = y_timeline.values
+
y_attrs = y_timeline.animate_attrs
+
else:
+
y_timeline = None
+
ytimes = [0]
+
yvalues = [lcontext.element.args.get('y', 0)]
+
if y_timeline is not None or height_timeline is not None:
+
ytimes, yvalues = _merge_timeline_inverted_y_values(
+
ytimes, yvalues, htimes, hvalues)
+
if ytimes is not None:
+
y_timeline = AnimatedAttributeTimeline(
+
'y', y_attrs, ytimes, yvalues)
+
all_timelines['y'] = y_timeline
+
return all_timelines
+
+
def children_with_context(self, lcontext=None):
+
all_timelines = self._timelines_adjusted_for_context(lcontext)
return [
+
timeline.as_animate_element(lcontext.context.animation_config)
+
for timeline in all_timelines.values()
]
+
+
def _merge_timeline_inverted_y_values(ytimes, yvalues, htimes, hvalues):
+
if len(yvalues) == 1:
+
try:
+
return htimes, [-yvalues[0]-h for h in hvalues]
+
except TypeError:
+
return None, None
+
elif len(hvalues) == 1:
+
try:
+
return ytimes, [-y-hvalues[0] for y in yvalues]
+
except TypeError:
+
return None, None
+
elif ytimes == htimes:
+
try:
+
return ytimes, [-y-h for y, h in zip(yvalues, hvalues)]
+
except TypeError:
+
return None, None
+
def interpolate(times, values, at_time):
+
if len(times) == 0:
+
return 0
+
idx = sum(t <= at_time for t in times)
+
if idx >= len(times):
+
return values[-1]
+
elif idx <= 0:
+
return values[0]
+
elif at_time == times[idx-1]:
+
return values[idx-1]
+
else:
+
fraction = (at_time-times[idx-1]) / (times[idx]-times[idx-1])
+
return values[idx-1] * (1-fraction)+ (values[idx] * fraction)
+
try:
+
# Offset y-value by height if invert_y
+
# Merge key_times for y and height animations
+
new_times = []
+
new_values = []
+
hi = yi = 0
+
inf = float('inf')
+
ht = htimes[0] if len(htimes) else inf
+
yt = ytimes[0] if len(ytimes) else inf
+
while ht < inf and yt < inf:
+
if yt < ht:
+
h_val = interpolate(htimes, hvalues, yt)
+
new_times.append(yt)
+
new_values.append(-yvalues[yi] - h_val)
+
yi += 1
+
elif ht < yt:
+
y_val = interpolate(ytimes, yvalues, ht)
+
new_times.append(ht)
+
new_values.append(-y_val - hvalues[hi])
+
hi += 1
+
else:
+
new_times.append(yt)
+
new_values.append(-yvalues[yi] - hvalues[hi])
+
yi += 1
+
hi += 1
+
yt = ytimes[yi] if yi < len(ytimes) else inf
+
ht = htimes[hi] if hi < len(htimes) else inf
+
return new_times, new_values
+
except TypeError:
+
return None, None
def animate_element_sequence(times, element_sequence):
'''Animate a list of elements to appear one-at-a-time in sequence.
+78 -39
drawsvg/types.py
···
invert_y: bool = False
animation_config: Optional[SyncedAnimationConfig] = None
-
def drawing_creation_hook(self, d):
-
'''Called by Drawing on initialization.'''
if self.animation_config:
-
self.animation_config.drawing_creation_hook(d, context=self)
def override_view_box(self, view_box):
if self.invert_y:
···
x, y, w, h = view_box
view_box = (x, -y-h, w, h)
return view_box
def override_args(self, args):
args = dict(args)
···
raise
return args
-
def write_svg_document_args(self, args, output_file):
'''Called by Drawing during SVG output of the <svg> tag.'''
args['viewBox'] = self.override_view_box(args['viewBox'])
self._write_tag_args(args, output_file)
-
-
def write_tag_args(self, args, output_file, id_map=None):
-
'''Called by an element during SVG output of its tag.'''
-
self._write_tag_args(
-
self.override_args(args), output_file, id_map=id_map)
def _write_tag_args(self, args, output_file, id_map=None):
'''Called by an element during SVG output of its tag.'''
···
output_file.write(' {}="{}"'.format(k,v))
class DrawingElement:
'''Base class for drawing elements.
Subclasses must implement write_svg_element.
'''
-
def write_svg_element(self, id_map, is_duplicate, output_file, context,
dry_run, force_dup=False):
raise NotImplementedError('Abstract base class')
def get_svg_defs(self):
return ()
def get_linked_elems(self):
return ()
-
def write_svg_defs(self, id_map, is_duplicate, output_file, context,
dry_run):
for defn in self.get_svg_defs():
if is_duplicate(defn):
continue
defn.write_svg_defs(
-
id_map, is_duplicate, output_file, context, dry_run)
if defn.id is None:
id_map[id(defn)]
defn.write_svg_element(
-
id_map, is_duplicate, output_file, context, dry_run,
force_dup=True)
if not dry_run:
output_file.write('\n')
···
if not self.has_content:
raise RuntimeError(
'{} does not support children'.format(type(self)))
-
def _extra_children_with_context_avoid_recompute(self, context=None):
if (self._cached_extra_children_with_context is not None
-
and self._cached_context == context):
return self._cached_extra_children_with_context
-
self._cached_context = context
self._cached_extra_children_with_context = (
-
self.extra_children_with_context(context))
return self._cached_extra_children_with_context
-
def extra_children_with_context(self, context=None):
-
return self.animation_data.children_with_context(context)
-
def all_children(self, context=None):
'''Return self.children and self.ordered_children as a single list.'''
output = list(self.children)
for z in sorted(self.ordered_children):
output.extend(self.ordered_children[z])
output.extend(
-
self._extra_children_with_context_avoid_recompute(context))
return output
@property
def id(self):
return self.args.get('id', None)
-
def write_svg_element(self, id_map, is_duplicate, output_file, context,
dry_run, force_dup=False):
-
children = self.all_children(context=context)
if dry_run:
if is_duplicate(self) and self.id is None:
id_map[id(self)]
···
id_map[id(elem.id)]
if self.has_content:
self.write_content(
-
id_map, is_duplicate, output_file, context, dry_run)
if children is not None and len(children):
self.write_children_content(
-
id_map, is_duplicate, output_file, context, dry_run)
return
if is_duplicate(self) and not force_dup:
mapped_id = self.id
···
if id(self) in id_map:
override_args = dict(override_args)
override_args['id'] = id_map[id(self)]
-
context.write_tag_args(override_args, output_file, id_map)
if not self.has_content and (children is None or len(children) == 0):
output_file.write(' />')
else:
output_file.write('>')
if self.has_content:
self.write_content(
-
id_map, is_duplicate, output_file, context, dry_run)
if children is not None and len(children):
self.write_children_content(
-
id_map, is_duplicate, output_file, context, dry_run)
output_file.write('</')
output_file.write(self.TAG_NAME)
output_file.write('>')
-
def write_content(self, id_map, is_duplicate, output_file, context,
dry_run):
'''Override in a subclass to add data between the start and end tags.
This will not be called if has_content is False.
'''
raise RuntimeError('This element has no content')
-
def write_children_content(self, id_map, is_duplicate, output_file, context,
-
dry_run):
'''Override in a subclass to add data between the start and end tags.
This will not be called if has_content is False.
'''
-
children = self.all_children(context=context)
if dry_run:
for child in children:
child.write_svg_element(
-
id_map, is_duplicate, output_file, context, dry_run)
return
output_file.write('\n')
for child in children:
-
child.write_svg_element(id_map, is_duplicate, output_file, context, dry_run)
output_file.write('\n')
def get_svg_defs(self):
return [v for v in self.args.values()
if isinstance(v, DrawingElement)]
-
def write_svg_defs(self, id_map, is_duplicate, output_file, context,
dry_run):
super().write_svg_defs(
-
id_map, is_duplicate, output_file, context, dry_run)
-
for child in self.all_children(context=context):
child.write_svg_defs(
-
id_map, is_duplicate, output_file, context, dry_run)
def __eq__(self, other):
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
···
self.ordered_children[z].extend(iterable)
else:
self.children.extend(iterable)
-
def write_content(self, id_map, is_duplicate, output_file, context,
dry_run):
pass
···
invert_y: bool = False
animation_config: Optional[SyncedAnimationConfig] = None
+
def extra_prepost_drawing_elements(self, d):
+
pre, post = [], []
+
if self.animation_config:
+
post.extend(self.animation_config.extra_drawing_elements(
+
d, context=self))
+
return pre, post
+
+
def extra_css(self, d):
if self.animation_config:
+
return self.animation_config.extra_css(d, context=self)
+
return []
+
+
def extra_javascript(self, d):
+
if self.animation_config:
+
return self.animation_config.extra_javascript(d, context=self)
+
return []
+
+
def extra_onload_js(self, d):
+
if self.animation_config:
+
return self.animation_config.extra_onload_js(d, context=self)
+
return []
def override_view_box(self, view_box):
if self.invert_y:
···
x, y, w, h = view_box
view_box = (x, -y-h, w, h)
return view_box
+
+
def is_attr_inverted(self, name):
+
return self.invert_y and name in ('y', 'cy', 'y1', 'y2')
def override_args(self, args):
args = dict(args)
···
raise
return args
+
def write_svg_document_args(self, d, args, output_file):
'''Called by Drawing during SVG output of the <svg> tag.'''
args['viewBox'] = self.override_view_box(args['viewBox'])
+
onload_list = self.extra_onload_js(d)
+
onload_list.extend(args.get('onload', '').split(';'))
+
onload = ';'.join(onload_list)
+
if onload:
+
args['onload'] = onload
self._write_tag_args(args, output_file)
def _write_tag_args(self, args, output_file, id_map=None):
'''Called by an element during SVG output of its tag.'''
···
output_file.write(' {}="{}"'.format(k,v))
+
@dataclasses.dataclass(frozen=True)
+
class LocalContext:
+
context: Context
+
element: 'DrawingElement'
+
parent: Union['DrawingElement', 'Drawing']
+
siblings: Sequence['DrawingElement'] = ()
+
+
def write_tag_args(self, args, output_file, id_map=None):
+
'''Called by an element during SVG output of its tag.'''
+
self.context._write_tag_args(
+
self.context.override_args(args), output_file, id_map=id_map)
+
+
class DrawingElement:
'''Base class for drawing elements.
Subclasses must implement write_svg_element.
'''
+
def write_svg_element(self, id_map, is_duplicate, output_file, lcontext,
dry_run, force_dup=False):
raise NotImplementedError('Abstract base class')
def get_svg_defs(self):
return ()
def get_linked_elems(self):
return ()
+
def write_svg_defs(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
for defn in self.get_svg_defs():
+
local = LocalContext(lcontext.context, defn, self, ())
if is_duplicate(defn):
continue
defn.write_svg_defs(
+
id_map, is_duplicate, output_file, local, dry_run)
if defn.id is None:
id_map[id(defn)]
defn.write_svg_element(
+
id_map, is_duplicate, output_file, local, dry_run,
force_dup=True)
if not dry_run:
output_file.write('\n')
···
if not self.has_content:
raise RuntimeError(
'{} does not support children'.format(type(self)))
+
def _extra_children_with_context_avoid_recompute(self, lcontext=None):
if (self._cached_extra_children_with_context is not None
+
and self._cached_context == lcontext.context):
return self._cached_extra_children_with_context
+
self._cached_context = lcontext.context
self._cached_extra_children_with_context = (
+
self.extra_children_with_context(lcontext))
return self._cached_extra_children_with_context
+
def extra_children_with_context(self, lcontext=None):
+
return self.animation_data.children_with_context(lcontext)
+
def all_children(self, lcontext=None):
'''Return self.children and self.ordered_children as a single list.'''
output = list(self.children)
for z in sorted(self.ordered_children):
output.extend(self.ordered_children[z])
output.extend(
+
self._extra_children_with_context_avoid_recompute(lcontext))
return output
@property
def id(self):
return self.args.get('id', None)
+
def write_svg_element(self, id_map, is_duplicate, output_file, lcontext,
dry_run, force_dup=False):
+
children = self.all_children(lcontext=lcontext)
if dry_run:
if is_duplicate(self) and self.id is None:
id_map[id(self)]
···
id_map[id(elem.id)]
if self.has_content:
self.write_content(
+
id_map, is_duplicate, output_file, lcontext, dry_run)
if children is not None and len(children):
self.write_children_content(
+
id_map, is_duplicate, output_file, lcontext, dry_run)
return
if is_duplicate(self) and not force_dup:
mapped_id = self.id
···
if id(self) in id_map:
override_args = dict(override_args)
override_args['id'] = id_map[id(self)]
+
lcontext.write_tag_args(override_args, output_file, id_map)
if not self.has_content and (children is None or len(children) == 0):
output_file.write(' />')
else:
output_file.write('>')
if self.has_content:
self.write_content(
+
id_map, is_duplicate, output_file, lcontext, dry_run)
if children is not None and len(children):
self.write_children_content(
+
id_map, is_duplicate, output_file, lcontext, dry_run)
output_file.write('</')
output_file.write(self.TAG_NAME)
output_file.write('>')
+
def write_content(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
'''Override in a subclass to add data between the start and end tags.
This will not be called if has_content is False.
'''
raise RuntimeError('This element has no content')
+
def write_children_content(self, id_map, is_duplicate, output_file,
+
lcontext, dry_run):
'''Override in a subclass to add data between the start and end tags.
This will not be called if has_content is False.
'''
+
children = self.all_children(lcontext=lcontext)
if dry_run:
for child in children:
+
local = LocalContext(lcontext.context, child, self, children)
child.write_svg_element(
+
id_map, is_duplicate, output_file, local, dry_run)
return
output_file.write('\n')
for child in children:
+
local = LocalContext(lcontext.context, child, self, children)
+
child.write_svg_element(
+
id_map, is_duplicate, output_file, local, dry_run)
output_file.write('\n')
def get_svg_defs(self):
return [v for v in self.args.values()
if isinstance(v, DrawingElement)]
+
def write_svg_defs(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
super().write_svg_defs(
+
id_map, is_duplicate, output_file, lcontext, dry_run)
+
children = self.all_children(lcontext=lcontext)
+
for child in children:
+
local = LocalContext(lcontext.context, child, self, children)
child.write_svg_defs(
+
id_map, is_duplicate, output_file, local, dry_run)
def __eq__(self, other):
if isinstance(other, type(self)):
return (self.TAG_NAME == other.TAG_NAME and
···
self.ordered_children[z].extend(iterable)
else:
self.children.extend(iterable)
+
def write_content(self, id_map, is_duplicate, output_file, lcontext,
dry_run):
pass