Skip to content

Commit

Permalink
Canvas simulates gradient path (#2423)
Browse files Browse the repository at this point in the history
* Canvas simulates gradient path

* updates

* spec

* support lineGradientProperty

* update getColorInMinStep
  • Loading branch information
deyihu authored Oct 14, 2024
1 parent fc39378 commit 8cdaea8
Show file tree
Hide file tree
Showing 7 changed files with 501 additions and 124 deletions.
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

0 comments on commit 8cdaea8

Please sign in to comment.