├── index.html
├── multi.html
├── mixture.html
├── predict.html
├── temperature.html
├── multi_svg.html
├── predict_svg.html
├── README.md
├── sketch_basic.js
├── sketch_temperature.js
├── sketch_mixture.js
├── sketch_predict_svg.js
├── sketch_predict.js
├── sketch_multi_svg.js
├── sketch_multi.js
├── model.js
└── p5.svg.js
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/multi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/mixture.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/predict.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/temperature.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/multi_svg.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/predict_svg.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rnn-tutorial
2 | RNN Tutorial for Artists
3 |
4 | 
5 |
6 | Suplementary material for [blog post](http://blog.otoro.net/2017/01/01/recurrent-neural-network-artist/).
7 |
8 | ## Usage ##
9 |
10 | In the html file, include the following files:
11 |
12 | ```html
13 |
14 |
15 |
16 |
17 |
18 | ```
19 |
20 | The sketch in `sketch_basic.js` is a basic example of how the Handwriting Model works. It is written with p5.js as an object hence allowing multiple sketches to be on the same page.
21 |
22 | ## License ##
23 |
24 | MIT
25 |
--------------------------------------------------------------------------------
/sketch_basic.js:
--------------------------------------------------------------------------------
1 | // Basic Example of Unconditional Handwriting Generation.
2 | var sketch = function( p ) {
3 | "use strict";
4 |
5 | // variables we need for this demo
6 | var dx, dy; // offsets of the pen strokes, in pixels
7 | var pen, prev_pen; // keep track of whether pen is touching paper
8 | var x, y; // absolute coordinates on the screen of where the pen is
9 |
10 | var rnn_state; // store the hidden states of rnn's neurons
11 | var pdf; // store all the parameters of a mixture-density distribution
12 | var temperature = 0.65; // controls the amount of uncertainty of the model
13 |
14 | var screen_width, screen_height; // stores the browser's dimensions
15 | var line_color;
16 |
17 | var restart = function() {
18 | // reinitialize variables before calling p5.js setup.
19 |
20 | // make sure we enforce some minimum size of our demo
21 | screen_width = Math.max(window.innerWidth, 480);
22 | screen_height = Math.max(window.innerHeight, 320);
23 |
24 | // start drawing from somewhere in middle of the canvas
25 | x = 50;
26 | y = p.random(100, screen_height-100);
27 |
28 | // initialize the scale factor for the model. Bigger -> large outputs
29 | Model.set_scale_factor(10.0);
30 |
31 | // initialize pen's states to zero.
32 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
33 |
34 | // randomize the rnn's initial states
35 | rnn_state = Model.random_state();
36 |
37 | // define color of line
38 | line_color = p.color(p.random(64, 224), p.random(64, 224), p.random(64, 224))
39 |
40 | };
41 |
42 | p.setup = function() {
43 | restart(); // initialize variables for this demo
44 | p.createCanvas(screen_width, screen_height);
45 | p.frameRate(60);
46 | p.background(255, 255, 255, 255);
47 | p.fill(255, 255, 255, 255);
48 | };
49 |
50 | p.draw = function() {
51 |
52 | // using the previous pen states, and hidden state, get next hidden state
53 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
54 |
55 | // get the parameters of the probability distribution (pdf) from hidden state
56 | pdf = Model.get_pdf(rnn_state);
57 |
58 | // sample the next pen's states from our probability distribution
59 | [dx, dy, pen] = Model.sample(pdf, temperature);
60 |
61 | // only draw on the paper if the pen is touching the paper
62 | if (prev_pen == 0) {
63 | p.stroke(line_color);
64 | p.strokeWeight(2.0);
65 | p.line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
66 | }
67 |
68 | // update the absolute coordinates from the offsets
69 | x += dx;
70 | y += dy;
71 |
72 | // update the previous pen's state to the current one we just sampled
73 | prev_pen = pen;
74 |
75 | // if the rnn starts drawing close to the right side of the canvas, reset demo
76 | if (x > screen_width - 50) {
77 | restart();
78 | }
79 |
80 | if (p.frameCount % 30 == 0) {
81 | p.background(255, 255, 255, 16); // fade out a bit.
82 | p.fill(255, 255, 255, 32);
83 | }
84 |
85 | };
86 |
87 | };
88 | var custom_p5 = new p5(sketch, 'sketch');
89 |
--------------------------------------------------------------------------------
/sketch_temperature.js:
--------------------------------------------------------------------------------
1 | // Basic Example of Unconditional Handwriting Generation.
2 | var sketch = function( p ) {
3 | "use strict";
4 |
5 | // variables we need for this demo
6 | var dx, dy; // offsets of the pen strokes, in pixels
7 | var pen, prev_pen; // keep track of whether pen is touching paper
8 | var x, y; // absolute coordinates on the screen of where the pen is
9 |
10 | var rnn_state; // store the hidden states of rnn's neurons
11 | var pdf; // store all the parameters of a mixture-density distribution
12 | var temperature = 0.65; // controls the amount of uncertainty of the model
13 |
14 | var screen_width, screen_height; // stores the browser's dimensions
15 | var line_color;
16 |
17 | var restart = function() {
18 | // reinitialize variables before calling p5.js setup.
19 |
20 | // make sure we enforce some minimum size of our demo
21 | screen_width = Math.max(window.innerWidth, 480);
22 | screen_height = Math.max(window.innerHeight, 320);
23 |
24 | // start drawing from somewhere in middle of the canvas
25 | x = 50;
26 | y = screen_height/2;
27 |
28 | // initialize the scale factor for the model. Bigger -> large outputs
29 | Model.set_scale_factor(10.0);
30 |
31 | // initialize pen's states to zero.
32 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
33 |
34 | // randomize the rnn's initial states
35 | rnn_state = Model.random_state();
36 |
37 | // define color of line
38 | line_color = p.color(255, 165, 0);
39 |
40 | };
41 |
42 | var generate = function() {
43 |
44 | p.noStroke();
45 | p.fill(255);
46 | p.rect(0, 0, screen_width, screen_height*0.105);
47 |
48 | p.fill(255, 165, 0, 128+127*temperature);
49 | p.rect(0, 0, screen_width*temperature/1.25, screen_height*0.10);
50 | p.textSize(40);
51 |
52 | p.text(Math.round(temperature*100)/100, screen_width*temperature/1.25+15, screen_height*0.10);
53 |
54 | }
55 |
56 | p.setup = function() {
57 | restart(); // initialize variables for this demo
58 | p.createCanvas(screen_width, screen_height);
59 | p.frameRate(60);
60 | p.background(255);
61 | p.fill(255);
62 | generate();
63 | };
64 |
65 | p.draw = function() {
66 |
67 | // using the previous pen states, and hidden state, get next hidden state
68 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
69 |
70 | // get the parameters of the probability distribution (pdf) from hidden state
71 | pdf = Model.get_pdf(rnn_state);
72 |
73 | // sample the next pen's states from our probability distribution
74 | [dx, dy, pen] = Model.sample(pdf, temperature);
75 |
76 | // only draw on the paper if the pen is touching the paper
77 | if (prev_pen == 0) {
78 | p.stroke(line_color);
79 | p.strokeWeight(2.0);
80 | p.line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
81 | }
82 |
83 | // update the absolute coordinates from the offsets
84 | x += dx;
85 | y += dy;
86 |
87 | // update the previous pen's state to the current one we just sampled
88 | prev_pen = pen;
89 |
90 | // if the rnn starts drawing close to the right side of the canvas, reset demo
91 | if (x > screen_width - 50) {
92 | restart();
93 | p.background(255); // fade out a bit.
94 | p.fill(255);
95 | generate();
96 | }
97 |
98 | };
99 |
100 | var touched = function() {
101 | var mx = p.mouseX;
102 | if (mx >= 0 && mx < screen_width) {
103 | temperature = 1.25*mx / screen_width;
104 | generate();
105 | }
106 | };
107 |
108 | p.touchMoved = touched;
109 | p.touchStarted = touched;
110 |
111 | };
112 | var custom_p5 = new p5(sketch, 'sketch');
113 |
--------------------------------------------------------------------------------
/sketch_mixture.js:
--------------------------------------------------------------------------------
1 | // Basic Example of Unconditional Handwriting Generation.
2 | var sketch = function( p ) {
3 | "use strict";
4 |
5 | // variables we need for this demo
6 |
7 | var temperature = 0.65; // controls the amount of uncertainty of the model
8 | var Nmix = 20;
9 | var screen_width;
10 | var screen_height;
11 |
12 | var Nbar = 401;
13 |
14 | var base_pi = new Array(Nmix);
15 | var base_mu = new Array(Nmix);
16 | var base_sigma = new Array(Nmix);
17 |
18 | var pi = new Array(Nmix);
19 | var mu = new Array(Nmix);
20 | var sigma = new Array(Nmix);
21 |
22 | var ybar = new Array(Nbar);
23 | var xbar = new Array(Nbar);
24 |
25 | var factor = 5.0;
26 |
27 | var erf_constant = 1/Math.sqrt(2*Math.PI);
28 |
29 | var gaussian = function(x, mean, std) {
30 | var f = erf_constant / std;
31 | var p = -1/2;
32 | var c = (x-mean)/std;
33 | c *= c;
34 | p *= c;
35 | return f * Math.pow(Math.E, p);
36 | };
37 |
38 | var create_base_distribution = function() {
39 | var i, pi_sample;
40 | var pi_sum = 0;
41 | for (i=0;i=0;i--) {
65 | x = z.get(i);
66 | x = Math.log(x) / temp;
67 | z.set(i, x);
68 | }
69 | x = z.max();
70 | z = nj.subtract(z, x);
71 | z = nj.exp(z);
72 | x = z.sum();
73 | z = nj.divide(z, x);
74 | //console.log("after="+z.get(0));
75 | var z_array = new Array(Nmix);
76 | for (i=0;i= 0 && mx < screen_width) {
156 | temperature = mx / screen_width;
157 | generate();
158 | }
159 | };
160 |
161 | p.touchMoved = touched;
162 | p.touchStarted = touched;
163 |
164 | };
165 | var custom_p5 = new p5(sketch, 'sketch');
166 |
--------------------------------------------------------------------------------
/sketch_predict_svg.js:
--------------------------------------------------------------------------------
1 | // svg version.
2 |
3 | // variables we need for this demo
4 | var dx, dy; // offsets of the pen strokes, in pixels
5 | var pen, prev_pen; // keep track of whether pen is touching paper
6 | var x, y; // absolute coordinates on the screen of where the pen is
7 | var temperature = 0.25; // controls the amount of uncertainty of the model
8 | var rnn_state; // store the hidden states of rnn's neurons
9 | var pdf; // store all the parameters of a mixture-density distribution
10 |
11 | var has_started = false; // set to true after user starts writing.
12 | var epsilon = 2.0; // to ignore data from user's pen staying in one spot.
13 |
14 | var screen_width, screen_height; // stores the browser's dimensions
15 |
16 | var predict_len = 10; // predict the next N steps constantly
17 |
18 | var restart = function() {
19 | // reinitialize variables before calling p5.js setu
20 |
21 | // make sure we enforce some minimum size of our demo
22 | screen_width = Math.max(window.innerWidth, 480);
23 | screen_height = Math.max(window.innerHeight, 320)/1.0;
24 |
25 | x = 50; // start drawing 50 pixels from the left side of the canvas
26 | y = screen_height/2; // start drawing from the middle of the canvas
27 |
28 | has_started = false;
29 |
30 | // initialize the scale factor for the model. Bigger -> large outputs
31 | Model.set_scale_factor(10.0);
32 |
33 | // initialize pen's states to zero.
34 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
35 |
36 | // initialize the rnn's initial states to zero
37 | rnn_state = Model.random_state();
38 |
39 | };
40 |
41 | function setup() {
42 | restart(); // initialize variables for this demo
43 | createCanvas(screen_width, screen_height, SVG);
44 | frameRate(60);
45 | background(255, 255, 255, 255);
46 | fill(255, 255, 255, 255);
47 | }
48 |
49 | var predict_path = function() {
50 | var temp_state = Model.copy_state(rnn_state); // create a copy
51 | var temp_dx, temp_dy, temp_pen, temp_prev_pen=prev_pen;
52 | var temp_x = x;
53 | var temp_y = y;
54 | var c = color(255, 165, 0, 48);
55 |
56 | for (var i=0; i screen_width-50) {
94 | // save svg file.
95 | noLoop();
96 | save();
97 | }
98 | var dx0 = mouseX-x; // candidate for dx
99 | var dy0 = mouseY-y; // candidate for dy
100 | if (dx0*dx0+dy0*dy0 > epsilon*epsilon) { // only if pen is not in same area
101 | dx = dx0;
102 | dy = dy0;
103 | pen = 0;
104 |
105 | if (prev_pen == 0) {
106 | stroke(255,165,0);
107 | strokeWeight(1.0); // nice thick line
108 | line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
109 | }
110 |
111 | // update the absolute coordinates from the offsets
112 | x += dx;
113 | y += dy;
114 |
115 | // using the previous pen states, and hidden state, get next hidden state
116 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
117 | }
118 | } else { // pen is above the paper
119 | pen = 1;
120 | if (pen !== prev_pen) {
121 | // using the previous pen states, and hidden state, get next hidden state
122 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
123 | }
124 | }
125 |
126 | if (has_started) {
127 | // predict a sample for the next possible path at every frame update.
128 | predict_path(rnn_state);
129 | }
130 |
131 | // update the previous pen's state to the current one we just sampled
132 | prev_pen = pen;
133 |
134 | /*
135 | if (frameCount % 8 == 0) {
136 | background(255, 255, 255, 16); // fade out a bit.
137 | fill(255, 255, 255, 32);
138 | }
139 | */
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/sketch_predict.js:
--------------------------------------------------------------------------------
1 | // Basic Example of Unconditional Handwriting Generation.
2 | var sketch = function( p ) {
3 | "use strict";
4 |
5 | // variables we need for this demo
6 | var dx, dy; // offsets of the pen strokes, in pixels
7 | var pen, prev_pen; // keep track of whether pen is touching paper
8 | var x, y; // absolute coordinates on the screen of where the pen is
9 | var temperature = 0.25; // controls the amount of uncertainty of the model
10 | var rnn_state; // store the hidden states of rnn's neurons
11 | var pdf; // store all the parameters of a mixture-density distribution
12 |
13 | var has_started = false; // set to true after user starts writing.
14 | var epsilon = 2.0; // to ignore data from user's pen staying in one spot.
15 |
16 | var screen_width, screen_height; // stores the browser's dimensions
17 |
18 | var predict_len = 10; // predict the next N steps constantly
19 |
20 | var restart = function() {
21 | // reinitialize variables before calling p5.js setup.
22 |
23 | // make sure we enforce some minimum size of our demo
24 | screen_width = Math.max(window.innerWidth, 480);
25 | screen_height = Math.max(window.innerHeight, 320)/1.0;
26 |
27 | x = 50; // start drawing 50 pixels from the left side of the canvas
28 | y = screen_height/2; // start drawing from the middle of the canvas
29 |
30 | has_started = false;
31 |
32 | // initialize the scale factor for the model. Bigger -> large outputs
33 | Model.set_scale_factor(10.0);
34 |
35 | // initialize pen's states to zero.
36 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
37 |
38 | // initialize the rnn's initial states to zero
39 | rnn_state = Model.random_state();
40 |
41 | };
42 |
43 | p.setup = function() {
44 | restart(); // initialize variables for this demo
45 | p.createCanvas(screen_width, screen_height);
46 | p.frameRate(60);
47 | p.background(255, 255, 255, 255);
48 | p.fill(255, 255, 255, 255);
49 | };
50 |
51 | var predict_path = function() {
52 | var temp_state = Model.copy_state(rnn_state); // create a copy
53 | var temp_dx, temp_dy, temp_pen, temp_prev_pen=prev_pen;
54 | var temp_x = x;
55 | var temp_y = y;
56 | var c = p.color(255, 165, 0, 48);
57 |
58 | for (var i=0; i epsilon*epsilon) { // only if pen is not in same area
98 | dx = dx0;
99 | dy = dy0;
100 | pen = 0;
101 |
102 | if (prev_pen == 0) {
103 | p.stroke(255,165,0);
104 | p.strokeWeight(1.0); // nice thick line
105 | p.line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
106 | }
107 |
108 | // update the absolute coordinates from the offsets
109 | x += dx;
110 | y += dy;
111 |
112 | // using the previous pen states, and hidden state, get next hidden state
113 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
114 | }
115 | } else { // pen is above the paper
116 | pen = 1;
117 | if (pen !== prev_pen) {
118 | // using the previous pen states, and hidden state, get next hidden state
119 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
120 | }
121 | }
122 |
123 | if (has_started) {
124 | // predict a sample for the next possible path at every frame update.
125 | predict_path(rnn_state);
126 | }
127 |
128 | // update the previous pen's state to the current one we just sampled
129 | prev_pen = pen;
130 |
131 | /*
132 | if (p.frameCount % 8 == 0) {
133 | p.background(255, 255, 255, 16); // fade out a bit.
134 | p.fill(255, 255, 255, 32);
135 | }
136 | */
137 |
138 | };
139 |
140 | };
141 | var custom_p5 = new p5(sketch, 'sketch');
142 |
--------------------------------------------------------------------------------
/sketch_multi_svg.js:
--------------------------------------------------------------------------------
1 | // variables we need for this demo
2 | var dx, dy; // offsets of the pen strokes, in pixels
3 | var pen, prev_pen; // keep track of whether pen is touching paper
4 | var x, y; // absolute coordinates on the screen of where the pen is
5 | var temperature = 0.50; // controls the amount of uncertainty of the model
6 | var rnn_state; // store the hidden states of rnn's neurons
7 | var pdf; // store all the parameters of a mixture-density distribution
8 |
9 | var has_started = false; // set to true after user starts writing.
10 | var epsilon = 4.0; // to ignore data from user's pen staying in one spot.
11 |
12 | var screen_width, screen_height; // stores the browser's dimensions
13 |
14 | var predict_len = 5; // predict the next N steps constantly
15 |
16 | // create a copy for predictor
17 | var temp_state;
18 | var temp_dx, temp_dy, temp_pen, temp_prev_pen;
19 | var temp_x = x;
20 | var temp_y = y;
21 | var c_predict;
22 |
23 | var restart = function() {
24 | // reinitialize variables before calling p5.js setu
25 |
26 | // make sure we enforce some minimum size of our demo
27 | screen_width = Math.max(window.innerWidth, 480);
28 | screen_height = Math.max(window.innerHeight, 320)/1.0;
29 |
30 | c_predict = color(241, 148, 138, 64); // set color of prediction.
31 |
32 | x = 50; // start drawing 50 pixels from the left side of the canvas
33 | y = screen_height/2; // start drawing from the middle of the canvas
34 |
35 | has_started = false;
36 |
37 | // initialize the scale factor for the model. Bigger -> large outputs
38 | Model.set_scale_factor(10.0);
39 |
40 | // initialize pen's states to zero.
41 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
42 |
43 | // initialize the rnn's initial states to zero
44 | rnn_state = Model.random_state();
45 |
46 | };
47 |
48 | var copy_rnn_state = function() {
49 | temp_state = Model.copy_state(rnn_state);
50 | temp_prev_pen = prev_pen;
51 | temp_x = x;
52 | temp_y = y;
53 | };
54 |
55 | var update_rnn_state = function() {
56 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
57 | copy_rnn_state();
58 | };
59 |
60 | setup = function() {
61 | restart(); // initialize variables for this demo
62 | createCanvas(screen_width, screen_height, SVG);
63 | frameRate(60);
64 | background(255, 255, 255, 255);
65 | fill(255, 255, 255, 255);
66 | };
67 |
68 | var predict_path = function() {
69 |
70 | for (var i=0; i screen_width) {
97 | copy_rnn_state();
98 | }
99 |
100 | }
101 | }
102 |
103 | draw = function() {
104 |
105 | // record pen drawing from user:
106 | if (mouseIsPressed) { // pen is touching the paper
107 | if (has_started == false) { // first time anything is written
108 | has_started = true;
109 | x = mouseX;
110 | y = mouseY;
111 | copy_rnn_state();
112 | }
113 | if (mouseX > screen_width-50) {
114 | // save svg file.
115 | noLoop();
116 | save();
117 | }
118 | var dx0 = mouseX-x; // candidate for dx
119 | var dy0 = mouseY-y; // candidate for dy
120 | if (dx0*dx0+dy0*dy0 > epsilon*epsilon) { // only if pen is not in same area
121 | dx = dx0;
122 | dy = dy0;
123 | pen = 0;
124 |
125 | if (prev_pen == 0) {
126 | stroke(241, 148, 138);
127 | strokeWeight(1.5); // nice thick line
128 | line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
129 | }
130 |
131 | // update the absolute coordinates from the offsets
132 | x += dx;
133 | y += dy;
134 |
135 | // using the previous pen states, and hidden state, get next hidden state
136 | update_rnn_state();
137 | }
138 | } else { // pen is above the paper
139 | pen = 1;
140 | if (pen !== prev_pen) {
141 | // using the previous pen states, and hidden state, get next hidden state
142 | update_rnn_state();
143 | }
144 | }
145 |
146 | if (has_started) {
147 | // predict a sample for the next possible path at every frame update.
148 | predict_path(rnn_state);
149 | }
150 |
151 | // update the previous pen's state to the current one we just sampled
152 | prev_pen = pen;
153 |
154 | /*
155 | if (frameCount % 8 == 0) {
156 | background(255, 255, 255, 16); // fade out a bit.
157 | fill(255, 255, 255, 32);
158 | }
159 | */
160 |
161 | };
--------------------------------------------------------------------------------
/sketch_multi.js:
--------------------------------------------------------------------------------
1 | // Basic Example of Unconditional Handwriting Generation.
2 | var sketch = function( p ) {
3 | "use strict";
4 |
5 | // variables we need for this demo
6 | var dx, dy; // offsets of the pen strokes, in pixels
7 | var pen, prev_pen; // keep track of whether pen is touching paper
8 | var x, y; // absolute coordinates on the screen of where the pen is
9 | var temperature = 0.50; // controls the amount of uncertainty of the model
10 | var rnn_state; // store the hidden states of rnn's neurons
11 | var pdf; // store all the parameters of a mixture-density distribution
12 |
13 | var has_started = false; // set to true after user starts writing.
14 | var epsilon = 4.0; // to ignore data from user's pen staying in one spot.
15 |
16 | var screen_width, screen_height; // stores the browser's dimensions
17 |
18 | var predict_len = 5; // predict the next N steps constantly
19 |
20 | // create a copy for predictor
21 | var temp_state;
22 | var temp_dx, temp_dy, temp_pen, temp_prev_pen;
23 | var temp_x = x;
24 | var temp_y = y;
25 | var c_predict;
26 |
27 | var restart = function() {
28 | // reinitialize variables before calling p5.js setup.
29 |
30 | // make sure we enforce some minimum size of our demo
31 | screen_width = Math.max(window.innerWidth, 480);
32 | screen_height = Math.max(window.innerHeight, 320)/1.0;
33 |
34 | c_predict = p.color(241, 148, 138, 64); // set color of prediction.
35 |
36 | x = 50; // start drawing 50 pixels from the left side of the canvas
37 | y = screen_height/2; // start drawing from the middle of the canvas
38 |
39 | has_started = false;
40 |
41 | // initialize the scale factor for the model. Bigger -> large outputs
42 | Model.set_scale_factor(10.0);
43 |
44 | // initialize pen's states to zero.
45 | [dx, dy, prev_pen] = Model.zero_input(); // the pen's states
46 |
47 | // initialize the rnn's initial states to zero
48 | rnn_state = Model.random_state();
49 |
50 | };
51 |
52 | var copy_rnn_state = function() {
53 | temp_state = Model.copy_state(rnn_state);
54 | temp_prev_pen = prev_pen;
55 | temp_x = x;
56 | temp_y = y;
57 | };
58 |
59 | var update_rnn_state = function() {
60 | rnn_state = Model.update([dx, dy, prev_pen], rnn_state);
61 | copy_rnn_state();
62 | };
63 |
64 | p.setup = function() {
65 | restart(); // initialize variables for this demo
66 | p.createCanvas(screen_width, screen_height);
67 | p.frameRate(60);
68 | p.background(255, 255, 255, 255);
69 | p.fill(255, 255, 255, 255);
70 | };
71 |
72 | var predict_path = function() {
73 |
74 | for (var i=0; i screen_width) {
101 | copy_rnn_state();
102 | }
103 |
104 | }
105 | }
106 |
107 | p.draw = function() {
108 |
109 | // record pen drawing from user:
110 | if (p.mouseIsPressed) { // pen is touching the paper
111 | if (has_started == false) { // first time anything is written
112 | has_started = true;
113 | x = p.mouseX;
114 | y = p.mouseY;
115 | copy_rnn_state();
116 | }
117 | var dx0 = p.mouseX-x; // candidate for dx
118 | var dy0 = p.mouseY-y; // candidate for dy
119 | if (dx0*dx0+dy0*dy0 > epsilon*epsilon) { // only if pen is not in same area
120 | dx = dx0;
121 | dy = dy0;
122 | pen = 0;
123 |
124 | if (prev_pen == 0) {
125 | p.stroke(241, 148, 138);
126 | p.strokeWeight(1.5); // nice thick line
127 | p.line(x, y, x+dx, y+dy); // draw line connecting prev point to current point.
128 | }
129 |
130 | // update the absolute coordinates from the offsets
131 | x += dx;
132 | y += dy;
133 |
134 | // using the previous pen states, and hidden state, get next hidden state
135 | update_rnn_state();
136 | }
137 | } else { // pen is above the paper
138 | pen = 1;
139 | if (pen !== prev_pen) {
140 | // using the previous pen states, and hidden state, get next hidden state
141 | update_rnn_state();
142 | }
143 | }
144 |
145 | if (has_started) {
146 | // predict a sample for the next possible path at every frame update.
147 | predict_path(rnn_state);
148 | }
149 |
150 | // update the previous pen's state to the current one we just sampled
151 | prev_pen = pen;
152 |
153 | /*
154 | if (p.frameCount % 8 == 0) {
155 | p.background(255, 255, 255, 16); // fade out a bit.
156 | p.fill(255, 255, 255, 32);
157 | }
158 | */
159 |
160 | };
161 |
162 | };
163 | var custom_p5 = new p5(sketch, 'sketch');
164 |
--------------------------------------------------------------------------------
/model.js:
--------------------------------------------------------------------------------
1 | // handwriting mdn-lstm model ported to JS
2 |
3 | if (typeof module != "undefined") {
4 | }
5 |
6 | var Model = {};
7 |
8 | (function(global) {
9 | "use strict";
10 |
11 | // init (import weights from weights.js)
12 |
13 | var string_to_uint8array = function(b64encoded) {
14 | var u8 = new Uint8Array(atob(b64encoded).split("").map(function(c) {
15 | return c.charCodeAt(0); }));
16 | return u8;
17 | }
18 |
19 | var uintarray_to_string = function(u8) {
20 | var b64encoded = btoa(String.fromCharCode.apply(null, u8));
21 | return b64encoded;
22 | };
23 |
24 | function encode_array(raw_array) {
25 | var i;
26 | var N = raw_array.length;
27 | var x = [];
28 | for (i=0;i 0.9) pen_s = 1;
144 | x.set(0, randn(0, std_));
145 | x.set(1, randn(0, std_));
146 | x.set(2, pen_s);
147 | return x;
148 | };
149 |
150 | var update = function(x_, s) {
151 | var x = nj.zeros(input_size);
152 | x.set(0, x_[0]/scale_factor);
153 | x.set(1, x_[1]/scale_factor);
154 | x.set(2, x_[2]);
155 | var h = s[0];
156 | var c = s[1];
157 | var concat = nj.concatenate([x, h]);
158 | var hidden = nj.dot(concat, W_full);
159 | hidden = nj.add(hidden, bias);
160 |
161 | var i=nj.sigmoid(hidden.slice([0*num_units, 1*num_units]));
162 | var g=nj.tanh(hidden.slice([1*num_units, 2*num_units]));
163 | var f=nj.sigmoid(nj.add(hidden.slice([2*num_units, 3*num_units]), forget_bias));
164 | var o=nj.sigmoid(hidden.slice([3*num_units, 4*num_units]));
165 |
166 | var new_c = nj.add(nj.multiply(c, f), nj.multiply(g, i));
167 | var new_h = nj.multiply(nj.tanh(new_c), o);
168 |
169 | return [new_h, new_c];
170 | }
171 |
172 | var get_pdf = function(s) {
173 | var h = s[0];
174 | var NOUT = N_mixture;
175 | var z=nj.add(nj.dot(h, h_w), h_b);
176 | var z_eos = nj.sigmoid(z.slice([0, 1]));
177 | var z_pi = z.slice([1+NOUT*0, 1+NOUT*1]);
178 | var z_mu1 = z.slice([1+NOUT*1, 1+NOUT*2]);
179 | var z_mu2 = z.slice([1+NOUT*2, 1+NOUT*3]);
180 | var z_sigma1 = nj.exp(z.slice([1+NOUT*3, 1+NOUT*4]));
181 | var z_sigma2 = nj.exp(z.slice([1+NOUT*4, 1+NOUT*5]));
182 | var z_corr = nj.tanh(z.slice([1+NOUT*5, 1+NOUT*6]));
183 | z_pi = nj.subtract(z_pi, z_pi.max());
184 | z_pi = nj.softmax(z_pi);
185 |
186 | return [z_pi, z_mu1, z_mu2, z_sigma1, z_sigma2, z_corr, z_eos];
187 | };
188 |
189 | var sample_pi_idx = function(z_pi) {
190 | var x = randf(0, 1);
191 | var N = N_mixture;
192 | var accumulate = 0;
193 | var i = 0;
194 | for (i=0;i= x) {
197 | return i;
198 | }
199 | }
200 | console.log('error sampling pi index');
201 | return -1;
202 | };
203 |
204 | var sample_eos = function(z_eos) {
205 | // eos = 1 if random.random() < o_eos[0][0] else 0
206 | var eos = 0;
207 | if (randf(0, 1) < z_eos.get(0)) {
208 | eos = 1;
209 | }
210 | return eos;
211 | }
212 |
213 | /*
214 | def adjust_temp(pi_pdf, temp):
215 | pi_pdf = np.log(pi_pdf) / temp
216 | pi_pdf -= pi_pdf.max()
217 | pi_pdf = np.exp(pi_pdf)
218 | pi_pdf /= pi_pdf.sum()
219 | return pi_pdf
220 | */
221 |
222 | var adjust_temp = function(z_old, temp) {
223 | var z = nj.array(z_old);
224 | var i;
225 | var x;
226 | //console.log("before="+z_old.get(0));
227 | for (i=z.shape[0]-1;i>=0;i--) {
228 | x = z.get(i);
229 | x = Math.log(x) / temp;
230 | z.set(i, x);
231 | }
232 | x = z.max();
233 | z = nj.subtract(z, x);
234 | z = nj.exp(z);
235 | x = z.sum();
236 | z = nj.divide(z, x);
237 | //console.log("after="+z.get(0));
238 | return z;
239 | };
240 |
241 | var sample = function(z, temperature) {
242 | // z is [z_pi, z_mu1, z_mu2, z_sigma1, z_sigma2, z_corr, z_eos]
243 | // returns [x, y, eos]
244 | var temp=0.65;
245 | if (typeof(temperature) === "number") {
246 | temp = temperature;
247 | }
248 | var z_0 = adjust_temp(z[0], temp);
249 | var z_6 = nj.array(z[6]);
250 | //var z6 = Math.exp(Math.log(z_6.get(0))/temp);
251 | //z_6.set(0, z6);
252 | var idx = sample_pi_idx(z_0);
253 | var mu1 = z[1].get(idx);
254 | var mu2 = z[2].get(idx);
255 | var sigma1 = z[3].get(idx)*temp;
256 | var sigma2 = z[4].get(idx)*temp;
257 | var corr = z[5].get(idx);
258 | var eos = sample_eos(z_6);
259 | var delta = birandn(mu1, mu2, sigma1, sigma2, corr);
260 | return [delta[0]*scale_factor, delta[1]*scale_factor, eos];
261 | }
262 |
263 | // Random numbers util (from https://github.com/karpathy/recurrentjs)
264 | var return_v = false;
265 | var v_val = 0.0;
266 | var gaussRandom = function() {
267 | if(return_v) {
268 | return_v = false;
269 | return v_val;
270 | }
271 | var u = 2*Math.random()-1;
272 | var v = 2*Math.random()-1;
273 | var r = u*u + v*v;
274 | if(r == 0 || r > 1) return gaussRandom();
275 | var c = Math.sqrt(-2*Math.log(r)/r);
276 | v_val = v*c; // cache this
277 | return_v = true;
278 | return u*c;
279 | }
280 | var randf = function(a, b) { return Math.random()*(b-a)+a; };
281 | var randi = function(a, b) { return Math.floor(Math.random()*(b-a)+a); };
282 | var randn = function(mu, std){ return mu+gaussRandom()*std; };
283 | // from http://www.math.grin.edu/~mooret/courses/math336/bivariate-normal.html
284 | var birandn = function(mu1, mu2, std1, std2, rho) {
285 | var z1 = randn(0, 1);
286 | var z2 = randn(0, 1);
287 | var x = Math.sqrt(1-rho*rho)*std1*z1 + rho*std1*z2 + mu1;
288 | var y = std2*z2 + mu2;
289 | return [x, y];
290 | };
291 |
292 | var set_scale_factor = function(scale) {
293 | scale_factor = scale;
294 | };
295 |
296 | global.zero_state = zero_state;
297 | global.zero_input = zero_input;
298 | global.random_state = random_state;
299 | global.copy_state = copy_state;
300 | global.random_input = random_input;
301 | global.update = update;
302 | global.get_pdf = get_pdf;
303 | global.randf = randf;
304 | global.randi = randi;
305 | global.randn = randn;
306 | global.birandn = birandn;
307 | global.sample = sample;
308 | global.set_scale_factor = set_scale_factor;
309 |
310 | })(Model);
311 | (function(lib) {
312 | "use strict";
313 | if (typeof module === "undefined" || typeof module.exports === "undefined") {
314 | //window.jsfeat = lib; // in ordinary browser attach library to window
315 | } else {
316 | module.exports = lib; // in nodejs
317 | }
318 | })(Model);
319 |
--------------------------------------------------------------------------------
/p5.svg.js:
--------------------------------------------------------------------------------
1 | /*!!
2 | * p5.svg v0.6.0.0
3 | * SVG Runtime for p5.js.
4 | *
5 | * Copyright (C) 2015-2016 Zeno Zeng
6 | * Licensed under the LGPL license.
7 | */
8 | (function (root, factory) {
9 | if (typeof define === 'function' && define.amd) {
10 | define('p5.svg', ['p5'], function (p5) {
11 | factory(p5);
12 | });
13 | }
14 | else if (typeof exports === 'object') {
15 | module.exports = factory;
16 | }
17 | else {
18 | factory(root['p5']);
19 | }
20 | })(this, function (p5) {
21 |
22 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) {
240 | options = defaultOptions;
241 | options.width = arguments[0];
242 | options.height = arguments[1];
243 | } else if( !o ) {
244 | options = defaultOptions;
245 | } else {
246 | options = o;
247 | }
248 |
249 | if(!(this instanceof ctx)) {
250 | //did someone call this without new?
251 | return new ctx(options);
252 | }
253 |
254 | //setup options
255 | this.width = options.width || defaultOptions.width;
256 | this.height = options.height || defaultOptions.height;
257 | this.enableMirroring = options.enableMirroring !== undefined ? options.enableMirroring : defaultOptions.enableMirroring;
258 |
259 | this.canvas = this; ///point back to this instance!
260 | this.__canvas = document.createElement("canvas");
261 | this.__ctx = this.__canvas.getContext("2d");
262 |
263 | this.__setDefaultStyles();
264 | this.__stack = [this.__getStyleState()];
265 | this.__groupStack = [];
266 |
267 | //the root svg element
268 | this.__root = document.createElementNS("http://www.w3.org/2000/svg", "svg");
269 | this.__root.setAttribute("version", 1.1);
270 | this.__root.setAttribute("xmlns", "http://www.w3.org/2000/svg");
271 | this.__root.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
272 | this.__root.setAttribute("width", this.width);
273 | this.__root.setAttribute("height", this.height);
274 |
275 | //make sure we don't generate the same ids in defs
276 | this.__ids = {};
277 |
278 | //defs tag
279 | this.__defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
280 | this.__root.appendChild(this.__defs);
281 |
282 | //also add a group child. the svg element can't use the transform attribute
283 | this.__currentElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
284 | this.__root.appendChild(this.__currentElement);
285 | };
286 |
287 | /**
288 | * Creates the specified svg element
289 | * @private
290 | */
291 | ctx.prototype.__createElement = function(elementName, properties, resetFill) {
292 | if (typeof properties === "undefined") {
293 | properties = {};
294 | }
295 |
296 | var element = document.createElementNS("http://www.w3.org/2000/svg", elementName),
297 | keys = Object.keys(properties), i, key;
298 | if(resetFill) {
299 | //if fill or stroke is not specified, the svg element should not display. By default SVG's fill is black.
300 | element.setAttribute("fill", "none");
301 | element.setAttribute("stroke", "none");
302 | }
303 | for(i=0; i 0) {
476 | var group = this.__createElement("g");
477 | parent.appendChild(group);
478 | this.__currentElement = group;
479 | }
480 |
481 | var transform = this.__currentElement.getAttribute("transform");
482 | if(transform) {
483 | transform += " ";
484 | } else {
485 | transform = "";
486 | }
487 | transform += t;
488 | this.__currentElement.setAttribute("transform", transform);
489 | };
490 |
491 | /**
492 | * scales the current element
493 | */
494 | ctx.prototype.scale = function(x, y) {
495 | if(y === undefined) {
496 | y = x;
497 | }
498 | this.__addTransform(format("scale({x},{y})", {x:x, y:y}));
499 | };
500 |
501 | /**
502 | * rotates the current element
503 | */
504 | ctx.prototype.rotate = function(angle){
505 | var degrees = (angle * 180 / Math.PI);
506 | this.__addTransform(format("rotate({angle},{cx},{cy})", {angle:degrees, cx:0, cy:0}));
507 | };
508 |
509 | /**
510 | * translates the current element
511 | */
512 | ctx.prototype.translate = function(x, y){
513 | this.__addTransform(format("translate({x},{y})", {x:x,y:y}));
514 | };
515 |
516 | /**
517 | * applies a transform to the current element
518 | */
519 | ctx.prototype.transform = function(a, b, c, d, e, f){
520 | this.__addTransform(format("matrix({a},{b},{c},{d},{e},{f})", {a:a, b:b, c:c, d:d, e:e, f:f}));
521 | };
522 |
523 | /**
524 | * Create a new Path Element
525 | */
526 | ctx.prototype.beginPath = function(){
527 | var path, parent;
528 |
529 | // Note that there is only one current default path, it is not part of the drawing state.
530 | // See also: https://html.spec.whatwg.org/multipage/scripting.html#current-default-path
531 | this.__currentDefaultPath = "";
532 | this.__currentPosition = {};
533 |
534 | path = this.__createElement("path", {}, true);
535 | parent = this.__closestGroupOrSvg();
536 | parent.appendChild(path);
537 | this.__currentElement = path;
538 | };
539 |
540 | /**
541 | * Helper function to apply currentDefaultPath to current path element
542 | * @private
543 | */
544 | ctx.prototype.__applyCurrentDefaultPath = function() {
545 | if(this.__currentElement.nodeName === "path") {
546 | var d = this.__currentDefaultPath;
547 | this.__currentElement.setAttribute("d", d);
548 | } else {
549 | throw new Error("Attempted to apply path command to node " + this.__currentElement.nodeName);
550 | }
551 | };
552 |
553 | /**
554 | * Helper function to add path command
555 | * @private
556 | */
557 | ctx.prototype.__addPathCommand = function(command){
558 | this.__currentDefaultPath += " ";
559 | this.__currentDefaultPath += command;
560 | };
561 |
562 | /**
563 | * Adds the move command to the current path element,
564 | * if the currentPathElement is not empty create a new path element
565 | */
566 | ctx.prototype.moveTo = function(x,y){
567 | if(this.__currentElement.nodeName !== "path") {
568 | this.beginPath();
569 | }
570 |
571 | // creates a new subpath with the given point
572 | this.__currentPosition = {x: x, y: y};
573 | this.__addPathCommand(format("M {x} {y}", {x:x, y:y}));
574 | };
575 |
576 | /**
577 | * Closes the current path
578 | */
579 | ctx.prototype.closePath = function(){
580 | this.__addPathCommand("Z");
581 | };
582 |
583 | /**
584 | * Adds a line to command
585 | */
586 | ctx.prototype.lineTo = function(x, y){
587 | this.__currentPosition = {x: x, y: y};
588 | if (this.__currentDefaultPath.indexOf('M') > -1) {
589 | this.__addPathCommand(format("L {x} {y}", {x:x, y:y}));
590 | } else {
591 | this.__addPathCommand(format("M {x} {y}", {x:x, y:y}));
592 | }
593 | };
594 |
595 | /**
596 | * Add a bezier command
597 | */
598 | ctx.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
599 | this.__currentPosition = {x: x, y: y};
600 | this.__addPathCommand(format("C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}",
601 | {cp1x:cp1x, cp1y:cp1y, cp2x:cp2x, cp2y:cp2y, x:x, y:y}));
602 | };
603 |
604 | /**
605 | * Adds a quadratic curve to command
606 | */
607 | ctx.prototype.quadraticCurveTo = function(cpx, cpy, x, y){
608 | this.__currentPosition = {x: x, y: y};
609 | this.__addPathCommand(format("Q {cpx} {cpy} {x} {y}", {cpx:cpx, cpy:cpy, x:x, y:y}));
610 | };
611 |
612 |
613 | /**
614 | * Return a new normalized vector of given vector
615 | */
616 | var normalize = function(vector) {
617 | var len = Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
618 | return [vector[0] / len, vector[1] / len];
619 | };
620 |
621 | /**
622 | * Adds the arcTo to the current path
623 | *
624 | * @see http://www.w3.org/TR/2015/WD-2dcontext-20150514/#dom-context-2d-arcto
625 | */
626 | ctx.prototype.arcTo = function(x1, y1, x2, y2, radius) {
627 | // Let the point (x0, y0) be the last point in the subpath.
628 | var x0 = this.__currentPosition && this.__currentPosition.x;
629 | var y0 = this.__currentPosition && this.__currentPosition.y;
630 |
631 | // First ensure there is a subpath for (x1, y1).
632 | if (typeof x0 == "undefined" || typeof y0 == "undefined") {
633 | return;
634 | }
635 |
636 | // Negative values for radius must cause the implementation to throw an IndexSizeError exception.
637 | if (radius < 0) {
638 | throw new Error("IndexSizeError: The radius provided (" + radius + ") is negative.");
639 | }
640 |
641 | // If the point (x0, y0) is equal to the point (x1, y1),
642 | // or if the point (x1, y1) is equal to the point (x2, y2),
643 | // or if the radius radius is zero,
644 | // then the method must add the point (x1, y1) to the subpath,
645 | // and connect that point to the previous point (x0, y0) by a straight line.
646 | if (((x0 === x1) && (y0 === y1))
647 | || ((x1 === x2) && (y1 === y2))
648 | || (radius === 0)) {
649 | this.lineTo(x1, y1);
650 | return;
651 | }
652 |
653 | // Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line,
654 | // then the method must add the point (x1, y1) to the subpath,
655 | // and connect that point to the previous point (x0, y0) by a straight line.
656 | var unit_vec_p1_p0 = normalize([x0 - x1, y0 - y1]);
657 | var unit_vec_p1_p2 = normalize([x2 - x1, y2 - y1]);
658 | if (unit_vec_p1_p0[0] * unit_vec_p1_p2[1] === unit_vec_p1_p0[1] * unit_vec_p1_p2[0]) {
659 | this.lineTo(x1, y1);
660 | return;
661 | }
662 |
663 | // Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius,
664 | // and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1),
665 | // and that has a different point tangent to the half-infinite line that ends at the point (x1, y1), and crosses the point (x2, y2).
666 | // The points at which this circle touches these two lines are called the start and end tangent points respectively.
667 |
668 | // note that both vectors are unit vectors, so the length is 1
669 | var cos = (unit_vec_p1_p0[0] * unit_vec_p1_p2[0] + unit_vec_p1_p0[1] * unit_vec_p1_p2[1]);
670 | var theta = Math.acos(Math.abs(cos));
671 |
672 | // Calculate origin
673 | var unit_vec_p1_origin = normalize([
674 | unit_vec_p1_p0[0] + unit_vec_p1_p2[0],
675 | unit_vec_p1_p0[1] + unit_vec_p1_p2[1]
676 | ]);
677 | var len_p1_origin = radius / Math.sin(theta / 2);
678 | var x = x1 + len_p1_origin * unit_vec_p1_origin[0];
679 | var y = y1 + len_p1_origin * unit_vec_p1_origin[1];
680 |
681 | // Calculate start angle and end angle
682 | // rotate 90deg clockwise (note that y axis points to its down)
683 | var unit_vec_origin_start_tangent = [
684 | -unit_vec_p1_p0[1],
685 | unit_vec_p1_p0[0]
686 | ];
687 | // rotate 90deg counter clockwise (note that y axis points to its down)
688 | var unit_vec_origin_end_tangent = [
689 | unit_vec_p1_p2[1],
690 | -unit_vec_p1_p2[0]
691 | ];
692 | var getAngle = function(vector) {
693 | // get angle (clockwise) between vector and (1, 0)
694 | var x = vector[0];
695 | var y = vector[1];
696 | if (y >= 0) { // note that y axis points to its down
697 | return Math.acos(x);
698 | } else {
699 | return -Math.acos(x);
700 | }
701 | };
702 | var startAngle = getAngle(unit_vec_origin_start_tangent);
703 | var endAngle = getAngle(unit_vec_origin_end_tangent);
704 |
705 | // Connect the point (x0, y0) to the start tangent point by a straight line
706 | this.lineTo(x + unit_vec_origin_start_tangent[0] * radius,
707 | y + unit_vec_origin_start_tangent[1] * radius);
708 |
709 | // Connect the start tangent point to the end tangent point by arc
710 | // and adding the end tangent point to the subpath.
711 | this.arc(x, y, radius, startAngle, endAngle);
712 | };
713 |
714 | /**
715 | * Sets the stroke property on the current element
716 | */
717 | ctx.prototype.stroke = function(){
718 | if(this.__currentElement.nodeName === "path") {
719 | this.__currentElement.setAttribute("paint-order", "fill stroke markers");
720 | }
721 | this.__applyCurrentDefaultPath();
722 | this.__applyStyleToCurrentElement("stroke");
723 | };
724 |
725 | /**
726 | * Sets fill properties on the current element
727 | */
728 | ctx.prototype.fill = function(){
729 | if(this.__currentElement.nodeName === "path") {
730 | this.__currentElement.setAttribute("paint-order", "stroke fill markers");
731 | }
732 | this.__applyCurrentDefaultPath();
733 | this.__applyStyleToCurrentElement("fill");
734 | };
735 |
736 | /**
737 | * Adds a rectangle to the path.
738 | */
739 | ctx.prototype.rect = function(x, y, width, height){
740 | if(this.__currentElement.nodeName !== "path") {
741 | this.beginPath();
742 | }
743 | this.moveTo(x, y);
744 | this.lineTo(x+width, y);
745 | this.lineTo(x+width, y+height);
746 | this.lineTo(x, y+height);
747 | this.lineTo(x, y);
748 | this.closePath();
749 | };
750 |
751 |
752 | /**
753 | * adds a rectangle element
754 | */
755 | ctx.prototype.fillRect = function(x, y, width, height){
756 | var rect, parent;
757 | rect = this.__createElement("rect", {
758 | x : x,
759 | y : y,
760 | width : width,
761 | height : height
762 | }, true);
763 | parent = this.__closestGroupOrSvg();
764 | parent.appendChild(rect);
765 | this.__currentElement = rect;
766 | this.__applyStyleToCurrentElement("fill");
767 | };
768 |
769 | /**
770 | * Draws a rectangle with no fill
771 | * @param x
772 | * @param y
773 | * @param width
774 | * @param height
775 | */
776 | ctx.prototype.strokeRect = function(x, y, width, height){
777 | var rect, parent;
778 | rect = this.__createElement("rect", {
779 | x : x,
780 | y : y,
781 | width : width,
782 | height : height
783 | }, true);
784 | parent = this.__closestGroupOrSvg();
785 | parent.appendChild(rect);
786 | this.__currentElement = rect;
787 | this.__applyStyleToCurrentElement("stroke");
788 | };
789 |
790 |
791 | /**
792 | * "Clears" a canvas by just drawing a white rectangle in the current group.
793 | */
794 | ctx.prototype.clearRect = function(x, y, width, height) {
795 | var rect, parent = this.__closestGroupOrSvg();
796 | rect = this.__createElement("rect", {
797 | x : x,
798 | y : y,
799 | width : width,
800 | height : height,
801 | fill : "#FFFFFF"
802 | }, true);
803 | parent.appendChild(rect);
804 | };
805 |
806 | /**
807 | * Adds a linear gradient to a defs tag.
808 | * Returns a canvas gradient object that has a reference to it's parent def
809 | */
810 | ctx.prototype.createLinearGradient = function(x1, y1, x2, y2){
811 | var grad = this.__createElement("linearGradient", {
812 | id : randomString(this.__ids),
813 | x1 : x1+"px",
814 | x2 : x2+"px",
815 | y1 : y1+"px",
816 | y2 : y2+"px",
817 | "gradientUnits" : "userSpaceOnUse"
818 | }, false);
819 | this.__defs.appendChild(grad);
820 | return new CanvasGradient(grad);
821 | };
822 |
823 | /**
824 | * Adds a radial gradient to a defs tag.
825 | * Returns a canvas gradient object that has a reference to it's parent def
826 | */
827 | ctx.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1){
828 | var grad = this.__createElement("radialGradient", {
829 | id : randomString(this.__ids),
830 | cx : x1+"px",
831 | cy : y1+"px",
832 | r : r1+"px",
833 | fx : x0+"px",
834 | fy : y0+"px",
835 | "gradientUnits" : "userSpaceOnUse"
836 | }, false);
837 | this.__defs.appendChild(grad);
838 | return new CanvasGradient(grad);
839 |
840 | };
841 |
842 | /**
843 | * Parses the font string and returns svg mapping
844 | * @private
845 | */
846 | ctx.prototype.__parseFont = function() {
847 | var regex = /^\s*(?=(?:(?:[-a-z]+\s*){0,2}(italic|oblique))?)(?=(?:(?:[-a-z]+\s*){0,2}(small-caps))?)(?=(?:(?:[-a-z]+\s*){0,2}(bold(?:er)?|lighter|[1-9]00))?)(?:(?:normal|\1|\2|\3)\s*){0,3}((?:xx?-)?(?:small|large)|medium|smaller|larger|[.\d]+(?:\%|in|[cem]m|ex|p[ctx]))(?:\s*\/\s*(normal|[.\d]+(?:\%|in|[cem]m|ex|p[ctx])))?\s*([-,\"\sa-z]+?)\s*$/i;
848 | var fontPart = regex.exec( this.font );
849 | var data = {
850 | style : fontPart[1] || 'normal',
851 | size : fontPart[4] || '10px',
852 | family : fontPart[6] || 'sans-serif',
853 | weight: fontPart[3] || 'normal',
854 | decoration : fontPart[2] || 'normal',
855 | href : null
856 | };
857 |
858 | //canvas doesn't support underline natively, but we can pass this attribute
859 | if(this.__fontUnderline === "underline") {
860 | data.decoration = "underline";
861 | }
862 |
863 | //canvas also doesn't support linking, but we can pass this as well
864 | if(this.__fontHref) {
865 | data.href = this.__fontHref;
866 | }
867 |
868 | return data;
869 | };
870 |
871 | /**
872 | * Helper to link text fragments
873 | * @param font
874 | * @param element
875 | * @return {*}
876 | * @private
877 | */
878 | ctx.prototype.__wrapTextLink = function(font, element) {
879 | if(font.href) {
880 | var a = this.__createElement("a");
881 | a.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", font.href);
882 | a.appendChild(element);
883 | return a;
884 | }
885 | return element;
886 | };
887 |
888 | /**
889 | * Fills or strokes text
890 | * @param text
891 | * @param x
892 | * @param y
893 | * @param action - stroke or fill
894 | * @private
895 | */
896 | ctx.prototype.__applyText = function(text, x, y, action) {
897 | var font = this.__parseFont(),
898 | parent = this.__closestGroupOrSvg(),
899 | textElement = this.__createElement("text", {
900 | "font-family" : font.family,
901 | "font-size" : font.size,
902 | "font-style" : font.style,
903 | "font-weight" : font.weight,
904 | "text-decoration" : font.decoration,
905 | "x" : x,
906 | "y" : y,
907 | "text-anchor": getTextAnchor(this.textAlign),
908 | "dominant-baseline": getDominantBaseline(this.textBaseline)
909 | }, true);
910 |
911 | textElement.appendChild(document.createTextNode(text));
912 | this.__currentElement = textElement;
913 | this.__applyStyleToCurrentElement(action);
914 | parent.appendChild(this.__wrapTextLink(font,textElement));
915 | };
916 |
917 | /**
918 | * Creates a text element
919 | * @param text
920 | * @param x
921 | * @param y
922 | */
923 | ctx.prototype.fillText = function(text, x, y){
924 | this.__applyText(text, x, y, "fill");
925 | };
926 |
927 | /**
928 | * Strokes text
929 | * @param text
930 | * @param x
931 | * @param y
932 | */
933 | ctx.prototype.strokeText = function(text, x, y){
934 | this.__applyText(text, x, y, "stroke");
935 | };
936 |
937 | /**
938 | * No need to implement this for svg.
939 | * @param text
940 | * @return {TextMetrics}
941 | */
942 | ctx.prototype.measureText = function(text){
943 | this.__ctx.font = this.font;
944 | return this.__ctx.measureText(text);
945 | };
946 |
947 | /**
948 | * Arc command!
949 | */
950 | ctx.prototype.arc = function(x, y, radius, startAngle, endAngle, counterClockwise) {
951 | startAngle = startAngle % (2*Math.PI);
952 | endAngle = endAngle % (2*Math.PI);
953 | if(startAngle === endAngle) {
954 | //circle time! subtract some of the angle so svg is happy (svg elliptical arc can't draw a full circle)
955 | endAngle = ((endAngle + (2*Math.PI)) - 0.001 * (counterClockwise ? -1 : 1)) % (2*Math.PI);
956 | }
957 | var endX = x+radius*Math.cos(endAngle),
958 | endY = y+radius*Math.sin(endAngle),
959 | startX = x+radius*Math.cos(startAngle),
960 | startY = y+radius*Math.sin(startAngle),
961 | sweepFlag = counterClockwise ? 0 : 1,
962 | largeArcFlag = 0,
963 | diff = endAngle - startAngle;
964 |
965 | // https://github.com/gliffy/canvas2svg/issues/4
966 | if(diff < 0) {
967 | diff += 2*Math.PI;
968 | }
969 |
970 | if(counterClockwise) {
971 | largeArcFlag = diff > Math.PI ? 0 : 1;
972 | } else {
973 | largeArcFlag = diff > Math.PI ? 1 : 0;
974 | }
975 |
976 | this.lineTo(startX, startY);
977 | this.__addPathCommand(format("A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}",
978 | {rx:radius, ry:radius, xAxisRotation:0, largeArcFlag:largeArcFlag, sweepFlag:sweepFlag, endX:endX, endY:endY}));
979 |
980 | this.__currentPosition = {x: endX, y: endY};
981 | };
982 |
983 | /**
984 | * Generates a ClipPath from the clip command.
985 | */
986 | ctx.prototype.clip = function(){
987 | var group = this.__closestGroupOrSvg(),
988 | clipPath = this.__createElement("clipPath"),
989 | id = randomString(this.__ids),
990 | newGroup = this.__createElement("g");
991 |
992 | group.removeChild(this.__currentElement);
993 | clipPath.setAttribute("id", id);
994 | clipPath.appendChild(this.__currentElement);
995 |
996 | this.__defs.appendChild(clipPath);
997 |
998 | //set the clip path to this group
999 | group.setAttribute("clip-path", format("url(#{id})", {id:id}));
1000 |
1001 | //clip paths can be scaled and transformed, we need to add another wrapper group to avoid later transformations
1002 | // to this path
1003 | group.appendChild(newGroup);
1004 |
1005 | this.__currentElement = newGroup;
1006 |
1007 | };
1008 |
1009 | /**
1010 | * Draws a canvas, image or mock context to this canvas.
1011 | * Note that all svg dom manipulation uses node.childNodes rather than node.children for IE support.
1012 | * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-drawimage
1013 | */
1014 | ctx.prototype.drawImage = function(){
1015 | //convert arguments to a real array
1016 | var args = Array.prototype.slice.call(arguments),
1017 | image=args[0],
1018 | dx, dy, dw, dh, sx=0, sy=0, sw, sh, parent, svg, defs, group,
1019 | currentElement, svgImage, canvas, context, id;
1020 |
1021 | if(args.length === 3) {
1022 | dx = args[1];
1023 | dy = args[2];
1024 | sw = image.width;
1025 | sh = image.height;
1026 | dw = sw;
1027 | dh = sh;
1028 | } else if(args.length === 5) {
1029 | dx = args[1];
1030 | dy = args[2];
1031 | dw = args[3];
1032 | dh = args[4];
1033 | sw = image.width;
1034 | sh = image.height;
1035 | } else if(args.length === 9) {
1036 | sx = args[1];
1037 | sy = args[2];
1038 | sw = args[3];
1039 | sh = args[4];
1040 | dx = args[5];
1041 | dy = args[6];
1042 | dw = args[7];
1043 | dh = args[8];
1044 | } else {
1045 | throw new Error("Inavlid number of arguments passed to drawImage: " + arguments.length);
1046 | }
1047 |
1048 | parent = this.__closestGroupOrSvg();
1049 | currentElement = this.__currentElement;
1050 |
1051 | if(image instanceof ctx) {
1052 | //canvas2svg mock canvas context. In the future we may want to clone nodes instead.
1053 | //also I'm currently ignoring dw, dh, sw, sh, sx, sy for a mock context.
1054 | svg = image.getSvg();
1055 | defs = svg.childNodes[0];
1056 | while(defs.childNodes.length) {
1057 | id = defs.childNodes[0].getAttribute("id");
1058 | this.__ids[id] = id;
1059 | this.__defs.appendChild(defs.childNodes[0]);
1060 | }
1061 | group = svg.childNodes[1];
1062 | parent.appendChild(group);
1063 | this.__currentElement = group;
1064 | this.translate(dx, dy);
1065 | this.__currentElement = currentElement;
1066 | } else if(image.nodeName === "CANVAS" || image.nodeName === "IMG") {
1067 | //canvas or image
1068 | svgImage = this.__createElement("image");
1069 | svgImage.setAttribute("width", dw);
1070 | svgImage.setAttribute("height", dh);
1071 | svgImage.setAttribute("preserveAspectRatio", "none");
1072 |
1073 | if(sx || sy || sw !== image.width || sh !== image.height) {
1074 | //crop the image using a temporary canvas
1075 | canvas = document.createElement("canvas");
1076 | canvas.width = dw;
1077 | canvas.height = dh;
1078 | context = canvas.getContext("2d");
1079 | context.drawImage(image, sx, sy, sw, sh, 0, 0, dw, dh);
1080 | image = canvas;
1081 | }
1082 |
1083 | svgImage.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href",
1084 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src"));
1085 | parent.appendChild(svgImage);
1086 | this.__currentElement = svgImage;
1087 | this.translate(dx, dy);
1088 | this.__currentElement = currentElement;
1089 | }
1090 | };
1091 |
1092 | /**
1093 | * Generates a pattern tag
1094 | */
1095 | ctx.prototype.createPattern = function(image, repetition){
1096 | var pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern"), id = randomString(this.__ids),
1097 | img;
1098 | pattern.setAttribute("id", id);
1099 | pattern.setAttribute("width", image.width);
1100 | pattern.setAttribute("height", image.height);
1101 | if(image.nodeName === "CANVAS" || image.nodeName === "IMG") {
1102 | img = document.createElementNS("http://www.w3.org/2000/svg", "image");
1103 | img.setAttribute("width", image.width);
1104 | img.setAttribute("height", image.height);
1105 | img.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href",
1106 | image.nodeName === "CANVAS" ? image.toDataURL() : image.getAttribute("src"));
1107 | pattern.appendChild(img);
1108 | this.__defs.appendChild(pattern);
1109 | } else if(image instanceof ctx) {
1110 | pattern.appendChild(image.__root.childNodes[1]);
1111 | this.__defs.appendChild(pattern);
1112 | }
1113 | return new CanvasPattern(pattern, this);
1114 | };
1115 |
1116 | /**
1117 | * Not yet implemented
1118 | */
1119 | ctx.prototype.drawFocusRing = function(){};
1120 | ctx.prototype.createImageData = function(){};
1121 | ctx.prototype.getImageData = function(){};
1122 | ctx.prototype.putImageData = function(){};
1123 | ctx.prototype.globalCompositeOperation = function(){};
1124 | ctx.prototype.setTransform = function(){};
1125 |
1126 | //add options for alternative namespace
1127 | module.exports = ctx;
1128 |
1129 | }());
1130 |
1131 | },{}],2:[function(require,module,exports){
1132 | var C2S = require('./canvas2svg');
1133 |
1134 | var Context = function(width, height, options) {
1135 | C2S.call(this);
1136 | this.__width = width;
1137 | this.__height = height;
1138 | this.generations = [[]]; // used to collect element references for different generations
1139 |
1140 | var _this = this;
1141 | this.__imageSmoothingEnabled = true;
1142 |
1143 | ["mozImageSmoothingEnabled",
1144 | "webkitImageSmoothingEnabled",
1145 | "msImageSmoothingEnabled",
1146 | "imageSmoothingEnabled"].forEach(function(k) {
1147 | Object.defineProperty(_this, k, {
1148 | get: function() {
1149 | return _this.__imageSmoothingEnabled;
1150 | },
1151 | set: function(val) {
1152 | _this.__imageSmoothingEnabled = val;
1153 | }
1154 | });
1155 | });
1156 |
1157 | options = options || {};
1158 |
1159 | ["fillStyle", "strokeStyle"].forEach(function(prop) {
1160 | var key = "__" + prop;
1161 | Object.defineProperty(_this, prop, {
1162 | get: function() {
1163 | return _this[key];
1164 | },
1165 | set: function(val) {
1166 | if (val.indexOf('NaN') > -1) {
1167 | console.warn("svgcanvas: invalid value for " + prop + ", fail to set it to " + val);
1168 | return;
1169 | }
1170 | _this[key] = val;
1171 | }
1172 | });
1173 | });
1174 |
1175 |
1176 | if (options.debug) {
1177 | this.__history = []; // method history
1178 |
1179 | var methods = [];
1180 | for(var key in this) {
1181 | if (typeof this[key] === "function") {
1182 | if (key.indexOf('__') !== 0) {
1183 | if (key !== 'getSerializedSvg') {
1184 | methods.push(key);
1185 | }
1186 | }
1187 | }
1188 | }
1189 | ["__fillStyle", "__strokeStyle"].forEach(function(prop) {
1190 | var key = "__debug__" + prop;
1191 | Object.defineProperty(_this, prop, {
1192 | get: function() {
1193 | return _this[key];
1194 | },
1195 | set: function(val) {
1196 | var call = prop.replace(/__/g, '') + " = " + val;
1197 | _this.__history.push(call);
1198 | _this[key] = val;
1199 | }
1200 | });
1201 | });
1202 | methods.forEach(function(method) {
1203 | var fn = _this[method];
1204 | _this[method] = function() {
1205 | var call = method + '(' + Array.prototype.slice.call(arguments).join(', ') + ');';
1206 |
1207 | // keep call history
1208 | _this.__history.push(call);
1209 | if (_this.__history.length > 100) {
1210 | _this.__history.shift();
1211 | }
1212 |
1213 | return fn.apply(_this, arguments);
1214 | };
1215 | });
1216 | }
1217 | };
1218 |
1219 | Context.prototype = Object.create(C2S.prototype);
1220 |
1221 | Context.prototype.scale = function(x, y) {
1222 | if (x === undefined || y === undefined) {
1223 | return;
1224 | } else {
1225 | C2S.prototype.scale.apply(this, arguments);
1226 | }
1227 | };
1228 |
1229 | Context.prototype.__createElement = function(elementName, properties, resetFill) {
1230 | if (!this.__imageSmoothingEnabled) {
1231 | // only shape elements can use the shape-rendering attribute
1232 | if (["circle", "ellipse", "line", "path", "polygon", "polyline", "rect"].indexOf(elementName) > -1) {
1233 | properties = properties || {};
1234 | properties["shape-rendering"] = "crispEdges"; // disable anti-aliasing
1235 | }
1236 | }
1237 |
1238 | var element = C2S.prototype.__createElement.call(this, elementName, properties, resetFill);
1239 | var currentGeneration = this.generations[this.generations.length - 1];
1240 | currentGeneration.push(element);
1241 | return element;
1242 | };
1243 |
1244 | Context.prototype.__gc = function() {
1245 | this.generations.push([]);
1246 | var ctx = this;
1247 | // make sure it happens after current job done
1248 | // for example: in p5.js's redraw use setTimeout will make gc called after both save() and restore() called
1249 | setTimeout(function() {
1250 | if (ctx.__groupStack.length > 0) {
1251 | // we are between ctx.save() and ctx.restore(), skip gc
1252 | return;
1253 | }
1254 | if (ctx.__currentElement.nodeName === 'path') {
1255 | // we are still in path, skip gc
1256 | return;
1257 | }
1258 | // keep only latest generation
1259 | while (ctx.generations.length > 1) {
1260 | var elements = ctx.generations.shift();
1261 | var lastCount = 0;
1262 | var count = elements.length;
1263 | while (count > 0) {
1264 | lastCount = count;
1265 | elements = elements.filter(function(elem) {
1266 | // in case children may from live generation, gc from bottom to top
1267 | var children = elem.children || elem.childNodes; // childNodes for IE
1268 | if (children.length === 0) {
1269 | elem.parentNode.removeChild(elem);
1270 | return false;
1271 | } else {
1272 | return true;
1273 | }
1274 | });
1275 | count = elements.length;
1276 | if (count === lastCount) {
1277 | // could not gc more, exit now
1278 | // save this elements to live generation
1279 | var liveGeneration = ctx.generations[ctx.generations.length - 1];
1280 | elements.forEach(function(elem) {
1281 | liveGeneration.push(elem);
1282 | });
1283 | // exit
1284 | break;
1285 | }
1286 | }
1287 | }
1288 | }, 0);
1289 | };
1290 |
1291 | /**
1292 | * Clear full canvas and do gc
1293 | * @private
1294 | */
1295 | Context.prototype.__clearCanvas = function() {
1296 | // remove all
1297 | this.generations.forEach(function(elems) {
1298 | elems.forEach(function(elem) {
1299 | if (elem) {
1300 | elem.parentNode.removeChild(elem);
1301 | }
1302 | });
1303 | });
1304 | this.generations = [[]];
1305 | var g = this.__createElement('g');
1306 | this.__root.appendChild(g);
1307 | this.__currentElement = g;
1308 | };
1309 |
1310 | Context.prototype.clearRect = function(x, y, w, h) {
1311 | if (x === 0 && y === 0 && w === this.__width && h === this.__height) {
1312 | this.__clearCanvas();
1313 | } else {
1314 | C2S.prototype.clearRect.call(this, x, y, w, h);
1315 | }
1316 | };
1317 |
1318 | Context.prototype.fillRect = function(x, y, w, h) {
1319 | if (x === 0 && y === 0 && w === this.__width && h === this.__height) {
1320 | this.__gc();
1321 | }
1322 | C2S.prototype.fillRect.call(this, x, y, w, h);
1323 | };
1324 |
1325 | // Simple version of drawImage
1326 | // Note that this version does not handle drawing mock context
1327 | Context.prototype.drawImage = function() {
1328 | var canvas = document.createElement('canvas');
1329 | canvas.width = this.__width;
1330 | canvas.height = this.__height;
1331 | var args = arguments;
1332 | var ctx = canvas.getContext('2d');
1333 | ctx.drawImage.apply(ctx, args);
1334 | // Note: don't use foreign object,
1335 | // otherwise the saved SVG may be unusable for other application
1336 | var url = canvas.toDataURL('image/png');
1337 | var image = this.__createElement('image', {
1338 | x: 0,
1339 | y: 0,
1340 | width: canvas.width,
1341 | height: canvas.height,
1342 | preserveAspectRatio: 'none'
1343 | });
1344 | var parent = this.__closestGroupOrSvg();
1345 | image.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", url);
1346 | parent.appendChild(image);
1347 | };
1348 |
1349 | Context.prototype.getSerializedSvg = null;
1350 |
1351 | module.exports = Context;
1352 |
1353 | },{"./canvas2svg":1}],3:[function(require,module,exports){
1354 | var Context = require('./context');
1355 |
1356 | function SVGCanvas(options) {
1357 |
1358 | var debug = options && options.debug;
1359 |
1360 | this.ctx = new Context(100, 100, {debug: debug});
1361 | this.svg = this.ctx.__root;
1362 |
1363 | // sync attributes to svg
1364 | var svg = this.svg;
1365 | var _this = this;
1366 |
1367 | var wrapper = document.createElement('div');
1368 | wrapper.style.display = 'inline-block';
1369 | wrapper.appendChild(svg);
1370 | this.wrapper = wrapper;
1371 |
1372 | Object.defineProperty(this, 'className', {
1373 | get: function() {
1374 | return wrapper.getAttribute('class') || '';
1375 | },
1376 | set: function(val) {
1377 | return wrapper.setAttribute('class', val);
1378 | }
1379 | });
1380 |
1381 | ["width", "height"].forEach(function(prop) {
1382 | Object.defineProperty(_this, prop, {
1383 | get: function() {
1384 | return svg.getAttribute(prop) | 0;
1385 | },
1386 | set: function(val) {
1387 | if (isNaN(val) || (typeof val === "undefined")) {
1388 | return;
1389 | }
1390 | _this.ctx['__'+prop] = val;
1391 | svg.setAttribute(prop, val);
1392 | return wrapper[prop] = val;
1393 | }
1394 | });
1395 | });
1396 |
1397 | ["style", "id"].forEach(function(prop) {
1398 | Object.defineProperty(_this, prop, {
1399 | get: function() {
1400 | return wrapper[prop];
1401 | },
1402 | set: function(val) {
1403 | if (typeof val !== "undefined") {
1404 | return wrapper[prop] = val;
1405 | }
1406 | }
1407 | });
1408 | });
1409 |
1410 | ["getBoundingClientRect"].forEach(function(fn) {
1411 | _this[fn] = function() {
1412 | return svg[fn]();
1413 | };
1414 | });
1415 | }
1416 |
1417 | SVGCanvas.prototype.getContext = function(type) {
1418 | if (type !== '2d') {
1419 | throw new Error('Unsupported type of context for SVGCanvas');
1420 | }
1421 |
1422 | return this.ctx;
1423 | };
1424 |
1425 | // you should always use URL.revokeObjectURL after your work done
1426 | SVGCanvas.prototype.toObjectURL = function() {
1427 | var data = new XMLSerializer().serializeToString(this.svg);
1428 | var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
1429 | return URL.createObjectURL(svg);
1430 | };
1431 |
1432 | SVGCanvas.prototype.toDataURL = function(type, options) {
1433 | var xml = new XMLSerializer().serializeToString(this.svg);
1434 |
1435 | // documentMode is an IE-only property
1436 | // http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
1437 | // http://stackoverflow.com/questions/10964966/detect-ie-version-prior-to-v9-in-javascript
1438 | var isIE = document.documentMode;
1439 |
1440 | if (isIE) {
1441 | // This is patch from canvas2svg
1442 | // IE search for a duplicate xmnls because they didn't implement setAttributeNS correctly
1443 | var xmlns = /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi;
1444 | if(xmlns.test(xml)) {
1445 | xml = xml.replace('xmlns="http://www.w3.org/2000/svg','xmlns:xlink="http://www.w3.org/1999/xlink');
1446 | }
1447 | }
1448 |
1449 | var SVGDataURL = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(xml);
1450 | if (type === "image/svg+xml" || !type) {
1451 | return SVGDataURL;
1452 | }
1453 | if (type === "image/jpeg" || type === "image/png") {
1454 | var canvas = document.createElement('canvas');
1455 | canvas.width = this.width;
1456 | canvas.height = this.height;
1457 | var ctx = canvas.getContext('2d');
1458 | var img = new Image();
1459 | img.src = SVGDataURL;
1460 | if (img.complete && img.width > 0 && img.height > 0) {
1461 | // for chrome, it's ready immediately
1462 | ctx.drawImage(img, 0, 0);
1463 | return canvas.toDataURL(type, options);
1464 | } else {
1465 | // for firefox, it's not possible to provide sync api in current thread
1466 | // and web worker doesn't provide canvas API, so
1467 | throw new Error('svgcanvas.toDataURL() for jpeg/png is only available in Chrome.');
1468 | }
1469 | }
1470 | throw new Error('Unknown type for SVGCanvas.prototype.toDataURL, please use image/jpeg | image/png | image/svg+xml.');
1471 | };
1472 |
1473 | SVGCanvas.prototype.addEventListener = function() {
1474 | return this.svg.addEventListener.apply(this, arguments);
1475 | };
1476 |
1477 | // will return wrapper element:
1478 | SVGCanvas.prototype.getElement = function() {
1479 | return this.wrapper;
1480 | };
1481 |
1482 | module.exports = SVGCanvas;
1483 |
1484 | },{"./context":2}],4:[function(require,module,exports){
1485 | var constants = {
1486 | SVG: 'svg'
1487 | };
1488 |
1489 | module.exports = constants;
1490 |
1491 | },{}],5:[function(require,module,exports){
1492 | module.exports = function(p5) {
1493 | /**
1494 | * Returns an Array of SVGElements of current SVG Graphics matching given selector
1495 | *
1496 | * @function querySVG
1497 | * @memberof p5.prototype
1498 | * @param {String} selector CSS selector for query
1499 | * @returns {SVGElement[]}
1500 | */
1501 | p5.prototype.querySVG = function(selector) {
1502 | var svg = this._renderer && this._renderer.svg;
1503 | if (!svg) {
1504 | return null;
1505 | }
1506 | return p5.SVGElement.prototype.query.call({elt: svg}, selector);
1507 | };
1508 |
1509 | /**
1510 | * @namespace SVGElement
1511 | * @constructor
1512 | * @param {Element} element
1513 | */
1514 | function SVGElement(element) {
1515 | if (!element) {
1516 | return null;
1517 | }
1518 | return p5.Element.apply(this, arguments);
1519 | }
1520 |
1521 | SVGElement.prototype = Object.create(p5.Element.prototype);
1522 |
1523 | /**
1524 | * Returns an Array of children of current SVG Element matching given selector
1525 | *
1526 | * @function query
1527 | * @memberof SVGElement.prototype
1528 | * @param {String} selector CSS selector for query
1529 | * @returns {SVGElement[]}
1530 | */
1531 | SVGElement.prototype.query = function(selector) {
1532 | var elements = this.elt.querySelectorAll(selector);
1533 | var objects = [];
1534 | for (var i = 0; i < elements.length; i++) {
1535 | objects[i] = new SVGElement(elements[i]);
1536 | }
1537 | return objects;
1538 | };
1539 |
1540 | /**
1541 | * Append a new child to current element.
1542 | *
1543 | * @function append
1544 | * @memberof SVGElement.prototype
1545 | * @param {SVGElement|Element} element
1546 | */
1547 | SVGElement.prototype.append = function(element) {
1548 | var elt = element.elt || element;
1549 | this.elt.appendChild(elt);
1550 | return this;
1551 | };
1552 |
1553 | /**
1554 | * Apply different attribute operation based on arguments.length
1555 | *
1556 | * - setAttribute(name, value)
1557 | * - setAttributeNS(namespace, name, value)
1558 | * - getAttribute(name)
1559 | *
1560 | *
1561 | * @function attribute
1562 | * @memberof SVGElement.prototype
1563 | */
1564 | SVGElement.prototype.attribute = function() {
1565 | var args = arguments;
1566 | if (args.length === 3) {
1567 | this.elt.setAttributeNS.apply(this.elt, args);
1568 | }
1569 | if (args.length === 2) {
1570 | this.elt.setAttribute.apply(this.elt, args);
1571 | }
1572 | if (args.length === 1) {
1573 | return this.elt.getAttribute.apply(this.elt, args);
1574 | }
1575 | return this;
1576 | };
1577 |
1578 | /**
1579 | * Apply filter on current element.
1580 | * If called multiple times,
1581 | * these filters will be chained together and combine to a bigger SVG filter.
1582 | *
1583 | * @function filter
1584 | * @memberof SVGElement.prototype
1585 | * @param {String} filter BLUR, GRAY, INVERT, THRESHOLD, OPAQUE, ERODE, DILATE (defined in p5's constants)
1586 | * @param {Any} argument Argument for that filter
1587 | */
1588 | SVGElement.prototype.filter = function(filter, arg) {
1589 | p5.SVGFilters.apply(this, filter, arg);
1590 | return this;
1591 | };
1592 |
1593 | /**
1594 | * Remove applied filter on current element
1595 | * After called, rest filters will be chained together
1596 | * and combine to a new SVG filter.
1597 | *
1598 | * @function unfilter
1599 | * @memberof SVGElement.prototype
1600 | * @param {String} filter BLUR, GRAY, INVERT, THRESHOLD, OPAQUE, ERODE, DILATE (defined in p5's constants)
1601 | * @param {Any} argument Argument for that filter
1602 | */
1603 | SVGElement.prototype.unfilter = function(filterName, arg) {
1604 | var filters = this.attribute('data-p5-svg-filters') || '[]';
1605 | filters = JSON.parse(filters);
1606 | if (arg === undefined) {
1607 | arg = null;
1608 | }
1609 | var found = false;
1610 | filters = filters.reverse().filter(function(filter) {
1611 | if ((filter[0] === filterName) && (filter[1] === arg) && !found) {
1612 | found = true;
1613 | return false;
1614 | }
1615 | return true;
1616 | }).reverse();
1617 | this.attribute('data-p5-svg-filters', JSON.stringify(filters));
1618 | p5.SVGFilters.apply(this, null);
1619 | return this;
1620 | };
1621 |
1622 | /**
1623 | * Create SVGElement
1624 | *
1625 | * @function create
1626 | * @memberof SVGElement
1627 | * @param {String} nodeName
1628 | * @param {Object} [attributes] Attributes for the element
1629 | * @return {SVGElement}
1630 | */
1631 | SVGElement.create = function(nodeName, attributes) {
1632 | attributes = attributes || {};
1633 | var elt = document.createElementNS('http://www.w3.org/2000/svg', nodeName);
1634 | Object.keys(attributes).forEach(function(k) {
1635 | elt.setAttribute(k, attributes[k]);
1636 | });
1637 | return new SVGElement(elt);
1638 | };
1639 |
1640 | /**
1641 | * Tell if current element matching given selector.
1642 | * This is polyfill from MDN.
1643 | *
1644 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
1645 | *
1646 | * @function matches
1647 | * @memberof SVGElement.prototype
1648 | * @param {String} selector CSS Selector
1649 | * @return {Bool}
1650 | */
1651 | SVGElement.prototype.matches = function(selector) {
1652 | var element = this.elt;
1653 | var matches = (element.document || element.ownerDocument).querySelectorAll(selector);
1654 | var i = 0;
1655 | while (matches[i] && matches[i] !== element) {
1656 | i++;
1657 | }
1658 | return matches[i] ? true : false;
1659 | };
1660 |
1661 | /**
1662 | * Get defs element, or create one if not exists
1663 | *
1664 | * @private
1665 | */
1666 | SVGElement.prototype._getDefs = function() {
1667 | var svg = this.parentNode('svg');
1668 | var defs = svg.query('defs');
1669 | if (defs[0]) {
1670 | defs = defs[0];
1671 | } else {
1672 | defs = SVGElement.create('defs');
1673 | svg.append(defs);
1674 | }
1675 | return defs;
1676 | };
1677 |
1678 | /**
1679 | * Get parentNode.
1680 | * If selector not given, returns parentNode.
1681 | * Otherwise, will look up all ancestors,
1682 | * and return closest element matching given selector,
1683 | * or return null if not found.
1684 | *
1685 | * @function parentNode
1686 | * @memberof SVGElement.prototype
1687 | * @param {String} [selector] CSS Selector
1688 | * @return {SVGElement}
1689 | */
1690 | SVGElement.prototype.parentNode = function(selector) {
1691 | if (!selector) {
1692 | return new SVGElement(this.elt.parentNode);
1693 | }
1694 | var elt = this;
1695 | while (elt) {
1696 | elt = this.parentNode();
1697 | if (elt && elt.matches(selector)) {
1698 | return elt;
1699 | }
1700 | }
1701 | return null;
1702 | };
1703 |
1704 | p5.SVGElement = SVGElement;
1705 | };
1706 |
1707 | },{}],6:[function(require,module,exports){
1708 | // SVG Filter
1709 |
1710 | module.exports = function(p5) {
1711 | var _filter = p5.prototype.filter;
1712 |
1713 | var SVGFilters = require('./p5.SVGFilters')(p5);
1714 |
1715 | /**
1716 | * Register a custom SVG Filter
1717 | *
1718 | * @function registerSVGFilter
1719 | * @memberof p5.prototype
1720 | * @param {String} name Name for Custom SVG filter
1721 | * @param {Function} filterFunction filterFunction(inGraphicsName, resultGraphicsName, value)
1722 | * should return SVGElement or Array of SVGElement.
1723 | * @example
1724 | * registerSVGFilter('myblur', function(inGraphicsName, resultGraphicsName, value) {
1725 | * return SVGElement.create('feGaussianBlur', {
1726 | * stdDeviation: val,
1727 | * in: inGraphics,
1728 | * result: resultGraphics,
1729 | * 'color-interpolation-filters': 'sRGB'
1730 | * });
1731 | * });
1732 | * filter('myblur', 5);
1733 | */
1734 | p5.prototype.registerSVGFilter = function(name, fn) {
1735 | SVGFilters[name] = fn;
1736 | };
1737 |
1738 | p5.prototype.filter = function(operation, value) {
1739 | var svg = this._renderer.svg;
1740 | if (svg) {
1741 | // move nodes to a new
1742 | var nodes = svg.children || svg.childNodes; // childNodes is for IE
1743 | var g = p5.SVGElement.create('g');
1744 | this._renderer._setGCFlag(g.elt);
1745 | svg.appendChild(g.elt);
1746 | // convert nodeList to array and use forEach
1747 | // instead of using for loop,
1748 | // which is buggy due to the length changed during append
1749 | nodes = Array.prototype.slice.call(nodes);
1750 | nodes.forEach(function(node) {
1751 | if (node !== g.elt && (node.nodeName.toLowerCase() !== 'defs')) {
1752 | g.elt.appendChild(node);
1753 | }
1754 | });
1755 |
1756 | // apply filter
1757 | g.filter(operation, value);
1758 |
1759 | // create new so that new element won't be influenced by the filter
1760 | g = p5.SVGElement.create('g');
1761 | this._renderer._setGCFlag(g.elt);
1762 | this._renderer.svg.appendChild(g.elt);
1763 | this._renderer.drawingContext.__currentElement = g.elt;
1764 | } else {
1765 | _filter.apply(this, arguments);
1766 | }
1767 | };
1768 | };
1769 |
1770 | },{"./p5.SVGFilters":10}],7:[function(require,module,exports){
1771 | module.exports = function(p5) {
1772 | /**
1773 | * @namespace p5
1774 | */
1775 | require('./p5.RendererSVG')(p5);
1776 | require('./rendering')(p5);
1777 | require('./io')(p5);
1778 | require('./element')(p5);
1779 | require('./filters')(p5);
1780 |
1781 | // attach constants to p5 instance
1782 | var constants = require('./constants');
1783 | Object.keys(constants).forEach(function(k) {
1784 | p5.prototype[k] = constants[k];
1785 | });
1786 | };
1787 |
1788 | },{"./constants":4,"./element":5,"./filters":6,"./io":8,"./p5.RendererSVG":9,"./rendering":11}],8:[function(require,module,exports){
1789 | module.exports = function(p5) {
1790 | /**
1791 | * Convert SVG Element to jpeg / png data url
1792 | *
1793 | * @private
1794 | * @param {SVGElement} svg SVG Element
1795 | * @param {String} mine Mine
1796 | * @param {Function} callback
1797 | */
1798 | var svg2img = function(svg, mine, callback) {
1799 | svg = (new XMLSerializer()).serializeToString(svg);
1800 | svg = 'data:image/svg+xml;charset=utf-8,' + encodeURI(svg);
1801 | if (mine == 'image/svg+xml') {
1802 | callback(null, svg);
1803 | return;
1804 | }
1805 | var img = new Image();
1806 | var canvas = document.createElement('canvas');
1807 | var ctx = canvas.getContext('2d');
1808 | img.onload = function() {
1809 | canvas.width = img.width;
1810 | canvas.height = img.height;
1811 | ctx.drawImage(img, 0, 0);
1812 | var dataURL = canvas.toDataURL(mine);
1813 | callback(null, dataURL);
1814 | };
1815 | img.src = svg;
1816 | };
1817 |
1818 | /**
1819 | * Get SVG frame, and convert to target type
1820 | *
1821 | * @private
1822 | * @param {Object} options
1823 | * @param {SVGElement} options.svg SVG Element, defaults to current svg element
1824 | * @param {String} options.filename
1825 | * @param {String} options.ext Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
1826 | * @param {Function} options.callback
1827 | */
1828 | p5.prototype._makeSVGFrame = function(options) {
1829 | var filename = options.filename || 'untitled';
1830 | var ext = options.extension;
1831 | ext = ext || this._checkFileExtension(filename, ext)[1];
1832 | var regexp = new RegExp('\\.' + ext + '$');
1833 | filename = filename.replace(regexp, '');
1834 | if (ext === '') {
1835 | ext = 'svg';
1836 | }
1837 | var mine = {
1838 | png: 'image/png',
1839 | jpeg: 'image/jpeg',
1840 | jpg: 'image/jpeg',
1841 | svg: 'image/svg+xml'
1842 | }[ext];
1843 | if (!mine) {
1844 | throw new Error('Fail to getFrame, invalid extension: ' + ext + ', please use png | jpeg | jpg | svg.');
1845 | }
1846 |
1847 | var svg = options.svg || this._renderer.svg;
1848 | svg2img(svg, mine, function(err, dataURL) {
1849 | var downloadMime = 'image/octet-stream';
1850 | dataURL = dataURL.replace(mine, downloadMime);
1851 | options.callback(err, {
1852 | imageData: dataURL,
1853 | filename: filename,
1854 | ext: ext
1855 | });
1856 | });
1857 | };
1858 |
1859 | /**
1860 | * Save the current SVG as an image. In Safari, will open the
1861 | * image in the window and the user must provide their own
1862 | * filename on save-as. Other browsers will either save the
1863 | * file immediately, or prompt the user with a dialogue window.
1864 | *
1865 | * @function saveSVG
1866 | * @memberof p5.prototype
1867 | * @param {Graphics|Element|SVGElement} [svg] Source to save
1868 | * @param {String} [filename]
1869 | * @param {String} [extension] Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
1870 | */
1871 | p5.prototype.saveSVG = function() {
1872 | // don't use slice on arguments because it prevents optimizations
1873 | var args = arguments;
1874 | args = [args[0], args[1], args[2]];
1875 |
1876 | var svg;
1877 |
1878 | if (args[0] instanceof p5.Graphics) {
1879 | svg = args[0]._renderer.svg;
1880 | args.shift();
1881 | }
1882 |
1883 | if (args[0] && args[0].elt) {
1884 | svg = args[0].elt;
1885 | args.shift();
1886 | }
1887 |
1888 | if (typeof args[0] == 'object') {
1889 | svg = args[0];
1890 | args.shift();
1891 | }
1892 |
1893 | var filename = args[0];
1894 | var ext = args[1];
1895 |
1896 | var p = this;
1897 | this._makeSVGFrame({
1898 | svg: svg,
1899 | filename: filename,
1900 | extension: ext,
1901 | callback: function(err, frame) {
1902 | p.downloadFile(frame.imageData, frame.filename, frame.ext);
1903 | }
1904 | });
1905 | };
1906 |
1907 | /**
1908 | * Extends p5's saveFrames with SVG support
1909 | *
1910 | * @function saveFrames
1911 | * @memberof p5.prototype
1912 | * @param {String} filename filename
1913 | * @param {String} extension Extension: 'svg' or 'jpg' or 'jpeg' or 'png'
1914 | * @param {Number} duration duration
1915 | * @param {Number} fps fps
1916 | * @param {Function} callback callback
1917 | */
1918 | var _saveFrames = p5.prototype.saveFrames;
1919 | p5.prototype.saveFrames = function(filename, extension, duration, fps, callback) {
1920 | var args = arguments;
1921 |
1922 | if (!this._renderer.svg) {
1923 | _saveFrames.apply(this, args);
1924 | return;
1925 | }
1926 |
1927 | duration = duration || 3;
1928 | duration = p5.prototype.constrain(duration, 0, 15);
1929 | duration = duration * 1000;
1930 | fps = fps || 15;
1931 | fps = p5.prototype.constrain(fps, 0, 22);
1932 | var count = 0;
1933 |
1934 | var frames = [];
1935 | var pending = 0;
1936 |
1937 | var p = this;
1938 | var frameFactory = setInterval(function () {
1939 | (function(count) {
1940 | pending++;
1941 | p._makeSVGFrame({
1942 | filename: filename + count,
1943 | extension: extension,
1944 | callback: function(err, frame) {
1945 | frames[count] = frame;
1946 | pending--;
1947 | }
1948 | });
1949 | })(count);
1950 | count++;
1951 | }, 1000 / fps);
1952 |
1953 | var done = function() {
1954 | if (pending > 0) {
1955 | setTimeout(function() {
1956 | done();
1957 | }, 10);
1958 | return;
1959 | }
1960 | if (callback) {
1961 | callback(frames);
1962 | } else {
1963 | frames.forEach(function(f) {
1964 | p.downloadFile(f.imageData, f.filename, f.ext);
1965 | });
1966 | }
1967 | };
1968 |
1969 | setTimeout(function () {
1970 | clearInterval(frameFactory);
1971 | done();
1972 | }, duration + 0.01);
1973 | };
1974 |
1975 | /**
1976 | * Extends p5's save method with SVG support
1977 | *
1978 | * @function save
1979 | * @memberof p5.prototype
1980 | * @param {Graphics|Element|SVGElement} [source] Source to save
1981 | * @param {String} [filename] filename
1982 | */
1983 | var _save = p5.prototype.save;
1984 | p5.prototype.save = function() {
1985 | var args = arguments;
1986 | args = [args[0], args[1]];
1987 |
1988 | var svg;
1989 |
1990 | if (args[0] instanceof p5.Graphics) {
1991 | var svgcanvas = args[0].elt;
1992 | svg = svgcanvas.svg;
1993 | args.shift();
1994 | }
1995 |
1996 | if (args[0] && args[0].elt) {
1997 | svg = args[0].elt;
1998 | args.shift();
1999 | }
2000 |
2001 | if (typeof args[0] == 'object') {
2002 | svg = args[0];
2003 | args.shift();
2004 | }
2005 |
2006 | svg = svg || (this._renderer && this._renderer.svg);
2007 |
2008 | var filename = args[0];
2009 | var supportedExtensions = ['jpeg', 'png', 'jpg', 'svg', ''];
2010 | var ext = this._checkFileExtension(filename, '')[1];
2011 |
2012 | var useSVG = svg && svg.nodeName && svg.nodeName.toLowerCase() === 'svg' && supportedExtensions.indexOf(ext) > -1;
2013 |
2014 | if (useSVG) {
2015 | this.saveSVG(svg, filename);
2016 | } else {
2017 | return _save.apply(this, arguments);
2018 | }
2019 | };
2020 |
2021 | /**
2022 | * Custom get in p5.svg (handles http and dataurl)
2023 | * @private
2024 | */
2025 | p5.prototype._svg_get = function(path, successCallback, failureCallback) {
2026 | if (path.indexOf('data:') === 0) {
2027 | if (path.indexOf(',') === -1) {
2028 | failureCallback(new Error('Fail to parse dataurl: ' + path));
2029 | return;
2030 | }
2031 | var svg = path.split(',').pop();
2032 | // force request to dataurl to be async
2033 | // so that it won't make preload mess
2034 | setTimeout(function() {
2035 | if (path.indexOf(';base64,') > -1) {
2036 | svg = atob(svg);
2037 | } else {
2038 | svg = decodeURIComponent(svg);
2039 | }
2040 | successCallback(svg);
2041 | }, 1);
2042 | return svg;
2043 | } else {
2044 | this.httpGet(path, successCallback);
2045 | return null;
2046 | }
2047 | };
2048 |
2049 | /**
2050 | * loadSVG (like loadImage, but will return SVGElement)
2051 | *
2052 | * @function loadSVG
2053 | * @memberof p5.prototype
2054 | * @returns {p5.SVGElement}
2055 | */
2056 | p5.prototype.loadSVG = function(path, successCallback, failureCallback) {
2057 | var div = document.createElement('div');
2058 | var element = new p5.SVGElement(div);
2059 | this._svg_get(path, function(svg) {
2060 | div.innerHTML = svg;
2061 | svg = div.querySelector('svg');
2062 | if (!svg) {
2063 | if (failureCallback) {
2064 | failureCallback(new Error('Fail to create