blob: 231e5ae24332329f05d2c2be9b1bdcde3210ec61 [file] [log] [blame]
// TODO
// 1. shadow
// 2. Image: sx, sy, sw, sh
import { createElement } from './core';
import PathProxy from '../core/PathProxy';
import BoundingRect from '../core/BoundingRect';
import * as matrix from '../core/matrix';
import * as textContain from '../contain/text';
import * as textHelper from '../graphic/helper/text';
import Text from '../graphic/Text';
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 = {};
export { svgPath as path };
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());
}
};
/***************************************************
* IMAGE
**************************************************/
var svgImage = {};
export { svgImage as image };
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());
}
};
/***************************************************
* TEXT
**************************************************/
var svgText = {};
export { svgText as text };
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);
}
svgText.drawRectText = svgTextDrawRectText;
svgText.brush = function (el) {
var style = el.style;
if (style.text != null) {
svgTextDrawRectText(el, false);
}
};