d3-curve-circlecorners
Version:
A curve function that can be passed to the d3.js line.curve and area.curve functions
149 lines (131 loc) • 5.07 kB
JavaScript
/**
* Point type definition
* @typedef {Object} Point
* @property {number} x - horizontal position in a canvas or SVG element
* @property {number} y - vertical position in a canvas or SVG element
*/
/**
* CircleCorners type definition
* @typedef {Object} CircleCorners
* @property {function} areaStart - triggered when ...
* @property {function} areaEnd - triggered when ...
* @property {function} lineStart - triggered when before the first datum in a line
* @property {function} lineEnd - triggered when after the last datum in a line
* @property {function} point - triggered for each point in an array of data defining a line
* @property {object} _context - 2d context of an HTML canvas element (or equivalant SVG drawing space)
* @property {string} _context._ draw commands to be applied to the d attribute of a path element
* @property {number} _radius - corner radius to apply to each line segment intersection
* @property {number} [_line] - state variable for ...
* @property {number} [_point] - state variable for how many points are saved as properties (vert & prev)
* @property {Point} [_prev] - the point preceding the vertex; either the first point of a line or an "out" anchor
* @property {Point} [_vert] - vertex currently being drawn; the curve will not actually touch this point
*/
function CircleCorners(context, radius) {
this._context = context;
this._radius = radius || 1;
}
/**
* @func alongSegment
* @desc locatates a point a certain disatance away, in the direction of a known position
* @param {Point} from - origin of a ray (or vector)
* @param {Point} from - second point defining the direction of the ray (or vector)
* @param {number} distanceAlong - distance away that the output point should be, along the ray
* @return {Point} a new point, the specified direction and distance from `from`
*/
function alongSegment(from, toward, distanceAlong) {
const rayAngle = Math.atan2(from.y - toward.y, from.x - toward.x);
return {
x: from.x - distanceAlong * Math.cos(rayAngle),
y: from.y - distanceAlong * Math.sin(rayAngle),
};
}
/**
* @func arcPast
* @desc ...
* @param {Object} that - context of the current draw command
* @param {number} x - ...
* @param {number} y - ...
*/
function arcPast(that, x, y) {
const angle = Math.abs(
Math.atan2(that._vert.y - that._prev.y, that._vert.x - that._prev.x) -
Math.atan2(that._vert.y - y, that._vert.x - x)
);
const acuteAngle = angle > Math.PI ? 2 * Math.PI - angle : angle;
const shortestRay = Math.min(
Math.sqrt(
Math.pow(that._vert.x - that._prev.x, 2) +
Math.pow(that._vert.y - that._prev.y, 2)
),
Math.sqrt(Math.pow(that._vert.x - x, 2) + Math.pow(that._vert.y - y, 2))
);
const radius = Math.min(that._radius, shortestRay * Math.tan(acuteAngle / 2));
const anchorDistance = acuteAngle
? Math.abs(radius / Math.tan(acuteAngle / 2))
: 0;
const determinant =
(that._vert.x - that._prev.x) * (that._vert.y - y) -
(that._vert.x - x) * (that._vert.y - that._prev.y);
const sweepFlag = determinant < 0 ? 1 : 0;
const aIn = alongSegment(that._vert, that._prev, anchorDistance);
const aOut = alongSegment(that._vert, { x, y }, anchorDistance);
// that._context.arcTo() doesn't work properly, so we'll modify the string directly
that._context._ += `L${aIn.x},${aIn.y}A${radius},${radius},0,0,${sweepFlag},${aOut.x},${aOut.y}`;
that._prev = aOut;
that._vert = { x, y };
}
CircleCorners.prototype = {
areaStart: function () {
this._line = 0;
},
areaEnd: function () {
this._line = NaN;
},
lineStart: function () {
this._prev = { x: NaN, y: NaN };
this._vert = { x: NaN, y: NaN };
this._point = 0;
},
lineEnd: function () {
// No more points, so draw a straight line to the end
if (this._point === 1) {
this._context.moveTo(this._vert.x, this._vert.y);
} else {
this._context.lineTo(this._vert.x, this._vert.y);
}
// proceed
if (this._line || (this._line !== 0 && this._point === 3))
this._context.closePath();
this._line = 1 - this._line;
},
point: function (x, y) {
(x = +x), (y = +y);
switch (this._point) {
case 0:
this._point = 1;
break;
case 1:
this._point = 2;
if (this._line) {
this._context.lineTo(this._vert.x, this._vert.y);
} else {
this._context.moveTo(this._vert.x, this._vert.y);
}
break;
default:
arcPast(this, x, y);
return;
}
this._prev = this._vert;
this._vert = { x, y };
},
};
export default (function custom(radius) {
function circleCorners(context) {
return new CircleCorners(context, radius);
}
circleCorners.radius = function (radius) {
return custom(+radius);
};
return circleCorners;
})(0);