feat(viewer): Add smooth zoom and scroll animations to viewer
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-document',
|
||||
version: '2.0.6',
|
||||
version: '2.1.0',
|
||||
description: 'A sophisticated framework for dynamically generating and rendering business documents like invoices with modern web technologies, featuring PDF creation, templating, and automation.'
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ export class DeDocumentViewer extends DeesElement {
|
||||
@state()
|
||||
accessor thumbnailPages: number[] = [];
|
||||
|
||||
// Zoom animation
|
||||
private zoomAnimationId: number | null = null;
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
@@ -905,31 +908,69 @@ export class DeDocumentViewer extends DeesElement {
|
||||
this.updateDisplayZoom();
|
||||
} else if (value === "fit-width") {
|
||||
// Calculate zoom to fit page width to viewport width
|
||||
this.calculateFitWidth();
|
||||
this.calculateFitWidth(true);
|
||||
} else if (value === "fit-page") {
|
||||
// Calculate zoom to fit entire page in viewport
|
||||
this.calculateFitPage();
|
||||
this.calculateFitPage(true);
|
||||
} else {
|
||||
this.zoomLevel = value;
|
||||
this.displayZoom = value;
|
||||
// Animate to numeric preset
|
||||
this.animateZoomTo(value);
|
||||
}
|
||||
}
|
||||
|
||||
private handleZoomStep(delta: number): void {
|
||||
const current = this.zoomLevel ?? this.displayZoom;
|
||||
const newZoom = Math.min(400, Math.max(25, current + delta));
|
||||
this.zoomLevel = newZoom;
|
||||
this.displayZoom = newZoom;
|
||||
this.animateZoomTo(newZoom);
|
||||
this.zoomMode = newZoom;
|
||||
}
|
||||
|
||||
private handleZoomSlider(e: Event): void {
|
||||
// Cancel any running animation when user takes manual control
|
||||
if (this.zoomAnimationId !== null) {
|
||||
cancelAnimationFrame(this.zoomAnimationId);
|
||||
this.zoomAnimationId = null;
|
||||
}
|
||||
const value = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
this.zoomLevel = value;
|
||||
this.displayZoom = value;
|
||||
this.zoomMode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate zoom level to target value
|
||||
* @param targetZoom - Target zoom percentage
|
||||
* @param duration - Animation duration in ms (default: 200)
|
||||
*/
|
||||
private animateZoomTo(targetZoom: number, duration: number = 200): void {
|
||||
// Cancel any existing animation
|
||||
if (this.zoomAnimationId !== null) {
|
||||
cancelAnimationFrame(this.zoomAnimationId);
|
||||
}
|
||||
|
||||
const startZoom = this.zoomLevel ?? this.displayZoom;
|
||||
const startTime = performance.now();
|
||||
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
|
||||
const currentZoom = startZoom + (targetZoom - startZoom) * easedProgress;
|
||||
this.zoomLevel = Math.round(currentZoom);
|
||||
this.displayZoom = this.zoomLevel;
|
||||
|
||||
if (progress < 1) {
|
||||
this.zoomAnimationId = requestAnimationFrame(animate);
|
||||
} else {
|
||||
this.zoomAnimationId = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.zoomAnimationId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
private handleSpacingPreset(value: number): void {
|
||||
this.pageGap = value;
|
||||
}
|
||||
@@ -1083,19 +1124,24 @@ export class DeDocumentViewer extends DeesElement {
|
||||
setTimeout(cleanup, 100);
|
||||
}
|
||||
|
||||
private calculateFitWidth(): void {
|
||||
private calculateFitWidth(animate: boolean = false): void {
|
||||
const viewport = this.shadowRoot?.querySelector(".viewport");
|
||||
if (!viewport) return;
|
||||
|
||||
// Account for padding and scrollbar width
|
||||
const viewportWidth = viewport.clientWidth - 32 - 16;
|
||||
const scale = viewportWidth / plugins.shared.A4_WIDTH;
|
||||
const targetZoom = Math.round(scale * 100);
|
||||
|
||||
this.zoomLevel = Math.round(scale * 100);
|
||||
this.displayZoom = this.zoomLevel;
|
||||
if (animate) {
|
||||
this.animateZoomTo(targetZoom);
|
||||
} else {
|
||||
this.zoomLevel = targetZoom;
|
||||
this.displayZoom = targetZoom;
|
||||
}
|
||||
}
|
||||
|
||||
private calculateFitPage(): void {
|
||||
private calculateFitPage(animate: boolean = false): void {
|
||||
const viewport = this.shadowRoot?.querySelector(".viewport");
|
||||
if (!viewport) return;
|
||||
|
||||
@@ -1110,9 +1156,14 @@ export class DeDocumentViewer extends DeesElement {
|
||||
const scaleByHeight = viewportHeight / plugins.shared.A4_HEIGHT;
|
||||
const scaleByWidth = viewportWidth / plugins.shared.A4_WIDTH;
|
||||
const scale = Math.min(scaleByHeight, scaleByWidth);
|
||||
const targetZoom = Math.round(scale * 100);
|
||||
|
||||
this.zoomLevel = Math.round(scale * 100);
|
||||
this.displayZoom = this.zoomLevel;
|
||||
if (animate) {
|
||||
this.animateZoomTo(targetZoom);
|
||||
} else {
|
||||
this.zoomLevel = targetZoom;
|
||||
this.displayZoom = targetZoom;
|
||||
}
|
||||
}
|
||||
|
||||
private updateDisplayZoom(): void {
|
||||
@@ -1297,14 +1348,42 @@ export class DeDocumentViewer extends DeesElement {
|
||||
const pageRect = targetPage.getBoundingClientRect();
|
||||
const pageOffsetFromDoc = pageRect.top - docRect.top;
|
||||
|
||||
// Scroll to the page position
|
||||
viewport.scrollTo({
|
||||
top: pageOffsetFromDoc,
|
||||
behavior: smooth ? "smooth" : "instant",
|
||||
});
|
||||
// Scroll to the page position with fast custom animation
|
||||
if (smooth) {
|
||||
this.animateScrollTo(viewport, pageOffsetFromDoc, 250);
|
||||
} else {
|
||||
viewport.scrollTop = pageOffsetFromDoc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate scroll to target position
|
||||
* @param element - The scrollable element
|
||||
* @param targetTop - Target scroll position
|
||||
* @param duration - Animation duration in ms (default: 150)
|
||||
*/
|
||||
private animateScrollTo(element: Element, targetTop: number, duration: number = 150): void {
|
||||
const startTop = element.scrollTop;
|
||||
const distance = targetTop - startTop;
|
||||
const startTime = performance.now();
|
||||
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
|
||||
|
||||
const step = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const easedProgress = easeOutCubic(progress);
|
||||
|
||||
element.scrollTop = startTop + distance * easedProgress;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the print dialog
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user