// widget_pathslider.ysl2 widget_desc("PathSlider") { longdesc || PathSlider - || shortdesc > Slide an SVG element along a path by dragging it path name="value" accepts="HMI_INT,HMI_REAL" > value path name="min" count="optional" accepts="HMI_INT,HMI_REAL" > min path name="max" count="optional" accepts="HMI_INT,HMI_REAL" > max arg name="min" count="optional" accepts="int,real" > minimum value arg name="max" count="optional" accepts="int,real" > maximum value } widget_class("PathSlider") { || frequency = 10; position = undefined; min = 0; max = 100; scannedPoints = []; pathLength = undefined; precision = undefined; origPt = undefined; scanPath() { this.pathLength = this.path_elt.getTotalLength(); this.precision = Math.floor(this.pathLength / 10); // save linear scan for coarse approximation for (var scanLength = 0; scanLength <= this.pathLength; scanLength += this.precision) { this.scannedPoints.push([this.path_elt.getPointAtLength(scanLength), scanLength]); } [this.origPt,] = this.scannedPoints[0]; } closestPoint(point) { var bestPoint, bestLength, bestDistance = Infinity, scanDistance; // use linear scan for coarse approximation for (let [scanPoint, scanLength] of this.scannedPoints){ if ((scanDistance = distance2(scanPoint)) < bestDistance) { bestPoint = scanPoint, bestLength = scanLength, bestDistance = scanDistance; } } // binary search for more precise estimate let precision = this.precision / 2; while (precision > 0.5) { var beforePoint, afterPoint, beforeLength, afterLength, beforeDistance, afterDistance; if ((beforeLength = bestLength - precision) >= 0 && (beforeDistance = distance2(beforePoint = this.path_elt.getPointAtLength(beforeLength))) < bestDistance) { bestPoint = beforePoint, bestLength = beforeLength, bestDistance = beforeDistance; } else if ((afterLength = bestLength + precision) <= this.pathLength && (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) { bestPoint = afterPoint, bestLength = afterLength, bestDistance = afterDistance; } precision /= 2; } return [bestPoint, bestLength]; function distance2(p) { var dx = p.x - point.x, dy = p.y - point.y; return dx * dx + dy * dy; } } dispatch(value,oldval, index) { switch(index) { case 0: this.position = value; break; case 1: this.min = value; break; case 2: this.max = value; break; } this.request_animate(); } get_current_point(){ let currLength = this.pathLength * (this.position - this.min) / (this.max - this.min) return this.path_elt.getPointAtLength(currLength); } animate(){ if(this.position == undefined) return; let currPt = this.get_current_point(); this.cursor_transform.setTranslate(currPt.x - this.origPt.x, currPt.y - this.origPt.y); } init() { if(this.args.length == 2) [this.min, this.max]=this.args; this.scanPath(); this.cursor_transform = svg_root.createSVGTransform(); this.cursor_elt.transform.baseVal.appendItem(this.cursor_transform); this.cursor_elt.onpointerdown = (e) => this.on_cursor_down(e); this.bound_drag = this.drag.bind(this); this.bound_drop = this.drop.bind(this); } start_dragging_from_event(e){ let clientPoint = new DOMPoint(e.clientX, e.clientY); let point = clientPoint.matrixTransform(this.invctm); let currPt = this.get_current_point(); this.draggingOffset = new DOMPoint(point.x - currPt.x , point.y - currPt.y); } apply_position_from_event(e){ let clientPoint = new DOMPoint(e.clientX, e.clientY); let rawPoint = clientPoint.matrixTransform(this.invctm); let point = new DOMPoint(rawPoint.x - this.draggingOffset.x , rawPoint.y - this.draggingOffset.y); let [closestPoint, closestLength] = this.closestPoint(point); let new_position = this.min + (this.max - this.min) * closestLength / this.pathLength; this.position = Math.round(Math.max(Math.min(new_position, this.max), this.min)); this.apply_hmi_value(0, this.position); } on_cursor_down(e){ // get scrollbar -> root transform let ctm = this.path_elt.getCTM(); // root -> path transform this.invctm = ctm.inverse(); this.start_dragging_from_event(e); svg_root.addEventListener("pointerup", this.bound_drop, true); svg_root.addEventListener("pointermove", this.bound_drag, true); } drop(e) { svg_root.removeEventListener("pointerup", this.bound_drop, true); svg_root.removeEventListener("pointermove", this.bound_drag, true); } drag(e) { this.apply_position_from_event(e); } || } widget_defs("PathSlider") { labels("cursor path"); }