mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 06:48:12 +08:00
Add a jostle hint to the website main carousel
This commit is contained in:
@@ -64,21 +64,21 @@ js = ["/image-interaction.js", "/fundraising.js", "/video-embed.js"]
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ▛ SCREENSHOTS ▜ -->
|
<!-- ▛ SCREENSHOTS ▜ -->
|
||||||
<section id="screenshots" class="carousel window-size-1" data-carousel>
|
<section id="screenshots" class="carousel window-size-1" data-carousel data-carousel-jostle-hint>
|
||||||
<div class="carousel-slide">
|
<div class="carousel-slide">
|
||||||
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #1" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #1" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #2" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #2" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #3" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="Graphite UI image #3" data-carousel-image />
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-slide torn left">
|
<div class="carousel-slide torn left">
|
||||||
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-slide torn right">
|
<div class="carousel-slide torn right">
|
||||||
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-demo-valley-of-spires__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-nodes__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
<img src="https://static.graphite.rs/content/index/gui-mockup-viewport__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" alt="" data-carousel-image />
|
||||||
</div>
|
</div>
|
||||||
<div class="screenshot-details">
|
<div class="screenshot-details">
|
||||||
<div class="carousel-controls">
|
<div class="carousel-controls">
|
||||||
@@ -103,7 +103,7 @@ js = ["/image-interaction.js", "/fundraising.js", "/video-embed.js"]
|
|||||||
<em>Valley of Spires</em> — <a href="https://editor.graphite.rs/#demo/valley-of-spires">Open this artwork</a> to explore it yourself.
|
<em>Valley of Spires</em> — <a href="https://editor.graphite.rs/#demo/valley-of-spires">Open this artwork</a> to explore it yourself.
|
||||||
</p>
|
</p>
|
||||||
<p data-carousel-description>
|
<p data-carousel-description>
|
||||||
Design mockup for the work-in-progress node graph raster editing pipeline. Some raster nodes shown here are not implemented yet.
|
Design mockup for the work-in-progress node graph raster editing pipeline. Some UI concepts and raster nodes shown here are not implemented yet.
|
||||||
</p>
|
</p>
|
||||||
<p data-carousel-description>
|
<p data-carousel-description>
|
||||||
Design mockup for the work-in-progress viewport raster editing workflow. Some features shown here are not implemented yet.
|
Design mockup for the work-in-progress viewport raster editing workflow. Some features shown here are not implemented yet.
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ function initializeCarousel() {
|
|||||||
const directionNext = carouselContainer.querySelector("[data-carousel-next]");
|
const directionNext = carouselContainer.querySelector("[data-carousel-next]");
|
||||||
const dots = carouselContainer.querySelectorAll("[data-carousel-dot]");
|
const dots = carouselContainer.querySelectorAll("[data-carousel-dot]");
|
||||||
const descriptions = carouselContainer.querySelectorAll("[data-carousel-description]");
|
const descriptions = carouselContainer.querySelectorAll("[data-carousel-description]");
|
||||||
|
const performJostleHint = carouselContainer.hasAttribute("data-carousel-jostle-hint");
|
||||||
const dragLastClientX = undefined;
|
const dragLastClientX = undefined;
|
||||||
const velocityDeltaWindow = Array.from({ length: FLING_VELOCITY_WINDOW_SIZE }, () => ({ time: 0, delta: 0 }));
|
const velocityDeltaWindow = Array.from({ length: FLING_VELOCITY_WINDOW_SIZE }, () => ({ time: 0, delta: 0 }));
|
||||||
|
const jostleNoLongerNeeded = false;
|
||||||
|
|
||||||
const carousel = {
|
const carousel = {
|
||||||
carouselContainer,
|
carouselContainer,
|
||||||
@@ -33,6 +35,7 @@ function initializeCarousel() {
|
|||||||
descriptions,
|
descriptions,
|
||||||
dragLastClientX,
|
dragLastClientX,
|
||||||
velocityDeltaWindow,
|
velocityDeltaWindow,
|
||||||
|
jostleNoLongerNeeded,
|
||||||
};
|
};
|
||||||
carousels.push(carousel);
|
carousels.push(carousel);
|
||||||
|
|
||||||
@@ -47,6 +50,42 @@ function initializeCarousel() {
|
|||||||
slideTo(carousel, index, true);
|
slideTo(carousel, index, true);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Jostle hint is a feature to briefly shift the carousel by a bit as a hint to users that it can be interacted with
|
||||||
|
if (performJostleHint) {
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.intersectionRatio === 1 && currentTransform(carousel) === 0 && !carousel.jostleNoLongerNeeded) {
|
||||||
|
const JOSTLE_TIME = 1000;
|
||||||
|
const MAX_JOSTLE_DISTANCE = -10;
|
||||||
|
|
||||||
|
let startTime;
|
||||||
|
const buildUp = (timeStep) => {
|
||||||
|
if (carousel.jostleNoLongerNeeded) return;
|
||||||
|
|
||||||
|
if (!startTime) startTime = timeStep;
|
||||||
|
const elapsedTime = timeStep - startTime;
|
||||||
|
|
||||||
|
const easeOutCirc = (x) => Math.sqrt(1 - Math.pow(x - 1, 2));
|
||||||
|
const movementFactor = easeOutCirc(Math.min(1, elapsedTime / JOSTLE_TIME));
|
||||||
|
|
||||||
|
setCurrentTransform(carousel, movementFactor * MAX_JOSTLE_DISTANCE, "%", false, true);
|
||||||
|
|
||||||
|
if (elapsedTime < JOSTLE_TIME) {
|
||||||
|
requestAnimationFrame(buildUp);
|
||||||
|
} else {
|
||||||
|
carousel.jostleNoLongerNeeded = true;
|
||||||
|
slideTo(carousel, 0, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(buildUp);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, { threshold: 1 })
|
||||||
|
.observe(directionPrev);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,11 +120,16 @@ function currentTransform(carousel) {
|
|||||||
return Number(currentTransformMatrix.split(",")[4] || "0");
|
return Number(currentTransformMatrix.split(",")[4] || "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrentTransform(carousel, x, unit, smooth) {
|
function setCurrentTransform(carousel, x, unit, smooth, doNotTerminateJostle = false) {
|
||||||
|
const xInitial = currentTransform(carousel);
|
||||||
|
|
||||||
Array.from(carousel.images).forEach((image) => {
|
Array.from(carousel.images).forEach((image) => {
|
||||||
image.style.transitionTimingFunction = smooth ? "ease-in-out" : "cubic-bezier(0, 0, 0.2, 1)";
|
image.style.transitionTimingFunction = smooth ? "ease-in-out" : "cubic-bezier(0, 0, 0.2, 1)";
|
||||||
image.style.transform = `translateX(${x}${unit})`;
|
image.style.transform = `translateX(${x}${unit})`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the user caused the carousel to move, we can assume they know how to use it and don't need the jostle hint anymore
|
||||||
|
if (!doNotTerminateJostle && x !== xInitial) carousel.jostleNoLongerNeeded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentClosestImageIndex(carousel) {
|
function currentClosestImageIndex(carousel) {
|
||||||
|
|||||||
Reference in New Issue
Block a user