├── LICENSE ├── README.md └── src ├── peel.css └── peel.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © Andrew Plummer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### About 2 | 3 | Peel.js is a Javascript micro library for creating peeling effects using just HTML5. 4 | It is supported in browsers that support clip paths and transforms (most evergreen 5 | browsers, but generally excluding IE). 6 | 7 | ### Docs and examples 8 | 9 | https://andrewplummer.github.io/peel-js/ 10 | -------------------------------------------------------------------------------- /src/peel.css: -------------------------------------------------------------------------------- 1 | 2 | .peel { 3 | position: relative; 4 | opacity: 0; 5 | } 6 | 7 | .peel-ready { 8 | opacity: 1; 9 | } 10 | 11 | .peel-svg-clip-element { 12 | position: absolute; 13 | top: -10000px; 14 | left: -10000px; 15 | width: 1px; 16 | height: 1px; 17 | opacity: 0; 18 | } 19 | 20 | .peel-layer { 21 | position: absolute; 22 | z-index: 1; 23 | width: 100%; 24 | height: 100%; 25 | top: 0; 26 | right: 0; 27 | bottom: 0; 28 | left: 0; 29 | -webkit-user-select: none; 30 | -moz-user-select: none; 31 | user-select: none; 32 | -webkit-transform-origin: top left; 33 | -moz-transform-origin: top left; 34 | transform-origin: top left; 35 | } 36 | 37 | /*------------] Some Defaults [------------*/ 38 | 39 | .peel-top { 40 | background-color: #81afcb; 41 | } 42 | 43 | .peel-back { 44 | background-color: #a0c7df; 45 | } 46 | 47 | .peel-bottom { 48 | background-color: #688394; 49 | } 50 | -------------------------------------------------------------------------------- /src/peel.js: -------------------------------------------------------------------------------- 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 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); 1621 | --------------------------------------------------------------------------------