1 /* 2 Copyright 2008-2026 3 Matthias Ehmann, 4 Michael Gerhaeuser, 5 Carsten Miller, 6 Bianca Valentin, 7 Alfred Wassermann, 8 Peter Wilfahrt 9 10 This file is part of JSXGraph. 11 12 JSXGraph is free software dual licensed under the GNU LGPL or MIT License. 13 14 You can redistribute it and/or modify it under the terms of the 15 16 * GNU Lesser General Public License as published by 17 the Free Software Foundation, either version 3 of the License, or 18 (at your option) any later version 19 OR 20 * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT 21 22 JSXGraph is distributed in the hope that it will be useful, 23 but WITHOUT ANY WARRANTY; without even the implied warranty of 24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 GNU Lesser General Public License for more details. 26 27 You should have received a copy of the GNU Lesser General Public License and 28 the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/> 29 and <https://opensource.org/licenses/MIT/>. 30 */ 31 32 /*global JXG: true, define: true*/ 33 /*jslint nomen: true, plusplus: true*/ 34 35 /** 36 * @fileoverview The geometry object slider is defined in this file. Slider stores all 37 * style and functional properties that are required to draw and use a slider on 38 * a board. 39 */ 40 41 import JXG from "../jxg.js"; 42 import Mat from "../math/math.js"; 43 import Const from "../base/constants.js"; 44 import Coords from "../base/coords.js"; 45 import Type from "../utils/type.js"; 46 import Point from "../base/point.js"; 47 48 /** 49 * @class A slider can be used to choose values from a given range of numbers. 50 * @pseudo 51 * @name Slider 52 * @augments Glider 53 * @constructor 54 * @type JXG.Point 55 * @throws {Exception} If the element cannot be constructed with the given parent objects an exception is thrown. 56 * @param {Array_Array_Array} start,end,range The first two arrays give the start and the end where the slider is drawn 57 * on the board. The third array gives the start and the end of the range the slider operates as the first resp. the 58 * third component of the array. The second component of the third array gives its start value. 59 * 60 * @example 61 * // Create a slider with values between 1 and 10, initial position is 5. 62 * var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]); 63 * </pre><div class="jxgbox" id="JXGcfb51cde-2603-4f18-9cc4-1afb452b374d" style="width: 200px; height: 200px;"></div> 64 * <script type="text/javascript"> 65 * (function () { 66 * var board = JXG.JSXGraph.initBoard('JXGcfb51cde-2603-4f18-9cc4-1afb452b374d', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false}); 67 * var s = board.create('slider', [[1, 2], [3, 2], [1, 5, 10]]); 68 * })(); 69 * </script><pre> 70 * @example 71 * // Create a slider taking integer values between 1 and 5. Initial value is 3. 72 * var s = board.create('slider', [[1, 3], [3, 1], [0, 3, 5]], { 73 * snapWidth: 1, 74 * minTicksDistance: 60, 75 * drawLabels: false 76 * }); 77 * </pre><div class="jxgbox" id="JXGe17128e6-a25d-462a-9074-49460b0d66f4" style="width: 200px; height: 200px;"></div> 78 * <script type="text/javascript"> 79 * (function () { 80 * var board = JXG.JSXGraph.initBoard('JXGe17128e6-a25d-462a-9074-49460b0d66f4', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false}); 81 * var s = board.create('slider', [[1, 3], [3, 1], [1, 3, 5]], { 82 * snapWidth: 1, 83 * minTicksDistance: 60, 84 * drawLabels: false 85 * }); 86 * })(); 87 * </script><pre> 88 * @example 89 * // Draggable slider 90 * var s1 = board.create('slider', [[-3, 1], [2, 1],[-10, 1, 10]], { 91 * visible: true, 92 * snapWidth: 2, 93 * point1: {fixed: false}, 94 * point2: {fixed: false}, 95 * baseline: {fixed: false, needsRegularUpdate: true} 96 * }); 97 * 98 * </pre><div id="JXGbfc67817-2827-44a1-bc22-40bf312e76f8" class="jxgbox" style="width: 300px; height: 300px;"></div> 99 * <script type="text/javascript"> 100 * (function() { 101 * var board = JXG.JSXGraph.initBoard('JXGbfc67817-2827-44a1-bc22-40bf312e76f8', 102 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 103 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 104 * visible: true, 105 * snapWidth: 2, 106 * point1: {fixed: false}, 107 * point2: {fixed: false}, 108 * baseline: {fixed: false, needsRegularUpdate: true} 109 * }); 110 * 111 * })(); 112 * 113 * </script><pre> 114 * 115 * @example 116 * // Set the slider by clicking on the base line: attribute 'moveOnUp' 117 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 118 * snapWidth: 2, 119 * moveOnUp: true // default value 120 * }); 121 * 122 * </pre><div id="JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc" class="jxgbox" style="width: 300px; height: 300px;"></div> 123 * <script type="text/javascript"> 124 * (function() { 125 * var board = JXG.JSXGraph.initBoard('JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc', 126 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 127 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 128 * snapWidth: 2, 129 * moveOnUp: true // default value 130 * }); 131 * 132 * })(); 133 * 134 * </script><pre> 135 * 136 * @example 137 * // Set colors 138 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 139 * 140 * baseline: { strokeColor: 'blue'}, 141 * highline: { strokeColor: 'red'}, 142 * fillColor: 'yellow', 143 * label: {fontSize: 24, strokeColor: 'orange'}, 144 * name: 'xyz', // Not shown, if suffixLabel is set 145 * suffixLabel: 'x = ', 146 * postLabel: ' u' 147 * 148 * }); 149 * 150 * </pre><div id="JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401" class="jxgbox" style="width: 300px; height: 300px;"></div> 151 * <script type="text/javascript"> 152 * (function() { 153 * var board = JXG.JSXGraph.initBoard('JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401', 154 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 155 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 156 * 157 * baseline: { strokeColor: 'blue'}, 158 * highline: { strokeColor: 'red'}, 159 * fillColor: 'yellow', 160 * label: {fontSize: 24, strokeColor: 'orange'}, 161 * name: 'xyz', // Not shown, if suffixLabel is set 162 * suffixLabel: 'x = ', 163 * postLabel: ' u' 164 * 165 * }); 166 * 167 * })(); 168 * 169 * </script><pre> 170 * 171 * @example 172 * // Create a "frozen" slider 173 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 174 * name:'a', 175 * frozen: true 176 * }); 177 * 178 * </pre><div id="JXG23afea4f-2e91-4006-a505-2895033cf1fc" class="jxgbox" style="width: 300px; height: 300px;"></div> 179 * <script type="text/javascript"> 180 * (function() { 181 * var board = JXG.JSXGraph.initBoard('JXG23afea4f-2e91-4006-a505-2895033cf1fc', 182 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 183 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 184 * name:'a', 185 * frozen: true 186 * }); 187 * 188 * })(); 189 * 190 * </script><pre> 191 * 192 * @example 193 * // Use MathJax for slider label (don't forget to load MathJax) 194 * var s = board.create('slider', [[-3, 2], [2, 2], [-10, 1, 10]], { 195 * name: 'A^{(2)}', 196 * suffixLabel: '\\(A^{(2)} = ', 197 * unitLabel: ' \\;km/h ', 198 * postLabel: '\\)', 199 * label: {useMathJax: true} 200 * }); 201 * 202 * </pre><div id="JXG76e78c5f-3598-4d44-b43f-1d78fd15302c" class="jxgbox" style="width: 300px; height: 300px;"></div> 203 * <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" id="MathJax-script"></script> 204 * <script type="text/javascript"> 205 * (function() { 206 * var board = JXG.JSXGraph.initBoard('JXG76e78c5f-3598-4d44-b43f-1d78fd15302c', 207 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 208 * // Use MathJax for slider label (don't forget to load MathJax) 209 * var s = board.create('slider', [[-3,2], [2,2],[-10,1,10]], { 210 * name: 'A^{(2)}', 211 * suffixLabel: '\\(A^{(2)} = ', 212 * unitLabel: ' \\;km/h ', 213 * postLabel: '\\)', 214 * label: {useMathJax: true} 215 * }); 216 * 217 * })(); 218 * 219 * </script><pre> 220 * 221 * 222 */ 223 JXG.createSlider = function (board, parents, attributes) { 224 var pos0, pos1, 225 smin, start, smax, diff, 226 p1, p2, p3, l1, l2, 227 ticks, ti, t, 228 startx, starty, 229 withText, withTicks, 230 snapValues, snapValueDistance, 231 snapWidth, sw, s, 232 attr; 233 234 attr = Type.copyAttributes(attributes, board.options, 'slider'); 235 withTicks = attr.withticks; 236 withText = attr.withlabel; 237 snapWidth = attr.snapwidth; 238 snapValues = attr.snapvalues; 239 snapValueDistance = attr.snapvaluedistance; 240 241 // Start point 242 p1 = board.create("point", parents[0], attr.point1); 243 244 // End point 245 p2 = board.create("point", parents[1], attr.point2); 246 //g = board.create('group', [p1, p2]); 247 248 // Base line 249 l1 = board.create("segment", [p1, p2], attr.baseline); 250 251 // This is required for a correct projection of the glider onto the segment below 252 l1.updateStdform(); 253 254 pos0 = p1.coords.usrCoords.slice(1); 255 pos1 = p2.coords.usrCoords.slice(1); 256 smin = parents[2][0]; 257 start = parents[2][1]; 258 smax = parents[2][2]; 259 diff = smax - smin; 260 261 sw = Type.evaluate(snapWidth); 262 s = sw === -1 ? 263 start : 264 Math.round((start - smin)/ sw) * sw + smin; 265 // Math.round(start / sw) * sw; 266 startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin); 267 starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin); 268 269 // glider point 270 // attr = Type.copyAttributes(attributes, board.options, 'slider'); 271 // overwrite this in any case; the sliders label is a special text element, not the gliders label. 272 // this will be set back to true after the text was created (and only if withlabel was true initially). 273 attr.withlabel = false; 274 // gliders set snapwidth=-1 by default (i.e. deactivate them) 275 p3 = board.create("glider", [startx, starty, l1], attr); 276 p3.setAttribute({ snapwidth: snapWidth, snapvalues: snapValues, snapvaluedistance: snapValueDistance }); 277 278 // Segment from start point to glider point: highline 279 // attr = Type.copyAttributes(attributes, board.options, "slider", 'highline'); 280 l2 = board.create("segment", [p1, p3], attr.highline); 281 282 /** 283 * Returns the current slider value. 284 * @memberOf Slider.prototype 285 * @name Value 286 * @function 287 * @returns {Number} 288 */ 289 p3.Value = function () { 290 var d = this._smax - this._smin, 291 ev_sw = this.evalVisProp('snapwidth'); 292 293 return ev_sw === -1 294 ? this.position * d + this._smin 295 : Math.round((this.position * d) / ev_sw) * ev_sw + this._smin; 296 }; 297 298 p3.methodMap = Type.deepCopy(p3.methodMap, { 299 Value: "Value", 300 setValue: "setValue", 301 smax: "_smax", 302 // Max: "_smax", 303 smin: "_smin", 304 // Min: "_smin", 305 setMax: "setMax", 306 setMin: "setMin", 307 point1: "point1", 308 point2: "point2", 309 baseline: "baseline", 310 highline: "highline", 311 ticks: "ticks", 312 label: "label" 313 }); 314 315 /** 316 * End value of the slider range. 317 * @memberOf Slider.prototype 318 * @name _smax 319 * @type Number 320 */ 321 p3._smax = smax; 322 323 /** 324 * Start value of the slider range. 325 * @memberOf Slider.prototype 326 * @name _smin 327 * @type Number 328 */ 329 p3._smin = smin; 330 331 /** 332 * Sets the maximum value of the slider. 333 * @memberOf Slider.prototype 334 * @function 335 * @name setMax 336 * @param {Number} val New maximum value 337 * @returns {Object} this object 338 */ 339 p3.setMax = function (val) { 340 this._smax = val; 341 return this; 342 }; 343 344 /** 345 * Sets the value of the slider. This call must be followed 346 * by a board update call. 347 * @memberOf Slider.prototype 348 * @name setValue 349 * @function 350 * @param {Number} val New value 351 * @returns {Object} this object 352 */ 353 p3.setValue = function (val) { 354 var d = this._smax - this._smin; 355 356 if (Math.abs(d) > Mat.eps) { 357 this.position = (val - this._smin) / d; 358 } else { 359 this.position = 0.0; //this._smin; 360 } 361 this.position = Math.max(0.0, Math.min(1.0, this.position)); 362 return this; 363 }; 364 365 /** 366 * Sets the minimum value of the slider. 367 * @memberOf Slider.prototype 368 * @name setMin 369 * @function 370 * @param {Number} val New minimum value 371 * @returns {Object} this object 372 */ 373 p3.setMin = function (val) { 374 this._smin = val; 375 return this; 376 }; 377 378 if (withText) { 379 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 380 t = board.create('text', [ 381 function () { 382 return (p2.X() - p1.X()) * 0.05 + p2.X(); 383 }, 384 function () { 385 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 386 }, 387 function () { 388 var n, 389 d = p3.evalVisProp('digits'), 390 sl = p3.evalVisProp('suffixlabel'), 391 ul = p3.evalVisProp('unitlabel'), 392 pl = p3.evalVisProp('postlabel'); 393 394 if (d === 2 && p3.evalVisProp('precision') !== 2) { 395 // Backwards compatibility 396 d = p3.evalVisProp('precision'); 397 } 398 399 if (sl !== null) { 400 n = sl; 401 } else if (p3.name && p3.name !== "") { 402 n = p3.name + " = "; 403 } else { 404 n = ""; 405 } 406 407 if (p3.useLocale()) { 408 n += p3.formatNumberLocale(p3.Value(), d); 409 } else { 410 n += Type.toFixed(p3.Value(), d); 411 } 412 413 if (ul !== null) { 414 n += ul; 415 } 416 if (pl !== null) { 417 n += pl; 418 } 419 420 return n; 421 } 422 ], 423 attr.label 424 ); 425 426 /** 427 * The text element to the right of the slider, indicating its current value. 428 * @memberOf Slider.prototype 429 * @name label 430 * @type JXG.Text 431 */ 432 p3.label = t; 433 434 // reset the withlabel attribute 435 p3.visProp.withlabel = true; 436 p3.hasLabel = true; 437 p3.inherits.push(t); 438 t.addParents(p3); 439 p3.addChild(t); 440 } 441 442 /** 443 * Start point of the base line. 444 * @memberOf Slider.prototype 445 * @name point1 446 * @type JXG.Point 447 */ 448 p3.point1 = p1; 449 450 /** 451 * End point of the base line. 452 * @memberOf Slider.prototype 453 * @name point2 454 * @type JXG.Point 455 */ 456 p3.point2 = p2; 457 458 /** 459 * The baseline the glider is bound to. 460 * @memberOf Slider.prototype 461 * @name baseline 462 * @type JXG.Line 463 */ 464 p3.baseline = l1; 465 466 /** 467 * A line on top of the baseline, indicating the slider's progress. 468 * @memberOf Slider.prototype 469 * @name highline 470 * @type JXG.Line 471 */ 472 p3.highline = l2; 473 474 if (withTicks) { 475 // Function to generate correct label texts 476 477 // attr = Type.copyAttributes(attributes, board.options, "slider", 'ticks'); 478 if (!Type.exists(attr.generatelabeltext)) { 479 attr.ticks.generateLabelText = function (tick, zero, value) { 480 var labelText, 481 dFull = p3.point1.Dist(p3.point2), 482 smin = p3._smin, 483 smax = p3._smax, 484 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 485 486 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 487 // Point is zero 488 labelText = '0'; 489 } else { 490 labelText = this.formatLabelText(val); 491 } 492 return labelText; 493 }; 494 } 495 ticks = 2; 496 ti = board.create( 497 "ticks", 498 [ 499 p3.baseline, 500 p3.point1.Dist(p1) / ticks, 501 502 function (tick) { 503 var dFull = p3.point1.Dist(p3.point2), 504 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 505 506 if (dFull < Mat.eps) { 507 return 0; 508 } 509 510 return (d / dFull) * diff + smin; 511 } 512 ], 513 attr.ticks 514 ); 515 516 /** 517 * Ticks give a rough indication about the slider's current value. 518 * @memberOf Slider.prototype 519 * @name ticks 520 * @type JXG.Ticks 521 */ 522 p3.ticks = ti; 523 } 524 525 // override the point's remove method to ensure the removal of all elements 526 p3.remove = function () { 527 if (withText) { 528 board.removeObject(t); 529 } 530 531 board.removeObject(l2); 532 board.removeObject(l1); 533 board.removeObject(p2); 534 board.removeObject(p1); 535 536 Point.prototype.remove.call(p3); 537 }; 538 539 p1.dump = false; 540 p2.dump = false; 541 l1.dump = false; 542 l2.dump = false; 543 if (withText) { 544 t.dump = false; 545 } 546 547 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 548 p3.elType = 'slider'; 549 p3.parents = parents; 550 p3.subs = { 551 point1: p1, 552 point2: p2, 553 baseLine: l1, 554 highLine: l2 555 }; 556 p3.inherits.push(p1, p2, l1, l2); 557 // Remove inherits to avoid circular inherits. 558 l1.inherits = []; 559 l2.inherits = []; 560 561 if (withTicks) { 562 ti.dump = false; 563 p3.subs.ticks = ti; 564 p3.inherits.push(ti); 565 } 566 567 p3.getParents = function () { 568 return [ 569 this.point1.coords.usrCoords.slice(1), 570 this.point2.coords.usrCoords.slice(1), 571 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 572 ]; 573 }; 574 575 p3.baseline.on("up", function (evt) { 576 var pos, c; 577 578 if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) { 579 pos = l1.board.getMousePosition(evt, 0); 580 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 581 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 582 p3.triggerEventHandlers(['drag'], [evt]); 583 } 584 }); 585 586 // This is necessary to show baseline, highline and ticks 587 // when opening the board in case the visible attributes are set 588 // to 'inherit'. 589 p3.prepareUpdate().update(); 590 if (!board.isSuspendedUpdate) { 591 p3.updateVisibility().updateRenderer(); 592 p3.baseline.prepareUpdate().updateVisibility().updateRenderer(); // prepareUpdate needed because needsRegularUpdate==false 593 p3.highline.updateVisibility().updateRenderer(); 594 if (withTicks) { 595 p3.prepareUpdate().ticks.updateVisibility().updateRenderer(); 596 } 597 } 598 599 return p3; 600 }; 601 602 JXG.registerElement("slider", JXG.createSlider); 603 604 // export default { 605 // createSlider: JXG.createSlider 606 // }; 607