@thinkpixellab-public/px-vue 3.0.97 → 3.0.99

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.
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <component :is="tag" :class="bem({ active: invert ? !active : active })" :style="styleVars" />
3
+ </template>
4
+
5
+ <script>
6
+ import { cssEasings, keywordsToAnimationCssVars } from '../utils/animation.js';
7
+
8
+ export default {
9
+ name: 'px-animate',
10
+ props: {
11
+ tag: { type: String, default: 'div' },
12
+
13
+ enabled: { type: Boolean, default: true },
14
+
15
+ active: { type: Boolean, default: false },
16
+
17
+ invert: { type: Boolean, default: false },
18
+
19
+ /**
20
+ * A string or object that reprensents the animation in it's active state (see animation.js
21
+ * for details).
22
+ */
23
+ animation: { type: [String, Object], default: '' },
24
+
25
+ /**
26
+ * The duration of the animation in seconds.
27
+ */
28
+ duration: { type: Number, default: null },
29
+
30
+ /**
31
+ * The easing function to be used for the animation.
32
+ */
33
+ easing: { type: String, default: null },
34
+
35
+ /**
36
+ * A delay (in seconds) to wait before starting the animation once the active state is entered.
37
+ */
38
+ delay: { type: Number, default: null },
39
+
40
+ /**
41
+ * When true we animate the element if active on load, otherwise it behaves like CSS (where
42
+ * we snap to the initial value). Only applies if active when loaded.
43
+ */
44
+ animateOnLoad: { type: Boolean, default: false },
45
+
46
+ /**
47
+ * An optional delay (in seconts) to wait before animating the element on load when
48
+ * animateOnLoad is true.
49
+ */
50
+ animateOnLoadDelay: { type: Number, default: 0 },
51
+ },
52
+ // data() { return { b: 0 }; },
53
+ computed: {
54
+ styleVars() {
55
+ if (!this.enabled || !this.animation?.length) {
56
+ return null;
57
+ }
58
+
59
+ const ease = this.easing in cssEasings ? cssEasings[this.easing] : this.easing;
60
+
61
+ const vars = keywordsToAnimationCssVars(this.animation, 'px-animate', {
62
+ duration: this.duration !== null ? this.duration + 's' : null,
63
+ delay: this.delay !== null ? this.delay + 's' : null,
64
+ ease: ease !== null ? ease : null,
65
+ });
66
+
67
+ return vars;
68
+ },
69
+ },
70
+ watch: {},
71
+ mounted() {
72
+ if (this.active) {
73
+ if (this.animateOnLoad) {
74
+ setTimeout(() => {
75
+ this.tween?.play();
76
+ }, this.animateOnLoadDelay * 1000);
77
+ } else {
78
+ this.tween?.progress(1);
79
+ }
80
+ }
81
+ },
82
+ methods: {},
83
+ };
84
+ </script>
85
+
86
+ <style lang="scss">
87
+ @use '../styles/px.scss' as *;
88
+
89
+ .px-animate {
90
+ transform-origin: center center;
91
+ perspective: 1000px;
92
+
93
+ @include transition(
94
+ opacity visibility transform,
95
+ $dur: var(--px-animate-duration, 0.75s),
96
+ $ease: var(--px-animate-ease, ease-out)
97
+ );
98
+
99
+ &--active {
100
+ opacity: var(--px-animate-opacity, 1);
101
+ transform: var(--px-animate-transform, none);
102
+ visibility: var(--px-animate-transform, initial);
103
+ }
104
+ }
105
+ </style>
@@ -53,6 +53,12 @@ export default {
53
53
  // precaution, we also show if balancing fails for some reason)
54
54
  hideUnbalanced: { type: Boolean, default: true },
55
55
 
56
+ /**
57
+ * Adjusts how much "give" there is in the line length. You can adjust this to make the
58
+ * balancing more forigiving or strict.
59
+ */
60
+ lineAdjust: { type: Number, default: 1.15 },
61
+
56
62
  // enable to get debug color (blue: balanced html, green: balance slot, red: disabled)
57
63
  debug: { type: Boolean, default: false },
58
64
  },
@@ -140,7 +146,7 @@ export default {
140
146
  this.$emit('start');
141
147
  this.balanceFailed = false;
142
148
  if (this.$refs.text && this.$refs.text.offsetWidth > 0) {
143
- balanceText(this.$refs.text);
149
+ balanceText(this.$refs.text, this.lineAdjust);
144
150
  }
145
151
  this.balanced = true;
146
152
  this.$emit('complete');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "3.0.97",
3
+ "version": "3.0.99",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": "Pixel Lab",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "@floating-ui/dom": "^1.0.1",
16
16
  "@popperjs/core": "^2.11.5",
17
- "@thinkpixellab-public/px-styles": "^3.7.9",
17
+ "@thinkpixellab-public/px-styles": "^3.7.10",
18
18
  "@thinkpixellab-public/px-vue-tester": "^2.0.0",
19
19
  "@thinkpixellab-public/vue-resize-directive": "^1.2.2",
20
20
  "body-scroll-lock": "^3.1.5",
@@ -0,0 +1,265 @@
1
+ import { cssHasUnit, cssUnitFallback, cssUnitScale, cssNum, cssUnit, cssRound } from './cssStr';
2
+
3
+ function prepareLinearValue(value, modifier = 1) {
4
+ if (!cssHasUnit(value)) {
5
+ return cssUnitFallback(value * 100 * modifier, '%');
6
+ }
7
+ return cssRound(cssUnitScale(value, modifier));
8
+ }
9
+
10
+ /**
11
+ * Converts a string that represents an animation into a set of parameters that can be passed to a
12
+ * library like gsap. The string can be comma or space delimited and made up of the following
13
+ * keywords:
14
+ *
15
+ * Translation: up, down, left, right, x, y
16
+ *
17
+ * Scale: zoomin, zoomout, grow, shrink, zoom, scale (expects a custom value like scale:1.5)
18
+ *
19
+ * Opacity: fadein, fadeout, fade (same as fadeout but also takes a custom value like fade:0.5)
20
+ *
21
+ * Visibility / Opacity: show, hide
22
+ *
23
+ * 3D Rotation: up3d, down3d, left3d, right3d
24
+ *
25
+ * Value modifiers: xl (2x), lg (1.5x), md (1.25x), sm (0.5x), xs (0.25x)
26
+ *
27
+ * The default values for most keywords is 0.33 or 33%. Custom values can also be supplied (e.g.
28
+ * up:100px). Modifiers will be applied to the custom values as well as the default value.
29
+ *
30
+ * Scale values are relative to 1.0 (e.g. zoomin is 1.33, zoomout is 0.67, etc.), Except for 'scale'
31
+ * which generally expects to be used with a custom value as an absolute scaler (e.g. scale:1.5).
32
+ *
33
+ * Keywords can be combined in any order and will be applied in the order they are found. The
34
+ * keywords parameter can be a string or an object like {grow: true, up: '100px'}.
35
+ */
36
+
37
+ export function keywordsToAnimationParams(keywords) {
38
+ if (!keywords) {
39
+ return {};
40
+ }
41
+
42
+ // construct a string from the object version of the keywords parameter
43
+ if (typeof keywords == 'object') {
44
+ let str = '';
45
+ for (const key in keywords) {
46
+ if (keywords[key] === true) {
47
+ str += key + ' ';
48
+ } else {
49
+ str += key + ':' + keywords[key] + ' ';
50
+ }
51
+ }
52
+
53
+ keywords = str;
54
+ }
55
+
56
+ const words = (keywords || '').split(/[, ]+/).map(s => s.trim());
57
+ const to = {};
58
+
59
+ if (!words || !words.length) {
60
+ return to;
61
+ }
62
+
63
+ let modifier = 1;
64
+
65
+ modifier = words.includes('xl') ? 2 : modifier;
66
+ modifier = words.includes('lg') ? 1.5 : modifier;
67
+ modifier = words.includes('md') ? 1.25 : modifier;
68
+ modifier = words.includes('sm') ? 0.5 : modifier;
69
+ modifier = words.includes('xs') ? 0.25 : modifier;
70
+
71
+ words.forEach(word => {
72
+ let value = 0.33;
73
+ let customValue = false;
74
+
75
+ word = word.toLowerCase().trim();
76
+
77
+ if (word.indexOf(':') > -1) {
78
+ let [w, v] = word.split(':');
79
+ word = w;
80
+ value = v;
81
+ customValue = true;
82
+ }
83
+
84
+ // TRANSLATE
85
+
86
+ if (['up', 'down', 'left', 'right', 'x', 'y'].includes(word)) {
87
+ const invert = ['up', 'left'].includes(word);
88
+
89
+ switch (word) {
90
+ case 'up':
91
+ case 'down':
92
+ case 'y':
93
+ to['y'] = prepareLinearValue(value, modifier * (invert ? -1 : 1));
94
+ break;
95
+
96
+ case 'left':
97
+ case 'right':
98
+ case 'x':
99
+ to['x'] = prepareLinearValue(value, modifier * (invert ? -1 : 1));
100
+ break;
101
+ }
102
+ }
103
+
104
+ // SCALE
105
+
106
+ if (['zoomin', 'zoomout', 'grow', 'shrink', 'zoom', 'scale'].includes(word)) {
107
+ const invert = ['zoomout', 'shrink'].includes(word);
108
+
109
+ // we divide percentages by 100
110
+ if (cssUnit(value) == '%') {
111
+ value = cssNum(value) / 100;
112
+ }
113
+
114
+ // we don't use units with scale values
115
+ value = cssNum(value, 1);
116
+
117
+ // scale keyword semantics are such that everything but 'scale' is a value relative to
118
+ // the default scale of 1. In other words, "grow:0.5" is equivalent to "scale:1.5" and
119
+ // "shrink:0.5" is equivalent to "scale:0.5". Modifiers are applied to the delta from 1,
120
+ // so we need to adjust values for the 'scale' keyword accordingly.
121
+
122
+ if (word == 'scale') {
123
+ value -= 1;
124
+ }
125
+
126
+ value *= modifier * (invert ? -1 : 1);
127
+
128
+ // add 1 since all scalers are relative at this point
129
+ value += 1;
130
+
131
+ // can't scale smaller than 0
132
+ value = Math.max(0, value);
133
+
134
+ to['scale'] = cssRound(value);
135
+ }
136
+
137
+ // 3D
138
+
139
+ if (['up3d', 'down3d', 'left3d', 'right3d'].includes(word)) {
140
+ const invert = ['down3d', 'left3d'].includes(word);
141
+
142
+ // convert percent to a decimal
143
+ if (cssUnit(value) == '%') {
144
+ value = cssNum(value) / 100;
145
+ }
146
+
147
+ value = cssNum(value, 1);
148
+
149
+ // convert numbers that are less than 1 to deg as a percent of 90
150
+ if (value < 1) {
151
+ value = value * 90;
152
+ }
153
+
154
+ value *= modifier;
155
+ value *= invert ? -1 : 1;
156
+
157
+ if (word == 'up3d' || word == 'down3d') {
158
+ to['rotateX'] = cssRound(value) + 'deg';
159
+ }
160
+
161
+ if (word == 'left3d' || word == 'right3d') {
162
+ to['rotateY'] = cssRound(value) + 'deg';
163
+ }
164
+ }
165
+
166
+ // OPACITY / VISIBILITY
167
+
168
+ if (['show', 'hide', 'fadein', 'fadeout', 'fade'].includes(word)) {
169
+ if (['show', 'fadein'].includes(word)) {
170
+ to['opacity'] = 1;
171
+ }
172
+
173
+ if (['hide', 'fadeout', 'fade'].includes(word)) {
174
+ to['opacity'] = 0;
175
+ }
176
+
177
+ if (word == 'fade' && customValue) {
178
+ to['opacity'] = parseFloat(value);
179
+ }
180
+
181
+ if (word == 'show') {
182
+ to['visibility'] = 'visible';
183
+ }
184
+
185
+ if (word == 'hide') {
186
+ to['visibility'] = 'hidden';
187
+ }
188
+ }
189
+ });
190
+
191
+ return to;
192
+ }
193
+
194
+ export function keywordsToAnimationCssVars(keywords, prefix = '', otherVars = {}) {
195
+ prefix = '--' + (prefix ? prefix + '-' : '');
196
+ const params = keywordsToAnimationParams(keywords);
197
+ const vars = {};
198
+
199
+ if (params.scale || params.x || params.y || params.rotateX || params.rotateY) {
200
+ vars[prefix + 'transform'] = `translate(${params.x || 0}, ${params.y || 0}) scale(${
201
+ params.scale || 1
202
+ })`;
203
+
204
+ if (params.rotateX || params.rotateY) {
205
+ vars[prefix + 'transform'] += `rotateX(${params.rotateX || 0}) rotateY(${
206
+ params.rotateY || 0
207
+ })`;
208
+ }
209
+
210
+ delete params.x;
211
+ delete params.y;
212
+ delete params.scale;
213
+ delete params.rotateX;
214
+ delete params.rotateY;
215
+ }
216
+
217
+ for (const key in params) {
218
+ vars[prefix + key] = params[key];
219
+ }
220
+
221
+ for (const key in otherVars) {
222
+ vars[prefix + key] = otherVars[key];
223
+ }
224
+
225
+ return vars;
226
+ }
227
+
228
+ export const cssEasings = {
229
+ // Standard eases
230
+ ease: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
231
+ easeIn: 'cubic-bezier(0.42, 0, 1, 1)',
232
+ easeOut: 'cubic-bezier(0, 0, 0.58, 1)',
233
+ easeInOut: 'cubic-bezier(0.42, 0, 0.58, 1)',
234
+
235
+ // Ease variations
236
+ easeInSine: 'cubic-bezier(0.47, 0, 0.745, 0.715)',
237
+ easeOutSine: 'cubic-bezier(0.39, 0.575, 0.565, 1)',
238
+ easeInOutSine: 'cubic-bezier(0.445, 0.05, 0.55, 0.95)',
239
+ easeInQuad: 'cubic-bezier(0.55, 0.085, 0.68, 0.53)',
240
+ easeOutQuad: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
241
+ easeInOutQuad: 'cubic-bezier(0.455, 0.03, 0.515, 0.955)',
242
+ easeInCubic: 'cubic-bezier(0.55, 0.055, 0.675, 0.19)',
243
+ easeOutCubic: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
244
+ easeInOutCubic: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
245
+ easeInQuart: 'cubic-bezier(0.895, 0.03, 0.685, 0.22)',
246
+ easeOutQuart: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
247
+ easeInOutQuart: 'cubic-bezier(0.77, 0, 0.175, 1)',
248
+ easeInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)',
249
+ easeOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
250
+ easeInOutQuint: 'cubic-bezier(0.86, 0, 0.07, 1)',
251
+
252
+ // Custom eases
253
+ easeInExpo: 'cubic-bezier(0.95, 0.05, 0.795, 0.035)',
254
+ easeOutExpo: 'cubic-bezier(0.19, 1, 0.22, 1)',
255
+ easeInOutExpo: 'cubic-bezier(1, 0, 0, 1)',
256
+ easeInCirc: 'cubic-bezier(0.6, 0.04, 0.98, 0.335)',
257
+ easeOutCirc: 'cubic-bezier(0.075, 0.82, 0.165, 1)',
258
+ easeInOutCirc: 'cubic-bezier(0.785, 0.135, 0.15, 0.86)',
259
+ easeInBack: 'cubic-bezier(0.6, -0.28, 0.735, 0.045)',
260
+ easeOutBack: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
261
+ easeInOutBack: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
262
+
263
+ // Linear
264
+ linear: 'linear',
265
+ };
@@ -20,24 +20,24 @@
20
20
 
21
21
  */
22
22
 
23
+ // Adjust the final maxwidth by this amount to give a more natural
24
+ // look to the balanced text.
25
+ const ADJUST = 1.15;
26
+
23
27
  // HELPER FUNCTION -- initializes recursive binary search
24
- var balanceText = function (element) {
28
+ var balanceText = function (element, lineAdjust = ADJUST) {
25
29
  if (textElementIsMultipleLines(element)) {
26
30
  element.style.maxWidth = '';
27
- squeezeContainer(element, element.clientHeight, 0, element.clientWidth);
31
+ squeezeContainer(element, element.clientHeight, 0, element.clientWidth, lineAdjust);
28
32
  }
29
33
  };
30
34
 
31
- // Adjust the final maxwidth by this amount to give a more natural
32
- // look to the balanced text.
33
- const ADJUST = 1.15;
34
-
35
35
  // Make the headline element as narrow as possible while maintaining
36
36
  // its current height (number of lines). Binary search.
37
- function squeezeContainer(headline, originalHeight, bottomRange, topRange) {
37
+ function squeezeContainer(headline, originalHeight, bottomRange, topRange, lineAdjust = ADJUST) {
38
38
  var mid;
39
39
  if (bottomRange >= topRange) {
40
- headline.style.maxWidth = topRange * ADJUST + 'px';
40
+ headline.style.maxWidth = topRange * lineAdjust + 'px';
41
41
  return;
42
42
  }
43
43
  mid = (bottomRange + topRange) / 2;
@@ -45,10 +45,10 @@ function squeezeContainer(headline, originalHeight, bottomRange, topRange) {
45
45
 
46
46
  if (headline.clientHeight > originalHeight) {
47
47
  // we've squoze too far and headline has spilled onto an additional line; recurse on wider range
48
- squeezeContainer(headline, originalHeight, mid + 1, topRange);
48
+ squeezeContainer(headline, originalHeight, mid + 1, topRange, lineAdjust);
49
49
  } else {
50
50
  // headline has not wrapped to another line; keep squeezing!
51
- squeezeContainer(headline, originalHeight, bottomRange + 1, mid);
51
+ squeezeContainer(headline, originalHeight, bottomRange + 1, mid, lineAdjust);
52
52
  }
53
53
  }
54
54
 
package/utils/cssStr.js CHANGED
@@ -3,7 +3,7 @@ Converts a number to a px string
3
3
  px(45) => '45px'
4
4
  */
5
5
  function cssPx(val, round = true, fallback = null) {
6
- if (!isNaN(val) && val !== null) {
6
+ if ((val || val === 0) && !isNaN(val)) {
7
7
  return (round ? Math.round(val) : val) + 'px';
8
8
  }
9
9
  return fallback;
@@ -13,9 +13,9 @@ function cssPx(val, round = true, fallback = null) {
13
13
  Converts a number to a em string
14
14
  px(45) => '45em'
15
15
  */
16
- function cssEm(val, fallback) {
17
- if (val) {
18
- return val + 'em';
16
+ function cssEm(val, fallback = null) {
17
+ if ((val || val === 0) && !isNaN(val)) {
18
+ return cssNum(val) + 'em';
19
19
  }
20
20
  return fallback;
21
21
  }
@@ -24,16 +24,21 @@ function cssEm(val, fallback) {
24
24
  Converts a number to a percentage string
25
25
  percent(0.45) => '45%'
26
26
  */
27
- function cssPcnt(val, roundDec = 0) {
28
- if (!isNaN(val)) {
27
+ function cssPcnt(val, roundDec = 2, fallback = null) {
28
+ if ((val || val === 0) && !isNaN(val)) {
29
29
  val *= 100;
30
30
 
31
31
  if (roundDec) {
32
32
  val = Math.round(val * Math.pow(10, roundDec)) / Math.pow(10, roundDec);
33
33
  }
34
+
35
+ if (roundDec == 0) {
36
+ val = Math.round(val);
37
+ }
38
+
34
39
  return val + '%';
35
40
  }
36
- return null;
41
+ return fallback;
37
42
  }
38
43
 
39
44
  /*
@@ -93,6 +98,13 @@ function cssUnit(val) {
93
98
  return '';
94
99
  }
95
100
 
101
+ /**
102
+ * Returns true if the value has a unit
103
+ */
104
+ function cssHasUnit(val) {
105
+ return cssUnit(val) !== '';
106
+ }
107
+
96
108
  /**
97
109
  * Scales a css value while leaving the unit intact.
98
110
  * @param {string} val The css value to scale
@@ -174,9 +186,9 @@ Add the specified unit if no other unit is specified.
174
186
  cssUnitFallback('10', 'em') => '10em'
175
187
  cssUnitFallback(10, 'em') => '10em'
176
188
  */
177
- function cssUnitFallback(val, fallback, allowNumberless = false) {
178
- if (!val) {
179
- return val;
189
+ function cssUnitFallback(val, fallback, ignoreCssFunc = false) {
190
+ if (!val && val !== 0) {
191
+ return null;
180
192
  }
181
193
 
182
194
  let num = typeof val == 'number' ? val : parseFloat(val.toString());
@@ -184,14 +196,14 @@ function cssUnitFallback(val, fallback, allowNumberless = false) {
184
196
  val = val.toString().trim();
185
197
 
186
198
  if (
187
- allowNumberless &&
199
+ ignoreCssFunc &&
188
200
  (val.startsWith('min') || val.startsWith('max') || val.startsWith('calc'))
189
201
  ) {
190
202
  return val;
191
203
  }
192
204
 
193
205
  if (isNaN(num)) {
194
- if (allowNumberless) {
206
+ if (ignoreCssFunc) {
195
207
  num = '';
196
208
  } else {
197
209
  return null;
@@ -210,19 +222,19 @@ function cssUnitFallback(val, fallback, allowNumberless = false) {
210
222
  /**
211
223
  * Ems version of cssUnitFallback
212
224
  */
213
- function cssEmFallback(val, allowNumberless = false) {
214
- return cssUnitFallback(val, 'em', allowNumberless);
225
+ function cssEmFallback(val, ignoreCssFunc = false) {
226
+ return cssUnitFallback(val, 'em', ignoreCssFunc);
215
227
  }
216
228
 
217
229
  /**
218
230
  * Px version of cssUnitFallback
219
231
  */
220
- function cssPxFallback(val, allowNumberless = false) {
221
- return cssUnitFallback(val, 'px', allowNumberless);
232
+ function cssPxFallback(val, ignoreCssFunc = false) {
233
+ return cssUnitFallback(val, 'px', ignoreCssFunc);
222
234
  }
223
235
 
224
236
  /**
225
- * Returns true if the value is a valid calc() function
237
+ * Returns true if the value appears to be a calc() function
226
238
  */
227
239
  function cssIsCalc(val) {
228
240
  if (val && typeof val == 'string' && val.indexOf('calc') > -1) {
@@ -279,12 +291,25 @@ function cssCalcAdd(val1 = 0, val2 = 0, op = '+') {
279
291
  }
280
292
 
281
293
  /**
282
- * Samem as cssCalcAdd with the op set to '-'. Makes reading code a little easier.
294
+ * Same as cssCalcAdd with the op set to '-'. Makes reading code a little easier.
283
295
  */
284
296
  function cssCalcSubtract(val1 = 0, val2 = 0) {
285
297
  return cssCalcAdd(val1, val2, '-');
286
298
  }
287
299
 
300
+ /**
301
+ * Rounds a css value to the specified number of decimal places (while preserving units).
302
+ */
303
+ function cssRound(val, dec = 3, preserveNumber = true) {
304
+ const isNum = typeof val == 'number';
305
+ const num = cssNum(val);
306
+ const unit = cssUnit(val);
307
+ const pow = Math.pow(10, dec);
308
+ const rounded = Math.round(num * pow) / pow;
309
+
310
+ return isNum && preserveNumber ? rounded : rounded + unit;
311
+ }
312
+
288
313
  export {
289
314
  cssPx,
290
315
  cssEm,
@@ -294,6 +319,7 @@ export {
294
319
  cssAspect,
295
320
  cssNum,
296
321
  cssUnit,
322
+ cssHasUnit,
297
323
  cssUnitScale,
298
324
  cssUnitFallback,
299
325
  cssEmFallback,
@@ -301,4 +327,5 @@ export {
301
327
  cssIsCalc,
302
328
  cssCalcAdd,
303
329
  cssCalcSubtract,
330
+ cssRound,
304
331
  };
@@ -0,0 +1,293 @@
1
+ /* =============================================================================
2
+
3
+ A visual explanation of the difference between 'cover' progress mode and
4
+ 'contain' progress mode. Basic idea:
5
+
6
+ Use 'cover' if you want progress to start when the element first enters
7
+ the viewport from the bottom and end when it fully exits from the top.
8
+
9
+ Use 'contain' if you want progress to start when the element is fully in
10
+ the viewport (as much as possible) and end once it starts to leave. contain
11
+ has slighty different behavior if the element is taller than the viewport.
12
+
13
+ ------------------------------------- -------------------------------------
14
+ progressMode = 'cover' progressMode = 'contain'
15
+ ------------------------------------- -------------------------------------
16
+
17
+ 0.0 0.5 1.0 0.0 0.5 1.0
18
+
19
+ #####
20
+ +---------+ +---------+ +--#####--+ +---------+ +---------+ +--#####--+
21
+ | | | ##### | | | | | | ##### | | ##### |
22
+ | | | ##### | | | | ##### | | ##### | | |
23
+ +--#####--+ +---------+ +---------+ +--#####--+ +---------+ +---------+
24
+ #####
25
+ #####
26
+ ##### #####
27
+ ##### ##### #####
28
+ +---------+ +--#####--+ +--#####--+ +--#####--+ +--#####--+ +--#####--+
29
+ | | | ##### | | | | ##### | | ##### | | ##### |
30
+ | | | ##### | | | | ##### | | ##### | | ##### |
31
+ +--#####--+ +--#####--+ +---------+ +--#####--+ +--#####--+ +--#####--+
32
+ ##### ##### #####
33
+ ##### #####
34
+ #####
35
+
36
+ ============================================================================= */
37
+
38
+ const watcherElementDefaults = {
39
+ // the area of the viewport that gets watched, default is the full screen but a single
40
+ // line in the middle of the screen could be specified as {viewportTop: 0.5, viewportHeight: 0}
41
+ viewportStart: 0,
42
+ viewportHeight: 1,
43
+
44
+ // see description above, options are 'cover' and 'contain'
45
+ progressMode: 'cover',
46
+
47
+ // called when page the scrolls, regardless of where the element is relative to the viewport
48
+ onScroll: null,
49
+
50
+ // called on page scroll if progress is 0 to 1
51
+ onProgress: null,
52
+
53
+ // called when the element enters the viewport
54
+ onEnter: null,
55
+ onEnterFromAbove: null,
56
+ onEnterFromBelow: null,
57
+
58
+ // called when the element exits the viewport
59
+ onExit: null,
60
+ onExitToAbove: null,
61
+ onExitToBelow: null,
62
+
63
+ // called when the element enters or exits the viewport
64
+ onEnterOrExit: null,
65
+ };
66
+
67
+ class ScrollWatcherElement {
68
+ constructor(element, options) {
69
+ Object.assign(this, watcherElementDefaults, options);
70
+
71
+ this.element = element;
72
+ this.isFirstUpdate = true;
73
+ this.frameCount = 0;
74
+
75
+ this.calc = {
76
+ element: element,
77
+ viewport: {},
78
+ };
79
+
80
+ this.updatePosition();
81
+ this.updateProgress(window.scrollY);
82
+ }
83
+
84
+ // called by ScrollWatcher on resize
85
+ updatePosition() {
86
+ if (!this.element) {
87
+ return;
88
+ }
89
+
90
+ // update the viewport
91
+ this.calc.windowHeight = window.innerHeight;
92
+ this.calc.viewport.top = this.calc.windowHeight * this.viewportStart;
93
+ this.calc.viewport.height = this.calc.windowHeight * this.viewportHeight;
94
+ this.calc.viewport.bottom = this.calc.viewport.top + this.calc.viewport.height;
95
+
96
+ // get the dimensions and location of the watched element
97
+ var r = this.element.getBoundingClientRect();
98
+ this.calc.elementRect = {
99
+ top: r.top + window.scrollY,
100
+ bottom: r.top + r.height + window.scrollY,
101
+ height: r.height,
102
+ };
103
+
104
+ if (this.progressMode === 'contain') {
105
+ // CONTAIN PROGRESS
106
+ // Occurs as the element overlaps the viewport (details in comments below)
107
+
108
+ if (this.calc.elementRect.height <= this.calc.viewport.height) {
109
+ // If element is smaller than viewport...
110
+ // overlapping begins when the bottom of the element meets the bottom of the viewport
111
+ this.calc.progressBegin = Math.round(
112
+ this.calc.elementRect.bottom - this.calc.viewport.bottom
113
+ );
114
+ this.calc.progressDuration = Math.round(
115
+ this.calc.viewport.height - this.calc.elementRect.height
116
+ );
117
+ } else {
118
+ // If element is larger than viewport...
119
+ // begin when the top of the element is at the top of the viewport
120
+ this.calc.progressBegin = Math.round(
121
+ this.calc.elementRect.top - this.calc.viewport.top
122
+ );
123
+ this.calc.progressDuration = Math.round(
124
+ this.calc.elementRect.height - this.calc.viewport.height
125
+ );
126
+ }
127
+ } else {
128
+ // cover PROGRESS
129
+ // Occurs as any part of the element is within any part of the viewport
130
+
131
+ this.calc.progressBegin = Math.round(
132
+ this.calc.elementRect.top - this.calc.viewport.bottom
133
+ );
134
+ this.calc.progressDuration = Math.round(
135
+ this.calc.viewport.height + this.calc.elementRect.height
136
+ );
137
+ }
138
+
139
+ this.calc.progressEnd = this.calc.progressBegin + this.calc.progressDuration;
140
+ }
141
+
142
+ // called by ScrollWatcher on window scroll events
143
+ updateProgress(scrollTop) {
144
+ this.calc.wasInViewport = this.calc.isInViewport;
145
+ this.calc.wasAboveViewport = this.calc.isAboveViewport;
146
+ this.calc.wasBelowViewport = this.calc.isBelowViewport;
147
+
148
+ // delta
149
+ this.calc.lastScrollTop = this.calc.currentScrollTop
150
+ ? this.calc.currentScrollTop
151
+ : scrollTop;
152
+ this.calc.currentScrollTop = scrollTop;
153
+ this.calc.scrollDelta = scrollTop - this.calc.lastScrollTop;
154
+
155
+ // progress
156
+ this.calc.actualProgress =
157
+ (scrollTop - this.calc.progressBegin) / this.calc.progressDuration;
158
+ this.calc.progress = Math.max(0, Math.min(1, this.calc.actualProgress));
159
+
160
+ // viewport
161
+ this.calc.isAboveViewport = this.calc.actualProgress > 1;
162
+ this.calc.isBelowViewport = this.calc.actualProgress < 0;
163
+ this.calc.isInViewport = this.calc.actualProgress > 0 && this.calc.actualProgress <= 1;
164
+
165
+ // scroll callback
166
+ if (this.onScroll) {
167
+ this.onScroll(this.calc);
168
+ }
169
+
170
+ // progress callback
171
+ if ((this.calc.isInViewport || this.calc.wasInViewport) && this.onProgress) {
172
+ this.onProgress(this.calc);
173
+ }
174
+
175
+ // enter callbacks
176
+ if (
177
+ (!this.calc.wasInViewport && this.calc.isInViewport) ||
178
+ (this.calc.wasAboveViewport && !this.calc.isAboveViewport) ||
179
+ (this.calc.wasBelowViewport && !this.calc.isBelowViewport)
180
+ ) {
181
+ if (this.onEnter) {
182
+ this.onEnter(this.calc);
183
+ }
184
+
185
+ if (this.onEnterOrExit) {
186
+ this.onEnterOrExit(this.calc);
187
+ }
188
+
189
+ if (this.calc.wasBelowViewport && !this.calc.isBelowViewport && this.onEnterFromBelow) {
190
+ this.onEnterFromBelow(this.calc);
191
+ }
192
+
193
+ if (this.calc.wasAboveViewport && !this.calc.isAboveViewport && this.onEnterFromAbove) {
194
+ this.onEnterFromAbove(this.calc);
195
+ }
196
+ }
197
+
198
+ // exit callbacks
199
+ if (
200
+ (this.calc.wasInViewport && !this.calc.isInViewport) ||
201
+ (this.isFirstUpdate && this.calc.isAboveViewport)
202
+ ) {
203
+ if (this.onExit) {
204
+ this.onExit(this.calc);
205
+ }
206
+
207
+ if (this.onEnterOrExit) {
208
+ this.onEnterOrExit(this.calc);
209
+ }
210
+
211
+ if (this.calc.isAboveViewport && this.onExitToAbove) {
212
+ this.onExitToAbove(this.calc);
213
+ }
214
+
215
+ if (this.calc.isBelowViewport && this.onExitToBelow) {
216
+ this.onExitToBelow(this.calc);
217
+ }
218
+ }
219
+
220
+ this.isFirstUpdate = false;
221
+ }
222
+ }
223
+
224
+ const scrollWatcherDefaults = {};
225
+
226
+ class ScrollWatcher {
227
+ static init(options) {
228
+ ScrollWatcher.elements = [];
229
+ ScrollWatcher.frames = 0;
230
+ ScrollWatcher.framesRAF = 0;
231
+
232
+ ScrollWatcher.options = Object.assign({}, scrollWatcherDefaults, options);
233
+
234
+ this.addEvents();
235
+
236
+ ScrollWatcher._isInitialized = true;
237
+ }
238
+
239
+ static addEvents() {
240
+ ScrollWatcher._isActive = true;
241
+ window.addEventListener('resize', ScrollWatcher._onResize);
242
+ window.addEventListener('scroll', ScrollWatcher._onScroll);
243
+ }
244
+
245
+ static removeEvents() {
246
+ ScrollWatcher._isActive = false;
247
+ window.removeEventListener('resize', ScrollWatcher._onResize);
248
+ window.removeEventListener('scroll', ScrollWatcher._onScroll);
249
+ }
250
+
251
+ static watch(element, options) {
252
+ if (!ScrollWatcher._isInitialized) {
253
+ ScrollWatcher.init();
254
+ }
255
+
256
+ if (!ScrollWatcher._isActive) {
257
+ ScrollWatcher.addEvents();
258
+ }
259
+
260
+ var watcherElement = new ScrollWatcherElement(element, options);
261
+ ScrollWatcher.elements.push(watcherElement);
262
+
263
+ return watcherElement;
264
+ }
265
+
266
+ static removeWatch(watcherElement) {
267
+ var idx = this.elements.indexOf(watcherElement);
268
+ this.elements = this.elements
269
+ .slice(0, idx)
270
+ .concat(this.elements.slice(idx + 1, this.elements.length));
271
+
272
+ if (this.elements.length === 0) {
273
+ this.removeEvents();
274
+ }
275
+ }
276
+
277
+ static _onResize() {
278
+ ScrollWatcher.elements.forEach(element => {
279
+ element.updatePosition();
280
+ });
281
+ }
282
+
283
+ // handle normal scroll events
284
+ static _onScroll() {
285
+ var scrollTop = window.scrollY;
286
+ var i = 0;
287
+ for (; i < ScrollWatcher.elements.length; i++) {
288
+ ScrollWatcher.elements[i].updateProgress(scrollTop);
289
+ }
290
+ }
291
+ }
292
+
293
+ export default ScrollWatcher;