我正在开发一个具有 GSAP 动画的 React 项目,我已经使用 JavaScript 示例实现了无限循环动画,但无法在 React 中实现它。当我使用一些预定义函数时,它会抛出很多错误,请检查屏幕截图以供参考。
在这里我提供了我已经实现的javascript的codepen以及我想要实现它的stackbiz。感谢您的帮助。
https://codepen.io/EmpChirag/pen/bGZNYwo
https://stackblitz.com/edit/gsap-react-basic-f48716-rm5kfn?file=src%2FApp.js
我尝试过将函数放入 useEffect、useGsap、useLayoutEffect 中,但我不明白如何管理这些函数。
这是我想使用的功能。
// use the helper function to build a seamless looping gsap.timeline() with some special properties/methods
let tween = verticalLoop(".box-wrapper:nth-child(odd) div", {
repeat: -1,
speed:0.5
});
let tween2 = verticalLoop(".box-wrapper:nth-child(even) div", {
repeat: -1,
speed:1
});
let tween3 = horizontalLoop(".cb-tagreel-row div", {
repeat: -1,
speed:1
});
/*
This helper function makes a group of elements animate along the y-axis in a seamless, responsive loop.
Features:
- Uses yPercent so that even if the widths change (like if the window gets resized), it should still work in most cases.
- When each item animates up or down enough, it will loop back to the other side
- Optionally pass in a config object with values like draggable: true, center: true, speed (default: 1, which travels at roughly 100 pixels per second), paused (boolean), repeat, reversed, and paddingBottom.
- The returned timeline will have the following methods added to it:
- next() - animates to the next element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
- previous() - animates to the previous element using a timeline.tweenTo() which it returns. You can pass in a vars object to control duration, easing, etc.
- toIndex() - pass in a zero-based index value of the element that it should animate to, and optionally pass in a vars object to control duration, easing, etc. Always goes in the shortest direction
- current() - returns the current index (if an animation is in-progress, it reflects the final index)
- times - an Array of the times on the timeline where each element hits the "starting" spot.
- elements - an Array of the elements that are being controlled by the timeline
*/
function verticalLoop(items, config) {
items = gsap.utils.toArray(items);
config = config || {};
let onChange = config.onChange,
lastIndex = 0,
tl = gsap.timeline({repeat: config.repeat, onUpdate: onChange && function() {
let i = tl.closestIndex()
if (lastIndex !== i) {
lastIndex = i;
onChange(items[i], i);
}
}, paused: config.paused, defaults: {ease: "none"}, onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100)}),
length = items.length,
startY = items[0].offsetTop,
times = [],
heights = [],
spaceBefore = [],
yPercents = [],
curIndex = 0,
center = config.center,
clone = obj => {
let result = {}, p;
for (p in obj) {
result[p] = obj[p];
}
return result;
},
pixelsPerSecond = (config.speed || 1) * 100,
snap = config.snap === false ? v => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
timeOffset = 0,
container = center === true ? items[0].parentNode : gsap.utils.toArray(center)[0] || items[0].parentNode,
totalHeight,
getTotalHeight = () => items[length-1].offsetTop + yPercents[length-1] / 100 * heights[length-1] - startY + spaceBefore[0] + items[length-1].offsetHeight * gsap.getProperty(items[length-1], "scaleY") + (parseFloat(config.paddingBottom) || 0),
populateHeights = () => {
let b1 = container.getBoundingClientRect(), b2;
items.forEach((el, i) => {
heights[i] = parseFloat(gsap.getProperty(el, "height", "px"));
yPercents[i] = snap(parseFloat(gsap.getProperty(el, "y", "px")) / heights[i] * 100 + gsap.getProperty(el, "yPercent"));
b2 = el.getBoundingClientRect();
spaceBefore[i] = b2.top - (i ? b1.bottom : b1.top);
b1 = b2;
});
gsap.set(items, { // convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster.
yPercent: i => yPercents[i]
});
totalHeight = getTotalHeight();
},
timeWrap,
populateOffsets = () => {
timeOffset = center ? tl.duration() * (container.offsetWidth / 2) / totalHeight : 0;
center && times.forEach((t, i) => {
times[i] = timeWrap(tl.labels["label" + i] + tl.duration() * heights[i] / 2 / totalHeight - timeOffset);
});
},
getClosest = (values, value, wrap) => {
let i = values.length,
closest = 1e10,
index = 0, d;
while (i--) {
d = Math.abs(values[i] - value);
if (d > wrap / 2) {
d = wrap - d;
}
if (d < closest) {
closest = d;
index = i;
}
}
return index;
},
populateTimeline = () => {
let i, item, curY, distanceToStart, distanceToLoop;
tl.clear();
for (i = 0; i < length; i++) {
item = items[i];
curY = yPercents[i] / 100 * heights[i];
distanceToStart = item.offsetTop + curY - startY + spaceBefore[0];
distanceToLoop = distanceToStart + heights[i] * gsap.getProperty(item, "scaleY");
tl.to(item, {yPercent: snap((curY - distanceToLoop) / heights[i] * 100), duration: distanceToLoop / pixelsPerSecond}, 0)
.fromTo(item, {yPercent: snap((curY - distanceToLoop + totalHeight) / heights[i] * 100)}, {yPercent: yPercents[i], duration: (curY - distanceToLoop + totalHeight - curY) / pixelsPerSecond, immediateRender: false}, distanceToLoop / pixelsPerSecond)
.add("label" + i, distanceToStart / pixelsPerSecond);
times[i] = distanceToStart / pixelsPerSecond;
}
timeWrap = gsap.utils.wrap(0, tl.duration());
},
refresh = (deep) => {
let progress = tl.progress();
tl.progress(0, true);
populateHeights();
deep && populateTimeline();
populateOffsets();
deep && tl.draggable ? tl.time(times[curIndex], true) : tl.progress(progress, true);
},
proxy;
gsap.set(items, {y: 0});
populateHeights();
populateTimeline();
populateOffsets();
window.addEventListener("resize", () => refresh(true));
function toIndex(index, vars) {
vars = clone(vars);
(Math.abs(index - curIndex) > length / 2) && (index += index > curIndex ? -length : length); // always go in the shortest direction
let newIndex = gsap.utils.wrap(0, length, index),
time = times[newIndex];
if (time > tl.time() !== index > curIndex) { // if we're wrapping the timeline's playhead, make the proper adjustments
time += tl.duration() * (index > curIndex ? 1 : -1);
}
if (vars.revolutions) {
time += tl.duration() * Math.round(vars.revolutions);
delete vars.revolutions;
}
if (time < 0 || time > tl.duration()) {
vars.modifiers = {time: timeWrap};
}
curIndex = newIndex;
vars.overwrite = true;
gsap.killTweensOf(proxy);
return tl.tweenTo(time, vars);
}
tl.elements = items;
tl.next = vars => toIndex(curIndex+1, vars);
tl.previous = vars => toIndex(curIndex-1, vars);
tl.current = () => curIndex;
tl.toIndex = (index, vars) => toIndex(index, vars);
tl.closestIndex = setCurrent => {
let index = getClosest(times, tl.time(), tl.duration());
setCurrent && (curIndex = index);
return index;
};
tl.times = times;
tl.progress(1, true).progress(0, true); // pre-render for performance
if (config.reversed) {
tl.vars.onReverseComplete();
tl.reverse();
}
if (config.draggable && typeof(Draggable) === "function") {
proxy = document.createElement("div")
let wrap = gsap.utils.wrap(0, 1),
ratio, startProgress, draggable, dragSnap,
align = () => tl.progress(wrap(startProgress + (draggable.startY - draggable.y) * ratio)),
syncIndex = () => tl.closestIndex(true);
typeof(InertiaPlugin) === "undefined" && console.warn("InertiaPlugin required for momentum-based scrolling and snapping. https://greensock.com/club");
draggable = Draggable.create(proxy, {
trigger: items[0].parentNode,
type: "y",
onPressInit() {
gsap.killTweensOf(tl);
startProgress = tl.progress();
refresh();
ratio = 1 / totalHeight;
gsap.set(proxy, {y: startProgress / -ratio})
},
onDrag: align,
onThrowUpdate: align,
inertia: true,
snap: value => {
let time = -(value * ratio) * tl.duration(),
wrappedTime = timeWrap(time),
snapTime = times[getClosest(times, wrappedTime, tl.duration())],
dif = snapTime - wrappedTime;
Math.abs(dif) > tl.duration() / 2 && (dif += dif < 0 ? tl.duration() : -tl.duration());
return (time + dif) / tl.duration() / -ratio;
},
onRelease: syncIndex,
onThrowComplete: syncIndex
})[0];
tl.draggable = draggable;
}
tl.closestIndex(true);
onChange && onChange(items[curIndex], curIndex);
return tl;
}
function horizontalLoop(items, config) {
items = gsap.utils.toArray(items);
config = config || {};
let tl = gsap.timeline({
repeat: config.repeat,
paused: config.paused,
defaults: { ease: "none" },
onReverseComplete: () => tl.totalTime(tl.rawTime() + tl.duration() * 100),
}),
length = items.length,
startX = items[0].offsetLeft,
times = [],
widths = [],
xPercents = [],
curIndex = 0,
pixelsPerSecond = (config.speed || 1) * 100,
snap = config.snap === false ? (v) => v : gsap.utils.snap(config.snap || 1), // some browsers shift by a pixel to accommodate flex layouts, so for example if width is 20% the first element's width might be 242px, and the next 243px, alternating back and forth. So we snap to 5 percentage points to make things look more natural
totalWidth,
curX,
distanceToStart,
distanceToLoop,
item,
i;
gsap.set(items, {
// convert "x" to "xPercent" to make things responsive, and populate the widths/xPercents Arrays to make lookups faster.
xPercent: (i, el) => {
let w = (widths[i] = parseFloat(gsap.getProperty(el, "width", "px")));
xPercents[i] = snap(
(parseFloat(gsap.getProperty(el, "x", "px")) / w) * 100 +
gsap.getProperty(el, "xPercent")
);
return xPercents[i];
},
});
gsap.set(items, { x: 0 });
totalWidth =
items[length - 1].offsetLeft +
(xPercents[length - 1] / 100) * widths[length - 1] -
startX +
items[length - 1].offsetWidth *
gsap.getProperty(items[length - 1], "scaleX") +
(parseFloat(config.paddingRight) || 0);
for (i = 0; i < length; i++) {
item = items[i];
curX = (xPercents[i] / 100) * widths[i];
distanceToStart = item.offsetLeft + curX - startX;
distanceToLoop =
distanceToStart + widths[i] * gsap.getProperty(item, "scaleX");
tl.to(
item,
{
xPercent: snap(((curX - distanceToLoop) / widths[i]) * 100),
duration: distanceToLoop / pixelsPerSecond,
},
0
)
.fromTo(
item,
{
xPercent: snap(
((curX - distanceToLoop + totalWidth) / widths[i]) * 100
),
},
{
xPercent: xPercents[i],
duration:
(curX - distanceToLoop + totalWidth - curX) / pixelsPerSecond,
immediateRender: false,
},
distanceToLoop / pixelsPerSecond
)
.add("label" + i, distanceToStart / pixelsPerSecond);
times[i] = distanceToStart / pixelsPerSecond;
}
function toIndex(index, vars) {
vars = vars || {};
Math.abs(index - curIndex) > length / 2 &&
(index += index > curIndex ? -length : length); // always go in the shortest direction
let newIndex = gsap.utils.wrap(0, length, index),
time = times[newIndex];
if (time > tl.time() !== index > curIndex) {
// if we're wrapping the timeline's playhead, make the proper adjustments
vars.modifiers = { time: gsap.utils.wrap(0, tl.duration()) };
time += tl.duration() * (index > curIndex ? 1 : -1);
}
curIndex = newIndex;
vars.overwrite = true;
return tl.tweenTo(time, vars);
}
tl.next = (vars) => toIndex(curIndex + 1, vars);
tl.previous = (vars) => toIndex(curIndex - 1, vars);
tl.current = () => curIndex;
tl.toIndex = (index, vars) => toIndex(index, vars);
tl.times = times;
tl.progress(1, true).progress(0, true); // pre-render for performance
if (config.reversed) {
tl.vars.onReverseComplete();
tl.reverse();
}
return tl;
}
*{
margin: 0;
padding: 0;
}
.slide-conten{
/* display: flex;*/
width: 100%;
height: 100vh;
background-color: #000000;
overflow: hidden;
gap: 5px;
}
.slide-box{
display: flex;
width: 100%;
height: 100vh;
/* background-color: #000000;*/
overflow: hidden;
gap: 5px;
}
.box-wrapper{
width: 100%;
height: 100vh;
/* background-color: yellow;*/
}
.child-box{
margin: 5px 5px;
width: 100%;
height: 300px;
background-color: #ffffff;
border-radius: 10px;
}
section {
display: block;
}
.cb-tagreel {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.cb-tagreel-content {
/* background-color: yellow;*/
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.cb-tagreel-items {
overflow: hidden;
cursor: default;
width: 100%;
}
.cb-tagreel-row {
display: flex;
position: relative;
text-align: center;
white-space: nowrap;
}
.cb-tagreel-item {
position: relative;
line-height: 100%;
font-size: 3.75vw;
flex: 0 0 33%;
/* padding: 58px 0;*/
/* text-transform: uppercase;*/
}
.cb-tagreel-item.-stroke {
color: transparent;
text-shadow: none;
-webkit-text-stroke: 2px rgba(0, 0, 0, 0.2);
}
.cb-tagreel-item span {
position: relative;
display: inline-block;
z-index: 1;
}
/**/
<div class="slide-conten">
<div class="slide-box">
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
<div class="box-wrapper">
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
<div class="child-box"></div>
</div>
</div>
<section class="cb-tagreel">
<div class="cb-tagreel-content">
<div class="cb-tagreel-items" role="marquee">
<div class="cb-tagreel-row">
<div class="cb-tagreel-item"><span>1</span></div>
<div class="cb-tagreel-item"><span>2</span></div>
<div class="cb-tagreel-item"><span>3</span></div>
<div class="cb-tagreel-item"><span>4</span></div>
<div class="cb-tagreel-item"><span>5</span></div>
<div class="cb-tagreel-item"><span>6</span></div>
</div>
</div>
</div>
</section>
</div>
<script type="text/javascript" src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
错误仅由 ESLint 造成。