1 /* 2 Copyright 2008-2024 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 50. Initial value is 50. 72 * var s = board.create('slider', [[1, 3], [3, 1], [0, 10, 50]], {snapWidth: 1, ticks: { drawLabels: true }}); 73 * </pre><div class="jxgbox" id="JXGe17128e6-a25d-462a-9074-49460b0d66f4" style="width: 200px; height: 200px;"></div> 74 * <script type="text/javascript"> 75 * (function () { 76 * var board = JXG.JSXGraph.initBoard('JXGe17128e6-a25d-462a-9074-49460b0d66f4', {boundingbox: [-1, 5, 5, -1], axis: true, showcopyright: false, shownavigation: false}); 77 * var s = board.create('slider', [[1, 3], [3, 1], [1, 10, 50]], {snapWidth: 1, ticks: { drawLabels: true }}); 78 * })(); 79 * </script><pre> 80 * @example 81 * // Draggable slider 82 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 83 * visible: true, 84 * snapWidth: 2, 85 * point1: {fixed: false}, 86 * point2: {fixed: false}, 87 * baseline: {fixed: false, needsRegularUpdate: true} 88 * }); 89 * 90 * </pre><div id="JXGbfc67817-2827-44a1-bc22-40bf312e76f8" class="jxgbox" style="width: 300px; height: 300px;"></div> 91 * <script type="text/javascript"> 92 * (function() { 93 * var board = JXG.JSXGraph.initBoard('JXGbfc67817-2827-44a1-bc22-40bf312e76f8', 94 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 95 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 96 * visible: true, 97 * snapWidth: 2, 98 * point1: {fixed: false}, 99 * point2: {fixed: false}, 100 * baseline: {fixed: false, needsRegularUpdate: true} 101 * }); 102 * 103 * })(); 104 * 105 * </script><pre> 106 * 107 * @example 108 * // Set the slider by clicking on the base line: attribute 'moveOnUp' 109 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 110 * snapWidth: 2, 111 * moveOnUp: true // default value 112 * }); 113 * 114 * </pre><div id="JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc" class="jxgbox" style="width: 300px; height: 300px;"></div> 115 * <script type="text/javascript"> 116 * (function() { 117 * var board = JXG.JSXGraph.initBoard('JXGc0477c8a-b1a7-4111-992e-4ceb366fbccc', 118 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 119 * var s1 = board.create('slider', [[-3,1], [2,1],[-10,1,10]], { 120 * snapWidth: 2, 121 * moveOnUp: true // default value 122 * }); 123 * 124 * })(); 125 * 126 * </script><pre> 127 * 128 * @example 129 * // Set colors 130 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 131 * 132 * baseline: { strokeColor: 'blue'}, 133 * highline: { strokeColor: 'red'}, 134 * fillColor: 'yellow', 135 * label: {fontSize: 24, strokeColor: 'orange'}, 136 * name: 'xyz', // Not shown, if suffixLabel is set 137 * suffixLabel: 'x = ', 138 * postLabel: ' u' 139 * 140 * }); 141 * 142 * </pre><div id="JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401" class="jxgbox" style="width: 300px; height: 300px;"></div> 143 * <script type="text/javascript"> 144 * (function() { 145 * var board = JXG.JSXGraph.initBoard('JXGd96c9e2c-2c25-4131-b6cf-9dbb80819401', 146 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 147 * var sl = board.create('slider', [[-3, 1], [1, 1], [-10, 1, 10]], { 148 * 149 * baseline: { strokeColor: 'blue'}, 150 * highline: { strokeColor: 'red'}, 151 * fillColor: 'yellow', 152 * label: {fontSize: 24, strokeColor: 'orange'}, 153 * name: 'xyz', // Not shown, if suffixLabel is set 154 * suffixLabel: 'x = ', 155 * postLabel: ' u' 156 * 157 * }); 158 * 159 * })(); 160 * 161 * </script><pre> 162 * 163 * @example 164 * // Create a "frozen" slider 165 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 166 * name:'a', 167 * point1: {frozen: true}, 168 * point2: {frozen: true} 169 * }); 170 * 171 * </pre><div id="JXG23afea4f-2e91-4006-a505-2895033cf1fc" class="jxgbox" style="width: 300px; height: 300px;"></div> 172 * <script type="text/javascript"> 173 * (function() { 174 * var board = JXG.JSXGraph.initBoard('JXG23afea4f-2e91-4006-a505-2895033cf1fc', 175 * {boundingbox: [-8, 8, 8,-8], axis: true, showcopyright: false, shownavigation: false}); 176 * var sli = board.create('slider', [[-4, 4], [-1.5, 4], [-10, 1, 10]], { 177 * name:'a', 178 * point1: {frozen: true}, 179 * point2: {frozen: true} 180 * }); 181 * 182 * })(); 183 * 184 * </script><pre> 185 * 186 * 187 */ 188 JXG.createSlider = function (board, parents, attributes) { 189 var pos0, pos1, 190 smin, start, smax, diff, 191 p1, p2, p3, l1, l2, 192 ticks, ti, t, 193 startx, starty, 194 withText, withTicks, 195 snapValues, snapValueDistance, 196 snapWidth, sw, s, 197 attr; 198 199 attr = Type.copyAttributes(attributes, board.options, "slider"); 200 withTicks = attr.withticks; 201 withText = attr.withlabel; 202 snapWidth = attr.snapwidth; 203 snapValues = attr.snapvalues; 204 snapValueDistance = attr.snapvaluedistance; 205 206 // Start point 207 p1 = board.create("point", parents[0], attr.point1); 208 209 // End point 210 p2 = board.create("point", parents[1], attr.point2); 211 //g = board.create('group', [p1, p2]); 212 213 // Base line 214 l1 = board.create("segment", [p1, p2], attr.baseline); 215 216 // This is required for a correct projection of the glider onto the segment below 217 l1.updateStdform(); 218 219 pos0 = p1.coords.usrCoords.slice(1); 220 pos1 = p2.coords.usrCoords.slice(1); 221 smin = parents[2][0]; 222 start = parents[2][1]; 223 smax = parents[2][2]; 224 diff = smax - smin; 225 226 sw = Type.evaluate(snapWidth); 227 s = sw === -1 ? 228 start : 229 Math.round((start - smin)/ sw) * sw + smin; 230 // Math.round(start / sw) * sw; 231 startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin); 232 starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin); 233 234 // glider point 235 // attr = Type.copyAttributes(attributes, board.options, "slider"); 236 // overwrite this in any case; the sliders label is a special text element, not the gliders label. 237 // this will be set back to true after the text was created (and only if withlabel was true initially). 238 attr.withlabel = false; 239 // gliders set snapwidth=-1 by default (i.e. deactivate them) 240 p3 = board.create("glider", [startx, starty, l1], attr); 241 p3.setAttribute({ snapwidth: snapWidth, snapvalues: snapValues, snapvaluedistance: snapValueDistance }); 242 243 // Segment from start point to glider point: highline 244 // attr = Type.copyAttributes(attributes, board.options, "slider", "highline"); 245 l2 = board.create("segment", [p1, p3], attr.highline); 246 247 /** 248 * Returns the current slider value. 249 * @memberOf Slider.prototype 250 * @name Value 251 * @function 252 * @returns {Number} 253 */ 254 p3.Value = function () { 255 var d = this._smax - this._smin, 256 ev_sw = this.evalVisProp('snapwidth'); 257 258 return ev_sw === -1 259 ? this.position * d + this._smin 260 : Math.round((this.position * d) / ev_sw) * ev_sw + this._smin; 261 }; 262 263 p3.methodMap = Type.deepCopy(p3.methodMap, { 264 Value: "Value", 265 setValue: "setValue", 266 smax: "_smax", 267 // Max: "_smax", 268 smin: "_smin", 269 // Min: "_smin", 270 setMax: "setMax", 271 setMin: "setMin", 272 point1: "point1", 273 point2: "point2", 274 baseline: "baseline", 275 highline: "highline", 276 ticks: "ticks", 277 label: "label" 278 }); 279 280 /** 281 * End value of the slider range. 282 * @memberOf Slider.prototype 283 * @name _smax 284 * @type Number 285 */ 286 p3._smax = smax; 287 288 /** 289 * Start value of the slider range. 290 * @memberOf Slider.prototype 291 * @name _smin 292 * @type Number 293 */ 294 p3._smin = smin; 295 296 /** 297 * Sets the maximum value of the slider. 298 * @memberOf Slider.prototype 299 * @function 300 * @name setMax 301 * @param {Number} val New maximum value 302 * @returns {Object} this object 303 */ 304 p3.setMax = function (val) { 305 this._smax = val; 306 return this; 307 }; 308 309 /** 310 * Sets the value of the slider. This call must be followed 311 * by a board update call. 312 * @memberOf Slider.prototype 313 * @name setValue 314 * @function 315 * @param {Number} val New value 316 * @returns {Object} this object 317 */ 318 p3.setValue = function (val) { 319 var d = this._smax - this._smin; 320 321 if (Math.abs(d) > Mat.eps) { 322 this.position = (val - this._smin) / d; 323 } else { 324 this.position = 0.0; //this._smin; 325 } 326 this.position = Math.max(0.0, Math.min(1.0, this.position)); 327 return this; 328 }; 329 330 /** 331 * Sets the minimum value of the slider. 332 * @memberOf Slider.prototype 333 * @name setMin 334 * @function 335 * @param {Number} val New minimum value 336 * @returns {Object} this object 337 */ 338 p3.setMin = function (val) { 339 this._smin = val; 340 return this; 341 }; 342 343 if (withText) { 344 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 345 t = board.create('text', [ 346 function () { 347 return (p2.X() - p1.X()) * 0.05 + p2.X(); 348 }, 349 function () { 350 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 351 }, 352 function () { 353 var n, 354 d = p3.evalVisProp('digits'), 355 sl = p3.evalVisProp('suffixlabel'), 356 ul = p3.evalVisProp('unitlabel'), 357 pl = p3.evalVisProp('postlabel'); 358 359 if (d === 2 && p3.evalVisProp('precision') !== 2) { 360 // Backwards compatibility 361 d = p3.evalVisProp('precision'); 362 } 363 364 if (sl !== null) { 365 n = sl; 366 } else if (p3.name && p3.name !== "") { 367 n = p3.name + " = "; 368 } else { 369 n = ""; 370 } 371 372 if (p3.useLocale()) { 373 n += p3.formatNumberLocale(p3.Value(), d); 374 } else { 375 n += Type.toFixed(p3.Value(), d); 376 } 377 378 if (ul !== null) { 379 n += ul; 380 } 381 if (pl !== null) { 382 n += pl; 383 } 384 385 return n; 386 } 387 ], 388 attr.label 389 ); 390 391 /** 392 * The text element to the right of the slider, indicating its current value. 393 * @memberOf Slider.prototype 394 * @name label 395 * @type JXG.Text 396 */ 397 p3.label = t; 398 399 // reset the withlabel attribute 400 p3.visProp.withlabel = true; 401 p3.hasLabel = true; 402 } 403 404 /** 405 * Start point of the base line. 406 * @memberOf Slider.prototype 407 * @name point1 408 * @type JXG.Point 409 */ 410 p3.point1 = p1; 411 412 /** 413 * End point of the base line. 414 * @memberOf Slider.prototype 415 * @name point2 416 * @type JXG.Point 417 */ 418 p3.point2 = p2; 419 420 /** 421 * The baseline the glider is bound to. 422 * @memberOf Slider.prototype 423 * @name baseline 424 * @type JXG.Line 425 */ 426 p3.baseline = l1; 427 428 /** 429 * A line on top of the baseline, indicating the slider's progress. 430 * @memberOf Slider.prototype 431 * @name highline 432 * @type JXG.Line 433 */ 434 p3.highline = l2; 435 436 if (withTicks) { 437 // Function to generate correct label texts 438 439 // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 440 if (!Type.exists(attr.generatelabeltext)) { 441 attr.ticks.generateLabelText = function (tick, zero, value) { 442 var labelText, 443 dFull = p3.point1.Dist(p3.point2), 444 smin = p3._smin, 445 smax = p3._smax, 446 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 447 448 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 449 // Point is zero 450 labelText = "0"; 451 } else { 452 labelText = this.formatLabelText(val); 453 } 454 return labelText; 455 }; 456 } 457 ticks = 2; 458 ti = board.create( 459 "ticks", 460 [ 461 p3.baseline, 462 p3.point1.Dist(p1) / ticks, 463 464 function (tick) { 465 var dFull = p3.point1.Dist(p3.point2), 466 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 467 468 if (dFull < Mat.eps) { 469 return 0; 470 } 471 472 return (d / dFull) * diff + smin; 473 } 474 ], 475 attr.ticks 476 ); 477 478 /** 479 * Ticks give a rough indication about the slider's current value. 480 * @memberOf Slider.prototype 481 * @name ticks 482 * @type JXG.Ticks 483 */ 484 p3.ticks = ti; 485 } 486 487 // override the point's remove method to ensure the removal of all elements 488 p3.remove = function () { 489 if (withText) { 490 board.removeObject(t); 491 } 492 493 board.removeObject(l2); 494 board.removeObject(l1); 495 board.removeObject(p2); 496 board.removeObject(p1); 497 498 Point.prototype.remove.call(p3); 499 }; 500 501 p1.dump = false; 502 p2.dump = false; 503 l1.dump = false; 504 l2.dump = false; 505 if (withText) { 506 t.dump = false; 507 } 508 509 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 510 p3.elType = "slider"; 511 p3.parents = parents; 512 p3.subs = { 513 point1: p1, 514 point2: p2, 515 baseLine: l1, 516 highLine: l2 517 }; 518 p3.inherits.push(p1, p2, l1, l2); 519 // Remove inherits to avoid circular inherits. 520 l1.inherits = []; 521 l2.inherits = []; 522 523 if (withTicks) { 524 ti.dump = false; 525 p3.subs.ticks = ti; 526 p3.inherits.push(ti); 527 } 528 529 p3.getParents = function () { 530 return [ 531 this.point1.coords.usrCoords.slice(1), 532 this.point2.coords.usrCoords.slice(1), 533 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 534 ]; 535 }; 536 537 p3.baseline.on("up", function (evt) { 538 var pos, c; 539 540 if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) { 541 pos = l1.board.getMousePosition(evt, 0); 542 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 543 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 544 p3.triggerEventHandlers(['drag'], [evt]); 545 } 546 }); 547 548 // This is necessary to show baseline, highline and ticks 549 // when opening the board in case the visible attributes are set 550 // to 'inherit'. 551 p3.prepareUpdate().update(); 552 if (!board.isSuspendedUpdate) { 553 p3.updateVisibility().updateRenderer(); 554 p3.baseline.updateVisibility().updateRenderer(); 555 p3.highline.updateVisibility().updateRenderer(); 556 if (withTicks) { 557 p3.ticks.updateVisibility().updateRenderer(); 558 } 559 } 560 561 return p3; 562 }; 563 564 JXG.registerElement("slider", JXG.createSlider); 565 566 // export default { 567 // createSlider: JXG.createSlider 568 // }; 569