ffdsa
This commit is contained in:
@@ -14457,6 +14457,32 @@ div.posts>a.thumb.has-notif:focus-visible::after {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Sidebar toggle reflow glide (flipSidebarGrids): tiles move under a still cursor, so hover styling
|
||||
would ride along with the tile that was hovered. Hover is off until the tiles have landed. */
|
||||
div.posts.sidebar-flipping {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
div.posts.sidebar-flipping>a:not(.notif-item):not(.has-notif):hover,
|
||||
div.posts.sidebar-flipping>a:not(.notif-item):not(.has-notif).touch-active {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[theme='light'] div.posts.sidebar-flipping>a:not(.notif-item):not(.has-notif):hover,
|
||||
html[theme='paper'] div.posts.sidebar-flipping>a:not(.notif-item):not(.has-notif):hover {
|
||||
border-color: #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
div.posts.sidebar-flipping>a.thumb.has-notif:hover {
|
||||
box-shadow: inset 0 0 0 1000px rgba(255, 0, 0, 0.28) !important;
|
||||
}
|
||||
|
||||
div.posts.sidebar-flipping>a:hover::after,
|
||||
div.posts.sidebar-flipping>a.touch-active::after {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
/* Stretch the <p> to fill the entire thumb so p::after is always centered */
|
||||
div.posts>a.thumb.has-notif p {
|
||||
position: absolute !important;
|
||||
|
||||
+195
-7
@@ -17840,6 +17840,180 @@ window.cancelAnimFrame = (function () {
|
||||
return document.querySelectorAll(sel);
|
||||
};
|
||||
|
||||
// Thumbnail grids re-wrap into a different column count when their wrapper changes width, so instead of
|
||||
// snapping they are FLIP-animated: measure every tile, apply the new width, measure again, then move each
|
||||
// tile in view from where it was to where it landed (position + size). The first visible tile keeps its
|
||||
// place on screen so the scroll position doesn't jump when the row count changes.
|
||||
// Tiles that stay in their lane glide there directly. Tiles that wrap to another row don't cut diagonally
|
||||
// back across the grid: they keep flowing in the reflow direction like a carousel. A stand-in copy slides
|
||||
// out past the row's edge while the tile itself slides in from the opposite edge of its new row, both
|
||||
// with the same offset, so it reads as one continuous stream.
|
||||
const SIDEBAR_GRID_ITEMS = '.posts > *, .tags-grid > *';
|
||||
let sidebarGridFlip = { anims: [], cleanup: [] };
|
||||
const clearSidebarGridFlip = () => {
|
||||
sidebarGridFlip.anims.forEach(a => a.cancel());
|
||||
sidebarGridFlip.cleanup.forEach(fn => fn());
|
||||
sidebarGridFlip = { anims: [], cleanup: [] };
|
||||
};
|
||||
const flipSidebarGrids = (gridWrappers, change, duration, easing) => {
|
||||
const items = [];
|
||||
gridWrappers.forEach(w => w.querySelectorAll(SIDEBAR_GRID_ITEMS).forEach(el => items.push(el)));
|
||||
const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (!items.length || reduceMotion || items.length > 5000 || typeof items[0].animate !== 'function') {
|
||||
clearSidebarGridFlip();
|
||||
change();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nearest scrolling ancestor (the index container), else the page itself
|
||||
let scroller = items[0].parentElement;
|
||||
while (scroller && scroller !== document.body) {
|
||||
const oy = getComputedStyle(scroller).overflowY;
|
||||
if ((oy === 'auto' || oy === 'scroll') && scroller.scrollHeight > scroller.clientHeight) break;
|
||||
scroller = scroller.parentElement;
|
||||
}
|
||||
if (!scroller || scroller === document.body) scroller = document.scrollingElement || document.documentElement;
|
||||
const isPage = scroller === document.scrollingElement || scroller === document.documentElement;
|
||||
const sr = isPage ? { top: 0, bottom: window.innerHeight } : scroller.getBoundingClientRect();
|
||||
const viewTop = Math.max(0, sr.top);
|
||||
const viewBottom = Math.min(window.innerHeight, sr.bottom);
|
||||
const inView = r => r.bottom > viewTop && r.top < viewBottom && r.width > 0;
|
||||
|
||||
// Visual positions: a glide still running (quick re-toggle) continues from where the tiles are now
|
||||
const before = items.map(el => el.getBoundingClientRect());
|
||||
const anchorIdx = before.findIndex(inView);
|
||||
clearSidebarGridFlip();
|
||||
const anchorTop = anchorIdx >= 0 ? items[anchorIdx].getBoundingClientRect().top : 0;
|
||||
const flip = sidebarGridFlip;
|
||||
const grids = new Map();
|
||||
items.forEach(el => {
|
||||
const g = el.parentElement;
|
||||
if (grids.has(g)) return;
|
||||
grids.set(g, { before: g.getBoundingClientRect(), gap: parseFloat(getComputedStyle(g).rowGap) || 0 });
|
||||
// No hover while tiles move under the cursor (see .sidebar-flipping in f0ckm.css)
|
||||
g.classList.add('sidebar-flipping');
|
||||
flip.cleanup.push(() => g.classList.remove('sidebar-flipping'));
|
||||
});
|
||||
|
||||
change();
|
||||
|
||||
// The scroller clips at its own edge, which moves in right away when the grid narrows: tiles still
|
||||
// gliding through the strip the sidebar hasn't covered yet would look cut off. Stretch its box back
|
||||
// over that strip (negative margin + matching padding, so the content width stays the new one).
|
||||
if (!isPage) {
|
||||
const ext = sr.right - scroller.getBoundingClientRect().right;
|
||||
if (ext > 0.5) {
|
||||
const cs = getComputedStyle(scroller);
|
||||
scroller.style.setProperty('margin-right', (parseFloat(cs.marginRight) - ext) + 'px', 'important');
|
||||
scroller.style.setProperty('padding-right', (parseFloat(cs.paddingRight) + ext) + 'px', 'important');
|
||||
flip.cleanup.push(() => {
|
||||
scroller.style.removeProperty('margin-right');
|
||||
scroller.style.removeProperty('padding-right');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (anchorIdx >= 0) {
|
||||
const shift = items[anchorIdx].getBoundingClientRect().top - anchorTop;
|
||||
if (Math.abs(shift) > 0.5) scroller.scrollTop += shift;
|
||||
}
|
||||
|
||||
grids.forEach((info, g) => {
|
||||
info.after = g.getBoundingClientRect();
|
||||
const dw = info.after.width - info.before.width;
|
||||
// Narrower grid: fewer columns, tiles flow forward (wrap off the right, in from the left); wider: backward
|
||||
info.dir = dw < -1 ? 1 : dw > 1 ? -1 : 0;
|
||||
});
|
||||
|
||||
const layerFor = (g, info) => {
|
||||
if (info.layer) return info.layer;
|
||||
// Stand-ins live in a fixed layer clipped to the grid's left edge and the scroller's visible band
|
||||
const layer = document.createElement('div');
|
||||
layer.setAttribute('aria-hidden', 'true');
|
||||
const left = Math.max(0, info.after.left);
|
||||
layer.style.cssText = `position:fixed;left:${left}px;top:${viewTop}px;width:${Math.max(0, window.innerWidth - left)}px;height:${Math.max(0, viewBottom - viewTop)}px;overflow:hidden;pointer-events:none;`;
|
||||
g.after(layer);
|
||||
info.layer = layer;
|
||||
flip.cleanup.push(() => layer.remove());
|
||||
if (info.dir === 1) {
|
||||
// Tiles slide in from beyond the left edge: clip them there (right side stays open for the sidebar)
|
||||
g.style.setProperty('clip-path', 'inset(-100vh -100vw -100vh 0)');
|
||||
flip.cleanup.push(() => g.style.removeProperty('clip-path'));
|
||||
}
|
||||
return layer;
|
||||
};
|
||||
|
||||
const after = items.map(el => el.getBoundingClientRect());
|
||||
items.forEach((el, i) => {
|
||||
const b = before[i];
|
||||
const a = after[i];
|
||||
if (!a.width || !b.width || (!inView(b) && !inView(a))) return;
|
||||
const s = b.width / a.width;
|
||||
const g = el.parentElement;
|
||||
const info = grids.get(g);
|
||||
const dir = info.dir;
|
||||
const wraps = dir !== 0 && g.classList.contains('posts')
|
||||
&& (dir === 1 ? a.left < b.left - a.width / 2 : a.left > b.left + a.width / 2);
|
||||
|
||||
if (!wraps) {
|
||||
const dx = b.left - a.left;
|
||||
const dy = b.top - a.top;
|
||||
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5 && Math.abs(s - 1) < 0.005) return;
|
||||
flip.anims.push(el.animate(
|
||||
[
|
||||
{ transformOrigin: '0 0', transform: `translate(${dx}px, ${dy}px) scale(${s})` },
|
||||
{ transformOrigin: '0 0', transform: 'none' }
|
||||
],
|
||||
{ duration, easing }
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Carousel wrap. L is chosen so the copy clears one edge exactly as the tile emerges from the other.
|
||||
const gl = info.after.left;
|
||||
const gr = info.after.right;
|
||||
const pitchOld = b.height + info.gap;
|
||||
const pitchNew = a.height + info.gap;
|
||||
const L = Math.max(a.width, dir === 1
|
||||
? (gr - b.left) + (a.right - gl) + info.gap
|
||||
: (b.right - gl) + (gr - a.left) + info.gap);
|
||||
|
||||
// The tile itself: enters its new row from the far side, starting one lane past its old row
|
||||
flip.anims.push(el.animate(
|
||||
[
|
||||
{ transformOrigin: '0 0', transform: `translate(${-dir * L}px, ${b.top + dir * pitchOld - a.top}px) scale(${s})` },
|
||||
{ transformOrigin: '0 0', transform: 'none' }
|
||||
],
|
||||
{ duration, easing }
|
||||
));
|
||||
|
||||
// The stand-in: leaves its old row over the near edge, drifting with that lane
|
||||
const layer = layerFor(g, info);
|
||||
const cs = getComputedStyle(el);
|
||||
const ghost = document.createElement('div');
|
||||
const lr = layer.getBoundingClientRect();
|
||||
ghost.style.cssText = `position:absolute;left:${b.left - lr.left}px;top:${b.top - lr.top}px;width:${b.width}px;height:${b.height}px;transform-origin:0 0;`;
|
||||
ghost.style.backgroundColor = cs.backgroundColor;
|
||||
ghost.style.backgroundImage = cs.backgroundImage;
|
||||
ghost.style.backgroundSize = cs.backgroundSize;
|
||||
ghost.style.backgroundPosition = cs.backgroundPosition;
|
||||
ghost.style.backgroundRepeat = 'no-repeat';
|
||||
layer.appendChild(ghost);
|
||||
flip.anims.push(ghost.animate(
|
||||
[
|
||||
{ transform: 'none' },
|
||||
{ transform: `translate(${dir * L}px, ${a.top - dir * pitchNew - b.top}px) scale(${1 / s})` }
|
||||
],
|
||||
{ duration, easing, fill: 'forwards' }
|
||||
));
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (sidebarGridFlip !== flip) return;
|
||||
clearSidebarGridFlip();
|
||||
}, duration + 50);
|
||||
};
|
||||
|
||||
// Smoothly toggle sidebar open/closed
|
||||
window.toggleSidebarRight = () => {
|
||||
const sidebar = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
|
||||
@@ -17854,9 +18028,9 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
// Smoothness: the sidebar, #bg canvas, drag pill, right-anchored bars, the Onara modal and the item
|
||||
// view's grid all move together, frame by frame, with one slow curtain-like easing (same feel as the
|
||||
// legacy comment slide). Only the thumbnail-grid wrappers (index pages) still change width ONCE
|
||||
// (before the slide when opening, after it when closing): their thumbnails would re-wrap into
|
||||
// different rows mid-slide, so that single reflow is masked with an opacity fade instead.
|
||||
// legacy comment slide). Thumbnail grids (index pages) change width once at the start and their tiles
|
||||
// FLIP-glide into the re-wrapped layout over the same duration. Other padding-based wrappers still
|
||||
// change width ONCE (before the slide when opening, after it when closing), masked with an opacity fade.
|
||||
const DURATION = 600;
|
||||
const EASING = 'cubic-bezier(0.45, 0, 0.2, 1)';
|
||||
const glide = DURATION + 'ms ' + EASING;
|
||||
@@ -17881,19 +18055,33 @@ window.cancelAnimFrame = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
// Padding-based wrappers (thumbnail grids): one-time reflow, softened
|
||||
const setWrapperPadding = (w, open) => {
|
||||
w.style.setProperty('transition', 'none', 'important');
|
||||
w.style.setProperty('padding-right', open ? sidebarWidth + 'px' : '0px', 'important');
|
||||
};
|
||||
|
||||
// Wrappers holding a thumbnail grid change width right away (both directions) and their tiles glide
|
||||
// into the new layout together with the sidebar (see flipSidebarGrids)
|
||||
const gridWrappers = animateWrappers
|
||||
? Array.from(wrappers).filter(w => !isEdgeWrapper(w) && w.querySelector(SIDEBAR_GRID_ITEMS))
|
||||
: [];
|
||||
|
||||
// Other padding-based wrappers: one-time reflow, softened
|
||||
const applyLayout = (open) => {
|
||||
// Let overlays that mirror the content area (page modal) resize at exactly the same moment
|
||||
window.dispatchEvent(new CustomEvent('f0ck:sidebar-right-layout', { detail: { open, width: sidebarWidth, overlay: !animateWrappers } }));
|
||||
if (!animateWrappers) return;
|
||||
wrappers.forEach(w => {
|
||||
if (isEdgeWrapper(w)) return;
|
||||
w.style.setProperty('transition', 'none', 'important');
|
||||
w.style.setProperty('padding-right', open ? sidebarWidth + 'px' : '0px', 'important');
|
||||
if (isEdgeWrapper(w) || gridWrappers.includes(w)) return;
|
||||
setWrapperPadding(w, open);
|
||||
if (typeof w.animate === 'function') w.animate([{ opacity: 0.55 }, { opacity: 1 }], { duration: 220, easing: 'ease-out' });
|
||||
});
|
||||
};
|
||||
|
||||
if (gridWrappers.length) {
|
||||
flipSidebarGrids(gridWrappers, () => gridWrappers.forEach(w => setWrapperPadding(w, isHidden)), DURATION, EASING);
|
||||
}
|
||||
|
||||
// Item view: animate the grid's sidebar track so media and comments glide with the sidebar instead
|
||||
// of jumping when the class flips at the end. The target comes from the current track sizes
|
||||
// (last track = sidebar column, the one before it = flexible content column) rather than from
|
||||
|
||||
Reference in New Issue
Block a user