|
1 // widget_pathslider.ysl2 |
|
2 widget_desc("PathSlider") { |
|
3 longdesc |
|
4 || |
|
5 PathSlider - |
|
6 || |
|
7 |
|
8 shortdesc > Slide an SVG element along a path by dragging it |
|
9 |
|
10 path name="value" accepts="HMI_INT,HMI_REAL" > value |
|
11 path name="min" count="optional" accepts="HMI_INT,HMI_REAL" > min |
|
12 path name="max" count="optional" accepts="HMI_INT,HMI_REAL" > max |
|
13 |
|
14 arg name="min" count="optional" accepts="int,real" > minimum value |
|
15 arg name="max" count="optional" accepts="int,real" > maximum value |
|
16 } |
|
17 |
|
18 widget_class("PathSlider") { |
|
19 || |
|
20 frequency = 10; |
|
21 position = undefined; |
|
22 min = 0; |
|
23 max = 100; |
|
24 scannedPoints = []; |
|
25 pathLength = undefined; |
|
26 precision = undefined; |
|
27 origPt = undefined; |
|
28 |
|
29 |
|
30 scanPath() { |
|
31 this.pathLength = this.path_elt.getTotalLength(); |
|
32 this.precision = Math.floor(this.pathLength / 10); |
|
33 |
|
34 // save linear scan for coarse approximation |
|
35 for (var scanLength = 0; scanLength <= this.pathLength; scanLength += this.precision) { |
|
36 this.scannedPoints.push([this.path_elt.getPointAtLength(scanLength), scanLength]); |
|
37 } |
|
38 [this.origPt,] = this.scannedPoints[0]; |
|
39 } |
|
40 |
|
41 closestPoint(point) { |
|
42 var bestPoint, |
|
43 bestLength, |
|
44 bestDistance = Infinity, |
|
45 scanDistance; |
|
46 |
|
47 // use linear scan for coarse approximation |
|
48 for (let [scanPoint, scanLength] of this.scannedPoints){ |
|
49 if ((scanDistance = distance2(scanPoint)) < bestDistance) { |
|
50 bestPoint = scanPoint, |
|
51 bestLength = scanLength, |
|
52 bestDistance = scanDistance; |
|
53 } |
|
54 } |
|
55 |
|
56 // binary search for more precise estimate |
|
57 let precision = this.precision / 2; |
|
58 while (precision > 0.5) { |
|
59 var beforePoint, |
|
60 afterPoint, |
|
61 beforeLength, |
|
62 afterLength, |
|
63 beforeDistance, |
|
64 afterDistance; |
|
65 if ((beforeLength = bestLength - precision) >= 0 && |
|
66 (beforeDistance = distance2(beforePoint = this.path_elt.getPointAtLength(beforeLength))) < bestDistance) { |
|
67 bestPoint = beforePoint, |
|
68 bestLength = beforeLength, |
|
69 bestDistance = beforeDistance; |
|
70 } else if ((afterLength = bestLength + precision) <= this.pathLength && |
|
71 (afterDistance = distance2(afterPoint = this.path_elt.getPointAtLength(afterLength))) < bestDistance) { |
|
72 bestPoint = afterPoint, |
|
73 bestLength = afterLength, |
|
74 bestDistance = afterDistance; |
|
75 } |
|
76 precision /= 2; |
|
77 } |
|
78 |
|
79 return [bestPoint, bestLength]; |
|
80 |
|
81 function distance2(p) { |
|
82 var dx = p.x - point.x, |
|
83 dy = p.y - point.y; |
|
84 return dx * dx + dy * dy; |
|
85 } |
|
86 } |
|
87 |
|
88 dispatch(value,oldval, index) { |
|
89 switch(index) { |
|
90 case 0: |
|
91 this.position = value; |
|
92 break; |
|
93 case 1: |
|
94 this.min = value; |
|
95 break; |
|
96 case 2: |
|
97 this.max = value; |
|
98 break; |
|
99 } |
|
100 |
|
101 this.request_animate(); |
|
102 } |
|
103 |
|
104 get_current_point(){ |
|
105 let currLength = this.pathLength * (this.position - this.min) / (this.max - this.min) |
|
106 return this.path_elt.getPointAtLength(currLength); |
|
107 } |
|
108 |
|
109 animate(){ |
|
110 if(this.position == undefined) |
|
111 return; |
|
112 |
|
113 let currPt = this.get_current_point(); |
|
114 this.cursor_transform.setTranslate(currPt.x - this.origPt.x, currPt.y - this.origPt.y); |
|
115 } |
|
116 |
|
117 init() { |
|
118 if(this.args.length == 2) |
|
119 [this.min, this.max]=this.args; |
|
120 |
|
121 this.scanPath(); |
|
122 |
|
123 this.cursor_transform = svg_root.createSVGTransform(); |
|
124 |
|
125 this.cursor_elt.transform.baseVal.appendItem(this.cursor_transform); |
|
126 |
|
127 this.cursor_elt.onpointerdown = (e) => this.on_cursor_down(e); |
|
128 |
|
129 this.bound_drag = this.drag.bind(this); |
|
130 this.bound_drop = this.drop.bind(this); |
|
131 } |
|
132 |
|
133 start_dragging_from_event(e){ |
|
134 let clientPoint = new DOMPoint(e.clientX, e.clientY); |
|
135 let point = clientPoint.matrixTransform(this.invctm); |
|
136 let currPt = this.get_current_point(); |
|
137 this.draggingOffset = new DOMPoint(point.x - currPt.x , point.y - currPt.y); |
|
138 } |
|
139 |
|
140 apply_position_from_event(e){ |
|
141 let clientPoint = new DOMPoint(e.clientX, e.clientY); |
|
142 let rawPoint = clientPoint.matrixTransform(this.invctm); |
|
143 let point = new DOMPoint(rawPoint.x - this.draggingOffset.x , rawPoint.y - this.draggingOffset.y); |
|
144 let [closestPoint, closestLength] = this.closestPoint(point); |
|
145 let new_position = this.min + (this.max - this.min) * closestLength / this.pathLength; |
|
146 this.position = Math.round(Math.max(Math.min(new_position, this.max), this.min)); |
|
147 this.apply_hmi_value(0, this.position); |
|
148 } |
|
149 |
|
150 on_cursor_down(e){ |
|
151 // get scrollbar -> root transform |
|
152 let ctm = this.path_elt.getCTM(); |
|
153 // root -> path transform |
|
154 this.invctm = ctm.inverse(); |
|
155 this.start_dragging_from_event(e); |
|
156 svg_root.addEventListener("pointerup", this.bound_drop, true); |
|
157 svg_root.addEventListener("pointermove", this.bound_drag, true); |
|
158 } |
|
159 |
|
160 drop(e) { |
|
161 svg_root.removeEventListener("pointerup", this.bound_drop, true); |
|
162 svg_root.removeEventListener("pointermove", this.bound_drag, true); |
|
163 } |
|
164 |
|
165 drag(e) { |
|
166 this.apply_position_from_event(e); |
|
167 } |
|
168 || |
|
169 } |
|
170 |
|
171 widget_defs("PathSlider") { |
|
172 labels("cursor path"); |
|
173 } |