diff --git a/package.json b/package.json index c6b217e5e9..139230ead3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/Canvas.ts b/src/core/Canvas.ts index 445d032249..ee203eb9e6 100644 --- a/src/core/Canvas.ts +++ b/src/core/Canvas.ts @@ -103,6 +103,49 @@ function getCubicControlPoints(x0, y0, x1, y1, x2, y2, x3, y3, smoothValue, t) { } } + +function pathDistance(points: Array) { + 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 }); @@ -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)) { @@ -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); }, @@ -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); // } @@ -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) { @@ -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(); } @@ -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(); @@ -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) { @@ -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[]) { diff --git a/src/renderer/geometry/VectorRenderer.ts b/src/renderer/geometry/VectorRenderer.ts index d1c3b4beba..f5e88b626f 100644 --- a/src/renderer/geometry/VectorRenderer.ts +++ b/src/renderer/geometry/VectorRenderer.ts @@ -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); @@ -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); diff --git a/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts b/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts index 468a22748d..ad78dc8ab0 100644 --- a/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts +++ b/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts @@ -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); @@ -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; @@ -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']); @@ -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); @@ -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); @@ -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; @@ -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 }); } } diff --git a/src/symbol/index.ts b/src/symbol/index.ts index a107553b72..f3a993ceff 100644 --- a/src/symbol/index.ts +++ b/src/symbol/index.ts @@ -127,6 +127,7 @@ export type LineSymbol = { linePatternFile?: string; lineDx?: SymbolNumberType; lineDy?: SymbolNumberType; + lineGradientProperty?: string; } export type FillSymbol = { diff --git a/src/types/typings.ts b/src/types/typings.ts index 73200b5be9..5b52f3c438 100644 --- a/src/types/typings.ts +++ b/src/types/typings.ts @@ -17,5 +17,6 @@ declare global { isClip: boolean; isMultiClip: boolean; dpr: number; + lineColorIn: any; } } diff --git a/test/geometry/symbol/StrokeAndFillSpec.js b/test/geometry/symbol/StrokeAndFillSpec.js index 962e3158f5..c56f08bb54 100644 --- a/test/geometry/symbol/StrokeAndFillSpec.js +++ b/test/geometry/symbol/StrokeAndFillSpec.js @@ -4,9 +4,62 @@ describe('StrokeAndFillSpec', function () { var map; var center = new maptalks.Coordinate(118.846825, 32.046534); var patternImage = 'data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'; + const lineCoordinates = [ + { + "x": 116.28890991210938, + "y": 39.98369699673039 + }, + { + "x": 116.43619537353516, + "y": 39.98737978325713 + }, + { + "x": 116.48529052734374, + "y": 39.95554342883535 + }, + { + "x": 116.47979736328125, + "y": 39.84887587825816 + }, + { + "x": 116.46743774414061, + "y": 39.83754093169162 + }, + { + "x": 116.45267486572266, + "y": 39.8314772852108 + }, + { + "x": 116.28135681152342, + "y": 39.829104408261685 + }, + { + "x": 116.27071380615233, + "y": 39.883396390093075 + }, + { + "x": 116.26831054687501, + "y": 39.89446035777916 + }, + { + "x": 116.26899719238281, + "y": 39.96791137735179 + }, + { + "x": 116.28719329833983, + "y": 39.98290780236021 + } + ]; + const lineColorStops = [ + [0.00, 'red'], + [1 / 4, 'orange'], + [2 / 4, 'green'], + [3 / 4, 'aqua'], + [1.00, 'white'] + ]; beforeEach(function () { - var setups = COMMON_CREATE_MAP(center); + var setups = COMMON_CREATE_MAP(center, null, { width: 800, height: 600 }); container = setups.container; map = setups.map; }); @@ -19,9 +72,9 @@ describe('StrokeAndFillSpec', function () { describe('pattern', function () { it('fill pattern', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonPatternFile' : 'resources/pattern2.png', - 'polygonOpacity' : 1 + symbol: { + 'polygonPatternFile': 'resources/pattern2.png', + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -34,10 +87,10 @@ describe('StrokeAndFillSpec', function () { it('fill pattern with polygonPatternDx', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonPatternFile' : 'resources/pattern2.png', - 'polygonPatternDx' : 5, - 'polygonOpacity' : 1 + symbol: { + 'polygonPatternFile': 'resources/pattern2.png', + 'polygonPatternDx': 5, + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -50,12 +103,12 @@ describe('StrokeAndFillSpec', function () { it('line pattern', function (done) { var line = new maptalks.LineString([center, center.add(0, -0.0001)], { - symbol:{ - 'linePatternFile' : 'resources/pattern2.png', - 'lineOpacity' : 1, - 'lineWidth' : 5, - 'polygonFill' : '#000', - 'polygonOpacity' : 0 + symbol: { + 'linePatternFile': 'resources/pattern2.png', + 'lineOpacity': 1, + 'lineWidth': 5, + 'polygonFill': '#000', + 'polygonOpacity': 0 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -68,13 +121,13 @@ describe('StrokeAndFillSpec', function () { it('line pattern with linePatternDx', function (done) { var line = new maptalks.LineString([center, center.add(0.0001, 0)], { - symbol:{ - 'linePatternFile' : 'resources/pattern2.png', - 'linePatternDx' : 2, - 'lineOpacity' : 1, - 'lineWidth' : 5, - 'polygonFill' : '#000', - 'polygonOpacity' : 0 + symbol: { + 'linePatternFile': 'resources/pattern2.png', + 'linePatternDx': 2, + 'lineOpacity': 1, + 'lineWidth': 5, + 'polygonFill': '#000', + 'polygonOpacity': 0 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -87,9 +140,9 @@ describe('StrokeAndFillSpec', function () { it('fill pattern with base64', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonPatternFile' : patternImage, - 'polygonOpacity' : 1 + symbol: { + 'polygonPatternFile': patternImage, + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -102,12 +155,12 @@ describe('StrokeAndFillSpec', function () { it('line pattern with base64', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'linePatternFile' : 'url(' + patternImage + ')', - 'lineOpacity' : 1, - 'lineWidth' : 5, - 'polygonFill' : '#000', - 'polygonOpacity' : 0 + symbol: { + 'linePatternFile': 'url(' + patternImage + ')', + 'lineOpacity': 1, + 'lineWidth': 5, + 'polygonFill': '#000', + 'polygonOpacity': 0 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -120,12 +173,12 @@ describe('StrokeAndFillSpec', function () { it('vector marker fill pattern', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerFillPatternFile' : 'resources/pattern.png', - 'markerFillOpacity' : 1, - 'markerWidth' : 20, - 'markerHeight' : 20 + symbol: { + 'markerType': 'ellipse', + 'markerFillPatternFile': 'resources/pattern.png', + 'markerFillOpacity': 1, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -138,14 +191,14 @@ describe('StrokeAndFillSpec', function () { it('vector marker line pattern', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerLinePatternFile' : 'resources/pattern.png', - 'markerLineOpacity' : 1, - 'markerLineWidth' : 5, - 'markerFillOpacity' : 0, - 'markerWidth' : 20, - 'markerHeight' : 20 + symbol: { + 'markerType': 'ellipse', + 'markerLinePatternFile': 'resources/pattern.png', + 'markerLineOpacity': 1, + 'markerLineWidth': 5, + 'markerFillOpacity': 0, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -158,12 +211,12 @@ describe('StrokeAndFillSpec', function () { it('vector marker fill pattern with base64', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerFillPatternFile' : patternImage, - 'markerFillOpacity' : 1, - 'markerWidth' : 20, - 'markerHeight' : 20 + symbol: { + 'markerType': 'ellipse', + 'markerFillPatternFile': patternImage, + 'markerFillOpacity': 1, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -176,14 +229,14 @@ describe('StrokeAndFillSpec', function () { it('vector marker line pattern with base64', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerLinePatternFile' : 'url(' + patternImage + ')', - 'markerLineOpacity' : 1, - 'markerLineWidth' : 5, - 'markerFillOpacity' : 0, - 'markerWidth' : 20, - 'markerHeight' : 20 + symbol: { + 'markerType': 'ellipse', + 'markerLinePatternFile': 'url(' + patternImage + ')', + 'markerLineOpacity': 1, + 'markerLineWidth': 5, + 'markerFillOpacity': 0, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -199,11 +252,11 @@ describe('StrokeAndFillSpec', function () { describe('radial gradient', function () { it('fill radial gradient', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonFill' : { - type : 'radial', - places : [0.5, 0.5, 1, 0.5, 0.5, 0], - colorStops : [ + symbol: { + 'polygonFill': { + type: 'radial', + places: [0.5, 0.5, 1, 0.5, 0.5, 0], + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -213,7 +266,7 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'polygonOpacity' : 1 + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -226,10 +279,10 @@ describe('StrokeAndFillSpec', function () { it('fill radial gradient 2', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonFill' : { - type : 'radial', - colorStops : [ + symbol: { + 'polygonFill': { + type: 'radial', + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -239,7 +292,7 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'polygonOpacity' : 1 + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -252,10 +305,10 @@ describe('StrokeAndFillSpec', function () { it('line radial gradient', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'lineColor' : { - type : 'radial', - colorStops : [ + symbol: { + 'lineColor': { + type: 'radial', + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -265,9 +318,9 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'lineWidth' : 3, - 'lineOpacity' : 1, - 'polygonOpacity' : 0 + 'lineWidth': 3, + 'lineOpacity': 1, + 'polygonOpacity': 0 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -281,11 +334,11 @@ describe('StrokeAndFillSpec', function () { it('vector marker', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerLineColor' : { - type : 'radial', - colorStops : [ + symbol: { + 'markerType': 'ellipse', + 'markerLineColor': { + type: 'radial', + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -295,10 +348,10 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'markerLineWidth' : 3, - 'markerFillOpacity' : 0, - 'markerWidth' : 20, - 'markerHeight' : 20 + 'markerLineWidth': 3, + 'markerFillOpacity': 0, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -316,11 +369,11 @@ describe('StrokeAndFillSpec', function () { it('fill linear gradient', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonFill' : { - type : 'linear', - places : [0, 0, 0.5, 0], - colorStops : [ + symbol: { + 'polygonFill': { + type: 'linear', + places: [0, 0, 0.5, 0], + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -330,7 +383,7 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'polygonOpacity' : 1 + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -343,10 +396,10 @@ describe('StrokeAndFillSpec', function () { it('fill linear gradient 2', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'polygonFill' : { - type : 'linear', - colorStops : [ + symbol: { + 'polygonFill': { + type: 'linear', + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -356,7 +409,7 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'polygonOpacity' : 1 + 'polygonOpacity': 1 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -368,10 +421,10 @@ describe('StrokeAndFillSpec', function () { }); it('line linear gradient', function (done) { var circle = new maptalks.Circle(center, 10, { - symbol:{ - 'lineColor' : { - type : 'linear', - colorStops : [ + symbol: { + 'lineColor': { + type: 'linear', + colorStops: [ [0.00, 'red'], [1 / 6, 'orange'], [2 / 6, 'yellow'], @@ -381,9 +434,9 @@ describe('StrokeAndFillSpec', function () { [1.00, 'white'], ] }, - 'lineWidth' : 3, - 'lineOpacity' : 1, - 'polygonOpacity' : 0 + 'lineWidth': 3, + 'lineOpacity': 1, + 'polygonOpacity': 0 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -396,18 +449,18 @@ describe('StrokeAndFillSpec', function () { it('vector marker', function (done) { var circle = new maptalks.Marker(center, { - symbol:{ - 'markerType' : 'ellipse', - 'markerFill' : { - type : 'linear', - colorStops : [ + symbol: { + 'markerType': 'ellipse', + 'markerFill': { + type: 'linear', + colorStops: [ [0.00, 'red'], [1.00, 'white'], ] }, - 'markerFillOpacity' : 1, - 'markerWidth' : 20, - 'markerHeight' : 20 + 'markerFillOpacity': 1, + 'markerWidth': 20, + 'markerHeight': 20 } }); var v = new maptalks.VectorLayer('v').addTo(map); @@ -444,8 +497,8 @@ describe('StrokeAndFillSpec', function () { done(); }); line.setSymbol({ - 'lineWidth' : 2, - 'lineDx' : 10 + 'lineWidth': 2, + 'lineDx': 10 }); }); v.addGeometry(line); @@ -466,11 +519,108 @@ describe('StrokeAndFillSpec', function () { done(); }); line.setSymbol({ - 'lineWidth' : 2, - 'lineDy' : 10 + 'lineWidth': 2, + 'lineDy': 10 }); }); v.addGeometry(line); }); }); + + + describe('lineColor gradient', function () { + + + it('#2123 #1712 Canvas simulates gradient path ', function (done) { + var line = new maptalks.LineString(lineCoordinates, { + symbol: { + // linear gradient + 'lineColor': { + 'type': 'linear', + 'colorStops': lineColorStops + }, + 'lineWidth': 4 + } + }) + var layer = new maptalks.VectorLayer('v').addTo(map); + line.addTo(layer); + const coordinates = line.getCoordinates(); + const first = coordinates[0], last = coordinates[coordinates.length - 1]; + map.fitExtent(layer.getExtent()); + const centerPt = map.coordinateToContainerPoint(map.getCenter()); + + setTimeout(() => { + const p1 = map.coordinateToContainerPoint(first).sub(centerPt); + const p2 = map.coordinateToContainerPoint(last).sub(centerPt); + expect(layer).to.be.painted(p1.x, p1.y, [255, 3, 0]); + expect(layer).to.be.painted(p2.x, p2.y, [249, 255, 255]); + done(); + }, 1000); + + + }); + + it('lineColor gradient from lineGradientProperty', function (done) { + var line = new maptalks.LineString(lineCoordinates, { + symbol: { + // linear gradient + lineGradientProperty: 'gradients', + 'lineWidth': 4 + }, + properties: { + gradients: lineColorStops + } + }) + var layer = new maptalks.VectorLayer('v').addTo(map); + line.addTo(layer); + const coordinates = line.getCoordinates(); + const first = coordinates[0], last = coordinates[coordinates.length - 1]; + map.fitExtent(layer.getExtent()); + const centerPt = map.coordinateToContainerPoint(map.getCenter()); + + setTimeout(() => { + const p1 = map.coordinateToContainerPoint(first).sub(centerPt); + const p2 = map.coordinateToContainerPoint(last).sub(centerPt); + expect(layer).to.be.painted(p1.x, p1.y, [255, 3, 0]); + expect(layer).to.be.painted(p2.x, p2.y, [249, 255, 255]); + done(); + }, 1000); + + + }); + + it('lineColor gradient from lineGradientProperty and colorStops is flat array', function (done) { + const colorStops = []; + lineColorStops.forEach(lineColorStop => { + colorStops.push(...lineColorStop); + }); + var line = new maptalks.LineString(lineCoordinates, { + symbol: { + // linear gradient + lineGradientProperty: 'gradients', + 'lineWidth': 4 + }, + properties: { + gradients: colorStops + } + }) + var layer = new maptalks.VectorLayer('v').addTo(map); + line.addTo(layer); + const coordinates = line.getCoordinates(); + const first = coordinates[0], last = coordinates[coordinates.length - 1]; + map.fitExtent(layer.getExtent()); + const centerPt = map.coordinateToContainerPoint(map.getCenter()); + + setTimeout(() => { + const p1 = map.coordinateToContainerPoint(first).sub(centerPt); + const p2 = map.coordinateToContainerPoint(last).sub(centerPt); + expect(layer).to.be.painted(p1.x, p1.y, [255, 3, 0]); + expect(layer).to.be.painted(p2.x, p2.y, [249, 255, 255]); + done(); + }, 1000); + + + }); + }); + });