523 lines
14 KiB
JavaScript
523 lines
14 KiB
JavaScript
var _core = require("./core");
|
|
|
|
var createElement = _core.createElement;
|
|
|
|
var PathProxy = require("../core/PathProxy");
|
|
|
|
var BoundingRect = require("../core/BoundingRect");
|
|
|
|
var matrix = require("../core/matrix");
|
|
|
|
var textContain = require("../contain/text");
|
|
|
|
var textHelper = require("../graphic/helper/text");
|
|
|
|
var Text = require("../graphic/Text");
|
|
|
|
// TODO
|
|
// 1. shadow
|
|
// 2. Image: sx, sy, sw, sh
|
|
var CMD = PathProxy.CMD;
|
|
var arrayJoin = Array.prototype.join;
|
|
var NONE = 'none';
|
|
var mathRound = Math.round;
|
|
var mathSin = Math.sin;
|
|
var mathCos = Math.cos;
|
|
var PI = Math.PI;
|
|
var PI2 = Math.PI * 2;
|
|
var degree = 180 / PI;
|
|
var EPSILON = 1e-4;
|
|
|
|
function round4(val) {
|
|
return mathRound(val * 1e4) / 1e4;
|
|
}
|
|
|
|
function isAroundZero(val) {
|
|
return val < EPSILON && val > -EPSILON;
|
|
}
|
|
|
|
function pathHasFill(style, isText) {
|
|
var fill = isText ? style.textFill : style.fill;
|
|
return fill != null && fill !== NONE;
|
|
}
|
|
|
|
function pathHasStroke(style, isText) {
|
|
var stroke = isText ? style.textStroke : style.stroke;
|
|
return stroke != null && stroke !== NONE;
|
|
}
|
|
|
|
function setTransform(svgEl, m) {
|
|
if (m) {
|
|
attr(svgEl, 'transform', 'matrix(' + arrayJoin.call(m, ',') + ')');
|
|
}
|
|
}
|
|
|
|
function attr(el, key, val) {
|
|
if (!val || val.type !== 'linear' && val.type !== 'radial') {
|
|
// Don't set attribute for gradient, since it need new dom nodes
|
|
el.setAttribute(key, val);
|
|
}
|
|
}
|
|
|
|
function attrXLink(el, key, val) {
|
|
el.setAttributeNS('http://www.w3.org/1999/xlink', key, val);
|
|
}
|
|
|
|
function bindStyle(svgEl, style, isText, el) {
|
|
if (pathHasFill(style, isText)) {
|
|
var fill = isText ? style.textFill : style.fill;
|
|
fill = fill === 'transparent' ? NONE : fill;
|
|
attr(svgEl, 'fill', fill);
|
|
attr(svgEl, 'fill-opacity', style.fillOpacity != null ? style.fillOpacity * style.opacity : style.opacity);
|
|
} else {
|
|
attr(svgEl, 'fill', NONE);
|
|
}
|
|
|
|
if (pathHasStroke(style, isText)) {
|
|
var stroke = isText ? style.textStroke : style.stroke;
|
|
stroke = stroke === 'transparent' ? NONE : stroke;
|
|
attr(svgEl, 'stroke', stroke);
|
|
var strokeWidth = isText ? style.textStrokeWidth : style.lineWidth;
|
|
var strokeScale = !isText && style.strokeNoScale ? el.getLineScale() : 1;
|
|
attr(svgEl, 'stroke-width', strokeWidth / strokeScale); // stroke then fill for text; fill then stroke for others
|
|
|
|
attr(svgEl, 'paint-order', isText ? 'stroke' : 'fill');
|
|
attr(svgEl, 'stroke-opacity', style.strokeOpacity != null ? style.strokeOpacity : style.opacity);
|
|
var lineDash = style.lineDash;
|
|
|
|
if (lineDash) {
|
|
attr(svgEl, 'stroke-dasharray', style.lineDash.join(','));
|
|
attr(svgEl, 'stroke-dashoffset', mathRound(style.lineDashOffset || 0));
|
|
} else {
|
|
attr(svgEl, 'stroke-dasharray', '');
|
|
} // PENDING
|
|
|
|
|
|
style.lineCap && attr(svgEl, 'stroke-linecap', style.lineCap);
|
|
style.lineJoin && attr(svgEl, 'stroke-linejoin', style.lineJoin);
|
|
style.miterLimit && attr(svgEl, 'stroke-miterlimit', style.miterLimit);
|
|
} else {
|
|
attr(svgEl, 'stroke', NONE);
|
|
}
|
|
}
|
|
/***************************************************
|
|
* PATH
|
|
**************************************************/
|
|
|
|
|
|
function pathDataToString(path) {
|
|
var str = [];
|
|
var data = path.data;
|
|
var dataLength = path.len();
|
|
|
|
for (var i = 0; i < dataLength;) {
|
|
var cmd = data[i++];
|
|
var cmdStr = '';
|
|
var nData = 0;
|
|
|
|
switch (cmd) {
|
|
case CMD.M:
|
|
cmdStr = 'M';
|
|
nData = 2;
|
|
break;
|
|
|
|
case CMD.L:
|
|
cmdStr = 'L';
|
|
nData = 2;
|
|
break;
|
|
|
|
case CMD.Q:
|
|
cmdStr = 'Q';
|
|
nData = 4;
|
|
break;
|
|
|
|
case CMD.C:
|
|
cmdStr = 'C';
|
|
nData = 6;
|
|
break;
|
|
|
|
case CMD.A:
|
|
var cx = data[i++];
|
|
var cy = data[i++];
|
|
var rx = data[i++];
|
|
var ry = data[i++];
|
|
var theta = data[i++];
|
|
var dTheta = data[i++];
|
|
var psi = data[i++];
|
|
var clockwise = data[i++];
|
|
var dThetaPositive = Math.abs(dTheta);
|
|
var isCircle = isAroundZero(dThetaPositive - PI2) || (clockwise ? dTheta >= PI2 : -dTheta >= PI2); // Mapping to 0~2PI
|
|
|
|
var unifiedTheta = dTheta > 0 ? dTheta % PI2 : dTheta % PI2 + PI2;
|
|
var large = false;
|
|
|
|
if (isCircle) {
|
|
large = true;
|
|
} else if (isAroundZero(dThetaPositive)) {
|
|
large = false;
|
|
} else {
|
|
large = unifiedTheta >= PI === !!clockwise;
|
|
}
|
|
|
|
var x0 = round4(cx + rx * mathCos(theta));
|
|
var y0 = round4(cy + ry * mathSin(theta)); // It will not draw if start point and end point are exactly the same
|
|
// We need to shift the end point with a small value
|
|
// FIXME A better way to draw circle ?
|
|
|
|
if (isCircle) {
|
|
if (clockwise) {
|
|
dTheta = PI2 - 1e-4;
|
|
} else {
|
|
dTheta = -PI2 + 1e-4;
|
|
}
|
|
|
|
large = true;
|
|
|
|
if (i === 9) {
|
|
// Move to (x0, y0) only when CMD.A comes at the
|
|
// first position of a shape.
|
|
// For instance, when drawing a ring, CMD.A comes
|
|
// after CMD.M, so it's unnecessary to move to
|
|
// (x0, y0).
|
|
str.push('M', x0, y0);
|
|
}
|
|
}
|
|
|
|
var x = round4(cx + rx * mathCos(theta + dTheta));
|
|
var y = round4(cy + ry * mathSin(theta + dTheta)); // FIXME Ellipse
|
|
|
|
str.push('A', round4(rx), round4(ry), mathRound(psi * degree), +large, +clockwise, x, y);
|
|
break;
|
|
|
|
case CMD.Z:
|
|
cmdStr = 'Z';
|
|
break;
|
|
|
|
case CMD.R:
|
|
var x = round4(data[i++]);
|
|
var y = round4(data[i++]);
|
|
var w = round4(data[i++]);
|
|
var h = round4(data[i++]);
|
|
str.push('M', x, y, 'L', x + w, y, 'L', x + w, y + h, 'L', x, y + h, 'L', x, y);
|
|
break;
|
|
}
|
|
|
|
cmdStr && str.push(cmdStr);
|
|
|
|
for (var j = 0; j < nData; j++) {
|
|
// PENDING With scale
|
|
str.push(round4(data[i++]));
|
|
}
|
|
}
|
|
|
|
return str.join(' ');
|
|
}
|
|
|
|
var svgPath = {};
|
|
|
|
svgPath.brush = function (el) {
|
|
var style = el.style;
|
|
var svgEl = el.__svgEl;
|
|
|
|
if (!svgEl) {
|
|
svgEl = createElement('path');
|
|
el.__svgEl = svgEl;
|
|
}
|
|
|
|
if (!el.path) {
|
|
el.createPathProxy();
|
|
}
|
|
|
|
var path = el.path;
|
|
|
|
if (el.__dirtyPath) {
|
|
path.beginPath();
|
|
path.subPixelOptimize = false;
|
|
el.buildPath(path, el.shape);
|
|
el.__dirtyPath = false;
|
|
var pathStr = pathDataToString(path);
|
|
|
|
if (pathStr.indexOf('NaN') < 0) {
|
|
// Ignore illegal path, which may happen such in out-of-range
|
|
// data in Calendar series.
|
|
attr(svgEl, 'd', pathStr);
|
|
}
|
|
}
|
|
|
|
bindStyle(svgEl, style, false, el);
|
|
setTransform(svgEl, el.transform);
|
|
|
|
if (style.text != null) {
|
|
svgTextDrawRectText(el, el.getBoundingRect());
|
|
} else {
|
|
removeOldTextNode(el);
|
|
}
|
|
};
|
|
/***************************************************
|
|
* IMAGE
|
|
**************************************************/
|
|
|
|
|
|
var svgImage = {};
|
|
|
|
svgImage.brush = function (el) {
|
|
var style = el.style;
|
|
var image = style.image;
|
|
|
|
if (image instanceof HTMLImageElement) {
|
|
var src = image.src;
|
|
image = src;
|
|
}
|
|
|
|
if (!image) {
|
|
return;
|
|
}
|
|
|
|
var x = style.x || 0;
|
|
var y = style.y || 0;
|
|
var dw = style.width;
|
|
var dh = style.height;
|
|
var svgEl = el.__svgEl;
|
|
|
|
if (!svgEl) {
|
|
svgEl = createElement('image');
|
|
el.__svgEl = svgEl;
|
|
}
|
|
|
|
if (image !== el.__imageSrc) {
|
|
attrXLink(svgEl, 'href', image); // Caching image src
|
|
|
|
el.__imageSrc = image;
|
|
}
|
|
|
|
attr(svgEl, 'width', dw);
|
|
attr(svgEl, 'height', dh);
|
|
attr(svgEl, 'x', x);
|
|
attr(svgEl, 'y', y);
|
|
setTransform(svgEl, el.transform);
|
|
|
|
if (style.text != null) {
|
|
svgTextDrawRectText(el, el.getBoundingRect());
|
|
} else {
|
|
removeOldTextNode(el);
|
|
}
|
|
};
|
|
/***************************************************
|
|
* TEXT
|
|
**************************************************/
|
|
|
|
|
|
var svgText = {};
|
|
|
|
var _tmpTextHostRect = new BoundingRect();
|
|
|
|
var _tmpTextBoxPos = {};
|
|
var _tmpTextTransform = [];
|
|
var TEXT_ALIGN_TO_ANCHRO = {
|
|
left: 'start',
|
|
right: 'end',
|
|
center: 'middle',
|
|
middle: 'middle'
|
|
};
|
|
/**
|
|
* @param {module:zrender/Element} el
|
|
* @param {Object|boolean} [hostRect] {x, y, width, height}
|
|
* If set false, rect text is not used.
|
|
*/
|
|
|
|
var svgTextDrawRectText = function (el, hostRect) {
|
|
var style = el.style;
|
|
var elTransform = el.transform;
|
|
var needTransformTextByHostEl = el instanceof Text || style.transformText;
|
|
el.__dirty && textHelper.normalizeTextStyle(style, true);
|
|
var text = style.text; // Convert to string
|
|
|
|
text != null && (text += '');
|
|
|
|
if (!textHelper.needDrawText(text, style)) {
|
|
return;
|
|
} // render empty text for svg if no text but need draw text.
|
|
|
|
|
|
text == null && (text = ''); // Follow the setting in the canvas renderer, if not transform the
|
|
// text, transform the hostRect, by which the text is located.
|
|
|
|
if (!needTransformTextByHostEl && elTransform) {
|
|
_tmpTextHostRect.copy(hostRect);
|
|
|
|
_tmpTextHostRect.applyTransform(elTransform);
|
|
|
|
hostRect = _tmpTextHostRect;
|
|
}
|
|
|
|
var textSvgEl = el.__textSvgEl;
|
|
|
|
if (!textSvgEl) {
|
|
textSvgEl = createElement('text');
|
|
el.__textSvgEl = textSvgEl;
|
|
} // style.font has been normalized by `normalizeTextStyle`.
|
|
|
|
|
|
var textSvgElStyle = textSvgEl.style;
|
|
var font = style.font || textContain.DEFAULT_FONT;
|
|
var computedFont = textSvgEl.__computedFont;
|
|
|
|
if (font !== textSvgEl.__styleFont) {
|
|
textSvgElStyle.font = textSvgEl.__styleFont = font; // The computedFont might not be the orginal font if it is illegal font.
|
|
|
|
computedFont = textSvgEl.__computedFont = textSvgElStyle.font;
|
|
}
|
|
|
|
var textPadding = style.textPadding;
|
|
var textLineHeight = style.textLineHeight;
|
|
var contentBlock = el.__textCotentBlock;
|
|
|
|
if (!contentBlock || el.__dirtyText) {
|
|
contentBlock = el.__textCotentBlock = textContain.parsePlainText(text, computedFont, textPadding, textLineHeight, style.truncate);
|
|
}
|
|
|
|
var outerHeight = contentBlock.outerHeight;
|
|
var lineHeight = contentBlock.lineHeight;
|
|
textHelper.getBoxPosition(_tmpTextBoxPos, el, style, hostRect);
|
|
var baseX = _tmpTextBoxPos.baseX;
|
|
var baseY = _tmpTextBoxPos.baseY;
|
|
var textAlign = _tmpTextBoxPos.textAlign || 'left';
|
|
var textVerticalAlign = _tmpTextBoxPos.textVerticalAlign;
|
|
setTextTransform(textSvgEl, needTransformTextByHostEl, elTransform, style, hostRect, baseX, baseY);
|
|
var boxY = textContain.adjustTextY(baseY, outerHeight, textVerticalAlign);
|
|
var textX = baseX;
|
|
var textY = boxY; // TODO needDrawBg
|
|
|
|
if (textPadding) {
|
|
textX = getTextXForPadding(baseX, textAlign, textPadding);
|
|
textY += textPadding[0];
|
|
} // `textBaseline` is set as 'middle'.
|
|
|
|
|
|
textY += lineHeight / 2;
|
|
bindStyle(textSvgEl, style, true, el); // FIXME
|
|
// Add a <style> to reset all of the text font as inherit?
|
|
// otherwise the outer <style> may set the unexpected style.
|
|
// Font may affect position of each tspan elements
|
|
|
|
var canCacheByTextString = contentBlock.canCacheByTextString;
|
|
var tspanList = el.__tspanList || (el.__tspanList = []);
|
|
var tspanOriginLen = tspanList.length; // Optimize for most cases, just compare text string to determine change.
|
|
|
|
if (canCacheByTextString && el.__canCacheByTextString && el.__text === text) {
|
|
if (el.__dirtyText && tspanOriginLen) {
|
|
for (var idx = 0; idx < tspanOriginLen; ++idx) {
|
|
updateTextLocation(tspanList[idx], textAlign, textX, textY + idx * lineHeight);
|
|
}
|
|
}
|
|
} else {
|
|
el.__text = text;
|
|
el.__canCacheByTextString = canCacheByTextString;
|
|
var textLines = contentBlock.lines;
|
|
var nTextLines = textLines.length;
|
|
var idx = 0;
|
|
|
|
for (; idx < nTextLines; idx++) {
|
|
// Using cached tspan elements
|
|
var tspan = tspanList[idx];
|
|
var singleLineText = textLines[idx];
|
|
|
|
if (!tspan) {
|
|
tspan = tspanList[idx] = createElement('tspan');
|
|
textSvgEl.appendChild(tspan);
|
|
tspan.appendChild(document.createTextNode(singleLineText));
|
|
} else if (tspan.__zrText !== singleLineText) {
|
|
tspan.innerHTML = '';
|
|
tspan.appendChild(document.createTextNode(singleLineText));
|
|
}
|
|
|
|
updateTextLocation(tspan, textAlign, textX, textY + idx * lineHeight);
|
|
} // Remove unused tspan elements
|
|
|
|
|
|
if (tspanOriginLen > nTextLines) {
|
|
for (; idx < tspanOriginLen; idx++) {
|
|
textSvgEl.removeChild(tspanList[idx]);
|
|
}
|
|
|
|
tspanList.length = nTextLines;
|
|
}
|
|
}
|
|
};
|
|
|
|
function setTextTransform(textSvgEl, needTransformTextByHostEl, elTransform, style, hostRect, baseX, baseY) {
|
|
matrix.identity(_tmpTextTransform);
|
|
|
|
if (needTransformTextByHostEl && elTransform) {
|
|
matrix.copy(_tmpTextTransform, elTransform);
|
|
} // textRotation only apply in RectText.
|
|
|
|
|
|
var textRotation = style.textRotation;
|
|
|
|
if (hostRect && textRotation) {
|
|
var origin = style.textOrigin;
|
|
|
|
if (origin === 'center') {
|
|
baseX = hostRect.width / 2 + hostRect.x;
|
|
baseY = hostRect.height / 2 + hostRect.y;
|
|
} else if (origin) {
|
|
baseX = origin[0] + hostRect.x;
|
|
baseY = origin[1] + hostRect.y;
|
|
}
|
|
|
|
_tmpTextTransform[4] -= baseX;
|
|
_tmpTextTransform[5] -= baseY; // Positive: anticlockwise
|
|
|
|
matrix.rotate(_tmpTextTransform, _tmpTextTransform, textRotation);
|
|
_tmpTextTransform[4] += baseX;
|
|
_tmpTextTransform[5] += baseY;
|
|
} // See the definition in `Style.js#textOrigin`, the default
|
|
// origin is from the result of `getBoxPosition`.
|
|
|
|
|
|
setTransform(textSvgEl, _tmpTextTransform);
|
|
} // FIXME merge the same code with `helper/text.js#getTextXForPadding`;
|
|
|
|
|
|
function getTextXForPadding(x, textAlign, textPadding) {
|
|
return textAlign === 'right' ? x - textPadding[1] : textAlign === 'center' ? x + textPadding[3] / 2 - textPadding[1] / 2 : x + textPadding[3];
|
|
}
|
|
|
|
function updateTextLocation(tspan, textAlign, x, y) {
|
|
// Consider different font display differently in vertial align, we always
|
|
// set vertialAlign as 'middle', and use 'y' to locate text vertically.
|
|
attr(tspan, 'dominant-baseline', 'middle');
|
|
attr(tspan, 'text-anchor', TEXT_ALIGN_TO_ANCHRO[textAlign]);
|
|
attr(tspan, 'x', x);
|
|
attr(tspan, 'y', y);
|
|
}
|
|
|
|
function removeOldTextNode(el) {
|
|
if (el && el.__textSvgEl) {
|
|
// textSvgEl may has no parentNode if el has been removed temporary.
|
|
if (el.__textSvgEl.parentNode) {
|
|
el.__textSvgEl.parentNode.removeChild(el.__textSvgEl);
|
|
}
|
|
|
|
el.__textSvgEl = null;
|
|
el.__tspanList = [];
|
|
el.__text = null;
|
|
}
|
|
}
|
|
|
|
svgText.drawRectText = svgTextDrawRectText;
|
|
|
|
svgText.brush = function (el) {
|
|
var style = el.style;
|
|
|
|
if (style.text != null) {
|
|
svgTextDrawRectText(el, false);
|
|
} else {
|
|
removeOldTextNode(el);
|
|
}
|
|
};
|
|
|
|
exports.path = svgPath;
|
|
exports.image = svgImage;
|
|
exports.text = svgText; |