├── .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 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 10 | $RCSfile: painting-fill-05-t.svg,v $ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | $Revision: 1.4 $ 30 | 31 | 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 | 18 | 19 | 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; --------------------------------------------------------------------------------