diff --git a/dist/maptalks.textPath.js b/dist/maptalks.textPath.js new file mode 100644 index 0000000..1ab72cb --- /dev/null +++ b/dist/maptalks.textPath.js @@ -0,0 +1,247 @@ +/*! + * maptalks.textPath v0.1.0 + * LICENSE : MIT + * (c) 2016-2018 maptalks.org + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('maptalks')) : + typeof define === 'function' && define.amd ? define(['exports', 'maptalks'], factory) : + (factory((global.maptalks = global.maptalks || {}),global.maptalks)); +}(this, (function (exports,maptalks) { 'use strict'; + +// https://github.com/Viglino/Canvas-TextPath +/** Render text along a path in a Canvas +* Adds extra functionality to the CanvasRenderingContext2D by extending its prototype. +* Extent the global object with options: +* - textOverflow {undefined|visible|ellipsis|string} the text to use on overflow, default "" (hidden) +* - textJustify {undefined|boolean} used to justify text (otherwise use textAlign), default false +* - textStrokeMin {undefined|number} the min length (in pixel) for the support path to draw the text upon, default 0 +* +* @param {string} text the text to render +* @param {Array} path an array of coordinates as support for the text (ie. [x1,y1,x2,y2,...] +*/ +(function () { + /* Usefull function */ + function dist2D(x1, y1, x2, y2) { + var dx = x2 - x1; + var dy = y2 - y1; + return Math.sqrt(dx * dx + dy * dy); + } + + /* Add new properties on CanvasRenderingContext2D */ + CanvasRenderingContext2D.prototype.textOverflow = ""; + CanvasRenderingContext2D.prototype.textJustify = false; + CanvasRenderingContext2D.prototype.textStrokeMin = 0; + + var state = []; + var save = CanvasRenderingContext2D.prototype.save; + CanvasRenderingContext2D.prototype.save = function () { + state.push({ textOverflow: this.textOverflow, + textJustify: this.textJustify, + textStrokeMin: this.textStrokeMin + }); + save.call(this); + }; + + var restore = CanvasRenderingContext2D.prototype.restore; + CanvasRenderingContext2D.prototype.restore = function () { + restore.call(this); + var s = state.pop(); + this.textOverflow = s.textOverflow; + this.textJustify = s.textJustify; + this.textStrokeMin = s.textStrokeMin; + }; + + /* textPath function */ + CanvasRenderingContext2D.prototype.textPath = function (text, path) { + // Helper to get a point on the path, starting at dl + // (return x, y and the angle on the path) + var di, + dpos = 0; + var pos = 2; + function pointAt(dl) { + if (!di || dpos + di < dl) { + for (; pos < path.length;) { + di = dist2D(path[pos - 2], path[pos - 1], path[pos], path[pos + 1]); + if (dpos + di > dl) break; + pos += 2; + if (pos >= path.length) break; + dpos += di; + } + } + + var x, + y, + dt = dl - dpos; + if (pos >= path.length) { + pos = path.length - 2; + } + + if (!dt) { + x = path[pos - 2]; + y = path[pos - 1]; + } else { + x = path[pos - 2] + (path[pos] - path[pos - 2]) * dt / di; + y = path[pos - 1] + (path[pos + 1] - path[pos - 1]) * dt / di; + } + return [x, y, Math.atan2(path[pos + 1] - path[pos - 1], path[pos] - path[pos - 2])]; + } + + var letterPadding = this.measureText(" ").width * 0.25; + + // Calculate length + var d = 0; + for (var i = 2; i < path.length; i += 2) { + d += dist2D(path[i - 2], path[i - 1], path[i], path[i + 1]); + } + if (d < this.minWidth) return; + var nbspace = text.split(" ").length - 1; + + // Remove char for overflow + if (this.textOverflow != "visible") { + if (d < this.measureText(text).width + (text.length - 1 + nbspace) * letterPadding) { + var overflow = this.textOverflow == "ellipsis" ? '\u2026' : this.textOverflow || ""; + var dt = overflow.length - 1; + do { + if (text[text.length - 1] === " ") nbspace--; + text = text.slice(0, -1); + } while (text && d < this.measureText(text + overflow).width + (text.length + dt + nbspace) * letterPadding); + text += overflow; + } + } + + // Calculate start point + var start = 0; + switch (this.textJustify || this.textAlign) {case true: // justify + case "center": + case "end": + case "right": + { + // Justify + if (this.textJustify) { + start = 0; + letterPadding = (d - this.measureText(text).width) / (text.length - 1 + nbspace); + } + // Text align + else { + start = d - this.measureText(text).width - (text.length + nbspace) * letterPadding; + if (this.textAlign == "center") start /= 2; + } + break; + } + // left + default: + break; + } + + // Do rendering + for (var t = 0; t < text.length; t++) { + var letter = text[t]; + var wl = this.measureText(letter).width; + + var p = pointAt(start + wl / 2); + + this.save(); + this.textAlign = "center"; + this.translate(p[0], p[1]); + this.rotate(p[2]); + if (this.lineWidth > 0.1) this.strokeText(letter, 0, 0); + this.fillText(letter, 0, 0); + this.restore(); + start += wl + letterPadding * (letter == " " ? 2 : 1); + } + }; +})(); + +function isSamePoint(pt1, pt2) { + if (Math.abs(pt1.x - pt2.x) < 0.000001 && Math.abs(pt1.y - pt2.y) < 0.000001) return true; + return false; +} + +// remove duplicate adjacent point +function delDuplicatePt(pts) { + let i = 0; + + while (i < pts.length - 1) { + if (isSamePoint(pts[i], pts[i + 1])) { + pts.splice(i + 1, 1); + continue; + } else { + i++; + } + } + return pts; +} + +const originPaintOn = maptalks.LineString.prototype._paintOn; + +const options = { + fontSize: "48px", + fontFamily: "Arial", + textJustify: true, + textOverflow: "visible", + textBaseline: "middle", + textStrokeMin: 20 +}; + +class TextPath extends maptalks.LineString { + _paintOn(ctx, points, lineOpacity, fillOpacity, dasharray) { + delDuplicatePt(points); // paint smoothline error when adjacent-points duplicate + if (this.options['text']) { + let fontSize = this.options["fontSize"]; + if (this.options["fontSize"].indexOf("px") != -1) { + fontSize = this.options["fontSize"]; + } else if (this.options["fontSize"].indexOf("m") != -1) { + const index = this.options["fontSize"].indexOf("m"); + const size = parseFloat(this.options["fontSize"].substring(0, index)); + const scale = this.getMap().getScale(); + fontSize = size / scale + "px"; + } + + const font = fontSize + " " + this.options["fontFamily"]; + this._paintPolylineTextPath(ctx, points, this.options['text'], font, this.options["symbol"]['lineColor'], this.options["symbol"]['lineWidth'], lineOpacity); + } else { + originPaintOn.apply(this, arguments); + } + } + + _paintPolylineTextPath(ctx, points, text, font, lineColor, lineWidth, lineOpacity) { + // Render text + ctx.font = font; + ctx.strokeStyle = lineColor; + ctx.lineWidth = lineWidth || 3; + ctx.globalAlpha = lineOpacity; + + ctx.textAlign = this.options['textAlign']; + ctx.textBaseline = this.options['textBaseline']; + ctx.textOverflow = this.options['textOverflow']; + ctx.textJustify = this.options['textJustify']; + ctx.textStrokeMin = this.options['textStrokeMin']; + + let path = []; + let len = points.length; + for (let i = 0; i < len; i++) { + path.push(points[i].x); + path.push(points[i].y); + } + + ctx.beginPath(); + ctx.moveTo(path[0], path[1]); + ctx.textPath(text, path); + maptalks.Canvas._stroke(ctx, lineOpacity); + } + +} + +TextPath.mergeOptions(options); + +TextPath.registerJSONType('TextPath'); + +exports.TextPath = TextPath; +exports['default'] = TextPath; + +Object.defineProperty(exports, '__esModule', { value: true }); + +typeof console !== 'undefined' && console.log('maptalks.textPath v0.1.0'); + +})));