UNPKG

@meta2d/core

Version:

@meta2d/core: Powerful, Beautiful, Simple, Open - Web-Based 2D At Its Best .

1,516 lines 143 kB
import { CanvasLayer, lineAnimateTargetType, LineAnimateType, LockState, needImgCanvasPatchFlagsProps, } from './model'; import { drawArrow, getLineRect, getSplitAnchor, getLinePointPosAndAngle, createSvgPath, getLineLength, renderLineDirectionMarkers, } from '../diagrams'; import { Direction, inheritanceProps } from '../data'; import { calcRotate, distance, facePoint, rotatePoint, scalePoint, translatePoint, TwoWay, } from '../point'; import { calcCenter, calcRightBottom, calcRelativePoint, calcRelativeRect, rectInRect, scaleRect, translateRect, calcPivot, } from '../rect'; import { globalStore } from '../store'; import { calcTextLines, calcTextDrawRect, calcTextRect } from './text'; import { deepClone } from '../utils/clone'; import { renderFromArrow, renderToArrow } from './arrow'; import { Gradient, PenType } from '../pen'; import { pSBC, rgba, cubicBezierY } from '../utils'; import { isEmptyText } from '../utils/tool'; const LINE = "line"; const REPEAT = "repeat"; /** * ancestor 是否是 pen 的祖先 * @param pen 当前画笔 * @param ancestor 祖先画笔 */ export function isAncestor(pen, ancestor) { if (!pen || !ancestor) { return false; } let parent = getParent(pen); while (parent) { if (parent.id === ancestor.id) { return true; } parent = getParent(parent); } return false; } export function getParent(pen, root) { if (!pen || !pen.parentId || !pen.calculative) { return undefined; } const store = pen.calculative.canvas.store; const parent = store.pens[pen.parentId]; if (!root) { return parent; } return getParent(parent, root) || parent; } export function getAllChildren(pen, store) { if (!pen || !pen.children) { return []; } const children = []; pen.children.forEach((id) => { const child = store.pens[id]; if (child) { children.push(child); children.push(...getAllChildren(child, store)); } }); return children; } export function getAllFollowers(pen, store) { if (!pen || !pen.followers) { return []; } const followers = []; pen.followers.forEach((id) => { const follower = store.pens[id]; if (follower && !follower.parentId) { followers.push(follower); followers.push(...getAllFollowers(follower, store)); } }); return followers; } function drawBkLinearGradient(ctx, pen) { const { worldRect, gradientFromColor, gradientToColor, gradientAngle } = pen.calculative; return linearGradient(ctx, worldRect, gradientFromColor, gradientToColor, gradientAngle); } /** * 避免副作用,把创建好后的径向渐变对象返回出来 * @param ctx 画布绘制对象 * @param pen 当前画笔 * @returns 径向渐变 */ function drawBkRadialGradient(ctx, pen) { const { worldRect, gradientFromColor, gradientToColor, gradientRadius } = pen.calculative; if (!gradientFromColor || !gradientToColor) { return; } const { width, height, center } = worldRect; const { x: centerX, y: centerY } = center; let r = width; if (r < height) { r = height; } r *= 0.5; const grd = ctx.createRadialGradient(centerX, centerY, r * (gradientRadius || 0), centerX, centerY, r); grd.addColorStop(0, gradientFromColor); grd.addColorStop(1, gradientToColor); return grd; } function getLinearGradientPoints(x1, y1, x2, y2, r) { let slantAngle = 0; slantAngle = Math.PI / 2 - Math.atan2(y2 - y1, x2 - x1); const originX = (x1 + x2) / 2; const originY = (y1 + y2) / 2; const perpX1 = originX + r * Math.sin((90 * Math.PI) / 180 - slantAngle); const perpY1 = originY + r * -Math.cos((90 * Math.PI) / 180 - slantAngle); const perpX2 = originX + r * Math.sin((270 * Math.PI) / 180 - slantAngle); const perpY2 = originY + r * -Math.cos((270 * Math.PI) / 180 - slantAngle); return [perpX1, perpY1, perpX2, perpY2]; } function getBkRadialGradient(ctx, pen) { const { worldRect, gradientColors, gradientRadius } = pen.calculative; if (!gradientColors) { return; } let color = pen.calculative.gradientColors; if (pen.calculative.checked) { color = pen.calculative.onGradientColors; } const { width, height, center } = worldRect; const { x: centerX, y: centerY } = center; let r = width; if (r < height) { r = height; } r *= 0.5; const { colors } = formatGradient(color); const grd = ctx.createRadialGradient(centerX, centerY, r * (gradientRadius || 0), centerX, centerY, r); colors.forEach((stop) => { grd.addColorStop(stop.i, stop.color); }); return grd; } function getBkGradient(ctx, pen) { const { x, y, ex, width, height, center } = pen.calculative.worldRect; let points = [ { x: ex, y: y + height / 2 }, { x: x, y: y + height / 2 }, ]; let color = pen.calculative.gradientColors; if (pen.calculative.checked) { color = pen.calculative.onGradientColors; } const { angle, colors } = formatGradient(color); let r = getGradientR(angle, width, height); points.forEach((point) => { rotatePoint(point, angle, center); }); return getLinearGradient(ctx, points, colors, r); } function getTextRadialGradient(ctx, pen) { const { worldRect, textGradientColors } = pen.calculative; if (!textGradientColors) { return; } const { width, height, center } = worldRect; const { x: centerX, y: centerY } = center; let r = width; if (r < height) { r = height; } r *= 0.5; const { colors } = formatGradient(textGradientColors); const grd = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, r); colors.forEach((stop) => { grd.addColorStop(stop.i, stop.color); }); return grd; } function getTextGradient(ctx, pen) { !pen.calculative.textDrawRect && calcTextDrawRect(ctx, pen); calcCenter(pen.calculative.textDrawRect); const { x, y, ex, width, height, center } = pen.calculative.textDrawRect; let points = [ { x: ex, y: y + height / 2 }, { x: x, y: y + height / 2 }, ]; const { angle, colors } = formatGradient(pen.calculative.textGradientColors); let r = getGradientR(angle, width, height); points.forEach((point) => { rotatePoint(point, angle, center); }); return getLinearGradient(ctx, points, colors, r); } function getGradientR(angle, width, height) { const dividAngle = (Math.atan(height / width) / Math.PI) * 180; let calculateAngle = (angle - 90) % 360; let r = 0; if ((calculateAngle > dividAngle && calculateAngle < 180 - dividAngle) || (calculateAngle > 180 + dividAngle && calculateAngle < 360 - dividAngle) || calculateAngle < 0) { //根据高计算 if (calculateAngle > 270) { calculateAngle = 360 - calculateAngle; } else if (calculateAngle > 180) { calculateAngle = calculateAngle - 180; } else if (calculateAngle > 90) { calculateAngle = 180 - calculateAngle; } r = Math.abs(height / Math.sin((calculateAngle / 180) * Math.PI) / 2); } else { //根据宽计算 if (calculateAngle > 270) { calculateAngle = 360 - calculateAngle; } else if (calculateAngle > 180) { calculateAngle = calculateAngle - 180; } else if (calculateAngle > 90) { calculateAngle = 180 - calculateAngle; } r = Math.abs(width / Math.cos((calculateAngle / 180) * Math.PI) / 2); } return r; } function formatGradient(color) { if (typeof color == 'string' && color.startsWith('linear-gradient')) { let arr = color.slice(16, -2).split('deg,'); if (arr.length > 1) { let _arr = arr[1].split('%,'); const colors = []; _arr.forEach((stap) => { if (/rgba?/.test(stap)) { let _arr = stap.split(') '); colors.push({ color: rgbaToHex(_arr[0] + ')'), i: parseFloat(_arr[1]) / 100, }); } else { let _arr = stap.split(' '); if (_arr.length > 2) { colors.push({ color: _arr[1], i: parseFloat(_arr[2]) / 100, }); } else { colors.push({ color: _arr[0], i: parseFloat(_arr[1]) / 100, }); } } }); return { angle: parseFloat(arr[0]), colors, }; } else { return { angle: parseFloat(arr[0]), colors: [], }; } } else { return { angle: 0, colors: [], }; } } function rgbaToHex(value) { if (/rgba?/.test(value)) { let array = value.split(','); //不符合rgb或rgb规则直接return if (array.length < 3) return ''; value = '#'; for (let i = 0, color; (color = array[i++]);) { if (i < 4) { //前三位转换成16进制 color = parseInt(color.replace(/[^\d]/gi, ''), 10).toString(16); value += color.length == 1 ? '0' + color : color; } else { //rgba的透明度转换成16进制 color = color.replace(')', ''); let colorA = parseInt(color * 255 + ''); let colorAHex = colorA.toString(16); colorAHex = colorAHex.length === 2 ? colorAHex : '0' + colorAHex; value += colorAHex; } } value = value.toUpperCase(); } return value; } function getLineGradient(ctx, pen) { const { x, y, ex, width, height, center } = pen.calculative.worldRect; let points = [ { x: ex, y: y + height / 2 }, { x: x, y: y + height / 2 }, ]; const { angle, colors } = formatGradient(pen.calculative.lineGradientColors); let r = getGradientR(angle, width, height); points.forEach((point) => { rotatePoint(point, angle, center); }); return getLinearGradient(ctx, points, colors, r); } function getLinearGradient(ctx, points, colors, radius) { let arr = getLinearGradientPoints(points[0].x, points[0].y, points[1].x, points[1].y, radius); let gradient = ctx.createLinearGradient(arr[0], arr[1], arr[2], arr[3]); colors.forEach((stop) => { gradient.addColorStop(stop.i, stop.color); }); return gradient; } function drawLinearGradientLine(ctx, pen, points) { let colors = []; if (pen.calculative.gradientColorStop) { colors = pen.calculative.gradientColorStop; } else { colors = formatGradient(pen.calculative.lineGradientColors).colors; pen.calculative.gradientColorStop = colors; } ctx.strokeStyle = getLinearGradient(ctx, points, colors, pen.calculative.lineWidth / 2); ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); ctx.lineTo(points[1].x, points[1].y); ctx.stroke(); } function ctxDrawLinearGradientPath(ctx, pen) { const anchors = pen.calculative.worldAnchors; let smoothLenth = pen.calculative.lineWidth * (pen.calculative.gradientSmooth || pen.calculative.lineSmooth || 0); for (let i = 0; i < anchors.length - 1; i++) { if ((pen.lineName === 'curve' || pen.lineName === 'mind') && anchors[i].curvePoints) { if (i > 0) { let lastCurvePoints = anchors[i - 1].curvePoints; if (lastCurvePoints) { //上一个存在锚点 smoothTransition(ctx, pen, smoothLenth, lastCurvePoints[lastCurvePoints.length - 1], anchors[i], anchors[i].curvePoints[0]); } else { smoothTransition(ctx, pen, smoothLenth, anchors[i - 1], anchors[i], anchors[i].curvePoints[0]); } //获取当前相对于0的位置 let next = getSmoothAdjacent(smoothLenth, anchors[i], anchors[i].curvePoints[0]); drawLinearGradientLine(ctx, pen, [next, anchors[i].curvePoints[1]]); } else { drawLinearGradientLine(ctx, pen, [ anchors[i], anchors[i].curvePoints[0], ]); drawLinearGradientLine(ctx, pen, [ anchors[i].curvePoints[0], anchors[i].curvePoints[1], ]); } let len = anchors[i].curvePoints.length - 1; for (let j = 1; j < len; j++) { drawLinearGradientLine(ctx, pen, [ anchors[i].curvePoints[j], anchors[i].curvePoints[j + 1], ]); } let last = getSmoothAdjacent(smoothLenth, anchors[i + 1], anchors[i].curvePoints[len]); drawLinearGradientLine(ctx, pen, [anchors[i].curvePoints[len], last]); } else { let _next = anchors[i]; let _last = anchors[i + 1]; if (i > 0 && i < anchors.length - 1) { //有突兀的地方 let lastCurvePoints = anchors[i - 1].curvePoints; if (lastCurvePoints) { smoothTransition(ctx, pen, smoothLenth, lastCurvePoints[lastCurvePoints.length - 1], anchors[i], anchors[i + 1]); } else { smoothTransition(ctx, pen, smoothLenth, anchors[i - 1], anchors[i], anchors[i + 1]); } } if (i > 0 && i < anchors.length - 1) { _next = getSmoothAdjacent(smoothLenth, anchors[i], anchors[i + 1]); } if (i < anchors.length - 2) { _last = getSmoothAdjacent(smoothLenth, anchors[i + 1], anchors[i]); } let flag = false; if (i === 0) { if (pen.fromLineCap && pen.fromLineCap !== 'butt') { ctx.save(); flag = true; ctx.lineCap = pen.fromLineCap; } } if (i !== 0 && i === anchors.length - 2) { if (pen.toLineCap && pen.toLineCap !== 'butt') { ctx.save(); flag = true; ctx.lineCap = pen.toLineCap; } } drawLinearGradientLine(ctx, pen, [_next, _last]); if (flag) { ctx.restore(); } if (anchors.length === 2 && i === 0) { ctx.save(); flag = true; ctx.lineCap = pen.toLineCap; let _y = 0.1; let _x = 0.1; if (_next.x - _last.x === 0) { _x = 0; } else { _y = ((_next.y - _last.y) / (_next.x - _last.x)) * 0.1; } drawLinearGradientLine(ctx, pen, [ { x: _last.x - _x, y: _last.y - _y }, _last, ]); ctx.restore(); } } } } function getSmoothAdjacent(smoothLenth, p1, p2) { let nexLength = Math.sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)); if (nexLength === 0) { return { x: p1.x, y: p1.y, }; } if (smoothLenth < nexLength) { return { x: p1.x + ((p2.x - p1.x) * smoothLenth) / nexLength, y: p1.y + ((p2.y - p1.y) * smoothLenth) / nexLength, }; } else { return { x: p1.x + (p2.x - p1.x) / nexLength / 2, y: p1.y + (p2.y - p1.y) / nexLength / 2, }; } } function smoothTransition(ctx, pen, smoothLenth, p1, p2, p3) { let last = getSmoothAdjacent(smoothLenth, p2, p1); let next = getSmoothAdjacent(smoothLenth, p2, p3); let contrlPoint = { x: p2.x, y: p2.y }; let points = getBezierPoints(pen.calculative.canvas.store.data.smoothNum || 20, last, contrlPoint, next); for (let k = 0; k < points.length - 1; k++) { drawLinearGradientLine(ctx, pen, [ { x: points[k].x, y: points[k].y, }, { x: points[k + 1].x, y: points[k + 1].y, }, ]); } } function smoothAnimateTransition(ctx, smoothLenth, p2, p3) { let next = getSmoothAdjacent(smoothLenth, p2, p3); let contrlPoint = { x: p2.x, y: p2.y }; ctx.quadraticCurveTo(contrlPoint.x, contrlPoint.y, next.x, next.y); } export function getGradientAnimatePath(pen) { const anchors = pen.calculative.worldAnchors; let smoothLenth = pen.calculative.lineWidth * (pen.calculative.gradientSmooth || pen.calculative.lineSmooth || 0); //只创建一次 const _path = new Path2D(); for (let i = 0; i < anchors.length - 1; i++) { let _next = anchors[i]; let _last = anchors[i + 1]; if (i == 0) { _path.moveTo(anchors[i].x, anchors[i].y); } if (i > 0 && i < anchors.length - 1) { //有突兀的地方 let lastCurvePoints = anchors[i - 1].curvePoints; // const path = new Path2D(); if (lastCurvePoints) { smoothAnimateTransition(_path, smoothLenth, anchors[i], anchors[i + 1]); } else { smoothAnimateTransition(_path, smoothLenth, anchors[i], anchors[i + 1]); } } if (i > 0 && i < anchors.length - 1) { _next = getSmoothAdjacent(smoothLenth, anchors[i], anchors[i + 1]); } if (i < anchors.length - 2) { _last = getSmoothAdjacent(smoothLenth, anchors[i + 1], anchors[i]); } _path.lineTo(_last.x, _last.y); } return _path; } function getAngle(p1, p2, p3) { let a = { x: 0, y: 0 }, b = { x: 0, y: 0 }; a.x = p1.x - p2.x; a.y = p1.y - p2.y; b.x = p3.x - p2.x; b.y = p3.y - p2.y; return ((Math.acos((a.x * b.x + a.y * b.y) / (Math.sqrt(a.x * a.x + a.y * a.y) * Math.sqrt(b.x * b.x + b.y * b.y))) / Math.PI) * 180); } function getBezierPoints(num = 100, p1, p2, p3, p4) { let func = null; const points = []; if (!p3 && !p4) { func = oneBezier; } else if (p3 && !p4) { func = twoBezier; } else if (p3 && p4) { func = threeBezier; } for (let i = 0; i < num; i++) { points.push(func(i / num, p1, p2, p3, p4)); } if (p4) { points.push(p4); } else if (p3) { points.push(p3); } return points; } /** * @desc 一阶贝塞尔 * @param t 当前百分比 * @param p1 起点坐标 * @param p2 终点坐标 */ function oneBezier(t, p1, p2) { const { x: x1, y: y1 } = p1; const { x: x2, y: y2 } = p2; let x = x1 + (x2 - x1) * t; let y = y1 + (y2 - y1) * t; return { x, y }; } /** * @desc 二阶贝塞尔 * @param t 当前百分比 * @param p1 起点坐标 * @param p2 终点坐标 * @param cp 控制点 */ function twoBezier(t, p1, cp, p2) { const { x: x1, y: y1 } = p1; const { x: cx, y: cy } = cp; const { x: x2, y: y2 } = p2; let x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2; let y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2; return { x, y }; } /** * @desc 三阶贝塞尔 * @param t 当前百分比 * @param p1 起点坐标 * @param p2 终点坐标 * @param cp1 控制点1 * @param cp2 控制点2 */ function threeBezier(t, p1, cp1, cp2, p2) { const { x: x1, y: y1 } = p1; const { x: x2, y: y2 } = p2; const { x: cx1, y: cy1 } = cp1; const { x: cx2, y: cy2 } = cp2; let x = x1 * (1 - t) * (1 - t) * (1 - t) + 3 * cx1 * t * (1 - t) * (1 - t) + 3 * cx2 * t * t * (1 - t) + x2 * t * t * t; let y = y1 * (1 - t) * (1 - t) * (1 - t) + 3 * cy1 * t * (1 - t) * (1 - t) + 3 * cy2 * t * t * (1 - t) + y2 * t * t * t; return { x, y }; } function strokeLinearGradient(ctx, pen) { const { worldRect, lineGradientFromColor, lineGradientToColor, lineGradientAngle, } = pen.calculative; return linearGradient(ctx, worldRect, lineGradientFromColor, lineGradientToColor, lineGradientAngle); } /** * 避免副作用,把创建好后的线性渐变对象返回出来 * @param ctx 画布绘制对象 * @param worldRect 世界坐标 * @returns 线性渐变 */ function linearGradient(ctx, worldRect, fromColor, toColor, angle) { if (!fromColor || !toColor) { return; } const { x, y, center, ex, ey } = worldRect; const from = { x, y: center.y, }; const to = { x: ex, y: center.y, }; if (angle % 90 === 0 && angle % 180) { from.x = center.x; to.x = center.x; if (angle % 270) { from.y = y; to.y = ey; } else { from.y = ey; to.y = y; } } else if (angle) { rotatePoint(from, angle, worldRect.center); rotatePoint(to, angle, worldRect.center); } // contributor: https://github.com/sunnyguohua/meta2d const grd = ctx.createLinearGradient(from.x, from.y, to.x, to.y); grd.addColorStop(0, fromColor); grd.addColorStop(1, toColor); return grd; } /** * 根据图片的宽高, imageRatio iconAlign 来获取图片的实际位置 * @param pen 画笔 */ function getImagePosition(pen) { const { worldIconRect: rect, iconWidth, iconHeight, imgNaturalWidth, imgNaturalHeight, worldRect } = pen.calculative; if (!rect) { return { x: worldRect.x, y: worldRect.y, width: worldRect.width || imgNaturalWidth || pen.calculative.img.naturalWidth, height: worldRect.height || imgNaturalHeight || pen.calculative.img.naturalHeight, }; } ; let { x, y, width: w, height: h } = rect; if (iconWidth) { w = iconWidth; } if (iconHeight) { h = iconHeight; } if (imgNaturalWidth && imgNaturalHeight && pen.imageRatio) { const scaleW = rect.width / imgNaturalWidth; const scaleH = rect.height / imgNaturalHeight; const scaleMin = Math.min(scaleW, scaleH); const wDivideH = imgNaturalWidth / imgNaturalHeight; if (iconWidth) { h = iconWidth / wDivideH; } else if (iconHeight) { w = iconHeight * wDivideH; } else { w = scaleMin * imgNaturalWidth; h = scaleMin * imgNaturalHeight; } } x += (rect.width - w) / 2; y += (rect.height - h) / 2; switch (pen.iconAlign) { case 'top': y = rect.y; break; case 'bottom': y = rect.ey - h; break; case 'left': x = rect.x; break; case 'right': x = rect.ex - w; break; case 'left-top': x = rect.x; y = rect.y; break; case 'right-top': x = rect.ex - w; y = rect.y; break; case 'left-bottom': x = rect.x; y = rect.ey - h; break; case 'right-bottom': x = rect.ex - w; y = rect.ey - h; break; } return { x, y, width: w, height: h, }; } export function drawImage(ctx, pen) { const { x, y, width, height } = getImagePosition(pen); const { worldIconRect, iconRotate, img } = pen.calculative; ctx.filter = pen.filter; if (iconRotate) { const { x: centerX, y: centerY } = worldIconRect.center; ctx.translate(centerX, centerY); ctx.rotate((iconRotate * Math.PI) / 180); ctx.translate(-centerX, -centerY); } if (pen.imageRadius) { ctx.save(); let wr = pen.calculative.imageRadius || 0, hr = wr; const { x: _x, y: _y, width: w, height: h, ex, ey, } = pen.calculative.worldRect; if (wr < 1) { wr = w * wr; hr = h * hr; } let r = wr < hr ? wr : hr; if (w < 2 * r) { r = w / 2; } if (h < 2 * r) { r = h / 2; } ctx.beginPath(); ctx.moveTo(_x + r, _y); ctx.arcTo(ex, _y, ex, ey, r); ctx.arcTo(ex, ey, _x, ey, r); ctx.arcTo(_x, ey, _x, _y, r); ctx.arcTo(_x, _y, ex, _y, r); ctx.clip(); ctx.drawImage(img, x, y, width, height); ctx.restore(); } else { let _y = y; let offsety = 0; if (pen.thumbImg) { // 缩略图 宽度充满 高度居中绘制 let _width = img.naturalWidth; let _height = img.naturalHeight; offsety = (height / width * _width - _height) / 2; if (height - 2 * offsety < 0) { offsety = (height - _height / _width * width) / 2; } _y = y + offsety; } ctx.drawImage(img, x, _y, width, height - 2 * offsety); } } /** * 获取文字颜色, textColor 优先其次 color */ export function getTextColor(pen, store) { const { textColor, color } = pen.calculative; const { styles } = store; return (textColor || color || styles.textColor || styles.color); } function drawText(ctx, pen) { const { fontStyle, fontWeight, fontSize, fontFamily, lineHeight, text, hiddenText, canvas, textHasShadow, textBackground, textType, } = pen.calculative; if (pen.input && isEmptyText(pen.text) && !(pen.calculative.canvas.inputDiv.dataset.penId === pen.id) && !pen.onShowInput) { ctx.save(); ctx.font = getFont({ fontStyle, fontWeight, fontFamily: fontFamily, fontSize, lineHeight, }); ctx.textBaseline = 'top'; ctx.fillStyle = pen.placeholderColor || '#c0c0c0'; const textLineWidth = ctx.measureText(pen.placeholder || '请输入').width; const rect = pen.calculative.worldTextRect; let x = 0; let y = (rect.height - pen.calculative.fontSize) / 2; if (pen.textAlign === 'center') { x = (rect.width - textLineWidth) / 2; } else if (pen.textAlign === 'right') { x = rect.width - textLineWidth; } if (pen.textBaseline === 'top') { y = 0; } else if (pen.textBaseline === 'bottom') { y = rect.height - pen.calculative.fontSize; } ctx.fillText(pen.placeholder || '请输入', rect.x + x, rect.y + y); ctx.restore(); } if (isEmptyText(text) || hiddenText || pen.hiddenText) { return; } const store = canvas.store; ctx.save(); if (!textHasShadow) { ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; } let fill = undefined; if (pen.calculative.disabled) { fill = pen.disabledTextColor || pen.disabledColor || pSBC(0.4, getTextColor(pen, store)); } else if (pen.calculative.hover) { fill = pen.hoverTextColor || pen.hoverColor || store.styles.hoverColor; } else if (pen.calculative.active) { fill = pen.activeTextColor || pen.activeColor || store.styles.activeColor; } let gradient = undefined; if (textType === Gradient.Linear) { gradient = getTextGradient(ctx, pen); } else if (textType === Gradient.Radial) { gradient = getTextRadialGradient(ctx, pen); } ctx.fillStyle = fill || gradient || getTextColor(pen, store); ctx.font = getFont({ fontStyle, fontWeight, fontFamily: fontFamily || store.options.fontFamily, fontSize, lineHeight, }); (!pen.calculative.textDrawRect || pen.calculative.fontsChecked) && calcTextDrawRect(ctx, pen); const { x: drawRectX, y: drawRectY, width, height, } = pen.calculative.textDrawRect; if (textBackground) { ctx.save(); ctx.fillStyle = textBackground; ctx.fillRect(drawRectX, drawRectY, width, height); ctx.restore(); } const y = 0.55; const textAlign = pen.textAlign || store.options.textAlign; const oneRowHeight = fontSize * lineHeight; pen.calculative.textLines && pen.calculative.textLines.forEach((text, i) => { const textLineWidth = pen.calculative.textLineWidths[i]; let x = 0; if (textAlign === 'center') { x = (width - textLineWidth) / 2; } else if (textAlign === 'right') { x = width - textLineWidth; } // 字间距 if (pen.letterSpacing) { fillTextWithSpacing(ctx, text, drawRectX + x, drawRectY + (i + y) * oneRowHeight, pen.calculative.letterSpacing); } else { ctx.fillText(text, drawRectX + x, drawRectY + (i + y) * oneRowHeight); } // 下划线 const { textDecorationColor, textDecorationDash, textDecoration } = pen; if (textDecoration) { drawUnderLine(ctx, { x: drawRectX + x, y: drawRectY + (i + y) * oneRowHeight, width: textLineWidth, }, { textDecorationColor, textDecorationDash, fontSize }); } // 删除线 const { textStrickoutColor, textStrickoutDash, textStrickout } = pen; if (textStrickout) { drawStrickout(ctx, { x: drawRectX + x, y: drawRectY + (i + y) * oneRowHeight, width: textLineWidth, }, { textStrickoutColor, textStrickoutDash, fontSize }); } }); ctx.restore(); } function fillTextWithSpacing(ctx, text, x, y, spacing = 0) { if (spacing === 0) { ctx.fillText(text, x, y); return; } let totalWidth = 0; for (let i = 0; i < text.length; i++) { ctx.fillText(text[i], x + totalWidth, y); totalWidth += ctx.measureText(text[i]).width + spacing; } } function drawUnderLine(ctx, location, config) { const { textDecorationColor, textDecorationDash, fontSize } = config; let { x, y, width } = location; switch (ctx.textBaseline) { case 'top': y += fontSize; break; case 'middle': y += fontSize / 2; break; } ctx.save(); ctx.beginPath(); ctx.strokeStyle = textDecorationColor ? textDecorationColor : ctx.fillStyle; ctx.lineWidth = 1; ctx.moveTo(x, y); ctx.setLineDash(textDecorationDash || []); ctx.lineTo(x + width, y); ctx.stroke(); ctx.restore(); } function drawStrickout(ctx, location, config) { const { textStrickoutColor, textStrickoutDash, fontSize } = config; let { x, y, width } = location; switch (ctx.textBaseline) { case 'top': y += fontSize / 2; break; case 'bottom': y -= fontSize / 2; break; } ctx.save(); ctx.beginPath(); ctx.strokeStyle = textStrickoutColor ? textStrickoutColor : ctx.fillStyle; ctx.lineWidth = 1; ctx.moveTo(x, y); ctx.setLineDash(textStrickoutDash || []); ctx.lineTo(x + width, y); ctx.stroke(); ctx.restore(); } function drawFillText(ctx, pen, text) { if (text == undefined) { return; } const { fontStyle, fontWeight, fontSize, fontFamily, lineHeight, canvas } = pen.calculative; const store = canvas.store; ctx.save(); let fill = undefined; if (pen.calculative.hover) { fill = pen.hoverTextColor || pen.hoverColor || store.styles.hoverColor; } else if (pen.calculative.active) { fill = pen.activeTextColor || pen.activeColor || store.styles.activeColor; } ctx.fillStyle = fill || getTextColor(pen, store); ctx.font = getFont({ fontStyle, fontWeight, fontFamily: fontFamily || store.options.fontFamily, fontSize, lineHeight, }); const w = ctx.measureText(text).width; let t; let prev; for (const anchor of pen.calculative.worldAnchors) { if (!prev) { prev = anchor; continue; } const dis = distance(prev, anchor); const n = Math.floor(dis / w); t = ''; for (let i = 0; i < n; i++) { t += text; } const angle = calcRotate(prev, anchor) - 270; ctx.save(); if (angle % 360 !== 0) { const { x, y } = prev; ctx.translate(x, y); let rotate = (angle * Math.PI) / 180; ctx.rotate(rotate); ctx.translate(-x, -y); } ctx.fillText(t, prev.x, prev.y + lineHeight / 2); ctx.restore(); prev = anchor; } ctx.restore(); } export function drawIcon(ctx, pen) { const store = pen.calculative.canvas.store; ctx.save(); ctx.shadowColor = ''; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; const iconRect = pen.calculative.worldIconRect; let x = iconRect.x + iconRect.width / 2; let y = iconRect.y + iconRect.height / 2; switch (pen.iconAlign) { case 'top': y = iconRect.y; ctx.textBaseline = 'top'; break; case 'bottom': y = iconRect.ey; ctx.textBaseline = 'bottom'; break; case 'left': x = iconRect.x; ctx.textAlign = 'left'; break; case 'right': x = iconRect.ex; ctx.textAlign = 'right'; break; case 'left-top': x = iconRect.x; y = iconRect.y; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; break; case 'right-top': x = iconRect.ex; y = iconRect.y; ctx.textAlign = 'right'; ctx.textBaseline = 'top'; break; case 'left-bottom': x = iconRect.x; y = iconRect.ey; ctx.textAlign = 'left'; ctx.textBaseline = 'bottom'; break; case 'right-bottom': x = iconRect.ex; y = iconRect.ey; ctx.textAlign = 'right'; ctx.textBaseline = 'bottom'; break; } const fontWeight = pen.calculative.iconWeight; let fontSize = undefined; const fontFamily = pen.calculative.iconFamily; if (pen.calculative.iconSize > 0) { fontSize = pen.calculative.iconSize; } else if (iconRect.width > iconRect.height) { fontSize = iconRect.height; } else { fontSize = iconRect.width; } ctx.font = getFont({ fontSize, fontWeight, fontFamily, }); ctx.fillStyle = pen.calculative.iconColor || getTextColor(pen, store); if (pen.calculative.iconRotate) { ctx.translate(iconRect.center.x, iconRect.center.y); ctx.rotate((pen.calculative.iconRotate * Math.PI) / 180); ctx.translate(-iconRect.center.x, -iconRect.center.y); } ctx.beginPath(); ctx.fillText(pen.calculative.icon, x, y); ctx.restore(); } export function drawDropdown(ctx, pen) { if (!pen.input) { return; } const scale = pen.calculative.canvas.store.data.scale; const inputPenId = pen.calculative.canvas.inputDiv.dataset.penId; const { x, y, width, height } = pen.calculative.worldRect; ctx.save(); ctx.beginPath(); if (pen.id === inputPenId) { ctx.moveTo(x + width - 20 * scale, y + height / 2 + 2 * scale); ctx.lineTo(x + width - 14 * scale, y + height / 2 - 4 * scale); ctx.lineTo(x + width - 8 * scale, y + height / 2 + 2 * scale); } else { ctx.moveTo(x + width - 20 * scale, y + height / 2 - 4 * scale); ctx.lineTo(x + width - 14 * scale, y + height / 2 + 2 * scale); ctx.lineTo(x + width - 8 * scale, y + height / 2 - 4 * scale); } ctx.stroke(); ctx.restore(); } /** * canvas2svg 中对 font 的解析规则比 canvas 中简单,能识别的类型很少 * @returns ctx.font */ export function getFont({ fontStyle = 'normal', textDecoration = 'normal', fontWeight = 'normal', fontSize = 12, fontFamily = 'Arial', lineHeight = 1, // TODO: lineHeight 默认值待测试 } = {}) { return `${fontStyle} ${textDecoration} ${fontWeight} ${fontSize}px/${lineHeight} ${fontFamily}`; } export function ctxFlip(ctx, pen) { // worldRect 可能为 undefined const { x, ex, y, ey } = pen.calculative.worldRect || {}; if (pen.calculative.flipX) { ctx.translate(x + ex, 0); ctx.scale(-1, 1); } if (pen.calculative.flipY) { ctx.translate(0, y + ey); ctx.scale(1, -1); } } export function ctxRotate(ctx, pen, noFlip = false) { if (pen.parentId && pen.rotateByRoot) { let rootParent = getParent(pen, true); if (rootParent) { const { x, y } = rootParent.calculative.worldRect.pivot || rootParent.calculative.worldRect.center; ctx.translate(x, y); let rotate = (rootParent.calculative.rotate * Math.PI) / 180; // 目前只有水平和垂直翻转,都需要 * -1 if (!noFlip) { if (rootParent.calculative.flipX) { rotate *= -1; } if (rootParent.calculative.flipY) { rotate *= -1; } } ctx.rotate(rotate); ctx.translate(-x, -y); } } else { const { x, y } = pen.calculative.worldRect.pivot || pen.calculative.worldRect.center; ctx.translate(x, y); let rotate = (pen.calculative.rotate * Math.PI) / 180; // 目前只有水平和垂直翻转,都需要 * -1 if (!noFlip) { if (pen.calculative.flipX) { rotate *= -1; } if (pen.calculative.flipY) { rotate *= -1; } } ctx.rotate(rotate); ctx.translate(-x, -y); } } export function renderPen(ctx, pen, download) { ctx.save(); ctx.translate(0.5, 0.5); ctx.beginPath(); drawFilter(ctx, pen); const store = pen.calculative.canvas.store; const textFlip = pen.textFlip || store.options.textFlip; const textRotate = pen.textRotate || store.options.textRotate; if (!textFlip || !textRotate) { ctx.save(); } ctxFlip(ctx, pen); if (pen.rotateByRoot || (pen.calculative.rotate && pen.name !== LINE)) { ctxRotate(ctx, pen); } if (pen.calculative.lineWidth > 1 || download) { ctx.lineWidth = pen.calculative.lineWidth; } inspectRect(ctx, store, pen); // 审查 rect let fill; // 该变量控制在 hover active 状态下的节点是否设置填充颜色 // let setBack = true; let lineGradientFlag = false; let _stroke = undefined; if (pen.calculative.disabled) { _stroke = pen.disabledColor || store.styles.disabledColor || pSBC(0.4, pen.calculative.color || store.styles.color); fill = pen.disabledBackground || store.styles.disabledBackground || pSBC(0.4, pen.calculative.background || store.styles.penBackground); } else if (pen.mouseDownValid && pen.calculative.mouseDown) { _stroke = pen.mouseDownColor || pSBC(-0.4, pen.calculative.color || store.styles.color); fill = pen.mouseDownBackground || pSBC(-0.4, pen.calculative.background || store.styles.penBackground); } else if (pen.switch && pen.calculative.checked) { if (!pen.calculative.bkType) { fill = pen.onBackground; } } else if (pen.calculative.hover) { _stroke = pen.hoverColor || store.styles.hoverColor; fill = pen.hoverBackground || store.styles.hoverBackground; // ctx.fillStyle = fill; // fill && (setBack = false); } else if (pen.calculative.active) { _stroke = pen.activeColor || store.styles.activeColor; fill = pen.activeBackground || store.styles.activeBackground; // ctx.fillStyle = fill; // fill && (setBack = false); } else if (pen.calculative.isDock) { if (pen.type === PenType.Line) { _stroke = store.styles.dockPenColor; } else { fill = rgba(store.styles.dockPenColor, 0.2); // ctx.fillStyle = fill; // fill && (setBack = false); } } // else { const strokeImg = pen.calculative.strokeImg; if (pen.calculative.strokeImage && strokeImg) { ctx.strokeStyle = _stroke || ctx.createPattern(strokeImg, REPEAT); // fill = true; } else { let stroke; // TODO: 线只有线性渐变 if (pen.calculative.strokeType) { if (pen.calculative.lineGradientColors) { if (pen.name === LINE) { lineGradientFlag = true; } else { if (pen.calculative.lineGradient) { stroke = pen.calculative.lineGradient; } else { stroke = getLineGradient(ctx, pen); pen.calculative.lineGradient = stroke; } } } else { stroke = strokeLinearGradient(ctx, pen); } } else { stroke = pen.calculative.color || (pen.type ? store.data.lineColor : '') || store.styles.color; } ctx.strokeStyle = _stroke || stroke; } // } //if (setBack) { const backgroundImg = pen.calculative.backgroundImg; if (pen.calculative.backgroundImage && backgroundImg) { ctx.fillStyle = fill || ctx.createPattern(backgroundImg, REPEAT); fill = true; } else { let back; if (pen.calculative.bkType === Gradient.Linear) { if (pen.calculative.gradientColors) { // if (!pen.type) { //连线不考虑渐进背景 if (pen.calculative.gradient) { //位置变化/放大缩小操作不会触发重新计算 back = pen.calculative.gradient; } else { back = getBkGradient(ctx, pen); pen.calculative.gradient = back; } // } } else { back = drawBkLinearGradient(ctx, pen); } } else if (pen.calculative.bkType === Gradient.Radial) { if (pen.calculative.gradientColors) { if (pen.calculative.radialGradient) { back = pen.calculative.radialGradient; } else { back = getBkRadialGradient(ctx, pen); pen.calculative.radialGradient = back; } } else { back = drawBkRadialGradient(ctx, pen); } } else { back = pen.calculative.background || store.styles.penBackground; } ctx.fillStyle = fill || back; fill = !!back; } // } setLineCap(ctx, pen); setLineJoin(ctx, pen); setGlobalAlpha(ctx, pen); if (pen.calculative.lineDash) { ctx.setLineDash(pen.calculative.lineDash.map((item) => item * pen.calculative.canvas.store.data.scale)); } if (pen.calculative.lineDashOffset) { ctx.lineDashOffset = pen.calculative.lineDashOffset; } if (pen.calculative.shadowColor) { ctx.shadowColor = pen.calculative.shadowColor; ctx.shadowOffsetX = pen.calculative.shadowOffsetX; ctx.shadowOffsetY = pen.calculative.shadowOffsetY; ctx.shadowBlur = pen.calculative.shadowBlur; } if (lineGradientFlag) { ctxDrawLinearGradientPath(ctx, pen); ctxDrawLinePath(true, ctx, pen, store); } else { ctxDrawPath(true, ctx, pen, store, fill); ctxDrawCanvas(ctx, pen); } if (!(pen.image && pen.calculative.img) && pen.calculative.icon) { drawIcon(ctx, pen); } if (pen.dropdownList) { drawDropdown(ctx, pen); } if (!textFlip || !textRotate) { ctx.restore(); } if (textFlip && !textRotate) { ctxFlip(ctx, pen); } if (!textFlip && textRotate) { if (pen.rotateByRoot || (pen.calculative.rotate && pen.name !== 'line')) { ctxRotate(ctx, pen, true); } } drawText(ctx, pen); if (pen.type === PenType.Line && pen.fillTexts?.length > 0) { for (const text of pen.fillTexts) { drawFillText(ctx, pen, text); } } ctx.restore(); } /** * 更改 ctx 的 lineCap 属性 */ export function setLineCap(ctx, pen) { const lineCap = pen.lineCap || (pen.type ? 'round' : 'square'); if (lineCap) { ctx.lineCap = lineCap; } else if (pen.type) { ctx.lineCap = 'round'; } } /** * 更改 ctx 的 lineJoin 属性 */ export function setLineJoin(ctx, pen) { const lineJoin = pen.lineJoin; if (lineJoin) { ctx.lineJoin = lineJoin; } else if (pen.type) { ctx.lineJoin = 'round'; } } /** * 通常用在下载 svg * canvas2svg 与 canvas ctx 设置 strokeStyle 表现不同 * 若设置值为 undefined ,canvas2svg 为空, canvas ctx 为上一个值 */ export function renderPenRaw(ctx, pen, rect, download) { ctx.save(); if (rect) { ctx.translate(-rect.x, -rect.y); } // for canvas2svg ctx.setAttrs?.(pen); // end let lineGradientFlag = false; const store = pen.calculative.canvas.store; const textFlip = pen.textFlip || store.options.textFlip; const textRotate = pen.textRotate || store.options.textRotate; ctx.beginPath(); if (!textFlip || !textRotate) { ctx.save(); } if (pen.calculative.flipX) { if (rect) { ctx.translate(pen.calculative.worldRect.x + pen.calculative.worldRect.ex, 0); } else { ctx.translate(pen.calculative.worldRect.x + pen.calculative.worldRect.ex, 0); } ctx.scale(-1, 1); } if (pen.calculative.flipY) { if (rect) { ctx.translate(0, pen.calculative.worldRect.y + pen.calculative.worldRect.ey); } else { ctx.translate(0, pen.calculative.worldRect.y + pen.calculative.worldRect.ey); } ctx.scale(1, -1); } if (pen.rotateByRoot || (pen.calculative.rotate && pen.name !== 'line')) { ctxRotate(ctx, pen); } if (pen.calculative.lineWidth > 1 || download) { ctx.lineWidth = pen.calculative.lineWidth; } let fill; if (pen.calculative.hover) { ctx.strokeStyle = pen.hoverColor || store.styles.hoverColor; ctx.fillStyle = pen.hoverBackground || store.styles.hoverBackground; fill = pen.hoverBackground || store.styles.hoverBackground; } else if (pen.calculative.active) { ctx.strokeStyle = pen.activeColor || store.styles.activeColor; ctx.fillStyle = pen.activeBackground || store.styles.activeBackground; fill = pen.activeBackground || store.styles.activeBackground; } else { if (pen.strokeImage) { if (pen.calculative.strokeImg) { ctx.strokeStyle = ctx.createPattern(pen.calculative.strokeImg, REPEAT); fill = true; } } else { let stroke; if (pen.calculative.strokeType && pen.calculative.lineGradientColors && pen.name === 'line') { lineGradientFlag = true; } else { stroke = pen.calculative.color || store.styles.color; //getGlobalColor(store); } ctx.strokeStyle = stroke; } if (pen.backgroundImage) { if (pen.calculative.backgroundImg) { ctx.fillStyle = ctx.createPattern(pen.calculative.backgroundImg, REPEAT); fill = true; } } else { ctx.fillStyle = pen.background; fill = !!pen.background; } } setLineCap(ctx, pen); setLineJoin(ctx, pen); setGlobalAlpha(ctx, pen); if (pen.calculative.lineDash) { ctx.setLineDash(pen.calculative.lineDash); } if (pen.calculative.lineDashOffset) { ctx.lineDashOffset = pen.calculative.lineDashOffset; } if (pen.calculative.shadowColor) { ctx.shadowColor = pen.calculative.shadowColor; ctx.shadowOffsetX = pen.calculative.shadowOffsetX; ctx.shadowOffsetY = pen.calculative.shadowOffsetY; ctx.shadowBlur = pen.calculative.shadowBlur; } if (lineGradientFlag) { ctxDrawLinearGradientPath(ctx, pen); ctxDrawLinePath(true, ctx, pen, store); } else { ctxDrawPath(false, ctx, pen, store, fill); ctxDrawCanvas(ctx, pen); } // renderPenRaw 用在 downloadPng svg , echarts 等图形需要 if (pen.calculative.img) { ctx.save(); ctx.shadowColor = ''; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; drawImage(ctx, pen); ctx.restore(); } else if (pen.calculative.icon) { drawIcon(ctx, pen); } if (pen.dropdownList) { drawDropdown(ctx, pen); } if (!textFlip || !textRotate) { ctx.restore(); } if (textFlip && !textRotate) { if (pen.calculative.flipX) { if (rect) { ctx.translate(pen.calculative.worldRect.x + pen.calculative.worldRect.ex, 0); } else { ctx.translate(pen.calculative.worldRect.x + pen.calculative.worldRect.ex