Deformation Basics

I’ve got a cool xy pen plotter AxiDraw v3 and I wanted to create some interesting drawings that showcased the fine detail possible with a really good pen Uniball Signo DX. The AxiDraw Plotter is driven by SVG plots, usually through Inkscape. Even though I can create some interesting SVG plots with Inkscape I thought that generating SVG files through Python might be an interesting opportunity to understand both SVG and Python a bit better. I chose to work with the drawSvg base library and see what I could accomplish by building my own library.

Here is an example if what I was able to create with this combination and I’m going to work through the process of creating the library that enabled me to create this.

SpiralDeformationPrintXY

This initial post will go through an effect that I have been interested in creating. For lack of a better name, I’ll call it a ‘deformation’. If I have a region of dots, and pick a point within the field, then push all the dots out from that center by a some proportional amount. In the picture above, the inner spirals have been plotted within a deformation.

So the rest of this post describes how I worked through this problem to create an initial implementation of a deformation.

Initial Setup

First I need to include the drawSvg package as well as my my SVG Deform repository

import drawSvg as Draw
from SVGDrawArea.DrawArea import DrawArea
from SVGDrawArea.Point import Point

Next define the drawing area and provide a nice grid background for reference.

extent = Point(200,200)
da = DrawArea(*extent)
da.grid_background(opacity=0.1, hlinestep = 25, vlinestep=25, centerpoint=2, stroke='black')
da.get_drawing()

svg

Since I will have a few cases where I want to clear the drawing area and start with a clean slate, I’ll define a clean_slate() function to make that happen.

def clean_slate():
    extent = Point(200,200)
    da = DrawArea(*extent)
    da.grid_background(opacity=0.1, hlinestep = 25, vlinestep=25, centerpoint=2, stroke='black')
    return da

Grid of dots

Now I will add a grid of green dots as a starting set of points to peform the deformation operation on. Note: range function includes start range, but does not include end of range so adding one step to end range results in inclusive points betwen 40 and 170.

Since I use this grid point generator a few times, I’ll put it in a function.

def point_grid():
    return (Point(x,y) for x in range(40,170,10) for y in range(40,170,10))

Then call the DrawArea points method with the point_grid to plot all the points.

da.points(point_grid(), r=2,fill='lightgreen',opacity=0.6)
da.get_drawing()

svg

My deformation will be centered at the center of the draw area, and will have a radius of 50. I’m going to define that circle and present it in a light red.

deformCenter = Point(100,100)
deformRadius = 50
da.points([deformCenter], r = deformRadius, fill='red',fill_opacity=0.1 )
da.get_drawing()

svg

The red circle is simply for reference on subsequent steps.

Points that will be moved

I need to identify the points that will be moved. This can be done with some trigonmetry and filter criteria. For each point in the field, determine if it is inside or outside the range of the deform circle. I could get close with a bounding box, but a bounding box would include points in the corners and I want to only move those points within the circle. I really want a bounding circle.

The calculation here is:

  • for each point under consideration (piter)
  • calculate distance between center point (deformCenter) and point under consideration (p)
  • collect all points where distance(deformCenter,p) <= deformRadius

For example, the distance between deformCenter: Point(100,100) and the Point(150,150) can be calculated by using the distance method on point. This wraps math.hypot see Pythagroean Theorem

deformCenter.distance(Point(150,150))

70.71067811865476

sqrt(502 + 502)=70.71

I found that the filterfalse function from itertools worked nicely for filtering out the points that were not inside the deform circle.

from itertools import filterfalse

boundedPoints = list(filterfalse(lambda p: deformCenter.distance(p) > deformRadius, point_grid()))

Note that I am converting the generator containing all the boundedPoints into a list list() this peforms the generator operation and saves a list of points to memory. Something that I have been trying to avoid, but in this case, a want to reference the list a few times so I decided it was worth it in this case. I could have also used the itertools.tee function to create two independent generators.

After filtering all the points that are within the range, I’ll plot these points back to the draw area with a blue fill.

da.points(boundedPoints, r = 1, fill='blue',fill_opacity=0.5 )
da.get_drawing()

svg

Moving points

Now that I know which points to move, the next thing to think about is how far to move them? As a first attempt, I’ll just move all of them out to the radius of the deformation circle… Let’s see what that looks like. I’ll start with the saved list of boundedPoints that will be moved.

da = clean_slate()
da.points(boundedPoints, r = 1, fill='blue',fill_opacity=0.5 )
da.get_drawing()

svg

For each boundedPoint, calculate the angle from the deform center to the bounded point. I’m using a function I wrote called angleDistTo which wraps math.atan2. A noteable consequence of calculating angle, distance tuples is that the point location is not kept. When converting back from an angle, radius to a point again, I have to define the point from which the angle and distance will be calculated.

help(Point.angleDistTo)
Help on function angleDistTo in module SVGDrawArea.Point:

angleDistTo(self, other:~T) -> [<class 'float'>, <class 'float'>]
    Calculate the angle and distance from self to other.  Return as tuple (angle,dist)
angleDistance = (deformCenter.angleDistTo(p) for p in boundedPoints)

angleDistance will contain all tuples from boundedPoints representing angle and distance from the deformCenter.

Then create a new point with the same angle with the distance set to deformRadius.

newPoints = (Point.fromAngleAbs(a,deformRadius) for a,_ in angleDistance)
da.points(newPoints, r = 2, fill='cyan',fill_opacity=0.5 )
da.get_drawing()

svg

Not what you expected? When calculating angle and distance between two points with angleDistTo, the result is relative to the origin point (0,0) But we want the new points to be relative to the boundedCenter In this case that is deformPoint. Here is the corrected code using a Point method fromAngle.

angleDistance = (deformCenter.angleDistTo(p) for p in boundedPoints)
newPoints = (deformCenter.fromAngle(a,deformRadius) for a,_ in angleDistance)
da.points(newPoints, r = 2, fill='cyan',fill_opacity=0.5 )
da.get_drawing()

svg

Push out to radius result

I’ll run the whole calculation pipline and show the final result.

Before deformation

da1 = clean_slate()
da1.points(point_grid(), r=2,fill='lightgreen',opacity=0.6)
da1.get_drawing()

svg

After deformation

da2 = clean_slate()
newPoints = (p if deformCenter.distance(p) > deformRadius 
             else deformCenter.fromAngle(deformCenter.angleDistTo(p)[0],deformRadius) 
             for p in point_grid())
da2.points(newPoints, r=2,fill='lightgreen',opacity=0.6)
da2.get_drawing()

svg

That looks like it accomplishes the goal, but I want to go one step further…

Proportonal Push Deformation

Just pusing all the points out to the deformation radius works, I wanted to try something a little more like a push out field. Here’s the same calculation, but rather than pushing everything out to the radius, the points are pushed out proportionally from the center to the edge. I like this effect a bit better.

def deformProportional(angle, radius, radiusMax):
    newRadius = (1 - radius/radiusMax) * 0.35 * radiusMax + radius
    return (angle, newRadius)
da3 = clean_slate()
newPoints = (p if deformCenter.distance(p) > deformRadius 
             else deformCenter.fromAngle(*deformProportional(*deformCenter.angleDistTo(p),deformRadius)) 
             for p in point_grid())
da3.points(newPoints, r=2,fill='lightgreen',opacity=0.6)
da3.get_drawing()

svg

More to come

This is just a starting point for other work that I’ll share in the next couple weeks.

Hope you enjoyed this thought experiment!