├── docs
└── img
│ ├── 04_fonts1.png
│ ├── 04_fonts2.png
│ ├── example1.png
│ ├── 01_line.svg
│ ├── 01_multilines.svg
│ ├── 01_polygon.svg
│ ├── 04_tspan3.svg
│ ├── 03_pHV.svg
│ ├── 03_pZ.svg
│ ├── 04_tspan2.svg
│ ├── 07_trans.svg
│ ├── 04_rot2.svg
│ ├── 03_pL.svg
│ ├── 04_rot.svg
│ ├── 01_circ.svg
│ ├── 04_tspan.svg
│ ├── 01_ellip.svg
│ ├── 03_pT.svg
│ ├── 01_multilines2.svg
│ ├── 07_cart1.svg
│ ├── 07_scale.svg
│ ├── 07_scale2.svg
│ ├── 07_cart3.svg
│ ├── 02_dash.svg
│ ├── 07_cart2.svg
│ ├── 05_lingrad.svg
│ ├── 02_join.svg
│ ├── 02_mlimit.svg
│ ├── 05_radgrad.svg
│ ├── 06_use.svg
│ ├── 07_rota.svg
│ ├── 02_fsc.svg
│ ├── 03_pA.svg
│ ├── 05_clip.svg
│ ├── 02_linecap.svg
│ ├── 01_rect.svg
│ ├── 01_rectround.svg
│ ├── 07_rota2.svg
│ ├── 05_mask.svg
│ ├── 04_multiline_text.svg
│ ├── 04_mutiline_text.svg
│ ├── 07_scalcent.svg
│ ├── 03_pS.svg
│ ├── 05_clip2.svg
│ ├── 07_skew.svg
│ ├── 04_len.svg
│ ├── 03_pQ.svg
│ ├── 04_fill.svg
│ ├── 04_align.svg
│ ├── 06_group.svg
│ ├── 04_weight.svg
│ ├── 02_foso.svg
│ ├── 05_clip3.svg
│ ├── 05_mask3.svg
│ ├── 02_strokewdth.svg
│ ├── 04_path.svg
│ ├── 05_mask2.svg
│ └── 03_pC.svg
├── examples
├── example1.png
├── example2.png
├── example3.png
├── example4.png
├── example5.gif
├── example6.gif
├── example6.png
├── example7.gif
├── example8.png
├── orbit-spritesheet.png
├── example3.svg
├── example4.svg
├── example8.svg
├── animated.svg
├── example2.svg
├── example1.svg
├── animated-fix-github.svg
├── playback-controls.svg
├── playback-controls.html
└── font.svg
├── .gitignore
├── mkdocs.yml
├── drawsvg
├── widgets
│ ├── __init__.py
│ ├── async_animation.py
│ ├── drawing_javascript.py
│ └── drawing_widget.py
├── native_animation
│ ├── __init__.py
│ ├── playback_control_ui.py
│ ├── playback_control_js.py
│ └── synced_animation.py
├── jupyter.py
├── url_encode.py
├── font_embed.py
├── raster.py
├── __init__.py
├── defs.py
├── frame_animation.py
├── color.py
├── video.py
├── types.py
└── drawing.py
├── FUNDING.yml
├── LICENSE.txt
├── setup.py
└── README.md
/docs/img/04_fonts1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/docs/img/04_fonts1.png
--------------------------------------------------------------------------------
/docs/img/04_fonts2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/docs/img/04_fonts2.png
--------------------------------------------------------------------------------
/docs/img/example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/docs/img/example1.png
--------------------------------------------------------------------------------
/examples/example1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example1.png
--------------------------------------------------------------------------------
/examples/example2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example2.png
--------------------------------------------------------------------------------
/examples/example3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example3.png
--------------------------------------------------------------------------------
/examples/example4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example4.png
--------------------------------------------------------------------------------
/examples/example5.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example5.gif
--------------------------------------------------------------------------------
/examples/example6.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example6.gif
--------------------------------------------------------------------------------
/examples/example6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example6.png
--------------------------------------------------------------------------------
/examples/example7.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example7.gif
--------------------------------------------------------------------------------
/examples/example8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/example8.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | __pycache__
3 | /MANIFEST
4 | /dist
5 | /*.egg-info
6 | *.ipynb_checkpoints
7 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Drawsvg Quick Reference
2 | nav:
3 | - Home: index.md
4 | theme: readthedocs
5 |
--------------------------------------------------------------------------------
/examples/orbit-spritesheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cduck/drawsvg/HEAD/examples/orbit-spritesheet.png
--------------------------------------------------------------------------------
/drawsvg/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | from .drawing_widget import DrawingWidget
2 | from .async_animation import AsyncAnimation
3 |
--------------------------------------------------------------------------------
/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
2 |
3 | github: [cduck]
4 |
--------------------------------------------------------------------------------
/docs/img/01_line.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/drawsvg/native_animation/__init__.py:
--------------------------------------------------------------------------------
1 | from .synced_animation import (
2 | SyncedAnimationConfig,
3 | AnimatedAttributeTimeline,
4 | AnimationHelperData,
5 | animate_element_sequence,
6 | animate_text_sequence,
7 | )
8 | from .playback_control_ui import draw_scrub
9 |
--------------------------------------------------------------------------------
/docs/img/01_multilines.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_polygon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_tspan3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pHV.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pZ.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_tspan2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_trans.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_rot2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pL.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_rot.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_circ.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_tspan.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_ellip.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pT.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_multilines2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_cart1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_scale.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_scale2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_cart3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_dash.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_cart2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_lingrad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_join.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_mlimit.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_radgrad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/06_use.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_rota.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/example3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_fsc.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pA.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/example4.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_clip.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_linecap.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_rect.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/example8.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/01_rectround.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_rota2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_multiline_text.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_mutiline_text.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_scalcent.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pS.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_clip2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/07_skew.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_len.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pQ.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_fill.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_align.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/06_group.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/animated.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_weight.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2023 Casey Duckering
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 |
--------------------------------------------------------------------------------
/docs/img/02_foso.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_clip3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/drawsvg/jupyter.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 |
3 | from . import url_encode
4 | from . import raster
5 |
6 |
7 | class _Rasterizable:
8 | def rasterize(self, to_file=None):
9 | if to_file is not None:
10 | return raster.Raster.from_svg_to_file(self.svg, to_file)
11 | else:
12 | return raster.Raster.from_svg(self.svg)
13 |
14 | @dataclasses.dataclass
15 | class JupyterSvgInline(_Rasterizable):
16 | '''Jupyter-displayable SVG displayed inline on the Jupyter web page.'''
17 | svg: str
18 | def _repr_html_(self):
19 | return self.svg
20 |
21 | @dataclasses.dataclass
22 | class JupyterSvgImage(_Rasterizable):
23 | '''Jupyter-displayable SVG displayed within an img tag on the Jupyter web
24 | page.
25 | '''
26 | svg: str
27 | def _repr_html_(self):
28 | uri = url_encode.svg_as_utf8_data_uri(self.svg)
29 | return '
'.format(uri)
30 |
31 | @dataclasses.dataclass
32 | class JupyterSvgFrame:
33 | '''Jupyter-displayable SVG displayed within an HTML iframe.'''
34 | svg: str
35 | width: float
36 | height: float
37 | mime: str = 'image/svg+xml'
38 | def _repr_html_(self):
39 | uri = url_encode.svg_as_data_uri(self.svg, mime=self.mime)
40 | return (f'')
42 |
--------------------------------------------------------------------------------
/docs/img/05_mask3.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/02_strokewdth.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/example2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/04_path.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/05_mask2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/drawsvg/url_encode.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import urllib.parse
3 | import re
4 |
5 |
6 | STRIP_CHARS = ('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11'
7 | '\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f')
8 |
9 | def bytes_as_data_uri(data, strip_chars=STRIP_CHARS, mime='image/svg+xml'):
10 | '''Return a data URI with base64 encoding.'''
11 | b64 = base64.b64encode(data)
12 | return f'data:{mime};base64,{b64.decode(encoding="ascii")}'
13 |
14 | def svg_as_data_uri(txt, strip_chars=STRIP_CHARS, mime='image/svg+xml'):
15 | '''Return a data URI with base64 encoding, stripping unsafe chars for SVG.
16 | '''
17 | search = re.compile('|'.join(strip_chars))
18 | data_safe = search.sub(lambda m: '', txt)
19 | return bytes_as_data_uri(data_safe.encode(encoding='utf-8'), mime=mime)
20 |
21 | def svg_as_utf8_data_uri(txt, unsafe_chars='"', strip_chars=STRIP_CHARS,
22 | mime='image/svg+xml'):
23 | '''Returns a data URI without base64 encoding.
24 |
25 | The characters '#&%' are always escaped. '#' and '&' break parsing of
26 | the data URI. If '%' is not escaped, plain text like '%50' will be
27 | incorrectly decoded to 'P'. The characters in `strip_chars` cause the
28 | SVG not to render even if they are escaped.
29 | '''
30 | unsafe_chars = (unsafe_chars or '') + '#&%'
31 | replacements = {
32 | char: urllib.parse.quote(char, safe='')
33 | for char in unsafe_chars
34 | }
35 | replacements.update({
36 | char: ''
37 | for char in strip_chars
38 | })
39 | search = re.compile('|'.join(map(re.escape, replacements.keys())))
40 | data_safe = search.sub(lambda m: replacements[m.group(0)], txt)
41 | return f'data:{mime};utf8,{data_safe}'
42 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 | import logging
3 | logger = logging.getLogger(__name__)
4 |
5 | version = '2.4.0'
6 |
7 | try:
8 | with open('README.md', 'r') as f:
9 | long_desc = f.read()
10 | except:
11 | logger.warning('Could not open README.md. long_description will be set to None.')
12 | long_desc = None
13 |
14 | setup(
15 | name = 'drawsvg',
16 | packages = find_packages(),
17 | version = version,
18 | description = 'A Python 3 library for programmatically generating SVG (vector) images and animations. Drawsvg can also render to PNG, MP4, and display your drawings in Jupyter notebook and Jupyter lab.',
19 | long_description = long_desc,
20 | long_description_content_type = 'text/markdown',
21 | author = 'Casey Duckering',
22 | #author_email = '',
23 | url = 'https://github.com/cduck/drawsvg',
24 | download_url = 'https://github.com/cduck/drawsvg/archive/{}.tar.gz'.format(version),
25 | keywords = ['SVG', 'draw', 'graphics', 'iPython', 'Jupyter', 'widget', 'animation'],
26 | classifiers = [
27 | 'License :: OSI Approved :: MIT License',
28 | 'Programming Language :: Python :: 3',
29 | 'Framework :: IPython',
30 | 'Framework :: Jupyter',
31 | ],
32 | install_requires = [
33 | ],
34 | extras_require = {
35 | 'raster': [
36 | 'cairoSVG~=2.3',
37 | 'numpy~=1.16',
38 | 'imageio~=2.5',
39 | 'imageio_ffmpeg~=0.4',
40 | ],
41 | 'color': [
42 | 'pwkit~=1.0',
43 | 'numpy~=1.16',
44 | ],
45 | 'all': [
46 | 'cairoSVG~=2.3',
47 | 'numpy~=1.16',
48 | 'imageio~=2.5',
49 | 'imageio_ffmpeg~=0.4',
50 | 'pwkit~=1.0',
51 | ],
52 | },
53 | )
54 |
55 |
--------------------------------------------------------------------------------
/drawsvg/font_embed.py:
--------------------------------------------------------------------------------
1 | import urllib.request, urllib.parse
2 | import re
3 |
4 | from . import url_encode
5 |
6 |
7 | def download_url(url):
8 | with urllib.request.urlopen(url) as r:
9 | return r.read()
10 |
11 | def download_url_to_data_uri(url, mime='application/octet-stream'):
12 | data = download_url(url)
13 | return url_encode.bytes_as_data_uri(data, strip_chars='', mime=mime)
14 |
15 | def embed_css_resources(css):
16 | '''Replace all URLs in the CSS string with downloaded data URIs.'''
17 | regex = re.compile(r'url\((https?://[^)]*)\)')
18 | def repl(match):
19 | url = match[1]
20 | uri = download_url_to_data_uri(url)
21 | return f'url({uri})'
22 | embedded, _ = regex.subn(repl, css)
23 | return embedded
24 |
25 | def download_google_font_css(family, text=None, display='swap', **kwargs):
26 | '''Download SVG-embeddable CSS from Google fonts.
27 |
28 | Args:
29 | family: Name of font family or list of font families.
30 | text: The set of characters required from the font. Only a font subset
31 | with these characters will be downloaded.
32 | display: The font-display CSS value.
33 | **kwargs: Other URL parameters sent to
34 | https://fonts.googleapis.com/css?...
35 | '''
36 | if not isinstance(family, str):
37 | family = '|'.join(family) # Request a list of families
38 | args = dict(family=family, display=display)
39 | if text is not None:
40 | if not isinstance(text, str):
41 | text = ''.join(text)
42 | args['text'] = text
43 | args.update(kwargs)
44 | params = urllib.parse.urlencode(args)
45 | url = f'https://fonts.googleapis.com/css?{params}'
46 | with urllib.request.urlopen(url) as r:
47 | css = r.read().decode('utf-8')
48 | return embed_css_resources(css)
49 |
--------------------------------------------------------------------------------
/examples/example1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/img/03_pC.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/animated-fix-github.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/drawsvg/raster.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import io
3 | import warnings
4 |
5 | from .url_encode import bytes_as_data_uri
6 |
7 | def delay_import_cairo():
8 | try:
9 | import cairosvg
10 | except OSError as e:
11 | raise ImportError(
12 | 'Failed to load CairoSVG. '
13 | 'drawSvg will be unable to output PNG or other raster image '
14 | 'formats. '
15 | 'See https://github.com/cduck/drawsvg#full-feature-install '
16 | 'for more details.'
17 | ) from e
18 | except ImportError as e:
19 | raise ImportError(
20 | 'CairoSVG will need to be installed to rasterize images. '
21 | 'Install with `python3 -m pip install "drawsvg[all]"` '
22 | 'or `python3 -m pip install "drawsvg[raster]"`. '
23 | 'See https://github.com/cduck/drawsvg#full-feature-install '
24 | 'for more details.'
25 | ) from e
26 | return cairosvg
27 |
28 | def delay_import_imageio():
29 | try:
30 | import imageio
31 | except ImportError as e:
32 | raise ImportError(
33 | 'Optional dependencies not installed. '
34 | 'Install with `python3 -m pip install "drawsvg[all]"` '
35 | 'or `python3 -m pip install "drawsvg[raster]"`. '
36 | 'See https://github.com/cduck/drawsvg#full-feature-install '
37 | 'for more details.'
38 | ) from e
39 | return imageio
40 |
41 |
42 | class Raster:
43 | def __init__(self, png_data=None, png_file=None):
44 | self.png_data = png_data
45 | self.png_file = png_file
46 | def save_png(self, fname):
47 | with open(fname, 'wb') as f:
48 | f.write(self.png_data)
49 | @staticmethod
50 | def from_svg(svg_data):
51 | cairosvg = delay_import_cairo()
52 | png_data = cairosvg.svg2png(bytestring=svg_data)
53 | return Raster(png_data)
54 | @staticmethod
55 | def from_svg_to_file(svg_data, out_file):
56 | cairosvg = delay_import_cairo()
57 | cairosvg.svg2png(bytestring=svg_data, write_to=out_file)
58 | return Raster(None, png_file=out_file)
59 | @staticmethod
60 | def from_arr(arr, out_file=None):
61 | imageio = delay_import_imageio()
62 | if out_file is None:
63 | with io.BytesIO() as f:
64 | imageio.imwrite(f, arr, format='png')
65 | f.seek(0)
66 | return Raster(f.read())
67 | else:
68 | imageio.imwrite(out_file, arr, format='png')
69 | return Raster(None, png_file=out_file)
70 | def _repr_png_(self):
71 | if self.png_data:
72 | return self.png_data
73 | elif self.png_file:
74 | try:
75 | with open(self.png_file, 'rb') as f:
76 | return f.read()
77 | except TypeError:
78 | pass
79 | try:
80 | self.png_file.seek(0)
81 | return self.png_file.read()
82 | except io.UnsupportedOperation:
83 | pass
84 | def as_data_uri(self):
85 | if self.png_data:
86 | data = self.png_data
87 | else:
88 | try:
89 | with open(self.png_file, 'rb') as f:
90 | data = f.read()
91 | except TypeError:
92 | self.png_file.seek(0)
93 | data = self.png_file.read()
94 | return bytes_as_data_uri(data, mime='image/png')
95 |
--------------------------------------------------------------------------------
/drawsvg/__init__.py:
--------------------------------------------------------------------------------
1 | '''
2 | A library for creating SVG files or just drawings that can be displayed in
3 | Jupyter notebooks
4 |
5 | Example:
6 | ```
7 | import drawsvg as draw
8 |
9 | d = draw.Drawing(200, 100, origin='center')
10 |
11 | # Draw an irregular polygon
12 | d.append(draw.Lines(-80, 45,
13 | 70, 49,
14 | 95, -49,
15 | -90, -40,
16 | close=False,
17 | fill='#eeee00',
18 | stroke='black'))
19 |
20 | # Draw a rectangle
21 | r = draw.Rectangle(-80, -50, 40, 50, fill='#1248ff')
22 | r.append_title("Our first rectangle") # Add a tooltip
23 | d.append(r)
24 |
25 | # Draw a circle
26 | d.append(draw.Circle(-40, 10, 30,
27 | fill='red', stroke_width=2, stroke='black'))
28 |
29 | # Draw an arbitrary path (a triangle in this case)
30 | p = draw.Path(stroke_width=2, stroke='lime', fill='black', fill_opacity=0.2)
31 | p.M(-10, -20) # Start path at point (-10, -20)
32 | p.C(30, 10, 30, -50, 70, -20) # Draw a curve to (70, -20)
33 | d.append(p)
34 |
35 | # Draw text
36 | d.append(draw.Text('Basic text', 8, -10, -35, fill='blue')) # 8pt text at (-10, -35)
37 | d.append(draw.Text('Path text', 8, path=p, text_anchor='start', line_height=1))
38 | d.append(draw.Text(['Multi-line', 'text'], 8, path=p, text_anchor='end', center=True))
39 |
40 | # Draw multiple circular arcs
41 | d.append(draw.ArcLine(60, 20, 20, 60, 270,
42 | stroke='red', stroke_width=5, fill='red', fill_opacity=0.2))
43 | d.append(draw.Arc(60, 20, 20, 60, 270, cw=False,
44 | stroke='green', stroke_width=3, fill='none'))
45 | d.append(draw.Arc(60, 20, 20, 270, 60, cw=True,
46 | stroke='blue', stroke_width=1, fill='black', fill_opacity=0.3))
47 |
48 | # Draw arrows
49 | arrow = draw.Marker(-0.1, -0.51, 0.9, 0.5, scale=4, orient='auto')
50 | arrow.append(draw.Lines(-0.1, 0.5, -0.1, -0.5, 0.9, 0, fill='red', close=True))
51 | p = draw.Path(stroke='red', stroke_width=2, fill='none',
52 | marker_end=arrow) # Add an arrow to the end of a path
53 | p.M(20, 40).L(20, 27).L(0, 20) # Chain multiple path commands
54 | d.append(p)
55 | d.append(draw.Line(30, 20, 0, 10,
56 | stroke='red', stroke_width=2, fill='none',
57 | marker_end=arrow)) # Add an arrow to the end of a line
58 |
59 | d.set_pixel_scale(2) # Set number of pixels per geometry unit
60 | #d.set_render_size(400, 200) # Alternative to set_pixel_scale
61 | d.save_svg('example.svg')
62 | d.save_png('example.png')
63 |
64 | # Display in Jupyter notebook
65 | d.rasterize() # Display as PNG
66 | d # Display as SVG
67 | ```
68 | '''
69 |
70 | from .defs import *
71 | from .raster import Raster
72 | from .drawing import Drawing
73 | from .types import (
74 | Context,
75 | DrawingElement,
76 | DrawingBasicElement,
77 | DrawingParentElement,
78 | )
79 | from .elements import *
80 | from .video import (
81 | render_svg_frames,
82 | save_video,
83 | )
84 | from .frame_animation import (
85 | FrameAnimation,
86 | frame_animate_video,
87 | frame_animate_jupyter,
88 | frame_animate_spritesheet,
89 | )
90 | from .native_animation import (
91 | SyncedAnimationConfig,
92 | animate_element_sequence,
93 | animate_text_sequence,
94 | )
95 | from .url_encode import (
96 | bytes_as_data_uri,
97 | svg_as_data_uri,
98 | svg_as_utf8_data_uri,
99 | )
100 |
--------------------------------------------------------------------------------
/drawsvg/native_animation/playback_control_ui.py:
--------------------------------------------------------------------------------
1 | from .. import elements as draw
2 |
3 |
4 | def draw_scrub(config: 'SyncedAnimationConfig', hidden=False) -> 'Group':
5 | hpad = config.bar_hpad
6 | bar_x = config.controls_x + hpad
7 | bar_y = config.controls_center_y
8 | bar_width = config.controls_width - 2*hpad
9 | knob_rad = config.knob_rad
10 | pause_width = config.pause_width
11 | pause_corner_rad = config.pause_corner_rad
12 | g = draw.Group(id='scrub', visibility='hidden' if hidden else None)
13 | g.append(draw.Line(
14 | bar_x, bar_y, bar_x+bar_width, bar_y,
15 | stroke=config.bar_color,
16 | stroke_width=config.bar_thickness,
17 | stroke_linecap='round'))
18 | progress = draw.Rectangle(
19 | bar_x, bar_y, 0, 0.001,
20 | stroke=config.bar_past_color,
21 | stroke_width=config.bar_thickness,
22 | stroke_linejoin='round')
23 | g.append(progress)
24 | g_capture = draw.Group(
25 | id='scrub-capture',
26 | data_xmin=bar_x,
27 | data_xmax=bar_x+bar_width,
28 | data_totaldur=config.total_duration,
29 | data_startdelay=config.start_delay,
30 | data_enddelay=config.end_delay,
31 | data_pauseonload=int(bool(config.pause_on_load)))
32 | g_capture.append(draw.Rectangle(
33 | bar_x-config.bar_thickness/2, bar_y-config.controls_height/2,
34 | bar_width+config.bar_thickness, config.controls_height,
35 | fill='rgba(255,255,255,0)'))
36 | knob = draw.Circle(
37 | bar_x, bar_y, knob_rad, fill=config.knob_fill,
38 | id='scrub-knob',
39 | visibility='hidden')
40 | g_capture.append(knob)
41 | g.append(g_capture)
42 | g_play = draw.Group(id='scrub-play', visibility='hidden')
43 | g_play.append(draw.Rectangle(
44 | bar_x - hpad/2 - knob_rad/2 - pause_width/2 + pause_corner_rad,
45 | bar_y - pause_width/2 + pause_corner_rad,
46 | pause_width - pause_corner_rad*2,
47 | pause_width - pause_corner_rad*2,
48 | fill=config.pause_color,
49 | stroke=config.pause_color,
50 | stroke_width=pause_corner_rad*2,
51 | stroke_linejoin='round'))
52 | g_play.append(draw.Path(fill=config.pause_icon_color)
53 | .M(bar_x - hpad/2 - knob_rad/2 - pause_width/4,
54 | bar_y - pause_width/4)
55 | .v(pause_width/2)
56 | .l(pause_width/4*2, -pause_width/4)
57 | .Z())
58 | g.append(g_play)
59 | g_pause = draw.Group(id='scrub-pause', visibility='hidden')
60 | g_pause.append(draw.Rectangle(
61 | bar_x - hpad/2 - knob_rad/2 - pause_width/2 + pause_corner_rad,
62 | bar_y - pause_width/2 + pause_corner_rad,
63 | pause_width - pause_corner_rad*2,
64 | pause_width - pause_corner_rad*2,
65 | fill=config.pause_color,
66 | stroke=config.pause_color,
67 | stroke_width=pause_corner_rad*2,
68 | stroke_linejoin='round'))
69 | g_pause.append(draw.Rectangle(
70 | bar_x - hpad/2 - knob_rad/2 - pause_width/16*3,
71 | bar_y - pause_width/4,
72 | pause_width/8,
73 | pause_width/2,
74 | fill=config.pause_icon_color))
75 | g_pause.append(draw.Rectangle(
76 | bar_x - hpad/2 - knob_rad/2 + pause_width/16,
77 | bar_y - pause_width/4,
78 | pause_width/8,
79 | pause_width/2,
80 | fill=config.pause_icon_color))
81 | g.append(g_pause)
82 |
83 | progress.add_key_frame(0, width=0)
84 | progress.add_key_frame(config.duration, width=bar_width)
85 | knob.add_key_frame(0, cx=bar_x)
86 | knob.add_key_frame(config.duration, cx=bar_x+bar_width)
87 | return g
88 |
--------------------------------------------------------------------------------
/drawsvg/defs.py:
--------------------------------------------------------------------------------
1 | from .elements import DrawingElement, DrawingParentElement
2 |
3 |
4 | class DrawingDef(DrawingParentElement):
5 | '''Parent class of SVG nodes that must be direct children of .'''
6 | def get_svg_defs(self):
7 | return (self,)
8 |
9 | class DrawingDefSub(DrawingParentElement):
10 | '''Parent class of SVG nodes that are meant to be descendants of a Def.'''
11 | pass
12 |
13 | class LinearGradient(DrawingDef):
14 | '''
15 | A linear gradient to use as a fill or other color.
16 |
17 | Has nodes as children, added with `.add_stop()`.
18 | '''
19 | TAG_NAME = 'linearGradient'
20 | def __init__(self, x1, y1, x2, y2, gradientUnits='userSpaceOnUse',
21 | **kwargs):
22 | super().__init__(x1=x1, y1=y1, x2=x2, y2=y2,
23 | gradientUnits=gradientUnits, **kwargs)
24 | def add_stop(self, offset, color, opacity=None, **kwargs):
25 | stop = GradientStop(offset=offset, stop_color=color,
26 | stop_opacity=opacity, **kwargs)
27 | self.append(stop)
28 | return stop
29 |
30 | class RadialGradient(DrawingDef):
31 | '''
32 | A radial gradient to use as a fill or other color.
33 |
34 | Has nodes as children, added with `.add_stop()`.
35 | '''
36 | TAG_NAME = 'radialGradient'
37 | def __init__(self, cx, cy, r, gradientUnits='userSpaceOnUse', fy=None,
38 | **kwargs):
39 | super().__init__(cx=cx, cy=cy, r=r, gradientUnits=gradientUnits,
40 | fy=fy, **kwargs)
41 | def add_stop(self, offset, color, opacity=None, **kwargs):
42 | stop = GradientStop(offset=offset, stop_color=color,
43 | stop_opacity=opacity, **kwargs)
44 | self.append(stop)
45 | return stop
46 |
47 | class GradientStop(DrawingDefSub):
48 | '''A control point for a radial or linear gradient.'''
49 | TAG_NAME = 'stop'
50 | has_content = False
51 |
52 | class Pattern(DrawingDef):
53 | '''
54 | A repeating pattern of other drawing elements to use as a fill or other
55 | color.
56 |
57 | Width and height specify the repetition period. Append regular drawing
58 | elements to create the pattern.
59 | '''
60 | TAG_NAME = 'pattern'
61 | def __init__(self, width, height, x=None, y=None,
62 | patternUnits='userSpaceOnUse', **kwargs):
63 | super().__init__(width=width, height=height, x=x, y=y,
64 | patternUnits=patternUnits, **kwargs)
65 |
66 | class ClipPath(DrawingDef):
67 | '''
68 | A shape used to crop another element by not drawing outside of this shape.
69 |
70 | Has regular drawing elements as children.
71 | '''
72 | TAG_NAME = 'clipPath'
73 |
74 | class Mask(DrawingDef):
75 | '''
76 | A drawing where the gray value and transparency are used to control the
77 | transparency of another shape.
78 |
79 | Has regular drawing elements as children.
80 | '''
81 | TAG_NAME = 'mask'
82 |
83 | class Filter(DrawingDef):
84 | '''
85 | A filter to apply to geometry.
86 |
87 | For example a blur filter.
88 | '''
89 | TAG_NAME = 'filter'
90 |
91 | class FilterItem(DrawingDefSub):
92 | '''A child of Filter with any tag name.'''
93 | def __init__(self, tag_name, **args):
94 | super().__init__(**args)
95 | self.TAG_NAME = tag_name
96 |
97 | class Marker(DrawingDef):
98 | '''
99 | A small drawing that can be placed at the ends of (or along) a path.
100 |
101 | This can be used for arrow heads or points on a graph for example.
102 | By default, units are multiples of stroke width.
103 | '''
104 | TAG_NAME = 'marker'
105 | def __init__(self, minx, miny, maxx, maxy, scale=1, orient='auto',
106 | **kwargs):
107 | width = maxx - minx
108 | height = maxy - miny
109 | kwargs = {
110 | 'markerWidth': width if scale == 1 else float(width) * scale,
111 | 'markerHeight': height if scale == 1 else float(height) * scale,
112 | 'viewBox': '{} {} {} {}'.format(minx, miny, width, height),
113 | 'orient': orient,
114 | **kwargs,
115 | }
116 | super().__init__(**kwargs)
117 |
--------------------------------------------------------------------------------
/drawsvg/frame_animation.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from . import video
4 |
5 |
6 | class FrameAnimation:
7 | def __init__(self, draw_func=None, callback=None):
8 | self.frames = []
9 | if draw_func is None:
10 | draw_func = lambda d:d
11 | self.draw_func = draw_func
12 | if callback is None:
13 | callback = lambda d:None
14 | self.callback = callback
15 |
16 | def append_frame(self, frame):
17 | self.frames.append(frame)
18 | self.callback(frame)
19 |
20 | def draw_frame(self, *args, **kwargs):
21 | frame = self.draw_func(*args, **kwargs)
22 | self.append_frame(frame)
23 | return frame
24 |
25 | def save_video(self, file, **kwargs):
26 | video.save_video(self.frames, file, **kwargs)
27 |
28 | def save_spritesheet(self, file, **kwargs):
29 | video.save_spritesheet(self.frames, file, **kwargs)
30 |
31 |
32 | class FrameAnimationContext:
33 | def __init__(self, draw_func=None, out_file=None,
34 | jupyter=False, spritesheet=False, pause=False,
35 | clear=True, delay=0, disable=False, video_args=None,
36 | _patch_delay=0.05):
37 | self.jupyter = jupyter
38 | self.spritesheet = spritesheet
39 | self.disable = disable
40 | if self.jupyter and not self.disable:
41 | from IPython import display
42 | self._jupyter_clear_output = display.clear_output
43 | self._jupyter_display = display.display
44 | callback = self.draw_jupyter_frame
45 | else:
46 | callback = None
47 | self.anim = FrameAnimation(draw_func, callback=callback)
48 | self.out_file = out_file
49 | self.pause = pause
50 | self.clear = clear
51 | self.delay = delay
52 | if video_args is None:
53 | video_args = {}
54 | self.video_args = video_args
55 | self._patch_delay = _patch_delay
56 |
57 | def draw_jupyter_frame(self, frame):
58 | if self.clear:
59 | self._jupyter_clear_output(wait=True)
60 | self._jupyter_display(frame)
61 | if self.pause:
62 | # Patch. Jupyter sometimes clears the input field otherwise.
63 | time.sleep(self._patch_delay)
64 | input('Next?')
65 | elif self.delay != 0:
66 | time.sleep(self.delay)
67 |
68 | def __enter__(self):
69 | return self.anim
70 |
71 | def __exit__(self, exc_type, exc_value, exc_traceback):
72 | if exc_value is None:
73 | # No error
74 | if self.out_file is not None and not self.disable:
75 | if self.spritesheet:
76 | self.anim.save_spritesheet(self.out_file, **self.video_args)
77 | else:
78 | self.anim.save_video(self.out_file, **self.video_args)
79 |
80 |
81 | def frame_animate_video(out_file, draw_func=None, jupyter=False, **video_args):
82 | '''
83 | Returns a context manager that stores frames and saves a video when the
84 | context exits.
85 |
86 | Example:
87 | ```
88 | with frame_animate_video('video.mp4') as anim:
89 | while True:
90 | ...
91 | anim.draw_frame(...)
92 | ```
93 | '''
94 | return FrameAnimationContext(draw_func=draw_func, out_file=out_file,
95 | jupyter=jupyter, video_args=video_args)
96 |
97 | def frame_animate_spritesheet(out_file, draw_func=None, jupyter=False,
98 | **video_args):
99 | '''
100 | Returns a context manager that stores frames and saves a spritesheet when
101 | the context exits.
102 |
103 | Example:
104 | ```
105 | with frame_animate_spritesheet('sheet.png', row_length=10) as anim:
106 | while True:
107 | ...
108 | anim.draw_frame(...)
109 | ```
110 | '''
111 | return FrameAnimationContext(draw_func=draw_func, out_file=out_file,
112 | jupyter=jupyter, spritesheet=True,
113 | video_args=video_args)
114 |
115 |
116 | def frame_animate_jupyter(draw_func=None, pause=False, clear=True, delay=0.1,
117 | **kwargs):
118 | '''
119 | Returns a context manager that displays frames in a Jupyter notebook.
120 |
121 | Example:
122 | ```
123 | with frame_animate_jupyter(delay=0.5) as anim:
124 | while True:
125 | ...
126 | anim.draw_frame(...)
127 | ```
128 | '''
129 | return FrameAnimationContext(draw_func=draw_func, jupyter=True, pause=pause,
130 | clear=clear, delay=delay, **kwargs)
131 |
--------------------------------------------------------------------------------
/drawsvg/native_animation/playback_control_js.py:
--------------------------------------------------------------------------------
1 | SVG_ONLOAD = 'svgOnLoad(event)'
2 | SVG_JS_CONTENT = '''
3 | /* Animation playback controls generated by drawsvg */
4 | /* https://github.com/cduck/drawsvg/ */
5 | function svgOnLoad(event) {
6 | /* Support standalone SVG or embedded in HTML or iframe */
7 | if (event && event.target && event.target.ownerDocument) {
8 | svgSetup(event.target.ownerDocument);
9 | } else if (document && document.currentScript
10 | && document.currentScript.parentElement) {
11 | svgSetup(document.currentScript.parentElement);
12 | }
13 | }
14 | function svgSetup(doc) {
15 | var svgRoot = doc.documentElement || doc;
16 | var scrubCapture = doc.getElementById("scrub-capture");
17 | /* Block multiple setups */
18 | if (!scrubCapture || scrubCapture.getAttribute("svgSetupDone")) {
19 | return;
20 | }
21 | scrubCapture.setAttribute("svgSetupDone", true);
22 | var scrubContainer = doc.getElementById("scrub");
23 | var scrubPlay = doc.getElementById("scrub-play");
24 | var scrubPause = doc.getElementById("scrub-pause");
25 | var scrubKnob = doc.getElementById("scrub-knob");
26 | var scrubXMin = parseFloat(scrubCapture.dataset.xmin);
27 | var scrubXMax = parseFloat(scrubCapture.dataset.xmax);
28 | var scrubTotalDur = parseFloat(scrubCapture.dataset.totaldur);
29 | var scrubStartDelay = parseFloat(scrubCapture.dataset.startdelay);
30 | var scrubEndDelay = parseFloat(scrubCapture.dataset.enddelay);
31 | var scrubPauseOnLoad = parseFloat(scrubCapture.dataset.pauseonload);
32 | var paused = false;
33 | var dragXOffset = 0;
34 | var point = svgRoot.createSVGPoint();
35 |
36 | function screenToSvgX(p) {
37 | var matrix = scrubKnob.getScreenCTM().inverse();
38 | point.x = p.x;
39 | point.y = p.y;
40 | return point.matrixTransform(matrix).x;
41 | };
42 | function screenToProgress(p) {
43 | var matrix = scrubKnob.getScreenCTM().inverse();
44 | point.x = p.x;
45 | point.y = p.y;
46 | var x = point.matrixTransform(matrix).x;
47 | if (x <= scrubXMin) {
48 | return scrubStartDelay / scrubTotalDur;
49 | }
50 | if (x >= scrubXMax) {
51 | return (scrubTotalDur - scrubEndDelay) / scrubTotalDur;
52 | }
53 | return (scrubStartDelay/scrubTotalDur
54 | + (x - dragXOffset - scrubXMin)
55 | / (scrubXMax - scrubXMin)
56 | * (scrubTotalDur - scrubStartDelay - scrubEndDelay)
57 | / scrubTotalDur);
58 | };
59 | function currentScrubX() {
60 | return scrubKnob.cx.animVal.value;
61 | };
62 | function pause() {
63 | svgRoot.pauseAnimations();
64 | scrubPlay.setAttribute("visibility", "visible");
65 | scrubPause.setAttribute("visibility", "hidden");
66 | paused = true;
67 | };
68 | function play() {
69 | svgRoot.unpauseAnimations();
70 | scrubPause.setAttribute("visibility", "visible");
71 | scrubPlay.setAttribute("visibility", "hidden");
72 | paused = false;
73 | };
74 | function scrub(playbackFraction) {
75 | var t = scrubTotalDur * playbackFraction;
76 | /* Stop 10ms before end to avoid loop (>=1ms needed on FF) */
77 | var limit = scrubTotalDur - 10e-3;
78 | if (t < 0) t = 0;
79 | else if (t > limit) t = limit;
80 | svgRoot.setCurrentTime(t);
81 | };
82 | function mousedown(e) {
83 | svgRoot.pauseAnimations();
84 | if (e.target == scrubKnob) {
85 | dragXOffset = screenToSvgX(e) - currentScrubX();
86 | } else {
87 | dragXOffset = 0;
88 | }
89 | scrub(screenToProgress(e));
90 | /* Global document listeners */
91 | document.addEventListener('mousemove', mousemove);
92 | document.addEventListener('mouseup', mouseup);
93 | e.preventDefault();
94 | };
95 | function mouseup(e) {
96 | dragXOffset = 0;
97 | document.removeEventListener('mousemove', mousemove);
98 | document.removeEventListener('mouseup', mouseup);
99 | if (!paused) {
100 | svgRoot.unpauseAnimations();
101 | }
102 | e.preventDefault();
103 | };
104 | function mousemove(e) {
105 | scrub(screenToProgress(e));
106 | };
107 | scrubPause.addEventListener("click", pause);
108 | scrubPlay.addEventListener("click", play);
109 | scrubCapture.addEventListener("mousedown", mousedown);
110 | scrubContainer.setAttribute("visibility", "visible");
111 | scrubKnob.setAttribute("visibility", "visible");
112 | if (scrubPauseOnLoad) {
113 | pause();
114 | scrub(0);
115 | } else {
116 | play();
117 | }
118 | };
119 | svgOnLoad();
120 | '''
121 |
--------------------------------------------------------------------------------
/drawsvg/widgets/async_animation.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from ..drawing import Drawing
4 | from .drawing_widget import DrawingWidget
5 |
6 |
7 | class AsyncAnimation(DrawingWidget):
8 | '''A Jupyter notebook widget for asynchronously displaying an animation.
9 |
10 | Example:
11 | # Jupyter cell 1:
12 | widget = AsyncAnimation(fps=10)
13 | widget
14 | # [Animation is displayed here]
15 |
16 | # Jupyter cell 2:
17 | global_variable = 'a'
18 | @widget.set_draw_frame # Animation above is automatically updated
19 | def draw_frame(secs=0):
20 | # Draw something...
21 | d = draw.Drawing(300, 40)
22 | d.append(draw.Text(global_variable, 20, 0, 10))
23 | d.append(draw.Text(str(secs), 20, 30, 10))
24 | return d
25 |
26 | # Jupyter cell 3:
27 | global_variable = 'b' # Animation above now displays 'b'
28 |
29 | Attributes:
30 | fps: The animation frame rate (frames per second).
31 | draw_frame: A function that takes a single argument (animation time) and
32 | returns a Drawing.
33 | paused: While True, the animation will not run. Only the current frame
34 | will be shown.
35 | disable: While True, the widget will not be interactive and the
36 | animation will not update.
37 | click_pause: If True, clicking the drawing will pause or resume the
38 | animation.
39 | mousemove_pause: If True, moving the mouse up across the drawing will
40 | pause the animation and moving the mouse down will resume it.
41 | mousemove_y_threshold: Controls the sensitivity of mousemove_pause in
42 | web browser pixels.
43 | '''
44 |
45 | def __init__(self, fps=10, draw_frame=None, *, paused=False, disable=False,
46 | click_pause=True, mousemove_pause=False,
47 | mousemove_y_threshold=10):
48 | self._fps = fps
49 | self._paused = paused
50 | if draw_frame is None:
51 | def draw_frame(secs):
52 | return Drawing(0, 0)
53 | self._draw_frame = draw_frame
54 | self._last_secs = 0
55 | self.click_pause = click_pause
56 | self.mousemove_pause = mousemove_pause
57 | self.mousemove_y_threshold = mousemove_y_threshold
58 | self._start_time = 0
59 | self._stop_time = 0
60 | self._y_loc = 0
61 | self._y_max = 0
62 | self._y_min = 0
63 | if self._paused:
64 | frame_delay = -1
65 | else:
66 | frame_delay = 1000 // self._fps
67 | self._start_time = time.monotonic()
68 | initial_drawing = self.draw_frame(0)
69 | super().__init__(initial_drawing, throttle=True, disable=disable,
70 | frame_delay=frame_delay)
71 |
72 | # Register callbacks
73 | @self.mousedown
74 | def mousedown(self, x, y, info):
75 | if not self.click_pause:
76 | return
77 | self._y_min = self._y_max = self._y_loc
78 | self.paused = not self.paused
79 |
80 | @self.mousemove
81 | def mousemove(self, x, y, info):
82 | self._y_loc += info['movementY']
83 | if not self.mousemove_pause:
84 | self._y_min = self._y_max = self._y_loc
85 | return
86 | self._y_max = max(self._y_max, self._y_loc)
87 | self._y_min = min(self._y_min, self._y_loc)
88 | thresh = self.mousemove_y_threshold
89 | invert = thresh < 0
90 | thresh = max(0.01, abs(thresh))
91 | down_triggered = self._y_loc - self._y_min >= thresh
92 | up_triggered = self._y_max - self._y_loc >= thresh
93 | if down_triggered:
94 | self._y_min = self._y_loc
95 | if up_triggered:
96 | self._y_max = self._y_loc
97 | if invert:
98 | down_triggered, up_triggered = up_triggered, down_triggered
99 | if down_triggered:
100 | self.paused = False
101 | if up_triggered:
102 | self.paused = True
103 |
104 | @self.timed
105 | def timed(self, info):
106 | secs = time.monotonic() - self._start_time
107 | self.drawing = self.draw_frame(secs)
108 | self._last_secs = secs
109 |
110 | @self.on_exception
111 | def on_exception(self, e):
112 | self.paused = True
113 |
114 | @property
115 | def fps(self):
116 | return self._fps
117 |
118 | @fps.setter
119 | def fps(self, new_fps):
120 | self._fps = new_fps
121 | if self.paused:
122 | return
123 | self.frame_delay = 1000 // self._fps
124 |
125 | @property
126 | def paused(self):
127 | return self._paused
128 |
129 | @paused.setter
130 | def paused(self, new_paused):
131 | if bool(self._paused) == bool(new_paused):
132 | return
133 | self._paused = new_paused
134 | if self._paused:
135 | self.frame_delay = -1
136 | self._stop_time = time.monotonic()
137 | else:
138 | self._start_time += time.monotonic() - self._stop_time
139 | self.frame_delay = 1000 // self._fps
140 |
141 | @property
142 | def draw_frame(self):
143 | return self._draw_frame
144 |
145 | @draw_frame.setter
146 | def draw_frame(self, new_draw_frame):
147 | self._draw_frame = new_draw_frame
148 | if self.paused:
149 | # Redraw if paused
150 | self.drawing = self._draw_frame(self._last_secs)
151 |
152 | def set_draw_frame(self, new_draw_frame):
153 | self.draw_frame = new_draw_frame
154 | return new_draw_frame
155 |
--------------------------------------------------------------------------------
/drawsvg/widgets/drawing_javascript.py:
--------------------------------------------------------------------------------
1 | javascript = '''
2 | require.undef('drawingview');
3 |
4 | define('drawingview', ['@jupyter-widgets/base'], function(widgets) {
5 | class DrawingModel extends widgets.DOMWidgetModel {
6 | defaults() {
7 | return {
8 | ...super.defaults(),
9 | _model_name: DrawingModel.model_name,
10 | _model_module: DrawingModel.model_module,
11 | _model_module_version: DrawingModel.model_module_version,
12 | _view_name: DrawingModel.view_name,
13 | _view_module: DrawingModel.view_module,
14 | _view_module_version: DrawingModel.view_module_version,
15 | };
16 | }
17 | static serializers = {
18 | ...widgets.DOMWidgetModel.serializers,
19 | };
20 | static model_name = 'DrawingModel';
21 | static model_module = 'drawingview';
22 | static model_module_version = '0.1.0';
23 | static view_name = 'DrawingView';
24 | static view_module = 'drawingview';
25 | static view_module_version = '0.1.0';
26 | }
27 |
28 | class DrawingView extends widgets.DOMWidgetView {
29 | render() {
30 | this.container = document.createElement('a');
31 | this.image_changed();
32 | this.container.appendChild(this.svg_view);
33 | this.el.appendChild(this.container);
34 | this.model.on('change:_image', this.image_changed, this);
35 | this.model.on('change:_mousemove_blocked', this.block_changed,
36 | this);
37 | this.model.on('change:frame_delay', this.delay_changed,
38 | this);
39 | this.model.on('change:_frame_blocked', this.delay_changed,
40 | this);
41 | this.model.on('change:disable', this.delay_changed,
42 | this);
43 | this.delay_changed();
44 | }
45 | image_changed() {
46 | this.container.innerHTML = this.model.get('_image');
47 | this.svg_view = this.container.getElementsByTagName('svg')[0];
48 | this.cursor_point = this.svg_view.createSVGPoint();
49 | this.register_events();
50 | }
51 | last_move = null;
52 | last_mousemove_blocked = null;
53 | last_timer = null;
54 | block_changed() {
55 | var widget = this;
56 | window.setTimeout(function() {
57 | if (widget.model.get('_mousemove_blocked')
58 | != widget.last_mousemove_blocked && widget.last_move) {
59 | widget.send_mouse_event('mousemove', widget.last_move);
60 | }
61 | }, 0);
62 | }
63 | send_mouse_event(name, e) {
64 | this.last_move = null;
65 | if (this.model.get('disable')) {
66 | return;
67 | }
68 |
69 | this.cursor_point.x = e.clientX;
70 | this.cursor_point.y = e.clientY;
71 | var svg_pt = this.cursor_point.matrixTransform(
72 | this.svg_view.getScreenCTM().inverse());
73 |
74 | var target_parents = [];
75 | var target = e.target;
76 | while(target && target != this.svg_view)
77 | {
78 | if (target.id) {
79 | target_parents.push(target.id);
80 | }
81 | target = target.parentNode;
82 | }
83 |
84 | this.send({
85 | name: name,
86 | x: svg_pt.x,
87 | y: -svg_pt.y,
88 | type: e.type,
89 | button: e.button,
90 | buttons: e.buttons,
91 | shiftKey: e.shiftKey,
92 | altKey: e.altKey,
93 | ctrlKey: e.ctrlKey,
94 | metaKey: e.metaKey,
95 | clientX: e.clientX,
96 | clientY: e.clientY,
97 | movementX: e.movementX,
98 | movementY: e.movementY,
99 | timeStamp: e.timeStamp,
100 | targetId: e.target ? e.target.id : null,
101 | targetParentIds: target_parents,
102 | currentTargetId: e.currentTarget ? e.currentTarget.id : null,
103 | relatedTargetId: e.relatedTarget ? e.relatedTarget.id : null,
104 | });
105 | }
106 | delay_changed() {
107 | var widget = this;
108 | window.clearTimeout(widget.last_timer);
109 | if (widget.model.get('disable')) {
110 | return;
111 | }
112 | var delay = widget.model.get('frame_delay');
113 | if (delay > 0) {
114 | widget.last_timer = window.setTimeout(function() {
115 | widget.send_timed_event('timed');
116 | }, delay);
117 | }
118 | }
119 | send_timed_event(name) {
120 | if (this.model.get('disable')) {
121 | return;
122 | }
123 |
124 | this.send({
125 | name: name,
126 | });
127 | }
128 | register_events() {
129 | var widget = this;
130 | this.svg_view.addEventListener('mousedown', function(e) {
131 | e.preventDefault();
132 | widget.send_mouse_event('mousedown', e);
133 | });
134 | this.svg_view.addEventListener('mousemove', function(e) {
135 | e.preventDefault();
136 | if (widget.model.get('_mousemove_blocked')
137 | == widget.last_mousemove_blocked) {
138 | widget.last_move = e;
139 | } else {
140 | widget.send_mouse_event('mousemove', e);
141 | }
142 | });
143 | this.svg_view.addEventListener('mouseup', function(e) {
144 | e.preventDefault();
145 | widget.send_mouse_event('mouseup', e);
146 | });
147 | }
148 | }
149 |
150 | return {
151 | DrawingModel: DrawingModel,
152 | DrawingView: DrawingView
153 | };
154 | });
155 | '''
156 |
--------------------------------------------------------------------------------
/drawsvg/widgets/drawing_widget.py:
--------------------------------------------------------------------------------
1 | from ipywidgets import widgets
2 | from traitlets import Unicode, Bool, Int
3 |
4 |
5 | # Register front end javascript
6 | from IPython import display
7 | from . import drawing_javascript
8 | display.display(display.Javascript(drawing_javascript.javascript))
9 | del drawing_javascript
10 |
11 |
12 | class DrawingWidget(widgets.DOMWidget):
13 | _model_name = Unicode('DrawingModel').tag(sync=True)
14 | _model_module = Unicode('drawingview').tag(sync=True)
15 | _model_module_version = Unicode('0.1.0').tag(sync=True)
16 | _view_name = Unicode('DrawingView').tag(sync=True)
17 | _view_module = Unicode('drawingview').tag(sync=True)
18 | _view_module_version = Unicode('0.1.0').tag(sync=True)
19 | _image = Unicode().tag(sync=True)
20 | _mousemove_blocked = Int(0).tag(sync=True)
21 | _frame_blocked = Int(0).tag(sync=True)
22 | throttle = Bool(True).tag(sync=True)
23 | disable = Bool(False).tag(sync=True)
24 | frame_delay = Int(-1).tag(sync=True)
25 |
26 | def __init__(self, drawing, throttle=True, disable=False, frame_delay=-1):
27 | '''An interactive Jupyter notebook widget.
28 |
29 | This works similarly to displaying a Drawing as a cell output but
30 | DrawingWidget can register callbacks for user mouse events. Within a
31 | callback modify the drawing then call .refresh() to update the output in
32 | real time.
33 |
34 | Arguments:
35 | drawing: The initial Drawing to display. Call .refresh() after
36 | modifying or just assign a new Drawing.
37 | throttle: If True, limit the rate of mousemove events. For drawings
38 | with many elements, this will significantly reduce lag.
39 | disable: While True, mouse events will be disabled.
40 | frame_delay: If greater than or equal to zero, a timed callback will
41 | occur frame_delay milliseconds after the previous drawing
42 | update.
43 | '''
44 | super().__init__()
45 | self.throttle = throttle
46 | self.disable = disable
47 | self.frame_delay = frame_delay
48 | self.drawing = drawing
49 | self.mousedown_callbacks = []
50 | self.mousemove_callbacks = []
51 | self.mouseup_callbacks = []
52 | self.timed_callbacks = []
53 | self.exception_callbacks = []
54 |
55 | self.on_msg(self._receive_msg)
56 |
57 | @property
58 | def drawing(self):
59 | return self._drawing
60 |
61 | @drawing.setter
62 | def drawing(self, drawing):
63 | self._drawing = drawing
64 | self.refresh()
65 |
66 | def refresh(self):
67 | '''
68 | Redraw the displayed output with the current value of self.drawing.
69 | '''
70 | self._image = self.drawing.as_svg()
71 |
72 | def _receive_msg(self, _, content, buffers):
73 | if not isinstance(content, dict):
74 | return
75 | name = content.get('name')
76 | callbacks = {
77 | 'mousedown': self.mousedown_callbacks,
78 | 'mousemove': self.mousemove_callbacks,
79 | 'mouseup': self.mouseup_callbacks,
80 | 'timed': self.timed_callbacks,
81 | }.get(name, ())
82 | try:
83 | if callbacks:
84 | if name == 'timed':
85 | self._call_handlers(callbacks, content)
86 | else:
87 | self._call_handlers(callbacks, content.get('x'),
88 | content.get('y'), content)
89 | except BaseException as e:
90 | suppress = any(
91 | handler(self, e)
92 | for handler in self.exception_callbacks
93 | )
94 | if not suppress:
95 | raise
96 | finally:
97 | if name == 'timed':
98 | self._frame_blocked += 1
99 | else:
100 | self._mousemove_blocked += 1
101 |
102 |
103 | def mousedown(self, handler, remove=False):
104 | '''
105 | Register (or unregister) a handler for the mousedown event.
106 |
107 | Arguments:
108 | remove: If True, unregister, otherwise register.
109 | '''
110 | self.on_msg
111 | self._register_handler(
112 | self.mousedown_callbacks, handler, remove=remove)
113 |
114 | def mousemove(self, handler, remove=False):
115 | '''
116 | Register (or unregister) a handler for the mousemove event.
117 |
118 | Arguments:
119 | remove: If True, unregister, otherwise register.
120 | '''
121 | self._register_handler(
122 | self.mousemove_callbacks, handler, remove=remove)
123 |
124 | def mouseup(self, handler, remove=False):
125 | '''
126 | Register (or unregister) a handler for the mouseup event.
127 |
128 | Arguments:
129 | remove: If True, unregister, otherwise register.
130 | '''
131 | self._register_handler(
132 | self.mouseup_callbacks, handler, remove=remove)
133 |
134 | def timed(self, handler, remove=False):
135 | '''
136 | Register (or unregister) a handler for the timed event.
137 |
138 | Arguments:
139 | remove: If True, unregister, otherwise register.
140 | '''
141 | self._register_handler(
142 | self.timed_callbacks, handler, remove=remove)
143 |
144 | def on_exception(self, handler, remove=False):
145 | '''
146 | Register (or unregister) a handler for exceptions in other handlers.
147 |
148 | If any handler returns True, the exception is suppressed.
149 |
150 | Arguments:
151 | remove: If True, unregister, otherwise register.
152 | '''
153 | self._register_handler(
154 | self.exception_callbacks, handler, remove=remove)
155 |
156 | def _register_handler(self, callback_list, handler, remove=False):
157 | if remove:
158 | callback_list.remove(handler)
159 | else:
160 | callback_list.append(handler)
161 |
162 | def _call_handlers(self, callback_list, *args, **kwargs):
163 | for callback in callback_list:
164 | callback(self, *args, **kwargs)
165 |
--------------------------------------------------------------------------------
/drawsvg/color.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | try:
4 | import numpy as np
5 | import pwkit.colormaps
6 | except ImportError as e:
7 | raise ImportError(
8 | 'Optional dependencies not installed. '
9 | 'Install with `python3 -m pip install "drawsvg[all]"` '
10 | 'or `python3 -m pip install "drawsvg[color]"`. '
11 | 'See https://github.com/cduck/drawsvg#full-feature-install '
12 | 'for more details.'
13 | ) from e
14 |
15 |
16 | # Most calculations from http://www.chilliant.com/rgb2hsv.html
17 |
18 |
19 | def limit(v, low=0, high=1):
20 | return max(min(v, high), low)
21 |
22 | class Srgb:
23 | LUMA_WEIGHTS = (0.299, 0.587, 0.114)
24 | def __init__(self, r, g, b):
25 | self.r = float(r)
26 | self.g = float(g)
27 | self.b = float(b)
28 | def __iter__(self):
29 | return iter((self.r, self.g, self.b))
30 | def __repr__(self):
31 | return 'RGB({}, {}, {})'.format(self.r, self.g, self.b)
32 | def __str__(self):
33 | return 'rgb({}%,{}%,{}%)'.format(self.r*100, self.g*100, self.b*100)
34 | def luma(self, wts=None):
35 | if wts is None: wts = self.LUMA_WEIGHTS
36 | rw, gw, bw = wts
37 | return rw*self.r + gw*self.g + bw*self.b
38 | def to_srgb(self):
39 | return self
40 | @staticmethod
41 | def from_hue(h):
42 | h = h % 1
43 | r = abs(h * 6 - 3) - 1
44 | g = 2 - abs(h * 6 - 2)
45 | b = 2 - abs(h * 6 - 4)
46 | return Srgb(limit(r), limit(g), limit(b))
47 |
48 | class Hsl:
49 | def __init__(self, h, s, l):
50 | self.h = float(h) % 1
51 | self.s = float(s)
52 | self.l = float(l)
53 | def __iter__(self):
54 | return iter((self.h, self.s, self.l))
55 | def __repr__(self):
56 | return 'HSL({}, {}, {})'.format(self.h, self.s, self.l)
57 | def __str__(self):
58 | r, g, b = self.to_srgb()
59 | return 'rgb({}%,{}%,{}%)'.format(
60 | round(r*100, 2), round(g*100, 2), round(b*100, 2))
61 | def to_srgb(self):
62 | hs = Srgb.from_hue(self.h)
63 | c = (1 - abs(2 * self.l - 1)) * self.s
64 | return Srgb(
65 | (hs.r - 0.5) * c + self.l,
66 | (hs.g - 0.5) * c + self.l,
67 | (hs.b - 0.5) * c + self.l
68 | )
69 |
70 | class Hsv:
71 | def __init__(self, h, s, v):
72 | self.h = float(h) % 1
73 | self.s = float(s)
74 | self.v = float(v)
75 | def __iter__(self):
76 | return iter((self.h, self.s, self.v))
77 | def __repr__(self):
78 | return 'HSV({}, {}, {})'.format(self.h, self.s, self.v)
79 | def __str__(self):
80 | r, g, b = self.to_srgb()
81 | return 'rgb({}%,{}%,{}%)'.format(
82 | round(r*100, 2), round(g*100, 2), round(b*100, 2))
83 | def to_srgb(self):
84 | hs = Srgb.from_hue(self.h)
85 | c = self.v * self.s
86 | hp = self.h * 6
87 | x = c * (1 - abs(hp % 2 - 1))
88 | if hp < 1:
89 | r1, g1, b1 = c, x, 0
90 | elif hp < 2:
91 | r1, g1, b1 = x, c, 0
92 | elif hp < 3:
93 | r1, g1, b1 = 0, c, x
94 | elif hp < 4:
95 | r1, g1, b1 = 0, x, c
96 | elif hp < 5:
97 | r1, g1, b1 = x, 0, c
98 | else:
99 | r1, g1, b1 = c, 0, x
100 | m = self.v - c
101 | return Srgb(r1+m, g1+m, b1+m)
102 |
103 | class Sin:
104 | def __init__(self, h, s, l):
105 | self.h = float(h) % 1
106 | self.s = float(s)
107 | self.l = float(l)
108 | def __iter__(self):
109 | return iter((self.h, self.s, self.l))
110 | def __repr__(self):
111 | return 'Sin({}, {}, {})'.format(self.h, self.s, self.l)
112 | def __str__(self):
113 | r, g, b = self.to_srgb()
114 | return 'rgb({}%,{}%,{}%)'.format(
115 | round(r*100, 2), round(g*100, 2), round(b*100, 2))
116 | def to_srgb(self):
117 | h = self.h
118 | scale = self.s / 2
119 | shift = self.l #* (1-2*scale)
120 | return Srgb(
121 | shift + scale * math.cos(math.pi*2 * (h - 0/6)),
122 | shift + scale * math.cos(math.pi*2 * (h - 2/6)),
123 | shift + scale * math.cos(math.pi*2 * (h - 4/6)),
124 | )
125 |
126 | class Hcy:
127 | HCY_WEIGHTS = Srgb.LUMA_WEIGHTS
128 | def __init__(self, h, c, y):
129 | self.h = float(h) % 1
130 | self.c = float(c)
131 | self.y = float(y)
132 | def __iter__(self):
133 | return iter((self.h, self.c, self.y))
134 | def __repr__(self):
135 | return 'HCY({}, {}, {})'.format(self.h, self.c, self.y)
136 | def __str__(self):
137 | r, g, b = self.to_srgb()
138 | return 'rgb({}%,{}%,{}%)'.format(r*100, g*100, b*100)
139 | def to_srgb(self):
140 | hs = Srgb.from_hue(self.h)
141 | y = hs.luma(wts=self.HCY_WEIGHTS)
142 | c = self.c
143 | if self.y < y:
144 | c *= self.y / y
145 | elif y < 1:
146 | c *= (1 - self.y) / (1 - y)
147 | return Srgb(
148 | (hs.r - y) * c + self.y,
149 | (hs.g - y) * c + self.y,
150 | (hs.b - y) * c + self.y,
151 | )
152 | @staticmethod
153 | def _rgb_to_hcv(srgb):
154 | if srgb.g < srgb.b:
155 | p = (srgb.b, srgb.g, -1., 2./3.)
156 | else:
157 | p = (srgb.g, srgb.b, 0., -1./3.)
158 | if srgb.r < p[0]:
159 | q = (p[0], p[1], p[3], srgb.r)
160 | else:
161 | q = (srgb.r, p[1], p[2], p[0])
162 | c = q[0] - min(q[3], q[1])
163 | h = abs((q[3] - q[1]) / (6*c + 1e-10) + q[2])
164 | return (h, c, q[0])
165 | @classmethod
166 | def from_srgb(cls, srgb):
167 | hcv = list(cls._rgb_to_hcv(srgb))
168 | rw, gw, bw = cls.HCY_WEIGHTS
169 | y = rw*srgb.r + gw*srgb.g + bw*srgb.b
170 | hs = Srgb.from_hue(hcv[0])
171 | z = rw*hs.r + gw*hs.g + bw*hs.b
172 | if y < z:
173 | hcv[1] *= z / (y + 1e-10)
174 | else:
175 | hcv[1] *= (1 - z) / (1 - y + 1e-10)
176 | return Hcy(hcv[0], hcv[1], y)
177 |
178 | class Cielab:
179 | REF_WHITE = (0.95047, 1., 1.08883)
180 | def __init__(self, l, a, b):
181 | self.l = float(l)
182 | self.a = float(a)
183 | self.b = float(b)
184 | def __iter__(self):
185 | return iter((self.l, self.a, self.b))
186 | def __repr__(self):
187 | return 'CIELAB({}, {}, {})'.format(self.l, self.a, self.b)
188 | def __str__(self):
189 | r, g, b = self.to_srgb()
190 | return 'rgb({}%,{}%,{}%)'.format(
191 | round(r*100, 2), round(g*100, 2), round(b*100, 2))
192 | def to_srgb(self):
193 | in_arr = np.array((self.l, self.a, self.b))
194 | xyz = pwkit.colormaps.cielab_to_xyz(in_arr, self.REF_WHITE)
195 | lin_srgb = pwkit.colormaps.xyz_to_linsrgb(xyz)
196 | r, g, b = pwkit.colormaps.linsrgb_to_srgb(lin_srgb)
197 | return Srgb(r, g, b)
198 | @classmethod
199 | def from_srgb(cls, srgb, ref_white=None):
200 | if ref_white is None: ref_white = cls.REF_WHITE
201 | in_arr = np.array((*srgb,), dtype=float)
202 | lin_srgb = pwkit.colormaps.srgb_to_linsrgb(in_arr)
203 | xyz = pwkit.colormaps.linsrgb_to_xyz(lin_srgb)
204 | l, a, b = pwkit.colormaps.xyz_to_cielab(xyz, ref_white)
205 | return Cielab(l, a, b)
206 |
--------------------------------------------------------------------------------
/examples/playback-controls.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/playback-controls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
168 |
169 |