@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.
- package/components/PxAnimate.vue +105 -0
- package/components/PxBalancedText.vue +7 -1
- package/package.json +2 -2
- package/utils/animation.js +265 -0
- package/utils/balanceText.js +10 -10
- package/utils/cssStr.js +45 -18
- package/utils/scrollWatcher.js +293 -0
|
@@ -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.
|
|
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.
|
|
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
|
+
};
|
package/utils/balanceText.js
CHANGED
|
@@ -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 *
|
|
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 (
|
|
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 =
|
|
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
|
|
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,
|
|
178
|
-
if (!val) {
|
|
179
|
-
return
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
214
|
-
return cssUnitFallback(val, 'em',
|
|
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,
|
|
221
|
-
return cssUnitFallback(val, 'px',
|
|
232
|
+
function cssPxFallback(val, ignoreCssFunc = false) {
|
|
233
|
+
return cssUnitFallback(val, 'px', ignoreCssFunc);
|
|
222
234
|
}
|
|
223
235
|
|
|
224
236
|
/**
|
|
225
|
-
* Returns true if the value
|
|
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
|
-
*
|
|
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;
|