/**
* Trigger points info
* @typedef {object} triggerInfo
* @property {number} point - Trigger index for SVGElement.points
* @property {number} sectionLength - Length from start of path to this trigger point
* @property {number} prevOffset - Vertical distance between previous point and this
* @property {number} nextOffset - Vertical distance between this point ant the next
*/
/**
* __svgLine__ options object
* @typedef {object} svglOptions
* @property {SVGElement} svg - SVG | Required
* @property {SVGPolylineElement | SVGPolygonElement} path - Path to draw | Required
* @property {triggerInfo[]} triggers - [triggerInfo]{@link triggerInfo} Array | Required
* @property {number} triggerPad - padding for the trigger points | Optional
*/
/**
* A plugin-less way to manipulate `SVGPolylineElement` or `SVGPolygonElement`.
* @param {svglOptions} options - [svglOptions]{@link svglOptions}
*/
class svgLine {
/**
* @param {svglOptions} options
*/
constructor(options) {
const style = getComputedStyle(options.path);
/**
* svgLine Options [svglOptions]{@link svglOptions}
* @type {svglOptions}
*/
this.options = Object.assign(
{
triggers: [],
triggerPad: 0
},
options
);
/**
* SVG viewbox height
* @type {number}
*/
this.height = options.path.viewportElement.viewBox.baseVal.height;
/**
* Path length
* @type {number}
*/
this.length = parseFloat(style['stroke-dasharray']);
/**
* Last active trigger point
* @type {number}
*/
this.active = 0;
if (this.options.triggers.length > 0) {
/**
* Segments ratio
* @type {number[]}
*/
this.ratios = this.getRatios();
}
}
/**
* Draws the path by changing the `strokeDashoffset` of it.
* @param {number} percent - decimal from 0 t0 1
* @returns {number} - New strokeDashoffset length.
*/
drawPath(percent) {
const l = this.length,
offset = l * percent,
newLength = l - offset,
changePath = () => {
requestAnimationFrame(() => {
this.options.path.style.strokeDashoffset = newLength;
});
};
this.offset = offset;
changePath();
return newLength;
}
/**
* Method use to calculate the last *active trigger point*.
* An *active trigger point* is a point by which the stroke has already passed.
* @returns {number} - active index
*/
reCheck() {
const triggerArray = this.options.triggers;
/**
* Recursive function that checks if `svgLine.length` is bigger than the following trigger points length.
* @param {number} index - index to check
* @returns {number} - new active index
*/
const checkForward = index => {
let nextTrigger = triggerArray[index];
if (index === triggerArray.length) {
return index;
}
if (this.offset === this.length) {
return triggerArray.length;
}
if (this.offset >= nextTrigger.sectionLength - this.options.triggerPad) {
if (index < triggerArray.length - 1) {
return checkForward(index + 1);
} else {
return index;
}
} else {
return index;
}
};
/**
* Recursive function that checks if `svgLine.length` is smaller than the previous trigger points length.
* @param {number} index - index to check
* @returns {number} - new active index
*/
const checkPrev = index => {
let prevTrigger,
prevIndex = index - 1;
if (index > 0) {
prevTrigger = triggerArray[prevIndex];
if (this.offset < prevTrigger.sectionLength - this.options.triggerPad) {
if (prevIndex > 0) return checkPrev(index - 1);
else return prevIndex;
} else {
return index;
}
} else {
return index;
}
};
let next = checkForward(this.active);
if (next !== this.active) {
return next;
} else {
return checkPrev(this.active);
}
}
/**
* Gets ratio of trigger points position relative to viewport height.
* @returns {number[]}
*/
getRatios() {
if (this.options.triggers.length === 0) return;
const triggerPoints = this.options.triggers;
const points = this.options.path.points;
return triggerPoints.map(triggerPoint => {
let y = points.getItem(triggerPoint.point).y;
return y / this.height * 100;
});
}
/**
* Redraws paths based on ratios provided.
* @param {number[]} ratios
*/
setRatios(ratios) {
if (this.ratios === null) return;
const triggerPoints = this.options.triggers,
oldRatios = this.ratios,
points = this.options.path.points;
/**
* Changes Trigger Point position, also previous and next point positions.
* @param {triggerInfo} triggerInfo - Trigger point index for SVGPointList "points"
* @param {number} index - Trigger point index
* @param {number} diff - Difference between Ratios
*/
const changeTriggerPoint = (triggerInfo, index, diff) => {
const triggerIndex = triggerInfo.point;
const trigger = points.getItem(triggerIndex);
let prevTriggerIndex = index > 0 ? triggerPoints[index - 1].point : 0,
prevPoint =
triggerIndex - 1 >= 0 ? points.getItem(triggerIndex - 1) : null,
nextPoint =
triggerIndex + 1 < points.numberOfItems - 1
? points.getItem(triggerIndex + 1)
: null,
secLength = 0;
if (triggerInfo.prevOffset == null && prevPoint != null) {
if (prevTriggerIndex !== triggerIndex - 1)
triggerInfo.prevOffset = svgLine.dY(prevPoint, trigger);
else prevPoint = null;
}
if (triggerInfo.nextOffset == null && nextPoint != null) {
triggerInfo.nextOffset = svgLine.dY(nextPoint, trigger);
if (triggerInfo.nextOffset > Math.abs(triggerInfo.prevOffset))
nextPoint = null;
}
if (triggerIndex < points.numberOfItems - 1) trigger.y = diff;
if (prevPoint != null) prevPoint.y = trigger.y + triggerInfo.prevOffset;
if (nextPoint != null) nextPoint.y = trigger.y + triggerInfo.nextOffset;
secLength = svgLine.calculateSectionLength(
this.options.path,
prevTriggerIndex,
triggerIndex
);
triggerInfo.sectionLength =
index > 0
? triggerPoints[index - 1].sectionLength + secLength
: secLength;
};
ratios.forEach((ratio, i) => {
let triggerInfo = triggerPoints[i],
y = points.getItem(triggerInfo.point).y,
ratioDiff = ratio / oldRatios[i],
newY = y * ratioDiff;
changeTriggerPoint(triggerInfo, i, newY);
});
this.ratios = ratios;
// the sectionLength of the last trigger point equals the total polyline length
this.length = triggerPoints[triggerPoints.length - 1].sectionLength;
}
/**
* Calculates length starting from start point up to end point.
* sectionLength is the sum of all the segment lengths inside the section.
* @param {SVGPolylineElement | SVGPolygonElement} path
* @param {number} start - Starting point index
* @param {number} end - End point index
* @returns {number} - Section length
*/
static calculateSectionLength(path, start, end) {
let pointIndex = start + 1,
length = 0;
for (pointIndex; pointIndex <= end; pointIndex++) {
length += svgLine.distance(
path.points.getItem(pointIndex - 1),
path.points.getItem(pointIndex)
);
}
return length;
}
/**
* Calculates total length.
* @param {SVGPolylineElement | SVGPolygonElement} path
* @returns {number} - Total length of path
*/
static getTotalLength(path) {
return svgLine.calculateSectionLength(path, 0, path.points.length - 1);
}
/**
* Calculate vertical difference between points
* @param {SVGPoint} point1
* @param {SVGPoint} point2
*/
static dY(point1, point2) {
return point1.y - point2.y;
}
/**
* Calculate horizontal difference between points
* @param {SVGPoint} point1
* @param {SVGPoint} point2
*/
static dX(point1, point2) {
return point1.x - point2.x;
}
/**
* Calculate length from point1 to point2
* @param {SVGPoint} point1
* @param {SVGPoint} point2
*/
static distance(point1, point2) {
return Math.hypot(svgLine.dX(point1, point2), svgLine.dY(point1, point2));
}
}
/**
* exports class [svgLine]{@link svgLine}
* @module src/svgLine
*/
export default svgLine;