/** * @namespace ludo.svg */ /** Class for creating SVG DOM Nodes @namespace ludo.canvas @class ludo.svg.Node @param {String} tag @param {Object} properties @optional @param {String} text @optional @example var v = new ludo.View({ renderTo: document.body, layout:{ width:'matchParent', height:'matchParent' } }); var svg = v.svg(); var circle = svg.$('circle', { cx: 100, cy: 100, r: 50 }); circle.css('fill', '#ff0000'); svg.append(circle); circle.animate({ cx:300, cy: 200 },{ duration: 1000, complete:function(){ console.log('completed'); } }); */ ludo.svg.Node = new Class({ Extends: Events, el: undefined, tagName: undefined, id: undefined, dirty: undefined, _bbox: undefined, _attr: undefined, classNameCache: [], /* * Transformation cache * @property tCache * @type {Object} * @private */ tCache: {}, /* * Internal cache * @property {Object} tCacheStrings * @private */ tCacheStrings: undefined, initialize: function (tagName, properties, text) { this._attr = {}; properties = properties || {}; properties.id = this.id = properties.id || 'ludo-svg-node-' + String.uniqueID(); if (tagName !== undefined)this.tagName = tagName; this.createNode(this.tagName, properties); if (text !== undefined) { this.text(text); } }, createNode: function (el, properties) { if (properties !== undefined) { if (typeof el == "string") { el = this.createNode(el); } var that = this; Object.each(properties, function (value, key) { if (value['getUrl'] !== undefined) { value = value.getUrl(); } if (key == 'css') { this.css(value); } else if (key.substring(0, 6) == "xlink:") { el.setAttributeNS("http://www.w3.org/1999/xlink", key.substring(6), value); } else { el.setAttribute(key, value); that._attr[key] = value; } }.bind(this)); } else { el = document.createElementNS("http://www.w3.org/2000/svg", el); } this.el = el; el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)"); return el; }, getEl: function () { return this.el; }, addEvents: function (events) { for (var key in events) { if (events.hasOwnProperty(key)) { this.on(key, events[key]); } } }, /** * Add DOM events to SVG node * @param {String} event * @param {Function} fn * @memberof ludo.svg.Node.prototype */ on: function (event, fn) { switch (event.toLowerCase()) { case 'mouseenter': ludo.canvasEventManager.addMouseEnter(this, fn); break; case 'mouseleave': ludo.canvasEventManager.addMouseLeave(this, fn); break; default: this._addEvent(event, this.getDOMEventFn(event, fn), this.el); this.addEvent(event, fn); } }, /** * Add event to DOM element * el is optional, default this.el * @function _addEvent * @param {String} ev * @param {Function} fn * @param {Object} el * @private * @memberof ludo.svg.Node.prototype */ _addEvent: (function () { if (document.addEventListener) { return function (ev, fn, el) { if (el == undefined)el = this.el; el.addEventListener(ev, fn, false); } } else { return function (ev, fn, el) { if (el == undefined)el = this.el; el.attachEvent("on" + ev, fn); } } })(), off: function (event, listener) { if (this.el.removeEventListener) this.el.removeEventListener(event, listener, false); else this.el.detachEvent('on' + event, listener); }, relativePosition: function (e) { var rect = this.el.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }, getDOMEventFn: function (eventName, fn) { return function (e) { e = e || window.event; var target = e.currentTarget || e.target || e.srcElement; while (target && target.nodeType == 3) target = target.parentNode; target = target['correspondingUseElement'] || target; var mouseX, mouseY; var touches = e.touches; var pX = e.pageX; var py = e.pageY; if (touches && touches.length > 0) { mouseX = touches[0].clientX; mouseY = touches[0].clientY; pX = touches[0].pageX; py = touches[0].pageY; } else { mouseX = e.clientX; mouseY = e.clientY; } var svg = this.el.tagName == 'svg' ? this.el : this.el.ownerSVGElement; var off = svg ? svg.getBoundingClientRect() : {left: 0, top: 0}; e = { target: target, pageX: (pX != null) ? pX : mouseX + document.scrollLeft, pageY: (py != null) ? py : mouseY + document.scrollTop, clientX: mouseX - off.left, // Relative position to SVG element clientY: mouseY - off.top, event: e }; if (fn) { fn.call(this, e, this, fn); } return false; }.bind(this); }, svgCoordinates: undefined, svgPos: function (target) { if (this.svgCoordinates == undefined) { while (target.tagName.toLowerCase() != 'g') { target = target.parentNode; } this.svgCoordinates = $(target).position(); console.log(this.svgCoordinates); } return this.svgCoordinates; }, /** * append a new node * @function append * @param {canvas.View|canvas.Node} node node * @return {canvas.Node} parent * @memberof ludo.svg.Node.prototype */ append: function (node) { this.el.appendChild(node.getEl()); node.parentNode = this; return this; }, parent: function () { return this.parentNode; }, /** * Show SVG node, i.e. set display css property to '' * @function show * @memberof ludo.svg.Node.prototype */ show: function () { this.css('display', ''); }, /** * Hides SVG node, i.e. set display css property to 'none' * @function hide * @memberof ludo.svg.Node.prototype */ hide: function () { this.css('display', 'none'); }, /** * Returns true if SVG node is hidden * @returns {boolean} * @memberof ludo.svg.Node.prototype */ isHidden: function () { return this.css('display') == 'none'; }, setProperties: function (p) { jQuery.each(p, function(key, val){ this.set(key, val); }.bind(this)); }, /** * Set or get attribute. * @param {String} key * @param {String|Number|ludo.svg.Node} value * @returns {*} * @memberof ludo.svg.Node.prototype * @example * var x = node.attr("x"); // Get attribute * node.attr("x", 100); // Sets attribute */ attr: function (key, value) { if (arguments.length == 1)return this.get(key); this.set(key, value); }, /** * Set SVG node attribute. If a ludo.svg.Node object is sent as value, the set function will * set an url attribute( url(#<id>). * @param {String} key * @param {String|Number|ludo.svg.Node} value * @memberof ludo.svg.Node.prototype */ set: function (key, value) { this._attr[key] = value; this.dirty = true; if (key.substring(0, 6) == "xlink:") { if (value['id'] !== undefined)value = '#' + value.getId(); this.el.setAttributeNS("http://www.w3.org/1999/xlink", key.substring(6), value); } else { if (value != undefined && value['id'] !== undefined)value = 'url(#' + value.getId() + ')'; this.el.setAttribute(key, value); } }, _setPlain: function (key, value) { this._attr[key] = value; this.dirty = true; this.el.setAttribute(key, value); }, /** * Remove SVG attribute * @param {String} key * @memberof ludo.svg.Node.prototype */ removeAttr: function (key) { if (key.substring(0, 6) == "xlink:") { this.el.removeAttributeNS("http://www.w3.org/1999/xlink", key.substring(6)); } else { this.el.removeAttribute(key); } }, remove: function () { if (this.el.parentNode) { this.el.parentNode.removeChild(this.el); } }, /** * Get SVG attribute * @param {String} key * @returns {*} * @memberof ludo.svg.Node.prototype */ get: function (key) { if (key.substring(0, 6) == "xlink:") { return this.el.getAttributeNS("http://www.w3.org/1999/xlink", key.substring(6)); } else { return this.el[key] != undefined && this.el[key].animVal != undefined ? this.el[key].animVal.value : this.el.getAttribute(key); } }, /** * Returns x and y translation, i.e. translated x and y coordinates * @function getTranslate * @memberof ludo.svg.Node.prototype * @returns {Array} * @example * var translate = node.getTranslate(); // returns [x,y], example; [100,150] */ getTranslate: function () { return this._getMatrix().getTranslate(); }, /** * Apply filter to node * @function applyFilter * @param {canvas.Filter} filter * @memberof ludo.svg.Node.prototype */ applyFilter: function (filter) { this.set('filter', filter.getUrl()); }, /** * Apply mask to node * @function addMask * @param {canvas.Node} mask * @memberof ludo.svg.Node.prototype */ applyMask: function (mask) { this.set('mask', mask.getUrl()); }, /** * Apply clip path to node. Passed argument should be a "clipPath" node * @function clip * @param {canvas.Node} clip * @memberof ludo.svg.Node.prototype * @example * var svg = view.svg(); * * var clipPath = s.$('clipPath'); * var clipCircle = s.$('circle', { cx:50,cy:50,r:50 }); * clipPath.append(clipPath); * s.appendDef(clipPath); // Append clip path to <defs> node of <svg> * * var rect = s.$('rect', { x:50, y:150, width:100,height:100, fill: '#ff0000' }); * rect.clip(clipPath); */ clip: function (clip) { this.set('clip-path', clip.getUrl()); }, setPattern:function(pattern){ this.set('fill', pattern.getUrl()); }, /** Create url reference @function url @param {String} key @memberof ludo.svg.Node.prototype @param {canvas.Node|String} to @example node.url('filter', filterObj); // sets node property filter="url(#<filterObj->id>)" node.url('mask', 'MyMask'); // sets node property mask="url(#MyMask)" */ url: function (key, to) { this.set(key, to['getUrl'] !== undefined ? to.getUrl() : 'url(#' + to + ')'); }, href: function (url) { this.set('xlink:href', url); }, /** * Update text content of node * @function text * @param {String} text * @memberof ludo.svg.Node.prototype */ text: function (text) { this.el.textContent = text; }, /** Adds a new child DOM node @function add @param {String} tagName @param {Object} properties @param {String} text content @optional @return {ludo.svg.Node} added node @memberof ludo.svg.Node.prototype @example var filter = new ludo.svg.Filter(); filter.add('feGaussianBlur', { 'stdDeviation' : 2, result:'blur' }); */ add: function (tagName, properties, text) { var node = new ludo.svg.Node(tagName, properties, text); this.append(node); return node; }, /** * Set or get CSS property * @param {String} key * @param {String|Number} value * @returns {ludo.svg.Node} * @memberof ludo.svg.Node.prototype * @example * var stroke = node.css('stroke'); // Get stroke css attribute * node.css('stroke', '#FFFFFF'); // set stroke css property */ css: function (key, value) { if (arguments.length == 1 && jQuery.type(key) == 'string') { return this.el.style[String.camelCase(key)]; } else if (arguments.length == 1) { var el = this.el; $.each(key, function (attr, val) { el.style[String.camelCase(attr)] = val; }); } else { this.el.style[String.camelCase(key)] = value; } return this; }, /** * Add css class to SVG node * @function addClass * @param {String} className * @returns {ludo.svg.Node} * @memberof ludo.svg.Node.prototype */ addClass: function (className) { if (!this.hasClass(className)) { this.classNameCache.push(className); this.updateNodeClassNameById(); } var cls = this.el.getAttribute('class'); if (cls) { cls = cls.split(/\s/g); if (cls.indexOf(className) >= 0)return; cls.push(className); this.set('class', cls.join(' ')); } else { this.set('class', className); } return this; }, /** Returns true if svg node has given css class name @function hasClass @param {String} className @return {Boolean} @memberof ludo.svg.Node.prototype @example var node = new ludo.svg.Node('rect', { id:'myId2'}); node.addClass('myClass'); alert(node.hasClass('myClass')); */ hasClass: function (className) { if (!this.classNameCache) { var cls = this.el.getAttribute('class'); if (cls) { this.classNameCache = cls.split(/\s/g); } else { this.classNameCache = []; } } return this.classNameCache.indexOf(className) >= 0; }, /** Remove css class name from css Node @function removeClass @param {String} className @memberof ludo.svg.Node.prototype @example var node = new ludo.svg.Node('rect', { id:'myId2'}); node.addClass('myClass'); node.addClass('secondClass'); node.removeClass('myClass'); */ removeClass: function (className) { if (this.hasClass(className)) { var id = this._attr['id']; this.classNameCache.erase(className); this.updateNodeClassNameById(); } return this; }, updateNodeClassNameById: function () { this.set('class', this.classNameCache.join(' ')); }, getId: function () { return this.id; }, getUrl: function () { return 'url(#' + this.id + ')'; }, /** * Returns nodes position relative to parent * @function position() * @returns {Object} * @memberof ludo.svg.Node.prototype * @example * var pos = node.position(); // returns {x: 100, y: 200 } * */ position: function () { var bbox = this.getBBox(); if (this.tagName == 'g') { if (this._matrix != undefined) { var translate = this._matrix.getTranslate(); return { left: translate[0], top: translate[1] } } else { return {left: 0, top: 0}; } } var off = [0, 0]; if (this._matrix != undefined) { off = this._matrix.getTranslate(); } return { left: bbox.x + off[0], top: bbox.y + off[1] } }, /** * Returns nodes position relative to top SVG element * @memberof ludo.svg.Node.prototype * @returns {Object} * @example * var pos = node.offset(); // returns {x: 100, y: 200 } */ offset: function () { var pos = this.position(); var p = this.parentNode; while (p && p.tagName != 'svg') { var parentPos = p.position(); pos.left += parentPos.left; pos.top += parentPos.top; p = p.parentNode; } return pos; }, /** * Returns bounding box of el as an object with x,y, width and height. * * @function getBBox * @return {Object} * @memberof ludo.svg.Node.prototype */ getBBox: function () { if (this.tagName == 'g' || this.tagName == 'text') { return this.el.getBBox(); } if (this._bbox == undefined || this.dirty) { var attr = this._attr; switch (this.tagName) { case 'rect': this._bbox = { x: attr.x || 0, y: attr.y || 0, width: attr.width, height: attr.height }; break; case 'circle': this._bbox = { x: attr.cx - attr.r, y: attr.cy - attr.r, width: attr.r * 2, height: attr.r * 2 }; break; case 'ellipse': this._bbox = { x: attr.cx - attr.rx, y: attr.cy - attr.ry, width: attr.rx * 2, height: attr.ry * 2 }; break; case 'path': this._setBBoxOfPath('d'); break; case 'polyline': case 'polygon': this._setBBoxOfPath('points'); break; case 'image': var rect = this.el.getBoundingClientRect(); this._bbox = {x: attr.x || 0, y: attr.y || 0, width: rect.width, height: rect.height}; break; default: this._bbox = {x: 0, y: 0, width: 0, height: 0}; } } return this._bbox; }, _setBBoxOfPath: function (property) { var p = this._attr[property]; p = p.replace(/,/g, ' '); p = p.replace(/([a-z])/g, '$1 '); p = p.replace(/\s+/g, ' '); if (property == 'd2') { if (this.el.getBoundingClientRect != undefined) { var r = this.el.getBoundingClientRect(); var tr = this.getTranslate(); var parent = this.el.parentNode.getBoundingClientRect(); if (r != undefined) { this._bbox = { x: r.left - tr[0] - parent.left, y: r.top - tr[1] - parent.top, width: r.width, height: r.height }; console.log(r, parent); return; } } } p = p.replace(/\s+/g, ' '); p = p.trim(); var points = p.split(/\s/g); var minX, minY, maxX, maxY; var currentChar = undefined; var i=0; while(i < points.length) { p = points[i]; if (isNaN(p)) { p = p.toLowerCase(); if (p == 'l' || p == 'm') { currentChar = p; i++; yi = i + 2; } } if (!isNaN(points[i])) { var x = parseFloat(points[i]); if (minX == undefined || x < minX)minX = x; if (maxX == undefined || x > maxX)maxX = x; var y = parseFloat(points[i+1]); if (minY == undefined || y < minY)minY = y; if (maxY == undefined || y > maxY)maxY = y; i+=2; }else{ i++; } } this._bbox = { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }, /** * Returns rectangular size of element, i.e. bounding box width - bounding box x and * bounding box width - bounding box y. Values are returned as { x : 100, y : 150 } * where x is width and y is height. * @function getSize * @return {Object} size x and y * @memberof ludo.svg.Node.prototype */ getSize: function () { var b = this.getBBox(); return { x: b.width - b.x, y: b.height - b.y }; }, /** * The nearest ancestor 'svg' element. Null if the given element is the outermost svg element. * @function svg * @return {ludo.svg.Node.el} svg * @memberof ludo.svg.Node.prototype */ svg: function () { return this.el.ownerSVGElement; }, /** * The element which established the current viewport. Often, the nearest ancestor ‘svg’ element. Null if the given element is the outermost svg element * @function getViewPort * @return {ludo.svg.Node.el} svg * @memberof ludo.svg.Node.prototype */ getViewPort: function () { return this.el.viewPortElement; }, /** * Returns rotation as a [degrees, x, y] * @function getRotate * @memberof ludo.svg.Node.prototype * @returns {Array} */ getRotate: function () { return this._getMatrix().getRotation(); }, /** * Set scale * @function setScale * @param {Number} x * @param {Number} y (Optional y scale, assumes x if not set) * @memberof ludo.svg.Node.prototype */ setScale: function (x, y) { this._getMatrix().setScale(x, y); }, /** * Scale SVG node. The difference between scale and setScale is that scale adds to existing * scale values * @function scale * @param {Number} x * @param {Number} y * @memberof ludo.svg.Node.prototype */ scale: function (x, y) { this._getMatrix().scale(x, y); }, /** * Set rotation * @function setRotate * @param {Number} degrees Rotation in degrees * @param {Number} x Optional x coordinate to rotate about * @param {Number} y Optional x coordinate to rotate about * @memberof ludo.svg.Node.prototype * @example * node.rotate(100); // Rotation is 100 * node.rotate(50); // Rotation is 50 */ setRotate: function (degrees, x, y) { this._getMatrix().setRotation(degrees, x, y); }, /** * Rotate SVG node * @functino rotate * @param {Number} degrees Rotation in degrees * @param {Number} x Optional x coordinate to rotate about * @param {Number} y Optional x coordinate to rotate about * @memberof ludo.svg.Node.prototype * @example * node.rotate(100); // Rotation is 100 * node.rotate(50); // Rotation is 150 */ rotate: function (degrees, x, y) { this._getMatrix().rotate(degrees, x, y); }, /** * Set SVG translation(movement in x and y direction) * @function setTranslate * @param {Number} x * @param {Number} y * @memberof ludo.svg.Node.prototype * @example * node.setTranslate(500,100); * node.setTranslate(550,200); // Node is offset by 550x200 ( second translation overwrites first) */ setTranslate: function (x, y) { this._getMatrix().setTranslate(x, y); }, /** * Translate SVG node(movement in x and y direction) * @function translate * @param {Number} x * @param {Number} y * @memberof ludo.svg.Node.prototype * @example * node.setTranslate(500,100); * node.setTranslate(550,200); // Node is offset by 1050x300 (first translation + second) */ translate: function (x, y) { this._getMatrix().translate(x, y); }, _matrix: undefined, _getMatrix: function () { if (this._matrix == undefined) { this._matrix = new ludo.svg.Matrix(this); } return this._matrix; }, empty: function () { this.el.textContent = ''; }, /** * Function to animate SVG properties such as radius, x,y, colors etc. * * When animating colors, set colors using the set function and not the css function since CSS fill and stroke has highest priority. * * For demos, see * <a href="../demo/svg/animation.php">animation.php</a> * <a href="../demo/svg/animation.php">queued-animation.php</a> * <a href="../demo/svg/animation.php">transformation.php</a> * <a href="../demo/svg/animation.php">clipping.php</a> * <a href="../demo/svg/animation.php">masking.php</a> * @function animate * @param {Object} properties Properties to animate, example: { x: 100, width: 100 } * @param {Object} options Animation options * @param {Object} options.duration - Animation duration in milliseconds, default: 400/1000seconds * @param {Function} options.easing Reference to ludo.svg.easing easing function, example: ludo.svg.easing.linear * @param {Function} options.complete Function to execute on complete * @param {Function} options.progress Function executed after each animation step. Argument: completed from 0 to 1 * @param {Boolean} options.queue True to queue animations for this SVG element. Default: true * @param {Function} options.validate Option function called before each step of the animation. If this function returns false, the animation will stop. * Arguments: 1) unique id of animation 2) unique id of latest animation for this SVG element. Useful if new animation should stop old animation. * @memberof ludo.svg.Node.prototype * @example * // Animating using properties and options objects. * circle.animate({ * 'cx': 100, cy: 100 * },{ * easing: ludo.svg.easing.bounce, * duration: 1000, * complete:function(){ * console.log('Animation complete'); * }, * progress:function(t){ // float from 0 to 1 * console.log('Progress: ' + Math.round(t*100) + '%'); * } * }); * * // or with duration, easing and complete function as parameters. * circle.animate({ * 'cx': 100, cy: 100 * }, 400, ludo.svg.easing.bounce, function(){ console.log('finished') }); * * */ animate: function (properties, options, easing, complete) { var opt = {}; if (!jQuery.isPlainObject(options)) { opt.duration = options; opt.easing = easing; opt.complete = complete; } else opt = options; ludo.svgAnimation.animate(this, properties, opt); return this; }, _animation: undefined, animation: function () { if (this._animation === undefined) { this._animation = new ludo.svg.Animation(this.getEl()); } return this._animation; }, /** * Bring nodes to front (z index) * @function toFront * @memberof ludo.svg.Node.prototype */ toFront: function () { if (Browser['ie'])this._toFront.delay(20, this); else this._toFront(); }, _toFront: function () { this.el.parentNode.appendChild(this.el); }, /** * Bring nodes to back (z index) * @function toFront * @memberof ludo.svg.Node.prototype */ toBack: function () { if (Browser['ie']) this._toBack.delay(20, this); else this._toBack(); }, _toBack: function () { this.el.parentNode.insertBefore(this.el, this.el.parentNode.firstChild); } });