跳转动画效果

This commit is contained in:
2026-06-20 10:37:40 +08:00 Unverified
parent afb3fcbe2f
commit 9921d24910
3 changed files with 378 additions and 2 deletions
+73 -2
View File
@@ -1,4 +1,72 @@
function randomPost() {
function randomPost(trigger) {
const source = trigger instanceof Element
? trigger
: document.activeElement?.closest?.('.nav-random, [onclick*="randomPost"]');
const overlay = document.createElement('div');
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
overlay.className = 'random-post-transition';
overlay.setAttribute('role', 'status');
overlay.setAttribute('aria-live', 'polite');
overlay.innerHTML = `
<div class="random-post-flight" aria-hidden="true"></div>
<div class="random-post-transition-card">
<i class="fa-solid fa-shuffle" aria-hidden="true"></i>
<span>\u6b63\u5728\u62bd\u53d6\u4e00\u7bc7\u6587\u7ae0</span>
</div>
`;
source?.classList.add('is-randomizing');
source?.setAttribute('aria-busy', 'true');
document.body.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add('is-visible'));
const finish = href => {
const delay = reduceMotion ? 80 : 1250;
window.setTimeout(() => {
location.href = href;
}, delay);
};
const startCardFlight = posts => {
if (reduceMotion) return;
const flight = overlay.querySelector('.random-post-flight');
if (!flight) return;
const candidates = posts
.filter(post => post && typeof post.href === 'string' && post.href.startsWith('/posts/'))
.map(post => ({
title: typeof post.title === 'string' && post.title.trim() ? post.title.trim() : '\u672a\u547d\u540d\u6587\u7ae0',
meta: typeof post.category === 'string' && post.category.trim() ? post.category.trim() : '\u968f\u673a\u6587\u7ae0',
}));
const pool = candidates.length ? candidates : [{ title: '\u968f\u673a\u6587\u7ae0', meta: '\u6b63\u5728\u62bd\u53d6' }];
const count = Math.min(9, Math.max(5, pool.length));
for (let index = 0; index < count; index += 1) {
const post = pool[Math.floor(Math.random() * pool.length)];
const card = document.createElement('div');
card.className = 'random-post-flying-card';
card.style.setProperty('--flight-delay', `${index * 0.11}s`);
card.style.setProperty('--flight-y', `${-42 + Math.random() * 84}px`);
card.style.setProperty('--flight-rotate', `${-8 + Math.random() * 16}deg`);
const meta = document.createElement('small');
const title = document.createElement('strong');
meta.textContent = post.meta;
title.textContent = post.title;
card.append(meta, title);
flight.appendChild(card);
}
};
const reset = () => {
source?.classList.remove('is-randomizing');
source?.removeAttribute('aria-busy');
overlay.classList.remove('is-visible');
window.setTimeout(() => overlay.remove(), reduceMotion ? 0 : 180);
};
fetch('/api/posts.json')
.then(res => {
if (!res.ok) throw new Error(`Failed to load posts: ${res.status}`);
@@ -11,12 +79,15 @@ function randomPost() {
if (postLinks.length === 0) {
console.warn('No posts available for random navigation.');
reset();
return;
}
location.href = postLinks[Math.floor(Math.random() * postLinks.length)];
startCardFlight(posts);
finish(postLinks[Math.floor(Math.random() * postLinks.length)]);
})
.catch(error => {
console.error('Random post navigation failed:', error);
reset();
});
}
+5
View File
@@ -57,6 +57,10 @@
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
<span>标签</span>
</a>
<button type="button" data-context-action="random-post">
<i class="fa-solid fa-shuffle" aria-hidden="true"></i>
<span>&#x968F;&#x673A;&#x6587;&#x7AE0;</span>
</button>
<button type="button" data-context-action="comments" data-comments-item>
<i class="fa-regular fa-comment-dots" aria-hidden="true"></i>
<span>跳转评论区</span>
@@ -239,6 +243,7 @@
if (action === 'copy-image') copyImage();
if (action === 'open-image') openImage();
if (action === 'copy-image-url') copyText(new URL(getImageUrl(), window.location.href).toString());
if (action === 'random-post' && typeof window.randomPost === 'function') window.randomPost(actionTarget);
if (action === 'comments') jumpToComments();
if (action === 'print') window.print();
});
+300
View File
@@ -190,6 +190,41 @@ a {
cursor: pointer;
}
.nav-random {
position: relative;
overflow: hidden;
}
.nav-random::after {
content: "";
position: absolute;
inset: 3px;
border-radius: inherit;
background:
linear-gradient(120deg, transparent 8%, rgba(255, 255, 255, 0.82) 34%, transparent 58%);
opacity: 0;
transform: translateX(-135%);
pointer-events: none;
}
.nav-random.is-randomizing {
color: var(--accent);
border-color: rgba(15, 118, 110, 0.22);
background: rgba(255, 255, 255, 0.5);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.76),
0 10px 24px rgba(15, 118, 110, 0.16);
}
.nav-random.is-randomizing::after {
opacity: 1;
animation: random-button-sheen 0.72s ease both;
}
.nav-random.is-randomizing .nav-icon {
animation: random-button-spin 0.72s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.nav-icon {
display: inline-grid;
width: 16px;
@@ -356,6 +391,136 @@ a {
transform: translateY(-1px);
}
.random-post-transition {
position: fixed;
inset: 0;
z-index: 1200;
display: grid;
place-items: center;
padding: var(--page-gutter);
opacity: 0;
pointer-events: none;
background: rgba(247, 245, 239, 0.34);
backdrop-filter: blur(6px) saturate(145%);
-webkit-backdrop-filter: blur(6px) saturate(145%);
transition: opacity 0.18s ease;
}
.random-post-transition.is-visible {
opacity: 1;
}
.random-post-flight {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
}
.random-post-flying-card {
position: absolute;
top: calc(50% + var(--flight-y));
left: 50%;
width: min(260px, 68vw);
border: 1px solid rgba(255, 255, 255, 0.64);
border-radius: 8px;
padding: 12px 14px;
color: #2f4358;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(238, 250, 246, 0.68)),
rgba(255, 255, 255, 0.74);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.86),
0 18px 48px rgba(44, 55, 63, 0.16);
opacity: 0;
transform: translate3d(-50vw, -50%, 0) rotate(var(--flight-rotate)) scale(0.82);
animation: random-card-fly 1.16s cubic-bezier(0.16, 0.84, 0.28, 1) var(--flight-delay) both;
}
.random-post-flying-card small,
.random-post-flying-card strong {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.random-post-flying-card small {
color: var(--muted);
font-size: 0.74rem;
font-weight: 700;
}
.random-post-flying-card strong {
margin-top: 4px;
font-size: 0.95rem;
font-weight: 800;
}
.random-post-transition-card {
position: relative;
display: inline-flex;
align-items: center;
gap: 12px;
min-width: min(280px, 100%);
border: 1px solid rgba(255, 255, 255, 0.66);
border-radius: 999px;
padding: 16px 22px;
color: var(--accent);
font-size: 1rem;
font-weight: 750;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.84), rgba(238, 250, 246, 0.62)),
rgba(255, 255, 255, 0.7);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.88),
0 24px 64px rgba(44, 55, 63, 0.2);
transform: translateY(8px) scale(0.96);
transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.random-post-transition.is-visible .random-post-transition-card {
transform: translateY(0) scale(1);
}
.random-post-transition-card::before,
.random-post-transition-card::after {
content: "";
position: absolute;
inset: -10px;
border: 1px solid rgba(15, 118, 110, 0.24);
border-radius: inherit;
opacity: 0;
animation: random-card-pulse 1.1s ease-out infinite;
}
.random-post-transition-card::after {
animation-delay: 0.28s;
}
.random-post-transition-card i {
display: inline-grid;
width: 22px;
min-width: 22px;
height: 22px;
place-items: center;
animation: random-button-spin 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.random-post-transition-card span {
min-width: 0;
overflow-wrap: anywhere;
}
.random-post-transition-card span::after {
content: "";
display: inline-block;
width: 1.25em;
text-align: left;
animation: random-loading-dots 1s steps(4, end) infinite;
}
.has-control-console {
overflow: hidden;
}
@@ -588,6 +753,45 @@ a {
0 8px 18px rgba(0, 0, 0, 0.18);
}
:root[data-theme='dark'] .nav-random.is-randomizing {
color: var(--accent);
border-color: rgba(139, 224, 213, 0.32);
background: rgba(125, 211, 199, 0.14);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
0 10px 24px rgba(0, 0, 0, 0.28);
}
:root[data-theme='dark'] .random-post-transition {
background: rgba(3, 10, 12, 0.38);
}
:root[data-theme='dark'] .random-post-transition-card {
border-color: rgba(166, 184, 194, 0.22);
background:
linear-gradient(135deg, rgba(25, 35, 42, 0.9), rgba(10, 17, 22, 0.76)),
rgba(14, 22, 28, 0.84);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 28px 74px rgba(0, 0, 0, 0.42);
}
:root[data-theme='dark'] .random-post-transition-card::before,
:root[data-theme='dark'] .random-post-transition-card::after {
border-color: rgba(139, 224, 213, 0.26);
}
:root[data-theme='dark'] .random-post-flying-card {
border-color: rgba(166, 184, 194, 0.2);
color: #edf7f5;
background:
linear-gradient(135deg, rgba(25, 35, 42, 0.88), rgba(10, 17, 22, 0.72)),
rgba(14, 22, 28, 0.78);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 22px 54px rgba(0, 0, 0, 0.34);
}
:root[data-theme='dark'] .control-console-backdrop {
background: rgba(3, 10, 12, 0.58);
}
@@ -4514,3 +4718,99 @@ meting-js {
text-align: center;
}
}
@keyframes random-button-spin {
0% {
transform: rotate(0deg) scale(1);
}
55% {
transform: rotate(210deg) scale(1.18);
}
100% {
transform: rotate(360deg) scale(1);
}
}
@keyframes random-button-sheen {
0% {
transform: translateX(-135%);
}
100% {
transform: translateX(135%);
}
}
@keyframes random-card-pulse {
0% {
opacity: 0.55;
transform: scale(0.94);
}
100% {
opacity: 0;
transform: scale(1.16);
}
}
@keyframes random-card-fly {
0% {
opacity: 0;
filter: blur(2px);
transform: translate3d(-58vw, -50%, 0) rotate(calc(var(--flight-rotate) - 8deg)) scale(0.74);
}
22% {
opacity: 0.88;
filter: blur(0);
}
58% {
opacity: 0.92;
transform: translate3d(-50%, -50%, 0) rotate(var(--flight-rotate)) scale(1);
}
100% {
opacity: 0;
filter: blur(2px);
transform: translate3d(58vw, -50%, 0) rotate(calc(var(--flight-rotate) + 10deg)) scale(0.78);
}
}
@keyframes random-loading-dots {
0% {
content: "";
}
25% {
content: ".";
}
50% {
content: "..";
}
75%,
100% {
content: "...";
}
}
@media (prefers-reduced-motion: reduce) {
.nav-random.is-randomizing::after,
.nav-random.is-randomizing .nav-icon,
.random-post-flying-card,
.random-post-transition-card::before,
.random-post-transition-card::after,
.random-post-transition-card i,
.random-post-transition-card span::after {
animation: none;
}
.random-post-transition,
.random-post-transition-card {
transition: none;
}
}