peel.js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +14 -0
- package/dist/peel.d.ts +587 -0
- package/dist/peel.d.ts.map +1 -0
- package/dist/peel.js +1226 -0
- package/dist/peel.js.map +1 -0
- package/package.json +36 -0
- package/src/peel.css +49 -0
- package/src/peel.ts +1434 -0
package/dist/peel.js
ADDED
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
const PRECISION = 1e2; // 2 decimals
|
|
2
|
+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
3
|
+
// General helpers
|
|
4
|
+
const round = (n) => Math.round(n * PRECISION) / PRECISION;
|
|
5
|
+
const clamp = (n) => Math.max(0, Math.min(1, n));
|
|
6
|
+
function normalize(n, min, max) {
|
|
7
|
+
return (n - min) / (max - min);
|
|
8
|
+
}
|
|
9
|
+
// Distributes a number between 0 and 1 along a bell curve.
|
|
10
|
+
function distribute(t, mult) {
|
|
11
|
+
return (mult || 1) * 2 * (.5 - Math.abs(t - .5));
|
|
12
|
+
}
|
|
13
|
+
function prefix(str) {
|
|
14
|
+
return 'peel-' + str;
|
|
15
|
+
}
|
|
16
|
+
// CSS Helper
|
|
17
|
+
function setTransform(el, t) {
|
|
18
|
+
el.style.transform = t;
|
|
19
|
+
}
|
|
20
|
+
function setBoxShadow(el, x, y, blur, spread, alpha) {
|
|
21
|
+
el.style.boxShadow = getShadowCss(x, y, blur, spread, alpha);
|
|
22
|
+
}
|
|
23
|
+
function setDropShadow(el, x, y, blur, alpha) {
|
|
24
|
+
el.style.filter = 'drop-shadow(' + getShadowCss(x, y, blur, null, alpha) + ')';
|
|
25
|
+
}
|
|
26
|
+
function getShadowCss(x, y, blur, spread, alpha) {
|
|
27
|
+
return round(x) + 'px ' +
|
|
28
|
+
round(y) + 'px ' +
|
|
29
|
+
round(blur) + 'px ' +
|
|
30
|
+
(spread ? round(spread) + 'px ' : '') +
|
|
31
|
+
'rgba(0,0,0,' + round(alpha) + ')';
|
|
32
|
+
}
|
|
33
|
+
function setOpacity(el, t) {
|
|
34
|
+
el.style.opacity = t;
|
|
35
|
+
}
|
|
36
|
+
function setBackgroundGradient(el, rotation, stops) {
|
|
37
|
+
var css;
|
|
38
|
+
if (stops.length === 0) {
|
|
39
|
+
css = 'none';
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
css = 'linear-gradient(' + round(rotation) + 'deg,' + stops.join(',') + ')';
|
|
43
|
+
}
|
|
44
|
+
el.style.backgroundImage = css;
|
|
45
|
+
}
|
|
46
|
+
// Event Helpers
|
|
47
|
+
function getEventCoordinates(evt, el) {
|
|
48
|
+
const pos = evt instanceof TouchEvent ? evt.changedTouches[0] : evt;
|
|
49
|
+
return {
|
|
50
|
+
'x': pos.clientX - el.offsetLeft + window.scrollX,
|
|
51
|
+
'y': pos.clientY - el.offsetTop + window.scrollY
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Color Helpers
|
|
55
|
+
function getBlackStop(a, pos) {
|
|
56
|
+
return getColorStop(0, 0, 0, a, pos);
|
|
57
|
+
}
|
|
58
|
+
function getWhiteStop(a, pos) {
|
|
59
|
+
return getColorStop(255, 255, 255, a, pos);
|
|
60
|
+
}
|
|
61
|
+
function getColorStop(r, g, b, a, pos) {
|
|
62
|
+
a = round(clamp(a));
|
|
63
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ') ' + round(pos * 100) + '%';
|
|
64
|
+
}
|
|
65
|
+
// DOM Element Helpers
|
|
66
|
+
function createElement(parent, className) {
|
|
67
|
+
var el = document.createElement('div');
|
|
68
|
+
el.classList.add(className);
|
|
69
|
+
parent.appendChild(el);
|
|
70
|
+
return el;
|
|
71
|
+
}
|
|
72
|
+
// SVG Helpers
|
|
73
|
+
function createSVGElement(tag, parent, attributes) {
|
|
74
|
+
parent = parent || document.documentElement;
|
|
75
|
+
var el = document.createElementNS(SVG_NAMESPACE, tag);
|
|
76
|
+
parent.appendChild(el);
|
|
77
|
+
for (var key in attributes) {
|
|
78
|
+
if (!attributes.hasOwnProperty(key))
|
|
79
|
+
continue;
|
|
80
|
+
setSVGAttribute(el, key, attributes[key]);
|
|
81
|
+
}
|
|
82
|
+
return el;
|
|
83
|
+
}
|
|
84
|
+
function setSVGAttribute(el, key, value) {
|
|
85
|
+
el.setAttributeNS(null, key, value);
|
|
86
|
+
}
|
|
87
|
+
const shapes = ["circle", "path", "polygon", "rect"];
|
|
88
|
+
/**
|
|
89
|
+
* Four constants representing the corners of the element from which peeling can occur.
|
|
90
|
+
*/
|
|
91
|
+
export var PeelCorners;
|
|
92
|
+
(function (PeelCorners) {
|
|
93
|
+
PeelCorners[PeelCorners["TOP_LEFT"] = 0] = "TOP_LEFT";
|
|
94
|
+
PeelCorners[PeelCorners["TOP_RIGHT"] = 1] = "TOP_RIGHT";
|
|
95
|
+
PeelCorners[PeelCorners["BOTTOM_LEFT"] = 2] = "BOTTOM_LEFT";
|
|
96
|
+
PeelCorners[PeelCorners["BOTTOM_RIGHT"] = 3] = "BOTTOM_RIGHT";
|
|
97
|
+
})(PeelCorners || (PeelCorners = {}));
|
|
98
|
+
/**
|
|
99
|
+
* Main class that controls the peeling effect.
|
|
100
|
+
*/
|
|
101
|
+
export class Peel {
|
|
102
|
+
/**
|
|
103
|
+
* The constructor will look for the required elements to make the peel effect
|
|
104
|
+
* in the options, and create them if they do not exist.
|
|
105
|
+
*/
|
|
106
|
+
constructor(el, options = {}) {
|
|
107
|
+
this.peelLineRotation = 0;
|
|
108
|
+
this.width = 0;
|
|
109
|
+
this.height = 0;
|
|
110
|
+
this.el = typeof el === "string" ? document.querySelector(el) : el;
|
|
111
|
+
this.options = Object.assign({}, Peel.defaultOptions, options);
|
|
112
|
+
this.constraints = [];
|
|
113
|
+
this.setupLayers();
|
|
114
|
+
this.setupDimensions();
|
|
115
|
+
this.corner = this.getPoint(this.options.corner);
|
|
116
|
+
if (this.options.preset) {
|
|
117
|
+
this.applyPreset(this.options.preset);
|
|
118
|
+
}
|
|
119
|
+
this.init();
|
|
120
|
+
}
|
|
121
|
+
applyPreset(preset) {
|
|
122
|
+
if (preset === 'book') {
|
|
123
|
+
// The order of constraints is important here so that the peel line
|
|
124
|
+
// approaches the horizontal smoothly without jumping.
|
|
125
|
+
this.addPeelConstraint(PeelCorners.BOTTOM_LEFT);
|
|
126
|
+
this.addPeelConstraint(PeelCorners.TOP_LEFT);
|
|
127
|
+
// Removing effect distribution will make the book still have some
|
|
128
|
+
// depth to the effect while fully open.
|
|
129
|
+
this.options.backReflection = false;
|
|
130
|
+
this.options.backShadowDistribute = false;
|
|
131
|
+
this.options.bottomShadowDistribute = false;
|
|
132
|
+
}
|
|
133
|
+
else if (preset === 'calendar') {
|
|
134
|
+
this.addPeelConstraint(PeelCorners.TOP_RIGHT);
|
|
135
|
+
this.addPeelConstraint(PeelCorners.TOP_LEFT);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sets a path along which the peel will follow.
|
|
140
|
+
* Can be a flat line segment or a bezier curve.
|
|
141
|
+
* @param {...number} x/y Points along the path. 4 arguments indicates a
|
|
142
|
+
* linear path along 2 points (p1 to p2), while 8 arguments indicates a
|
|
143
|
+
* bezier curve from p1 to p2 using control points c1 and c2. The first
|
|
144
|
+
* and last two arguments represent p1 and p2, respectively.
|
|
145
|
+
*/
|
|
146
|
+
setPeelPath(...args) {
|
|
147
|
+
const p1 = new Point(args[0], args[1]);
|
|
148
|
+
if (args.length === 4) {
|
|
149
|
+
const p2 = new Point(args[2], args[3]);
|
|
150
|
+
this.path = new LineSegment(p1, p2);
|
|
151
|
+
}
|
|
152
|
+
else if (args.length === 8) {
|
|
153
|
+
const c1 = new Point(args[2], args[3]);
|
|
154
|
+
const c2 = new Point(args[4], args[5]);
|
|
155
|
+
const p2 = new Point(args[6], args[7]);
|
|
156
|
+
this.path = new BezierCurve(p1, c1, c2, p2);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Sets a function to be called when the user either presses or drags.
|
|
161
|
+
* @param {Function} fn The function to be called on press. This function will
|
|
162
|
+
* be called with the original event as the first argument, and the x, y
|
|
163
|
+
* coordinates of the event as the 2nd and 3rd arguments, respectively.
|
|
164
|
+
* @param {HTMLElement} el The element to initiate the event.
|
|
165
|
+
* If not passed, this will be the element associated with the Peel
|
|
166
|
+
* instance. Allowing this to be passed lets another element serve as a
|
|
167
|
+
* "hit area" that can be larger than the element itself.
|
|
168
|
+
*/
|
|
169
|
+
handle(event, fn, el) {
|
|
170
|
+
if (event === "drag") {
|
|
171
|
+
this.dragHandler = fn;
|
|
172
|
+
}
|
|
173
|
+
else if (event === "press") {
|
|
174
|
+
this.pressHandler = fn;
|
|
175
|
+
}
|
|
176
|
+
this.setupDragListeners(el);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Sets up the drag events needed for both drag and press handlers.
|
|
180
|
+
* @returns {Function} A function that can be called to remove the listeners.
|
|
181
|
+
*/
|
|
182
|
+
setupDragListeners(el) {
|
|
183
|
+
if (this._removeDragListeners)
|
|
184
|
+
return;
|
|
185
|
+
el = el || this.el;
|
|
186
|
+
let isDragging = false;
|
|
187
|
+
const dragStart = (evt) => {
|
|
188
|
+
if (this.options.dragPreventsDefault) {
|
|
189
|
+
evt.preventDefault();
|
|
190
|
+
}
|
|
191
|
+
isDragging = true;
|
|
192
|
+
};
|
|
193
|
+
const dragMove = (evt) => {
|
|
194
|
+
if (isDragging) {
|
|
195
|
+
callHandlerIfAny(this.dragHandler, evt);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const dragEnd = (evt) => {
|
|
199
|
+
if (isDragging && this.el.contains(evt.target)) {
|
|
200
|
+
callHandlerIfAny(this.pressHandler, evt);
|
|
201
|
+
}
|
|
202
|
+
isDragging = false;
|
|
203
|
+
};
|
|
204
|
+
const callHandlerIfAny = (fn, evt) => {
|
|
205
|
+
var coords = getEventCoordinates(evt, this.el);
|
|
206
|
+
if (fn) {
|
|
207
|
+
fn(evt, coords.x, coords.y);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
el.addEventListener('mousedown', dragStart);
|
|
211
|
+
el.addEventListener('touchstart', dragStart);
|
|
212
|
+
document.documentElement.addEventListener('mousemove', dragMove);
|
|
213
|
+
document.documentElement.addEventListener('touchmove', dragMove);
|
|
214
|
+
document.documentElement.addEventListener('mouseup', dragEnd, { passive: false });
|
|
215
|
+
document.documentElement.addEventListener('touchend', dragEnd, { passive: false });
|
|
216
|
+
this._removeDragListeners = () => {
|
|
217
|
+
el.removeEventListener('mousedown', dragStart);
|
|
218
|
+
el.removeEventListener('touchstart', dragStart);
|
|
219
|
+
document.documentElement.removeEventListener('mousemove', dragMove);
|
|
220
|
+
document.documentElement.removeEventListener('touchmove', dragMove);
|
|
221
|
+
document.documentElement.removeEventListener('mouseup', dragEnd);
|
|
222
|
+
document.documentElement.removeEventListener('touchend', dragEnd);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
removeDragListeners() {
|
|
226
|
+
this._removeDragListeners?.();
|
|
227
|
+
this._removeDragListeners = undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Sets the peel effect to a point in time along a previously
|
|
231
|
+
* specified path. Will throw an error if no path exists.
|
|
232
|
+
* @param {number} n The time value (between 0 and 1).
|
|
233
|
+
*/
|
|
234
|
+
setTimeAlongPath(t) {
|
|
235
|
+
t = clamp(t);
|
|
236
|
+
var point = this.path.getPointForTime(t);
|
|
237
|
+
this.timeAlongPath = t;
|
|
238
|
+
this.setPeelPosition(point);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Sets the position of the peel effect. This point is the position
|
|
242
|
+
* of the corner that is being peeled back.
|
|
243
|
+
*/
|
|
244
|
+
setPeelPosition(...args) {
|
|
245
|
+
var pos = this.getPoint(...args);
|
|
246
|
+
pos = this.getConstrainedPeelPosition(pos);
|
|
247
|
+
if (!pos) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.peelLineSegment = this.getPeelLineSegment(pos);
|
|
251
|
+
this.peelLineRotation = this.peelLineSegment.getAngle();
|
|
252
|
+
this.setClipping();
|
|
253
|
+
this.setBackTransform(pos);
|
|
254
|
+
this.setEffects();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Sets a constraint on the distance of the peel. This can be thought of as a
|
|
258
|
+
* point on the layers that are connected and cannot be torn apart. Typically
|
|
259
|
+
* this only makes sense as a point on the outer edge, such as the left edge
|
|
260
|
+
* of an open book, or the top edge of a calendar. In this case, simply using
|
|
261
|
+
* 2 constraint points (top-left/bottom-left for a book, etc) will create the
|
|
262
|
+
* desired effect. An arbitrary point can also be used with an effect like a
|
|
263
|
+
* thumbtack holding the pages together.
|
|
264
|
+
*/
|
|
265
|
+
addPeelConstraint(...args) {
|
|
266
|
+
var p = this.getPoint(...args);
|
|
267
|
+
var radius = this.corner.subtract(p).getLength();
|
|
268
|
+
this.constraints.push(new Circle(p, radius));
|
|
269
|
+
this.calculateFlipConstraint();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Gets the ratio of the area of the clipped top layer to the total area. This
|
|
273
|
+
* is used to calculate a fade threshold.
|
|
274
|
+
* @returns {number} A value between 0 and 1.
|
|
275
|
+
*/
|
|
276
|
+
getAmountClipped() {
|
|
277
|
+
var topArea = this.getTopClipArea();
|
|
278
|
+
var totalArea = this.width * this.height;
|
|
279
|
+
return normalize(topArea, totalArea, 0);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Gets the area of the clipped top layer.
|
|
283
|
+
* @returns {number}
|
|
284
|
+
*/
|
|
285
|
+
getTopClipArea() {
|
|
286
|
+
var top = new Polygon();
|
|
287
|
+
this.elementBox.forEach((side) => {
|
|
288
|
+
this.distributeLineByPeelLine(side, top);
|
|
289
|
+
}, this);
|
|
290
|
+
return Polygon.getArea(top.getPoints());
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Determines which of the constraints should be used as the flip constraint
|
|
294
|
+
* by checking which has a y value closes to the corner (because the
|
|
295
|
+
* constraint operates relative to the vertical midline). Only one constraint
|
|
296
|
+
* should be required - changing the order of the constraints can help to
|
|
297
|
+
* achieve the proper effect and more than one will interfere with each other.
|
|
298
|
+
*/
|
|
299
|
+
calculateFlipConstraint() {
|
|
300
|
+
this.flipConstraint = this.constraints.slice().sort((a, b) => {
|
|
301
|
+
var aY = this.corner.y - a.center.y;
|
|
302
|
+
var bY = this.corner.y - b.center.y;
|
|
303
|
+
return aY - bY;
|
|
304
|
+
})[0];
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Sets the clipping points of the top and back layers based on a line
|
|
308
|
+
* segment that represents the peel line.
|
|
309
|
+
*/
|
|
310
|
+
setClipping() {
|
|
311
|
+
var top = new Polygon();
|
|
312
|
+
var back = new Polygon();
|
|
313
|
+
this.clippingBox.forEach((side) => {
|
|
314
|
+
this.distributeLineByPeelLine(side, top, back);
|
|
315
|
+
}, this);
|
|
316
|
+
this.topClip.setPoints(top.getPoints());
|
|
317
|
+
this.backClip.setPoints(back.getPoints());
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Distributes the first point in the given line segment and its intersect
|
|
321
|
+
* with the peel line, if there is one.
|
|
322
|
+
* @param {LineSegment} seg The line segment to check against.
|
|
323
|
+
* @param {Polygon} poly1 The first polygon.
|
|
324
|
+
* @param {Polygon} [poly2] The second polygon.
|
|
325
|
+
*/
|
|
326
|
+
distributeLineByPeelLine(seg, poly1, poly2) {
|
|
327
|
+
var intersect = this.peelLineSegment.getIntersectPoint(seg);
|
|
328
|
+
this.distributePointByPeelLine(seg.p1, poly1, poly2);
|
|
329
|
+
this.distributePointByPeelLine(intersect, poly1, poly2);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Distributes the given point to one of two polygons based on which side of
|
|
333
|
+
* the peel line it falls upon (if it falls directly on the line segment
|
|
334
|
+
* it is added to both).
|
|
335
|
+
* @param {Point} p The point to be distributed.
|
|
336
|
+
* @param {Polygon} poly1 The first polygon.
|
|
337
|
+
* @param {Polygon} [poly2] The second polygon.
|
|
338
|
+
*/
|
|
339
|
+
distributePointByPeelLine(p, poly1, poly2) {
|
|
340
|
+
if (!p)
|
|
341
|
+
return;
|
|
342
|
+
var d = this.peelLineSegment.getPointDeterminant(p);
|
|
343
|
+
if (d <= 0) {
|
|
344
|
+
poly1.addPoint(p);
|
|
345
|
+
}
|
|
346
|
+
if (d >= 0 && poly2) {
|
|
347
|
+
poly2.addPoint(this.flipPointHorizontally(p));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Finds or creates a layer in the dom.
|
|
352
|
+
* @param {HTMLElement} parent The parent if the element needs to be created.
|
|
353
|
+
* @param {numer} zIndex The z index of the layer.
|
|
354
|
+
* @returns {HTMLElement}
|
|
355
|
+
*/
|
|
356
|
+
findOrCreateLayer(layer, parent, zIndex) {
|
|
357
|
+
const optId = layer + '-element';
|
|
358
|
+
const domId = prefix(layer);
|
|
359
|
+
let el = parent.querySelector(this.options[optId] || (`.${domId}`));
|
|
360
|
+
if (!el) {
|
|
361
|
+
el = createElement(parent, domId);
|
|
362
|
+
}
|
|
363
|
+
el.classList.add(prefix('layer'));
|
|
364
|
+
el.style.zIndex = "" + zIndex;
|
|
365
|
+
return el;
|
|
366
|
+
}
|
|
367
|
+
getPoint(...args) {
|
|
368
|
+
let xy;
|
|
369
|
+
if (args.length === 1) {
|
|
370
|
+
const value = args[0];
|
|
371
|
+
if (typeof value === "number") {
|
|
372
|
+
return this.getCornerPoint(value);
|
|
373
|
+
}
|
|
374
|
+
else if (Array.isArray(value)) {
|
|
375
|
+
xy = value;
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
xy = [value.x, value.y];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
xy = args;
|
|
383
|
+
}
|
|
384
|
+
return new Point(...xy);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Returns a corner point based on an id defined in PeelCorners.
|
|
388
|
+
*/
|
|
389
|
+
getCornerPoint(corner) {
|
|
390
|
+
var x = +!!(corner & 1) * this.width;
|
|
391
|
+
var y = +!!(corner & 2) * this.height;
|
|
392
|
+
return new Point(x, y);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Sets up the main layers used for the effect that may include a possible
|
|
396
|
+
* subclip shape.
|
|
397
|
+
*/
|
|
398
|
+
setupLayers() {
|
|
399
|
+
// The inner layers may be wrapped later, so keep a reference to them here.
|
|
400
|
+
var topInnerLayer = this.topLayer = this.findOrCreateLayer('top', this.el, 2);
|
|
401
|
+
var backInnerLayer = this.backLayer = this.findOrCreateLayer('back', this.el, 3);
|
|
402
|
+
this.bottomLayer = this.findOrCreateLayer('bottom', this.el, 1);
|
|
403
|
+
const matchedShapes = shapes.filter(key => key in this.options);
|
|
404
|
+
if (matchedShapes.length > 1) {
|
|
405
|
+
throw new Error(`You must specify at most one of the shapes: ${shapes}`);
|
|
406
|
+
}
|
|
407
|
+
if (matchedShapes.length === 1) {
|
|
408
|
+
const shape = {
|
|
409
|
+
type: matchedShapes[0],
|
|
410
|
+
attributes: this.options[matchedShapes[0]],
|
|
411
|
+
};
|
|
412
|
+
// If there is an SVG shape specified in the options, then this needs to
|
|
413
|
+
// be a separate clipped element because Safari/Mobile Safari can't handle
|
|
414
|
+
// nested clip-paths. The current top/back element will become the shape
|
|
415
|
+
// clip, so wrap them with an "outer" clip element that will become the
|
|
416
|
+
// new layer for the peel effect. The bottom layer does not require this
|
|
417
|
+
// effect, so the shape clip can be set directly on it.
|
|
418
|
+
this.topLayer = this.wrapShapeLayer(this.topLayer, 'top-outer-clip');
|
|
419
|
+
this.backLayer = this.wrapShapeLayer(this.backLayer, 'back-outer-clip');
|
|
420
|
+
this.topShapeClip = new SVGClip(topInnerLayer, shape);
|
|
421
|
+
this.backShapeClip = new SVGClip(backInnerLayer, shape);
|
|
422
|
+
this.bottomShapeClip = new SVGClip(this.bottomLayer, shape);
|
|
423
|
+
if (this.options.topShadowCreatesShape) {
|
|
424
|
+
this.topShadowElement = this.setupDropShadow(shape, topInnerLayer);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.topShadowElement = this.findOrCreateLayer('top-shadow', topInnerLayer, 1);
|
|
429
|
+
}
|
|
430
|
+
this.topClip = new SVGClip(this.topLayer);
|
|
431
|
+
this.backClip = new SVGClip(this.backLayer);
|
|
432
|
+
this.backShadowElement = this.findOrCreateLayer('back-shadow', backInnerLayer, 1);
|
|
433
|
+
this.backReflectionElement = this.findOrCreateLayer('back-reflection', backInnerLayer, 2);
|
|
434
|
+
this.bottomShadowElement = this.findOrCreateLayer('bottom-shadow', this.bottomLayer, 1);
|
|
435
|
+
this.usesBoxShadow = matchedShapes.length === 0;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Creates an inline SVG element to be used as a layer for a drop shadow filter
|
|
439
|
+
* effect. Note that drop shadow filters currently have some odd quirks in
|
|
440
|
+
* Blink such as blur radius changing depending on rotation, etc.
|
|
441
|
+
* @param {Object} shape A shape describing the SVG element to be used.
|
|
442
|
+
* @param {HTMLElement} parent The parent element where the layer will be added.
|
|
443
|
+
* @returns {SVGElement}
|
|
444
|
+
*/
|
|
445
|
+
setupDropShadow(shape, parent) {
|
|
446
|
+
var svg = createSVGElement('svg', parent, {
|
|
447
|
+
'class': prefix('layer')
|
|
448
|
+
});
|
|
449
|
+
createSVGElement(shape.type, svg, shape.attributes);
|
|
450
|
+
return svg;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Wraps the passed element in another layer, preserving its z-index. Also
|
|
454
|
+
* add a "shape-layer" class to the layer which now becomes a shape clip.
|
|
455
|
+
*/
|
|
456
|
+
wrapShapeLayer(el, layer) {
|
|
457
|
+
el.classList.add(prefix('shape-layer'));
|
|
458
|
+
var outerLayer = this.findOrCreateLayer(layer, this.el, Number.parseInt(el.style.zIndex));
|
|
459
|
+
outerLayer.appendChild(el);
|
|
460
|
+
return outerLayer;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Sets up the dimensions of the element box and clipping box that area used
|
|
464
|
+
* in the effect.
|
|
465
|
+
*/
|
|
466
|
+
setupDimensions() {
|
|
467
|
+
this.width = this.el.offsetWidth;
|
|
468
|
+
this.height = this.el.offsetHeight;
|
|
469
|
+
this.center = new Point(this.width / 2, this.height / 2);
|
|
470
|
+
this.elementBox = this.getScaledBox(1);
|
|
471
|
+
this.clippingBox = this.getScaledBox(this.options.clippingBoxScale);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Gets a box defined by 4 line segments that is at a scale of the main
|
|
475
|
+
* element.
|
|
476
|
+
* @param {number} scale The scale for the box to be.
|
|
477
|
+
*/
|
|
478
|
+
getScaledBox(scale) {
|
|
479
|
+
// Box scale is equal to:
|
|
480
|
+
// 1 * the bottom/right scale
|
|
481
|
+
// 0 * the top/left scale.
|
|
482
|
+
var brScale = scale;
|
|
483
|
+
var tlScale = scale - 1;
|
|
484
|
+
var tl = new Point(-this.width * tlScale, -this.height * tlScale);
|
|
485
|
+
var tr = new Point(this.width * brScale, -this.height * tlScale);
|
|
486
|
+
var br = new Point(this.width * brScale, this.height * brScale);
|
|
487
|
+
var bl = new Point(-this.width * tlScale, this.height * brScale);
|
|
488
|
+
return [
|
|
489
|
+
new LineSegment(tl, tr),
|
|
490
|
+
new LineSegment(tr, br),
|
|
491
|
+
new LineSegment(br, bl),
|
|
492
|
+
new LineSegment(bl, tl)
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Returns the peel position adjusted by constraints, if there are any.
|
|
497
|
+
* @param {Point} point The peel position to be constrained.
|
|
498
|
+
* @returns {Point}
|
|
499
|
+
*/
|
|
500
|
+
getConstrainedPeelPosition(pos) {
|
|
501
|
+
this.constraints.forEach((area) => {
|
|
502
|
+
var offset = this.getFlipConstraintOffset(area, pos);
|
|
503
|
+
if (offset) {
|
|
504
|
+
area = new Circle(area.center, area.radius - offset);
|
|
505
|
+
}
|
|
506
|
+
pos = area.constrainPoint(pos);
|
|
507
|
+
}, this);
|
|
508
|
+
return pos;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Returns an offset to "pull" a corner in to prevent the peel effect from
|
|
512
|
+
* suddenly flipping around its axis. This offset is intended to be applied
|
|
513
|
+
* on the Y axis when dragging away from the center.
|
|
514
|
+
* @param {Circle} area The constraint to check against.
|
|
515
|
+
* @param {Point} point The peel position to be constrained.
|
|
516
|
+
*/
|
|
517
|
+
getFlipConstraintOffset(area, pos) {
|
|
518
|
+
const offset = this.options.flipConstraintOffset;
|
|
519
|
+
if (area === this.flipConstraint && offset) {
|
|
520
|
+
var cornerToCenter = this.corner.subtract(this.center);
|
|
521
|
+
var cornerToConstraint = this.corner.subtract(area.center);
|
|
522
|
+
var baseAngle = cornerToConstraint.getAngle();
|
|
523
|
+
// Normalized angles are rotated to be in the same space relative
|
|
524
|
+
// to the constraint.
|
|
525
|
+
var nCornerToConstraint = cornerToConstraint.rotate(-baseAngle);
|
|
526
|
+
var nPosToConstraint = pos.subtract(area.center).rotate(-baseAngle);
|
|
527
|
+
// Flip the vector vertically if the corner is in the bottom left or top
|
|
528
|
+
// right relative to the center, as the effect should always pull away
|
|
529
|
+
// from the vertical midline.
|
|
530
|
+
if (cornerToCenter.x * cornerToCenter.y < 0) {
|
|
531
|
+
nPosToConstraint.y *= -1;
|
|
532
|
+
}
|
|
533
|
+
if (nPosToConstraint.x > 0 && nPosToConstraint.y > 0) {
|
|
534
|
+
return normalize(nPosToConstraint.getAngle(), 45, 0) * offset;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Gets the line segment that represents the current peel line.
|
|
540
|
+
* @param {Point} point The position of the peel corner.
|
|
541
|
+
* @returns {LineSegment}
|
|
542
|
+
*/
|
|
543
|
+
getPeelLineSegment(point) {
|
|
544
|
+
// The point midway between the peel position and the corner.
|
|
545
|
+
var halfToCorner = this.corner.subtract(point).scale(.5);
|
|
546
|
+
var midpoint = point.add(halfToCorner);
|
|
547
|
+
if (halfToCorner.x === 0 && halfToCorner.y === 0) {
|
|
548
|
+
// If the corner is the same as the point, then set half to corner
|
|
549
|
+
// to be the center, and keep the midpoint where it is. This will
|
|
550
|
+
// ensure a non-zero peel line.
|
|
551
|
+
halfToCorner = point.subtract(this.center);
|
|
552
|
+
}
|
|
553
|
+
var l = halfToCorner.getLength();
|
|
554
|
+
var mult = (Math.max(this.width, this.height) / l) * 10;
|
|
555
|
+
var half = halfToCorner.rotate(-90).scale(mult);
|
|
556
|
+
var p1 = midpoint.add(half);
|
|
557
|
+
var p2 = midpoint.subtract(half);
|
|
558
|
+
return new LineSegment(p1, p2);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Sets the transform of the back layer.
|
|
562
|
+
* @param {Point} pos The position of the peeling corner.
|
|
563
|
+
*/
|
|
564
|
+
setBackTransform(pos) {
|
|
565
|
+
var mirroredCorner = this.flipPointHorizontally(this.corner);
|
|
566
|
+
var r = (this.peelLineRotation - 90) * 2;
|
|
567
|
+
var t = pos.subtract(mirroredCorner.rotate(r));
|
|
568
|
+
var css = 'translate(' + round(t.x) + 'px, ' + round(t.y) + 'px) rotate(' + round(r) + 'deg)';
|
|
569
|
+
setTransform(this.backLayer, css);
|
|
570
|
+
// Set the top shadow element here as well, as the
|
|
571
|
+
// position and rotation matches that of the back layer.
|
|
572
|
+
setTransform(this.topShadowElement, css);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Gets the distance of the peel line along an imaginary line that runs
|
|
576
|
+
* between the corners that it "faces". For example, if the peel line
|
|
577
|
+
* is rotated 45 degrees, then it can be considered to be between the top left
|
|
578
|
+
* and bottom right corners. This function will return how far the peel line
|
|
579
|
+
* has advanced along that line.
|
|
580
|
+
* @returns {number} A position >= 0.
|
|
581
|
+
*/
|
|
582
|
+
getPeelLineDistance() {
|
|
583
|
+
let cornerId, opposingCornerId;
|
|
584
|
+
if (this.peelLineRotation < 90) {
|
|
585
|
+
cornerId = PeelCorners.TOP_RIGHT;
|
|
586
|
+
opposingCornerId = PeelCorners.BOTTOM_LEFT;
|
|
587
|
+
}
|
|
588
|
+
else if (this.peelLineRotation < 180) {
|
|
589
|
+
cornerId = PeelCorners.BOTTOM_RIGHT;
|
|
590
|
+
opposingCornerId = PeelCorners.TOP_LEFT;
|
|
591
|
+
}
|
|
592
|
+
else if (this.peelLineRotation < 270) {
|
|
593
|
+
cornerId = PeelCorners.BOTTOM_LEFT;
|
|
594
|
+
opposingCornerId = PeelCorners.TOP_RIGHT;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
cornerId = PeelCorners.TOP_LEFT;
|
|
598
|
+
opposingCornerId = PeelCorners.BOTTOM_RIGHT;
|
|
599
|
+
}
|
|
600
|
+
const corner = this.getCornerPoint(cornerId);
|
|
601
|
+
const opposingCorner = this.getCornerPoint(opposingCornerId);
|
|
602
|
+
// Scale the line segment past the original corners so that the effects
|
|
603
|
+
// can have a nice fadeout even past 1.
|
|
604
|
+
var cornerToCorner = new LineSegment(corner, opposingCorner).scale(2);
|
|
605
|
+
var intersect = this.peelLineSegment.getIntersectPoint(cornerToCorner);
|
|
606
|
+
if (!intersect) {
|
|
607
|
+
// If there is no intersect, then assume that it has run past the opposing
|
|
608
|
+
// corner and set the distance to well past the full distance.
|
|
609
|
+
return 2;
|
|
610
|
+
}
|
|
611
|
+
var distanceToPeelLine = corner.subtract(intersect).getLength();
|
|
612
|
+
var totalDistance = corner.subtract(opposingCorner).getLength();
|
|
613
|
+
return (distanceToPeelLine / totalDistance);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Sets shadows and fade effects.
|
|
617
|
+
*/
|
|
618
|
+
setEffects() {
|
|
619
|
+
var t = this.getPeelLineDistance();
|
|
620
|
+
this.setTopShadow(t);
|
|
621
|
+
this.setBackShadow(t);
|
|
622
|
+
this.setBackReflection(t);
|
|
623
|
+
this.setBottomShadow(t);
|
|
624
|
+
this.setFade();
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Sets the top shadow as either a box-shadow or a drop-shadow filter.
|
|
628
|
+
* @param {number} t Position of the peel line from corner to corner.
|
|
629
|
+
*/
|
|
630
|
+
setTopShadow(t) {
|
|
631
|
+
if (!this.options.topShadow) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const { topShadowBlur, topShadowOffsetX, topShadowOffsetY, topShadowAlpha } = this.options;
|
|
635
|
+
const sAlpha = this.exponential(t, 5, topShadowAlpha);
|
|
636
|
+
if (this.usesBoxShadow) {
|
|
637
|
+
setBoxShadow(this.topShadowElement, topShadowOffsetX, topShadowOffsetY, topShadowBlur, 0, sAlpha);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
setDropShadow(this.topShadowElement, topShadowOffsetX, topShadowOffsetY, topShadowBlur, sAlpha);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Gets a number either distributed along a bell curve or increasing linearly.
|
|
645
|
+
* @param {number} n The number to transform.
|
|
646
|
+
* @param {boolean} dist Whether or not to use distribution.
|
|
647
|
+
* @param {number} mult A multiplier for the result.
|
|
648
|
+
* @returns {number}
|
|
649
|
+
*/
|
|
650
|
+
distributeOrLinear(n, dist, mult) {
|
|
651
|
+
if (dist) {
|
|
652
|
+
return distribute(n, mult);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
return n * mult;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Gets a number either distributed exponentially, clamped to a range between
|
|
660
|
+
* 0 and 1, and multiplied by a multiplier.
|
|
661
|
+
* @param {number} n The number to transform.
|
|
662
|
+
* @param {number} exp The exponent to be used.
|
|
663
|
+
* @param {number} mult A multiplier for the result.
|
|
664
|
+
* @returns {number}
|
|
665
|
+
*/
|
|
666
|
+
exponential(n, exp, mult) {
|
|
667
|
+
return mult * clamp(Math.pow(1 + n, exp) - 1);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Sets reflection of the back face as a linear gradient.
|
|
671
|
+
* @param {number} t Position of the peel line from corner to corner.
|
|
672
|
+
*/
|
|
673
|
+
setBackReflection(t) {
|
|
674
|
+
const stops = [];
|
|
675
|
+
if (this.options.backReflection && t > 0) {
|
|
676
|
+
var rDistribute = this.options.backReflectionDistribute;
|
|
677
|
+
var rSize = this.options.backReflectionSize;
|
|
678
|
+
var rOffset = this.options.backReflectionOffset;
|
|
679
|
+
var rAlpha = this.options.backReflectionAlpha;
|
|
680
|
+
var reflectionSize = this.distributeOrLinear(t, rDistribute, rSize);
|
|
681
|
+
var rStop = t - rOffset;
|
|
682
|
+
var rMid = rStop - reflectionSize;
|
|
683
|
+
var rStart = rMid - reflectionSize;
|
|
684
|
+
stops.push(getWhiteStop(0, 0));
|
|
685
|
+
stops.push(getWhiteStop(0, rStart));
|
|
686
|
+
stops.push(getWhiteStop(rAlpha, rMid));
|
|
687
|
+
stops.push(getWhiteStop(0, rStop));
|
|
688
|
+
}
|
|
689
|
+
setBackgroundGradient(this.backReflectionElement, 180 - this.peelLineRotation, stops);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Sets shadow of the back face as a linear gradient.
|
|
693
|
+
* @param {number} t Position of the peel line from corner to corner.
|
|
694
|
+
*/
|
|
695
|
+
setBackShadow(t) {
|
|
696
|
+
const stops = [];
|
|
697
|
+
if (this.options.backShadow && t > 0) {
|
|
698
|
+
const { backShadowSize: sSize, backShadowOffset: sOffset, backShadowAlpha: sAlpha, backShadowDistribute: sDistribute, } = this.options;
|
|
699
|
+
var shadowSize = this.distributeOrLinear(t, sDistribute, sSize);
|
|
700
|
+
var shadowStop = t - sOffset;
|
|
701
|
+
var shadowMid = shadowStop - shadowSize;
|
|
702
|
+
var shadowStart = shadowMid - shadowSize;
|
|
703
|
+
stops.push(getBlackStop(0, 0));
|
|
704
|
+
stops.push(getBlackStop(0, shadowStart));
|
|
705
|
+
stops.push(getBlackStop(sAlpha, shadowMid));
|
|
706
|
+
stops.push(getBlackStop(sAlpha, shadowStop));
|
|
707
|
+
}
|
|
708
|
+
setBackgroundGradient(this.backShadowElement, 180 - this.peelLineRotation, stops);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Sets the bottom shadow as a linear gradient.
|
|
712
|
+
* @param {number} t Position of the peel line from corner to corner.
|
|
713
|
+
*/
|
|
714
|
+
setBottomShadow(t) {
|
|
715
|
+
const stops = [];
|
|
716
|
+
if (this.options.bottomShadow && t > 0) {
|
|
717
|
+
const { bottomShadowSize: sSize, bottomShadowOffset: offset, bottomShadowDarkAlpha: darkAlpha, bottomShadowLightAlpha: lightAlpha, bottomShadowDistribute: sDistribute } = this.options;
|
|
718
|
+
var darkShadowStart = t - (.025 - offset);
|
|
719
|
+
var midShadowStart = darkShadowStart - (this.distributeOrLinear(t, sDistribute, .03) * sSize) - offset;
|
|
720
|
+
var lightShadowStart = midShadowStart - ((.02 * sSize) - offset);
|
|
721
|
+
stops.push(getBlackStop(0, 0), getBlackStop(0, lightShadowStart), getBlackStop(lightAlpha, midShadowStart), getBlackStop(lightAlpha, darkShadowStart), getBlackStop(darkAlpha, t));
|
|
722
|
+
}
|
|
723
|
+
setBackgroundGradient(this.bottomShadowElement, this.peelLineRotation + 180, stops);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Sets the fading effect of the top layer, if a threshold is set.
|
|
727
|
+
*/
|
|
728
|
+
setFade() {
|
|
729
|
+
const { fadeThreshold } = this.options;
|
|
730
|
+
let opacity = 1;
|
|
731
|
+
let n;
|
|
732
|
+
if (fadeThreshold) {
|
|
733
|
+
if (this.timeAlongPath !== undefined) {
|
|
734
|
+
n = this.timeAlongPath;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
n = this.getAmountClipped();
|
|
738
|
+
}
|
|
739
|
+
if (n > fadeThreshold) {
|
|
740
|
+
opacity = (1 - n) / (1 - fadeThreshold);
|
|
741
|
+
}
|
|
742
|
+
setOpacity(this.topLayer, opacity);
|
|
743
|
+
setOpacity(this.backLayer, opacity);
|
|
744
|
+
setOpacity(this.bottomShadowElement, opacity);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Flips a point along an imaginary vertical midpoint.
|
|
749
|
+
* @param {Array} points The points to be flipped.
|
|
750
|
+
* @returns {Array}
|
|
751
|
+
*/
|
|
752
|
+
flipPointHorizontally(p) {
|
|
753
|
+
return new Point(p.x - ((p.x - this.center.x) * 2), p.y);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Post setup initialization.
|
|
757
|
+
*/
|
|
758
|
+
init() {
|
|
759
|
+
if (this.options.setPeelOnInit) {
|
|
760
|
+
this.setPeelPosition(this.corner);
|
|
761
|
+
}
|
|
762
|
+
this.el.classList.add(prefix('ready'));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
Peel.defaultOptions = {
|
|
766
|
+
/**
|
|
767
|
+
* Sets the corner for the effect to peel back from.
|
|
768
|
+
*
|
|
769
|
+
* @defaults PeelCorners.BOTTOM_RIGHT
|
|
770
|
+
*/
|
|
771
|
+
corner: PeelCorners.BOTTOM_RIGHT,
|
|
772
|
+
/**
|
|
773
|
+
* Threshold above which the top layer (including the backside) layer
|
|
774
|
+
* will begin to fade out. This is calculated based on the visible clipped
|
|
775
|
+
* area of the polygon. If a peel path is set, it will use the progress along
|
|
776
|
+
* the path instead.
|
|
777
|
+
*/
|
|
778
|
+
fadeThreshold: 0,
|
|
779
|
+
/**
|
|
780
|
+
* Creates a shadow effect on the top layer of the peel. This may be a box-shadow or drop-shadow (filter) depending on the shape of the clipping.
|
|
781
|
+
*/
|
|
782
|
+
topShadow: true,
|
|
783
|
+
topShadowBlur: 5,
|
|
784
|
+
topShadowAlpha: .5,
|
|
785
|
+
topShadowOffsetX: 0,
|
|
786
|
+
topShadowOffsetY: 1,
|
|
787
|
+
/**
|
|
788
|
+
* When a complex (non-rectangular) shape is used for the clipping effect, if this option is true another SVG shape will be embedded in the top layer to be used as the drop shadow. This is required for the drop-shadow filter effect but can be turned off here as the drop shadow effect can sometimes produce odd results.
|
|
789
|
+
*/
|
|
790
|
+
topShadowCreatesShape: true,
|
|
791
|
+
/**
|
|
792
|
+
* Creates a shiny effect on the back layer of the peel.
|
|
793
|
+
*/
|
|
794
|
+
backReflection: false,
|
|
795
|
+
backReflectionSize: .02,
|
|
796
|
+
backReflectionOffset: 0,
|
|
797
|
+
backReflectionAlpha: .15,
|
|
798
|
+
/**
|
|
799
|
+
* When true, the reflection effect will reach its maximum halfway through the peel, then diminish again. If false, it will continue to grow as the peel advances.
|
|
800
|
+
*/
|
|
801
|
+
backReflectionDistribute: true,
|
|
802
|
+
/**
|
|
803
|
+
* Creates a shadow effect on the back layer of the peel.
|
|
804
|
+
*/
|
|
805
|
+
backShadow: true,
|
|
806
|
+
backShadowSize: .04,
|
|
807
|
+
backShadowOffset: 0,
|
|
808
|
+
backShadowAlpha: .1,
|
|
809
|
+
/**
|
|
810
|
+
* When true, the back shadow effect will reach its maximum halfway through
|
|
811
|
+
* the peel, then diminish again. If false, it will continue to grow as the
|
|
812
|
+
* peel advances. "Book" mode sets this to false so that the effect can still
|
|
813
|
+
* have some depth when the book is "fully open".
|
|
814
|
+
*/
|
|
815
|
+
backShadowDistribute: true,
|
|
816
|
+
/**
|
|
817
|
+
* Creates a shadow effect on the bottom layer of the peel.
|
|
818
|
+
*/
|
|
819
|
+
bottomShadow: true,
|
|
820
|
+
bottomShadowSize: 1.5,
|
|
821
|
+
bottomShadowOffset: 0,
|
|
822
|
+
/**
|
|
823
|
+
* Alpha value (color is black) of the dark shadow on the bottom layer.
|
|
824
|
+
*/
|
|
825
|
+
bottomShadowDarkAlpha: .7,
|
|
826
|
+
/**
|
|
827
|
+
* Alpha value (color is black) of the light shadow on the bottom layer.
|
|
828
|
+
*/
|
|
829
|
+
bottomShadowLightAlpha: .1,
|
|
830
|
+
/**
|
|
831
|
+
* When true, the bottom shadow effect will reach its maximum halfway through the peel, then diminish again. If false, it will continue to grow as the peel advances. "Book" mode sets this to false so that the effect can still have some depth when the book is "fully open".
|
|
832
|
+
*/
|
|
833
|
+
bottomShadowDistribute: true,
|
|
834
|
+
/**
|
|
835
|
+
* If true, the peel effect will be set to its relative corner on initialization.
|
|
836
|
+
*/
|
|
837
|
+
setPeelOnInit: true,
|
|
838
|
+
/**
|
|
839
|
+
* Sets the scale of the clipping box around the element. Default is 4, which means 4 times the element size. This allows the effects like box shadow to be seen even when the upper layer falls outsize the element boundaries. Setting this too high may encounter odd effects with clipping.
|
|
840
|
+
*/
|
|
841
|
+
clippingBoxScale: 4,
|
|
842
|
+
/**
|
|
843
|
+
* When constraining the peel, the effect will "flip" around the axis of the constraint, which tends to look unnatural. This offset will pull the corner in a few pixels when approaching the axis line, which results in a smoother transition instead of a sudden flip. The value here determines how many pixels the corner is pulled in.
|
|
844
|
+
*/
|
|
845
|
+
flipConstraintOffset: 5,
|
|
846
|
+
/**
|
|
847
|
+
* Whether initiating a drag event (by mouse or touch) will call `preventDefault` on the original event.
|
|
848
|
+
*/
|
|
849
|
+
dragPreventsDefault: true,
|
|
850
|
+
preset: undefined,
|
|
851
|
+
};
|
|
852
|
+
/**
|
|
853
|
+
* Class that clips an HTMLElement by an SVG path.
|
|
854
|
+
*/
|
|
855
|
+
class SVGClip {
|
|
856
|
+
/**
|
|
857
|
+
* @param {HTMLElement} el The element to be clipped.
|
|
858
|
+
* @param {Object} [shape] An object defining the SVG element to use in the new
|
|
859
|
+
* clip path. Defaults to a polygon.
|
|
860
|
+
*/
|
|
861
|
+
constructor(el, shape) {
|
|
862
|
+
this.el = el;
|
|
863
|
+
this.shape = SVGClip.createClipPath(el, shape || {
|
|
864
|
+
'type': 'polygon'
|
|
865
|
+
});
|
|
866
|
+
// Chrome needs this for some reason for the clipping to work.
|
|
867
|
+
setTransform(this.el, 'translate(0px,0px)');
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Sets up the global SVG element and its nested defs object to use for new
|
|
871
|
+
* clip paths.
|
|
872
|
+
* @returns {SVGElement}
|
|
873
|
+
*/
|
|
874
|
+
static getDefs() {
|
|
875
|
+
if (!this.defs) {
|
|
876
|
+
this.svg = createSVGElement('svg', null, {
|
|
877
|
+
'class': prefix('svg-clip-element')
|
|
878
|
+
});
|
|
879
|
+
this.defs = createSVGElement('defs', this.svg);
|
|
880
|
+
}
|
|
881
|
+
return this.defs;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Creates a new <clipPath> SVG element and sets the passed html element to be
|
|
885
|
+
* clipped by it.
|
|
886
|
+
* @param {HTMLElement} el The html element to be clipped.
|
|
887
|
+
* @param {Object} obj An object defining the SVG element to be used in the
|
|
888
|
+
* clip path.
|
|
889
|
+
* @returns {SVGElement}
|
|
890
|
+
*/
|
|
891
|
+
static createClipPath(el, obj) {
|
|
892
|
+
var id = SVGClip.getId();
|
|
893
|
+
var clipPath = createSVGElement('clipPath', this.getDefs());
|
|
894
|
+
var svgEl = createSVGElement(obj.type, clipPath, obj.attributes);
|
|
895
|
+
setSVGAttribute(clipPath, 'id', id);
|
|
896
|
+
el.style.clipPath = 'url(#' + id + ')';
|
|
897
|
+
return svgEl;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Gets the next svg clipping id.
|
|
901
|
+
*/
|
|
902
|
+
static getId() {
|
|
903
|
+
if (!SVGClip.id) {
|
|
904
|
+
SVGClip.id = 1;
|
|
905
|
+
}
|
|
906
|
+
return 'svg-clip-' + SVGClip.id++;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Sets the "points" attribute of the clip path shape. This only makes sense
|
|
910
|
+
* for polygon shapes.
|
|
911
|
+
* @param {Array} points The points to be used.
|
|
912
|
+
*/
|
|
913
|
+
setPoints(points) {
|
|
914
|
+
var str = points.map(function (p) {
|
|
915
|
+
return round(p.x) + ',' + round(p.y);
|
|
916
|
+
}).join(' ');
|
|
917
|
+
setSVGAttribute(this.shape, 'points', str);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* A class that represents a circle.
|
|
922
|
+
*/
|
|
923
|
+
class Circle {
|
|
924
|
+
constructor(center, radius) {
|
|
925
|
+
this.center = center;
|
|
926
|
+
this.radius = radius;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Determines whether a point is contained within the circle.
|
|
930
|
+
* @param {Point} p The point.
|
|
931
|
+
* @returns {boolean}
|
|
932
|
+
*/
|
|
933
|
+
containsPoint(p) {
|
|
934
|
+
if (this.boundingRectContainsPoint(p)) {
|
|
935
|
+
var dx = this.center.x - p.x;
|
|
936
|
+
var dy = this.center.y - p.y;
|
|
937
|
+
dx *= dx;
|
|
938
|
+
dy *= dy;
|
|
939
|
+
var distanceSquared = dx + dy;
|
|
940
|
+
var radiusSquared = this.radius * this.radius;
|
|
941
|
+
return distanceSquared <= radiusSquared;
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Determines whether a point is contained within the bounding box of the circle.
|
|
947
|
+
* @param {Point} p The point.
|
|
948
|
+
* @returns {boolean}
|
|
949
|
+
*/
|
|
950
|
+
boundingRectContainsPoint(p) {
|
|
951
|
+
return p.x >= this.center.x - this.radius && p.x <= this.center.x + this.radius &&
|
|
952
|
+
p.y >= this.center.y - this.radius && p.y <= this.center.y + this.radius;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Moves a point outside the circle to the closest point on the circumference.
|
|
956
|
+
* Rotated angle from the center point should be the same.
|
|
957
|
+
* @param {Point} p The point.
|
|
958
|
+
* @returns {boolean}
|
|
959
|
+
*/
|
|
960
|
+
constrainPoint(p) {
|
|
961
|
+
if (!this.containsPoint(p)) {
|
|
962
|
+
var rotation = p.subtract(this.center).getAngle();
|
|
963
|
+
p = this.center.add(new Point(this.radius, 0).rotate(rotation));
|
|
964
|
+
}
|
|
965
|
+
return p;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* A class that represents a polygon.
|
|
970
|
+
*/
|
|
971
|
+
class Polygon {
|
|
972
|
+
constructor() {
|
|
973
|
+
this.points = [];
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Gets the area of the polygon.
|
|
977
|
+
* @param {Array} points The points describing the polygon.
|
|
978
|
+
*/
|
|
979
|
+
static getArea(points) {
|
|
980
|
+
var sum1 = 0, sum2 = 0;
|
|
981
|
+
points.forEach(function (p, i, arr) {
|
|
982
|
+
var next = arr[(i + 1) % arr.length];
|
|
983
|
+
sum1 += (p.x * next.y);
|
|
984
|
+
sum2 += (p.y * next.x);
|
|
985
|
+
});
|
|
986
|
+
return (sum1 - sum2) / 2;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Adds a point to the polygon.
|
|
990
|
+
* @param {Point} point
|
|
991
|
+
*/
|
|
992
|
+
addPoint(point) {
|
|
993
|
+
this.points.push(point);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Gets the points of the polygon as an array.
|
|
997
|
+
* @returns {Array}
|
|
998
|
+
*/
|
|
999
|
+
getPoints() {
|
|
1000
|
+
return this.points;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* A class representing a bezier curve.
|
|
1005
|
+
*/
|
|
1006
|
+
class BezierCurve {
|
|
1007
|
+
/**
|
|
1008
|
+
* @param p1 The starting point.
|
|
1009
|
+
* @param c1 The control point of p1.
|
|
1010
|
+
* @param c2 The control point of p2.
|
|
1011
|
+
* @param p2 The ending point.
|
|
1012
|
+
*/
|
|
1013
|
+
constructor(p1, c1, c2, p2) {
|
|
1014
|
+
this.p1 = p1;
|
|
1015
|
+
this.c1 = c1;
|
|
1016
|
+
this.c2 = c2;
|
|
1017
|
+
this.p2 = p2;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Gets a point along the line segment for a given time.
|
|
1021
|
+
* @param {number} t The time along the segment, between 0 and 1.
|
|
1022
|
+
* @returns {Point}
|
|
1023
|
+
*/
|
|
1024
|
+
getPointForTime(t) {
|
|
1025
|
+
var b0 = Math.pow(1 - t, 3);
|
|
1026
|
+
var b1 = 3 * t * Math.pow(1 - t, 2);
|
|
1027
|
+
var b2 = 3 * Math.pow(t, 2) * (1 - t);
|
|
1028
|
+
var b3 = Math.pow(t, 3);
|
|
1029
|
+
var x = (b0 * this.p1.x) + (b1 * this.c1.x) + (b2 * this.c2.x) + (b3 * this.p2.x);
|
|
1030
|
+
var y = (b0 * this.p1.y) + (b1 * this.c1.y) + (b2 * this.c2.y) + (b3 * this.p2.y);
|
|
1031
|
+
return new Point(x, y);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* A class that represents a line segment.
|
|
1036
|
+
*/
|
|
1037
|
+
class LineSegment {
|
|
1038
|
+
constructor(p1, p2) {
|
|
1039
|
+
this.p1 = p1;
|
|
1040
|
+
this.p2 = p2;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Gets a point along the line segment for a given time.
|
|
1044
|
+
* @param {number} t The time along the segment, between 0 and 1.
|
|
1045
|
+
* @returns {Point}
|
|
1046
|
+
*/
|
|
1047
|
+
getPointForTime(t) {
|
|
1048
|
+
return this.p1.add(this.getVector().scale(t));
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Takes a scalar and returns a new scaled line segment.
|
|
1052
|
+
* @param {number} n The amount to scale the segment by.
|
|
1053
|
+
* @returns {LineSegment}
|
|
1054
|
+
*/
|
|
1055
|
+
scale(n) {
|
|
1056
|
+
var half = 1 + (n / 2);
|
|
1057
|
+
var p1 = this.p1.add(this.p2.subtract(this.p1).scale(n));
|
|
1058
|
+
var p2 = this.p2.add(this.p1.subtract(this.p2).scale(n));
|
|
1059
|
+
return new LineSegment(p1, p2);
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* The determinant is a number that indicates which side of a line a point
|
|
1063
|
+
* falls on. A positive number means that the point falls inside the area
|
|
1064
|
+
* "clockwise" of the line, ie. the area that the line would sweep if it were
|
|
1065
|
+
* rotated 180 degrees. A negative number would mean the point is in the area
|
|
1066
|
+
* the line would sweep if it were rotated counter-clockwise, or -180 degrees.
|
|
1067
|
+
* 0 indicates that the point falls exactly on the line.
|
|
1068
|
+
* @param {Point} p The point to test against.
|
|
1069
|
+
* @returns {number} A signed number.
|
|
1070
|
+
*/
|
|
1071
|
+
getPointDeterminant(p) {
|
|
1072
|
+
var d = ((p.x - this.p1.x) * (this.p2.y - this.p1.y)) - ((p.y - this.p1.y) * (this.p2.x - this.p1.x));
|
|
1073
|
+
// Tolerance for near-zero.
|
|
1074
|
+
if (d > -LineSegment.EPSILON && d < LineSegment.EPSILON) {
|
|
1075
|
+
d = 0;
|
|
1076
|
+
}
|
|
1077
|
+
return d;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Calculates the point at which another line segment intersects, if any.
|
|
1081
|
+
* @param {LineSegment} seg The second line segment.
|
|
1082
|
+
* @returns {Point|null}
|
|
1083
|
+
*/
|
|
1084
|
+
getIntersectPoint(seg2) {
|
|
1085
|
+
var seg1 = this;
|
|
1086
|
+
function crossProduct(p1, p2) {
|
|
1087
|
+
return p1.x * p2.y - p1.y * p2.x;
|
|
1088
|
+
}
|
|
1089
|
+
var r = seg1.p2.subtract(seg1.p1);
|
|
1090
|
+
var s = seg2.p2.subtract(seg2.p1);
|
|
1091
|
+
var uNumerator = crossProduct(seg2.p1.subtract(seg1.p1), r);
|
|
1092
|
+
var denominator = crossProduct(r, s);
|
|
1093
|
+
if (denominator == 0) {
|
|
1094
|
+
// ignoring colinear and parallel
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
var u = uNumerator / denominator;
|
|
1098
|
+
var t = crossProduct(seg2.p1.subtract(seg1.p1), s) / denominator;
|
|
1099
|
+
if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {
|
|
1100
|
+
return seg1.p1.add(r.scale(t));
|
|
1101
|
+
}
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Returns the angle of the line segment in degrees.
|
|
1106
|
+
* @returns {number}
|
|
1107
|
+
*/
|
|
1108
|
+
getAngle() {
|
|
1109
|
+
return this.getVector().getAngle();
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Gets the vector that represents the line segment.
|
|
1113
|
+
* @returns {Point}
|
|
1114
|
+
*/
|
|
1115
|
+
getVector() {
|
|
1116
|
+
if (!this.vector) {
|
|
1117
|
+
this.vector = this.p2.subtract(this.p1);
|
|
1118
|
+
}
|
|
1119
|
+
return this.vector;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
LineSegment.EPSILON = 1e-6;
|
|
1123
|
+
/**
|
|
1124
|
+
* A class representing a point or 2D vector.
|
|
1125
|
+
*/
|
|
1126
|
+
class Point {
|
|
1127
|
+
constructor(x, y) {
|
|
1128
|
+
this.x = x;
|
|
1129
|
+
this.y = y;
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Gets degrees in radians.
|
|
1133
|
+
* @param {number} deg
|
|
1134
|
+
* @returns {number}
|
|
1135
|
+
*/
|
|
1136
|
+
static degToRad(deg) {
|
|
1137
|
+
return deg / Point.DEGREES_IN_RADIANS;
|
|
1138
|
+
}
|
|
1139
|
+
;
|
|
1140
|
+
/**
|
|
1141
|
+
* Gets radians in degrees.
|
|
1142
|
+
* @param {number} rad
|
|
1143
|
+
* @returns {number}
|
|
1144
|
+
*/
|
|
1145
|
+
static radToDeg(rad) {
|
|
1146
|
+
var deg = rad * Point.DEGREES_IN_RADIANS;
|
|
1147
|
+
while (deg < 0)
|
|
1148
|
+
deg += 360;
|
|
1149
|
+
return deg;
|
|
1150
|
+
}
|
|
1151
|
+
;
|
|
1152
|
+
/**
|
|
1153
|
+
* Creates a new point given a rotation in degrees and a length.
|
|
1154
|
+
* @param {number} deg The rotation of the vector.
|
|
1155
|
+
* @param {number} len The length of the vector.
|
|
1156
|
+
* @returns {Point}
|
|
1157
|
+
*/
|
|
1158
|
+
static vector(deg, len) {
|
|
1159
|
+
var rad = Point.degToRad(deg);
|
|
1160
|
+
return new Point(Math.cos(rad) * len, Math.sin(rad) * len);
|
|
1161
|
+
}
|
|
1162
|
+
;
|
|
1163
|
+
/**
|
|
1164
|
+
* Adds a point.
|
|
1165
|
+
* @param {Point} p
|
|
1166
|
+
* @returns {Point}
|
|
1167
|
+
*/
|
|
1168
|
+
add(p) {
|
|
1169
|
+
return new Point(this.x + p.x, this.y + p.y);
|
|
1170
|
+
}
|
|
1171
|
+
;
|
|
1172
|
+
/**
|
|
1173
|
+
* Subtracts a point.
|
|
1174
|
+
* @param {Point} p
|
|
1175
|
+
* @returns {Point}
|
|
1176
|
+
*/
|
|
1177
|
+
subtract(p) {
|
|
1178
|
+
return new Point(this.x - p.x, this.y - p.y);
|
|
1179
|
+
}
|
|
1180
|
+
;
|
|
1181
|
+
/**
|
|
1182
|
+
* Scales a point by a scalar.
|
|
1183
|
+
* @param {number} n
|
|
1184
|
+
* @returns {Point}
|
|
1185
|
+
*/
|
|
1186
|
+
scale(n) {
|
|
1187
|
+
return new Point(this.x * n, this.y * n);
|
|
1188
|
+
}
|
|
1189
|
+
;
|
|
1190
|
+
/**
|
|
1191
|
+
* Gets the length of the distance to the point.
|
|
1192
|
+
* @returns {number}
|
|
1193
|
+
*/
|
|
1194
|
+
getLength() {
|
|
1195
|
+
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
|
|
1196
|
+
}
|
|
1197
|
+
;
|
|
1198
|
+
/**
|
|
1199
|
+
* Gets the angle of the point in degrees.
|
|
1200
|
+
* @returns {number}
|
|
1201
|
+
*/
|
|
1202
|
+
getAngle() {
|
|
1203
|
+
return Point.radToDeg(Math.atan2(this.y, this.x));
|
|
1204
|
+
}
|
|
1205
|
+
;
|
|
1206
|
+
/**
|
|
1207
|
+
* Returns a new point of the same length with a different angle.
|
|
1208
|
+
* @param {number} deg The angle in degrees.
|
|
1209
|
+
* @returns {Point}
|
|
1210
|
+
*/
|
|
1211
|
+
setAngle(deg) {
|
|
1212
|
+
return Point.vector(deg, this.getLength());
|
|
1213
|
+
}
|
|
1214
|
+
;
|
|
1215
|
+
/**
|
|
1216
|
+
* Rotates the point.
|
|
1217
|
+
* @param {number} deg The amount to rotate by in degrees.
|
|
1218
|
+
* @returns {Point}
|
|
1219
|
+
*/
|
|
1220
|
+
rotate(deg) {
|
|
1221
|
+
return this.setAngle(this.getAngle() + deg);
|
|
1222
|
+
}
|
|
1223
|
+
;
|
|
1224
|
+
}
|
|
1225
|
+
Point.DEGREES_IN_RADIANS = 180 / Math.PI;
|
|
1226
|
+
//# sourceMappingURL=peel.js.map
|