var Class = require('../../core/class'); var Transform = require('../../core/transform'); var Color = require('../../core/color'); var Node = require('./node'); var DOM = require('./dom'); var precision = 100; var defaultBox = { left: 0, top: 0, width: 500, height: 500 }; module.exports = Class(Node, { element_initialize: Node.prototype.initialize, initialize: function(tag){ this.element_initialize(tag); var element = this.element; var skew = this.skewElement = DOM.createElement('skew'); skew.on = true; element.appendChild(skew); var fill = this.fillElement = DOM.createElement('fill'); fill.on = false; element.appendChild(fill); var stroke = this.strokeElement = DOM.createElement('stroke'); stroke.on = false; element.appendChild(stroke); }, /* transform */ _transform: function(){ var container = this.parentNode; // Active Transformation Matrix var m = container ? new Transform(container._activeTransform).transform(this) : this; // Box in shape user space var box = this._boxCoords || this._size || defaultBox; var originX = box.left || 0, originY = box.top || 0, width = box.width || 1, height = box.height || 1; // Flipped var flip = m.yx / m.xx > m.yy / m.xy; if (m.xx < 0 ? m.xy >= 0 : m.xy < 0) flip = !flip; flip = flip ? -1 : 1; m = new Transform().scale(flip, 1).transform(m); // Rotation is approximated based on the transform var rotation = Math.atan2(-m.xy, m.yy) * 180 / Math.PI; // Reverse the rotation, leaving the final transform in box space var rad = rotation * Math.PI / 180, sin = Math.sin(rad), cos = Math.cos(rad); var transform = new Transform( (m.xx * cos - m.xy * sin), (m.yx * cos - m.yy * sin) * flip, (m.xy * cos + m.xx * sin) * flip, (m.yy * cos + m.yx * sin) ); var rotationTransform = new Transform().rotate(rotation, 0, 0); var shapeToBox = new Transform().rotate(-rotation, 0, 0).transform(m).moveTo(0,0); // Scale box after reversing rotation width *= Math.abs(shapeToBox.xx); height *= Math.abs(shapeToBox.yy); // Place box var left = m.x, top = m.y; // Compensate for offset by center origin rotation var vx = -width / 2, vy = -height / 2; var point = rotationTransform.point(vx, vy); left -= point.x - vx; top -= point.y - vy; // Adjust box position based on offset var rsm = new Transform(m).moveTo(0,0); point = rsm.point(originX, originY); left += point.x; top += point.y; if (flip < 0) left = -left - width; // Place transformation origin var point0 = rsm.point(-originX, -originY); var point1 = rotationTransform.point(width, height); var point2 = rotationTransform.point(width, 0); var point3 = rotationTransform.point(0, height); var minX = Math.min(0, point1.x, point2.x, point3.x), maxX = Math.max(0, point1.x, point2.x, point3.x), minY = Math.min(0, point1.y, point2.y, point3.y), maxY = Math.max(0, point1.y, point2.y, point3.y); var transformOriginX = (point0.x - point1.x / 2) / (maxX - minX) * flip, transformOriginY = (point0.y - point1.y / 2) / (maxY - minY); // Adjust the origin point = shapeToBox.point(originX, originY); originX = point.x; originY = point.y; // Scale stroke var strokeWidth = this._strokeWidth; if (strokeWidth){ // Scale is the hypothenus between the two vectors // TODO: Use area calculation instead var vx = m.xx + m.xy, vy = m.yy + m.yx; strokeWidth *= Math.sqrt(vx * vx + vy * vy) / Math.sqrt(2); } // convert to multiplied precision space originX *= precision; originY *= precision; left *= precision; top *= precision; width *= precision; height *= precision; // Set box var element = this.element; element.coordorigin = originX + ',' + originY; element.coordsize = width + ',' + height; element.style.left = left + 'px'; element.style.top = top + 'px'; element.style.width = width; element.style.height = height; element.style.rotation = rotation.toFixed(8); element.style.flip = flip < 0 ? 'x' : ''; // Set transform var skew = this.skewElement; skew.matrix = [transform.xx.toFixed(4), transform.xy.toFixed(4), transform.yx.toFixed(4), transform.yy.toFixed(4), 0, 0]; skew.origin = transformOriginX + ',' + transformOriginY; // Set stroke this.strokeElement.weight = strokeWidth + 'px'; }, /* styles */ _createGradient: function(style, stops){ var fill = this.fillElement; // Temporarily eject the fill from the DOM this.element.removeChild(fill); fill.type = style; fill.method = 'none'; fill.rotate = true; var colors = [], color1, color2; var addColor = function(offset, color){ color = Color.detach(color); if (color1 == null) color1 = color2 = color; else color2 = color; colors.push(offset + ' ' + color[0]); }; // Enumerate stops, assumes offsets are enumerated in order if ('length' in stops) for (var i = 0, l = stops.length - 1; i <= l; i++) addColor(i / l, stops[i]); else for (var offset in stops) addColor(offset, stops[offset]); fill.color = color1[0]; fill.color2 = color2[0]; //if (fill.colors) fill.colors.value = colors; else fill.colors = colors; // Opacity order gets flipped when color stops are specified fill.opacity = color2[1]; fill['ao:opacity2'] = color1[1]; fill.on = true; this.element.appendChild(fill); return fill; }, _setColor: function(type, color){ var element = type == 'fill' ? this.fillElement : this.strokeElement; if (color == null){ element.on = false; } else { color = Color.detach(color); element.color = color[0]; element.opacity = color[1]; element.on = true; } }, fill: function(color){ if (arguments.length > 1){ this.fillLinear(arguments); } else { this._boxCoords = defaultBox; var fill = this.fillElement; fill.type = 'solid'; fill.color2 = ''; fill['ao:opacity2'] = ''; if (fill.colors) fill.colors.value = ''; this._setColor('fill', color); } return this; }, fillRadial: function(stops, focusX, focusY, radiusX, radiusY, centerX, centerY){ var fill = this._createGradient('gradientradial', stops); if (focusX == null) focusX = this.left + this.width * 0.5; if (focusY == null) focusY = this.top + this.height * 0.5; if (radiusY == null) radiusY = radiusX || (this.height * 0.5); if (radiusX == null) radiusX = this.width * 0.5; if (centerX == null) centerX = focusX; if (centerY == null) centerY = focusY; centerX += centerX - focusX; centerY += centerY - focusY; var box = this._boxCoords = { left: centerX - radiusX * 2, top: centerY - radiusY * 2, width: radiusX * 4, height: radiusY * 4 }; focusX -= box.left; focusY -= box.top; focusX /= box.width; focusY /= box.height; fill.focussize = '0 0'; fill.focusposition = focusX + ',' + focusY; fill.focus = '50%'; this._transform(); return this; }, fillLinear: function(stops, x1, y1, x2, y2){ var fill = this._createGradient('gradient', stops); fill.focus = '100%'; if (arguments.length == 5){ var w = Math.abs(x2 - x1), h = Math.abs(y2 - y1); this._boxCoords = { left: Math.min(x1, x2), top: Math.min(y1, y2), width: w < 1 ? h : w, height: h < 1 ? w : h }; fill.angle = (360 + Math.atan2((x2 - x1) / h, (y2 - y1) / w) * 180 / Math.PI) % 360; } else { this._boxCoords = null; fill.angle = (x1 == null) ? 0 : (90 + x1) % 360; } this._transform(); return this; }, fillImage: function(url, width, height, left, top, color1, color2){ var fill = this.fillElement; if (color1 != null){ color1 = Color.detach(color1); if (color2 != null) color2 = Color.detach(color2); fill.type = 'pattern'; fill.color = color1[0]; fill.color2 = color2 == null ? color1[0] : color2[0]; fill.opacity = color2 == null ? 0 : color2[1]; fill['ao:opacity2'] = color1[1]; } else { fill.type = 'tile'; fill.color = ''; fill.color2 = ''; fill.opacity = 1; fill['ao:opacity2'] = 1; } if (fill.colors) fill.colors.value = ''; fill.rotate = true; fill.src = url; fill.size = '1,1'; fill.position = '0,0'; fill.origin = '0,0'; fill.aspect = 'ignore'; // ignore, atleast, atmost fill.on = true; if (!left) left = 0; if (!top) top = 0; this._boxCoords = width ? { left: left + 0.5, top: top + 0.5, width: width, height: height } : null; this._transform(); return this; }, /* stroke */ stroke: function(color, width, cap, join){ var stroke = this.strokeElement; this._strokeWidth = (width != null) ? width : 1; stroke.weight = (width != null) ? width + 'px' : 1; stroke.endcap = (cap != null) ? ((cap == 'butt') ? 'flat' : cap) : 'round'; stroke.joinstyle = (join != null) ? join : 'round'; this._setColor('stroke', color); return this; } });