Skip to content

Implemented text on a path SVG element#22

Merged
cduck merged 32 commits intocduck:masterfrom
marekyggdrasil:master
Feb 9, 2021
Merged

Implemented text on a path SVG element#22
cduck merged 32 commits intocduck:masterfrom
marekyggdrasil:master

Conversation

@marekyggdrasil
Copy link
Contributor

Regarding issue #21

Let me know if you like anything done differently. I modified README to add an example.

Copy link
Owner

@cduck cduck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing. The TextOnPath class works fine for a standalone implementation, but to integrate with the package, I would rather you add a path= argument to Text. Can you see if it's possible to make all the current text alignment options work on a path? (x and y could be an offset instead of an absolute position.) If this turns out to be too complex then the current solution is good.

@marekyggdrasil
Copy link
Contributor Author

marekyggdrasil commented Oct 4, 2020

Some progress

path = draw.Path(stroke='lightblue', fill='white')
path.M(50, 100-50).C(100, 100-0, 200, 100-100, 250, 100-50)
d.append(path)

x = 150
y = 50
text = draw.Text('Text on a path.', 24, x=x, y=y, path=path, center=False)

example8

path = draw.Path(stroke='lightblue', fill='white')
path.M(50, 100-50).C(100, 100-0, 200, 100-100, 250, 100-50)
d.append(path)

x = 150
y = 50
text = draw.Text('Text on a path.', 24, x=x, y=y, path=None, center=True)

example8

We should also consider adding textPath-specific attributes, especially startOffset and side seem very important, at least to me.
https://www.w3.org/TR/SVG/text.html#TextPathAttributes

But before that, first need to resolve an issue, I noticed that as long as we provide a manually defined path it works well, but if we provide say, a circle then it does not work

path = draw.Circle(150, 50, 45, stroke='lightblue', fill='white')
d.append(path)

x = None
y = None
text = draw.Text('Text on a path.', 24, x=x, y=y, path=path, center=False)

example8

@cduck
Copy link
Owner

cduck commented Oct 5, 2020

Good progress. What is the second output supposed to be? I would expect the text to follow the path but centered between the two ends.

I don't think SVG supports text on a <circle> element, only on a path representing a circle.

@marekyggdrasil
Copy link
Contributor Author

Well I think it should support it...

Screenshot 2020-10-06 at 9 26 34 PM

ref: https://www.w3.org/TR/SVG/text.html#TextPathAttributes

@cduck
Copy link
Owner

cduck commented Oct 6, 2020

That documentation says "a path referencing a circle". I assume this means a path element with two A commands (the A command draws an elliptical arc). Otherwise how do you control the start point?

If you want, you could add an additional feature to here that (implicitly or explicitly) converts any supported shape into a path to make your example above work.

@marekyggdrasil
Copy link
Contributor Author

@cduck I implemented attributes specific to textpath tag, however I see that the built-in rasterize function does not necessarily support them, see the example below:

import drawSvg as draw

len_x = 300
len_y = 300

d = draw.Drawing(len_x, len_y, origin='center', displayInline=False)

r = 50

cx = len_x/4
cy = len_y/4

ix = cx-r
iy = cy

fx = cx+r
fy = cy

d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

x = None
y = None

text = draw.Text('just some text', 10, x=x, y=y, path=path)
d.append(text)

cx = -len_x/4
cy = len_y/4

ix = cx-r
iy = cy

fx = cx+r
fy = cy

d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

text = draw.Text('10% offset', 10, x=x, y=y, path=path, startOffset='10%')
d.append(text)

cx = len_x/4
cy = -len_y/4

ix = cx-r
iy = cy

fx = cx+r
fy = cy

d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

text = draw.Text('inside text', 10, x=x, y=y, path=path, side='right', spacing='auto', method='stretch')
d.append(text)

cx = -len_x/4
cy = -len_y/4

ix = cx-r
iy = cy

fx = cx+r
fy = cy

d.append(draw.Circle(ix, iy, 1, stroke='green'))
d.append(draw.Circle(cx, cy, 1, stroke='red'))
d.append(draw.Circle(fx, fy, 1, stroke='blue'))

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

text = draw.Text('just some centered text', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='50%')
d.append(text)

d.setRenderSize(400)
d.rasterize()

d.saveSvg('examples/example8.svg')
d.savePng('examples/example8.png')

built-in rasterize renders:

example8

what I see in Firefox if I open the output svg

Screenshot 2020-10-09 at 5 19 27 PM

It seems like the side attribute has a poor browser compatibility anyway... ref: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/side

Maybe we could find some hack to make it work regardles? Like document to do path in reverse for example.

@cduck
Copy link
Owner

cduck commented Oct 9, 2020

That makes sense. rasterize uses CairoSVG which only supports SVG 1.1 but it looks like side is only in the SVG 2 draft.

I think it would be possible to create a path with the reverse direction to simulate that but I suggest leaving that to a later PR. Try to keep this PR focused on adding the base function of text on a path. I suggest adding Circle and side in follow-up PRs.

@marekyggdrasil
Copy link
Contributor Author

@cduck let me know what you think of this example, if it is not too long etc. I decided to stay with closed path examples as we keep the startOffset attribute.

import drawSvg as draw
import math

len_x = 300
len_y = 300

d = draw.Drawing(len_x, len_y, origin='center')

# radii of the circles
R = 85
r = 65

# x, y are None when path argument provided for Text node
x = None
y = None

# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2

# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset', 10, x=x, y=y, path=path, startOffset='10%')
d.append(text)

# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some text on a closed path', 10, x=x, y=y, path=path)
d.append(text)

# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 50% offset', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='50%')
d.append(text)

# text on an arbitrary path
ix = -len_x/3
iy = -len_y/4

path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/3, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text('it can be used for any path element, including paths that are not closed', 10, x=x, y=y, path=path)
d.append(text)

d.rasterize()

example8

I'll be happy to make further updates based on your feedback.

@marekyggdrasil marekyggdrasil requested a review from cduck October 10, 2020 09:01
@cduck
Copy link
Owner

cduck commented Oct 10, 2020

Looking good. Did you find a way to adjust the vertical position of the text relative to the path (e.g. have the center of each character on the path)? Or is that not possible?

I'll review the code when I have some time in the next few days.

@marekyggdrasil
Copy link
Contributor Author

That was a good question. I found a way to achieve that, basically:

  1. Now textPath can be a regular node if no dy or letter-spacing attributes are provided
  2. Otherwise it becomes a parent node and includes tspan as a child with dy and/or letter-spacing attributes
  3. I included letter-spacing to work also with regular text
  4. unfortunately if letter-spacing is provided then text-anchor="middle" doesn't necessarily work
import drawSvg as draw
import math

len_x = 300
len_y = 300

d = draw.Drawing(len_x, len_y, origin='center')

# radii of the circles
R = 85
r = 65

# x, y are None when path argument provided for Text node
x = None
y = None

# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2

# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset and increased letter spacing', 10, x=x, y=y, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)

# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some text on a closed path', 10, x=x, y=y, path=path)
d.append(text)

# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, x=x, y=y, path=path, text_anchor='middle', startOffset='75%', dy=7)
d.append(text)

# text on an arbitrary path
ix = -len_x/3
iy = -len_y/4

path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/3, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text('it can be used for any path element, including paths that are not closed', 10, x=x, y=y, path=path, dy=7)
d.append(text)

text = draw.Text('this is just a regular text without any path', 10, x=0, y=len_y/2-10, text_anchor='middle')
d.append(text)

text = draw.Text('regular text with increased letter spacing', 10, x=0, y=-len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)

d.rasterize()

example8

@marekyggdrasil
Copy link
Contributor Author

@cduck if you are not available for the review perhaps we should convert this PR into a draft?

Copy link
Owner

@cduck cduck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder. I shouldn't have left this for so long. I really appreciate your help on the project.

I have a lot of minor suggestions but overall it looks pretty good. The main thing I noticed when trying it out is this implementation doesn't handle the center argument or multi-line text.

When center=True, I would expect it to default to text_anchor='middle', startOffset='50%', and an appropriate emOffset (only for each that isn't specified).

I checked multi-line text on a path is supported by SVG. Just make sure the tspans added on line 436 are put in the textPath tag instead of the parent text tag.

README.md Outdated
Comment on lines 175 to 200
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to keep the example focused on the API of Text. I would inline these values where they are used and replace them with pre-computed constants where possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I will update with constants but it will do it after everything else is ready to merge, as it is now makes it possible for me to easily modify the example just in case if it will be required.

Comment on lines 419 to 421
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to clean up this bit and remove the use of translate by removing centerOffset and using emOffset instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like to take care of it in this PR or leave it for another PR? Since we are updating the Text node I think it is a great chance to clean-up a bit and make such improvements.

Comment on lines 468 to 472
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the previous comment. Maybe delete this and just use a TSpan.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not really sure how does TSpan help here. Wouldn't it create a child node in the SVG tree and break the structure I am trying to establish?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code does what we want but in a confusing way: "if this random attribute is set, don't write any content". If you just set encodedText to None or the empty string in __init__ I think it's more clear.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using TSpan when the text is a single line is optional and I'll leave the choice up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned-up a little bit. I can see how my implementation of this logic might be confusing but at same time I don't see clearly how TSpan makes is simpler. Let me know what you think of current version.

Regarding the multi-line texts on paths, still working on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

written a comment with few examples for multiline texts on paths

@marekyggdrasil
Copy link
Contributor Author

@cduck please take a look at solution for multi-line texts on path I propose

  1. if text is given as iterable and path is also an iterable, it will put each line on own path (good for circles)
  2. if text is just a text containing \n character and path is not an iterable, it will use translate transformation to shift the path by font size in y-direction

example:

text = draw.Text(
    [
        'just some text on a closed path worth with multiple',
        'lines, each given own path'],
    10, path=[
        path,
        nested_path])
d.append(text)

and

text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, path=path, dy=7)
d.append(text)

looks good in Firefox

Screenshot 2020-11-14 at 2 24 12 PM

but CairoSVG unfortunately is not capable of properly rendering it

example8

looking forward to hear your comments!

@cduck
Copy link
Owner

cduck commented Nov 15, 2020

Nice. I think the misaligned text by CairoSVG is caused by some bug/limitation in CairoSVG and unnecessary use of transforms. Also, while testing the API with different arguments, I got some errors.

You didn't give the code for your last example so I used

draw.Text(['just some centered text', 'on a closed path', 'more text'],
          10, dy=0, path=path, center=True)  # KeyError if `dy` is not given

Your code produces

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
     width="300" height="300" viewBox="-150.0 -150.0 300 300">
<defs>
<path d="M-138.6121593216773,-63.74999999999999 A65,65,90,1,1,-8.612159321677296,-63.74999999999999 A65,65,90,1,1,-138.6121593216773,-63.74999999999999" stroke="lightblue" fill="none" id="d0" />
</defs>
<use xlink:href="#d0" />
<circle cx="-138.6121593216773" cy="-63.74999999999999" r="1" stroke="blue" />

<text x="0" y="0" font-size="10" dy="0" text-anchor="middle">
    <textPath xlink:href="#d0" dy="0" transform="translate(0,0)" startOffset="0">
        <tspan dy="0" transform="translate(0,0)">just some centered text</tspan>
    </textPath>
    <textPath xlink:href="#d0" dy="10" transform="translate(0,10)" startOffset="0">
        <tspan dy="10" transform="translate(0,10)">on a closed path</tspan>
    </textPath>
    <textPath xlink:href="#d0" dy="20" transform="translate(0,20)" startOffset="0">
        <tspan dy="20" transform="translate(0,20)">more text</tspan>
    </textPath>
</text>

</svg>

Firefox:
Screen Shot 2020-11-15 at 17 18 -0600

This is the output I would expect but it doesn't give consistent results (x="0" aligns each line with the others):

...
<text id="test">
    <textPath xlink:href="#d0" font-size="10" text-anchor="middle" startOffset="50%">
        <tspan x="0" dy="-0.6em">just some text</tspan>
        <tspan x="0" dy="1em">on a closed path</tspan>
        <tspan x="0" dy="1em">more text</tspan>
    </textPath>
</text>
...

Firefox (correct):
Firefox rendering
Chrome/Safari (seems to center the full width before moving each line to the left):
Safari rendering
CairoSVG:
Safari rendering

This unfortunately also gives three inconsistent results:

...
<text font-size="10" text-anchor="middle">
    <textPath xlink:href="#d0" startOffset="50%">
        <tspan dy="-0.6em">just some centered text</tspan>
    </textPath>
    <textPath xlink:href="#d0" startOffset="50%">
        <tspan dy="0.4em">on a closed path</tspan>
    </textPath>
    <textPath xlink:href="#d0" startOffset="50%">
        <tspan dy="1.4em">more text</tspan>
    </textPath>
</text>
...

This is the only way I've found that works:

...
<g id="test">
<text>
    <textPath xlink:href="#d0" font-size="10" text-anchor="middle" startOffset="50%">
        <tspan dy="-0.6em">just some centered text</tspan>
    </textPath>
</text>
<text>
    <textPath xlink:href="#d0" font-size="10" text-anchor="middle" startOffset="50%">
        <tspan dy="0.4em">on a closed path</tspan>
    </textPath>
</text>
<text>
    <textPath xlink:href="#d0" font-size="10" text-anchor="middle" startOffset="50%">
        <tspan dy="1.4em">more text</tspan>
    </textPath>
</text>
</g>
...

Firefox/Chrome/Safari/CairoSVG:
Screen Shot 2020-11-15 at 17 14 -0600

Hope this helps.

@marekyggdrasil
Copy link
Contributor Author

Thanks a lot @cduck, I will look into that and sorry for not sharing the full source of the example, I just wanted to save up on information pollution in the thread... Unfortunately it turned out useful this time. I will investigate.

@marekyggdrasil
Copy link
Contributor Author

@cduck I made some updates, please check at your convenience

import drawSvg as draw
import math

len_x = 300
len_y = 300

d = draw.Drawing(len_x, len_y, origin='center')

# radii of the circles
R = 85
r = 65
rr = r - 10 # nested circle radius

# x, y are None when path argument provided for Text node
x = None
y = None

# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2

# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('text on a closed path with 10% offset and increased letter spacing', 10, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)

# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text(
    [
        'just some text on a closed path',
        'with multiple lines'],
    10, path=path)
d.append(text)

# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, path=path, text_anchor='middle', startOffset='75%', dy=7)
d.append(text)

# text on an arbitrary path
ix = -len_x/2
iy = -len_y/4

path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/4, len_y, len_x/3, -len_y, len_x-len_x/6, len_y/2)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, path=path, dy=1)
d.append(text)

text = draw.Text('this is just a regular text without any path', 10, x=0, y=len_y/2-10, text_anchor='middle')
d.append(text)

text = draw.Text('regular text with increased letter spacing', 10, x=0, y=-len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)

d.rasterize()

example8

Copy link
Owner

@cduck cduck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting better. The core feature works but there are some final details. Thanks for your patience.

  • dy shifts by pixels for single line text but by em for multi-line text.
  • center=True still causes weird misalignment and doesn't center on the path.
  • Drawing normal text without keyword arguments text no longer works: draw.Text('Text', 10, 0, 0)

Now that the API is mostly settled, we can also look at simplifying the example. I think your 4 or 5 examples can fit on about 20 lines of code. Each example should emphasize what one of the arguments to Text does.

If you want, I can do the final touches on the PR like making sure center works and everything is backwards compatible.

@marekyggdrasil
Copy link
Contributor Author

Thanks for your feedback @cduck and for offering to help out. I propose the following. Let me make a one more round of corrections and then you could give it a final touch and I'll simplify the example in parallel. If you agree for such plan please leave a "👍" under this comment.

@marekyggdrasil
Copy link
Contributor Author

@cduck just finished implementing the next round of changes.

  1. dy is in pixels but in case of multi-line text it is ignored, would that be expected behaviour? this is how I implemented it, if it is not what is required please take a look, I am not sure how to combine dy in pixels and em units for same element.
  2. center=True is fixed (example and screenshot below)
  3. I brought back the x and y arguments for backwards compatibility (example and screenshot below)

Let me know what do you think!

import drawSvg as draw
import math

len_x = 300
len_y = 300

d = draw.Drawing(len_x, len_y, origin='center')

# radii of the circles
R = 85
r = 65
rr = r - 10 # nested circle radius

# x, y are None when path argument provided for Text node
x = None
y = None

# centering circles around the whole canvas
ox = 0
oy = (len_y - (R*math.sin(math.pi/6) + R + 2*r))/2

# top right circle
cx = ox + R*math.cos(math.pi/6)
cy = oy + R*math.sin(math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, None, None, fx, fy)
path.A(r, r, 90, None, None, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('reversed direction text on a path with 10% offset and increased letter spacing', 10, None, None, path=path, startOffset='10%', letter_spacing=1.5)
d.append(text)

# top left circle
cx = ox + R*math.cos(5*math.pi/6)
cy = oy + R*math.sin(5*math.pi/6)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text(
    [
        'just some text on a closed path',
        'with multiple lines'],
    10, None, None, path=path)
d.append(text)

# bottom circle
cx = ox + R*math.cos(3*math.pi/2)
cy = oy + R*math.sin(3*math.pi/2)

ix = cx-r
iy = cy

fx = cx+r
fy = cy

path = draw.Path(stroke='lightblue', fill='none')
path.M(ix, iy)
path.A(r, r, 90, 1, 1, fx, fy)
path.A(r, r, 90, 1, 1, ix, iy)
d.append(path)

d.append(draw.Circle(ix, iy, 1, stroke='blue'))
text = draw.Text('just some centered text around 75% offset', 10, None, None, path=path, text_anchor='middle', startOffset='75%')
d.append(text)

# text on an arbitrary path
ix = -len_x/2
iy = -len_y/4

path = draw.Path(stroke='red', fill='none')
path.M(ix, iy).C(-len_x/2, 1.125*len_y, len_x/6, -len_y, len_x/2, len_y/4)
d.append(path)
d.append(draw.Circle(ix, iy, 1, stroke='blue'))

text = draw.Text('it can be used for any path element, including paths that are not closed\nand automatically shifts the path for consecutive lines', 10, None, None, path=path, dy=1, center=True)
d.append(text)

text = draw.Text('this is just a regular text without any path', 10, 0, len_y/2-10, text_anchor='middle')
d.append(text)

text = draw.Text('regular text with increased letter spacing', 10, 0, -len_y/2+7, text_anchor='middle', letter_spacing=1.25)
d.append(text)

text = draw.Text('backwards compatibility test', 10, 0, 0)
d.append(text)

d.rasterize()

example8

@marekyggdrasil
Copy link
Contributor Author

hi @cduck ! are we planning to merge it? Let me know if anything else is required from my side.

@cduck
Copy link
Owner

cduck commented Feb 5, 2021

Yes. Let me go through and merge the code this weekend. Then maybe you can write a simplified version of your example for the README.

@cduck cduck force-pushed the master branch 2 times, most recently from 6e5c9fe to e154e91 Compare February 7, 2021 03:08
@cduck
Copy link
Owner

cduck commented Feb 7, 2021

@marekyggdrasil I changed a lot of the code but kept the core design you implemented. I'm ready to merge it once you've taken a look.

Summary of changes:

  • Removed the legacy text transform I mentioned.
  • The units of dy were inconsistent so I renamed it to lineOffset.
  • Changed the Group of Text workaround to use new instead of hacking TAG_NAME.
  • Removed your long example and put a tiny example in. When you simplify the calculations in your example, just make a new PR.
  • Added separate pathArgs and tspanArgs instead of duplicating all the arguments to all the SVG tags.

@marekyggdrasil
Copy link
Contributor Author

Thank you for handling the changes. I have reviewed them and I am ok with merging. I will make a new PR with example. Awesome work!

@cduck cduck merged commit afd0de6 into cduck:master Feb 9, 2021
@cduck
Copy link
Owner

cduck commented Feb 9, 2021

Great. Thanks for all your hard work and patience on this. I just published version 1.8.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments