Skip to content

Commit

Permalink
tdoc.svg: Automatically scope styles to the image.
Browse files Browse the repository at this point in the history
  • Loading branch information
rblank committed Oct 10, 2024
1 parent 106638a commit bd1e150
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 78 deletions.
40 changes: 21 additions & 19 deletions docs/demo/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,31 @@ image creates an output block displaying the image.
:editable:
from tdoc import svg
def paint_heart(g):
g.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
def paint_heart(c):
c.path('M -40,-20 A 20,20 0,0,1 0,-20 A 20,20 0,0,1 40,-20 '
'Q 40,10 0,40 Q -40,10 -40,-20 z',
stroke='red', fill='transparent')
g.path('M -40,30 -30,30 -30,40 '
c.path('M -40,30 -30,30 -30,40 '
'M -30,30 0,0 M 34,-34 45,-45'
'M 35,-45 45,-45 45,-35',
stroke=svg.Stroke('black', width=2), fill='transparent')
img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff', id='graphics')
img.styles("""
#graphics {
width: 100%;
height: 100%;
polygon { fill: #c0ffc0; }
img = svg.Image(400, 100, stroke='darkorange', fill='#c0c0ff',
style='width: 100%; height: 100%')
img.styles = """
.bold {
stroke: blue;
stroke-width: 2;
fill: #c0ffc0;
}
""")
"""
img.circle(20, 30, 10)
img.ellipse(20, 70, 10, 20)
img.ellipse(20, 70, 10, 20, klass='bold')
img.line(0, 0, 400, 100)
img.polygon((200, 10), (230, 10), (240, 30))
img.polyline((200, 10), (230, 10), (240, 30), fill='transparent',
transform=svg.translate(x=50, y=10))
g = img.group(transform=svg.translate(200, 10))
g.polygon((0, 0), (30, 0), (40, 20), klass='bold')
g.polyline((0, 0), (30, 0), (40, 20), fill='transparent',
transform=svg.translate(x=50, y=10))
img.rect(0, 0, 400, 100, fill='transparent')
img.text(50, 90, "Some text", fill='green')
paint_heart(img.group(transform=svg.translate(360, 30).rotate(20).scale(0.5)))
Expand All @@ -124,12 +126,12 @@ import asyncio
import random
img = svg.Image(400, 100, style='width: 100%; height: 100%')
sym = img.symbol(id='heart')
sym = img.symbol()
paint_heart(sym)
hearts = [
(img.use(href='#heart'),
random.uniform(0, 100), random.uniform(0, 100), random.uniform(-180, 180))
for _ in range(20)]
hearts = [(img.use(href=f'#{sym.id}'),
random.uniform(0, 100), random.uniform(0, 100),
random.uniform(-180, 180))
for _ in range(20)]
def saw(value, amplitude):
return abs((value + amplitude) % (2 * amplitude) - amplitude)
Expand Down
11 changes: 11 additions & 0 deletions tdoc/common/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ def write(self, data, /):
line_buffering=True)


_next_id = 0

@public
def new_id():
"""Generate a unique ID, usable in id= attributes."""
global _next_id
v = _next_id
_next_id += 1
return f'tdoc-id-{v}'


@public
def render(html, name=''):
"""Render some HTML in an output block."""
Expand Down
106 changes: 47 additions & 59 deletions tdoc/common/python/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: MIT

import html
import tdoc


def esc(v, quote=True):
Expand Down Expand Up @@ -128,25 +129,35 @@ def skew_y(*args, **kwargs): return Transform().skew_y(*args, **kwargs)


class _Element:
_slots = ('stroke', 'fill', 'style', 'id')
_slots = ('_id', 'klass', 'style', 'stroke', 'fill')

def __init__(self, stroke, fill, style, id):
def __init__(self, *, klass=None, style=None, stroke=None, fill=None):
self._id, self.klass, self.style = None, klass, style
self.stroke, self.fill = Stroke(stroke), Fill(fill)
self.style, self.id = style, id

@property
def id(self):
if self._id is None: self._id = tdoc.new_id()
return self._id

@id.setter
def id(self, value):
self._id = value

def _attrs(self):
if (v := self._id) is not None: yield f' id="{esc(v)}"'
if (v := self.klass) is not None: yield f' class="{esc(v)}"'
if (v := self.style) is not None: yield f' style="{esc(v)}"'
yield from self.stroke
yield from self.fill
if (v := self.style) is not None: yield f' style="{esc(v)}"'
if (v := self.id) is not None: yield f' id="{esc(v)}"'


class _Shape(_Element):
_slots = _Element._slots + ('transform',)

def __init__(self, stroke, fill, style, id, transform):
super().__init__(stroke, fill, style, id)
def __init__(self, *, transform=None, **kwargs):
self.transform = transform
super().__init__(**kwargs)

def _attrs(self):
yield from super()._attrs()
Expand All @@ -156,10 +167,9 @@ def _attrs(self):
class Circle(_Shape):
__slots__ = _Shape._slots + ('x', 'y', 'r')

def __init__(self, x, y, r, *, stroke=None, fill=None, style=None, id=None,
transform=None):
def __init__(self, x, y, r, **kwargs):
self.x, self.y, self.r = x, y, r
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<circle cx="{esc(self.x)}" cy="{esc(self.y)}" r="{esc(self.r)}"'
Expand All @@ -170,10 +180,9 @@ def __iter__(self):
class Ellipse(_Shape):
__slots__ = _Shape._slots + ('x', 'y', 'rx', 'ry')

def __init__(self, x, y, rx, ry, *, stroke=None, fill=None, style=None,
id=None, transform=None):
def __init__(self, x, y, rx, ry, **kwargs):
self.x, self.y, self.rx, self.ry = x, y, rx, ry
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<ellipse cx="{esc(self.x)}" cy="{esc(self.y)}"'
Expand All @@ -185,10 +194,9 @@ def __iter__(self):
class Line(_Shape):
__slots__ = _Shape._slots + ('x1', 'y1', 'x2', 'y2')

def __init__(self, x1, y1, x2, y2, *, stroke=None, style=None, id=None,
transform=None):
def __init__(self, x1, y1, x2, y2, **kwargs):
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
super().__init__(stroke, None, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<line x1="{esc(self.x1)}" y1="{esc(self.y1)}"'
Expand All @@ -200,10 +208,9 @@ def __iter__(self):
class Path(_Shape):
__slots__ = _Shape._slots + ('path',)

def __init__(self, *path, stroke=None, fill=None, style=None, id=None,
transform=None):
def __init__(self, *path, **kwargs):
self.path = path
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<path d="'
Expand All @@ -219,10 +226,9 @@ def __iter__(self):


class _Poly(_Shape):
def __init__(self, *points, stroke=None, fill=None, style=None, id=None,
transform=None):
def __init__(self, *points, **kwargs):
self.points = points
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<{self._tag} points="{esc(' '.join(f'{p[0]},{p[1]}'
Expand All @@ -244,11 +250,10 @@ class Polyline(_Poly):
class Rect(_Shape):
__slots__ = _Shape._slots + ('x', 'y', 'width', 'height', 'rx', 'ry')

def __init__(self, x, y, width, height, *, rx=None, ry=None, stroke=None,
fill=None, style=None, id=None, transform=None):
def __init__(self, x, y, width, height, *, rx=None, ry=None, **kwargs):
self.x, self.y, self.width, self.height = x, y, width, height
self.rx, self.ry = rx, ry
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<rect x="{esc(self.x)}" y="{esc(self.y)}"'
Expand All @@ -262,10 +267,9 @@ def __iter__(self):
class Text(_Shape):
__slots__ = _Shape._slots + ('x', 'y', 'text')

def __init__(self, x, y, text, *, stroke='transparent', fill=None,
style=None, id=None, transform=None):
def __init__(self, x, y, text, *, stroke='transparent', **kwargs):
self.x, self.y, self.text = x, y, text
super().__init__(stroke, fill, style, id, transform)
super().__init__(stroke=stroke, **kwargs)

def __iter__(self):
yield f'<text x="{esc(self.x)}" y="{esc(self.y)}"'
Expand All @@ -276,10 +280,9 @@ def __iter__(self):
class Use(_Shape):
__slots__ = _Shape._slots + ('href', 'x', 'y')

def __init__(self, href, *, x=0, y=0, stroke=None, fill=None, style=None,
id=None, transform=None):
def __init__(self, href, *, x=0, y=0, **kwargs):
self.href, self.x, self.y = href, x, y
super().__init__(stroke, fill, style, id, transform)
super().__init__(**kwargs)

def __iter__(self):
yield f'<use href="{esc(self.href)}" '
Expand All @@ -288,16 +291,6 @@ def __iter__(self):
yield '/>'


class Styles:
__slots__ = ('styles',)

def __init__(self, styles):
self.styles = styles

def __iter__(self):
yield f'<style>{esc(self.styles)}</style>'


class _Container(_Shape):
_slots = _Shape._slots + ('children',)

Expand Down Expand Up @@ -345,9 +338,6 @@ def symbol(self, *args, **kwargs):
def use(self, *args, **kwargs):
return self.add(Use(*args, **kwargs))

def styles(self, *args, **kwargs):
return self.add(Styles(*args, **kwargs))

def __iter__(self):
yield f'<{self._tag}'
yield from self._attrs()
Expand All @@ -360,37 +350,35 @@ class Group(_Container):
__slots__ = _Container._slots
_tag = 'g'

def __init__(self, *, stroke=None, fill=None, style=None, id=None,
transform=None):
super().__init__(stroke, fill, style, id, transform)


class Symbol(_Container):
__slots__ = _Container._slots
_tag = 'symbol'

def __init__(self, *, stroke=None, fill=None, style=None, id=None,
transform=None):
super().__init__(stroke, fill, style, id, transform)

def _attrs(self):
yield from super()._attrs()
yield ' overflow="visible"'


class Image(_Container):
__slots__ = _Container._slots + ('width', 'height')
_tag = 'svg'
__slots__ = _Container._slots + ('width', 'height', 'styles')

def __init__(self, width, height, *, stroke=Stroke.default,
fill=Fill.default, style=None, id=None, transform=None):
super().__init__(stroke, fill, style, id, transform)
self.width, self.height = width, height
def __init__(self, width, height, *, styles=None, **kwargs):
self.width, self.height, self.styles = width, height, styles
super().__init__(**kwargs)

def _attrs(self):
yield ' xmlns="http://www.w3.org/2000/svg"'
yield ' xmlns:xlink="http://www.w3.org/1999/xlink"'
yield ' version="2"'
yield f' viewBox="0 0 {esc(self.width)} {esc(self.height)}"'
yield f' width="{esc(self.width)}" height="{esc(self.height)}"'
yield from super()._attrs()

def __iter__(self):
if styles := self.styles: id = self.id # Force the allocation of an ID
yield '<svg'
yield from self._attrs()
yield '>'
if styles: yield f'<style>\n#{id} {{{esc(styles)}}}\n</style>'
for child in self.children: yield from child
yield '</svg>'

0 comments on commit bd1e150

Please sign in to comment.