├── .npmignore
├── .gitignore
├── bin
└── canvgc
├── example
├── simple.svg
├── generate.sh
├── simple.js
├── example.html
├── complex.svg
└── complex.js
├── src
├── canvgc
│ ├── randomId.coffee
│ ├── canvgc.coffee
│ ├── coa.coffee
│ ├── stenographer.coffee
│ └── CanvasRenderingContext2DShim.coffee
├── browser
│ └── Painter.coffee
└── canvg
│ ├── StackBlur.js
│ └── canvg.js
├── Makefile
├── package.json
├── LICENSE
├── README.md
└── test
├── testStenographer.coffee
└── testShim.coffee
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | example
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
--------------------------------------------------------------------------------
/bin/canvgc:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('../lib/canvgc/coa').run();
4 |
--------------------------------------------------------------------------------
/example/simple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/canvgc/randomId.coffee:
--------------------------------------------------------------------------------
1 | ID_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"
2 | ID_CHARS_LEN = ID_CHARS.length
3 |
4 | randomId = (prefix='', n=6)->
5 | digits = '';
6 | for i in [0..n]
7 | digits += ID_CHARS.charAt(Math.floor(Math.random() * ID_CHARS_LEN))
8 | return prefix + digits;
9 |
10 | module.exports = randomId;
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PATH := ./node_modules/.bin:${PATH}
2 |
3 | .PHONY : init clean build dist test publish
4 |
5 | init:
6 | npm install
7 |
8 | clean:
9 | rm -rf lib/
10 |
11 | build:
12 | coffee -o lib/ -c src/ && mkdir -p lib/canvg && cp -r src/canvg/*.js lib/canvg
13 |
14 | test:
15 | mocha test/*.coffee -r coffee-script --reporter spec
16 |
17 | dist: clean init build
18 |
19 | publish: dist
20 | npm publish
--------------------------------------------------------------------------------
/example/generate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
4 | PROJECT_ROOT=$( cd "$( dirname "$SCIRPT_DIR" )" && pwd )
5 |
6 | node "$PROJECT_ROOT/bin/canvgc" "$PROJECT_ROOT/example/simple.svg" "$PROJECT_ROOT/example/simple.js" --prepend 'if(typeof window.canvgc=="undefined")window.canvgc={};window.canvgc.simple =' --append ';'
7 | node "$PROJECT_ROOT/bin/canvgc" "$PROJECT_ROOT/example/complex.svg" "$PROJECT_ROOT/example/complex.js" --prepend 'if(typeof window.canvgc=="undefined")window.canvgc={};window.canvgc.complex =' --append ';'
8 |
--------------------------------------------------------------------------------
/example/simple.js:
--------------------------------------------------------------------------------
1 | if(typeof window.canvgc=="undefined")window.canvgc={};window.canvgc.simple ={"w":100,"h":100,"d":[function($,p){
2 | $.save();
3 | p.stack++;
4 | $.strokeStyle = "rgba(0,0,0,0)";
5 | $.lineCap = "butt";
6 | $.lineJoin = "miter";
7 | $.miterLimit = 4;
8 | $.font = " 10px sans-serif";
9 | $.translate(0,0);
10 | $.save();
11 | p.stack++;
12 | $.fillStyle = "red";
13 | $.strokeStyle = "black";
14 | $.lineWidth = 2;
15 | $.font = " 10px sans-serif";
16 | $.beginPath();
17 | $.arc(50,50,40,0,6.283185307179586,true);
18 | $.closePath();
19 | $.fill();
20 | $.stroke();
21 | $.restore();
22 | p.stack--;
23 | $.restore();
24 | p.stack--;
25 | }],"i":{}};
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "canvgc",
3 | "description": "A port of canvg & jscanvas, which pareses svg input and outputs js code to reproduce on a canvas.",
4 | "version": "0.1.5",
5 | "author": "Nathan Muir ",
6 | "main": "./lib/canvgc.js",
7 | "bin": {
8 | "canvgc": "./bin/canvgc"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/nathan-muir/canvgc.git"
13 | },
14 | "dependencies": {
15 | "canvas": "1.3.x",
16 | "xmldom": ">=0.1.16",
17 | "rgbcolor": ">=0.0.3",
18 | "underscore": ">=1.5.1",
19 | "coa": "~0.3.7"
20 | },
21 | "devDependencies": {
22 | "coffee-script": ">=1.6.3",
23 | "mocha": "1.12.x",
24 | "chai": "1.7.x"
25 | },
26 | "keywords": ["canvas", "svg"],
27 | "license": "MIT",
28 | "engines": { "node": ">= 0.8.15" }
29 | }
30 |
--------------------------------------------------------------------------------
/src/canvgc/canvgc.coffee:
--------------------------------------------------------------------------------
1 | Canvas = require("canvas")
2 | canvg = require('../canvg/canvg')
3 | CanvasRenderingContext2DShim = require('./CanvasRenderingContext2DShim')
4 | Stenographer = require('./stenographer')
5 |
6 | canvgc = (svgAsText, callsPerFunc = null, onComplete)->
7 | canvas = new Canvas()
8 | context2d = canvas.getContext('2d')
9 | stenographer = new Stenographer(callsPerFunc)
10 | shim = new CanvasRenderingContext2DShim(stenographer, context2d)
11 | canvas.getContext = (type)->
12 | if type == '2d'
13 | return shim
14 | else
15 | return null
16 |
17 | canvg canvas, svgAsText,
18 | ignoreDimensions: true
19 | ignoreClear: true
20 | ignoreMouse: true
21 | renderCallback: ()->
22 | onComplete.call(null, null, stenographer.toJS(canvas.width, canvas.height))
23 | return
24 | return
25 |
26 | module.exports = canvgc
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2011 Gabe Lerner (gabelerner@gmail.com) - http://code.google.com/p/canvg/
2 | Copyright (c) Apr 6 08:05:34 MTCC Michael Thomas - https://code.google.com/p/jscapturecanvas/
3 |
4 | Permission is hereby granted, free of charge, to any person
5 | obtaining a copy of this software and associated documentation
6 | files (the "Software"), to deal in the Software without
7 | restriction, including without limitation the rights to use,
8 | copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/example/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | `canvgc` Demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
32 |
33 |
--------------------------------------------------------------------------------
/src/canvgc/coa.coffee:
--------------------------------------------------------------------------------
1 | PKG = require('../../package.json')
2 | FS = require('fs')
3 | canvgc = require('./canvgc')
4 |
5 | #process.on("uncaughtException", ()-> console.log(arguments))
6 |
7 | module.exports = require('coa').Cmd()
8 | .helpful()
9 | .name(PKG.name)
10 | .title(PKG.description)
11 | .opt()
12 | .name("input")
13 | .title("Input file")
14 | .short("i").long("input")
15 | .val((val) ->
16 | return val or @reject("Option --input must have a value.")
17 | )
18 | .end()
19 | .opt()
20 | .name("output")
21 | .title("Output file")
22 | .short("o").long("output")
23 | .val((val) ->
24 | return val or @reject("Option --output must have a value.")
25 | )
26 | .end()
27 | .opt()
28 | .name("chunk")
29 | .title("The number of CanvasRenderingContext2D commands to call per function")
30 | .long("chunk")
31 | .val((val) ->
32 | n = parseInt(val)
33 | if isNaN(n)
34 | return null
35 | else
36 | return n
37 | )
38 | .end()
39 | .opt()
40 | .name("prepend")
41 | .title("Javascript to prepend to the output file")
42 | .long("prepend")
43 | .end()
44 | .opt()
45 | .name("append")
46 | .title("Javascript to append to the output file")
47 | .long("append")
48 | .end()
49 | .arg()
50 | .name('input').title('Alias to --input')
51 | .end()
52 | .arg()
53 | .name('output').title('Alias to --output')
54 | .end()
55 |
56 | .act (opts, args)->
57 | input = args?.input ? opts.input
58 | output = args?.output ? opts.output
59 | unless input? and output?
60 | return @usage()
61 |
62 | FS.readFile input, 'utf8', (err, data)->
63 | if (err)
64 | throw err;
65 | canvgc data, opts.chunk, (err, jsData)->
66 | if (err)
67 | throw err;
68 | if opts.prepend?
69 | jsData = "#{opts.prepend}#{jsData}";
70 | if opts.append?
71 | jsData += opts.append
72 | FS.writeFile output, jsData, 'utf8', (err)->
73 | return
--------------------------------------------------------------------------------
/src/browser/Painter.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | An Example of how to render the output of COA
3 | ###
4 | class Painter
5 |
6 | ###
7 | * @param {Array} renderList
8 | * @param {Function} defer
9 | ###
10 | constructor: (@plan, @defer)->
11 | @renderId = 0
12 | @ready = false
13 | @renderCtx =
14 | stack: 0
15 | @renderListPosition = 0
16 | @rendering = false
17 |
18 | loadImages: (cb)->
19 | imageNames = _.keys(@plan.i)
20 | if imageNames.length == 0
21 | @ready = true
22 | @defer(cb)
23 | for name, data of @plan.i
24 | do (name, data) =>
25 | img = new Image()
26 | img.onload = () =>
27 | @renderCtx[name] = img
28 | if _.every(imageNames, (n)=> @renderCtx[n]?)
29 | @ready = true
30 | @defer(cb)
31 | return
32 | img.src = data
33 | return
34 |
35 | cancel: (context2d)->
36 | @renderListPosition = 0
37 | @rendering = false
38 | while @renderCtx.stack > 0
39 | context2d.restore()
40 | @renderCtx.stack--
41 | return
42 |
43 | renderImmediately: (context2d)->
44 | unless @ready
45 | throw new Error('Painter not yet ready.')
46 | for f in @plan.d
47 | f(context2d, @renderCtx)
48 | return
49 |
50 | renderDeferred: (context2d, cb)->
51 | unless @ready
52 | throw new Error('Painter not yet ready.')
53 | @rendering = true
54 | @renderId += 1
55 | @renderListPosition = 0
56 |
57 | enqueue = (renderId)=>
58 | if @renderListPosition < @plan.d.length
59 | fn = @plan.d[@renderListPosition]
60 | @renderListPosition += 1
61 | @defer ()=>
62 | if !@rendering or renderId != @renderId
63 | console.log('Rendering cancelled for renderId', renderId);
64 | return
65 | fn(context2d, @renderCtx)
66 | enqueue(renderId)
67 | return
68 | else
69 | @renderId = 0
70 | @renderListPosition = 0
71 | @rendering = false
72 | @defer(cb)
73 |
74 | return
75 | enqueue(@renderId)
76 | return
77 |
78 | window.Painter = Painter;
--------------------------------------------------------------------------------
/example/complex.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | canvgc
2 | =====
3 |
4 | `canvgc` is a nodejs tool for compiling SVG to html5 CanvasRenderingContext2D commands.
5 |
6 | ## Version
7 | 0.1.3
8 |
9 | ## Installation
10 | ```sh
11 | npm install canvgc
12 | ```
13 |
14 | ## Example
15 |
16 | ```xml
17 |
20 | ```
21 |
22 | ```js
23 | {
24 | "w": 100,
25 | "h": 100,
26 | "d": [
27 | function ($, p) {
28 | $.save();
29 | p.stack++;
30 | $.strokeStyle = "rgba(0,0,0,0)";
31 | $.miterLimit = 4;
32 | $.font = " 10px sans-serif";
33 | $.translate(0,0);
34 | $.save();
35 | p.stack++;
36 | $.fillStyle = "red";
37 | $.strokeStyle = "black";
38 | $.beginPath();
39 | $.arc(50,50,40,0,6.283185307179586,true);
40 | $.closePath();
41 | $.fill();
42 | $.stroke();
43 | $.restore();
44 | p.stack--;
45 | $.restore();
46 | p.stack--;
47 | }
48 | ],
49 | "i": {}
50 | }
51 | ```
52 |
53 | ## Usage (Server Side)
54 | ```bash
55 | canvgc file.svg file.js # basic conversion
56 |
57 | canvgc file.svg file.js --prepend 'window.canvgc={"file":' --append '};' # assign to some variable
58 |
59 | canvgc file.svg file.js --prepend 'callback(' --append ');' # call a function with result
60 |
61 | canvgc file.svg file.js --chunk 500 # break in to blocks of code (prevent event loop starvation when rendering large files)
62 | ```
63 |
64 |
65 | ## Usage (Client Side)
66 | ```js
67 | // basic render at original width & height, without transforms
68 | function render(canvas, plan){
69 | var painter = new Painter(plan,function(cb){window.setTimeout(cb,0);}); // can use setImmediate poly-fill
70 |
71 | canvas.width = plan.w;
72 | canvas.height = plan.h;
73 |
74 | // need to wait on load images - even for dataURL images.
75 | painter.loadImages(function(){
76 | // renders the whole file (even if chunked) in one go
77 | painter.renderImmediately(canvas.getContext('2d'))
78 | // if large file & is chunked
79 | //painter.renderDeferred(canvas.getContext('2d'), function(){ console.log('done');})
80 | })
81 | }
82 |
83 | render(document.getElementById('canvas'), window.canvgc.file);
84 | ```
85 |
86 | ## Credits
87 |
88 | Gabe Lerner (gabelerner@gmail.com) - http://code.google.com/p/canvg/
89 |
90 | Michael Thomas - https://code.google.com/p/jscapturecanvas/
91 |
92 | ## Thank You
93 |
94 | To the authors of all of the projects on which this depends & is built upon.
95 |
--------------------------------------------------------------------------------
/src/canvgc/stenographer.coffee:
--------------------------------------------------------------------------------
1 | Canvas = require("canvas")
2 | randomId = require('./randomId')
3 |
4 | class Stenographer
5 |
6 | constructor: (@callsPerFunc=null)->
7 |
8 | @output = ''
9 | @images = {}
10 | @contextName = '$'
11 | @propertiesName = 'p'
12 | @calls = 0
13 |
14 | @startFunc = "function(#{@contextName},#{@propertiesName}){\n"
15 | @endFunc = "}"
16 | @splitFunc = "#{@endFunc},#{@startFunc}"
17 |
18 | toJS: (width, height)->
19 | "{\"w\":#{JSON.stringify(width)},\"h\":#{JSON.stringify(height)},\"d\":[#{@startFunc}#{@output}#{@endFunc}],\"i\":#{JSON.stringify(@images)}}"
20 |
21 | serialize: (value, roundNumbers=false)->
22 | t = typeof value;
23 |
24 | if t == 'number' and roundNumbers
25 | return (+value.toFixed(6)).toString()
26 | else if t in ["boolean", "number", "string"] or value == null
27 | return JSON.stringify(value)
28 | else if value?.targetId?
29 | return "#{@propertiesName}.#{value.targetId}"
30 | else if value?.tagName == "IMG" || value?.src?
31 | imageId = randomId('img')
32 | @images[imageId] = @getBase64Image(value)
33 | return "#{@propertiesName}.#{imageId}"
34 | else if value?.tagName == "CANVAS" || value.toString() == '[object Canvas]'
35 | imageId = randomId('img')
36 | @images[imageId] = value.toDataURL('image/png')
37 | return "#{@propertiesName}.#{imageId}"
38 | else
39 | throw new Error("Could not serialize value: #{value.toString()}")
40 |
41 | getBase64Image:(img) ->
42 | # Create an empty canvas element
43 | canvas = new Canvas()
44 | canvas.width = img.width
45 | canvas.height = img.height
46 | # Copy the image contents to the canvas
47 | ctx = canvas.getContext("2d")
48 | ctx.drawImage(img, 0, 0)
49 | return canvas.toDataURL("image/png")
50 |
51 | serializeArgs: (args, roundNumbers=false)->
52 | out = []
53 | for arg in args
54 | out.push(@serialize(arg, roundNumbers))
55 | return out.join(',')
56 |
57 | setContextProperty: (propertyName, value)->
58 | @output += "#{@contextName}.#{propertyName} = #{@serialize(value)};\n"
59 | return
60 |
61 | invokeChildObject: (name, fn, args)->
62 | @output += "#{@propertiesName}.#{name}.#{fn}(#{@serializeArgs(args)});\n"
63 | return
64 |
65 | shouldRoundNumbers: (fn)->
66 | return fn in ["moveTo","lineTo","bezierCurveTo","quadraticCurveTo"]
67 |
68 | invokeContext: (fn, args, assignTarget = null)->
69 | if assignTarget?
70 | @output += "#{@propertiesName}.#{assignTarget} = ";
71 |
72 | @output += "#{@contextName}.#{fn}(#{@serializeArgs(args, @shouldRoundNumbers(fn))});\n"
73 |
74 | if fn == "save"
75 | @output += "#{@propertiesName}.stack++;\n"
76 | if fn == "restore"
77 | @output += "#{@propertiesName}.stack--;\n"
78 |
79 | @calls += 1
80 | if @callsPerFunc? and @calls % @callsPerFunc == 0
81 | @output += @splitFunc
82 | return
83 |
84 |
85 | module.exports = Stenographer
--------------------------------------------------------------------------------
/src/canvgc/CanvasRenderingContext2DShim.coffee:
--------------------------------------------------------------------------------
1 | _ = require('underscore')
2 | randomId = require('./randomId')
3 |
4 | Function::property = (prop, desc) ->
5 | Object.defineProperty @prototype, prop, desc
6 |
7 |
8 |
9 | class CanvasRenderingContext2DShim
10 | ###
11 | * @param {Stenographer} stenographer
12 | * @param {CanvasRenderingContext2D} realContext
13 | * @param {Object} defaults
14 | ###
15 | constructor: (@stenographer, @context2d) ->
16 |
17 | get: (name)->
18 | return @context2d[name]
19 |
20 | set: (name, value)->
21 | @stenographer.setContextProperty(name, value)
22 | @context2d[name] = value
23 | return
24 |
25 | invoke:(fn, args, assignTarget = null) ->
26 | # reconcile & output changed properties since last invocation
27 | @stenographer.invokeContext(fn, args, assignTarget)
28 | return @context2d[fn]?.apply(@context2d, args)
29 |
30 | fill:->
31 | return @invoke("fill", arguments)
32 |
33 | stroke:->
34 | return @invoke("stroke", arguments)
35 |
36 | translate:->
37 | return @invoke("translate", arguments)
38 |
39 | transform:->
40 | return @invoke("transform", arguments)
41 |
42 | rotate:->
43 | return @invoke("rotate", arguments)
44 |
45 | scale:->
46 | return @invoke("scale", arguments)
47 |
48 | save:->
49 | return @invoke("save", arguments)
50 |
51 | restore:->
52 | return @invoke("restore", arguments)
53 |
54 | beginPath:->
55 | return @invoke("beginPath", arguments)
56 |
57 | closePath:->
58 | return @invoke("closePath", arguments)
59 |
60 | moveTo:->
61 | return @invoke("moveTo", arguments)
62 |
63 | lineTo:->
64 | return @invoke("lineTo", arguments)
65 |
66 | clip:->
67 | return @invoke("clip", arguments)
68 |
69 | quadraticCurveTo:->
70 | return @invoke("quadraticCurveTo", arguments)
71 |
72 | bezierCurveTo:->
73 | return @invoke("bezierCurveTo", arguments)
74 |
75 | arc:->
76 | return @invoke("arc", arguments)
77 |
78 | createPattern:->
79 | targetId = randomId('p')
80 | pattern = @invoke("createPattern", arguments, targetId)
81 | pattern.targetId = targetId
82 | return pattern
83 |
84 | createLinearGradient:->
85 | targetId = randomId('lg')
86 | linearGradient = @invoke("createLinearGradient", arguments, targetId)
87 | linearGradient.targetId = targetId
88 | actualAddColorStop = linearGradient.addColorStop
89 | # override the colorstop function
90 | linearGradient.addColorStop = () =>
91 | @stenographer.invokeChildObject(targetId, "addColorStop", arguments)
92 | # execute in parent
93 | return actualAddColorStop.apply(linearGradient, arguments)
94 | return linearGradient
95 |
96 | createRadialGradient:->
97 | targetId = randomId('rg')
98 | radialGradient = @invoke("createRadialGradient", arguments, targetId)
99 | radialGradient.targetId = targetId
100 | actualAddColorStop = radialGradient.addColorStop
101 | # override the colorstop function
102 | radialGradient.addColorStop = () =>
103 | @stenographer.invokeChildObject(targetId, "addColorStop", arguments)
104 | # execute in parent
105 | return actualAddColorStop.apply(radialGradient, arguments)
106 | return radialGradient
107 |
108 | fillText:->
109 | return @invoke("fillText", arguments)
110 |
111 | strokeText:->
112 | return @invoke("strokeText", arguments)
113 |
114 | measureText:->
115 | return @invoke("measureText", arguments)
116 |
117 | drawImage:->
118 | return @invoke("drawImage", arguments)
119 |
120 | fillRect:->
121 | return @invoke("fillRect", arguments)
122 |
123 | clearRect:->
124 | return @invoke("clearRect", arguments)
125 |
126 | getImageData:->
127 | return @invoke("getImageData", arguments)
128 |
129 | putImageData:->
130 | return @invoke("putImageData", arguments)
131 |
132 | isPointPath:->
133 | return @invoke("isPointPath", arguments)
134 |
135 | @property 'canvas',
136 | get: ()-> @get('canvas')
137 | set: (canvas)-> @set('canvas', canvas)
138 |
139 | @property 'fillStyle',
140 | get: ()-> @get('fillStyle')
141 | set: (fillStyle)-> @set('fillStyle', fillStyle)
142 |
143 | @property 'font',
144 | get: ()-> @get('font')
145 | set: (font)-> @set('font', font)
146 |
147 | @property 'globalAlpha',
148 | get: ()-> @get('globalAlpha')
149 | set: (globalAlpha)-> @set('globalAlpha', globalAlpha)
150 |
151 | @property 'globalCompositeOperation',
152 | get: ()-> @get('globalCompositeOperation')
153 | set: (globalCompositeOperation)-> @set('globalCompositeOperation', globalCompositeOperation)
154 |
155 | @property 'lineCap',
156 | get: ()-> @get('lineCap')
157 | set: (lineCap)-> @set('lineCap', lineCap)
158 |
159 | @property 'lineDashOffset',
160 | get: ()-> @get('lineDashOffset')
161 | set: (lineDashOffset)-> @set('lineDashOffset', lineDashOffset)
162 |
163 | @property 'lineJoin',
164 | get: ()-> @get('lineJoin')
165 | set: (lineJoin)-> @set('lineJoin', lineJoin)
166 |
167 | @property 'lineWidth',
168 | get: ()-> @get('lineWidth')
169 | set: (lineWidth)-> @set('lineWidth', lineWidth)
170 |
171 | @property 'miterLimit',
172 | get: ()-> @get('miterLimit')
173 | set: (miterLimit)-> @set('miterLimit', miterLimit)
174 |
175 | @property 'shadowBlur',
176 | get: ()-> @get('shadowBlur')
177 | set: (shadowBlur)-> @set('shadowBlur', shadowBlur)
178 |
179 | @property 'shadowColor',
180 | get: ()-> @get('shadowColor')
181 | set: (shadowColor)-> @set('shadowColor', shadowColor)
182 |
183 | @property 'shadowOffsetX',
184 | get: ()-> @get('shadowOffsetX')
185 | set: (shadowOffsetX)-> @set('shadowOffsetX', shadowOffsetX)
186 |
187 | @property 'shadowOffsetY',
188 | get: ()-> @get('shadowOffsetY')
189 | set: (shadowOffsetY)-> @set('shadowOffsetY', shadowOffsetY)
190 |
191 | @property 'strokeStyle',
192 | get: ()-> @get('strokeStyle')
193 | set: (strokeStyle)-> @set('strokeStyle', strokeStyle)
194 |
195 | @property 'textAlign',
196 | get: ()-> @get('textAlign')
197 | set: (textAlign)-> @set('textAlign', textAlign)
198 |
199 | @property 'textBaseline',
200 | get: ()-> @get('textBaseline')
201 | set: (textBaseline)-> @set('textBaseline', textBaseline)
202 |
203 |
204 | module.exports = CanvasRenderingContext2DShim
205 |
206 |
--------------------------------------------------------------------------------
/test/testStenographer.coffee:
--------------------------------------------------------------------------------
1 | chai = require('chai')
2 | assert = require('assert')
3 |
4 |
5 | Stenographer = require('../lib/canvgc/stenographer')
6 |
7 | chai.should()
8 |
9 | describe 'Stenographer', ()->
10 |
11 | stenographer = new Stenographer()
12 |
13 | describe '#serialize()', ()->
14 | it 'should serialize boolean, strings and numbers', ()->
15 | stenographer.serialize(true).should.equal("true")
16 | stenographer.serialize(false).should.equal("false")
17 | stenographer.serialize(null).should.equal("null")
18 | stenographer.serialize("testString").should.equal('"testString"')
19 | stenographer.serialize("55.00").should.equal('"55.00"')
20 | stenographer.serialize(55.00).should.equal("55")
21 | return
22 |
23 | it 'should respond to objects with a "targetId"', ()->
24 | stenographer.serialize({"targetId":"test"}).should.equal("p.test")
25 | return
26 | ###
27 | it 'should serialize IMG tags', ()->
28 | # TODO - test for serialzing IMG tags
29 | true.should.be.false
30 | return
31 |
32 | it 'should serialize CANVAS tags', ()->
33 | # TODO - test for serializing CANVAS tags
34 | true.should.be.false
35 | return
36 | ###
37 | it 'should throw an error for unsupported types', ()->
38 | fn = ()->
39 | stenographer.serialize(["potato"])
40 | fn.should.throw(Error)
41 | return
42 |
43 | return
44 |
45 | describe '#serializeArgs()', ()->
46 | it 'should return "" when array is empty', ()->
47 | stenographer.serializeArgs([]).should.equal("")
48 | return
49 |
50 | it 'should serialize an array of values', ()->
51 | stenographer.serializeArgs([true]).should.equal("true")
52 | stenographer.serializeArgs([false]).should.equal("false")
53 | stenographer.serializeArgs([true, false]).should.equal("true,false")
54 | return
55 |
56 | return
57 |
58 |
59 | describe '#setContextProperty()', ()->
60 | it 'should append property to @output', ()->
61 | tmpOutput = stenographer.output
62 | stenographer.output = ''
63 | stenographer.setContextProperty('test',true)
64 | stenographer.output.should.equal("$.test = true;\n")
65 | stenographer.output = ''
66 | stenographer.setContextProperty('abcDef',55)
67 | stenographer.output.should.equal("$.abcDef = 55;\n")
68 | stenographer.output = tmpOutput
69 | return
70 |
71 | return
72 |
73 | describe '#invokeContext()', ()->
74 | it 'should append invocation to @output', ()->
75 | tmpOutput = stenographer.output
76 | stenographer.output = ''
77 | stenographer.invokeContext('someFunc',[true, false, 55, "abc"])
78 | stenographer.output.should.equal('$.someFunc(true,false,55,"abc");\n')
79 | stenographer.output = tmpOutput
80 | return
81 | it 'invoking "save" should increment the stack', ()->
82 | tmpOutput = stenographer.output
83 | stenographer.output = ''
84 | stenographer.invokeContext('save',[])
85 | stenographer.output.should.equal('$.save();\np.stack++;\n')
86 | stenographer.output = tmpOutput
87 | return
88 | it 'invoking "restore" should decrement the stack', ()->
89 | tmpOutput = stenographer.output
90 | stenographer.output = ''
91 | stenographer.invokeContext('restore',[])
92 | stenographer.output.should.equal('$.restore();\np.stack--;\n')
93 | stenographer.output = tmpOutput
94 | return
95 | it 'should round numbers for moveTo, lineTo, bezierCurveTo, quadraticCurveTo', ()->
96 | tmpOutput = stenographer.output
97 | stenographer.output = ''
98 | stenographer.invokeContext('moveTo',[55.123456789, 55.987654321])
99 | stenographer.output.should.equal('$.moveTo(55.123457,55.987654);\n')
100 | stenographer.output = tmpOutput
101 |
102 | tmpOutput = stenographer.output
103 | stenographer.output = ''
104 | stenographer.invokeContext('lineTo',[55.123456789, 55.987654321])
105 | stenographer.output.should.equal('$.lineTo(55.123457,55.987654);\n')
106 | stenographer.output = tmpOutput
107 |
108 | tmpOutput = stenographer.output
109 | stenographer.output = ''
110 | stenographer.invokeContext('bezierCurveTo',[55.123456789, 55.987654321])
111 | stenographer.output.should.equal('$.bezierCurveTo(55.123457,55.987654);\n')
112 | stenographer.output = tmpOutput
113 |
114 |
115 | tmpOutput = stenographer.output
116 | stenographer.output = ''
117 | stenographer.invokeContext('quadraticCurveTo',[55.123456789, 55.987654321])
118 | stenographer.output.should.equal('$.quadraticCurveTo(55.123457,55.987654);\n')
119 | stenographer.output = tmpOutput
120 |
121 | return
122 |
123 | it 'should not round numbers for transform, rotate, scale, skew', ()->
124 | tmpOutput = stenographer.output
125 | stenographer.output = ''
126 | stenographer.invokeContext('transform',[55.123456789, 55.987654321])
127 | stenographer.output.should.equal('$.transform(55.123456789,55.987654321);\n')
128 | stenographer.output = tmpOutput
129 |
130 | tmpOutput = stenographer.output
131 | stenographer.output = ''
132 | stenographer.invokeContext('rotate',[55.123456789, 55.987654321])
133 | stenographer.output.should.equal('$.rotate(55.123456789,55.987654321);\n')
134 | stenographer.output = tmpOutput
135 |
136 | tmpOutput = stenographer.output
137 | stenographer.output = ''
138 | stenographer.invokeContext('scale',[55.123456789, 55.987654321])
139 | stenographer.output.should.equal('$.scale(55.123456789,55.987654321);\n')
140 | stenographer.output = tmpOutput
141 |
142 | tmpOutput = stenographer.output
143 | stenographer.output = ''
144 | stenographer.invokeContext('skew',[55.123456789, 55.987654321])
145 | stenographer.output.should.equal('$.skew(55.123456789,55.987654321);\n')
146 | stenographer.output = tmpOutput
147 | return
148 |
149 | describe '#invokeChildObject()', ()->
150 | it 'should append invocation to @output', ()->
151 | tmpOutput = stenographer.output
152 | stenographer.output = ''
153 | stenographer.invokeChildObject('obj','someFunc',[true, false, 55, "abc"])
154 | stenographer.output.should.equal('p.obj.someFunc(true,false,55,"abc");\n')
155 | stenographer.output = tmpOutput
156 | return
157 | return
--------------------------------------------------------------------------------
/test/testShim.coffee:
--------------------------------------------------------------------------------
1 | chai = require('chai')
2 |
3 | CanvasRenderingContext2DShim = require('../lib/canvgc/CanvasRenderingContext2DShim')
4 |
5 | chai.should()
6 |
7 | describe 'CanvasRenderingContext2DShim', ()->
8 | mockStenographer =
9 | 'invokeContext': (fn, args, assignTarget)-> return
10 | 'setContextProperty': (propertyName, value)-> return
11 | 'invokeChildObject': (name, fn, args)-> return
12 |
13 | mockContext =
14 | createPattern: ()->
15 | return {}
16 | createLinearGradient: ()->
17 | return {
18 | addColorStop: ()-> return
19 | }
20 | createRadialGradient: ()->
21 | return {
22 | addColorStop: ()-> return
23 | }
24 |
25 | shim = new CanvasRenderingContext2DShim(mockStenographer, mockContext)
26 |
27 | describe '#set()', ()->
28 | it 'setting a property should call stenographer.setContextProperty', ()->
29 | mock = mockStenographer.setContextProperty
30 |
31 | properties = [
32 | 'canvas',
33 | 'fillStyle',
34 | 'font',
35 | 'globalAlpha',
36 | 'globalCompositeOperation',
37 | 'lineCap',
38 | 'lineDashOffset',
39 | 'lineJoin',
40 | 'lineWidth',
41 | 'miterLimit',
42 | 'shadowBlur',
43 | 'shadowColor',
44 | 'shadowOffsetX',
45 | 'shadowOffsetY',
46 | 'strokeStyle',
47 | 'textAlign',
48 | 'textBaseline'
49 | ]
50 | for property in properties
51 | called = false
52 | mockStenographer.setContextProperty = (propertyName, value)->
53 | called = true
54 | propertyName.should.be.a('string')
55 | propertyName.should.equal(property)
56 | value.should.equal("")
57 | return
58 | shim[property] = ""
59 | called.should.be.true
60 | mockStenographer.setContextProperty = mock
61 | return
62 | describe '#call()', ()->
63 | it 'invoking a valid CanvasRenderingContext2D function, should call stenographer.invokeContext', ()->
64 | mock = mockStenographer.invokeContext
65 |
66 | context2dFunctionNames = [
67 | 'fill',
68 | 'stroke',
69 | 'translate',
70 | 'transform',
71 | 'rotate',
72 | 'scale',
73 | 'save',
74 | 'restore',
75 | 'beginPath',
76 | 'closePath',
77 | 'moveTo',
78 | 'lineTo',
79 | 'clip',
80 | 'quadraticCurveTo',
81 | 'bezierCurveTo',
82 | 'arc',
83 | 'createPattern',
84 | 'createLinearGradient',
85 | 'createRadialGradient',
86 | 'fillText',
87 | 'strokeText',
88 | 'measureText',
89 | 'drawImage',
90 | 'fillRect',
91 | 'clearRect',
92 | 'getImageData',
93 | 'putImageData',
94 | 'isPointPath'
95 | ]
96 | for context2dFunctionName in context2dFunctionNames
97 | called = false
98 | mockStenographer.invokeContext = (invokedFn, invokedArgs, invokedAssignTarget)->
99 | called = true
100 | invokedFn.should.be.a('string')
101 | invokedFn.should.equal(context2dFunctionName)
102 | invokedArgs.length.should.equal(2)
103 | return
104 | shim[context2dFunctionName].call(shim, 'testing', 'abc')
105 | called.should.be.true
106 | mockStenographer.invokeContext = mock
107 | return
108 |
109 | it '#createPattern() should return an object with targetId', ()->
110 | mock = mockStenographer.invokeContext
111 | called = false
112 | valueOfInvokedAssignTarget = null
113 | mockStenographer.invokeContext = (invokedFn, invokedArgs, invokedAssignTarget)->
114 | called = true
115 | invokedFn.should.be.a('string')
116 | invokedFn.should.equal('createPattern')
117 | invokedArgs.length.should.equal(0)
118 | invokedAssignTarget.should.be.a('string')
119 | valueOfInvokedAssignTarget = invokedAssignTarget;
120 | return
121 | pattern = shim.createPattern()
122 | called.should.be.true
123 | pattern.targetId.should.equal(valueOfInvokedAssignTarget)
124 | mockStenographer.invokeContext = mock
125 | return
126 |
127 | it '#createLinearGradient() should return an object with targetId, and mocked addColorStop', ()->
128 | mockInvokeContext = mockStenographer.invokeContext
129 | mockInvokeChildObject = mockStenographer.invokeChildObject
130 | called = false
131 | valueOfInvokedAssignTarget = null
132 | mockStenographer.invokeContext = (invokedFn, invokedArgs, invokedAssignTarget)->
133 | called = true
134 | invokedFn.should.be.a('string')
135 | invokedFn.should.equal('createLinearGradient')
136 | invokedArgs.length.should.equal(0)
137 | invokedAssignTarget.should.be.a('string')
138 | valueOfInvokedAssignTarget = invokedAssignTarget;
139 | return
140 | linearGradient = shim.createLinearGradient()
141 | called.should.be.true
142 | linearGradient.targetId.should.equal(valueOfInvokedAssignTarget)
143 |
144 | called = false
145 | mockStenographer.invokeChildObject = (invokedTargetName, invokedFn, invokedArgs)->
146 | called = true
147 | invokedTargetName.should.equal(linearGradient.targetId)
148 | invokedFn.should.equal('addColorStop')
149 | invokedArgs.length.should.equal(0)
150 | return
151 | linearGradient.addColorStop()
152 | called.should.be.true
153 |
154 | mockStenographer.invokeContext = mockInvokeContext
155 | mockStenographer.invokeChildObject = mockInvokeChildObject
156 | return
157 |
158 | it '#createRadialGradient() should return an object with targetId, and mocked addColorStop', ()->
159 | mockInvokeContext = mockStenographer.invokeContext
160 | mockInvokeChildObject = mockStenographer.invokeChildObject
161 | called = false
162 | valueOfInvokedAssignTarget = null
163 | mockStenographer.invokeContext = (invokedFn, invokedArgs, invokedAssignTarget)->
164 | called = true
165 | invokedFn.should.be.a('string')
166 | invokedFn.should.equal('createRadialGradient')
167 | invokedArgs.length.should.equal(0)
168 | invokedAssignTarget.should.be.a('string')
169 | valueOfInvokedAssignTarget = invokedAssignTarget;
170 | return
171 | linearGradient = shim.createRadialGradient()
172 | called.should.be.true
173 | linearGradient.targetId.should.equal(valueOfInvokedAssignTarget)
174 |
175 | called = false
176 | mockStenographer.invokeChildObject = (invokedTargetName, invokedFn, invokedArgs)->
177 | called = true
178 | invokedTargetName.should.equal(linearGradient.targetId)
179 | invokedFn.should.equal('addColorStop')
180 | invokedArgs.length.should.equal(0)
181 | return
182 | linearGradient.addColorStop()
183 | called.should.be.true
184 |
185 | mockStenographer.invokeContext = mockInvokeContext
186 | mockStenographer.invokeChildObject = mockInvokeChildObject
187 | return
188 | return
189 | return
--------------------------------------------------------------------------------
/example/complex.js:
--------------------------------------------------------------------------------
1 | if(typeof window.canvgc=="undefined")window.canvgc={};window.canvgc.complex ={"w":480,"h":360,"d":[function($,p){
2 | $.save();
3 | p.stack++;
4 | $.strokeStyle = "rgba(0,0,0,0)";
5 | $.lineCap = "butt";
6 | $.lineJoin = "miter";
7 | $.miterLimit = 4;
8 | $.font = " 10px sans-serif";
9 | $.translate(0,0);
10 | $.translate(0,0);
11 | $.translate(0,0);
12 | $.scale(1,1);
13 | $.translate(0,0);
14 | $.save();
15 | p.stack++;
16 | $.restore();
17 | p.stack--;
18 | $.save();
19 | p.stack++;
20 | $.font = " 10px sans-serif";
21 | $.save();
22 | p.stack++;
23 | $.fillStyle = "#0000FF";
24 | $.fillStyle = "rgba(0, 0, 255, 0.0)";
25 | $.strokeStyle = "black";
26 | $.font = " 10px sans-serif";
27 | $.beginPath();
28 | $.moveTo(20,20);
29 | $.lineTo(80,20);
30 | $.quadraticCurveTo(80,20,80,20);
31 | $.lineTo(80,80);
32 | $.quadraticCurveTo(80,80,80,80);
33 | $.lineTo(20,80);
34 | $.quadraticCurveTo(20,80,20,80);
35 | $.lineTo(20,20);
36 | $.quadraticCurveTo(20,20,20,20);
37 | $.closePath();
38 | $.fill();
39 | $.stroke();
40 | $.restore();
41 | p.stack--;
42 | $.save();
43 | p.stack++;
44 | $.fillStyle = "#0000FF";
45 | $.fillStyle = "rgba(0, 0, 255, 0.2)";
46 | $.strokeStyle = "black";
47 | $.font = " 10px sans-serif";
48 | $.beginPath();
49 | $.moveTo(50,50);
50 | $.lineTo(110,50);
51 | $.quadraticCurveTo(110,50,110,50);
52 | $.lineTo(110,110);
53 | $.quadraticCurveTo(110,110,110,110);
54 | $.lineTo(50,110);
55 | $.quadraticCurveTo(50,110,50,110);
56 | $.lineTo(50,50);
57 | $.quadraticCurveTo(50,50,50,50);
58 | $.closePath();
59 | $.fill();
60 | $.stroke();
61 | $.restore();
62 | p.stack--;
63 | $.save();
64 | p.stack++;
65 | $.fillStyle = "#0000FF";
66 | $.fillStyle = "rgba(0, 0, 255, 0.4)";
67 | $.strokeStyle = "black";
68 | $.font = " 10px sans-serif";
69 | $.beginPath();
70 | $.moveTo(80,80);
71 | $.lineTo(140,80);
72 | $.quadraticCurveTo(140,80,140,80);
73 | $.lineTo(140,140);
74 | $.quadraticCurveTo(140,140,140,140);
75 | $.lineTo(80,140);
76 | $.quadraticCurveTo(80,140,80,140);
77 | $.lineTo(80,80);
78 | $.quadraticCurveTo(80,80,80,80);
79 | $.closePath();
80 | $.fill();
81 | $.stroke();
82 | $.restore();
83 | p.stack--;
84 | $.save();
85 | p.stack++;
86 | $.fillStyle = "#0000FF";
87 | $.fillStyle = "rgba(0, 0, 255, 0.6)";
88 | $.strokeStyle = "black";
89 | $.font = " 10px sans-serif";
90 | $.beginPath();
91 | $.moveTo(110,110);
92 | $.lineTo(170,110);
93 | $.quadraticCurveTo(170,110,170,110);
94 | $.lineTo(170,170);
95 | $.quadraticCurveTo(170,170,170,170);
96 | $.lineTo(110,170);
97 | $.quadraticCurveTo(110,170,110,170);
98 | $.lineTo(110,110);
99 | $.quadraticCurveTo(110,110,110,110);
100 | $.closePath();
101 | $.fill();
102 | $.stroke();
103 | $.restore();
104 | p.stack--;
105 | $.save();
106 | p.stack++;
107 | $.fillStyle = "#0000FF";
108 | $.fillStyle = "rgba(0, 0, 255, 0.8)";
109 | $.strokeStyle = "black";
110 | $.font = " 10px sans-serif";
111 | $.beginPath();
112 | $.moveTo(140,140);
113 | $.lineTo(200,140);
114 | $.quadraticCurveTo(200,140,200,140);
115 | $.lineTo(200,200);
116 | $.quadraticCurveTo(200,200,200,200);
117 | $.lineTo(140,200);
118 | $.quadraticCurveTo(140,200,140,200);
119 | $.lineTo(140,140);
120 | $.quadraticCurveTo(140,140,140,140);
121 | $.closePath();
122 | $.fill();
123 | $.stroke();
124 | $.restore();
125 | p.stack--;
126 | $.save();
127 | p.stack++;
128 | $.fillStyle = "#0000FF";
129 | $.fillStyle = "rgba(0, 0, 255, 1.0)";
130 | $.strokeStyle = "black";
131 | $.font = " 10px sans-serif";
132 | $.beginPath();
133 | $.moveTo(170,170);
134 | $.lineTo(230,170);
135 | $.quadraticCurveTo(230,170,230,170);
136 | $.lineTo(230,230);
137 | $.quadraticCurveTo(230,230,230,230);
138 | $.lineTo(170,230);
139 | $.quadraticCurveTo(170,230,170,230);
140 | $.lineTo(170,170);
141 | $.quadraticCurveTo(170,170,170,170);
142 | $.closePath();
143 | $.fill();
144 | $.stroke();
145 | $.restore();
146 | p.stack--;
147 | $.save();
148 | p.stack++;
149 | $.fillStyle = "#0000FF";
150 | $.fillStyle = "rgba(0, 0, 255, -100.0)";
151 | $.strokeStyle = "black";
152 | $.font = " 10px sans-serif";
153 | $.beginPath();
154 | $.moveTo(200,20);
155 | $.lineTo(260,20);
156 | $.quadraticCurveTo(260,20,260,20);
157 | $.lineTo(260,80);
158 | $.quadraticCurveTo(260,80,260,80);
159 | $.lineTo(200,80);
160 | $.quadraticCurveTo(200,80,200,80);
161 | $.lineTo(200,20);
162 | $.quadraticCurveTo(200,20,200,20);
163 | $.closePath();
164 | $.fill();
165 | $.stroke();
166 | $.restore();
167 | p.stack--;
168 | $.save();
169 | p.stack++;
170 | $.fillStyle = "#0000FF";
171 | $.fillStyle = "rgba(0, 0, 255, -10.0)";
172 | $.strokeStyle = "black";
173 | $.font = " 10px sans-serif";
174 | $.beginPath();
175 | $.moveTo(230,50);
176 | $.lineTo(290,50);
177 | $.quadraticCurveTo(290,50,290,50);
178 | $.lineTo(290,110);
179 | $.quadraticCurveTo(290,110,290,110);
180 | $.lineTo(230,110);
181 | $.quadraticCurveTo(230,110,230,110);
182 | $.lineTo(230,50);
183 | $.quadraticCurveTo(230,50,230,50);
184 | $.closePath();
185 | $.fill();
186 | $.stroke();
187 | $.restore();
188 | p.stack--;
189 | $.save();
190 | p.stack++;
191 | $.fillStyle = "#0000FF";
192 | $.fillStyle = "rgba(0, 0, 255, -0.1)";
193 | $.strokeStyle = "black";
194 | $.font = " 10px sans-serif";
195 | $.beginPath();
196 | $.moveTo(260,80);
197 | $.lineTo(320,80);
198 | $.quadraticCurveTo(320,80,320,80);
199 | $.lineTo(320,140);
200 | $.quadraticCurveTo(320,140,320,140);
201 | $.lineTo(260,140);
202 | $.quadraticCurveTo(260,140,260,140);
203 | $.lineTo(260,80);
204 | $.quadraticCurveTo(260,80,260,80);
205 | $.closePath();
206 | $.fill();
207 | $.stroke();
208 | $.restore();
209 | p.stack--;
210 | $.save();
211 | p.stack++;
212 | $.fillStyle = "#0000FF";
213 | $.fillStyle = "rgba(0, 0, 255, 1.1)";
214 | $.strokeStyle = "black";
215 | $.font = " 10px sans-serif";
216 | $.beginPath();
217 | $.moveTo(290,110);
218 | $.lineTo(350,110);
219 | $.quadraticCurveTo(350,110,350,110);
220 | $.lineTo(350,170);
221 | $.quadraticCurveTo(350,170,350,170);
222 | $.lineTo(290,170);
223 | $.quadraticCurveTo(290,170,290,170);
224 | $.lineTo(290,110);
225 | $.quadraticCurveTo(290,110,290,110);
226 | $.closePath();
227 | $.fill();
228 | $.stroke();
229 | $.restore();
230 | p.stack--;
231 | $.save();
232 | p.stack++;
233 | $.fillStyle = "#0000FF";
234 | $.fillStyle = "rgba(0, 0, 255, 10.0)";
235 | $.strokeStyle = "black";
236 | $.font = " 10px sans-serif";
237 | $.beginPath();
238 | $.moveTo(320,140);
239 | $.lineTo(380,140);
240 | $.quadraticCurveTo(380,140,380,140);
241 | $.lineTo(380,200);
242 | $.quadraticCurveTo(380,200,380,200);
243 | $.lineTo(320,200);
244 | $.quadraticCurveTo(320,200,320,200);
245 | $.lineTo(320,140);
246 | $.quadraticCurveTo(320,140,320,140);
247 | $.closePath();
248 | $.fill();
249 | $.stroke();
250 | $.restore();
251 | p.stack--;
252 | $.save();
253 | p.stack++;
254 | $.fillStyle = "#0000FF";
255 | $.fillStyle = "rgba(0, 0, 255, 100.0)";
256 | $.strokeStyle = "black";
257 | $.font = " 10px sans-serif";
258 | $.beginPath();
259 | $.moveTo(350,170);
260 | $.lineTo(410,170);
261 | $.quadraticCurveTo(410,170,410,170);
262 | $.lineTo(410,230);
263 | $.quadraticCurveTo(410,230,410,230);
264 | $.lineTo(350,230);
265 | $.quadraticCurveTo(350,230,350,230);
266 | $.lineTo(350,170);
267 | $.quadraticCurveTo(350,170,350,170);
268 | $.closePath();
269 | $.fill();
270 | $.stroke();
271 | $.restore();
272 | p.stack--;
273 | $.restore();
274 | p.stack--;
275 | $.save();
276 | p.stack++;
277 | $.fillStyle = "black";
278 | $.strokeStyle = "rgba(0,0,0,0)";
279 | $.font = " 40px sans-serif";
280 | $.save();
281 | p.stack++;
282 | $.fillStyle = "black";
283 | $.strokeStyle = "rgba(0,0,0,0)";
284 | $.font = " 40px sans-serif";
285 | $.measureText("$Revision: 1.4 $");
286 | $.restore();
287 | p.stack--;
288 | $.save();
289 | p.stack++;
290 | $.fillStyle = "black";
291 | $.strokeStyle = "rgba(0,0,0,0)";
292 | $.font = " 40px sans-serif";
293 | $.fillText("$Revision: 1.4 $",10,340);
294 | $.strokeText("$Revision: 1.4 $",10,340);
295 | $.restore();
296 | p.stack--;
297 | $.restore();
298 | p.stack--;
299 | $.save();
300 | p.stack++;
301 | $.fillStyle = "rgba(0,0,0,0)";
302 | $.strokeStyle = "#000000";
303 | $.font = " 40px sans-serif";
304 | $.beginPath();
305 | $.moveTo(1,1);
306 | $.lineTo(479,1);
307 | $.quadraticCurveTo(479,1,479,1);
308 | $.lineTo(479,359);
309 | $.quadraticCurveTo(479,359,479,359);
310 | $.lineTo(1,359);
311 | $.quadraticCurveTo(1,359,1,359);
312 | $.lineTo(1,1);
313 | $.quadraticCurveTo(1,1,1,1);
314 | $.closePath();
315 | $.fill();
316 | $.stroke();
317 | $.restore();
318 | p.stack--;
319 | $.restore();
320 | p.stack--;
321 | }],"i":{}};
--------------------------------------------------------------------------------
/src/canvg/StackBlur.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | StackBlur - a fast almost Gaussian Blur For Canvas
4 |
5 | Version: 0.5
6 | Author: Mario Klingemann
7 | Contact: mario@quasimondo.com
8 | Website: http://www.quasimondo.com/StackBlurForCanvas
9 | Twitter: @quasimondo
10 |
11 | In case you find this class useful - especially in commercial projects -
12 | I am not totally unhappy for a small donation to my PayPal account
13 | mario@quasimondo.de
14 |
15 | Or support me on flattr:
16 | https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript
17 |
18 | Copyright (c) 2010 Mario Klingemann
19 |
20 | Permission is hereby granted, free of charge, to any person
21 | obtaining a copy of this software and associated documentation
22 | files (the "Software"), to deal in the Software without
23 | restriction, including without limitation the rights to use,
24 | copy, modify, merge, publish, distribute, sublicense, and/or sell
25 | copies of the Software, and to permit persons to whom the
26 | Software is furnished to do so, subject to the following
27 | conditions:
28 |
29 | The above copyright notice and this permission notice shall be
30 | included in all copies or substantial portions of the Software.
31 |
32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
34 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
36 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
37 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
38 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
39 | OTHER DEALINGS IN THE SOFTWARE.
40 | */
41 |
42 | var mul_table = [
43 | 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512,
44 | 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512,
45 | 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456,
46 | 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512,
47 | 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328,
48 | 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456,
49 | 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335,
50 | 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512,
51 | 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405,
52 | 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328,
53 | 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271,
54 | 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456,
55 | 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,
56 | 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335,
57 | 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292,
58 | 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259];
59 |
60 |
61 | var shg_table = [
62 | 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
63 | 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
64 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
65 | 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
66 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
67 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
68 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
69 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23,
70 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
71 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
72 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
73 | 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
74 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
75 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
76 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
77 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ];
78 |
79 | function BlurStack() {
80 | this.r = 0;
81 | this.g = 0;
82 | this.b = 0;
83 | this.a = 0;
84 | this.next = null;
85 | }
86 |
87 | function stackBlur(context, top_x, top_y, width, height, radius) {
88 | var imageData;
89 | if (isNaN(radius) || radius < 1) return;
90 | radius |= 0;
91 |
92 | try {
93 | imageData = context.getImageData(top_x, top_y, width, height);
94 | } catch (e) {
95 | throw new Error("unable to access image data: " + e);
96 | }
97 |
98 | var pixels = imageData.data;
99 |
100 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum,
101 | r_out_sum, g_out_sum, b_out_sum, a_out_sum,
102 | r_in_sum, g_in_sum, b_in_sum, a_in_sum,
103 | pr, pg, pb, pa, rbs;
104 |
105 | var div = radius + radius + 1;
106 | var w4 = width << 2;
107 | var widthMinus1 = width - 1;
108 | var heightMinus1 = height - 1;
109 | var radiusPlus1 = radius + 1;
110 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2;
111 |
112 | var stackStart = new BlurStack();
113 | var stack = stackStart;
114 | for (i = 1; i < div; i++) {
115 | stack = stack.next = new BlurStack();
116 | if (i == radiusPlus1) var stackEnd = stack;
117 | }
118 | stack.next = stackStart;
119 | var stackIn = null;
120 | var stackOut = null;
121 |
122 | yw = yi = 0;
123 |
124 | var mul_sum = mul_table[radius];
125 | var shg_sum = shg_table[radius];
126 |
127 | for (y = 0; y < height; y++) {
128 | r_in_sum = g_in_sum = b_in_sum = a_in_sum = r_sum = g_sum = b_sum = a_sum = 0;
129 |
130 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] );
131 | g_out_sum = radiusPlus1 * ( pg = pixels[yi + 1] );
132 | b_out_sum = radiusPlus1 * ( pb = pixels[yi + 2] );
133 | a_out_sum = radiusPlus1 * ( pa = pixels[yi + 3] );
134 |
135 | r_sum += sumFactor * pr;
136 | g_sum += sumFactor * pg;
137 | b_sum += sumFactor * pb;
138 | a_sum += sumFactor * pa;
139 |
140 | stack = stackStart;
141 |
142 | for (i = 0; i < radiusPlus1; i++) {
143 | stack.r = pr;
144 | stack.g = pg;
145 | stack.b = pb;
146 | stack.a = pa;
147 | stack = stack.next;
148 | }
149 |
150 | for (i = 1; i < radiusPlus1; i++) {
151 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 );
152 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i );
153 | g_sum += ( stack.g = ( pg = pixels[p + 1])) * rbs;
154 | b_sum += ( stack.b = ( pb = pixels[p + 2])) * rbs;
155 | a_sum += ( stack.a = ( pa = pixels[p + 3])) * rbs;
156 |
157 | r_in_sum += pr;
158 | g_in_sum += pg;
159 | b_in_sum += pb;
160 | a_in_sum += pa;
161 |
162 | stack = stack.next;
163 | }
164 |
165 |
166 | stackIn = stackStart;
167 | stackOut = stackEnd;
168 | for (x = 0; x < width; x++) {
169 | pixels[yi + 3] = pa = (a_sum * mul_sum) >> shg_sum;
170 | if (pa != 0) {
171 | pa = 255 / pa;
172 | pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa;
173 | pixels[yi + 1] = ((g_sum * mul_sum) >> shg_sum) * pa;
174 | pixels[yi + 2] = ((b_sum * mul_sum) >> shg_sum) * pa;
175 | } else {
176 | pixels[yi] = pixels[yi + 1] = pixels[yi + 2] = 0;
177 | }
178 |
179 | r_sum -= r_out_sum;
180 | g_sum -= g_out_sum;
181 | b_sum -= b_out_sum;
182 | a_sum -= a_out_sum;
183 |
184 | r_out_sum -= stackIn.r;
185 | g_out_sum -= stackIn.g;
186 | b_out_sum -= stackIn.b;
187 | a_out_sum -= stackIn.a;
188 |
189 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2;
190 |
191 | r_in_sum += ( stackIn.r = pixels[p]);
192 | g_in_sum += ( stackIn.g = pixels[p + 1]);
193 | b_in_sum += ( stackIn.b = pixels[p + 2]);
194 | a_in_sum += ( stackIn.a = pixels[p + 3]);
195 |
196 | r_sum += r_in_sum;
197 | g_sum += g_in_sum;
198 | b_sum += b_in_sum;
199 | a_sum += a_in_sum;
200 |
201 | stackIn = stackIn.next;
202 |
203 | r_out_sum += ( pr = stackOut.r );
204 | g_out_sum += ( pg = stackOut.g );
205 | b_out_sum += ( pb = stackOut.b );
206 | a_out_sum += ( pa = stackOut.a );
207 |
208 | r_in_sum -= pr;
209 | g_in_sum -= pg;
210 | b_in_sum -= pb;
211 | a_in_sum -= pa;
212 |
213 | stackOut = stackOut.next;
214 |
215 | yi += 4;
216 | }
217 | yw += width;
218 | }
219 |
220 |
221 | for (x = 0; x < width; x++) {
222 | g_in_sum = b_in_sum = a_in_sum = r_in_sum = g_sum = b_sum = a_sum = r_sum = 0;
223 |
224 | yi = x << 2;
225 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]);
226 | g_out_sum = radiusPlus1 * ( pg = pixels[yi + 1]);
227 | b_out_sum = radiusPlus1 * ( pb = pixels[yi + 2]);
228 | a_out_sum = radiusPlus1 * ( pa = pixels[yi + 3]);
229 |
230 | r_sum += sumFactor * pr;
231 | g_sum += sumFactor * pg;
232 | b_sum += sumFactor * pb;
233 | a_sum += sumFactor * pa;
234 |
235 | stack = stackStart;
236 |
237 | for (i = 0; i < radiusPlus1; i++) {
238 | stack.r = pr;
239 | stack.g = pg;
240 | stack.b = pb;
241 | stack.a = pa;
242 | stack = stack.next;
243 | }
244 |
245 | yp = width;
246 |
247 | for (i = 1; i <= radius; i++) {
248 | yi = ( yp + x ) << 2;
249 |
250 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i );
251 | g_sum += ( stack.g = ( pg = pixels[yi + 1])) * rbs;
252 | b_sum += ( stack.b = ( pb = pixels[yi + 2])) * rbs;
253 | a_sum += ( stack.a = ( pa = pixels[yi + 3])) * rbs;
254 |
255 | r_in_sum += pr;
256 | g_in_sum += pg;
257 | b_in_sum += pb;
258 | a_in_sum += pa;
259 |
260 | stack = stack.next;
261 |
262 | if (i < heightMinus1) {
263 | yp += width;
264 | }
265 | }
266 |
267 | yi = x;
268 | stackIn = stackStart;
269 | stackOut = stackEnd;
270 | for (y = 0; y < height; y++) {
271 | p = yi << 2;
272 | pixels[p + 3] = pa = (a_sum * mul_sum) >> shg_sum;
273 | if (pa > 0) {
274 | pa = 255 / pa;
275 | pixels[p] = ((r_sum * mul_sum) >> shg_sum ) * pa;
276 | pixels[p + 1] = ((g_sum * mul_sum) >> shg_sum ) * pa;
277 | pixels[p + 2] = ((b_sum * mul_sum) >> shg_sum ) * pa;
278 | } else {
279 | pixels[p] = pixels[p + 1] = pixels[p + 2] = 0;
280 | }
281 |
282 | r_sum -= r_out_sum;
283 | g_sum -= g_out_sum;
284 | b_sum -= b_out_sum;
285 | a_sum -= a_out_sum;
286 |
287 | r_out_sum -= stackIn.r;
288 | g_out_sum -= stackIn.g;
289 | b_out_sum -= stackIn.b;
290 | a_out_sum -= stackIn.a;
291 |
292 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2;
293 |
294 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p]));
295 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p + 1]));
296 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p + 2]));
297 | a_sum += ( a_in_sum += ( stackIn.a = pixels[p + 3]));
298 |
299 | stackIn = stackIn.next;
300 |
301 | r_out_sum += ( pr = stackOut.r );
302 | g_out_sum += ( pg = stackOut.g );
303 | b_out_sum += ( pb = stackOut.b );
304 | a_out_sum += ( pa = stackOut.a );
305 |
306 | r_in_sum -= pr;
307 | g_in_sum -= pg;
308 | b_in_sum -= pb;
309 | a_in_sum -= pa;
310 |
311 | stackOut = stackOut.next;
312 |
313 | yi += width;
314 | }
315 | }
316 |
317 | context.putImageData(imageData, top_x, top_y);
318 |
319 | }
320 |
321 | module.exports = stackBlur;
--------------------------------------------------------------------------------
/src/canvg/canvg.js:
--------------------------------------------------------------------------------
1 | var Canvas = require("canvas");
2 | var xmldom = require('xmldom');
3 | var RGBColor = require("rgbcolor");
4 | var http = require("http");
5 | var stackBlur = require('./StackBlur');
6 |
7 | /*
8 | * canvg.js - Javascript SVG parser and renderer on Canvas
9 | * MIT Licensed
10 | * Gabe Lerner (gabelerner@gmail.com)
11 | * http://code.google.com/p/canvg/
12 | *
13 | * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/
14 | */
15 |
16 | function canvg(target, s, opts) {
17 | // no parameters
18 | if (target == null || s == null) {
19 | return;
20 | }
21 | opts = opts || {};
22 | // store class on canvas
23 | target.svg = svg = build();
24 | svg.opts = opts;
25 | var ctx = target.getContext('2d');
26 | svg.loadXmlDoc(ctx, svg.parseXml(s));
27 |
28 | }
29 |
30 | function build() {
31 | var svg = { };
32 |
33 | svg.FRAMERATE = 30;
34 | svg.MAX_VIRTUAL_PIXELS = 30000;
35 |
36 | // globals
37 | svg.init = function (ctx) {
38 | svg.Definitions = {};
39 | svg.Styles = {};
40 | svg.Animations = [];
41 | svg.Images = [];
42 | svg.ctx = ctx;
43 | svg.ViewPort = new (function () {
44 | this.viewPorts = [];
45 | this.Clear = function () {
46 | this.viewPorts = [];
47 | };
48 | this.SetCurrent = function (width, height) {
49 | this.viewPorts.push({ width: width, height: height });
50 | };
51 | this.RemoveCurrent = function () {
52 | this.viewPorts.pop();
53 | };
54 | this.Current = function () {
55 | return this.viewPorts[this.viewPorts.length - 1];
56 | };
57 | this.width = function () {
58 | return this.Current().width;
59 | };
60 | this.height = function () {
61 | return this.Current().height;
62 | };
63 | this.ComputeSize = function (d) {
64 | if (d != null && typeof(d) == 'number') return d;
65 | if (d == 'x') return this.width();
66 | if (d == 'y') return this.height();
67 | return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2);
68 | }
69 | });
70 | };
71 | svg.init();
72 |
73 | // images loaded
74 | svg.ImagesLoaded = function () {
75 | for (var i = 0; i < svg.Images.length; i++) {
76 | if (!svg.Images[i].loaded) return false;
77 | }
78 | return true;
79 | };
80 |
81 | // trim
82 | svg.trim = function (s) {
83 | return s.replace(/^\s+|\s+$/g, '');
84 | };
85 |
86 | // compress spaces
87 | svg.compressSpaces = function (s) {
88 | return s.replace(/[\s\r\t\n]+/gm, ' ');
89 | };
90 |
91 | // ajax
92 | svg.ajax = function (url, ajax_callback) {
93 | http.get(url, function (res) {
94 | var data = '';
95 | res.on('data', function (chunk) {
96 | data += chunk;
97 | });
98 | res.on('end', function () {
99 | ajax_callback(data);
100 | });
101 | });
102 | };
103 |
104 | // remote
105 | svg.remote = function (url, remote_callback) {
106 | http.get(url, function (res) {
107 | var data = new Buffer(parseInt(res.headers['content-length'], 10));
108 | var pos = 0;
109 | res.on('data', function (chunk) {
110 | chunk.copy(data, pos);
111 | pos += chunk.length;
112 | });
113 | res.on('end', function () {
114 | remote_callback(data);
115 | });
116 | });
117 | };
118 |
119 | // parse xml
120 | svg.parseXml = function (xml) {
121 | var parser = new xmldom.DOMParser();
122 | return parser.parseFromString(xml, 'text/xml');
123 | };
124 |
125 | svg.Property = function (name, value) {
126 | this.name = name;
127 | this.value = value;
128 | };
129 | svg.Property.prototype.getValue = function () {
130 | return this.value;
131 | };
132 |
133 | svg.Property.prototype.hasValue = function () {
134 | return (this.value != null && this.value !== '');
135 | };
136 |
137 | // return the numerical value of the property
138 | svg.Property.prototype.numValue = function () {
139 | if (!this.hasValue()) return 0;
140 |
141 | var n = parseFloat(this.value);
142 | if ((this.value + '').match(/%$/)) {
143 | n = n / 100.0;
144 | }
145 | return n;
146 | };
147 |
148 | svg.Property.prototype.valueOrDefault = function (def) {
149 | if (this.hasValue()) return this.value;
150 | return def;
151 | };
152 |
153 | svg.Property.prototype.numValueOrDefault = function (def) {
154 | if (this.hasValue()) return this.numValue();
155 | return def;
156 | };
157 |
158 | // color extensions
159 | // augment the current color value with the opacity
160 | svg.Property.prototype.addOpacity = function (opacity) {
161 | var newValue = this.value;
162 | if (opacity != null && opacity != '' && typeof(this.value) == 'string') { // can only add opacity to colors, not patterns
163 | var color = new RGBColor(this.value);
164 | if (color.ok) {
165 | newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')';
166 | }
167 | }
168 | return new svg.Property(this.name, newValue);
169 | };
170 |
171 | // definition extensions
172 | // get the definition from the definitions table
173 | svg.Property.prototype.getDefinition = function () {
174 | var name = this.value.match(/#([^\)'"]+)/);
175 | if (name) {
176 | name = name[1];
177 | }
178 | if (!name) {
179 | name = this.value;
180 | }
181 | return svg.Definitions[name];
182 | };
183 |
184 | svg.Property.prototype.isUrlDefinition = function () {
185 | return this.value.indexOf('url(') == 0
186 | };
187 |
188 | svg.Property.prototype.getFillStyleDefinition = function (e, opacityProp) {
189 | var def = this.getDefinition();
190 |
191 | // gradient
192 | if (def != null && def.createGradient) {
193 | return def.createGradient(svg.ctx, e, opacityProp);
194 | }
195 |
196 | // pattern
197 | if (def != null && def.createPattern) {
198 | if (def.getHrefAttribute().hasValue()) {
199 | var pt = def.attribute('patternTransform');
200 | def = def.getHrefAttribute().getDefinition();
201 | if (pt.hasValue()) {
202 | def.attribute('patternTransform', true).value = pt.value;
203 | }
204 | }
205 | return def.createPattern(svg.ctx, e);
206 | }
207 |
208 | return null;
209 | };
210 |
211 | // length extensions
212 | svg.Property.prototype.getDPI = function (viewPort) {
213 | return 96.0; // TODO: compute?
214 | };
215 |
216 | svg.Property.prototype.getEM = function (viewPort) {
217 | var em = 12;
218 |
219 | var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize);
220 | if (fontSize.hasValue()) em = fontSize.toPixels(viewPort);
221 |
222 | return em;
223 | };
224 |
225 | svg.Property.prototype.getUnits = function () {
226 | var s = this.value + '';
227 | return s.replace(/[0-9\.\-]/g, '');
228 | };
229 |
230 | // get the length as pixels
231 | svg.Property.prototype.toPixels = function (viewPort, processPercent) {
232 | if (!this.hasValue()) return 0;
233 | var s = this.value + '';
234 | if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort);
235 | if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0;
236 | if (s.match(/px$/)) return this.numValue();
237 | if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0);
238 | if (s.match(/pc$/)) return this.numValue() * 15;
239 | if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54;
240 | if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4;
241 | if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort);
242 | if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort);
243 | var n = this.numValue();
244 | if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort);
245 | return n;
246 | };
247 |
248 | // time extensions
249 | // get the time as milliseconds
250 | svg.Property.prototype.toMilliseconds = function () {
251 | if (!this.hasValue()) return 0;
252 | var s = this.value + '';
253 | if (s.match(/s$/)) return this.numValue() * 1000;
254 | if (s.match(/ms$/)) return this.numValue();
255 | return this.numValue();
256 | };
257 |
258 | // angle extensions
259 | // get the angle as radians
260 | svg.Property.prototype.toRadians = function () {
261 | if (!this.hasValue()) return 0;
262 | var s = this.value + '';
263 | if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0);
264 | if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0);
265 | if (s.match(/rad$/)) return this.numValue();
266 | return this.numValue() * (Math.PI / 180.0);
267 | };
268 |
269 | // fonts
270 | svg.Font = new (function () {
271 | this.Styles = 'normal|italic|oblique|inherit';
272 | this.Variants = 'normal|small-caps|inherit';
273 | this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit';
274 |
275 | this.CreateFont = function (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) {
276 | var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font);
277 | return {
278 | fontFamily: fontFamily || f.fontFamily,
279 | fontSize: fontSize || f.fontSize,
280 | fontStyle: fontStyle || f.fontStyle,
281 | fontWeight: fontWeight || f.fontWeight,
282 | fontVariant: fontVariant || f.fontVariant,
283 | toString: function () {
284 | return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ')
285 | }
286 | }
287 | };
288 |
289 | var that = this;
290 | this.Parse = function (s) {
291 | var f = {};
292 | var d = svg.trim(svg.compressSpaces(s || '')).split(' ');
293 | var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false };
294 | var ff = '';
295 | for (var i = 0; i < d.length; i++) {
296 | if (!set.fontStyle && that.Styles.indexOf(d[i]) != -1) {
297 | if (d[i] != 'inherit') f.fontStyle = d[i];
298 | set.fontStyle = true;
299 | }
300 | else if (!set.fontVariant && that.Variants.indexOf(d[i]) != -1) {
301 | if (d[i] != 'inherit') f.fontVariant = d[i];
302 | set.fontStyle = set.fontVariant = true;
303 | }
304 | else if (!set.fontWeight && that.Weights.indexOf(d[i]) != -1) {
305 | if (d[i] != 'inherit') f.fontWeight = d[i];
306 | set.fontStyle = set.fontVariant = set.fontWeight = true;
307 | }
308 | else if (!set.fontSize) {
309 | if (d[i] != 'inherit') f.fontSize = d[i].split('/')[0];
310 | set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true;
311 | }
312 | else {
313 | if (d[i] != 'inherit') ff += d[i];
314 | }
315 | }
316 | if (ff != '') f.fontFamily = ff;
317 | return f;
318 | }
319 | });
320 |
321 | // points and paths
322 | svg.ToNumberArray = function (s) {
323 | var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' ');
324 | for (var i = 0; i < a.length; i++) {
325 | a[i] = parseFloat(a[i]);
326 | }
327 | return a;
328 | };
329 | svg.Point = function (x, y) {
330 | this.x = x;
331 | this.y = y;
332 | };
333 | svg.Point.prototype.angleTo = function (p) {
334 | return Math.atan2(p.y - this.y, p.x - this.x);
335 | };
336 |
337 | svg.Point.prototype.applyTransform = function (v) {
338 | var xp = this.x * v[0] + this.y * v[2] + v[4];
339 | var yp = this.x * v[1] + this.y * v[3] + v[5];
340 | this.x = xp;
341 | this.y = yp;
342 | };
343 |
344 | svg.CreatePoint = function (s) {
345 | var a = svg.ToNumberArray(s);
346 | return new svg.Point(a[0], a[1]);
347 | };
348 | svg.CreatePath = function (s) {
349 | var a = svg.ToNumberArray(s);
350 | var path = [];
351 | for (var i = 0; i < a.length; i += 2) {
352 | path.push(new svg.Point(a[i], a[i + 1]));
353 | }
354 | return path;
355 | };
356 |
357 | // bounding box
358 | svg.BoundingBox = function (x1, y1, x2, y2) { // pass in initial points if you want
359 | this.x1 = Number.NaN;
360 | this.y1 = Number.NaN;
361 | this.x2 = Number.NaN;
362 | this.y2 = Number.NaN;
363 |
364 | this.x = function () {
365 | return this.x1;
366 | };
367 | this.y = function () {
368 | return this.y1;
369 | };
370 | this.width = function () {
371 | return this.x2 - this.x1;
372 | };
373 | this.height = function () {
374 | return this.y2 - this.y1;
375 | };
376 |
377 | this.addPoint = function (x, y) {
378 | if (x != null) {
379 | if (isNaN(this.x1) || isNaN(this.x2)) {
380 | this.x1 = x;
381 | this.x2 = x;
382 | }
383 | if (x < this.x1) this.x1 = x;
384 | if (x > this.x2) this.x2 = x;
385 | }
386 |
387 | if (y != null) {
388 | if (isNaN(this.y1) || isNaN(this.y2)) {
389 | this.y1 = y;
390 | this.y2 = y;
391 | }
392 | if (y < this.y1) this.y1 = y;
393 | if (y > this.y2) this.y2 = y;
394 | }
395 | };
396 | this.addX = function (x) {
397 | this.addPoint(x, null);
398 | };
399 | this.addY = function (y) {
400 | this.addPoint(null, y);
401 | };
402 |
403 | this.addBoundingBox = function (bb) {
404 | this.addPoint(bb.x1, bb.y1);
405 | this.addPoint(bb.x2, bb.y2);
406 | };
407 |
408 | this.addQuadraticCurve = function (p0x, p0y, p1x, p1y, p2x, p2y) {
409 | var cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0)
410 | var cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0)
411 | var cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0)
412 | var cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0)
413 | this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y);
414 | };
415 |
416 | this.addBezierCurve = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
417 | // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
418 | var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y];
419 | this.addPoint(p0[0], p0[1]);
420 | this.addPoint(p3[0], p3[1]);
421 |
422 | for (var i = 0; i <= 1; i++) {
423 | var f = function (t) {
424 | return Math.pow(1 - t, 3) * p0[i]
425 | + 3 * Math.pow(1 - t, 2) * t * p1[i]
426 | + 3 * (1 - t) * Math.pow(t, 2) * p2[i]
427 | + Math.pow(t, 3) * p3[i];
428 | };
429 |
430 | var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
431 | var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
432 | var c = 3 * p1[i] - 3 * p0[i];
433 |
434 | if (a == 0) {
435 | if (b == 0) continue;
436 | var t = -c / b;
437 | if (0 < t && t < 1) {
438 | if (i == 0) this.addX(f(t));
439 | if (i == 1) this.addY(f(t));
440 | }
441 | continue;
442 | }
443 |
444 | var b2ac = Math.pow(b, 2) - 4 * c * a;
445 | if (b2ac < 0) continue;
446 | var t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
447 | if (0 < t1 && t1 < 1) {
448 | if (i == 0) this.addX(f(t1));
449 | if (i == 1) this.addY(f(t1));
450 | }
451 | var t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
452 | if (0 < t2 && t2 < 1) {
453 | if (i == 0) this.addX(f(t2));
454 | if (i == 1) this.addY(f(t2));
455 | }
456 | }
457 | };
458 |
459 | this.isPointInBox = function (x, y) {
460 | return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2);
461 | };
462 |
463 | this.addPoint(x1, y1);
464 | this.addPoint(x2, y2);
465 | };
466 |
467 | // transforms
468 | svg.Transform = function (v) {
469 | var that = this;
470 | this.Type = {};
471 |
472 | // translate
473 | this.Type.translate = function (s) {
474 | this.p = svg.CreatePoint(s);
475 | this.apply = function (ctx) {
476 | ctx.translate(this.p.x || 0.0, this.p.y || 0.0);
477 | };
478 | this.unapply = function (ctx) {
479 | ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0);
480 | };
481 | this.applyToPoint = function (p) {
482 | p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]);
483 | }
484 | };
485 |
486 | // rotate
487 | this.Type.rotate = function (s) {
488 | var a = svg.ToNumberArray(s);
489 | this.angle = new svg.Property('angle', a[0]);
490 | this.cx = a[1] || 0;
491 | this.cy = a[2] || 0;
492 | this.apply = function (ctx) {
493 | ctx.translate(this.cx, this.cy);
494 | ctx.rotate(this.angle.toRadians());
495 | ctx.translate(-this.cx, -this.cy);
496 | };
497 | this.unapply = function (ctx) {
498 | ctx.translate(this.cx, this.cy);
499 | ctx.rotate(-1.0 * this.angle.toRadians());
500 | ctx.translate(-this.cx, -this.cy);
501 | };
502 | this.applyToPoint = function (p) {
503 | var a = this.angle.toRadians();
504 | p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]);
505 | p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]);
506 | p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]);
507 | }
508 | };
509 |
510 | this.Type.scale = function (s) {
511 | this.p = svg.CreatePoint(s);
512 | this.apply = function (ctx) {
513 | ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0);
514 | };
515 | this.unapply = function (ctx) {
516 | ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0);
517 | };
518 | this.applyToPoint = function (p) {
519 | p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]);
520 | }
521 | };
522 |
523 | this.Type.matrix = function (s) {
524 | this.m = svg.ToNumberArray(s);
525 | this.apply = function (ctx) {
526 | ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]);
527 | };
528 | this.applyToPoint = function (p) {
529 | p.applyTransform(this.m);
530 | }
531 | };
532 |
533 | this.Type.SkewBase = function (s) {
534 | this.base = that.Type.matrix;
535 | this.base(s);
536 | this.angle = new svg.Property('angle', s);
537 | };
538 | this.Type.SkewBase.prototype = new this.Type.matrix;
539 |
540 | this.Type.skewX = function (s) {
541 | this.base = that.Type.SkewBase;
542 | this.base(s);
543 | this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0];
544 | };
545 | this.Type.skewX.prototype = new this.Type.SkewBase;
546 |
547 | this.Type.skewY = function (s) {
548 | this.base = that.Type.SkewBase;
549 | this.base(s);
550 | this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0];
551 | };
552 | this.Type.skewY.prototype = new this.Type.SkewBase;
553 |
554 | this.transforms = [];
555 |
556 | this.apply = function (ctx) {
557 | for (var i = 0; i < this.transforms.length; i++) {
558 | this.transforms[i].apply(ctx);
559 | }
560 | };
561 |
562 | this.unapply = function (ctx) {
563 | for (var i = this.transforms.length - 1; i >= 0; i--) {
564 | this.transforms[i].unapply(ctx);
565 | }
566 | };
567 |
568 | this.applyToPoint = function (p) {
569 | for (var i = 0; i < this.transforms.length; i++) {
570 | this.transforms[i].applyToPoint(p);
571 | }
572 | };
573 |
574 | var data = svg.trim(svg.compressSpaces(v)).replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/);
575 | for (var i = 0; i < data.length; i++) {
576 | var type = svg.trim(data[i].split('(')[0]);
577 | var s = data[i].split('(')[1].replace(')', '');
578 | var transform = new this.Type[type](s);
579 | transform.type = type;
580 | this.transforms.push(transform);
581 | }
582 | };
583 |
584 | // aspect ratio
585 | svg.AspectRatio = function (ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) {
586 | // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute
587 | aspectRatio = svg.compressSpaces(aspectRatio);
588 | aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer
589 | var align = aspectRatio.split(' ')[0] || 'xMidYMid';
590 | var meetOrSlice = aspectRatio.split(' ')[1] || 'meet';
591 |
592 | // calculate scale
593 | var scaleX = width / desiredWidth;
594 | var scaleY = height / desiredHeight;
595 | var scaleMin = Math.min(scaleX, scaleY);
596 | var scaleMax = Math.max(scaleX, scaleY);
597 | if (meetOrSlice == 'meet') {
598 | desiredWidth *= scaleMin;
599 | desiredHeight *= scaleMin;
600 | }
601 | if (meetOrSlice == 'slice') {
602 | desiredWidth *= scaleMax;
603 | desiredHeight *= scaleMax;
604 | }
605 |
606 | refX = new svg.Property('refX', refX);
607 | refY = new svg.Property('refY', refY);
608 | if (refX.hasValue() && refY.hasValue()) {
609 | ctx.translate(-scaleMin * refX.toPixels('x'), -scaleMin * refY.toPixels('y'));
610 | }
611 | else {
612 | // align
613 | if (align.match(/^xMid/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0);
614 | if (align.match(/YMid$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0);
615 | if (align.match(/^xMax/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width - desiredWidth, 0);
616 | if (align.match(/YMax$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height - desiredHeight);
617 | }
618 |
619 | // scale
620 | if (align == 'none') ctx.scale(scaleX, scaleY);
621 | else if (meetOrSlice == 'meet') ctx.scale(scaleMin, scaleMin);
622 | else if (meetOrSlice == 'slice') ctx.scale(scaleMax, scaleMax);
623 |
624 | // translate
625 | ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY);
626 | };
627 |
628 | // elements
629 | svg.Element = {};
630 |
631 | svg.EmptyProperty = new svg.Property('EMPTY', '');
632 |
633 | svg.Element.ElementBase = function (node) {
634 | this.attributes = {};
635 | this.styles = {};
636 | this.children = [];
637 |
638 | // get or create attribute
639 | this.attribute = function (name, createIfNotExists) {
640 | var a = this.attributes[name];
641 | if (a != null) return a;
642 |
643 | if (createIfNotExists == true) {
644 | a = new svg.Property(name, '');
645 | this.attributes[name] = a;
646 | }
647 | return a || svg.EmptyProperty;
648 | };
649 |
650 | this.getHrefAttribute = function () {
651 | for (var a in this.attributes) {
652 | if (a.match(/:href$/)) {
653 | return this.attributes[a];
654 | }
655 | }
656 | return svg.EmptyProperty;
657 | };
658 |
659 | // get or create style, crawls up node tree
660 | this.style = function (name, createIfNotExists) {
661 | var s = this.styles[name];
662 | if (s != null) return s;
663 |
664 | var a = this.attribute(name);
665 | if (a != null && a.hasValue()) {
666 | this.styles[name] = a; // move up to me to cache
667 | return a;
668 | }
669 |
670 | var p = this.parent;
671 | if (p != null) {
672 | var ps = p.style(name);
673 | if (ps != null && ps.hasValue()) {
674 | return ps;
675 | }
676 | }
677 |
678 | if (createIfNotExists == true) {
679 | s = new svg.Property(name, '');
680 | this.styles[name] = s;
681 | }
682 | return s || svg.EmptyProperty;
683 | };
684 |
685 | // base render
686 | this.render = function (ctx) {
687 | // don't render display=none
688 | if (this.style('display').value == 'none') return;
689 |
690 | // don't render visibility=hidden
691 | if (this.attribute('visibility').value == 'hidden') return;
692 |
693 | ctx.save();
694 | if (this.attribute('mask').hasValue()) { // mask
695 | var mask = this.attribute('mask').getDefinition();
696 | if (mask != null) mask.apply(ctx, this);
697 | }
698 | else if (this.style('filter').hasValue()) { // filter
699 | var filter = this.style('filter').getDefinition();
700 | if (filter != null) filter.apply(ctx, this);
701 | }
702 | else {
703 | this.setContext(ctx);
704 | this.renderChildren(ctx);
705 | this.clearContext(ctx);
706 | }
707 | ctx.restore();
708 | };
709 |
710 | // base set context
711 | this.setContext = function (ctx) {
712 | // OVERRIDE ME!
713 | };
714 |
715 | // base clear context
716 | this.clearContext = function (ctx) {
717 | // OVERRIDE ME!
718 | };
719 |
720 | // base render children
721 | this.renderChildren = function (ctx) {
722 | for (var i = 0; i < this.children.length; i++) {
723 | this.children[i].render(ctx);
724 | }
725 | };
726 |
727 | this.addChild = function (childNode, create) {
728 | var child = childNode;
729 | if (create) child = svg.CreateElement(childNode);
730 | child.parent = this;
731 | this.children.push(child);
732 | };
733 |
734 | if (node != null && node.nodeType == 1) { //ELEMENT_NODE
735 | // add children
736 | for (var i = 0; i < node.childNodes.length; i++) {
737 | var childNode = node.childNodes[i];
738 | if (childNode.nodeType == 1) this.addChild(childNode, true); //ELEMENT_NODE
739 | if (this.captureTextNodes && childNode.nodeType == 3) {
740 | var text = childNode.nodeValue || childNode.text || '';
741 | if (svg.trim(svg.compressSpaces(text)) != '') {
742 | this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE
743 | }
744 | }
745 | }
746 |
747 | // add attributes
748 | for (var i = 0; i < node.attributes.length; i++) {
749 | var attribute = node.attributes[i];
750 | this.attributes[attribute.nodeName] = new svg.Property(attribute.nodeName, attribute.nodeValue);
751 | }
752 |
753 | // add tag styles
754 | var styles = svg.Styles[node.nodeName];
755 | if (styles != null) {
756 | for (var name in styles) {
757 | this.styles[name] = styles[name];
758 | }
759 | }
760 |
761 | // add class styles
762 | if (this.attribute('class').hasValue()) {
763 | var classes = svg.compressSpaces(this.attribute('class').value).split(' ');
764 | for (var j = 0; j < classes.length; j++) {
765 | styles = svg.Styles['.' + classes[j]];
766 | if (styles != null) {
767 | for (var name in styles) {
768 | this.styles[name] = styles[name];
769 | }
770 | }
771 | styles = svg.Styles[node.nodeName + '.' + classes[j]];
772 | if (styles != null) {
773 | for (var name in styles) {
774 | this.styles[name] = styles[name];
775 | }
776 | }
777 | }
778 | }
779 |
780 | // add id styles
781 | if (this.attribute('id').hasValue()) {
782 | var styles = svg.Styles['#' + this.attribute('id').value];
783 | if (styles != null) {
784 | for (var name in styles) {
785 | this.styles[name] = styles[name];
786 | }
787 | }
788 | }
789 |
790 | // add inline styles
791 | if (this.attribute('style').hasValue()) {
792 | var styles = this.attribute('style').value.split(';');
793 | for (var i = 0; i < styles.length; i++) {
794 | if (svg.trim(styles[i]) != '') {
795 | var style = styles[i].split(':');
796 | var name = svg.trim(style[0]);
797 | var value = svg.trim(style[1]);
798 | this.styles[name] = new svg.Property(name, value);
799 | }
800 | }
801 | }
802 |
803 | // add id
804 | if (this.attribute('id').hasValue()) {
805 | if (svg.Definitions[this.attribute('id').value] == null) {
806 | svg.Definitions[this.attribute('id').value] = this;
807 | }
808 | }
809 | }
810 | };
811 |
812 | svg.Element.RenderedElementBase = function (node) {
813 | this.base = svg.Element.ElementBase;
814 | this.base(node);
815 |
816 | this.setContext = function (ctx) {
817 | // fill
818 | if (this.style('fill').isUrlDefinition()) {
819 | var fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity'));
820 | if (fs != null) ctx.fillStyle = fs;
821 | }
822 | else if (this.style('fill').hasValue()) {
823 | var fillStyle = this.style('fill');
824 | if (fillStyle.value == 'currentColor') fillStyle.value = this.style('color').value;
825 | ctx.fillStyle = (fillStyle.value == 'none' ? 'rgba(0,0,0,0)' : fillStyle.value);
826 | }
827 | if (this.style('fill-opacity').hasValue()) {
828 | var fillStyle = new svg.Property('fill', ctx.fillStyle);
829 | fillStyle = fillStyle.addOpacity(this.style('fill-opacity').value);
830 | ctx.fillStyle = fillStyle.value;
831 | }
832 |
833 | // stroke
834 | if (this.style('stroke').isUrlDefinition()) {
835 | var fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity'));
836 | if (fs != null) ctx.strokeStyle = fs;
837 | }
838 | else if (this.style('stroke').hasValue()) {
839 | var strokeStyle = this.style('stroke');
840 | if (strokeStyle.value == 'currentColor') strokeStyle.value = this.style('color').value;
841 | ctx.strokeStyle = (strokeStyle.value == 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value);
842 | }
843 | if (this.style('stroke-opacity').hasValue()) {
844 | var strokeStyle = new svg.Property('stroke', ctx.strokeStyle);
845 | strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity').value);
846 | ctx.strokeStyle = strokeStyle.value;
847 | }
848 | if (this.style('stroke-width').hasValue()) {
849 | var newLineWidth = this.style('stroke-width').toPixels();
850 | ctx.lineWidth = newLineWidth == 0 ? 0.001 : newLineWidth; // browsers don't respect 0
851 | }
852 | if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value;
853 | if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value;
854 | if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value;
855 | if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value != 'none') {
856 | var gaps = svg.ToNumberArray(this.style('stroke-dasharray').value);
857 | if (typeof(ctx.setLineDash) != 'undefined') {
858 | ctx.setLineDash(gaps);
859 | }
860 | else if (typeof(ctx.webkitLineDash) != 'undefined') {
861 | ctx.webkitLineDash = gaps;
862 | }
863 | else if (typeof(ctx.mozDash ) != 'undefined') {
864 | ctx.mozDash = gaps;
865 | }
866 |
867 | var offset = this.style('stroke-dashoffset').numValueOrDefault(1);
868 | if (typeof(ctx.lineDashOffset) != 'undefined') {
869 | ctx.lineDashOffset = offset;
870 | }
871 | else if (typeof(ctx.webkitLineDashOffset) != 'undefined') {
872 | ctx.webkitLineDashOffset = offset;
873 | }
874 | else if (typeof(ctx.mozDashOffset) != 'undefined') {
875 | ctx.mozDashOffset = offset;
876 | }
877 | }
878 |
879 | // font
880 | if (typeof(ctx.font) != 'undefined') {
881 | ctx.font = svg.Font.CreateFont(
882 | this.style('font-style').value,
883 | this.style('font-variant').value,
884 | this.style('font-weight').value,
885 | this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '',
886 | this.style('font-family').value).toString();
887 | }
888 |
889 | // transform
890 | if (this.attribute('transform').hasValue()) {
891 | var transform = new svg.Transform(this.attribute('transform').value);
892 | transform.apply(ctx);
893 | }
894 |
895 | // clip
896 | if (this.style('clip-path').hasValue()) {
897 | var clip = this.style('clip-path').getDefinition();
898 | if (clip != null) clip.apply(ctx);
899 | }
900 |
901 | // opacity
902 | if (this.style('opacity').hasValue()) {
903 | ctx.globalAlpha = this.style('opacity').numValue();
904 | }
905 | }
906 | };
907 | svg.Element.RenderedElementBase.prototype = new svg.Element.ElementBase;
908 |
909 | svg.Element.PathElementBase = function (node) {
910 | this.base = svg.Element.RenderedElementBase;
911 | this.base(node);
912 |
913 | this.path = function (ctx) {
914 | if (ctx != null) ctx.beginPath();
915 | return new svg.BoundingBox();
916 | };
917 |
918 | this.renderChildren = function (ctx) {
919 | this.path(ctx);
920 | svg.Mouse.checkPath(this, ctx);
921 | if (ctx.fillStyle != '') {
922 | if (this.attribute('fill-rule').valueOrDefault('inherit') != 'inherit') {
923 | ctx.fill(this.attribute('fill-rule').value);
924 | }
925 | else {
926 | ctx.fill();
927 | }
928 | }
929 | if (ctx.strokeStyle != '') ctx.stroke();
930 |
931 | var markers = this.getMarkers();
932 | if (markers != null) {
933 | if (this.style('marker-start').isUrlDefinition()) {
934 | var marker = this.style('marker-start').getDefinition();
935 | marker.render(ctx, markers[0][0], markers[0][1]);
936 | }
937 | if (this.style('marker-mid').isUrlDefinition()) {
938 | var marker = this.style('marker-mid').getDefinition();
939 | for (var i = 1; i < markers.length - 1; i++) {
940 | marker.render(ctx, markers[i][0], markers[i][1]);
941 | }
942 | }
943 | if (this.style('marker-end').isUrlDefinition()) {
944 | var marker = this.style('marker-end').getDefinition();
945 | marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]);
946 | }
947 | }
948 | };
949 |
950 | this.getBoundingBox = function () {
951 | return this.path();
952 | };
953 |
954 | this.getMarkers = function () {
955 | return null;
956 | }
957 | };
958 | svg.Element.PathElementBase.prototype = new svg.Element.RenderedElementBase;
959 |
960 | // svg element
961 | svg.Element.svg = function (node) {
962 | this.base = svg.Element.RenderedElementBase;
963 | this.base(node);
964 |
965 | this.baseClearContext = this.clearContext;
966 | this.clearContext = function (ctx) {
967 | this.baseClearContext(ctx);
968 | svg.ViewPort.RemoveCurrent();
969 | };
970 |
971 | this.baseSetContext = this.setContext;
972 | this.setContext = function (ctx) {
973 | // initial values
974 | ctx.strokeStyle = 'rgba(0,0,0,0)';
975 | ctx.lineCap = 'butt';
976 | ctx.lineJoin = 'miter';
977 | ctx.miterLimit = 4;
978 |
979 | this.baseSetContext(ctx);
980 |
981 | // create new view port
982 | if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0;
983 | if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0;
984 | ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y'));
985 |
986 | var width = svg.ViewPort.width();
987 | var height = svg.ViewPort.height();
988 |
989 | if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%';
990 | if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%';
991 | if (typeof(this.root) == 'undefined') {
992 | width = this.attribute('width').toPixels('x');
993 | height = this.attribute('height').toPixels('y');
994 |
995 | var x = 0;
996 | var y = 0;
997 | if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) {
998 | x = -this.attribute('refX').toPixels('x');
999 | y = -this.attribute('refY').toPixels('y');
1000 | }
1001 |
1002 | ctx.beginPath();
1003 | ctx.moveTo(x, y);
1004 | ctx.lineTo(width, y);
1005 | ctx.lineTo(width, height);
1006 | ctx.lineTo(x, height);
1007 | ctx.closePath();
1008 | ctx.clip();
1009 | }
1010 | svg.ViewPort.SetCurrent(width, height);
1011 |
1012 | // viewbox
1013 | if (this.attribute('viewBox').hasValue()) {
1014 | var viewBox = svg.ToNumberArray(this.attribute('viewBox').value);
1015 | var minX = viewBox[0];
1016 | var minY = viewBox[1];
1017 | width = viewBox[2];
1018 | height = viewBox[3];
1019 |
1020 | svg.AspectRatio(ctx,
1021 | this.attribute('preserveAspectRatio').value,
1022 | svg.ViewPort.width(),
1023 | width,
1024 | svg.ViewPort.height(),
1025 | height,
1026 | minX,
1027 | minY,
1028 | this.attribute('refX').value,
1029 | this.attribute('refY').value);
1030 |
1031 | svg.ViewPort.RemoveCurrent();
1032 | svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]);
1033 | }
1034 | }
1035 | };
1036 | svg.Element.svg.prototype = new svg.Element.RenderedElementBase;
1037 |
1038 | // rect element
1039 | svg.Element.rect = function (node) {
1040 | this.base = svg.Element.PathElementBase;
1041 | this.base(node);
1042 |
1043 | this.path = function (ctx) {
1044 | var x = this.attribute('x').toPixels('x');
1045 | var y = this.attribute('y').toPixels('y');
1046 | var width = this.attribute('width').toPixels('x');
1047 | var height = this.attribute('height').toPixels('y');
1048 | var rx = this.attribute('rx').toPixels('x');
1049 | var ry = this.attribute('ry').toPixels('y');
1050 | if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx;
1051 | if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry;
1052 | rx = Math.min(rx, width / 2.0);
1053 | ry = Math.min(ry, height / 2.0);
1054 | if (ctx != null) {
1055 | ctx.beginPath();
1056 | ctx.moveTo(x + rx, y);
1057 | ctx.lineTo(x + width - rx, y);
1058 | ctx.quadraticCurveTo(x + width, y, x + width, y + ry);
1059 | ctx.lineTo(x + width, y + height - ry);
1060 | ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height);
1061 | ctx.lineTo(x + rx, y + height);
1062 | ctx.quadraticCurveTo(x, y + height, x, y + height - ry);
1063 | ctx.lineTo(x, y + ry);
1064 | ctx.quadraticCurveTo(x, y, x + rx, y);
1065 | ctx.closePath();
1066 | }
1067 |
1068 | return new svg.BoundingBox(x, y, x + width, y + height);
1069 | }
1070 | };
1071 | svg.Element.rect.prototype = new svg.Element.PathElementBase;
1072 |
1073 | // circle element
1074 | svg.Element.circle = function (node) {
1075 | this.base = svg.Element.PathElementBase;
1076 | this.base(node);
1077 |
1078 | this.path = function (ctx) {
1079 | var cx = this.attribute('cx').toPixels('x');
1080 | var cy = this.attribute('cy').toPixels('y');
1081 | var r = this.attribute('r').toPixels();
1082 |
1083 | if (ctx != null) {
1084 | ctx.beginPath();
1085 | ctx.arc(cx, cy, r, 0, Math.PI * 2, true);
1086 | ctx.closePath();
1087 | }
1088 |
1089 | return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r);
1090 | }
1091 | };
1092 | svg.Element.circle.prototype = new svg.Element.PathElementBase;
1093 |
1094 | // ellipse element
1095 | svg.Element.ellipse = function (node) {
1096 | this.base = svg.Element.PathElementBase;
1097 | this.base(node);
1098 |
1099 | this.path = function (ctx) {
1100 | var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3);
1101 | var rx = this.attribute('rx').toPixels('x');
1102 | var ry = this.attribute('ry').toPixels('y');
1103 | var cx = this.attribute('cx').toPixels('x');
1104 | var cy = this.attribute('cy').toPixels('y');
1105 |
1106 | if (ctx != null) {
1107 | ctx.beginPath();
1108 | ctx.moveTo(cx, cy - ry);
1109 | ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy);
1110 | ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry);
1111 | ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy);
1112 | ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry);
1113 | ctx.closePath();
1114 | }
1115 |
1116 | return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry);
1117 | }
1118 | };
1119 | svg.Element.ellipse.prototype = new svg.Element.PathElementBase;
1120 |
1121 | // line element
1122 | svg.Element.line = function (node) {
1123 | this.base = svg.Element.PathElementBase;
1124 | this.base(node);
1125 |
1126 | this.getPoints = function () {
1127 | return [
1128 | new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')),
1129 | new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))];
1130 | };
1131 |
1132 | this.path = function (ctx) {
1133 | var points = this.getPoints();
1134 |
1135 | if (ctx != null) {
1136 | ctx.beginPath();
1137 | ctx.moveTo(points[0].x, points[0].y);
1138 | ctx.lineTo(points[1].x, points[1].y);
1139 | }
1140 |
1141 | return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y);
1142 | };
1143 |
1144 | this.getMarkers = function () {
1145 | var points = this.getPoints();
1146 | var a = points[0].angleTo(points[1]);
1147 | return [
1148 | [points[0], a],
1149 | [points[1], a]
1150 | ];
1151 | }
1152 | };
1153 | svg.Element.line.prototype = new svg.Element.PathElementBase;
1154 |
1155 | // polyline element
1156 | svg.Element.polyline = function (node) {
1157 | this.base = svg.Element.PathElementBase;
1158 | this.base(node);
1159 |
1160 | this.points = svg.CreatePath(this.attribute('points').value);
1161 | this.path = function (ctx) {
1162 | var bb = new svg.BoundingBox(this.points[0].x, this.points[0].y);
1163 | if (ctx != null) {
1164 | ctx.beginPath();
1165 | ctx.moveTo(this.points[0].x, this.points[0].y);
1166 | }
1167 | for (var i = 1; i < this.points.length; i++) {
1168 | bb.addPoint(this.points[i].x, this.points[i].y);
1169 | if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y);
1170 | }
1171 | return bb;
1172 | };
1173 |
1174 | this.getMarkers = function () {
1175 | var markers = [];
1176 | for (var i = 0; i < this.points.length - 1; i++) {
1177 | markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]);
1178 | }
1179 | markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]);
1180 | return markers;
1181 | }
1182 | };
1183 | svg.Element.polyline.prototype = new svg.Element.PathElementBase;
1184 |
1185 | // polygon element
1186 | svg.Element.polygon = function (node) {
1187 | this.base = svg.Element.polyline;
1188 | this.base(node);
1189 |
1190 | this.basePath = this.path;
1191 | this.path = function (ctx) {
1192 | var bb = this.basePath(ctx);
1193 | if (ctx != null) {
1194 | ctx.lineTo(this.points[0].x, this.points[0].y);
1195 | ctx.closePath();
1196 | }
1197 | return bb;
1198 | }
1199 | };
1200 | svg.Element.polygon.prototype = new svg.Element.polyline;
1201 |
1202 | // path element
1203 | svg.Element.path = function (node) {
1204 | this.base = svg.Element.PathElementBase;
1205 | this.base(node);
1206 |
1207 | var d = this.attribute('d').value;
1208 | // TODO: convert to real lexer based on http://www.w3.org/TR/SVG11/paths.html#PathDataBNF
1209 | d = d.replace(/,/gm, ' '); // get rid of all commas
1210 | d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands
1211 | d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands
1212 | d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2'); // separate commands from points
1213 | d = d.replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from points
1214 | d = d.replace(/([0-9])([+\-])/gm, '$1 $2'); // separate digits when no comma
1215 | d = d.replace(/(\.[0-9]*)(\.)/gm, '$1 $2'); // separate digits when no comma
1216 | d = d.replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax
1217 | d = svg.compressSpaces(d); // compress multiple spaces
1218 | d = svg.trim(d);
1219 | this.PathParser = new (function (d) {
1220 | this.tokens = d.split(' ');
1221 |
1222 | this.reset = function () {
1223 | this.i = -1;
1224 | this.command = '';
1225 | this.previousCommand = '';
1226 | this.start = new svg.Point(0, 0);
1227 | this.control = new svg.Point(0, 0);
1228 | this.current = new svg.Point(0, 0);
1229 | this.points = [];
1230 | this.angles = [];
1231 | };
1232 |
1233 | this.isEnd = function () {
1234 | return this.i >= this.tokens.length - 1;
1235 | };
1236 |
1237 | this.isCommandOrEnd = function () {
1238 | if (this.isEnd()) return true;
1239 | return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null;
1240 | };
1241 |
1242 | this.isRelativeCommand = function () {
1243 | switch (this.command) {
1244 | case 'm':
1245 | case 'l':
1246 | case 'h':
1247 | case 'v':
1248 | case 'c':
1249 | case 's':
1250 | case 'q':
1251 | case 't':
1252 | case 'a':
1253 | case 'z':
1254 | return true;
1255 | break;
1256 | }
1257 | return false;
1258 | };
1259 |
1260 | this.getToken = function () {
1261 | this.i++;
1262 | return this.tokens[this.i];
1263 | };
1264 |
1265 | this.getScalar = function () {
1266 | return parseFloat(this.getToken());
1267 | };
1268 |
1269 | this.nextCommand = function () {
1270 | this.previousCommand = this.command;
1271 | this.command = this.getToken();
1272 | };
1273 |
1274 | this.getPoint = function () {
1275 | var p = new svg.Point(this.getScalar(), this.getScalar());
1276 | return this.makeAbsolute(p);
1277 | };
1278 |
1279 | this.getAsControlPoint = function () {
1280 | var p = this.getPoint();
1281 | this.control = p;
1282 | return p;
1283 | };
1284 |
1285 | this.getAsCurrentPoint = function () {
1286 | var p = this.getPoint();
1287 | this.current = p;
1288 | return p;
1289 | };
1290 |
1291 | this.getReflectedControlPoint = function () {
1292 | if (this.previousCommand.toLowerCase() != 'c' &&
1293 | this.previousCommand.toLowerCase() != 's' &&
1294 | this.previousCommand.toLowerCase() != 'q' &&
1295 | this.previousCommand.toLowerCase() != 't') {
1296 | return this.current;
1297 | }
1298 |
1299 | // reflect point
1300 | return new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y);
1301 | };
1302 |
1303 | this.makeAbsolute = function (p) {
1304 | if (this.isRelativeCommand()) {
1305 | p.x += this.current.x;
1306 | p.y += this.current.y;
1307 | }
1308 | return p;
1309 | };
1310 |
1311 | this.addMarker = function (p, from, priorTo) {
1312 | // if the last angle isn't filled in because we didn't have this point yet ...
1313 | if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) {
1314 | this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo);
1315 | }
1316 | this.addMarkerAngle(p, from == null ? null : from.angleTo(p));
1317 | };
1318 |
1319 | this.addMarkerAngle = function (p, a) {
1320 | this.points.push(p);
1321 | this.angles.push(a);
1322 | };
1323 |
1324 | this.getMarkerPoints = function () {
1325 | return this.points;
1326 | };
1327 | this.getMarkerAngles = function () {
1328 | for (var i = 0; i < this.angles.length; i++) {
1329 | if (this.angles[i] == null) {
1330 | for (var j = i + 1; j < this.angles.length; j++) {
1331 | if (this.angles[j] != null) {
1332 | this.angles[i] = this.angles[j];
1333 | break;
1334 | }
1335 | }
1336 | }
1337 | }
1338 | return this.angles;
1339 | }
1340 | })(d);
1341 |
1342 | this.path = function (ctx) {
1343 | var pp = this.PathParser;
1344 | pp.reset();
1345 |
1346 | var bb = new svg.BoundingBox();
1347 | if (ctx != null) ctx.beginPath();
1348 | while (!pp.isEnd()) {
1349 | pp.nextCommand();
1350 | switch (pp.command) {
1351 | case 'M':
1352 | case 'm':
1353 | var p = pp.getAsCurrentPoint();
1354 | pp.addMarker(p);
1355 | bb.addPoint(p.x, p.y);
1356 | if (ctx != null) ctx.moveTo(p.x, p.y);
1357 | pp.start = pp.current;
1358 | while (!pp.isCommandOrEnd()) {
1359 | var p = pp.getAsCurrentPoint();
1360 | pp.addMarker(p, pp.start);
1361 | bb.addPoint(p.x, p.y);
1362 | if (ctx != null) ctx.lineTo(p.x, p.y);
1363 | }
1364 | break;
1365 | case 'L':
1366 | case 'l':
1367 | while (!pp.isCommandOrEnd()) {
1368 | var c = pp.current;
1369 | var p = pp.getAsCurrentPoint();
1370 | pp.addMarker(p, c);
1371 | bb.addPoint(p.x, p.y);
1372 | if (ctx != null) ctx.lineTo(p.x, p.y);
1373 | }
1374 | break;
1375 | case 'H':
1376 | case 'h':
1377 | while (!pp.isCommandOrEnd()) {
1378 | var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y);
1379 | pp.addMarker(newP, pp.current);
1380 | pp.current = newP;
1381 | bb.addPoint(pp.current.x, pp.current.y);
1382 | if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y);
1383 | }
1384 | break;
1385 | case 'V':
1386 | case 'v':
1387 | while (!pp.isCommandOrEnd()) {
1388 | var newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar());
1389 | pp.addMarker(newP, pp.current);
1390 | pp.current = newP;
1391 | bb.addPoint(pp.current.x, pp.current.y);
1392 | if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y);
1393 | }
1394 | break;
1395 | case 'C':
1396 | case 'c':
1397 | while (!pp.isCommandOrEnd()) {
1398 | var curr = pp.current;
1399 | var p1 = pp.getPoint();
1400 | var cntrl = pp.getAsControlPoint();
1401 | var cp = pp.getAsCurrentPoint();
1402 | pp.addMarker(cp, cntrl, p1);
1403 | bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);
1404 | if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);
1405 | }
1406 | break;
1407 | case 'S':
1408 | case 's':
1409 | while (!pp.isCommandOrEnd()) {
1410 | var curr = pp.current;
1411 | var p1 = pp.getReflectedControlPoint();
1412 | var cntrl = pp.getAsControlPoint();
1413 | var cp = pp.getAsCurrentPoint();
1414 | pp.addMarker(cp, cntrl, p1);
1415 | bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);
1416 | if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y);
1417 | }
1418 | break;
1419 | case 'Q':
1420 | case 'q':
1421 | while (!pp.isCommandOrEnd()) {
1422 | var curr = pp.current;
1423 | var cntrl = pp.getAsControlPoint();
1424 | var cp = pp.getAsCurrentPoint();
1425 | pp.addMarker(cp, cntrl, cntrl);
1426 | bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y);
1427 | if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y);
1428 | }
1429 | break;
1430 | case 'T':
1431 | case 't':
1432 | while (!pp.isCommandOrEnd()) {
1433 | var curr = pp.current;
1434 | var cntrl = pp.getReflectedControlPoint();
1435 | pp.control = cntrl;
1436 | var cp = pp.getAsCurrentPoint();
1437 | pp.addMarker(cp, cntrl, cntrl);
1438 | bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y);
1439 | if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y);
1440 | }
1441 | break;
1442 | case 'A':
1443 | case 'a':
1444 | while (!pp.isCommandOrEnd()) {
1445 | var curr = pp.current;
1446 | var rx = pp.getScalar();
1447 | var ry = pp.getScalar();
1448 | var xAxisRotation = pp.getScalar() * (Math.PI / 180.0);
1449 | var largeArcFlag = pp.getScalar();
1450 | var sweepFlag = pp.getScalar();
1451 | var cp = pp.getAsCurrentPoint();
1452 |
1453 | // Conversion from endpoint to center parameterization
1454 | // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
1455 | // x1', y1'
1456 | var currp = new svg.Point(
1457 | Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0,
1458 | -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0
1459 | );
1460 | // adjust radii
1461 | var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2);
1462 | if (l > 1) {
1463 | rx *= Math.sqrt(l);
1464 | ry *= Math.sqrt(l);
1465 | }
1466 | // cx', cy'
1467 | var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt(
1468 | ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) /
1469 | (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2))
1470 | );
1471 | if (isNaN(s)) s = 0;
1472 | var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx);
1473 | // cx, cy
1474 | var centp = new svg.Point(
1475 | (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y,
1476 | (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y
1477 | );
1478 | // vector magnitude
1479 | var m = function (v) {
1480 | return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2));
1481 | };
1482 | // ratio between two vectors
1483 | var r = function (u, v) {
1484 | return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v))
1485 | };
1486 | // angle between two vectors
1487 | var a = function (u, v) {
1488 | return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v));
1489 | };
1490 | // initial angle
1491 | var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]);
1492 | // angle delta
1493 | var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry];
1494 | var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry];
1495 | var ad = a(u, v);
1496 | if (r(u, v) <= -1) ad = Math.PI;
1497 | if (r(u, v) >= 1) ad = 0;
1498 |
1499 | // for markers
1500 | var dir = 1 - sweepFlag ? 1.0 : -1.0;
1501 | var ah = a1 + dir * (ad / 2.0);
1502 | var halfWay = new svg.Point(
1503 | centp.x + rx * Math.cos(ah),
1504 | centp.y + ry * Math.sin(ah)
1505 | );
1506 | pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2);
1507 | pp.addMarkerAngle(cp, ah - dir * Math.PI);
1508 |
1509 | bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better
1510 | if (ctx != null) {
1511 | var r = rx > ry ? rx : ry;
1512 | var sx = rx > ry ? 1 : rx / ry;
1513 | var sy = rx > ry ? ry / rx : 1;
1514 |
1515 | ctx.translate(centp.x, centp.y);
1516 | ctx.rotate(xAxisRotation);
1517 | ctx.scale(sx, sy);
1518 | ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag);
1519 | ctx.scale(1 / sx, 1 / sy);
1520 | ctx.rotate(-xAxisRotation);
1521 | ctx.translate(-centp.x, -centp.y);
1522 | }
1523 | }
1524 | break;
1525 | case 'Z':
1526 | case 'z':
1527 | if (ctx != null) ctx.closePath();
1528 | pp.current = pp.start;
1529 | }
1530 | }
1531 |
1532 | return bb;
1533 | };
1534 |
1535 | this.getMarkers = function () {
1536 | var points = this.PathParser.getMarkerPoints();
1537 | var angles = this.PathParser.getMarkerAngles();
1538 |
1539 | var markers = [];
1540 | for (var i = 0; i < points.length; i++) {
1541 | markers.push([points[i], angles[i]]);
1542 | }
1543 | return markers;
1544 | }
1545 | };
1546 | svg.Element.path.prototype = new svg.Element.PathElementBase;
1547 |
1548 | // pattern element
1549 | svg.Element.pattern = function (node) {
1550 | this.base = svg.Element.ElementBase;
1551 | this.base(node);
1552 |
1553 | this.createPattern = function (ctx, element) {
1554 | var width = this.attribute('width').toPixels('x', true);
1555 | var height = this.attribute('height').toPixels('y', true);
1556 |
1557 | // render me using a temporary svg element
1558 | var tempSvg = new svg.Element.svg();
1559 | tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value);
1560 | tempSvg.attributes['width'] = new svg.Property('width', width + 'px');
1561 | tempSvg.attributes['height'] = new svg.Property('height', height + 'px');
1562 | tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value);
1563 | tempSvg.children = this.children;
1564 |
1565 | var c = document.createElement('canvas');
1566 | c.width = width;
1567 | c.height = height;
1568 | var cctx = c.getContext('2d');
1569 | if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) {
1570 | cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true));
1571 | }
1572 | // render 3x3 grid so when we transform there's no white space on edges
1573 | for (var x = -1; x <= 1; x++) {
1574 | for (var y = -1; y <= 1; y++) {
1575 | cctx.save();
1576 | cctx.translate(x * c.width, y * c.height);
1577 | tempSvg.render(cctx);
1578 | cctx.restore();
1579 | }
1580 | }
1581 | return ctx.createPattern(c, 'repeat');
1582 | }
1583 | };
1584 | svg.Element.pattern.prototype = new svg.Element.ElementBase;
1585 |
1586 | // marker element
1587 | svg.Element.marker = function (node) {
1588 | this.base = svg.Element.ElementBase;
1589 | this.base(node);
1590 |
1591 | this.baseRender = this.render;
1592 | this.render = function (ctx, point, angle) {
1593 | ctx.translate(point.x, point.y);
1594 | if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(angle);
1595 | if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth);
1596 | ctx.save();
1597 |
1598 | // render me using a temporary svg element
1599 | var tempSvg = new svg.Element.svg();
1600 | tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value);
1601 | tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value);
1602 | tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value);
1603 | tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value);
1604 | tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value);
1605 | tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black'));
1606 | tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none'));
1607 | tempSvg.children = this.children;
1608 | tempSvg.render(ctx);
1609 |
1610 | ctx.restore();
1611 | if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth);
1612 | if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(-angle);
1613 | ctx.translate(-point.x, -point.y);
1614 | }
1615 | };
1616 | svg.Element.marker.prototype = new svg.Element.ElementBase;
1617 |
1618 | // definitions element
1619 | svg.Element.defs = function (node) {
1620 | this.base = svg.Element.ElementBase;
1621 | this.base(node);
1622 |
1623 | this.render = function (ctx) {
1624 | // NOOP
1625 | }
1626 | };
1627 | svg.Element.defs.prototype = new svg.Element.ElementBase;
1628 |
1629 | // base for gradients
1630 | svg.Element.GradientBase = function (node) {
1631 | this.base = svg.Element.ElementBase;
1632 | this.base(node);
1633 |
1634 | this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox');
1635 |
1636 | this.stops = [];
1637 | for (var i = 0; i < this.children.length; i++) {
1638 | var child = this.children[i];
1639 | if (child.type == 'stop') this.stops.push(child);
1640 | }
1641 |
1642 | this.getGradient = function () {
1643 | // OVERRIDE ME!
1644 | };
1645 |
1646 | this.createGradient = function (ctx, element, parentOpacityProp) {
1647 | var stopsContainer = this;
1648 | if (this.getHrefAttribute().hasValue()) {
1649 | stopsContainer = this.getHrefAttribute().getDefinition();
1650 | }
1651 |
1652 | var addParentOpacity = function (color) {
1653 | if (parentOpacityProp.hasValue()) {
1654 | var p = new svg.Property('color', color);
1655 | return p.addOpacity(parentOpacityProp.value).value;
1656 | }
1657 | return color;
1658 | };
1659 |
1660 | var g = this.getGradient(ctx, element);
1661 | if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color);
1662 | for (var i = 0; i < stopsContainer.stops.length; i++) {
1663 | g.addColorStop(stopsContainer.stops[i].offset, addParentOpacity(stopsContainer.stops[i].color || 'black'));
1664 | }
1665 |
1666 | if (this.attribute('gradientTransform').hasValue()) {
1667 | // render as transformed pattern on temporary canvas
1668 | var rootView = svg.ViewPort.viewPorts[0];
1669 |
1670 | var rect = new svg.Element.rect();
1671 | rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0);
1672 | rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0);
1673 | rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS);
1674 | rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS);
1675 |
1676 | var group = new svg.Element.g();
1677 | group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value);
1678 | group.children = [ rect ];
1679 |
1680 | var tempSvg = new svg.Element.svg();
1681 | tempSvg.attributes['x'] = new svg.Property('x', 0);
1682 | tempSvg.attributes['y'] = new svg.Property('y', 0);
1683 | tempSvg.attributes['width'] = new svg.Property('width', rootView.width);
1684 | tempSvg.attributes['height'] = new svg.Property('height', rootView.height);
1685 | tempSvg.children = [ group ];
1686 |
1687 | var c = new Canvas();
1688 | c.width = rootView.width;
1689 | c.height = rootView.height;
1690 | var tempCtx = c.getContext('2d');
1691 | tempCtx.fillStyle = g;
1692 | tempSvg.render(tempCtx);
1693 | return ctx.createPattern(c, 'no-repeat');
1694 | }
1695 |
1696 | return g;
1697 | }
1698 | };
1699 | svg.Element.GradientBase.prototype = new svg.Element.ElementBase;
1700 |
1701 | // linear gradient element
1702 | svg.Element.linearGradient = function (node) {
1703 | this.base = svg.Element.GradientBase;
1704 | this.base(node);
1705 |
1706 | this.getGradient = function (ctx, element) {
1707 | var bb = this.gradientUnits == 'objectBoundingBox' ? element.getBoundingBox() : null;
1708 |
1709 | if (!this.attribute('x1').hasValue()
1710 | && !this.attribute('y1').hasValue()
1711 | && !this.attribute('x2').hasValue()
1712 | && !this.attribute('y2').hasValue()) {
1713 | this.attribute('x1', true).value = 0;
1714 | this.attribute('y1', true).value = 0;
1715 | this.attribute('x2', true).value = 1;
1716 | this.attribute('y2', true).value = 0;
1717 | }
1718 |
1719 | var x1 = (this.gradientUnits == 'objectBoundingBox'
1720 | ? bb.x() + bb.width() * this.attribute('x1').numValue()
1721 | : this.attribute('x1').toPixels('x'));
1722 | var y1 = (this.gradientUnits == 'objectBoundingBox'
1723 | ? bb.y() + bb.height() * this.attribute('y1').numValue()
1724 | : this.attribute('y1').toPixels('y'));
1725 | var x2 = (this.gradientUnits == 'objectBoundingBox'
1726 | ? bb.x() + bb.width() * this.attribute('x2').numValue()
1727 | : this.attribute('x2').toPixels('x'));
1728 | var y2 = (this.gradientUnits == 'objectBoundingBox'
1729 | ? bb.y() + bb.height() * this.attribute('y2').numValue()
1730 | : this.attribute('y2').toPixels('y'));
1731 |
1732 | if (x1 == x2 && y1 == y2) return null;
1733 | return ctx.createLinearGradient(x1, y1, x2, y2);
1734 | }
1735 | };
1736 | svg.Element.linearGradient.prototype = new svg.Element.GradientBase;
1737 |
1738 | // radial gradient element
1739 | svg.Element.radialGradient = function (node) {
1740 | this.base = svg.Element.GradientBase;
1741 | this.base(node);
1742 |
1743 | this.getGradient = function (ctx, element) {
1744 | var bb = element.getBoundingBox();
1745 |
1746 | if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%';
1747 | if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%';
1748 | if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%';
1749 |
1750 | var cx = (this.gradientUnits == 'objectBoundingBox'
1751 | ? bb.x() + bb.width() * this.attribute('cx').numValue()
1752 | : this.attribute('cx').toPixels('x'));
1753 | var cy = (this.gradientUnits == 'objectBoundingBox'
1754 | ? bb.y() + bb.height() * this.attribute('cy').numValue()
1755 | : this.attribute('cy').toPixels('y'));
1756 |
1757 | var fx = cx;
1758 | var fy = cy;
1759 | if (this.attribute('fx').hasValue()) {
1760 | fx = (this.gradientUnits == 'objectBoundingBox'
1761 | ? bb.x() + bb.width() * this.attribute('fx').numValue()
1762 | : this.attribute('fx').toPixels('x'));
1763 | }
1764 | if (this.attribute('fy').hasValue()) {
1765 | fy = (this.gradientUnits == 'objectBoundingBox'
1766 | ? bb.y() + bb.height() * this.attribute('fy').numValue()
1767 | : this.attribute('fy').toPixels('y'));
1768 | }
1769 |
1770 | var r = (this.gradientUnits == 'objectBoundingBox'
1771 | ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue()
1772 | : this.attribute('r').toPixels());
1773 |
1774 | return ctx.createRadialGradient(fx, fy, 0, cx, cy, r);
1775 | }
1776 | };
1777 | svg.Element.radialGradient.prototype = new svg.Element.GradientBase;
1778 |
1779 | // gradient stop element
1780 | svg.Element.stop = function (node) {
1781 | this.base = svg.Element.ElementBase;
1782 | this.base(node);
1783 |
1784 | this.offset = this.attribute('offset').numValue();
1785 | if (this.offset < 0) this.offset = 0;
1786 | if (this.offset > 1) this.offset = 1;
1787 |
1788 | var stopColor = this.style('stop-color');
1789 | if (this.style('stop-opacity').hasValue()) stopColor = stopColor.addOpacity(this.style('stop-opacity').value);
1790 | this.color = stopColor.value;
1791 | };
1792 | svg.Element.stop.prototype = new svg.Element.ElementBase;
1793 |
1794 | // animation base element
1795 | svg.Element.AnimateBase = function (node) {
1796 | this.base = svg.Element.ElementBase;
1797 | this.base(node);
1798 |
1799 | svg.Animations.push(this);
1800 |
1801 | this.duration = 0.0;
1802 | this.begin = this.attribute('begin').toMilliseconds();
1803 | this.maxDuration = this.begin + this.attribute('dur').toMilliseconds();
1804 |
1805 | this.getProperty = function () {
1806 | var attributeType = this.attribute('attributeType').value;
1807 | var attributeName = this.attribute('attributeName').value;
1808 |
1809 | if (attributeType == 'CSS') {
1810 | return this.parent.style(attributeName, true);
1811 | }
1812 | return this.parent.attribute(attributeName, true);
1813 | };
1814 |
1815 | this.initialValue = null;
1816 | this.initialUnits = '';
1817 | this.removed = false;
1818 |
1819 | this.calcValue = function () {
1820 | // OVERRIDE ME!
1821 | return '';
1822 | };
1823 |
1824 | this.update = function (delta) {
1825 | // set initial value
1826 | if (this.initialValue == null) {
1827 | this.initialValue = this.getProperty().value;
1828 | this.initialUnits = this.getProperty().getUnits();
1829 | }
1830 |
1831 | // if we're past the end time
1832 | if (this.duration > this.maxDuration) {
1833 | // loop for indefinitely repeating animations
1834 | if (this.attribute('repeatCount').value == 'indefinite'
1835 | || this.attribute('repeatDur').value == 'indefinite') {
1836 | this.duration = 0.0
1837 | }
1838 | else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) {
1839 | this.removed = true;
1840 | this.getProperty().value = this.initialValue;
1841 | return true;
1842 | }
1843 | else {
1844 | return false; // no updates made
1845 | }
1846 | }
1847 | this.duration = this.duration + delta;
1848 |
1849 | // if we're past the begin time
1850 | var updated = false;
1851 | if (this.begin < this.duration) {
1852 | var newValue = this.calcValue(); // tween
1853 |
1854 | if (this.attribute('type').hasValue()) {
1855 | // for transform, etc.
1856 | var type = this.attribute('type').value;
1857 | newValue = type + '(' + newValue + ')';
1858 | }
1859 |
1860 | this.getProperty().value = newValue;
1861 | updated = true;
1862 | }
1863 |
1864 | return updated;
1865 | };
1866 |
1867 | this.from = this.attribute('from');
1868 | this.to = this.attribute('to');
1869 | this.values = this.attribute('values');
1870 | if (this.values.hasValue()) this.values.value = this.values.value.split(';');
1871 |
1872 | // fraction of duration we've covered
1873 | this.progress = function () {
1874 | var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) };
1875 | if (this.values.hasValue()) {
1876 | var p = ret.progress * (this.values.value.length - 1);
1877 | var lb = Math.floor(p), ub = Math.ceil(p);
1878 | ret.from = new svg.Property('from', parseFloat(this.values.value[lb]));
1879 | ret.to = new svg.Property('to', parseFloat(this.values.value[ub]));
1880 | ret.progress = (p - lb) / (ub - lb);
1881 | }
1882 | else {
1883 | ret.from = this.from;
1884 | ret.to = this.to;
1885 | }
1886 | return ret;
1887 | }
1888 | };
1889 | svg.Element.AnimateBase.prototype = new svg.Element.ElementBase;
1890 |
1891 | // animate element
1892 | svg.Element.animate = function (node) {
1893 | this.base = svg.Element.AnimateBase;
1894 | this.base(node);
1895 |
1896 | this.calcValue = function () {
1897 | var p = this.progress();
1898 |
1899 | // tween value linearly
1900 | var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress;
1901 | return newValue + this.initialUnits;
1902 | };
1903 | };
1904 | svg.Element.animate.prototype = new svg.Element.AnimateBase;
1905 |
1906 | // animate color element
1907 | svg.Element.animateColor = function (node) {
1908 | this.base = svg.Element.AnimateBase;
1909 | this.base(node);
1910 |
1911 | this.calcValue = function () {
1912 | var p = this.progress();
1913 | var from = new RGBColor(p.from.value);
1914 | var to = new RGBColor(p.to.value);
1915 |
1916 | if (from.ok && to.ok) {
1917 | // tween color linearly
1918 | var r = from.r + (to.r - from.r) * p.progress;
1919 | var g = from.g + (to.g - from.g) * p.progress;
1920 | var b = from.b + (to.b - from.b) * p.progress;
1921 | return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')';
1922 | }
1923 | return this.attribute('from').value;
1924 | };
1925 | };
1926 | svg.Element.animateColor.prototype = new svg.Element.AnimateBase;
1927 |
1928 | // animate transform element
1929 | svg.Element.animateTransform = function (node) {
1930 | this.base = svg.Element.AnimateBase;
1931 | this.base(node);
1932 |
1933 | this.calcValue = function () {
1934 | var p = this.progress();
1935 |
1936 | // tween value linearly
1937 | var from = svg.ToNumberArray(p.from.value);
1938 | var to = svg.ToNumberArray(p.to.value);
1939 | var newValue = '';
1940 | for (var i = 0; i < from.length; i++) {
1941 | newValue += from[i] + (to[i] - from[i]) * p.progress + ' ';
1942 | }
1943 | return newValue;
1944 | };
1945 | };
1946 | svg.Element.animateTransform.prototype = new svg.Element.animate;
1947 |
1948 | // font element
1949 | svg.Element.font = function (node) {
1950 | this.base = svg.Element.ElementBase;
1951 | this.base(node);
1952 |
1953 | this.horizAdvX = this.attribute('horiz-adv-x').numValue();
1954 |
1955 | this.isRTL = false;
1956 | this.isArabic = false;
1957 | this.fontFace = null;
1958 | this.missingGlyph = null;
1959 | this.glyphs = [];
1960 | for (var i = 0; i < this.children.length; i++) {
1961 | var child = this.children[i];
1962 | if (child.type == 'font-face') {
1963 | this.fontFace = child;
1964 | if (child.style('font-family').hasValue()) {
1965 | svg.Definitions[child.style('font-family').value] = this;
1966 | }
1967 | }
1968 | else if (child.type == 'missing-glyph') this.missingGlyph = child;
1969 | else if (child.type == 'glyph') {
1970 | if (child.arabicForm != '') {
1971 | this.isRTL = true;
1972 | this.isArabic = true;
1973 | if (typeof(this.glyphs[child.unicode]) == 'undefined') this.glyphs[child.unicode] = [];
1974 | this.glyphs[child.unicode][child.arabicForm] = child;
1975 | }
1976 | else {
1977 | this.glyphs[child.unicode] = child;
1978 | }
1979 | }
1980 | }
1981 | };
1982 | svg.Element.font.prototype = new svg.Element.ElementBase;
1983 |
1984 | // font-face element
1985 | svg.Element.fontface = function (node) {
1986 | this.base = svg.Element.ElementBase;
1987 | this.base(node);
1988 |
1989 | this.ascent = this.attribute('ascent').value;
1990 | this.descent = this.attribute('descent').value;
1991 | this.unitsPerEm = this.attribute('units-per-em').numValue();
1992 | };
1993 | svg.Element.fontface.prototype = new svg.Element.ElementBase;
1994 |
1995 | // missing-glyph element
1996 | svg.Element.missingglyph = function (node) {
1997 | this.base = svg.Element.path;
1998 | this.base(node);
1999 |
2000 | this.horizAdvX = 0;
2001 | };
2002 | svg.Element.missingglyph.prototype = new svg.Element.path;
2003 |
2004 | // glyph element
2005 | svg.Element.glyph = function (node) {
2006 | this.base = svg.Element.path;
2007 | this.base(node);
2008 |
2009 | this.horizAdvX = this.attribute('horiz-adv-x').numValue();
2010 | this.unicode = this.attribute('unicode').value;
2011 | this.arabicForm = this.attribute('arabic-form').value;
2012 | };
2013 | svg.Element.glyph.prototype = new svg.Element.path;
2014 |
2015 | // text element
2016 | svg.Element.text = function (node) {
2017 | this.captureTextNodes = true;
2018 | this.base = svg.Element.RenderedElementBase;
2019 | this.base(node);
2020 |
2021 | this.baseSetContext = this.setContext;
2022 | this.setContext = function (ctx) {
2023 | this.baseSetContext(ctx);
2024 | if (this.style('dominant-baseline').hasValue()) ctx.textBaseline = this.style('dominant-baseline').value;
2025 | if (this.style('alignment-baseline').hasValue()) ctx.textBaseline = this.style('alignment-baseline').value;
2026 | };
2027 |
2028 | this.getBoundingBox = function () {
2029 | var x = this.attribute('x').toPixels('x');
2030 | var y = this.attribute('y').toPixels('y');
2031 | var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);
2032 | return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y);
2033 | };
2034 |
2035 | this.renderChildren = function (ctx) {
2036 | this.x = this.attribute('x').toPixels('x');
2037 | this.y = this.attribute('y').toPixels('y');
2038 | this.x += this.getAnchorDelta(ctx, this, 0);
2039 | for (var i = 0; i < this.children.length; i++) {
2040 | this.renderChild(ctx, this, i);
2041 | }
2042 | };
2043 |
2044 | this.getAnchorDelta = function (ctx, parent, startI) {
2045 | var textAnchor = this.style('text-anchor').valueOrDefault('start');
2046 | if (textAnchor != 'start') {
2047 | var width = 0;
2048 | for (var i = startI; i < parent.children.length; i++) {
2049 | var child = parent.children[i];
2050 | if (i > startI && child.attribute('x').hasValue()) break; // new group
2051 | width += child.measureTextRecursive(ctx);
2052 | }
2053 | return -1 * (textAnchor == 'end' ? width : width / 2.0);
2054 | }
2055 | return 0;
2056 | };
2057 |
2058 | this.renderChild = function (ctx, parent, i) {
2059 | var child = parent.children[i];
2060 | if (child.attribute('x').hasValue()) {
2061 | child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i);
2062 | }
2063 | else {
2064 | if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x');
2065 | if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x');
2066 | child.x = this.x;
2067 | }
2068 | this.x = child.x + child.measureText(ctx);
2069 |
2070 | if (child.attribute('y').hasValue()) {
2071 | child.y = child.attribute('y').toPixels('y');
2072 | }
2073 | else {
2074 | if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y');
2075 | if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y');
2076 | child.y = this.y;
2077 | }
2078 | this.y = child.y;
2079 |
2080 | child.render(ctx);
2081 |
2082 | for (var i = 0; i < child.children.length; i++) {
2083 | this.renderChild(ctx, child, i);
2084 | }
2085 | }
2086 | };
2087 | svg.Element.text.prototype = new svg.Element.RenderedElementBase;
2088 |
2089 | // text base
2090 | svg.Element.TextElementBase = function (node) {
2091 | this.base = svg.Element.RenderedElementBase;
2092 | this.base(node);
2093 |
2094 | this.getGlyph = function (font, text, i) {
2095 | var c = text[i];
2096 | var glyph = null;
2097 | if (font.isArabic) {
2098 | var arabicForm = 'isolated';
2099 | if ((i == 0 || text[i - 1] == ' ') && i < text.length - 2 && text[i + 1] != ' ') arabicForm = 'terminal';
2100 | if (i > 0 && text[i - 1] != ' ' && i < text.length - 2 && text[i + 1] != ' ') arabicForm = 'medial';
2101 | if (i > 0 && text[i - 1] != ' ' && (i == text.length - 1 || text[i + 1] == ' ')) arabicForm = 'initial';
2102 | if (typeof(font.glyphs[c]) != 'undefined') {
2103 | glyph = font.glyphs[c][arabicForm];
2104 | if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c];
2105 | }
2106 | }
2107 | else {
2108 | glyph = font.glyphs[c];
2109 | }
2110 | if (glyph == null) glyph = font.missingGlyph;
2111 | return glyph;
2112 | };
2113 |
2114 | this.renderChildren = function (ctx) {
2115 | var customFont = this.parent.style('font-family').getDefinition();
2116 | if (customFont != null) {
2117 | var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);
2118 | var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle);
2119 | var text = this.getText();
2120 | if (customFont.isRTL) text = text.split("").reverse().join("");
2121 |
2122 | var dx = svg.ToNumberArray(this.parent.attribute('dx').value);
2123 | for (var i = 0; i < text.length; i++) {
2124 | var glyph = this.getGlyph(customFont, text, i);
2125 | var scale = fontSize / customFont.fontFace.unitsPerEm;
2126 | ctx.translate(this.x, this.y);
2127 | ctx.scale(scale, -scale);
2128 | var lw = ctx.lineWidth;
2129 | ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize;
2130 | if (fontStyle == 'italic') ctx.transform(1, 0, .4, 1, 0, 0);
2131 | glyph.render(ctx);
2132 | if (fontStyle == 'italic') ctx.transform(1, 0, -.4, 1, 0, 0);
2133 | ctx.lineWidth = lw;
2134 | ctx.scale(1 / scale, -1 / scale);
2135 | ctx.translate(-this.x, -this.y);
2136 |
2137 | this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm;
2138 | if (typeof(dx[i]) != 'undefined' && !isNaN(dx[i])) {
2139 | this.x += dx[i];
2140 | }
2141 | }
2142 | return;
2143 | }
2144 |
2145 | if (ctx.fillStyle != '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y);
2146 | if (ctx.strokeStyle != '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y);
2147 | };
2148 |
2149 | this.getText = function () {
2150 | // OVERRIDE ME
2151 | };
2152 |
2153 | this.measureTextRecursive = function (ctx) {
2154 | var width = this.measureText(ctx);
2155 | for (var i = 0; i < this.children.length; i++) {
2156 | width += this.children[i].measureTextRecursive(ctx);
2157 | }
2158 | return width;
2159 | };
2160 |
2161 | this.measureText = function (ctx) {
2162 | var customFont = this.parent.style('font-family').getDefinition();
2163 | if (customFont != null) {
2164 | var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize);
2165 | var measure = 0;
2166 | var text = this.getText();
2167 | if (customFont.isRTL) text = text.split("").reverse().join("");
2168 | var dx = svg.ToNumberArray(this.parent.attribute('dx').value);
2169 | for (var i = 0; i < text.length; i++) {
2170 | var glyph = this.getGlyph(customFont, text, i);
2171 | measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm;
2172 | if (typeof(dx[i]) != 'undefined' && !isNaN(dx[i])) {
2173 | measure += dx[i];
2174 | }
2175 | }
2176 | return measure;
2177 | }
2178 |
2179 | var textToMeasure = svg.compressSpaces(this.getText());
2180 | if (!ctx.measureText) return textToMeasure.length * 10;
2181 |
2182 | ctx.save();
2183 | this.setContext(ctx);
2184 | var width = ctx.measureText(textToMeasure).width;
2185 | ctx.restore();
2186 | return width;
2187 | }
2188 | };
2189 | svg.Element.TextElementBase.prototype = new svg.Element.RenderedElementBase;
2190 |
2191 | // tspan
2192 | svg.Element.tspan = function (node) {
2193 | this.captureTextNodes = true;
2194 | this.base = svg.Element.TextElementBase;
2195 | this.base(node);
2196 |
2197 | this.text = node.nodeValue || node.text || '';
2198 | this.getText = function () {
2199 | return this.text;
2200 | }
2201 | };
2202 | svg.Element.tspan.prototype = new svg.Element.TextElementBase;
2203 |
2204 | // tref
2205 | svg.Element.tref = function (node) {
2206 | this.base = svg.Element.TextElementBase;
2207 | this.base(node);
2208 |
2209 | this.getText = function () {
2210 | var element = this.getHrefAttribute().getDefinition();
2211 | if (element == null) {
2212 | return null;
2213 | }
2214 | return element.children[0].getText();
2215 | }
2216 | };
2217 | svg.Element.tref.prototype = new svg.Element.TextElementBase;
2218 |
2219 | // a element
2220 | svg.Element.a = function (node) {
2221 | this.base = svg.Element.TextElementBase;
2222 | this.base(node);
2223 |
2224 | this.hasText = true;
2225 | for (var i = 0; i < node.childNodes.length; i++) {
2226 | if (node.childNodes[i].nodeType != 3) this.hasText = false;
2227 | }
2228 |
2229 | // this might contain text
2230 | this.text = this.hasText ? node.childNodes[0].nodeValue : '';
2231 | this.getText = function () {
2232 | return this.text;
2233 | };
2234 |
2235 | this.baseRenderChildren = this.renderChildren;
2236 | this.renderChildren = function (ctx) {
2237 | if (this.hasText) {
2238 | // render as text element
2239 | this.baseRenderChildren(ctx);
2240 | var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize);
2241 | svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y));
2242 | }
2243 | else {
2244 | // render as temporary group
2245 | var g = new svg.Element.g();
2246 | g.children = this.children;
2247 | g.parent = this;
2248 | g.render(ctx);
2249 | }
2250 | };
2251 |
2252 | this.onclick = function () {
2253 | window.open(this.getHrefAttribute().value);
2254 | };
2255 |
2256 | this.onmousemove = function () {
2257 | svg.ctx.canvas.style.cursor = 'pointer';
2258 | }
2259 | };
2260 | svg.Element.a.prototype = new svg.Element.TextElementBase;
2261 |
2262 | // image element
2263 | svg.Element.image = function (node) {
2264 | var self = this;
2265 | this.base = svg.Element.RenderedElementBase;
2266 | this.base(node);
2267 |
2268 | var href = this.getHrefAttribute().value;
2269 | var isSvg = href.match(/\.svg$/);
2270 |
2271 | svg.Images.push(this);
2272 | this.loaded = false;
2273 | if (href.substr(0, 5) === 'data:') {
2274 | var base64 = href.replace(/^data:image\/(jpg|png|gif|jpeg);base64,/, "");
2275 | if (base64.substr(0, 5) === 'data:') {
2276 | throw new Error("Image with href 'data:' prefix doesn't match a known content type.");
2277 | }
2278 | var buffer = new Buffer(base64, 'base64');
2279 |
2280 | this.img = new Canvas.Image();
2281 | self.img.onload = function () {
2282 | self.loaded = true;
2283 | };
2284 | self.img.onerror = function () {
2285 | throw new Error('Failed to load image');
2286 | };
2287 | this.img.src = buffer;
2288 | } else {
2289 |
2290 | if (isSvg) {
2291 | throw new Error('Unhandled Image Type');
2292 | }
2293 | svg.remote(href, function (data) {
2294 | self.img = new Canvas.Image();
2295 | self.img.onload = function () {
2296 | self.loaded = true;
2297 | };
2298 | self.img.onerror = function () {
2299 | throw new Error('Failed to load image');
2300 | };
2301 | self.img.src = data;
2302 | });
2303 | }
2304 |
2305 | this.renderChildren = function (ctx) {
2306 | var x = this.attribute('x').toPixels('x');
2307 | var y = this.attribute('y').toPixels('y');
2308 |
2309 | var width = this.attribute('width').toPixels('x');
2310 | var height = this.attribute('height').toPixels('y');
2311 | if (width == 0 || height == 0) return;
2312 |
2313 | ctx.save();
2314 | if (isSvg) {
2315 | throw new Error('Unhandled Image Type');
2316 | }
2317 | else {
2318 | ctx.translate(x, y);
2319 | svg.AspectRatio(ctx,
2320 | this.attribute('preserveAspectRatio').value,
2321 | width,
2322 | this.img.width,
2323 | height,
2324 | this.img.height,
2325 | 0,
2326 | 0);
2327 | ctx.drawImage(this.img, 0, 0);
2328 | }
2329 | ctx.restore();
2330 | };
2331 |
2332 | this.getBoundingBox = function () {
2333 | var x = this.attribute('x').toPixels('x');
2334 | var y = this.attribute('y').toPixels('y');
2335 | var width = this.attribute('width').toPixels('x');
2336 | var height = this.attribute('height').toPixels('y');
2337 | return new svg.BoundingBox(x, y, x + width, y + height);
2338 | }
2339 | };
2340 | svg.Element.image.prototype = new svg.Element.RenderedElementBase;
2341 |
2342 | // group element
2343 | svg.Element.g = function (node) {
2344 | this.base = svg.Element.RenderedElementBase;
2345 | this.base(node);
2346 |
2347 | this.getBoundingBox = function () {
2348 | var bb = new svg.BoundingBox();
2349 | for (var i = 0; i < this.children.length; i++) {
2350 | bb.addBoundingBox(this.children[i].getBoundingBox());
2351 | }
2352 | return bb;
2353 | };
2354 | };
2355 | svg.Element.g.prototype = new svg.Element.RenderedElementBase;
2356 |
2357 | // symbol element
2358 | svg.Element.symbol = function (node) {
2359 | this.base = svg.Element.RenderedElementBase;
2360 | this.base(node);
2361 |
2362 | this.baseSetContext = this.setContext;
2363 | this.setContext = function (ctx) {
2364 | this.baseSetContext(ctx);
2365 |
2366 | // viewbox
2367 | if (this.attribute('viewBox').hasValue()) {
2368 | var viewBox = svg.ToNumberArray(this.attribute('viewBox').value);
2369 | var minX = viewBox[0];
2370 | var minY = viewBox[1];
2371 | var width = viewBox[2];
2372 | var height = viewBox[3];
2373 |
2374 | svg.AspectRatio(ctx,
2375 | this.attribute('preserveAspectRatio').value,
2376 | this.attribute('width').toPixels('x'),
2377 | width,
2378 | this.attribute('height').toPixels('y'),
2379 | height,
2380 | minX,
2381 | minY);
2382 |
2383 | svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]);
2384 | }
2385 | }
2386 | };
2387 | svg.Element.symbol.prototype = new svg.Element.RenderedElementBase;
2388 |
2389 | // style element
2390 | svg.Element.style = function (node) {
2391 | this.base = svg.Element.ElementBase;
2392 | this.base(node);
2393 |
2394 | // text, or spaces then CDATA
2395 | var css = '';
2396 | for (var i = 0; i < node.childNodes.length; i++) {
2397 | css += node.childNodes[i].nodeValue;
2398 | }
2399 | css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments
2400 | css = svg.compressSpaces(css); // replace whitespace
2401 | var cssDefs = css.split('}');
2402 | for (var i = 0; i < cssDefs.length; i++) {
2403 | if (svg.trim(cssDefs[i]) != '') {
2404 | var cssDef = cssDefs[i].split('{');
2405 | var cssClasses = cssDef[0].split(',');
2406 | var cssProps = cssDef[1].split(';');
2407 | for (var j = 0; j < cssClasses.length; j++) {
2408 | var cssClass = svg.trim(cssClasses[j]);
2409 | if (cssClass != '') {
2410 | var props = {};
2411 | for (var k = 0; k < cssProps.length; k++) {
2412 | var prop = cssProps[k].indexOf(':');
2413 | var name = cssProps[k].substr(0, prop);
2414 | var value = cssProps[k].substr(prop + 1, cssProps[k].length - prop);
2415 | if (name != null && value != null) {
2416 | props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value));
2417 | }
2418 | }
2419 | svg.Styles[cssClass] = props;
2420 | if (cssClass == '@font-face') {
2421 | var fontFamily = props['font-family'].value.replace(/"/g, '');
2422 | var srcs = props['src'].value.split(',');
2423 | for (var s = 0; s < srcs.length; s++) {
2424 | if (srcs[s].indexOf('format("svg")') > 0) {
2425 | var urlStart = srcs[s].indexOf('url');
2426 | var urlEnd = srcs[s].indexOf(')', urlStart);
2427 | var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6);
2428 | var doc = svg.parseXml(svg.ajax(url));
2429 | var fonts = doc.getElementsByTagName('font');
2430 | for (var f = 0; f < fonts.length; f++) {
2431 | svg.Definitions[fontFamily] = svg.CreateElement(fonts[f]);
2432 | }
2433 | }
2434 | }
2435 | }
2436 | }
2437 | }
2438 | }
2439 | }
2440 | };
2441 | svg.Element.style.prototype = new svg.Element.ElementBase;
2442 |
2443 | // use element
2444 | svg.Element.use = function (node) {
2445 | this.base = svg.Element.RenderedElementBase;
2446 | this.base(node);
2447 |
2448 | this.baseSetContext = this.setContext;
2449 | this.setContext = function (ctx) {
2450 | this.baseSetContext(ctx);
2451 | if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0);
2452 | if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y'));
2453 | };
2454 |
2455 | this.getDefinition = function () {
2456 | var element = this.getHrefAttribute().getDefinition();
2457 | if (this.attribute('width').hasValue()) element.attribute('width', true).value = this.attribute('width').value;
2458 | if (this.attribute('height').hasValue()) element.attribute('height', true).value = this.attribute('height').value;
2459 | return element;
2460 | };
2461 |
2462 | this.path = function (ctx) {
2463 | var element = this.getDefinition();
2464 | if (element != null) element.path(ctx);
2465 | };
2466 |
2467 | this.getBoundingBox = function () {
2468 | var element = this.getDefinition();
2469 | if (element == null) {
2470 | return null;
2471 | }
2472 | return element.getBoundingBox();
2473 | };
2474 |
2475 | this.renderChildren = function (ctx) {
2476 | var element = this.getDefinition();
2477 | if (element != null) {
2478 | // temporarily detach from parent and render
2479 | var oldParent = element.parent;
2480 | element.parent = null;
2481 | element.render(ctx);
2482 | element.parent = oldParent;
2483 | }
2484 | }
2485 | };
2486 | svg.Element.use.prototype = new svg.Element.RenderedElementBase;
2487 |
2488 | // mask element
2489 | svg.Element.mask = function (node) {
2490 | this.base = svg.Element.ElementBase;
2491 | this.base(node);
2492 |
2493 | this.apply = function (ctx, element) {
2494 | // render as temp svg
2495 | var x = this.attribute('x').toPixels('x');
2496 | var y = this.attribute('y').toPixels('y');
2497 | var width = this.attribute('width').toPixels('x');
2498 | var height = this.attribute('height').toPixels('y');
2499 |
2500 | if (width == 0 && height == 0) {
2501 | var bb = new svg.BoundingBox();
2502 | for (var i = 0; i < this.children.length; i++) {
2503 | bb.addBoundingBox(this.children[i].getBoundingBox());
2504 | }
2505 | var x = Math.floor(bb.x1);
2506 | var y = Math.floor(bb.y1);
2507 | var width = Math.floor(bb.width());
2508 | var height = Math.floor(bb.height());
2509 | }
2510 |
2511 | // temporarily remove mask to avoid recursion
2512 | var mask = element.attribute('mask').value;
2513 | element.attribute('mask').value = '';
2514 |
2515 | var cMask = new Canvas();
2516 | cMask.width = x + width;
2517 | cMask.height = y + height;
2518 | var maskCtx = cMask.getContext('2d');
2519 | this.renderChildren(maskCtx);
2520 |
2521 | var c = new Canvas();
2522 | c.width = x + width;
2523 | c.height = y + height;
2524 | var tempCtx = c.getContext('2d');
2525 | element.render(tempCtx);
2526 | tempCtx.globalCompositeOperation = 'destination-in';
2527 | tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat');
2528 | tempCtx.fillRect(0, 0, x + width, y + height);
2529 |
2530 | ctx.fillStyle = ctx.createPattern(c, 'no-repeat');
2531 | ctx.fillRect(0, 0, x + width, y + height);
2532 |
2533 | // reassign mask
2534 | element.attribute('mask').value = mask;
2535 | };
2536 |
2537 | this.render = function (ctx) {
2538 | // NO RENDER
2539 | }
2540 | };
2541 | svg.Element.mask.prototype = new svg.Element.ElementBase;
2542 |
2543 | // clip element
2544 | svg.Element.clipPath = function (node) {
2545 | this.base = svg.Element.ElementBase;
2546 | this.base(node);
2547 |
2548 | this.apply = function (ctx) {
2549 | for (var i = 0; i < this.children.length; i++) {
2550 | var child = this.children[i];
2551 | if (typeof(child.path) != 'undefined') {
2552 | var transform = null;
2553 | if (child.attribute('transform').hasValue()) {
2554 | transform = new svg.Transform(child.attribute('transform').value);
2555 | transform.apply(ctx);
2556 | }
2557 | child.path(ctx);
2558 | ctx.clip();
2559 | if (transform) {
2560 | transform.unapply(ctx);
2561 | }
2562 | }
2563 | }
2564 | };
2565 |
2566 | this.render = function (ctx) {
2567 | // NO RENDER
2568 | }
2569 | };
2570 | svg.Element.clipPath.prototype = new svg.Element.ElementBase;
2571 |
2572 | // filters
2573 | svg.Element.filter = function (node) {
2574 | this.base = svg.Element.ElementBase;
2575 | this.base(node);
2576 |
2577 | this.apply = function (ctx, element) {
2578 | // render as temp svg
2579 | var bb = element.getBoundingBox();
2580 | var x = Math.floor(bb.x1);
2581 | var y = Math.floor(bb.y1);
2582 | var width = Math.floor(bb.width());
2583 | var height = Math.floor(bb.height());
2584 |
2585 | // temporarily remove filter to avoid recursion
2586 | var filter = element.style('filter').value;
2587 | element.style('filter').value = '';
2588 |
2589 | var px = 0, py = 0;
2590 | for (var i = 0; i < this.children.length; i++) {
2591 | var efd = this.children[i].extraFilterDistance || 0;
2592 | px = Math.max(px, efd);
2593 | py = Math.max(py, efd);
2594 | }
2595 |
2596 | var c = document.createElement('canvas');
2597 | c.width = width + 2 * px;
2598 | c.height = height + 2 * py;
2599 | var tempCtx = c.getContext('2d');
2600 | tempCtx.translate(-x + px, -y + py);
2601 | element.render(tempCtx);
2602 |
2603 | // apply filters
2604 | for (var i = 0; i < this.children.length; i++) {
2605 | this.children[i].apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py);
2606 | }
2607 |
2608 | // render on me
2609 | ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py);
2610 |
2611 | // reassign filter
2612 | element.style('filter', true).value = filter;
2613 | };
2614 |
2615 | this.render = function (ctx) {
2616 | // NO RENDER
2617 | }
2618 | };
2619 | svg.Element.filter.prototype = new svg.Element.ElementBase;
2620 |
2621 | svg.Element.feMorphology = function (node) {
2622 | this.base = svg.Element.ElementBase;
2623 | this.base(node);
2624 |
2625 | this.apply = function (ctx, x, y, width, height) {
2626 | // TODO: implement
2627 | }
2628 | };
2629 | svg.Element.feMorphology.prototype = new svg.Element.ElementBase;
2630 |
2631 | svg.Element.feComposite = function (node) {
2632 | this.base = svg.Element.ElementBase;
2633 | this.base(node);
2634 |
2635 | this.apply = function (ctx, x, y, width, height) {
2636 | // TODO: implement
2637 | }
2638 | };
2639 | svg.Element.feComposite.prototype = new svg.Element.ElementBase;
2640 |
2641 | svg.Element.feColorMatrix = function (node) {
2642 | this.base = svg.Element.ElementBase;
2643 | this.base(node);
2644 |
2645 | var matrix = svg.ToNumberArray(this.attribute('values').value);
2646 | switch (this.attribute('type').valueOrDefault('matrix')) { // http://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
2647 | case 'saturate':
2648 | var s = matrix[0];
2649 | matrix = [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0,
2650 | 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0,
2651 | 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0,
2652 | 0, 0, 0, 1, 0,
2653 | 0, 0, 0, 0, 1];
2654 | break;
2655 | case 'hueRotate':
2656 | var a = matrix[0] * Math.PI / 180.0;
2657 | var c = function (m1, m2, m3) {
2658 | return m1 + Math.cos(a) * m2 + Math.sin(a) * m3;
2659 | };
2660 | matrix = [c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0,
2661 | c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0,
2662 | c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0,
2663 | 0, 0, 0, 1, 0,
2664 | 0, 0, 0, 0, 1];
2665 | break;
2666 | case 'luminanceToAlpha':
2667 | matrix = [0, 0, 0, 0, 0,
2668 | 0, 0, 0, 0, 0,
2669 | 0, 0, 0, 0, 0,
2670 | 0.2125, 0.7154, 0.0721, 0, 0,
2671 | 0, 0, 0, 0, 1];
2672 | break;
2673 | }
2674 |
2675 | function imGet(img, x, y, width, height, rgba) {
2676 | return img[y * width * 4 + x * 4 + rgba];
2677 | }
2678 |
2679 | function imSet(img, x, y, width, height, rgba, val) {
2680 | img[y * width * 4 + x * 4 + rgba] = val;
2681 | }
2682 |
2683 | function m(i, v) {
2684 | var mi = matrix[i];
2685 | return mi * (mi < 0 ? v - 255 : v);
2686 | }
2687 |
2688 | this.apply = function (ctx, x, y, width, height) {
2689 | // assuming x==0 && y==0 for now
2690 | var srcData = ctx.getImageData(0, 0, width, height);
2691 | for (var y = 0; y < height; y++) {
2692 | for (var x = 0; x < width; x++) {
2693 | var r = imGet(srcData.data, x, y, width, height, 0);
2694 | var g = imGet(srcData.data, x, y, width, height, 1);
2695 | var b = imGet(srcData.data, x, y, width, height, 2);
2696 | var a = imGet(srcData.data, x, y, width, height, 3);
2697 | imSet(srcData.data, x, y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1));
2698 | imSet(srcData.data, x, y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1));
2699 | imSet(srcData.data, x, y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1));
2700 | imSet(srcData.data, x, y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1));
2701 | }
2702 | }
2703 | ctx.clearRect(0, 0, width, height);
2704 | ctx.putImageData(srcData, 0, 0);
2705 | }
2706 | };
2707 | svg.Element.feColorMatrix.prototype = new svg.Element.ElementBase;
2708 |
2709 | svg.Element.feGaussianBlur = function (node) {
2710 | this.base = svg.Element.ElementBase;
2711 | this.base(node);
2712 |
2713 | this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue());
2714 | this.extraFilterDistance = this.blurRadius;
2715 |
2716 | this.apply = function (ctx, x, y, width, height) {
2717 | // StackBlur requires canvas be on document
2718 | ctx.canvas.id = svg.UniqueId();
2719 | ctx.canvas.style.display = 'none';
2720 | document.body.appendChild(ctx.canvas);
2721 | stackBlur(ctx.canvas.id, x, y, width, height, this.blurRadius);
2722 | document.body.removeChild(ctx.canvas);
2723 | }
2724 | };
2725 | svg.Element.feGaussianBlur.prototype = new svg.Element.ElementBase;
2726 |
2727 | // title element, do nothing
2728 | svg.Element.title = function (node) {
2729 | };
2730 | svg.Element.title.prototype = new svg.Element.ElementBase;
2731 |
2732 | // desc element, do nothing
2733 | svg.Element.desc = function (node) {
2734 | };
2735 | svg.Element.desc.prototype = new svg.Element.ElementBase;
2736 |
2737 | svg.Element.MISSING = function (node) {
2738 | if (typeof(console) != 'undefined') {
2739 | console.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.');
2740 | }
2741 | };
2742 | svg.Element.MISSING.prototype = new svg.Element.ElementBase;
2743 |
2744 | // element factory
2745 | svg.CreateElement = function (node) {
2746 | var className = node.nodeName.replace(/^[^:]+:/, ''); // remove namespace
2747 | className = className.replace(/\-/g, ''); // remove dashes
2748 | var e = null;
2749 | if (typeof(svg.Element[className]) != 'undefined') {
2750 | e = new svg.Element[className](node);
2751 | }
2752 | else {
2753 | e = new svg.Element.MISSING(node);
2754 | }
2755 |
2756 | e.type = node.nodeName;
2757 | return e;
2758 | };
2759 |
2760 | // load from url
2761 | svg.load = function (ctx, url) {
2762 | svg.loadXml(ctx, svg.ajax(url));
2763 | };
2764 |
2765 | // load from xml
2766 | svg.loadXml = function (ctx, xml) {
2767 | svg.loadXmlDoc(ctx, svg.parseXml(xml));
2768 | };
2769 |
2770 | svg.loadXmlDoc = function (ctx, dom) {
2771 | svg.init(ctx);
2772 |
2773 | var mapXY = function (p) {
2774 | var e = ctx.canvas;
2775 | while (e) {
2776 | p.x -= e.offsetLeft;
2777 | p.y -= e.offsetTop;
2778 | e = e.offsetParent;
2779 | }
2780 | if (window.scrollX) p.x += window.scrollX;
2781 | if (window.scrollY) p.y += window.scrollY;
2782 | return p;
2783 | };
2784 |
2785 | var e = svg.CreateElement(dom.documentElement);
2786 | e.root = true;
2787 |
2788 | // render loop
2789 | var isFirstRender = true;
2790 | var draw = function () {
2791 | svg.ViewPort.Clear();
2792 | if (ctx.canvas.parentNode) svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight);
2793 |
2794 | if (svg.opts['ignoreDimensions'] != true) {
2795 | // set canvas size
2796 | if (e.style('width').hasValue()) {
2797 | ctx.canvas.width = e.style('width').toPixels('x');
2798 | ctx.canvas.style.width = ctx.canvas.width + 'px';
2799 | }
2800 | if (e.style('height').hasValue()) {
2801 | ctx.canvas.height = e.style('height').toPixels('y');
2802 | ctx.canvas.style.height = ctx.canvas.height + 'px';
2803 | }
2804 | }
2805 | var cWidth = ctx.canvas.clientWidth || ctx.canvas.width;
2806 | var cHeight = ctx.canvas.clientHeight || ctx.canvas.height;
2807 | var viewBox;
2808 | if (svg.opts['ignoreDimensions'] == true && e.style('width').hasValue() && e.style('height').hasValue()) {
2809 | if (e.attribute('viewBox').hasValue()){
2810 | viewBox = svg.ToNumberArray(e.attribute('viewBox').value);
2811 | svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]);
2812 | }
2813 | cWidth = e.style('width').toPixels('x');
2814 | cHeight = e.style('height').toPixels('y');
2815 | }
2816 | svg.ViewPort.SetCurrent(cWidth, cHeight);
2817 | ctx.canvas.width = cWidth;
2818 | ctx.canvas.height = cHeight;
2819 |
2820 | if (svg.opts['offsetX'] != null) e.attribute('x', true).value = svg.opts['offsetX'];
2821 | if (svg.opts['offsetY'] != null) e.attribute('y', true).value = svg.opts['offsetY'];
2822 | if (svg.opts['scaleWidth'] != null && svg.opts['scaleHeight'] != null) {
2823 | var xRatio = 1, yRatio = 1;
2824 | viewBox = svg.ToNumberArray(e.attribute('viewBox').value);
2825 | if (e.attribute('width').hasValue()) xRatio = e.attribute('width').toPixels('x') / svg.opts['scaleWidth'];
2826 | else if (!isNaN(viewBox[2])) xRatio = viewBox[2] / svg.opts['scaleWidth'];
2827 | if (e.attribute('height').hasValue()) yRatio = e.attribute('height').toPixels('y') / svg.opts['scaleHeight'];
2828 | else if (!isNaN(viewBox[3])) yRatio = viewBox[3] / svg.opts['scaleHeight'];
2829 |
2830 | e.attribute('width', true).value = svg.opts['scaleWidth'];
2831 | e.attribute('height', true).value = svg.opts['scaleHeight'];
2832 | e.attribute('viewBox', true).value = '0 0 ' + (cWidth * xRatio) + ' ' + (cHeight * yRatio);
2833 | e.attribute('preserveAspectRatio', true).value = 'none';
2834 | }
2835 |
2836 | // clear and render
2837 | if (svg.opts['ignoreClear'] != true) {
2838 | ctx.clearRect(0, 0, cWidth, cHeight);
2839 | }
2840 | e.render(ctx);
2841 | if (isFirstRender) {
2842 | isFirstRender = false;
2843 | if (typeof(svg.opts['renderCallback']) == 'function') svg.opts['renderCallback'](dom);
2844 | }
2845 | };
2846 |
2847 | var waitingForImages = true;
2848 | if (svg.ImagesLoaded()) {
2849 | waitingForImages = false;
2850 | draw();
2851 | } else {
2852 | svg.intervalID = setInterval(function () {
2853 | var needUpdate = false;
2854 |
2855 | if (waitingForImages && svg.ImagesLoaded()) {
2856 | waitingForImages = false;
2857 | needUpdate = true;
2858 | }
2859 |
2860 | // need update from redraw?
2861 | if (typeof(svg.opts['forceRedraw']) == 'function') {
2862 | if (svg.opts['forceRedraw']() == true) needUpdate = true;
2863 | }
2864 |
2865 | // render if needed
2866 | if (needUpdate) {
2867 | draw();
2868 | svg.Mouse.runEvents(); // run and clear our events
2869 | }
2870 |
2871 | if (!waitingForImages) {
2872 | svg.stop()
2873 | }
2874 | }, 1000 / svg.FRAMERATE);
2875 | }
2876 | };
2877 |
2878 | svg.stop = function () {
2879 | if (svg.intervalID) {
2880 | clearInterval(svg.intervalID);
2881 | }
2882 | };
2883 |
2884 | svg.Mouse = new (function () {
2885 | this.events = [];
2886 | this.hasEvents = function () {
2887 | return this.events.length != 0;
2888 | };
2889 |
2890 | this.onclick = function (x, y) {
2891 | this.events.push({ type: 'onclick', x: x, y: y,
2892 | run: function (e) {
2893 | if (e.onclick) e.onclick();
2894 | }
2895 | });
2896 | };
2897 |
2898 | this.onmousemove = function (x, y) {
2899 | this.events.push({ type: 'onmousemove', x: x, y: y,
2900 | run: function (e) {
2901 | if (e.onmousemove) e.onmousemove();
2902 | }
2903 | });
2904 | };
2905 |
2906 | this.eventElements = [];
2907 |
2908 | this.checkPath = function (element, ctx) {
2909 | for (var i = 0; i < this.events.length; i++) {
2910 | var e = this.events[i];
2911 | if (ctx.isPointInPath && ctx.isPointInPath(e.x, e.y)) this.eventElements[i] = element;
2912 | }
2913 | };
2914 |
2915 | this.checkBoundingBox = function (element, bb) {
2916 | for (var i = 0; i < this.events.length; i++) {
2917 | var e = this.events[i];
2918 | if (bb.isPointInBox(e.x, e.y)) this.eventElements[i] = element;
2919 | }
2920 | };
2921 |
2922 | this.runEvents = function () {
2923 | svg.ctx.canvas.style.cursor = '';
2924 |
2925 | for (var i = 0; i < this.events.length; i++) {
2926 | var e = this.events[i];
2927 | var element = this.eventElements[i];
2928 | while (element) {
2929 | e.run(element);
2930 | element = element.parent;
2931 | }
2932 | }
2933 |
2934 | // done running, clear
2935 | this.events = [];
2936 | this.eventElements = [];
2937 | }
2938 | });
2939 |
2940 | return svg;
2941 | }
2942 |
2943 |
2944 | module.exports = canvg;
--------------------------------------------------------------------------------