@meta2d/core
Version:
@meta2d/core: Powerful, Beautiful, Simple, Open - Web-Based 2D At Its Best .
1,530 lines • 146 kB
JavaScript
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';
import { TRANSPARENT_COLOR } from "../options";
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')) {
return {
angle: 0,
colors: [],
};
}
const start = color.indexOf('(');
const end = color.lastIndexOf(')');
if (start === -1 || end === -1 || end <= start) {
return { angle: 0, colors: [] };
}
const inner = color.slice(start + 1, end).trim();
const parts = splitByCommaRespectingParens(inner);
if (parts.length === 0) {
return { angle: 0, colors: [] };
}
let angle = 0;
let colorStartIndex = 0;
const firstPart = parts[0].trim();
if (firstPart.endsWith('deg')) {
angle = parseFloat(firstPart);
colorStartIndex = 1;
}
else if (firstPart.startsWith('to ')) {
angle = directionToAngle(firstPart);
colorStartIndex = 1;
}
const rawStops = [];
for (let i = colorStartIndex; i < parts.length; i++) {
const stop = parseColorStop(parts[i].trim());
if (stop) {
rawStops.push(stop);
}
}
if (rawStops.length === 0) {
return { angle, colors: [] };
}
fillMissingOffsets(rawStops);
const colors = rawStops.map((stop) => ({
color: stop.color,
i: stop.offset / 100,
}));
return { angle, colors };
}
function splitByCommaRespectingParens(str) {
const parts = [];
let current = '';
let depth = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (char === '(') {
depth++;
current += char;
}
else if (char === ')') {
depth--;
current += char;
}
else if (char === ',' && depth === 0) {
parts.push(current);
current = '';
}
else {
current += char;
}
}
if (current.trim()) {
parts.push(current);
}
return parts;
}
function parseColorStop(str) {
if (!str)
return null;
const percentMatch = str.match(/^(.+?)\s+(\d+(?:\.\d+)?%)\s*$/);
if (percentMatch) {
return {
color: normalizeColor(percentMatch[1].trim()),
offset: parseFloat(percentMatch[2]),
};
}
return { color: normalizeColor(str) };
}
function normalizeColor(color) {
if (/^rgba?\s*\(/.test(color)) {
return rgbaToHex(color);
}
return color;
}
function directionToAngle(dir) {
const map = {
'to top': 90,
'to right': 180,
'to bottom': 270,
'to left': 0,
'to top right': 135,
'to right top': 135,
'to bottom right': 225,
'to right bottom': 225,
'to bottom left': 315,
'to left bottom': 315,
'to top left': 45,
'to left top': 45,
};
return map[dir] ?? 0;
}
function fillMissingOffsets(stops) {
if (stops.length === 0)
return;
if (stops[0].offset == null) {
stops[0].offset = 0;
}
if (stops[stops.length - 1].offset == null) {
stops[stops.length - 1].offset = 100;
}
let i = 0;
while (i < stops.length) {
if (stops[i].offset == null) {
let prevIndex = i - 1;
let nextIndex = i + 1;
while (nextIndex < stops.length && stops[nextIndex].offset == null) {
nextIndex++;
}
const prevOffset = stops[prevIndex].offset;
const nextOffset = stops[nextIndex].offset;
const gap = nextIndex - prevIndex;
for (let j = 1; j < gap; j++) {
stops[prevIndex + j].offset = prevOffset + (nextOffset - prevOffset) * (j / gap);
}
i = nextIndex;
}
else {
i++;
}
}
}
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;
const backgroundStr = pen.calculative.background || '';
if (typeof backgroundStr === 'string' && backgroundStr.startsWith('linear-gradient')) {
//让background为linear开头的兼容到gradientColors
pen.calculative.gradientColors = backgroundStr;
pen.calculative.bkType = Gradient.Linear;
}
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.fillStyl