1(function(win) { 2 3 // Constants 4 5 var PRECISION = 1e2; // 2 decimals 6 var VENDOR_PREFIXES = ['webkit','moz', '']; 7 var SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 8 var CSS_PREFIX = 'peel-'; 9 10 var clipProperty, transformProperty, boxShadowProperty, filterProperty; 11 var backgroundGradientSupport; 12 var docEl = document.documentElement; 13 var style = docEl.style; 14 15 16 // Support 17 18 function getCssProperty(name) { 19 var prefix, str; 20 for (var i = 0; i < VENDOR_PREFIXES.length; i++) { 21 prefix = VENDOR_PREFIXES[i]; 22 str = prefix ? prefix + capitalize(name) : name; 23 if (str in style) { 24 return str; 25 } 26 } 27 } 28 29 function setCssProperties() { 30 clipProperty = getCssProperty('clipPath'); 31 transformProperty = getCssProperty('transform'); 32 boxShadowProperty = getCssProperty('boxShadow'); 33 filterProperty = getCssProperty('filter'); 34 setBackgroundGradientSupport(); 35 Peel.supported = !!(clipProperty && transformProperty); 36 Peel.effectsSupported = backgroundGradientSupport; 37 } 38 39 function setBackgroundGradientSupport() { 40 var el = document.createElement('div'); 41 var style = el.style; 42 style.cssText = 'background:linear-gradient(45deg,#9f9,white);'; 43 backgroundGradientSupport = (style.backgroundImage || '').indexOf('gradient') > -1; 44 } 45 46 47 48 // General helpers 49 50 function round(n) { 51 return Math.round(n * PRECISION) / PRECISION; 52 } 53 54 // Clamps the number to be between 0 and 1. 55 function clamp(n) { 56 return Math.max(0, Math.min(1, n)); 57 } 58 59 function normalize(n, min, max) { 60 return (n - min) / (max - min); 61 } 62 63 // Distributes a number between 0 and 1 along a bell curve. 64 function distribute(t, mult) { 65 return (mult || 1) * 2 * (.5 - Math.abs(t - .5)); 66 } 67 68 function capitalize(str) { 69 return str.slice(0,1).toUpperCase() + str.slice(1); 70 } 71 72 function camelize(str) { 73 return str.replace(/-(\w)/g, function(a, b) { 74 return b.toUpperCase(); 75 }); 76 } 77 78 function prefix(str) { 79 return CSS_PREFIX + str; 80 } 81 82 // CSS Helpers 83 84 function setCSSClip(el, clip) { 85 el.style[clipProperty] = clip; 86 } 87 88 function setTransform(el, t) { 89 el.style[transformProperty] = t; 90 } 91 92 function setBoxShadow(el, x, y, blur, spread, intensity) { 93 el.style[boxShadowProperty] = getShadowCss(x, y, blur, spread, intensity); 94 } 95 96 function setDropShadow(el, x, y, blur, intensity) { 97 el.style[filterProperty] = 'drop-shadow(' + getShadowCss(x, y, blur, null, intensity) + ')'; 98 } 99 100 function getShadowCss(x, y, blur, spread, intensity) { 101 return round(x) + 'px ' + 102 round(y) + 'px ' + 103 round(blur) + 'px ' + 104 (spread ? round(spread) + 'px ' : '') + 105 'rgba(0,0,0,' + round(intensity) + ')'; 106 } 107 108 function setOpacity(el, t) { 109 el.style.opacity = t; 110 } 111 112 function setBackgroundGradient(el, rotation, stops) { 113 if (!backgroundGradientSupport) return; 114 var css; 115 if (stops.length === 0) { 116 css = 'none'; 117 } else { 118 css = 'linear-gradient(' + round(rotation) + 'deg,' + stops.join(',') + ')'; 119 } 120 el.style.backgroundImage = css; 121 } 122 123 // Event Helpers 124 125 function addEvent(el, type, fn) { 126 el.addEventListener(type, fn) 127 } 128 129 function removeEvent(el, type, fn) { 130 el.removeEventListener(type, fn); 131 } 132 133 function getEventCoordinates(evt, el) { 134 var pos = evt.changedTouches ? evt.changedTouches[0] : evt; 135 return { 136 'x': pos.clientX - el.offsetLeft + window.scrollX, 137 'y': pos.clientY - el.offsetTop + window.scrollY 138 } 139 } 140 141 function bindWithEvent(fn, scope, arg1, arg2) { 142 return function(evt) { 143 fn.call(scope, evt, arg1, arg2); 144 } 145 } 146 147 // Color Helpers 148 149 function getBlackStop(a, pos) { 150 return getColorStop(0, 0, 0, a, pos); 151 } 152 153 function getWhiteStop(a, pos) { 154 return getColorStop(255, 255, 255, a, pos); 155 } 156 157 function getColorStop(r, g, b, a, pos) { 158 a = round(clamp(a)); 159 return 'rgba('+ r +','+ g +','+ b +','+ a +') ' + round(pos * 100) + '%'; 160 } 161 162 163 // DOM Element Helpers 164 165 function getElement(obj, node) { 166 if (typeof obj === 'string') { 167 obj = (node || document).querySelector(obj); 168 } 169 return obj; 170 } 171 172 function createElement(parent, className) { 173 var el = document.createElement('div'); 174 addClass(el, className); 175 parent.appendChild(el); 176 return el; 177 } 178 179 function removeClass(el, str) { 180 el.classList.remove(str); 181 } 182 183 function addClass(el, str) { 184 el.classList.add(str); 185 } 186 187 function getZIndex(el) { 188 return el.style.zIndex; 189 } 190 191 function setZIndex(el, index) { 192 el.style.zIndex = index; 193 } 194 195 196 // SVG Helpers 197 198 function createSVGElement(tag, parent, attributes) { 199 parent = parent || docEl; 200 var el = document.createElementNS(SVG_NAMESPACE, tag); 201 parent.appendChild(el); 202 for (var key in attributes) { 203 if (!attributes.hasOwnProperty(key)) continue; 204 setSVGAttribute(el, key, attributes[key]); 205 } 206 return el; 207 } 208 209 function setSVGAttribute(el, key, value) { 210 el.setAttributeNS(null, key, value); 211 } 212 213 214 /** 215 * Main class that controls the peeling effect. 216 * @param {HTMLElement|string} el The main container element (can be query). 217 * @param {object} options Options for the effect. 218 * @constructor 219 * @public 220 */ 221 function Peel (el, opt) { 222 this.setOptions(opt); 223 this.el = getElement(el, docEl); 224 this.constraints = []; 225 this.events = []; 226 this.setupLayers(); 227 this.setupDimensions(); 228 this.setCorner(this.getOption('corner')); 229 this.setMode(this.getOption('mode')); 230 this.init(); 231 } 232 233 /** 234 * Four constants representing the corners of the element from which peeling can occur. 235 * @constant 236 * @public 237 */ 238 Peel.Corners = { 239 TOP_LEFT: 0x0, 240 TOP_RIGHT: 0x1, 241 BOTTOM_LEFT: 0x2, 242 BOTTOM_RIGHT: 0x3 243 } 244 245 /** 246 * Defaults 247 * @constant 248 */ 249 Peel.Defaults = { 250 'topShadow': true, 251 'topShadowBlur': 5, 252 'topShadowAlpha': .5, 253 'topShadowOffsetX': 0, 254 'topShadowOffsetY': 1, 255 'topShadowCreatesShape': true, 256 257 'backReflection': false, 258 'backReflectionSize': .02, 259 'backReflectionOffset': 0, 260 'backReflectionAlpha': .15, 261 'backReflectionDistribute': true, 262 263 'backShadow': true, 264 'backShadowSize': .04, 265 'backShadowOffset': 0, 266 'backShadowAlpha': .1, 267 'backShadowDistribute': true, 268 269 'bottomShadow': true, 270 'bottomShadowSize': 1.5, 271 'bottomShadowOffset': 0, 272 'bottomShadowDarkAlpha': .7, 273 'bottomShadowLightAlpha': .1, 274 'bottomShadowDistribute': true, 275 276 'setPeelOnInit': true, 277 'clippingBoxScale': 4, 278 'flipConstraintOffset': 5, 279 'dragPreventsDefault': true 280 } 281 282 /** 283 * Sets the corner for the peel effect to happen from (default is bottom right). 284 * @param {Mixed} [...] Either x,y or a corner id. 285 * @public 286 */ 287 Peel.prototype.setCorner = function() { 288 var args = arguments; 289 if (args[0] === undefined) { 290 args = [Peel.Corners.BOTTOM_RIGHT]; 291 } else if (args[0].length) { 292 args = args[0]; 293 } 294 this.corner = this.getPointOrCorner(args); 295 } 296 297 /** 298 * Sets a pre-defined "mode". 299 * @param {string} mode The mode to set. 300 * @public 301 */ 302 Peel.prototype.setMode = function(mode) { 303 if (mode === 'book') { 304 // The order of constraints is important here so that the peel line 305 // approaches the horizontal smoothly without jumping. 306 this.addPeelConstraint(Peel.Corners.BOTTOM_LEFT); 307 this.addPeelConstraint(Peel.Corners.TOP_LEFT); 308 // Removing effect distribution will make the book still have some 309 // depth to the effect while fully open. 310 this.setOption('backReflection', false); 311 this.setOption('backShadowDistribute', false); 312 this.setOption('bottomShadowDistribute', false); 313 } else if (mode === 'calendar') { 314 this.addPeelConstraint(Peel.Corners.TOP_RIGHT); 315 this.addPeelConstraint(Peel.Corners.TOP_LEFT); 316 } 317 } 318 319 /** 320 * Sets a path along which the peel will follow. 321 * Can be a flat line segment or a bezier curve. 322 * @param {...number} x/y Points along the path. 4 arguments indicates a 323 * linear path along 2 points (p1 to p2), while 8 arguments indicates a 324 * bezier curve from p1 to p2 using control points c1 and c2. The first 325 * and last two arguments represent p1 and p2, respectively. 326 * @public 327 */ 328 Peel.prototype.setPeelPath = function(x1, y1) { 329 var args = arguments, p1, p2, c1, c2; 330 p1 = new Point(x1, y1); 331 if (args.length === 4) { 332 p2 = new Point(args[2], args[3]); 333 this.path = new LineSegment(p1, p2); 334 } else if (args.length === 8) { 335 c1 = new Point(args[2], args[3]); 336 c2 = new Point(args[4], args[5]); 337 p2 = new Point(args[6], args[7]); 338 this.path = new BezierCurve(p1, c1, c2, p2); 339 } 340 } 341 342 /** 343 * Sets a function to be called when the user drags, either with a mouse or 344 * with a finger (touch events). 345 * @param {Function} fn The function to be called on drag. This function will 346 * be called with the Peel instance as the "this" keyword, the original 347 * event as the first argument, and the x, y coordinates of the drag as 348 * the 2nd and 3rd arguments, respectively. 349 * @param {HTMLElement} el The element to initiate the drag on mouse/touch start. 350 * If not passed, this will be the element associated with the Peel 351 * instance. Allowing this to be passed lets another element serve as a 352 * "hit area" that can be larger than the element itself. 353 * @public 354 */ 355 Peel.prototype.handleDrag = function(fn, el) { 356 this.dragHandler = fn; 357 this.setupDragEvents(el); 358 } 359 360 /** 361 * Sets a function to be called when the user either clicks with a mouse or 362 * taps with a finger (touch events). 363 * @param {Function} fn The function to be called on press. This function will 364 * be called with the Peel instance as the "this" keyword, the original 365 * event as the first argument, and the x, y coordinates of the event as 366 * the 2nd and 3rd arguments, respectively. 367 * @param {HTMLElement} el The element to initiate the event. 368 * If not passed, this will be the element associated with the Peel 369 * instance. Allowing this to be passed lets another element serve as a 370 * "hit area" that can be larger than the element itself. 371 * @public 372 */ 373 Peel.prototype.handlePress = function(fn, el) { 374 this.pressHandler = fn; 375 this.setupDragEvents(el); 376 } 377 378 /** 379 * Sets up the drag events needed for both drag and press handlers. 380 * @param {HTMLElement} el The element to initiate the dragStart event on. 381 * @private 382 */ 383 Peel.prototype.setupDragEvents = function(el) { 384 var self = this, isDragging, moveName, endName; 385 386 if (this.dragEventsSetup) { 387 return; 388 } 389 390 el = el || this.el; 391 392 function dragStart (touch, evt) { 393 if (self.getOption('dragPreventsDefault')) { 394 evt.preventDefault(); 395 } 396 moveName = touch ? 'touchmove' : 'mousemove'; 397 endName = touch ? 'touchend' : 'mouseup'; 398 399 addEvent(docEl, moveName, dragMove); 400 addEvent(docEl, endName, dragEnd); 401 isDragging = false;
402 } 403 404 function dragMove (evt) { 405 if (self.dragHandler) { 406 callHandler(self.dragHandler, evt); 407 } 408 isDragging = true; 409 } 410 411 function dragEnd(evt) { 412 if (!isDragging && self.pressHandler) { 413 callHandler(self.pressHandler, evt); 414 } 415 removeEvent(docEl, moveName, dragMove); 416 removeEvent(docEl, endName, dragEnd); 417 } 418 419 function callHandler(fn, evt) { 420 var coords = getEventCoordinates(evt, self.el); 421 fn.call(self, evt, coords.x, coords.y); 422 } 423 424 this.addEvent(el, 'mousedown', dragStart.bind(this, false)); 425 this.addEvent(el, 'touchstart', dragStart.bind(this, true)); 426 this.dragEventsSetup = true; 427 } 428 429 /** 430 * Remove all event handlers previously added to the instance. 431 * @public 432 */ 433 Peel.prototype.removeEvents = function() { 434 this.events.forEach(function(e, i) { 435 removeEvent(e.el, e.type, e.handler); 436 }); 437 this.events = []; 438 } 439 440 /** 441 * Sets the peel effect to a point in time along a previously 442 * specified path. Will throw an error if no path exists. 443 * @param {number} n The time value (between 0 and 1). 444 * @public 445 */ 446 Peel.prototype.setTimeAlongPath = function(t) { 447 t = clamp(t); 448 var point = this.path.getPointForTime(t); 449 this.timeAlongPath = t; 450 this.setPeelPosition(point.x, point.y); 451 } 452 453 /** 454 * Sets a threshold above which the top layer (including the backside) layer 455 * will begin to fade out. This is calculated based on the visible clipped 456 * area of the polygon. If a peel path is set, it will use the progress along 457 * the path instead. 458 * @param {number} n A point between 0 and 1. 459 * @public 460 */ 461 Peel.prototype.setFadeThreshold = function(n) { 462 this.fadeThreshold = n; 463 } 464 465 /** 466 * Sets the position of the peel effect. This point is the position 467 * of the corner that is being peeled back. 468 * @param {Mixed} [...] Either x,y or a corner id. 469 * @public 470 */ 471 Peel.prototype.setPeelPosition = function() { 472 var pos = this.getPointOrCorner(arguments); 473 pos = this.getConstrainedPeelPosition(pos); 474 if (!pos) { 475 return; 476 } 477 this.peelLineSegment = this.getPeelLineSegment(pos); 478 this.peelLineRotation = this.peelLineSegment.getAngle(); 479 this.setClipping(); 480 this.setBackTransform(pos); 481 this.setEffects(); 482 } 483 484 /** 485 * Sets a constraint on the distance of the peel. This can be thought of as a 486 * point on the layers that are connected and cannot be torn apart. Typically 487 * this only makes sense as a point on the outer edge, such as the left edge 488 * of an open book, or the top edge of a calendar. In this case, simply using 489 * 2 constraint points (top-left/bottom-left for a book, etc) will create the 490 * desired effect. An arbitrary point can also be used with an effect like a 491 * thumbtack holding the pages together. 492 * @param {Mixed} [...] Either x,y or a corner id. 493 * @public 494 */ 495 /** 496 * Sets the corner for the peel effect to happen from. 497 * @public 498 */ 499 Peel.prototype.addPeelConstraint = function() { 500 var p = this.getPointOrCorner(arguments); 501 var radius = this.corner.subtract(p).getLength(); 502 this.constraints.push(new Circle(p, radius)); 503 this.calculateFlipConstraint(); 504 } 505 506 /** 507 * Sets an option to use for the effect. 508 * @param {string} key The option to set. 509 * @param {Mixed} value The value for the option. 510 * @public 511 */ 512 Peel.prototype.setOption = function(key, value) { 513 this.options[key] = value; 514 } 515 516 /** 517 * Gets an option set by the user. 518 * @param {string} key The key of the option to get. 519 * @returns {Mixed} 520 * @public 521 */ 522 Peel.prototype.getOption = function(key) { 523 return this.options[camelize(key)]; 524 } 525 526 /** 527 * Gets the ratio of the area of the clipped top layer to the total area. 528 * @returns {number} A value between 0 and 1. 529 * @public 530 */ 531 Peel.prototype.getAmountClipped = function() { 532 var topArea = this.getTopClipArea(); 533 var totalArea = this.width * this.height; 534 return normalize(topArea, totalArea, 0); 535 } 536 537 /** 538 * Adds an event listener to the element and keeps track of it for later 539 * removal. 540 * @param {Element} el The element to add the handler to. 541 * @param {string} type The event type. 542 * @param {Function} fn The handler function. 543 * @private 544 */ 545 Peel.prototype.addEvent = function(el, type, fn) { 546 addEvent(el, type, fn); 547 this.events.push({ 548 el: el, 549 type: type, 550 handler: fn 551 }); 552 return fn; 553 } 554 /** 555 * Gets the area of the clipped top layer. 556 * @returns {number} 557 * @private 558 */ 559 Peel.prototype.getTopClipArea = function() { 560 var top = new Polygon();
561 this.elementBox.forEach(function(side) { 562 this.distributeLineByPeelLine(side, top); 563 }, this); 564 return Polygon.getArea(top.getPoints()); 565 } 566 567 /** 568 * Determines which of the constraints should be used as the flip constraint 569 * by checking which has a y value closes to the corner (because the 570 * constraint operates relative to the vertical midline). Only one constraint 571 * should be required - changing the order of the constraints can help to 572 * achieve the proper effect and more than one will interfere with each other. 573 * @private 574 */ 575 Peel.prototype.calculateFlipConstraint = function() { 576 var corner = this.corner, arr = this.constraints.concat(); 577 this.flipConstraint = arr.sort(function(a, b) { 578 var aY = corner.y - a.center.y; 579 var bY = corner.y - b.center.y; 580 return a - b; 581 })[0]; 582 } 583 584 /** 585 * Called when the drag event starts. 586 * @param {Event} evt The original DOM event. 587 * @param {string} type The event type, "mouse" or "touch". 588 * @param {Function} fn The handler function to be called on drag. 589 * @private 590 */ 591 Peel.prototype.dragStart = function(evt, type, fn) { 592 } 593 594 /** 595 * Calls an event handler using the coordinates of the event. 596 * @param {Event} evt The original event. 597 * @param {Function} fn The handler to call. 598 * @private 599 */ 600 Peel.prototype.fireHandler = function(evt, fn) { 601 var coords = getEventCoordinates(evt, this.el); 602 fn.call(this, evt, coords.x, coords.y); 603 } 604 605 /** 606 * Sets the clipping points of the top and back layers based on a line 607 * segment that represents the peel line. 608 * @private 609 */ 610 Peel.prototype.setClipping = function() { 611 var top = new Polygon(); 612 var back = new Polygon(); 613 this.clippingBox.forEach(function(side) { 614 this.distributeLineByPeelLine(side, top, back); 615 }, this); 616 617 this.topClip.setPoints(top.getPoints()); 618 this.backClip.setPoints(back.getPoints()); 619 } 620 621 /** 622 * Distributes the first point in the given line segment and its intersect 623 * with the peel line, if there is one. 624 * @param {LineSegment} seg The line segment to check against. 625 * @param {Polygon} poly1 The first polygon. 626 * @param {Polygon} [poly2] The second polygon. 627 * @private 628 */ 629 Peel.prototype.distributeLineByPeelLine = function(seg, poly1, poly2) { 630 var intersect = this.peelLineSegment.getIntersectPoint(seg); 631 this.distributePointByPeelLine(seg.p1, poly1, poly2); 632 this.distributePointByPeelLine(intersect, poly1, poly2); 633 } 634 635 /** 636 * Distributes the given point to one of two polygons based on which side of 637 * the peel line it falls upon (if it falls directly on the line segment 638 * it is added to both). 639 * @param {Point} p The point to be distributed. 640 * @param {Polygon} poly1 The first polygon. 641 * @param {Polygon} [poly2] The second polygon. 642 * @private 643 */ 644 Peel.prototype.distributePointByPeelLine = function(p, poly1, poly2) { 645 if (!p) return; 646 var d = this.peelLineSegment.getPointDeterminant(p); 647 if (d <= 0) { 648 poly1.addPoint(p); 649 } 650 if (d >= 0 && poly2) { 651 poly2.addPoint(this.flipPointHorizontally(p)); 652 } 653 } 654 655 /** 656 * Sets the options for the effect, merging in defaults. 657 * @param {Object} opt User options. 658 * @private 659 */ 660 Peel.prototype.setOptions = function(opt) { 661 var options = opt || {}, defaults = Peel.Defaults; 662 for (var key in defaults) { 663 if (!defaults.hasOwnProperty(key) || key in options) { 664 continue; 665 } 666 options[key] = defaults[key]; 667 } 668 this.options = options; 669 } 670 671 /** 672 * Finds or creates a layer in the dom. 673 * @param {string} id The internal id of the element to be found or created. 674 * @param {HTMLElement} parent The parent if the element needs to be created. 675 * @param {numer} zIndex The z index of the layer. 676 * @returns {HTMLElement} 677 * @private 678 */ 679 Peel.prototype.findOrCreateLayer = function(id, parent, zIndex) { 680 var optId = id + '-element'; 681 var domId = prefix(id); 682 var el = getElement(this.getOption(optId) || '.' + domId, parent); 683 if (!el) { 684 el = createElement(parent, domId); 685 } 686 addClass(el, prefix('layer')); 687 setZIndex(el, zIndex); 688 return el; 689 } 690 691 /** 692 * Returns either a point created from 2 arguments (x/y) or a corner point 693 * created from the first argument as a corner id. 694 * @param {Arguments} args The arguments object from the original function. 695 * @returns {Point} 696 * @private 697 */ 698 Peel.prototype.getPointOrCorner = function(args) { 699 if (args.length === 2) { 700 return new Point(args[0], args[1]); 701 } else if(typeof args[0] === 'number') { 702 return this.getCornerPoint(args[0]); 703 } 704 return args[0]; 705 } 706 707 /** 708 * Returns a corner point based on an id defined in Peel.Corners. 709 * @param {number} id The id of the corner. 710 * @private 711 */ 712 Peel.prototype.getCornerPoint = function(id) { 713 var x = +!!(id & 1) * this.width; 714 var y = +!!(id & 2) * this.height; 715 return new Point(x, y); 716 } 717 718 /** 719 * Gets an optional clipping shape that may be set by the user. 720 * @returns {Object} 721 * @private 722 */ 723 Peel.prototype.getOptionalShape = function() { 724 var shapes = ['rect', 'polygon', 'path', 'circle'], found; 725 shapes.some(function(type) { 726 var attr = this.getOption(type), obj; 727 if (attr) { 728 obj = {}; 729 obj.attributes = attr; 730 obj.type = type; 731 found = obj; 732 } 733 return found; 734 }, this); 735 return found; 736 } 737 738 /** 739 * Sets up the main layers used for the effect that may include a possible 740 * subclip shape. 741 * @private 742 */ 743 Peel.prototype.setupLayers = function() { 744 var shape = this.getOptionalShape(); 745 746 // The inner layers may be wrapped later, so keep a reference to them here. 747 var topInnerLayer = this.topLayer = this.findOrCreateLayer('top', this.el, 2);
748 var backInnerLayer = this.backLayer = this.findOrCreateLayer('back', this.el, 3); 749 750 this.bottomLayer = this.findOrCreateLayer('bottom', this.el, 1); 751 752 if (shape) { 753 // If there is an SVG shape specified in the options, then this needs to 754 // be a separate clipped element because Safari/Mobile Safari can't handle 755 // nested clip-paths. The current top/back element will become the shape 756 // clip, so wrap them with an "outer" clip element that will become the 757 // new layer for the peel effect. The bottom layer does not require this 758 // effect, so the shape clip can be set directly on it. 759 this.topLayer = this.wrapShapeLayer(this.topLayer, 'top-outer-clip'); 760 this.backLayer = this.wrapShapeLayer(this.backLayer, 'back-outer-clip'); 761 762 this.topShapeClip = new SVGClip(topInnerLayer, shape); 763 this.backShapeClip = new SVGClip(backInnerLayer, shape); 764 this.bottomShapeClip = new SVGClip(this.bottomLayer, shape); 765 766 if (this.getOption('topShadowCreatesShape')) { 767 this.topShadowElement = this.setupDropShadow(shape, topInnerLayer); 768 } 769 } else { 770 this.topShadowElement = this.findOrCreateLayer('top-shadow', topInnerLayer, 1); 771 } 772 773 this.topClip = new SVGClip(this.topLayer); 774 this.backClip = new SVGClip(this.backLayer); 775 776 this.backShadowElement = this.findOrCreateLayer('back-shadow', backInnerLayer, 1); 777 this.backReflectionElement = this.findOrCreateLayer('back-reflection', backInnerLayer, 2); 778 this.bottomShadowElement = this.findOrCreateLayer('bottom-shadow', this.bottomLayer, 1); 779 780 this.usesBoxShadow = !shape; 781 } 782 783 /** 784 * Creates an inline SVG element to be used as a layer for a drop shadow filter 785 * effect. Note that drop shadow filters currently have some odd quirks in 786 * Blink such as blur radius changing depending on rotation, etc. 787 * @param {Object} shape A shape describing the SVG element to be used. 788 * @param {HTMLElement} parent The parent element where the layer will be added. 789 * @returns {SVGElement} 790 * @private 791 */ 792 Peel.prototype.setupDropShadow = function(shape, parent) { 793 var svg = createSVGElement('svg', parent, { 794 'class': prefix('layer') 795 }); 796 createSVGElement(shape.type, svg, shape.attributes); 797 return svg; 798 } 799 800 /** 801 * Wraps the passed element in another layer, preserving its z-index. Also 802 * add a "shape-layer" class to the layer which now becomes a shape clip. 803 * @param {HTMLElement} el The element to become the wrapped shape layer. 804 * @param {string} id The identifier for the new layer that will wrap the element. 805 * @returns {HTMLElement} The new element that wraps the shape layer. 806 * @private 807 */ 808 Peel.prototype.wrapShapeLayer = function(el, id) { 809 var zIndex = getZIndex(el); 810 addClass(el, prefix('shape-layer')); 811 var outerLayer = this.findOrCreateLayer(id, this.el, zIndex); 812 outerLayer.appendChild(el); 813 return outerLayer; 814 } 815 816 /** 817 * Sets up the dimensions of the element box and clipping box that area used 818 * in the effect. 819 * @private 820 */ 821 Peel.prototype.setupDimensions = function() { 822 this.width = this.el.offsetWidth; 823 this.height = this.el.offsetHeight; 824 this.center = new Point(this.width / 2, this.height / 2); 825 826 this.elementBox = this.getScaledBox(1); 827 this.clippingBox = this.getScaledBox(this.getOption('clippingBoxScale')); 828 } 829 830 /** 831 * Gets a box defined by 4 line segments that is at a scale of the main 832 * element. 833 * @param {number} scale The scale for the box to be. 834 * @private 835 */ 836 Peel.prototype.getScaledBox = function(scale) { 837 838 // Box scale is equal to: 839 // 1 * the bottom/right scale 840 // 0 * the top/left scale. 841 var brScale = scale; 842 var tlScale = scale - 1; 843 844 var tl = new Point(-this.width * tlScale, -this.height * tlScale); 845 var tr = new Point( this.width * brScale, -this.height * tlScale); 846 var br = new Point( this.width * brScale, this.height * brScale); 847 var bl = new Point(-this.width * tlScale, this.height * brScale); 848 849 return [ 850 new LineSegment(tl, tr), 851 new LineSegment(tr, br), 852 new LineSegment(br, bl), 853 new LineSegment(bl, tl) 854 ]; 855 } 856 857 /** 858 * Returns the peel position adjusted by constraints, if there are any. 859 * @param {Point} point The peel position to be constrained. 860 * @returns {Point} 861 * @private 862 */ 863 Peel.prototype.getConstrainedPeelPosition = function(pos) {
864 this.constraints.forEach(function(area) { 865 var offset = this.getFlipConstraintOffset(area, pos); 866 if (offset) { 867 area = new Circle(area.center, area.radius - offset); 868 } 869 pos = area.constrainPoint(pos); 870 }, this); 871 return pos; 872 } 873 874 /** 875 * Returns an offset to "pull" a corner in to prevent the peel effect from 876 * suddenly flipping around its axis. This offset is intended to be applied 877 * on the Y axis when dragging away from the center. 878 * @param {Circle} area The constraint to check against. 879 * @param {Point} point The peel position to be constrained. 880 * @returns {number|undefined} 881 * @private 882 */ 883 Peel.prototype.getFlipConstraintOffset = function(area, pos) { 884 var offset = this.getOption('flipConstraintOffset'); 885 if (area === this.flipConstraint && offset) { 886 var cornerToCenter = this.corner.subtract(this.center); 887 var cornerToConstraint = this.corner.subtract(area.center); 888 var baseAngle = cornerToConstraint.getAngle(); 889 890 // Normalized angles are rotated to be in the same space relative 891 // to the constraint. 892 var nCornerToConstraint = cornerToConstraint.rotate(-baseAngle); 893 var nPosToConstraint = pos.subtract(area.center).rotate(-baseAngle); 894 895 // Flip the vector vertically if the corner is in the bottom left or top 896 // right relative to the center, as the effect should always pull away 897 // from the vertical midline. 898 if (cornerToCenter.x * cornerToCenter.y < 0) { 899 nPosToConstraint.y *= -1; 900 } 901 902 if (nPosToConstraint.x > 0 && nPosToConstraint.y > 0) { 903 return normalize(nPosToConstraint.getAngle(), 45, 0) * offset; 904 } 905 906 } 907 } 908 909 /** 910 * Gets the line segment that represents the current peel line. 911 * @param {Point} point The position of the peel corner. 912 * @returns {LineSegment} 913 * @private 914 */ 915 Peel.prototype.getPeelLineSegment = function(point) { 916 // The point midway between the peel position and the corner. 917 var halfToCorner = this.corner.subtract(point).scale(.5); 918 var midpoint = point.add(halfToCorner); 919 if (halfToCorner.x === 0 && halfToCorner.y === 0) { 920 // If the corner is the same as the point, then set half to corner 921 // to be the center, and keep the midpoint where it is. This will 922 // ensure a non-zero peel line. 923 halfToCorner = point.subtract(this.center); 924 } 925 var l = halfToCorner.getLength() 926 var mult = (Math.max(this.width, this.height) / l) * 10; 927 var half = halfToCorner.rotate(-90).scale(mult); 928 var p1 = midpoint.add(half); 929 var p2 = midpoint.subtract(half); 930 return new LineSegment(p1, p2); 931 } 932 933 /** 934 * Sets the transform of the back layer. 935 * @param {Point} pos The position of the peeling corner. 936 * @private 937 */ 938 Peel.prototype.setBackTransform = function(pos) { 939 var mirroredCorner = this.flipPointHorizontally(this.corner); 940 var r = (this.peelLineRotation - 90) * 2; 941 var t = pos.subtract(mirroredCorner.rotate(r)); 942 var css = 'translate('+ round(t.x) +'px, '+ round(t.y) +'px) rotate('+ round(r) +'deg)'; 943 setTransform(this.backLayer, css); 944 945 // Set the top shadow element here as well, as the 946 // position and rotation matches that of the back layer. 947 setTransform(this.topShadowElement, css); 948 } 949 950 /** 951 * Gets the distance of the peel line along an imaginary line that runs 952 * between the corners that it "faces". For example, if the peel line 953 * is rotated 45 degrees, then it can be considered to be between the top left 954 * and bottom right corners. This function will return how far the peel line 955 * has advanced along that line. 956 * @returns {number} A position >= 0. 957 * @private 958 */ 959 Peel.prototype.getPeelLineDistance = function() { 960 var cornerId, opposingCornerId, corner, opposingCorner; 961 if (this.peelLineRotation < 90) { 962 cornerId = Peel.Corners.TOP_RIGHT; 963 opposingCornerId = Peel.Corners.BOTTOM_LEFT; 964 } else if (this.peelLineRotation < 180) { 965 cornerId = Peel.Corners.BOTTOM_RIGHT; 966 opposingCornerId = Peel.Corners.TOP_LEFT; 967 } else if (this.peelLineRotation < 270) { 968 cornerId = Peel.Corners.BOTTOM_LEFT; 969 opposingCornerId = Peel.Corners.TOP_RIGHT; 970 } else if (this.peelLineRotation < 360) { 971 cornerId = Peel.Corners.TOP_LEFT; 972 opposingCornerId = Peel.Corners.BOTTOM_RIGHT; 973 } 974 corner = this.getCornerPoint(cornerId); 975 opposingCorner = this.getCornerPoint(opposingCornerId); 976 977 // Scale the line segment past the original corners so that the effects 978 // can have a nice fadeout even past 1. 979 var cornerToCorner = new LineSegment(corner, opposingCorner).scale(2); 980 var intersect = this.peelLineSegment.getIntersectPoint(cornerToCorner); 981 if (!intersect) { 982 // If there is no intersect, then assume that it has run past the opposing 983 // corner and set the distance to well past the full distance. 984 return 2; 985 } 986 var distanceToPeelLine = corner.subtract(intersect).getLength(); 987 var totalDistance = corner.subtract(opposingCorner).getLength(); 988 return (distanceToPeelLine / totalDistance); 989 } 990 991 /** 992 * Sets shadows and fade effects. 993 * @private 994 */ 995 Peel.prototype.setEffects = function() { 996 var t = this.getPeelLineDistance(); 997 this.setTopShadow(t); 998 this.setBackShadow(t); 999 this.setBackReflection(t); 1000 this.setBottomShadow(t); 1001 this.setFade(); 1002 } 1003 1004 /** 1005 * Sets the top shadow as either a box-shadow or a drop-shadow filter. 1006 * @param {number} t Position of the peel line from corner to corner. 1007 * @private 1008 */ 1009 Peel.prototype.setTopShadow = function(t) { 1010 if (!this.getOption('topShadow')) { 1011 return; 1012 } 1013 var sBlur = this.getOption('topShadowBlur'); 1014 var sX = this.getOption('topShadowOffsetX'); 1015 var sY = this.getOption('topShadowOffsetY'); 1016 var alpha = this.getOption('topShadowAlpha'); 1017 var sAlpha = this.exponential(t, 5, alpha); 1018 if (this.usesBoxShadow) { 1019 setBoxShadow(this.topShadowElement, sX, sY, sBlur, 0, sAlpha); 1020 } else { 1021 setDropShadow(this.topShadowElement, sX, sY, sBlur, sAlpha); 1022 } 1023 } 1024 1025 /** 1026 * Gets a number either distributed along a bell curve or increasing linearly. 1027 * @param {number} n The number to transform. 1028 * @param {boolean} dist Whether or not to use distribution. 1029 * @param {number} mult A multiplier for the result. 1030 * @returns {number} 1031 * @private 1032 */ 1033 Peel.prototype.distributeOrLinear = function(n, dist, mult) { 1034 if (dist) { 1035 return distribute(n, mult); 1036 } else { 1037 return n * mult; 1038 } 1039 } 1040 1041 /** 1042 * Gets a number either distributed exponentially, clamped to a range between 1043 * 0 and 1, and multiplied by a multiplier. 1044 * @param {number} n The number to transform. 1045 * @param {number} exp The exponent to be used. 1046 * @param {number} mult A multiplier for the result. 1047 * @returns {number} 1048 * @private 1049 */
1050 Peel.prototype.exponential = function(n, exp, mult) { 1051 return mult * clamp(Math.pow(1 + n, exp) - 1); 1052 } 1053 1054 /** 1055 * Sets reflection of the back face as a linear gradient. 1056 * @param {number} t Position of the peel line from corner to corner. 1057 * @private 1058 */ 1059 Peel.prototype.setBackReflection = function(t) { 1060 var stops = []; 1061 if (this.canSetLinearEffect('backReflection', t)) { 1062 1063 var rDistribute = this.getOption('backReflectionDistribute'); 1064 var rSize = this.getOption('backReflectionSize'); 1065 var rOffset = this.getOption('backReflectionOffset'); 1066 var rAlpha = this.getOption('backReflectionAlpha'); 1067 1068 var reflectionSize = this.distributeOrLinear(t, rDistribute, rSize); 1069 var rStop = t - rOffset; 1070 var rMid = rStop - reflectionSize; 1071 var rStart = rMid - reflectionSize; 1072 1073 stops.push(getWhiteStop(0, 0)); 1074 stops.push(getWhiteStop(0, rStart)); 1075 stops.push(getWhiteStop(rAlpha, rMid)); 1076 stops.push(getWhiteStop(0, rStop)); 1077 } 1078 setBackgroundGradient(this.backReflectionElement, 180 - this.peelLineRotation, stops); 1079 } 1080 1081 /** 1082 * Sets shadow of the back face as a linear gradient. 1083 * @param {number} t Position of the peel line from corner to corner. 1084 * @private 1085 */ 1086 Peel.prototype.setBackShadow = function(t) { 1087 var stops = []; 1088 if (this.canSetLinearEffect('backShadow', t)) { 1089 1090 var sSize = this.getOption('backShadowSize'); 1091 var sOffset = this.getOption('backShadowOffset'); 1092 var sAlpha = this.getOption('backShadowAlpha'); 1093 var sDistribute = this.getOption('backShadowDistribute'); 1094 1095 var shadowSize = this.distributeOrLinear(t, sDistribute, sSize); 1096 var shadowStop = t - sOffset; 1097 var shadowMid = shadowStop - shadowSize; 1098 var shadowStart = shadowMid - shadowSize; 1099 1100 stops.push(getBlackStop(0, 0)); 1101 stops.push(getBlackStop(0, shadowStart)); 1102 stops.push(getBlackStop(sAlpha, shadowMid)); 1103 stops.push(getBlackStop(sAlpha, shadowStop)); 1104 } 1105 setBackgroundGradient(this.backShadowElement, 180 - this.peelLineRotation, stops); 1106 } 1107 1108 /** 1109 * Sets the bottom shadow as a linear gradient. 1110 * @param {number} t Position of the peel line from corner to corner. 1111 * @private 1112 */ 1113 Peel.prototype.setBottomShadow = function(t) { 1114 var stops = []; 1115 if (this.canSetLinearEffect('bottomShadow', t)) { 1116 1117 // Options 1118 var sSize = this.getOption('bottomShadowSize'); 1119 var offset = this.getOption('bottomShadowOffset'); 1120 var darkAlpha = this.getOption('bottomShadowDarkAlpha'); 1121 var lightAlpha = this.getOption('bottomShadowLightAlpha'); 1122 var sDistribute = this.getOption('bottomShadowDistribute'); 1123 1124 var darkShadowStart = t - (.025 - offset); 1125 var midShadowStart = darkShadowStart - (this.distributeOrLinear(t, sDistribute, .03) * sSize) - offset; 1126 var lightShadowStart = midShadowStart - ((.02 * sSize) - offset); 1127 stops = [ 1128 getBlackStop(0, 0), 1129 getBlackStop(0, lightShadowStart), 1130 getBlackStop(lightAlpha, midShadowStart), 1131 getBlackStop(lightAlpha, darkShadowStart), 1132 getBlackStop(darkAlpha, t) 1133 ]; 1134 } 1135 setBackgroundGradient(this.bottomShadowElement, this.peelLineRotation + 180, stops); 1136 } 1137 1138 /** 1139 * Whether a linear effect can be set. 1140 * @param {string} name Name of the effect 1141 * @param {number} t Current position of the linear effect line. 1142 * @returns {boolean} 1143 * @private 1144 */ 1145 Peel.prototype.canSetLinearEffect = function(name, t) { 1146 return this.getOption(name) && t > 0; 1147 } 1148 1149 /** 1150 * Sets the fading effect of the top layer, if a threshold is set. 1151 * @private 1152 */ 1153 Peel.prototype.setFade = function() { 1154 var threshold = this.fadeThreshold, opacity = 1, n; 1155 if (threshold) { 1156 if (this.timeAlongPath !== undefined) { 1157 n = this.timeAlongPath; 1158 } else { 1159 n = this.getAmountClipped(); 1160 } 1161 if (n > threshold) { 1162 opacity = (1 - n) / (1 - threshold); 1163 } 1164 setOpacity(this.topLayer, opacity); 1165 setOpacity(this.backLayer, opacity); 1166 setOpacity(this.bottomShadowElement, opacity); 1167 } 1168 } 1169 1170 /** 1171 * Flips a point along an imaginary vertical midpoint. 1172 * @param {Array} points The points to be flipped. 1173 * @returns {Array} 1174 * @private 1175 */ 1176 Peel.prototype.flipPointHorizontally = function(p) { 1177 return new Point(p.x - ((p.x - this.center.x) * 2), p.y); 1178 } 1179 1180 /** 1181 * Post setup initialization. 1182 * @private 1183 */ 1184 Peel.prototype.init = function() { 1185 if (this.getOption('setPeelOnInit')) { 1186 this.setPeelPosition(this.corner); 1187 } 1188 addClass(this.el, prefix('ready')); 1189 } 1190 1191 /** 1192 * Class that clips an HTMLElement by an SVG path. 1193 * @param {HTMLElement} el The element to be clipped. 1194 * @param {Object} [shape] An object defining the SVG element to use in the new 1195 * clip path. Defaults to a polygon. 1196 * @constructor 1197 */ 1198 function SVGClip (el, shape) { 1199 this.el = el; 1200 this.shape = SVGClip.createClipPath(el, shape || { 1201 'type': 'polygon' 1202 }); 1203 // Chrome needs this for some reason for the clipping to work. 1204 setTransform(this.el, 'translate(0px,0px)'); 1205 } 1206 1207 /** 1208 * Sets up the global SVG element and its nested defs object to use for new 1209 * clip paths. 1210 * @returns {SVGElement} 1211 * @public 1212 */ 1213 SVGClip.getDefs = function() { 1214 if (!this.defs) { 1215 this.svg = createSVGElement('svg', null, { 1216 'class': prefix('svg-clip-element') 1217 }); 1218 this.defs = createSVGElement('defs', this.svg); 1219 } 1220 return this.defs; 1221 } 1222 1223 /** 1224 * Creates a new <clipPath> SVG element and sets the passed html element to be 1225 * clipped by it. 1226 * @param {HTMLElement} el The html element to be clipped. 1227 * @param {Object} obj An object defining the SVG element to be used in the 1228 * clip path. 1229 * @returns {SVGElement} 1230 * @public 1231 */ 1232 SVGClip.createClipPath = function(el, obj) { 1233 var id = SVGClip.getId(); 1234 var clipPath = createSVGElement('clipPath', this.getDefs()); 1235 var svgEl = createSVGElement(obj.type, clipPath, obj.attributes); 1236 setSVGAttribute(clipPath, 'id', id); 1237 setCSSClip(el, 'url(#' + id + ')'); 1238 return svgEl; 1239 } 1240 1241 /** 1242 * Gets the next svg clipping id. 1243 * @public 1244 */ 1245 SVGClip.getId = function() { 1246 if (!SVGClip.id) { 1247 SVGClip.id = 1; 1248 }
1249 return 'svg-clip-' + SVGClip.id++; 1250 } 1251 1252 /** 1253 * Sets the "points" attribute of the clip path shape. This only makes sense 1254 * for polygon shapes. 1255 * @param {Array} points The points to be used. 1256 * @public 1257 */ 1258 SVGClip.prototype.setPoints = function(points) { 1259 var str = points.map(function(p) { 1260 return round(p.x) + ',' + round(p.y); 1261 }).join(' '); 1262 setSVGAttribute(this.shape, 'points', str); 1263 } 1264 1265 /** 1266 * A class that represents a circle. 1267 * @param {Point} center The center point. 1268 * @param {Point} radius The radius. 1269 * @constructor 1270 */ 1271 function Circle (center, radius) { 1272 this.center = center; 1273 this.radius = radius; 1274 } 1275 1276 /** 1277 * Determines whether a point is contained within the circle. 1278 * @param {Point} p The point. 1279 * @returns {boolean} 1280 * @public 1281 */ 1282 Circle.prototype.containsPoint = function(p) { 1283 if(this.boundingRectContainsPoint(p)) { 1284 var dx = this.center.x - p.x; 1285 var dy = this.center.y - p.y; 1286 dx *= dx; 1287 dy *= dy; 1288 var distanceSquared = dx + dy; 1289 var radiusSquared = this.radius * this.radius; 1290 return distanceSquared <= radiusSquared; 1291 } 1292 return false; 1293 } 1294 1295 /** 1296 * Determines whether a point is contained within the bounding box of the circle. 1297 * @param {Point} p The point. 1298 * @returns {boolean} 1299 * @private 1300 */ 1301 Circle.prototype.boundingRectContainsPoint = function(p) { 1302 return p.x >= this.center.x - this.radius && p.x <= this.center.x + this.radius && 1303 p.y >= this.center.y - this.radius && p.y <= this.center.y + this.radius; 1304 } 1305 1306 /** 1307 * Moves a point outside the circle to the closest point on the circumference. 1308 * Rotated angle from the center point should be the same. 1309 * @param {Point} p The point. 1310 * @returns {boolean} 1311 * @public 1312 */ 1313 Circle.prototype.constrainPoint = function(p) { 1314 if (!this.containsPoint(p)) { 1315 var rotation = p.subtract(this.center).getAngle(); 1316 p = this.center.add(new Point(this.radius, 0).rotate(rotation)); 1317 } 1318 return p; 1319 } 1320 1321 /** 1322 * A class that represents a polygon. 1323 * @constructor 1324 */ 1325 function Polygon() { 1326 this.points = []; 1327 } 1328 1329 /** 1330 * Gets the area of the polygon. 1331 * @param {Array} points The points describing the polygon. 1332 * @public 1333 */ 1334 Polygon.getArea = function(points) { 1335 var sum1 = 0, sum2 = 0; 1336 points.forEach(function(p, i, arr) { 1337 var next = arr[(i + 1) % arr.length]; 1338 sum1 += (p.x * next.y); 1339 sum2 += (p.y * next.x); 1340 }); 1341 return (sum1 - sum2) / 2; 1342 } 1343 1344 /** 1345 * Adds a point to the polygon. 1346 * @param {Point} point 1347 * @public 1348 */ 1349 Polygon.prototype.addPoint = function(point) { 1350 this.points.push(point); 1351 } 1352 1353 /** 1354 * Gets the points of the polygon as an array. 1355 * @returns {Array} 1356 * @public 1357 */ 1358 Polygon.prototype.getPoints = function() { 1359 return this.points; 1360 } 1361 1362 1363 /** 1364 * A class representing a bezier curve. 1365 * @param {Point} p1 The starting point. 1366 * @param {Point} c1 The control point of p1. 1367 * @param {Point} c2 The control point of p2. 1368 * @param {Point} p2 The ending point. 1369 * @constructor 1370 */ 1371 function BezierCurve (p1, c1, c2, p2) { 1372 this.p1 = p1; 1373 this.c1 = c1; 1374 this.p2 = p2; 1375 this.c2 = c2; 1376 } 1377 1378 /** 1379 * Gets a point along the line segment for a given time. 1380 * @param {number} t The time along the segment, between 0 and 1. 1381 * @returns {Point} 1382 */ 1383 BezierCurve.prototype.getPointForTime = function(t) { 1384 var b0 = Math.pow(1 - t, 3); 1385 var b1 = 3 * t * Math.pow(1 - t, 2); 1386 var b2 = 3 * Math.pow(t, 2) * (1 - t); 1387 var b3 = Math.pow(t, 3); 1388 1389 var x = (b0 * this.p1.x) + (b1 * this.c1.x) + (b2 * this.c2.x) + (b3 * this.p2.x) 1390 var y = (b0 * this.p1.y) + (b1 * this.c1.y) + (b2 * this.c2.y) + (b3 * this.p2.y) 1391 return new Point(x, y); 1392 } 1393 1394 1395 /** 1396 * A class that represents a line segment. 1397 * @param {Point} p1 The start of the segment. 1398 * @param {Point} p2 The end of the segment. 1399 * @constructor 1400 */ 1401 function LineSegment (p1, p2) { 1402 this.p1 = p1; 1403 this.p2 = p2; 1404 } 1405 1406 /** 1407 * @constant 1408 */ 1409 LineSegment.EPSILON = 1e-6; 1410 1411 /** 1412 * Gets a point along the line segment for a given time. 1413 * @param {number} t The time along the segment, between 0 and 1. 1414 * @returns {Point} 1415 * @public 1416 */ 1417 LineSegment.prototype.getPointForTime = function(t) { 1418 return this.p1.add(this.getVector().scale(t)); 1419 } 1420 1421 /** 1422 * Takes a scalar and returns a new scaled line segment. 1423 * @param {number} n The amount to scale the segment by. 1424 * @returns {LineSegment} 1425 * @public 1426 */ 1427 LineSegment.prototype.scale = function(n) { 1428 var half = 1 + (n / 2); 1429 var p1 = this.p1.add(this.p2.subtract(this.p1).scale(n)); 1430 var p2 = this.p2.add(this.p1.subtract(this.p2).scale(n)); 1431 return new LineSegment(p1, p2); 1432 } 1433 1434 /** 1435 * The determinant is a number that indicates which side of a line a point 1436 * falls on. A positive number means that the point falls inside the area 1437 * "clockwise" of the line, ie. the area that the line would sweep if it were 1438 * rotated 180 degrees. A negative number would mean the point is in the area 1439 * the line would sweep if it were rotated counter-clockwise, or -180 degrees. 1440 * 0 indicates that the point falls exactly on the line. 1441 * @param {Point} p The point to test against. 1442 * @returns {number} A signed number. 1443 * @public 1444 */ 1445 LineSegment.prototype.getPointDeterminant = function(p) { 1446 var d = ((p.x - this.p1.x) * (this.p2.y - this.p1.y)) - ((p.y - this.p1.y) * (this.p2.x - this.p1.x)); 1447 // Tolerance for near-zero. 1448 if (d > -LineSegment.EPSILON && d < LineSegment.EPSILON) { 1449 d = 0; 1450 } 1451 return d; 1452 } 1453 1454 /** 1455 * Calculates the point at which another line segment intersects, if any. 1456 * @param {LineSegment} seg The second line segment. 1457 * @returns {Point|null} 1458 * @public 1459 */ 1460 LineSegment.prototype.getIntersectPoint = function(seg2) { 1461 var seg1 = this; 1462 1463 function crossProduct(p1, p2) { 1464 return p1.x * p2.y - p1.y * p2.x; 1465 } 1466 1467 var r = seg1.p2.subtract(seg1.p1); 1468 var s = seg2.p2.subtract(seg2.p1); 1469 1470 var uNumerator = crossProduct(seg2.p1.subtract(seg1.p1), r); 1471 var denominator = crossProduct(r, s); 1472 1473 if (denominator == 0) { 1474 // ignoring colinear and parallel 1475 return null; 1476 } 1477 1478 var u = uNumerator / denominator; 1479 var t = crossProduct(seg2.p1.subtract(seg1.p1), s) / denominator; 1480 1481 if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { 1482 return seg1.p1.add(r.scale(t)); 1483 } 1484 1485 return null; 1486 } 1487 1488 /** 1489 * Returns the angle of the line segment in degrees. 1490 * @returns {number} 1491 * @public 1492 */ 1493 LineSegment.prototype.getAngle = function() { 1494 return this.getVector().getAngle(); 1495 } 1496 1497 /** 1498 * Gets the vector that represents the line segment. 1499 * @returns {Point} 1500 * @private 1501 */ 1502 LineSegment.prototype.getVector = function() { 1503 if (!this.vector) { 1504 this.vector = this.p2.subtract(this.p1); 1505 } 1506 return this.vector; 1507 } 1508 1509 /** 1510 * A class representing a point or 2D vector. 1511 * @param {number} x The x coordinate. 1512 * @param {number} y The y coordinate. 1513 * @constructor 1514 */ 1515 function Point (x, y) { 1516 this.x = x; 1517 this.y = y; 1518 } 1519 1520 /** 1521 * @constant 1522 */ 1523 Point.DEGREES_IN_RADIANS = 180 / Math.PI; 1524 1525 /** 1526 * Gets degrees in radians. 1527 * @param {number} deg 1528 * @returns {number} 1529 */ 1530 Point.degToRad = function(deg) { 1531 return deg / Point.DEGREES_IN_RADIANS; 1532 }; 1533 1534 /** 1535 * Gets radians in degrees. 1536 * @param {number} rad 1537 * @returns {number} 1538 */ 1539 Point.radToDeg = function(rad) { 1540 var deg = rad * Point.DEGREES_IN_RADIANS; 1541 while(deg < 0) deg += 360; 1542 return deg; 1543 }; 1544 1545 /** 1546 * Creates a new point given a rotation in degrees and a length. 1547 * @param {number} deg The rotation of the vector. 1548 * @param {number} len The length of the vector. 1549 * @returns {Point} 1550 */ 1551 Point.vector = function(deg, len) { 1552 var rad = Point.degToRad(deg); 1553 return new Point(Math.cos(rad) * len, Math.sin(rad) * len); 1554 }; 1555 1556 /** 1557 * Adds a point. 1558 * @param {Point} p 1559 * @returns {Point} 1560 */ 1561 Point.prototype.add = function(p) { 1562 return new Point(this.x + p.x, this.y + p.y); 1563 }; 1564 1565 /** 1566 * Subtracts a point. 1567 * @param {Point} p 1568 * @returns {Point} 1569 */ 1570 Point.prototype.subtract = function(p) { 1571 return new Point(this.x - p.x, this.y - p.y); 1572 }; 1573 1574 /**
1575 * Scales a point by a scalar. 1576 * @param {number} n 1577 * @returns {Point} 1578 */ 1579 Point.prototype.scale = function(n) { 1580 return new Point(this.x * n, this.y * n); 1581 }; 1582 1583 /** 1584 * Gets the length of the distance to the point. 1585 * @returns {number} 1586 */ 1587 Point.prototype.getLength = function() { 1588 return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2)); 1589 }; 1590 1591 /** 1592 * Gets the angle of the point in degrees. 1593 * @returns {number} 1594 */ 1595 Point.prototype.getAngle = function() { 1596 return Point.radToDeg(Math.atan2(this.y, this.x)); 1597 }; 1598 1599 /** 1600 * Returns a new point of the same length with a different angle. 1601 * @param {number} deg The angle in degrees. 1602 * @returns {Point} 1603 */ 1604 Point.prototype.setAngle = function(deg) { 1605 return Point.vector(deg, this.getLength()); 1606 }; 1607 1608 /** 1609 * Rotates the point. 1610 * @param {number} deg The amount to rotate by in degrees. 1611 * @returns {Point} 1612 */ 1613 Point.prototype.rotate = function(deg) { 1614 return this.setAngle(this.getAngle() + deg); 1615 }; 1616 1617 setCssProperties(); 1618 win.Peel = Peel; 1619 1620})(window);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.