feat(viewer): Add smooth zoom and scroll animations to viewer
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-12-11 - 2.1.0 - feat(viewer)
|
||||||
|
Add smooth zoom and scroll animations to viewer
|
||||||
|
|
||||||
|
- Introduce smooth zoom animation: new private animateZoomTo(targetZoom, duration) method and zoomAnimationId to track/cancel running animations
|
||||||
|
- Animate transitions for numeric zoom presets and zoom step changes (handleZoomStep now uses animateZoomTo)
|
||||||
|
- Cancel ongoing zoom animation when user manipulates the zoom slider
|
||||||
|
- Make fit-to-width and fit-to-page calculations optionally animate (calculateFitWidth/ calculateFitPage accept animate flag)
|
||||||
|
- Add animated scrolling helper animateScrollTo for smooth page scrolling when requested (used when smooth=true)
|
||||||
|
|
||||||
## 2025-12-11 - 2.0.6 - fix(DeDocumentViewer)
|
## 2025-12-11 - 2.0.6 - fix(DeDocumentViewer)
|
||||||
Account for toolbar and padding when calculating Fit Page zoom in viewer
|
Account for toolbar and padding when calculating Fit Page zoom in viewer
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-document',
|
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.'
|
description: 'A sophisticated framework for dynamically generating and rendering business documents like invoices with modern web technologies, featuring PDF creation, templating, and automation.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-document',
|
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.'
|
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()
|
@state()
|
||||||
accessor thumbnailPages: number[] = [];
|
accessor thumbnailPages: number[] = [];
|
||||||
|
|
||||||
|
// Zoom animation
|
||||||
|
private zoomAnimationId: number | null = null;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
@@ -905,31 +908,69 @@ export class DeDocumentViewer extends DeesElement {
|
|||||||
this.updateDisplayZoom();
|
this.updateDisplayZoom();
|
||||||
} else if (value === "fit-width") {
|
} else if (value === "fit-width") {
|
||||||
// Calculate zoom to fit page width to viewport width
|
// Calculate zoom to fit page width to viewport width
|
||||||
this.calculateFitWidth();
|
this.calculateFitWidth(true);
|
||||||
} else if (value === "fit-page") {
|
} else if (value === "fit-page") {
|
||||||
// Calculate zoom to fit entire page in viewport
|
// Calculate zoom to fit entire page in viewport
|
||||||
this.calculateFitPage();
|
this.calculateFitPage(true);
|
||||||
} else {
|
} else {
|
||||||
this.zoomLevel = value;
|
// Animate to numeric preset
|
||||||
this.displayZoom = value;
|
this.animateZoomTo(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleZoomStep(delta: number): void {
|
private handleZoomStep(delta: number): void {
|
||||||
const current = this.zoomLevel ?? this.displayZoom;
|
const current = this.zoomLevel ?? this.displayZoom;
|
||||||
const newZoom = Math.min(400, Math.max(25, current + delta));
|
const newZoom = Math.min(400, Math.max(25, current + delta));
|
||||||
this.zoomLevel = newZoom;
|
this.animateZoomTo(newZoom);
|
||||||
this.displayZoom = newZoom;
|
|
||||||
this.zoomMode = newZoom;
|
this.zoomMode = newZoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleZoomSlider(e: Event): void {
|
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);
|
const value = parseInt((e.target as HTMLInputElement).value, 10);
|
||||||
this.zoomLevel = value;
|
this.zoomLevel = value;
|
||||||
this.displayZoom = value;
|
this.displayZoom = value;
|
||||||
this.zoomMode = 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 {
|
private handleSpacingPreset(value: number): void {
|
||||||
this.pageGap = value;
|
this.pageGap = value;
|
||||||
}
|
}
|
||||||
@@ -1083,19 +1124,24 @@ export class DeDocumentViewer extends DeesElement {
|
|||||||
setTimeout(cleanup, 100);
|
setTimeout(cleanup, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateFitWidth(): void {
|
private calculateFitWidth(animate: boolean = false): void {
|
||||||
const viewport = this.shadowRoot?.querySelector(".viewport");
|
const viewport = this.shadowRoot?.querySelector(".viewport");
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
|
|
||||||
// Account for padding and scrollbar width
|
// Account for padding and scrollbar width
|
||||||
const viewportWidth = viewport.clientWidth - 32 - 16;
|
const viewportWidth = viewport.clientWidth - 32 - 16;
|
||||||
const scale = viewportWidth / plugins.shared.A4_WIDTH;
|
const scale = viewportWidth / plugins.shared.A4_WIDTH;
|
||||||
|
const targetZoom = Math.round(scale * 100);
|
||||||
|
|
||||||
this.zoomLevel = Math.round(scale * 100);
|
if (animate) {
|
||||||
this.displayZoom = this.zoomLevel;
|
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");
|
const viewport = this.shadowRoot?.querySelector(".viewport");
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
|
|
||||||
@@ -1110,9 +1156,14 @@ export class DeDocumentViewer extends DeesElement {
|
|||||||
const scaleByHeight = viewportHeight / plugins.shared.A4_HEIGHT;
|
const scaleByHeight = viewportHeight / plugins.shared.A4_HEIGHT;
|
||||||
const scaleByWidth = viewportWidth / plugins.shared.A4_WIDTH;
|
const scaleByWidth = viewportWidth / plugins.shared.A4_WIDTH;
|
||||||
const scale = Math.min(scaleByHeight, scaleByWidth);
|
const scale = Math.min(scaleByHeight, scaleByWidth);
|
||||||
|
const targetZoom = Math.round(scale * 100);
|
||||||
|
|
||||||
this.zoomLevel = Math.round(scale * 100);
|
if (animate) {
|
||||||
this.displayZoom = this.zoomLevel;
|
this.animateZoomTo(targetZoom);
|
||||||
|
} else {
|
||||||
|
this.zoomLevel = targetZoom;
|
||||||
|
this.displayZoom = targetZoom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDisplayZoom(): void {
|
private updateDisplayZoom(): void {
|
||||||
@@ -1297,14 +1348,42 @@ export class DeDocumentViewer extends DeesElement {
|
|||||||
const pageRect = targetPage.getBoundingClientRect();
|
const pageRect = targetPage.getBoundingClientRect();
|
||||||
const pageOffsetFromDoc = pageRect.top - docRect.top;
|
const pageOffsetFromDoc = pageRect.top - docRect.top;
|
||||||
|
|
||||||
// Scroll to the page position
|
// Scroll to the page position with fast custom animation
|
||||||
viewport.scrollTo({
|
if (smooth) {
|
||||||
top: pageOffsetFromDoc,
|
this.animateScrollTo(viewport, pageOffsetFromDoc, 250);
|
||||||
behavior: smooth ? "smooth" : "instant",
|
} 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
|
* Trigger the print dialog
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user