Plotting 4D Graphs With QOpenGLWidget

A 4D graph can be used for plotting a graph of a complex function, with complex numbers as its input and output. A complex number can be represented as a pair of two real numbers:

• A real part and an imaginary part.
• A magnitude and an argument.

An image has a width and a height, but you can add an illusion of depth using a linear transform and changes in hue. A common way to add the fourth dimension is to use colors to represent its value. If you use a color for the fourth dimension, you want to add a legend to tell the viewer how to estimate the value of the fourth coordinate.

A good example of one such graph is the graph of the Lambert function found in the Wikipedia.

The Lambert W function is defined as follows:

$x=ye^y\Rightarrow y=W_n(x)$

where $n \in \mathbb{Z}$ is the branch number.

In this post, I will discuss the use of Qt5 modules in creating my version of the graph:

The graph is not perfect and complete, but in this post I will explain how to create the components of the graph.

Combining OpenGL and 2D Painting

In Qt5, the OpenGL widget is a painting device like any other widget. So, you can use a painter of class QPainter to paint 2D shapes on it. The painter is useful if you want to add text to an image. Remember to unbind every GL object (vertex array objects, buffers, textures) before painting with the painter.

Before painting with the GL functions, call the painter’s method beginNativePainting. After finishing the use of GL functions, call endNativePainting

If you want the text rotation, you can use SVG. A simple way to add SVG text is using the QSvgRenderer class, that can create an SVG image from a file or a string.

Single Points

You can draw single points in OpenGL. To do so, you must enable GL_PROGRAM_POINT_SIZE using glEnable in your calling program, and define glPointSize in the vertex shader.

Matrices

The main types of matrices used in my example are:

• Frustum – A perspective matrix defined using the minimum and maximum values of coordinates.
• Viewport – a matrix that transforms the coordinates used by GL into the coordinates of the input rectangle. That transform will be used for placing the axis ticks snd texts.

The Program

The main window is a Qt Widget, so

The QMake File: qmake.pro:

The following definition will be used for creating the make file.

TARGET=executable
SOURCES=main.cpp

DESTDIR=bin
QT=core
QT+=gui
QT+=svg

greaterThan(QT_MAJOR_VERSION, 4): QT+=widgets


The header files ‘graph.h’ contains some definition to be used by the main program and the widget extending the QT Widget:

#include <QtWidgets/QOpenGLWidget>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLTexture>
#include <QPainter>
#include <QtSvg>
#include <QStyleOptionGraphicsItem>

struct graph_point {
GLfloat coords[3];
GLfloat hue;
};

struct vertex2D {
GLfloat coords[2];
GLfloat hue;
};

struct axis_ticks {
GLfloat rect_coords[3];
GLfloat texture_coords[2];
};

class GraphWidget:public QOpenGLWidget{
private:
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_point_vbo;
QOpenGLBuffer m_ibo;

QPainter m_painter;
QFont m_font;
int fontSize;
int m_width, m_height;
float legend_left;
QMatrix4x4 transform;
QMatrix4x4 viewport;

void populatePointBuffer(void);
void paintGraph(void);
void draw3DLine(QVector4D fromVec, QVector4D toVec);

public:
GraphWidget(QWidget *parent=nullptr);
~GraphWidget();
void initializeGL();
void paintGL();
void resizeGL(int w, int h);
};


HSV to RGB

The fourth coordinate of our graph represents arg(W(z)), an angle. In the graph we’ll use HSV (Hue, Saturation, Value) because the hue is given by the value of an angle. A shader-language function to covert from HSV to RGB is defined in file “shaders.h“:

#define TO_RGB_FUNC					\
"#version 140\n"					\
"vec4 to_rgb(float hue){ \n"				\
"float s=1.0;\n"					\
"float v=1.0;\n"					\
"float c=v*s;\n"					\
"float   x=c*(1-abs(mod(3*hue,2)-1));\n"		\
"float   m=v-c;\n"					\
"vec3  tempRGB=hue< -2./3?  vec3(0,x,c):\n"		\
"              hue< -1./3?  vec3(x,0,c): \n"		\
"              hue<     0?  vec3(c,0,x): \n"		\
"              hue<  1./3?  vec3(c,x,0): \n"		\
"              hue<  2./3?  vec3(x,c,0): \n"		\
"		              vec3(0,c,x); \n"		\
"\n"							\
"return vec4(tempRGB+vec3(m,m,m),1);\n"		\
"}\n"


The shader fragment for both the graph points and the legend is defined as follows:

#define FRAGMENT_SHADER				\
TO_RGB_FUNC					\
"varying float f_hue;\n"			\
"void main(void){\n"				\
"    gl_FragColor=to_rgb(f_hue);\n"		\
"}\n"


Main Program Includes and Definitions

#include <stdio.h>

#include <graph.h>
#include <QApplication>
#include "svg.h"
#include <complex>
#include <iostream>

#define MIN_WIDTH 640
#define MIN_HEIGHT 480

using namespace std;



Constructor and Destructor

GraphWidget::GraphWidget(QWidget *parent):
QOpenGLWidget(parent),
m_vao(),
m_point_vbo(),
m_ibo(QOpenGLBuffer::IndexBuffer),
m_font()
{
setGeometry(0,0,MIN_WIDTH,MIN_HEIGHT);
setMinimumWidth(MIN_WIDTH);
setMinimumHeight(MIN_HEIGHT);
}

GraphWidget::~GraphWidget(){
makeCurrent();
m_point_vbo.destroy();
m_ibo.destroy();
m_vao.destroy();
doneCurrent();

}

// NUM_ARGS - Args of complex numbers.
// NUM_MAGNS - Number of magnitudes.
#define NUM_ARGS 140
#define NUM_MAGNS 200


Initializing, Populating the Point Buffer

Here we’ll define the variables that are initialize once. The following function, initializeGL, is called once upon the widget’s initization:

void GraphWidget::initializeGL(){
makeCurrent();
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glEnable( GL_PROGRAM_POINT_SIZE );
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

m_vao.create();

m_point_vbo.create();
m_point_vbo.bind();
m_point_vbo.allocate(NUM_MAGNS*NUM_ARGS*sizeof(graph_point));
populatePointBuffer();

GLshort indices[6]={0,1,2,2,3,0};
m_ibo.create();
m_ibo.bind();
m_ibo.allocate(indices,sizeof(indices));
m_ibo.release();
m_point_vbo.release();
}


A call to glEnable( GL_PROGRAM_POINT_SIZE ); enables drawing single points.

The function populatePointBuffer uses the method ‘write‘ of class QOpenGLBuffer to write points to the vertex buffer. I use the buffer this way so I can use functions of complex variables.

void GraphWidget::populatePointBuffer(void){
int vboOffset=0;
graph_point vec[NUM_MAGNS];
for (float magn=0;magn<1.4;magn+=1.4/NUM_MAGNS){
int pos=0;
for (float arg=-1;arg<1;arg+=2./NUM_ARGS){
// The arg of W(Z) divided by pi.
complex<float> iarg;
iarg=complex<float>(0,M_PI*arg);
complex<float> w=magn*exp(iarg);
complex<float> z=w*exp(w);
vec[pos].hue=arg;
vec[pos].coords[0]=imag(z);
vec[pos].coords[1]=magn;
vec[pos].coords[2]=real(z);
pos+=1;
}
m_point_vbo.write(vboOffset,vec,sizeof(vec));
vboOffset+=sizeof(vec);
}
}


Resizing

The function resizeGL is called as a response to a widget resize event. In this function the values that change due to a resize event are set. The properties set here are:

• The font size for the painter
• The transform matrix
• The viewport matrix
void GraphWidget::resizeGL(int w, int h){
m_width=w;
m_height=h;
fontSize=12.* min((float)m_width/MIN_WIDTH,(float)m_height/MIN_HEIGHT);
m_font.setPixelSize(fontSize);
float z_far=0.05;
float z_near=0.01;
float a=2./(m_height-2*fontSize);
float b=1.8-a*m_height;
float y_floor=b-2.*a*fontSize;

// leftInPixels,RightInPixels - left and right boundaries
//                              of the graph.
float leftInPixels=12*fontSize+7;
float rightInPixels=m_width - 12 - 15*fontSize;
a=2/(rightInPixels-leftInPixels);
b=-1-a*leftInPixels;
float x_left=b;
float x_right=a*m_width+b;

transform.setToIdentity();

transform.frustum(x_left,x_right,y_floor,1.8,z_near,z_far);
transform.translate(0,0,-(z_far+z_near)/2);
transform.scale(1,1,(z_far-z_near)/2);
viewport.setToIdentity();
viewport.viewport(rect());
}


Painting

The function paintGL is called as a response to paint events. Following is the code of paintGL:

void GraphWidget::paintGL(){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_painter.begin(this);
m_painter.setPen(QColor(255,255,255,255));
m_painter.setFont(m_font);

paintGraph();

m_painter.end();
}


Let us look at the functions called by the paint event handler:

The function addLegend uses OpenGL function to create the HSV gradient and the painter to add ticks and text values:

void GraphWidget::addLegend(void){
QString str="arg(W(Z))";

QRect boundingRect=m_painter.boundingRect(rect(),0,str);

m_painter.beginNativePainting();

m_vao.bind();
QMatrix4x4 mat;
mat.ortho(rect());
GLfloat top=boundingRect.height()+20;
GLfloat bottom=top+0.75*m_height;
GLfloat left=m_width-boundingRect.width()-10;
GLfloat right=left+0.25*boundingRect.width();
legend_left = left; // Add it to the private members.
vertex2D rect_vertices[4]={{{left,top},1},{{right,top},1},
{{right,bottom},-1},{{left,bottom},-1}};
m_ibo.bind();
GLshort indices[6]={0,1,2,2,3,0};
m_ibo.write(0,indices,sizeof(indices));

m_program->bind();
m_program->setAttributeBuffer("legend_coords",
GL_FLOAT,
0,
2,
sizeof(vertex2D));
m_program->setAttributeBuffer("legend_hue",
GL_FLOAT,
sizeof(rect_vertices[0].coords),
1,
sizeof(vertex2D));
m_program->setUniformValue("ortho",mat);

m_vao.release();
m_vao.bind();
m_program->enableAttributeArray("legend_coords");
m_program->enableAttributeArray("legend_hue");
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,0);

m_vao.release();
m_program->disableAttributeArray("legend_coords");
m_program->disableAttributeArray("legend_hue");
m_program->release();
m_ibo.release();

m_painter.endNativePainting();
m_painter.drawText(m_width-boundingRect.width()-10,boundingRect.height()+10,str);

QString vals[7]={"pi","2pi/3","pi/3","0","-pi/3","-2pi/3","-pi"};
float curr_height=top;
float dist=(bottom-top)/6;
for (int i=0;i<7;i++){
m_painter.drawLine(right,curr_height,right+10,curr_height);
m_painter.drawText(right+15,curr_height+0.4*boundingRect.height(),vals[i]);
curr_height+=dist;
}
}


Following is the macro in shaders.h, which defines the vertex shader for the legend:

#define LEGEND_VERTEX_SHADER					\
"attribute vec2 legend_coords;\n"				\
"attribute float legend_hue;\n"				\
"uniform mat4 ortho;\n"					\
"varying float f_hue;\n"					\
"void main(void){\n"						\
"    gl_Position=ortho*vec4(legend_coords,0,1);\n"		\
"    f_hue=legend_hue;\n"					\
"}"


The function addAxes uses the painter and the viewport matrix to draw axes and add the ticks. You hove noticed that the matrices are of size 4×4 and the vectors are of length 4; when a matrix is multiplied by 4, you should divide coordinates x,y by coordinate w to find the vector’s location on the 2D window. Special treatment is added to the slanted z-axis, where SVG is used:

void GraphWidget::addAxes(void){

QVector4D startVec,endVec;

// Draw the x-axis
startVec=viewport*transform*QVector4D(-1,-0.2,1.1,1);
endVec=viewport*transform*QVector4D(1,-0.2,1.1,1);
draw3DLine(startVec,endVec);

// Draw the y-axis
startVec=viewport*transform*QVector4D(-1,-0.2,1.1,1);
endVec=viewport*transform*QVector4D(-1,1.4,1.1,1);
draw3DLine(startVec,endVec);

// Draw the z-axis
startVec=viewport*transform*QVector4D(1,-0.2,-1,1);
endVec=viewport*transform*QVector4D(1,-0.2,1.1,1);
draw3DLine(startVec,endVec);
}

void GraphWidget::draw3DLine(QVector4D fromVec, QVector4D toVec){
m_painter.drawLine(fromVec.x()/fromVec.w(),
m_height-fromVec.y()/fromVec.w(),
toVec.x()/toVec.w(),
m_height-toVec.y()/toVec.w());
}

QVector4D start_point,end_point;
QString vals[]={"-1","-0.5","0","0.5","1"};
int i=0;
for (float loc=-1;loc<=1;loc+=0.5){
start_point=viewport*transform*QVector4D(loc,-0.2,1.1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setY(end_point.y()+5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
m_painter.drawText(start_point.x(),end_point.y()+fontSize+1,vals[i++]);
}
const char *text="Im(Z)";
QVector4D start_vector=viewport*transform*QVector4D(-1,-0.2,1.1,1);
QVector4D end_vector=viewport*transform*QVector4D(1,-0.2,1.1,1);
float text_start=(start_vector.x()+end_vector.x())/(2.*end_vector.w())-2*fontSize;
float bottom=m_height-end_vector.y()/end_vector.w()+2*fontSize+5;
m_painter.drawText(text_start,bottom,text);
}

QVector4D start_point,end_point;
QString vals[]={"0.0","0.2","0.4","0.6","0.8","1.0","1.2","1.4"};
int i=0;
for (float loc=0;loc<=1.41;loc+=0.2){
start_point=viewport*transform*QVector4D(-1,loc,1.1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setX(end_point.x()-5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
m_painter.drawText(end_point.x()-2*fontSize,end_point.y()+0.5*fontSize,vals[i++]);

}
const char *text="|W(z)|";
QVector4D start_vector=viewport*transform*QVector4D(-1,-0.2,1.1,1);
QVector4D end_vector=viewport*transform*QVector4D(1,1,1.1,1);
float text_y_position=(start_vector.y()+end_vector.y())/(2.*end_vector.w())-2*fontSize;
float text_rightmost=start_vector.x()/start_vector.w()-6*fontSize;
m_painter.drawText(text_rightmost,text_y_position,text);
}

QVector4D start_point,end_point;
QString vals[]={"-1","-0.5","0","0.5","1"};
int i=0;
for (float loc=-1;loc<=1;loc+=0.5){
start_point=viewport*transform*QVector4D(1,-0.2,loc,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=start_point;
end_point.setY(end_point.y()-5);
m_painter.drawLine(start_point.x(),start_point.y(), end_point.x(),end_point.y());
if (i!=1){
m_painter.drawText(end_point.x()-fontSize/2.,end_point.y()-1,vals[i]);
}
i+=1;
}
}

char *svg_str=(char *)calloc(strlen(SVG_TEXT_STR)+1,sizeof(char));
const char *text="Re(Z)";
QVector4D start_point,end_point;
start_point=viewport*transform*QVector4D(1,-0.2,1,1);
start_point.setX(start_point.x()/start_point.w());
start_point.setY(m_height-start_point.y()/start_point.w());
end_point=viewport*transform*QVector4D(1,-0.2,-1,1);
end_point.setX(end_point.x()/end_point.w());
end_point.setY(m_height-end_point.y()/end_point.w());
float centerX=(start_point.x()+end_point.x())/2.;
float centerY=(start_point.y()+end_point.y())/2.;
float lenOfText=(strlen(text)-1.5)*fontSize;
float textX=centerX-lenOfText;
float textY=centerY+fontSize;

sprintf(svg_str,SVG_TEXT_STR,textX,textY,fontSize,inDegrees,centerX,centerY,text);
cout<<svg_str<<endl;
cout<<"Length of svg_str"<<strlen(svg_str)<<endl;
cout<<"Lenght of SVG_TEXT_STR"<<strlen(SVG_TEXT_STR)<<endl;

QSvgRenderer renderer((QByteArray(svg_str)));
renderer.render(&m_painter,rect());

}



The function add_z_text renders the SVG text using an object of call QSvgRendered. It accepts a byte array made from a string. It can also take a file name. The macro SVG_TEXT_STR is defined in file “svg.h” as follows:/code

#define SVG_TEXT_STR							\
"<svg>"								\
"    <text x='%.0f'" 							\
"          y='%.0f'"							\
"          font-size='%dpx'"						\
"          stroke='white'"						\
"          fill='white'"						\
"          transform='rotate(%.0f ,  %.0f ,  %.0f)'>%s</text>"	\
"</svg>"



Painting the Graph

Following is the function that paints the graph:

void GraphWidget::paintGraph(){
m_painter.beginNativePainting();
m_vao.bind();
m_point_vbo.bind();
m_program->bind();
m_program->setAttributeBuffer("graph_coords",
GL_FLOAT,
0,
3,
sizeof(graph_point));
m_program->setAttributeBuffer("hue",
GL_FLOAT,
3*sizeof(GLfloat),
1,
sizeof(graph_point));
m_program->setUniformValue("transform",transform);
m_vao.release();
m_vao.bind();
m_program->enableAttributeArray("graph_coords");
m_program->enableAttributeArray("hue");
glDrawArrays(GL_POINTS,0,NUM_ARGS*NUM_MAGNS);
m_program->release();
m_point_vbo.release();
m_vao.release();
m_painter.endNativePainting();
}


The Main Function

Following is the code of the main function:

int main(int argc, char *argv[]){
QApplication app(argc, argv);
GraphWidget w;
w.show();

return app.exec();
}


The Requested Gradient Cannot Be Found: Use the HTML5 Canvas

Some days ago I found that someone was looking for a D3.js expert. To prove one is an expert one has to pass a test, and one of the tasks on this test is to create a 3D color picker with RGB for axes.

The cube faces cannot be filled with a bi-dimensional linear gradients because such gradients are not supported. So, you have to explicitly write a loop to add the pixels.
Using SVG to add the pixels is a bad idea: SVG is an XML language, and uses a DOM tree. Using SVG will use a lot of memory and will slow down your computer. Use a canvas instead. Drawing on a canvas is done by simple Javascript commands, that add lines and shapes.
Following is a little code snippet that fills a cube face:

    for (i=x1; i<=x2;i++){
for (j=y1; j<=y2; j++){
rgb_arr[d.rgb_variable[0]]=i;
rgb_arr[d.rgb_variable[1]]=j;
ctx.fillStyle=d3.rgb(rgb_arr[0],rgb_arr[1],rgb_arr[2]);
ctx.fillRect(i,j,1,1);
}
}


Now, to get the color where the mouse points, first get the position using the mouse event’s offsetX and offsetY. These properties will hold the correct value even if the canvas is rotated.
Then you can get the RGBA values of the pixel using method getImageData of the canvas’ context. The method returns the data of a rectangle defined by 4 arguments: x,t,width and height.
Following is an example:


<pre class="wp-block-syntaxhighlighter-code">   canvas.on('click', function(evt){
var ctx=d3.event.target.getContext('2d');
var pixelData = ctx.getImageData(d3.event.offsetX,d3.event.offsetY,1,1).data;
});
</pre>


Teach Yourself D3.js

D3 is a JavaScript framework used for data visualization.
Learning how to work with D3.js is very easy. If you begin with the basics, you’ll see how easy it is to use D3, because:

• the DOM function names are much shorter than those in the legacy DOM library.
• the same functions are used for both setting and getting values.
• setters return a value, so you can keep working with the same element without repeating its name.

If you already know some framework, you probably feel that there’s nothing new under the sun, which is good because it makes learning the new stuff easier.

So, What’s New?

In D3, you can:
* add and remove SVG and HTML elements using the setters and getter.
* set and get the element’s properties, attributes, styles, text and… data

Getting Started

After you include it in your script, you can use selections, that is wrappers for elements.
To get a selection, use one of the functions:

• d3.select(selector) – to select the first element matching selector.
• d3.selectAll(selector) – to select the all elements matching selector.

Like the d3 object, selections has their select and selectAll methods to select descendant.

If selection is a selection, it’s easy to understand what selection.text(), selection.attribute(), selection.style(), selection.insert(), and selection.append() do. But, what does selection.data() do?

Data And Update Selection

The selection’s method data() helps you use the HTML DOM as a database.
selection.data() returns an array of all data that belongs to the elements of the selectionץ The data is held in the property __data__ of the element.
selection.data(arr, func) – defines an update selection and distribute the elements of array arr to the elements of selection.
func(d,i)is a function that:

• receives d an element of arr and its index i.
• returns a record key.

If func is not passed, the key is the element’s index.

A recommended way to create an update selection is:
var updSelection = selection1.selectAll(selector).data(arr)

Now, updSelection is a selection of existing elements whose data has been changed.
In addition, updSelection has to methods:

• updSelection.enter() – returns place holders for elements to be created under selection1.
• updSelection.exit()– returns elements that have lost their data, and are usually to be removed.

Using the data

The methods attr, style, property, text and data can accept a function as a value to set. The function will be called for each element of the selection’s data. And its arguments will be the array element and its index.

Now, let us see it in an example.
The following code arranges data in a donut layout:

<!DOCTYPE html>
<html>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Testing Pie Chart</title>
<script type="text/javascript" src="d3.min.js"></script>

<style type="text/css">
.slice text {
font-size: 12pt;
font-family: Arial;
}
</style>
<body>
<script type="text/javascript">
var width = 300,
height = 300,
color = d3.scale.category20c(); //builtin range of colors

// Step 1: creating a container for the donut chart, and attach raw data to it.
var vis = d3.select("body")
.append("svg")     //create the SVG element inside the <body>
.datum([{"label":"first", "value":20},
{"label":"second", "value":10},
{"label":"third", "value":30},
{"label":"fourth", "value":25}])
.attr({"width": width, "height": height})
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")") //move the center of the pie chart from 0, 0 to r, r

// Step two: defining the accessors of the layour function.
//           The function 'pie' will use the 'value' accessor to compute the arc's length.
var pie = d3.layout.pie()
.value(function(d) { return d.value; })
.sort(function(a,b){return a.label<b.label?-1:a.label==b.label?0:1;});

// Step 3: Create the update selection.
.data(pie);    // This will use the function 'pie' to create arc data from the row data attached to the visualisation element.

// Step 4: Using the data in the update selection to create shape elements.
var arc = d3.svg.arc()
;
.append('g')
.attr('class','slice')
.append("path")
.style("fill", function(d, i) { return color(i); } ) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc);                                     // Create the arc from the arc data attached to each element.

.attr("transform",
function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d, i) { return d.data.label; });

</script>
</body>
</html> 

And following is the chart:

For more funcions, see the API Reference.

Bézier Curves

The Bézier curve is a popular way to draw curves in graphic editors such as GIMP and Inkscape. A curve of degree n is defined using n+1 points, where the first and last are the start and end points of the curve, respectively, and the rest are control points.
For example:

The curve in the image above is a cubic Bézier curve. It has start and end points (filled with blue) and two control points (with no fill).
Each control point is attached by a straight line to a start or an end point, for a reason:

• The control points allows the user to control the curve intuitively.
• The straight line between the start(or end) point and its control point is tangent to the curve at the start(or end) point.

The Definition

A Bézier curve is defined as the collection of points that are the result of the function
B(t) for every t in [0,1].
A linear Bézier is simply a straight line between to points P0 and P1. The function is:
(1 – t)BP0 + tBP1

For n>1, Be P0, P1 … Pn the list of the curve’s points. Then the curve’s function is defined as
BP0P1…Pn(t) = (t – 1)BP0P1…Pn-1(t) + tBP1P2…Pn(t)

Or, in its explicit form:

(Not a very precise definition because 00 is not a number, so use the value 1 instead.)

This equation can be proved easily using the Pascal triangle.
From the explicit definition, you can see that the translation is done by adding the same coordinates to which of the curves start, end and control points.
because:
Rotations and translations are done by a transform matrix. So, if T is a transform matrix:
TBP1,P2,…Pn = BTP1,TP2,…TPn

Now, in a Bézier curve, BP0P1…Pn(t), The line P0 – P1 is tangent to the curve at point P0, and Pn – Pn-1 is tangent to the curve at point Pn

To prove this we’ll have to show that the derivative of a Bézier curve of degree n at the start and end points is a non-zero scalar multiplied by the difference between P1 and P0, and between Pn and Pn-1.
That scalar is n.

For n=1;
BP0,P1 = (1 – t)P0 + tP1
Let’s derive:
B’P0,P1 = -P0 + P1

Good!

Let’s assume it’s correct for n, and prove for n+1
BP0,P1…,Pn+1(t) = (1 – t)BP0,P1…,Pn(t) + tBP1,P2…,Pn+1(t)
Let’s derive:
B’P0,P1…,Pn+1(t) = -BP0,P1…,Pn(t) + (1-t)B’P0,P1…,Pn(t) + BP1,P2…,Pn+1(t) + tB’P1,P2…,Pn+1(t)

Now, to get the tangent to the curve at p0, let;s assign t=0:
B’P0,P1…,Pn+1(0) = -BP0,P1…,Pn(0) + B’P0,P1…,Pn(0) + BP1,P2…,Pn+1(0) =
= – P0 + n(P1 – P0) + P1 = (n+1)(P1 – P0)

Good!
Now, to get the tangent to the curve at p0, let;s assign t=1:
B’P0,P1…,Pn+1(1) = -BP0,P1…,Pn(1) + BP1,P2…,Pn+1(1) + B’P1,P2…,Pn+1(1) =
= – Pn + Pn+1 + n(Pn+1 – Pn) + P1 = (n+1)(Pn+1 – Pn)

QED

SVG supports Bézier curves of up to the third degree. A path consisting of such curves are good approximations of shapes provided that you have enough points.

ImageMagick Workaround for SVG Support

I have installed the PECL package IMagick in my computer. Its current version 7:6.6.2.6 -1 does a great job converting SVG files into raster images. In my hosting account the Imagemagick package is not the most recent: I have an SVG file that contained elemants repeated using the Inkscape’s spray can, but when I convert it using a test program in my host it only print one of the repeated stars.

How can I resolve this?

Well, as you probably know, SVG is an XML format, so I can read it using a text editor. Doing this I found that there’s an element named ‘use’ with an attribute named ‘href’. “href” we know from HTML, and the element’s named “use” makes sense when using an existing element. So I commented out all the “use” elements, and viewing the SVG file as an image I saw the star at the exact location as in the  erroneous raster image, but with other elements not printed there. Probably the SAX engine stopped after the first unknown element. Commented out all elements from the first “use” elements until the end of file, and BINGO! This reproduced the problem. Well, the “use” elements should be replaced by the original ones with changes to their attributes. The following code does it:

 /** * I found that the "use" tags are not supported, so I'm replacing them * by something else. * * The function returns the content of the file, the returned value can be the 1st argument of * Imagick::readImageBlob */ function svg_workaround($filename){$mimeType = mime_content_type($filename); if ($mimeType != 'text/xml' && mimeType!='image/svg+xml'){ // At home the mime type is "Image/svg+xml" in the hosting account "text/xml" return file_get_contents($filename); }$contents = file_get_contents($filename);$docRoot = new DOMDocument(); // For random access and fast location of referenced objects // you would use 'DOM'. if (!$docRoot->loadXML($contents)) return FALSE; $xlinkNs = getXLinkNs($docRoot); // For the "href" attribute. $svgElems =$docRoot->getElementsByTagName('svg'); if (!$svgElems->length) return$contents; $nodeList =$docRoot->getElementsByTagName('use'); // Because the node lists changes when elements are replaces, we keep the elements in an array. $nodes = array(); for ($i=0; $ilength;$i++) $nodes[]=$nodeList->item($i); foreach ($nodes as $node) if (!replace_use_tag($node, $docRoot,$xlinkNs)) return false; return $docRoot->saveXML(); } // The function that replaces the "use" elements function replace_use_tag($node, $doc,$xlinkNs){ $href=$node->getAttribute($xlinkNs . ':href'); if (!$href) $href=$node->getAttribute('href'); if (!$href) return FALSE;$id=substr($href, 1); // Removing the '#' from the href. if (!$id) return FALSE; // We use 'xpath' to locate a node with a given id because, unlike HTML, the use of getElementById // in XML DOM is not trivial. $xpath=new DOMXPath($doc); $elems=$xpath->query('//*[@id=\'' . $id . '\']');$orig=$elems->item(0); if (!$orig) return FALSE; $newNode=$orig->cloneNode(TRUE); // TRUE for deep cloning; When cloning a composite such as "g", // its child elements should be copied as well. // Copy attributes from the 'use' node to the new node, not including the 'href' $attributes=$node->attributes; $length=$attributes->length; for ($i=0;$iitem($i);$i++){ $attrName=$item->name; $attrValue=$item->value; if ($attrName=='href' ||$attrName==$xlinkNs . ':href') continue;$newNode->setAttribute($attrName,$attrValue); } $parent=$node->parentNode; $parent->replaceChild($newNode, $node); return TRUE; } // Get the namespace for XLink. function getXLinkNs($doc){ return \$doc->lookupPrefix('http://www.w3.org/1999/xlink'); }