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 ? start : Math.round(start / sw) * sw; 228 startx = pos0[0] + ((pos1[0] - pos0[0]) * (s - smin)) / (smax - smin); 229 starty = pos0[1] + ((pos1[1] - pos0[1]) * (s - smin)) / (smax - smin); 230 231 // glider point 232 // attr = Type.copyAttributes(attributes, board.options, "slider"); 233 // overwrite this in any case; the sliders label is a special text element, not the gliders label. 234 // this will be set back to true after the text was created (and only if withlabel was true initially). 235 attr.withlabel = false; 236 // gliders set snapwidth=-1 by default (i.e. deactivate them) 237 p3 = board.create("glider", [startx, starty, l1], attr); 238 p3.setAttribute({ snapwidth: snapWidth, snapvalues: snapValues, snapvaluedistance: snapValueDistance }); 239 240 // Segment from start point to glider point: highline 241 // attr = Type.copyAttributes(attributes, board.options, "slider", "highline"); 242 l2 = board.create("segment", [p1, p3], attr.highline); 243 244 /** 245 * Returns the current slider value. 246 * @memberOf Slider.prototype 247 * @name Value 248 * @function 249 * @returns {Number} 250 */ 251 p3.Value = function () { 252 var d = this._smax - this._smin, 253 ev_sw = this.evalVisProp('snapwidth'); 254 255 return ev_sw === -1 256 ? this.position * d + this._smin 257 : Math.round((this.position * d + this._smin) / ev_sw) * ev_sw; 258 }; 259 260 p3.methodMap = Type.deepCopy(p3.methodMap, { 261 Value: "Value", 262 setValue: "setValue", 263 smax: "_smax", 264 // Max: "_smax", 265 smin: "_smin", 266 // Min: "_smin", 267 setMax: "setMax", 268 setMin: "setMin", 269 point1: "point1", 270 point2: "point2", 271 baseline: "baseline", 272 highline: "highline", 273 ticks: "ticks", 274 label: "label" 275 }); 276 277 /** 278 * End value of the slider range. 279 * @memberOf Slider.prototype 280 * @name _smax 281 * @type Number 282 */ 283 p3._smax = smax; 284 285 /** 286 * Start value of the slider range. 287 * @memberOf Slider.prototype 288 * @name _smin 289 * @type Number 290 */ 291 p3._smin = smin; 292 293 /** 294 * Sets the maximum value of the slider. 295 * @memberOf Slider.prototype 296 * @function 297 * @name setMax 298 * @param {Number} val New maximum value 299 * @returns {Object} this object 300 */ 301 p3.setMax = function (val) { 302 this._smax = val; 303 return this; 304 }; 305 306 /** 307 * Sets the value of the slider. This call must be followed 308 * by a board update call. 309 * @memberOf Slider.prototype 310 * @name setValue 311 * @function 312 * @param {Number} val New value 313 * @returns {Object} this object 314 */ 315 p3.setValue = function (val) { 316 var d = this._smax - this._smin; 317 318 if (Math.abs(d) > Mat.eps) { 319 this.position = (val - this._smin) / d; 320 } else { 321 this.position = 0.0; //this._smin; 322 } 323 this.position = Math.max(0.0, Math.min(1.0, this.position)); 324 return this; 325 }; 326 327 /** 328 * Sets the minimum value of the slider. 329 * @memberOf Slider.prototype 330 * @name setMin 331 * @function 332 * @param {Number} val New minimum value 333 * @returns {Object} this object 334 */ 335 p3.setMin = function (val) { 336 this._smin = val; 337 return this; 338 }; 339 340 if (withText) { 341 // attr = Type.copyAttributes(attributes, board.options, 'slider', 'label'); 342 t = board.create('text', [ 343 function () { 344 return (p2.X() - p1.X()) * 0.05 + p2.X(); 345 }, 346 function () { 347 return (p2.Y() - p1.Y()) * 0.05 + p2.Y(); 348 }, 349 function () { 350 var n, 351 d = p3.evalVisProp('digits'), 352 sl = p3.evalVisProp('suffixlabel'), 353 ul = p3.evalVisProp('unitlabel'), 354 pl = p3.evalVisProp('postlabel'); 355 356 if (d === 2 && p3.evalVisProp('precision') !== 2) { 357 // Backwards compatibility 358 d = p3.evalVisProp('precision'); 359 } 360 361 if (sl !== null) { 362 n = sl; 363 } else if (p3.name && p3.name !== "") { 364 n = p3.name + " = "; 365 } else { 366 n = ""; 367 } 368 369 if (p3.useLocale()) { 370 n += p3.formatNumberLocale(p3.Value(), d); 371 } else { 372 n += Type.toFixed(p3.Value(), d); 373 } 374 375 if (ul !== null) { 376 n += ul; 377 } 378 if (pl !== null) { 379 n += pl; 380 } 381 382 return n; 383 } 384 ], 385 attr.label 386 ); 387 388 /** 389 * The text element to the right of the slider, indicating its current value. 390 * @memberOf Slider.prototype 391 * @name label 392 * @type JXG.Text 393 */ 394 p3.label = t; 395 396 // reset the withlabel attribute 397 p3.visProp.withlabel = true; 398 p3.hasLabel = true; 399 } 400 401 /** 402 * Start point of the base line. 403 * @memberOf Slider.prototype 404 * @name point1 405 * @type JXG.Point 406 */ 407 p3.point1 = p1; 408 409 /** 410 * End point of the base line. 411 * @memberOf Slider.prototype 412 * @name point2 413 * @type JXG.Point 414 */ 415 p3.point2 = p2; 416 417 /** 418 * The baseline the glider is bound to. 419 * @memberOf Slider.prototype 420 * @name baseline 421 * @type JXG.Line 422 */ 423 p3.baseline = l1; 424 425 /** 426 * A line on top of the baseline, indicating the slider's progress. 427 * @memberOf Slider.prototype 428 * @name highline 429 * @type JXG.Line 430 */ 431 p3.highline = l2; 432 433 if (withTicks) { 434 // Function to generate correct label texts 435 436 // attr = Type.copyAttributes(attributes, board.options, "slider", "ticks"); 437 if (!Type.exists(attr.generatelabeltext)) { 438 attr.ticks.generateLabelText = function (tick, zero, value) { 439 var labelText, 440 dFull = p3.point1.Dist(p3.point2), 441 smin = p3._smin, 442 smax = p3._smax, 443 val = (this.getDistanceFromZero(zero, tick) * (smax - smin)) / dFull + smin; 444 445 if (dFull < Mat.eps || Math.abs(val) < Mat.eps) { 446 // Point is zero 447 labelText = "0"; 448 } else { 449 labelText = this.formatLabelText(val); 450 } 451 return labelText; 452 }; 453 } 454 ticks = 2; 455 ti = board.create( 456 "ticks", 457 [ 458 p3.baseline, 459 p3.point1.Dist(p1) / ticks, 460 461 function (tick) { 462 var dFull = p3.point1.Dist(p3.point2), 463 d = p3.point1.coords.distance(Const.COORDS_BY_USER, tick); 464 465 if (dFull < Mat.eps) { 466 return 0; 467 } 468 469 return (d / dFull) * diff + smin; 470 } 471 ], 472 attr.ticks 473 ); 474 475 /** 476 * Ticks give a rough indication about the slider's current value. 477 * @memberOf Slider.prototype 478 * @name ticks 479 * @type JXG.Ticks 480 */ 481 p3.ticks = ti; 482 } 483 484 // override the point's remove method to ensure the removal of all elements 485 p3.remove = function () { 486 if (withText) { 487 board.removeObject(t); 488 } 489 490 board.removeObject(l2); 491 board.removeObject(l1); 492 board.removeObject(p2); 493 board.removeObject(p1); 494 495 Point.prototype.remove.call(p3); 496 }; 497 498 p1.dump = false; 499 p2.dump = false; 500 l1.dump = false; 501 l2.dump = false; 502 if (withText) { 503 t.dump = false; 504 } 505 506 // p3.type = Const.OBJECT_TYPE_SLIDER; // No! type has to be Const.OBJECT_TYPE_GLIDER 507 p3.elType = "slider"; 508 p3.parents = parents; 509 p3.subs = { 510 point1: p1, 511 point2: p2, 512 baseLine: l1, 513 highLine: l2 514 }; 515 p3.inherits.push(p1, p2, l1, l2); 516 // Remove inherits to avoid circular inherits. 517 l1.inherits = []; 518 l2.inherits = []; 519 520 if (withTicks) { 521 ti.dump = false; 522 p3.subs.ticks = ti; 523 p3.inherits.push(ti); 524 } 525 526 p3.getParents = function () { 527 return [ 528 this.point1.coords.usrCoords.slice(1), 529 this.point2.coords.usrCoords.slice(1), 530 [this._smin, this.position * (this._smax - this._smin) + this._smin, this._smax] 531 ]; 532 }; 533 534 p3.baseline.on("up", function (evt) { 535 var pos, c; 536 537 if (p3.evalVisProp('moveonup') && !p3.evalVisProp('fixed')) { 538 pos = l1.board.getMousePosition(evt, 0); 539 c = new Coords(Const.COORDS_BY_SCREEN, pos, this.board); 540 p3.moveTo([c.usrCoords[1], c.usrCoords[2]]); 541 p3.triggerEventHandlers(['drag'], [evt]); 542 } 543 }); 544 545 // This is necessary to show baseline, highline and ticks 546 // when opening the board in case the visible attributes are set 547 // to 'inherit'. 548 p3.prepareUpdate().update(); 549 if (!board.isSuspendedUpdate) { 550 p3.updateVisibility().updateRenderer(); 551 p3.baseline.updateVisibility().updateRenderer(); 552 p3.highline.updateVisibility().updateRenderer(); 553 if (withTicks) { 554 p3.ticks.updateVisibility().updateRenderer(); 555 } 556 } 557 558 return p3; 559 }; 560 561 JXG.registerElement("slider", JXG.createSlider); 562 563 // export default { 564 // createSlider: JXG.createSlider 565 // }; 566