Write Inkscape extensions that modify objects
Tutorial – Tremble
Writing your own extension for Inkscape opens up a whole world of possibilities. Apart from creating new objects, you can modify existing objects and even animate them.
In last month's issue [1], we saw how to write an extension that rendered an object (a circle) in an Inkscape document. But apart from creating new objects, Inkscape extensions can also be used to modify existing objects. Let's see how this can be done be creating an extension that will generate "wobbly" animation. The idea is to take a given path – say, a piece of text – and to move its nodes around a little bit. Then save the result as a frame, move the nodes a little more, save again, and so on. When you put the frames together, it will give the impression that the text wobbles. This video shows how to do it in After Effects [2], but it seems like a lot of work for something that could be done with a relatively simple script.
Unfortunately, documentation and tutorials explaining how to create this type of script are all but nonexistent. Again the only way forward is to wade through source code and comments within code [3] to try and figure out what tools are available to achieve our ends.
What Is a Node?
Any object in Inkscape can be broken down into a bunch of paths, and paths, in turn, are made up by a bunch of nodes. So the key to modifying any object is modifying its nodes. But what is a node? You may think it is the point on a path where the path can change direction. In fact, that is just one control point, and a node is made up of three control points.
If you have used Inkscape to any extent, you will be familiar with this: Draw a Bézier line with several segments from top to bottom. Using the Edit Paths by Nodes tool, select all the nodes (Ctrl+A) and make the segments curve using the button in the toolbar. For added clarity, make the selected nodes symmetric, and you will end up with something like what you can see in Figure 1 (left).
The three control points, in order, are as follows: control point 0 is the control handle at the top, control point 1 is on the curve itself, and control point 2 is the handle at the bottom. If you drew the curve from bottom to top (Figure 1, right), the order would be inverted. Likewise if you drew from left to right, the first control point would be on the left, and drawing from right to left puts the first control point by default on the right.
Of course, you can grab the control points and move them anywhere you want, inverting their position, for example, but then you get loops and whorls in your path.
How do we know all this? Not through any documentation on Inkscape, but you can figure it out by experimenting with the extension shown in Listings 1 and 2.
Listing 1, cps.inx
, is a very basic interface to the extension. Read the sister article to this one to get the details [1]. In essence, it reads in three parameters, CPS0
, CPS1
,and CPS2
(lines 7, 8, and 9), one for each control point of each node. The parameters then get passed off to a Python script (line 19) that does the actual work.
Listing 1
cps.inx
01 <?xml version="1.0" encoding="UTF-8"?> 02 <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> 03 04 <name>CPS</name> 05 <id>org.linuxmagazine.inkscape.effects.cps</id> 06 07 <param name="CPS0" type="float" gui-text="CPS0:" min="-10" max="10">1</param> 08 <param name="CPS1" type="float" gui-text="CPS1:" min="-10" max="10">1</param> 09 <param name="CPS2" type="float" gui-text="CPS2:" min="-10" max="10">1</param> 10 11 <effect> 12 <object-type>path</object-type> 13 <effects-menu> 14 <submenu name="Modify Path"/> 15 </effects-menu> 16 </effect> 17 18 <script> 19 <command location="inx" interpreter="python">cps.py</command> 20 </script> 21 </inkscape-extension>
The cps.py
script (Listing 2) receives the parameters on lines 9, 10, and 11, and adds them to the coordinates of each of the control points of each node in the selected path (lines 24 to 29). Note that each node has an x and a y coordinate, so for example, the coordinates for control point 0 are csp [0][0]
(x coordinate) and csp [0][1]
(y coordinate).
Listing 2
cps.py
01 #!/usr/bin/env python 02 # coding=utf-8 03 04 import inkex 05 06 class CPS (inkex.EffectExtension): 07 08 def add_arguments (self, pars): 09 pars.add_argument ("--CPS0", type=float, default=1, help="CPS0") 10 pars.add_argument ("--CPS1", type=float, default=1, help="CPS1") 11 pars.add_argument ("--CPS2", type=float, default=1, help="CPS2") 12 13 def effect(self): 14 for node in self.svg.get_selected(inkex.PathElement): 15 path = node.path.to_superpath() 16 17 for subpath in path: 18 closed = subpath[0] == subpath[-1] 19 for index, csp in enumerate(subpath): 20 if closed and index == len(subpath) - 1: 21 subpath[index] = subpath[0] 22 break 23 else: 24 csp[0][0] += self.options.CPS0 25 csp[0][1] += self.options.CPS0 26 csp[1][0] += self.options.CPS1 27 csp[1][1] += self.options.CPS1 28 csp[2][0] += self.options.CPS2 29 csp[2][1] += self.options.CPS2 30 31 node.path = path 32 33 if __name__ == '__main__': 34 CPS().run()
Save cps.inx
and cps.py
to your $HOME/.config/inkscape/extensions
directory and the CPS… extension will appear under the Extensions | Modify Path menu next time you start Inkscape.
If, for example, you input 5 into the CPS0 field in the extension's dialog, the first handles of all the nodes in the selected path will move down and to the right five pixels, millimeters, or whatever unit you are using, from their original position as shown in Figure 2. Note that position (0, 0) is located by default at the upper left-hand corner of the page in Inkscape, so x coordinate plus five is five units to the right and y coordinate plus five is five units down.
Moving Nodes
The implication of all of the above is that, to move a node, you have to move each of its three control points (handles and position on curve) all at the same time … or not, if you want an even messier wobble.
This is what the Jitter Nodes… extension does. This extension is shipped by default with Inkscape and you can find its code in the /usr/share/inkscape/extensions
directory. Indeed, jitter.py
is the inspiration (read "I blatantly ripped it off") for Tremble, my own extension shown in Listings 3 and 4. Tremble is the extension that makes trembling, wibbly-wobbly animations out of selected objects.
The inx file, tremble.inx
(Listing 3), is pretty standard. It allows you to decide the amount of wobbliness to apply to each node (line 7), how many frames to generate (line 9), and where to save the frames (line 10).
Listing 3
tremble.inx
01 <?xml version="1.0" encoding="UTF-8"?> 02 <inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> 03 04 <name>Tremble</name> 05 <id>org.linuxmagazine.inkscape.effects.tremble</id> 06 07 <param name="radius" type="float" gui-text="Radius:">1</param> 08 <param name="accumulate" type="bool" gui-text="Accumulate changes">false</param> 09 <param name="frames" type="int" gui-text="Frames:" min="1" max="250"/> 10 <param name="folder" type="path" gui-text="Save frames in:" mode="folder"/> 11 12 <effect> 13 <object-type>path</object-type> 14 <effects-menu> 15 <submenu name="Modify Path"/> 16 </effects-menu> 17 </effect> 18 19 <script> 20 <command location="inx" interpreter="python">tremble.py</command> 21 </script> 22 </inkscape-extension>
The latter is the only new parameter type in this interface; all the rest we saw in the previous article. The path
parameters lets the user input or select a path to a file (mode = "file"
) or a folder (mode = "folder"
), either to load it into Inkscape or save something later to disk. You can give the user the option of opening several files (mode = "files"
) or folders (mode = "folders"
) at the same time or create a new file (mode = "file_new"
) or folder (mode = "folder_new"
).
The script that does the actual work, tremble.py
(Listing 4), is also not terribly complicated. Up in the heading, you import Inkscape's inkex
module, which contains many of the methods and attributes you need to write extensions. You will also need Python's random
module. Finally you will be using something from Inkscape's command
module.
Listing 4
tremble.py
01 #!/usr/bin/env python 02 # coding=utf-8 03 04 import random 05 import inkex 06 from inkex import command 07 08 class Tremble (inkex.EffectExtension): 09 10 def add_arguments (self, pars): 11 pars.add_argument ("--radius", type=float, default=1, help="Radius") 12 pars.add_argument ("--frames", type=int, default=1, help="Frames") 13 pars.add_argument ("--folder", type=str, help="Path") 14 pars.add_argument ("--accumulate", type=bool, help="Accumulate deformation") 15 16 def effect(self): 17 for node in self.svg.get_selected(inkex.PathElement): 18 path = node.path.to_superpath() 19 20 for frame in range (1, self.options.frames): 21 for subpath in path: 22 closed = subpath[0] == subpath[-1] 23 for index, csp in enumerate(subpath): 24 if closed and index == len(subpath) - 1: 25 subpath[index] = subpath[0] 26 break 27 else: 28 delta = random.uniform (-self.options.radius, self.options.radius) 29 csp[0][0] += delta 30 csp[0][1] += delta 31 csp[1][0] += delta 32 csp[1][1] += delta 33 csp[2][0] += delta 34 csp[2][1] += delta 35 36 node.path = path 37 command.write_svg(self.svg, self.options.folder + "/frame" + f'{frame:03}' + ".svg") 38 39 if __name__ == '__main__': 40 Tremble().run()
The command
module includes, among other things, some interesting methods, like take_snapshot ()
, which saves a bitmap snapshot of the current SVG loaded into Inkscape; and write_svg ()
, which saves the SVG generated by Inkscape to a file. You will use the latter a bit later in your script as you can see on line 37.
Inkscape's add_arguments ()
method (lines 10 to 14) is what Inkscape's interpreter expects to find when it needs to read in the parameters coming from the inx file. The --radius
parameter (line 11) is read into self.options.radius
, the --frames
parameter (line 12) is read into self.options.frames
, and so on. Notice how there is not a special type for the path held in the --folder
parameter: It is just a regular str
type.
The real action starts in the effect ()
method (lines 16 to 37), another of the standard methods that Inkscape's interpreter expects and what it runs when it is done reading in parameters.
The first thing you need to do is to grab the information from the selected object. Your script can find the nodes of the currently selected object using the get_selected ()
method from inx's svg
module (line 17). The parameter inkex.PathElement
tells get_selected ()
what sort of object it should expect, in this case, a path.
This allows you to loop over each path the nodes belong to and turn them into superpaths (line 18)!!! Okay … to be fair, there is nothing that much to get excited about here, despite the name. Superpaths are just an internal construct that makes it simpler for Inkscape's interpreter to calculate the changes that you will inflict on the path by modifying the node.
Let's hold line 20 for a moment and move on to line 21.
As each object can be made up of several paths (an object created from the letter "i" for example, has two paths: the body of the letter and the dot), you have to iterate over each subpath (line 21) and iterate over every control point of each individual node (line 23).
First, though, you have to check to see if the object is closed (line 22). The last node in a closed body coincides in its location with the first one, but internally the first and last nodes are two different items in the SVG markup. You check by comparing the first subpath (subpath [0]
) with the last subpath (subpath [-1]
). If they are the same, you must treat the object as closed (lines 24 to 26). This means that when you reach the last node, you need to skip messing with it and quit the loop (line 26), because you are done.
Just to get back to the to_superpath ()
method a second, it splits all the nodes into two parts: the index
, which you can see being used on lines 23, 24, and 25, is the number of the node (the first node is 0, the second is 1, etc.). This allows you to know when you have reached the last node in the subpath, as you can see in line 24.
The second part is the data regarding the control points, which we talked about above. Once you calculate the delta
that you are going to apply to each node (i.e., the amount by which you are going to move the node), you add it to each of the x and y coordinates of the handles (lines 29, 30 and 33, 34) and the point on the curve itself (lines 31 and 32).
Once you have dealt with all the nodes in all the subpaths, you can dump the superpath path
, with the modified values, back into nodes.path
. The changes get written to the internal SVG structure, and you will see the changes appear in your Inkscape drawing.
The script then saves the svg
object to the folder you chose on line 37.
Of course, you have to do this as many times as the number of frames you want. That is why you envelop the path-wobbling process in a loop on line 20.
Copy both tremble.inx
and tremble.py
to your $HOME/.config/share/inkscape/extensions
folder, and it will be ready to go the next time you start Inkscape.
Workflow
You make the most of this extension like this:
- Draw the thing you want to animate and select it. Convert it to a path by picking Path | Object to Path from the menus or by pressing Ctrl+Shift+C. If the object is made up of different bits, like different letters in a piece of text, make sure to combine them all together into one path by selecting all nodes (Ctrl+A) and choosing Path | Combine from the menus or by pressing Ctrl+K.
- If the object does not have many nodes, like a rectangle or a circle, you may want to add more to get more wobbliness. You can do this by hand by adding nodes at strategic points on the curves, or you can add nodes automatically using Extensions | Modify Path | Add Nodes….
Now is the moment to use Tremble. Make sure your object is selected, navigate to Extensions | Modify Path | Tremble…, fill in the form, and press Apply. A bunch of SVGs will pop up in the folder you chose (Figure 3). Note that, after running the extension, the object in Inkscape will be a version of your object that has been modified by the script. If you want to keep the original, do NOT save it! You will overwrite your original image. You can always press Ctrl+Z to undo the changes.
Most video editors will not load SVGs as a sequence, so you may want to convert your images into something they will accept, like PNGs. You can do that en masse by changing to the directory where your frames are located and running:
for i in *.svg; do convert $i ${i%svg}png; rm $i; done
Note this will delete the original SVGs. Get rid of rm $i
if that is not what you want.
- Use a video editor like Kdenlive or FFmpeg to convert the sequence of images into a movie.
- Impress your friends and family … or not.
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.