Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"dependencies": {
"@maptalks/feature-filter": "^1.3.0",
"@maptalks/function-type": "^1.3.1",
"colorin": "^0.6.0",
"frustum-intersects": "^0.1.0",
"lineclip": "^1.1.5",
"rbush": "^2.0.2",
Expand Down
182 changes: 177 additions & 5 deletions src/core/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ function getCubicControlPoints(x0, y0, x1, y1, x2, y2, x3, y3, smoothValue, t) {
}
}


function pathDistance(points: Array<Point>) {
if (points.length < 2) {
return 0;
}
let distance = 0;
for (let i = 1, len = points.length; i < len; i++) {
const p1 = points[i - 1], p2 = points[i];
distance += p1.distanceTo(p2);
}
return distance;
}

function getColorInMinStep(colorIn: any) {
if (isNumber(colorIn.minStep)) {
return colorIn.minStep;
}
const colors = colorIn.colors || [];
const len = colors.length;
const steps = [];
for (let i = 0; i < len; i++) {
steps[i] = colors[i][0];
}
steps.sort((a, b) => {
return a - b;
});
let min = Infinity;
for (let i = 1; i < len; i++) {
const step1 = steps[i - 1], step2 = steps[i];
const stepOffset = step2 - step1;
min = Math.min(min, stepOffset);
}
colorIn.minStep = min;
return min;

}

function getSegmentPercentPoint(p1: Point, p2: Point, percent: number) {
const x1 = p1.x, y1 = p1.y, x2 = p2.x, y2 = p2.y;
const dx = x2 - x1, dy = y2 - y1;
return new Point(x1 + dx * percent, y1 + dy * percent);
}

const Canvas = {
getCanvas2DContext(canvas: HTMLCanvasElement) {
return canvas.getContext('2d', { willReadFrequently: true });
Expand Down Expand Up @@ -537,6 +580,113 @@ const Canvas = {
}
},

/**
* mock gradient path
* 利用颜色插值来模拟渐变的Path
* @param ctx
* @param points
* @param lineDashArray
* @param lineOpacity
* @param isRing
* @returns
*/
_gradientPath(ctx: CanvasRenderingContext2D, points, lineDashArray, lineOpacity, isRing = false) {
if (!isNumber(lineOpacity)) {
lineOpacity = 1;
}
if (hitTesting) {
lineOpacity = 1;
}
if (lineOpacity === 0 || ctx.lineWidth === 0) {
return;
}
const alpha = ctx.globalAlpha;
ctx.globalAlpha *= lineOpacity;
const colorIn = ctx.lineColorIn;
//颜色插值的最小步数
const minStep = getColorInMinStep(colorIn);
const distance = pathDistance(points);
let step = 0;
let preColor, color;
let preX, preY, currentX, currentY, nextPoint;

const [r, g, b, a] = colorIn.getColor(0);
preColor = `rgba(${r},${g},${b},${a})`;

const firstPoint = points[0];
preX = firstPoint.x;
preY = firstPoint.y;
//check polygon ring
if (isRing) {
const len = points.length;
const lastPoint = points[len - 1];
if (!firstPoint.equals(lastPoint)) {
points.push(firstPoint);
}
}

const dashArrayEnable = lineDashArray && Array.isArray(lineDashArray) && lineDashArray.length > 1;

const drawSegment = () => {
//绘制底色,来掩盖多个segment绘制接头的锯齿
if (!dashArrayEnable && nextPoint) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(preX, preY);
ctx.lineTo(currentX, currentY);
ctx.lineTo(nextPoint.x, nextPoint.y);
ctx.stroke();
}
const grad = ctx.createLinearGradient(preX, preY, currentX, currentY);
grad.addColorStop(0, preColor);
grad.addColorStop(1, color);
ctx.strokeStyle = grad;
ctx.beginPath();
ctx.moveTo(preX, preY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
preColor = color;
preX = currentX;
preY = currentY;
}

for (let i = 1, len = points.length; i < len; i++) {
const prePoint = points[i - 1], currentPoint = points[i];
nextPoint = points[i + 1];
const x = currentPoint.x, y = currentPoint.y;
const dis = currentPoint.distanceTo(prePoint);
const percent = dis / distance;

//segment的步数小于minStep
if (percent <= minStep) {
const [r, g, b, a] = colorIn.getColor(step + percent);
color = `rgba(${r},${g},${b},${a})`;
currentX = x;
currentY = y;
drawSegment();
} else {
//拆分segment
const segments = Math.ceil(percent / minStep);
nextPoint = currentPoint;
for (let n = 1; n <= segments; n++) {
const tempStep = Math.min((n * minStep), percent);
const [r, g, b, a] = colorIn.getColor(step + tempStep);
color = `rgba(${r},${g},${b},${a})`;
if (color === preColor) {
continue;
}
const point = getSegmentPercentPoint(prePoint, currentPoint, tempStep / percent);
currentX = point.x;
currentY = point.y;
drawSegment();
}
}
step += percent;
}
ctx.globalAlpha = alpha;
},


//@internal
_path(ctx, points, lineDashArray?, lineOpacity?, ignoreStrokePattern?) {
if (!isArrayHasData(points)) {
Expand Down Expand Up @@ -582,13 +732,18 @@ const Canvas = {
}
},

path(ctx, points, lineOpacity, fillOpacity?, lineDashArray?) {
path(ctx: CanvasRenderingContext2D, points, lineOpacity, fillOpacity?, lineDashArray?) {
if (!isArrayHasData(points)) {
return;
}
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
Canvas._path(ctx, points, lineDashArray, lineOpacity);

if (ctx.lineColorIn) {
this._gradientPath(ctx, points, lineDashArray, lineOpacity);
} else {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
Canvas._path(ctx, points, lineDashArray, lineOpacity);
}
Canvas._stroke(ctx, lineOpacity);
},

Expand Down Expand Up @@ -654,6 +809,8 @@ const Canvas = {
}

}
const lineColorIn = ctx.lineColorIn;
const lineWidth = ctx.lineWidth;
// function fillPolygon(points, i, op) {
// Canvas.fillCanvas(ctx, op, points[i][0].x, points[i][0].y);
// }
Expand All @@ -665,6 +822,10 @@ const Canvas = {
if (!isArrayHasData(points[i])) {
continue;
}
//渐变时忽略不在绘制storke
if (lineColorIn) {
ctx.lineWidth = 0.1;
}
Canvas._ring(ctx, points[i], null, 0, true);
op = fillOpacity;
if (i > 0) {
Expand All @@ -679,6 +840,10 @@ const Canvas = {
ctx.fillStyle = '#fff';
}
Canvas._stroke(ctx, 0);
ctx.lineWidth = lineWidth;
if (lineColorIn) {
Canvas._gradientPath(ctx, points, null, 0, true);
}
}
ctx.restore();
}
Expand All @@ -687,7 +852,9 @@ const Canvas = {
if (!isArrayHasData(points[i])) {
continue;
}

if (lineColorIn) {
ctx.lineWidth = 0.1;
}
if (smoothness) {
Canvas.paintSmoothLine(ctx, points[i], lineOpacity, smoothness, true);
ctx.closePath();
Expand All @@ -711,6 +878,10 @@ const Canvas = {
}
}
Canvas._stroke(ctx, lineOpacity);
ctx.lineWidth = lineWidth;
if (lineColorIn) {
Canvas._gradientPath(ctx, points[i], lineDashArray, lineOpacity, true);
}
}
//还原fillStyle
if (ctx.fillStyle !== fillStyle) {
Expand Down Expand Up @@ -1221,6 +1392,7 @@ function copyProperties(ctx: CanvasRenderingContext2D, savedCtx) {
ctx.shadowOffsetX = savedCtx.shadowOffsetX;
ctx.shadowOffsetY = savedCtx.shadowOffsetY;
ctx.strokeStyle = savedCtx.strokeStyle;
ctx.lineColorIn = savedCtx.lineColorIn;
}

function setLineDash(ctx: CanvasRenderingContext2D, lineDashArray: number[]) {
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/geometry/VectorRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,14 @@ const lineStringInclude = {
},

//@internal
_paintOn(ctx: CanvasRenderingContext2D, points: Point[], lineOpacity?: number, fillOpacity?: number, dasharray?: number[]) {
_paintOn(ctx: CanvasRenderingContext2D, points: Point[], lineOpacity?: number, fillOpacity?: number, dasharray?: number[], lineColorIn?: any) {
const r = isWithinPixel(this._painter);
if (r.within) {
Canvas.pixelRect(ctx, r.center, lineOpacity, fillOpacity);
} else if (this.options['smoothness']) {
Canvas.paintSmoothLine(ctx, points, lineOpacity, this.options['smoothness'], false, this._animIdx, this._animTailRatio);
} else {
ctx.lineColorIn = lineColorIn;
Canvas.path(ctx, points, lineOpacity, null, dasharray);
}
this._paintArrow(ctx, points, lineOpacity);
Expand Down Expand Up @@ -478,11 +479,12 @@ const polygonInclude = {
},

//@internal
_paintOn(ctx: CanvasRenderingContext2D, points: Point[], lineOpacity?: number, fillOpacity?: number, dasharray?: number[]) {
_paintOn(ctx: CanvasRenderingContext2D, points: Point[], lineOpacity?: number, fillOpacity?: number, dasharray?: number[], lineColorIn?: any) {
const r = isWithinPixel(this._painter);
if (r.within) {
Canvas.pixelRect(ctx, r.center, lineOpacity, fillOpacity);
} else {
ctx.lineColorIn = lineColorIn;
Canvas.polygon(ctx, points, lineOpacity, fillOpacity, dasharray, this.options['smoothness']);
}
return this._getRenderBBOX(ctx, points);
Expand Down
54 changes: 52 additions & 2 deletions src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PointExtent from '../../../geo/PointExtent';
import { Geometry } from '../../../geometry';
import Painter from '../Painter';
import CanvasSymbolizer from './CanvasSymbolizer';
import { ColorIn } from 'colorin';

const TEMP_COORD0 = new Coordinate(0, 0);
const TEMP_COORD1 = new Coordinate(0, 0);
Expand All @@ -17,6 +18,10 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
_extMax: Coordinate;
//@internal
_pxExtent: PointExtent;
//@internal
_lineColorStopsKey?: string;
//@internal
_lineColorIn?: any;
static test(symbol: any, geometry: Geometry): boolean {
if (!symbol) {
return false;
Expand Down Expand Up @@ -59,7 +64,7 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
return;
}
this._prepareContext(ctx);
const isGradient = checkGradient(style['lineColor']),
const isGradient = checkGradient(style['lineColor']) || style['lineGradientProperty'],
isPath = this.geometry.getJSONType() === 'Polygon' || this.geometry.type === 'LineString';
if (isGradient && (style['lineColor']['places'] || !isPath)) {
style['lineGradientExtent'] = this.geometry.getContainerExtent()._expand(style['lineWidth']);
Expand All @@ -86,6 +91,9 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
params.push(...paintParams.slice(1));
}
params.push(style['lineOpacity'], style['polygonOpacity'], style['lineDasharray']);
if (isGradient) {
params.push(this._lineColorIn);
}
// @ts-expect-error todo 属性“_paintOn”在类型“Geometry”上不存在
const bbox = this.geometry._paintOn(...params);
this._setBBOX(ctx, bbox);
Expand All @@ -99,6 +107,9 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
const params = [ctx];
params.push(...paintParams);
params.push(style['lineOpacity'], style['polygonOpacity'], style['lineDasharray']);
if (isGradient) {
params.push(this._lineColorIn);
}
// @ts-expect-error todo 属性“_paintOn”在类型“Geometry”上不存在
const bbox = this.geometry._paintOn(...params);
this._setBBOX(ctx, bbox);
Expand Down Expand Up @@ -173,6 +184,7 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
polygonPatternDy: getValueOrDefault(s['polygonPatternDy'], 0),
linePatternDx: getValueOrDefault(s['linePatternDx'], 0),
linePatternDy: getValueOrDefault(s['linePatternDy'], 0),
lineGradientProperty: getValueOrDefault(s['lineGradientProperty'], null),
};
if (result['lineWidth'] === 0) {
result['lineOpacity'] = 0;
Expand All @@ -194,11 +206,49 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
console.error('unable create canvas LinearGradient,error data:', points);
return;
}
let colorStops;
//get colorStops from style
if (lineColor['colorStops']) {
colorStops = lineColor['colorStops'];
}
// get colorStops from properties
if (!colorStops) {
const properties = this.geometry.properties || {};
const style = this.style || {};
colorStops = properties[style['lineGradientProperty']];
}
if (!colorStops || !Array.isArray(colorStops) || colorStops.length < 2) {
return;
}
//is flat colorStops https://github.com/maptalks/maptalks.js/pull/2423
if (!Array.isArray(colorStops[0])) {
const colorStopsArray = [];
let colors = [];
let idx = 0;
for (let i = 0, len = colorStops.length; i < len; i += 2) {
colors[0] = colorStops[i];
colors[1] = colorStops[i + 1];
colorStopsArray[idx] = colors;
idx++;
colors = [];
}
colorStops = colorStopsArray;
}
const grad = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
lineColor['colorStops'].forEach(function (stop: [number, string]) {
colorStops.forEach(function (stop: [number, string]) {
grad.addColorStop(...stop);
});
ctx.strokeStyle = grad;

const key = JSON.stringify(colorStops);
if (key === this._lineColorStopsKey) {
return;
}
this._lineColorStopsKey = key;
const colors: Array<[value: number, color: string]> = colorStops.map(c => {
return [parseFloat(c[0]), c[1]];
})
this._lineColorIn = new ColorIn(colors, { height: 1, width: 100 });
}
}

Expand Down
Loading