@leavepulse/ui 0.48.0 → 0.49.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/dist/chunks/{LpVideoBackdrop-DN6xPUav.js → LpVideoBackdrop-qWRYBOTQ.js} +71 -23
- package/dist/components/LpVideoBackdrop.vue.d.ts +5 -0
- package/dist/components/LpVideoBackdrop.vue.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/LpVideoBackdrop.vue +103 -22
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent, ref, computed, watch, onMounted, onBeforeUnmount, openBlock, createElementBlock, mergeProps, unref, normalizeStyle, createCommentVNode, createElementVNode, normalizeClass } from "vue";
|
|
2
|
-
import { prefersReducedMotion } from "../composables/easing.js";
|
|
2
|
+
import { prefersReducedMotion, easeTravel } from "../composables/easing.js";
|
|
3
3
|
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
4
4
|
import { planLoop, isNativeLoop, seamReached, seamProgress } from "../components/videoBackdrop.js";
|
|
5
5
|
const _hoisted_1 = ["src"];
|
|
@@ -19,7 +19,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
19
19
|
dim: { default: 0 },
|
|
20
20
|
fit: { default: "cover" },
|
|
21
21
|
paused: { type: Boolean, default: false },
|
|
22
|
-
playWithReducedMotion: { type: Boolean, default: false }
|
|
22
|
+
playWithReducedMotion: { type: Boolean, default: false },
|
|
23
|
+
easePlayback: { default: 600 }
|
|
23
24
|
},
|
|
24
25
|
emits: ["loaded", "error"],
|
|
25
26
|
setup(__props, { emit: __emit }) {
|
|
@@ -32,6 +33,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
32
33
|
const active = ref(0);
|
|
33
34
|
const incoming = ref(null);
|
|
34
35
|
const incomingOpacity = ref(0);
|
|
36
|
+
const running = ref(false);
|
|
37
|
+
const throttle = ref(1);
|
|
38
|
+
const SLOWEST_RATE = 1 / 16;
|
|
35
39
|
const reducedMotion = ref(prefersReducedMotion());
|
|
36
40
|
const pageHidden = ref(typeof document !== "undefined" && document.hidden);
|
|
37
41
|
const plan = computed(
|
|
@@ -47,7 +51,36 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
47
51
|
function setRate(video) {
|
|
48
52
|
if (!video) return;
|
|
49
53
|
video.defaultPlaybackRate = props.rate;
|
|
50
|
-
video.playbackRate = props.rate;
|
|
54
|
+
video.playbackRate = Math.max(SLOWEST_RATE, props.rate * throttle.value);
|
|
55
|
+
}
|
|
56
|
+
function applyRate() {
|
|
57
|
+
setRate(first.value);
|
|
58
|
+
setRate(second.value);
|
|
59
|
+
}
|
|
60
|
+
let ramp = 0;
|
|
61
|
+
function rampTo(target, ms, done) {
|
|
62
|
+
if (ramp) cancelAnimationFrame(ramp);
|
|
63
|
+
ramp = 0;
|
|
64
|
+
const from = throttle.value;
|
|
65
|
+
const span = ms * Math.abs(target - from);
|
|
66
|
+
if (span <= 0) {
|
|
67
|
+
throttle.value = target;
|
|
68
|
+
applyRate();
|
|
69
|
+
done?.();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const began = performance.now();
|
|
73
|
+
const step = (now) => {
|
|
74
|
+
const k = Math.min(1, (now - began) / span);
|
|
75
|
+
throttle.value = from + (target - from) * easeTravel(k);
|
|
76
|
+
applyRate();
|
|
77
|
+
if (k < 1) ramp = requestAnimationFrame(step);
|
|
78
|
+
else {
|
|
79
|
+
ramp = 0;
|
|
80
|
+
done?.();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
ramp = requestAnimationFrame(step);
|
|
51
84
|
}
|
|
52
85
|
function onMetadata(event) {
|
|
53
86
|
const video = event.target;
|
|
@@ -86,7 +119,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
86
119
|
let frame = 0;
|
|
87
120
|
function watchFrames() {
|
|
88
121
|
frame = 0;
|
|
89
|
-
if (!
|
|
122
|
+
if (!running.value) return;
|
|
90
123
|
const current = element(active.value);
|
|
91
124
|
const loop = plan.value;
|
|
92
125
|
if (current && !native.value) {
|
|
@@ -113,28 +146,43 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
113
146
|
}
|
|
114
147
|
frame = requestAnimationFrame(watchFrames);
|
|
115
148
|
}
|
|
116
|
-
function
|
|
149
|
+
function startElements() {
|
|
150
|
+
running.value = true;
|
|
151
|
+
applyRate();
|
|
152
|
+
void element(active.value)?.play().catch(() => {
|
|
153
|
+
});
|
|
154
|
+
if (incoming.value !== null) void element(incoming.value)?.play().catch(() => {
|
|
155
|
+
});
|
|
156
|
+
if (!frame) frame = requestAnimationFrame(watchFrames);
|
|
157
|
+
}
|
|
158
|
+
function stopElements() {
|
|
159
|
+
running.value = false;
|
|
160
|
+
first.value?.pause();
|
|
161
|
+
second.value?.pause();
|
|
162
|
+
if (frame) cancelAnimationFrame(frame);
|
|
163
|
+
frame = 0;
|
|
117
164
|
const current = element(active.value);
|
|
118
|
-
|
|
165
|
+
if (still.value && current && ready.value) current.currentTime = plan.value.from;
|
|
166
|
+
}
|
|
167
|
+
function sync() {
|
|
168
|
+
const easing = props.easePlayback > 0 && !prefersReducedMotion() ? props.easePlayback : 0;
|
|
119
169
|
if (playing.value) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
if (!frame) frame = requestAnimationFrame(watchFrames);
|
|
170
|
+
if (!running.value) throttle.value = easing ? 0 : 1;
|
|
171
|
+
startElements();
|
|
172
|
+
rampTo(1, easing);
|
|
125
173
|
return;
|
|
126
174
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
175
|
+
if (!running.value || pageHidden.value || still.value || !easing) {
|
|
176
|
+
rampTo(throttle.value, 0);
|
|
177
|
+
stopElements();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
rampTo(0, easing, () => {
|
|
181
|
+
if (!playing.value) stopElements();
|
|
182
|
+
});
|
|
132
183
|
}
|
|
133
184
|
watch(playing, sync, { flush: "post" });
|
|
134
|
-
watch(() => props.rate,
|
|
135
|
-
setRate(first.value);
|
|
136
|
-
setRate(second.value);
|
|
137
|
-
});
|
|
185
|
+
watch(() => props.rate, applyRate);
|
|
138
186
|
watch(
|
|
139
187
|
() => props.src,
|
|
140
188
|
() => {
|
|
@@ -170,14 +218,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
170
218
|
}
|
|
171
219
|
let motionQuery = null;
|
|
172
220
|
onMounted(() => {
|
|
173
|
-
|
|
174
|
-
setRate(second.value);
|
|
221
|
+
applyRate();
|
|
175
222
|
motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
176
223
|
motionQuery.addEventListener("change", onMotionChange);
|
|
177
224
|
document.addEventListener("visibilitychange", onVisibility);
|
|
178
225
|
});
|
|
179
226
|
onBeforeUnmount(() => {
|
|
180
227
|
if (frame) cancelAnimationFrame(frame);
|
|
228
|
+
if (ramp) cancelAnimationFrame(ramp);
|
|
181
229
|
motionQuery?.removeEventListener("change", onMotionChange);
|
|
182
230
|
document.removeEventListener("visibilitychange", onVisibility);
|
|
183
231
|
});
|
|
@@ -190,7 +238,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
190
238
|
};
|
|
191
239
|
}
|
|
192
240
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
193
|
-
"pointer-events-none absolute inset-0 overflow-hidden"
|
|
241
|
+
"pointer-events-none absolute inset-0 isolate overflow-hidden"
|
|
194
242
|
);
|
|
195
243
|
return (_ctx, _cache) => {
|
|
196
244
|
return openBlock(), createElementBlock("div", mergeProps({
|
|
@@ -18,6 +18,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
18
18
|
paused?: boolean;
|
|
19
19
|
/** Keep playing when the system asks for reduced motion (it shows a still otherwise). */
|
|
20
20
|
playWithReducedMotion?: boolean;
|
|
21
|
+
/** Milliseconds over which a pause slows the picture to a halt and a resume brings it back up; 0 stops dead. */
|
|
22
|
+
easePlayback?: number;
|
|
21
23
|
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
22
24
|
error: () => any;
|
|
23
25
|
loaded: (info: {
|
|
@@ -43,6 +45,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
43
45
|
paused?: boolean;
|
|
44
46
|
/** Keep playing when the system asks for reduced motion (it shows a still otherwise). */
|
|
45
47
|
playWithReducedMotion?: boolean;
|
|
48
|
+
/** Milliseconds over which a pause slows the picture to a halt and a resume brings it back up; 0 stops dead. */
|
|
49
|
+
easePlayback?: number;
|
|
46
50
|
}> & Readonly<{
|
|
47
51
|
onError?: (() => any) | undefined;
|
|
48
52
|
onLoaded?: ((info: {
|
|
@@ -58,4 +62,5 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
58
62
|
rate: number;
|
|
59
63
|
fit: "cover" | "contain" | "fill" | "none";
|
|
60
64
|
playWithReducedMotion: boolean;
|
|
65
|
+
easePlayback: number;
|
|
61
66
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
package/dist/index.js
CHANGED
|
@@ -87,7 +87,7 @@ import { _ as _61 } from "./chunks/LpSidebar-DpEiGi8L.js";
|
|
|
87
87
|
import { _ as _62 } from "./chunks/LpSkeleton-BB6XIV-w.js";
|
|
88
88
|
import { _ as _63 } from "./chunks/LpSlider-Cuzmrd7p.js";
|
|
89
89
|
import { _ as _64 } from "./chunks/LpRangeSlider-C1yF9-nP.js";
|
|
90
|
-
import { _ as _65 } from "./chunks/LpVideoBackdrop-
|
|
90
|
+
import { _ as _65 } from "./chunks/LpVideoBackdrop-qWRYBOTQ.js";
|
|
91
91
|
import { formatClipTime, isNativeLoop, planLoop, seamProgress, seamReached } from "./components/videoBackdrop.js";
|
|
92
92
|
import { _ as _66 } from "./chunks/LpSparkline-CB80N8SJ.js";
|
|
93
93
|
import { _ as _67 } from "./chunks/LpStat-B-7udBRP.js";
|
package/package.json
CHANGED
|
@@ -24,13 +24,16 @@ export default { inheritAttrs: false }
|
|
|
24
24
|
* - Pausing where motion is unwanted: `paused`, a hidden tab, and the
|
|
25
25
|
* system's reduced-motion setting, which shows the first frame of the
|
|
26
26
|
* stretch instead (`playWithReducedMotion` overrides it).
|
|
27
|
+
* - Easing in and out of a pause (`easePlayback`): the picture slows to a
|
|
28
|
+
* standstill and gathers speed again instead of freezing and lurching. A
|
|
29
|
+
* hidden tab and reduced motion still stop at once.
|
|
27
30
|
*
|
|
28
31
|
* The second element exists only while there is a seam to blend, so a plain
|
|
29
32
|
* loop decodes one stream. Both play the same `src`: a URL the media element
|
|
30
33
|
* fetches by range, or an object URL of a file already in memory.
|
|
31
34
|
*/
|
|
32
35
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
33
|
-
import { prefersReducedMotion } from "../composables/easing"
|
|
36
|
+
import { easeTravel, prefersReducedMotion } from "../composables/easing"
|
|
34
37
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
35
38
|
import { isNativeLoop, planLoop, seamProgress, seamReached } from "./videoBackdrop"
|
|
36
39
|
|
|
@@ -53,8 +56,19 @@ const props = withDefaults(
|
|
|
53
56
|
paused?: boolean
|
|
54
57
|
/** Keep playing when the system asks for reduced motion (it shows a still otherwise). */
|
|
55
58
|
playWithReducedMotion?: boolean
|
|
59
|
+
/** Milliseconds over which a pause slows the picture to a halt and a resume brings it back up; 0 stops dead. */
|
|
60
|
+
easePlayback?: number
|
|
56
61
|
}>(),
|
|
57
|
-
{
|
|
62
|
+
{
|
|
63
|
+
start: 0,
|
|
64
|
+
crossfade: 0,
|
|
65
|
+
rate: 1,
|
|
66
|
+
dim: 0,
|
|
67
|
+
fit: "cover",
|
|
68
|
+
paused: false,
|
|
69
|
+
playWithReducedMotion: false,
|
|
70
|
+
easePlayback: 600,
|
|
71
|
+
},
|
|
58
72
|
)
|
|
59
73
|
|
|
60
74
|
const emit = defineEmits<{
|
|
@@ -74,6 +88,18 @@ const active = ref<0 | 1>(0)
|
|
|
74
88
|
const incoming = ref<0 | 1 | null>(null)
|
|
75
89
|
const incomingOpacity = ref(0)
|
|
76
90
|
|
|
91
|
+
/** The elements are playing, which includes slowing down into a pause. */
|
|
92
|
+
const running = ref(false)
|
|
93
|
+
/** Share of `rate` played at, 0 to 1: below 1 while easing into or out of a pause. */
|
|
94
|
+
const throttle = ref(1)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The slowest rate a pause eases down to before the element is paused.
|
|
98
|
+
* Chromium refuses a rate below a sixteenth; past that point the picture is
|
|
99
|
+
* all but still anyway.
|
|
100
|
+
*/
|
|
101
|
+
const SLOWEST_RATE = 1 / 16
|
|
102
|
+
|
|
77
103
|
const reducedMotion = ref(prefersReducedMotion())
|
|
78
104
|
const pageHidden = ref(typeof document !== "undefined" && document.hidden)
|
|
79
105
|
|
|
@@ -93,7 +119,39 @@ function setRate(video: HTMLVideoElement | null) {
|
|
|
93
119
|
if (!video) return
|
|
94
120
|
// `defaultPlaybackRate` too: a reload of the source resets the rate to it.
|
|
95
121
|
video.defaultPlaybackRate = props.rate
|
|
96
|
-
video.playbackRate = props.rate
|
|
122
|
+
video.playbackRate = Math.max(SLOWEST_RATE, props.rate * throttle.value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyRate() {
|
|
126
|
+
setRate(first.value)
|
|
127
|
+
setRate(second.value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let ramp = 0
|
|
131
|
+
/** Move `throttle` to `target` over `ms`, eased, then call `done`. */
|
|
132
|
+
function rampTo(target: number, ms: number, done?: () => void) {
|
|
133
|
+
if (ramp) cancelAnimationFrame(ramp)
|
|
134
|
+
ramp = 0
|
|
135
|
+
const from = throttle.value
|
|
136
|
+
const span = ms * Math.abs(target - from)
|
|
137
|
+
if (span <= 0) {
|
|
138
|
+
throttle.value = target
|
|
139
|
+
applyRate()
|
|
140
|
+
done?.()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const began = performance.now()
|
|
144
|
+
const step = (now: number) => {
|
|
145
|
+
const k = Math.min(1, (now - began) / span)
|
|
146
|
+
throttle.value = from + (target - from) * easeTravel(k)
|
|
147
|
+
applyRate()
|
|
148
|
+
if (k < 1) ramp = requestAnimationFrame(step)
|
|
149
|
+
else {
|
|
150
|
+
ramp = 0
|
|
151
|
+
done?.()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
ramp = requestAnimationFrame(step)
|
|
97
155
|
}
|
|
98
156
|
|
|
99
157
|
function onMetadata(event: Event) {
|
|
@@ -140,7 +198,7 @@ function finishSeam() {
|
|
|
140
198
|
let frame = 0
|
|
141
199
|
function watchFrames() {
|
|
142
200
|
frame = 0
|
|
143
|
-
if (!
|
|
201
|
+
if (!running.value) return
|
|
144
202
|
const current = element(active.value)
|
|
145
203
|
const loop = plan.value
|
|
146
204
|
if (current && !native.value) {
|
|
@@ -167,28 +225,47 @@ function watchFrames() {
|
|
|
167
225
|
frame = requestAnimationFrame(watchFrames)
|
|
168
226
|
}
|
|
169
227
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
function startElements() {
|
|
229
|
+
running.value = true
|
|
230
|
+
applyRate()
|
|
231
|
+
void element(active.value)?.play().catch(() => {})
|
|
232
|
+
if (incoming.value !== null) void element(incoming.value)?.play().catch(() => {})
|
|
233
|
+
if (!frame) frame = requestAnimationFrame(watchFrames)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function stopElements() {
|
|
237
|
+
running.value = false
|
|
238
|
+
first.value?.pause()
|
|
239
|
+
second.value?.pause()
|
|
181
240
|
if (frame) cancelAnimationFrame(frame)
|
|
182
241
|
frame = 0
|
|
183
242
|
// A still shows the stretch's first frame, not wherever the clip stopped.
|
|
243
|
+
const current = element(active.value)
|
|
184
244
|
if (still.value && current && ready.value) current.currentTime = plan.value.from
|
|
185
245
|
}
|
|
186
246
|
|
|
247
|
+
function sync() {
|
|
248
|
+
const easing = props.easePlayback > 0 && !prefersReducedMotion() ? props.easePlayback : 0
|
|
249
|
+
if (playing.value) {
|
|
250
|
+
// A resume from a standstill gathers speed from the slowest rate.
|
|
251
|
+
if (!running.value) throttle.value = easing ? 0 : 1
|
|
252
|
+
startElements()
|
|
253
|
+
rampTo(1, easing)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
// Nobody sees a hidden tab, and a still is asked for outright: both stop at once.
|
|
257
|
+
if (!running.value || pageHidden.value || still.value || !easing) {
|
|
258
|
+
rampTo(throttle.value, 0)
|
|
259
|
+
stopElements()
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
rampTo(0, easing, () => {
|
|
263
|
+
if (!playing.value) stopElements()
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
187
267
|
watch(playing, sync, { flush: "post" })
|
|
188
|
-
watch(() => props.rate,
|
|
189
|
-
setRate(first.value)
|
|
190
|
-
setRate(second.value)
|
|
191
|
-
})
|
|
268
|
+
watch(() => props.rate, applyRate)
|
|
192
269
|
watch(
|
|
193
270
|
() => props.src,
|
|
194
271
|
() => {
|
|
@@ -230,8 +307,7 @@ function onVisibility() {
|
|
|
230
307
|
let motionQuery: MediaQueryList | null = null
|
|
231
308
|
|
|
232
309
|
onMounted(() => {
|
|
233
|
-
|
|
234
|
-
setRate(second.value)
|
|
310
|
+
applyRate()
|
|
235
311
|
motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
236
312
|
motionQuery.addEventListener("change", onMotionChange)
|
|
237
313
|
document.addEventListener("visibilitychange", onVisibility)
|
|
@@ -239,6 +315,7 @@ onMounted(() => {
|
|
|
239
315
|
|
|
240
316
|
onBeforeUnmount(() => {
|
|
241
317
|
if (frame) cancelAnimationFrame(frame)
|
|
318
|
+
if (ramp) cancelAnimationFrame(ramp)
|
|
242
319
|
motionQuery?.removeEventListener("change", onMotionChange)
|
|
243
320
|
document.removeEventListener("visibilitychange", onVisibility)
|
|
244
321
|
})
|
|
@@ -252,8 +329,12 @@ function layerStyle(index: 0 | 1) {
|
|
|
252
329
|
}
|
|
253
330
|
}
|
|
254
331
|
|
|
332
|
+
// `isolate`: the element fading in at the seam is raised over the one going
|
|
333
|
+
// out with a z-index, and without a stacking context of its own here that
|
|
334
|
+
// z-index lifted it over the page's content too, so text laid on the backdrop
|
|
335
|
+
// faded away at every seam.
|
|
255
336
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
256
|
-
"pointer-events-none absolute inset-0 overflow-hidden",
|
|
337
|
+
"pointer-events-none absolute inset-0 isolate overflow-hidden",
|
|
257
338
|
)
|
|
258
339
|
</script>
|
|
259
340
|
|