返回 presentation-ai
useImageDimensions.ts
1 import { useEffect, useState } from "react";
2
3 import {
4 BASE_HEIGHT,
5 BASE_WIDTH_PERCENTAGE,
6 } from "@/hooks/presentation/useRootImageActions";
7 import { type RootImage as RootImageType } from "../../../utils/parser";
8
9 const MAX_HEIGHT_RATIO_WITH_WINDOW = 0.6;
10 const TOTAL_PADDING_FROM_SHEET = 48;
11 const HORIZONTAL_PADDING = 48; // Padding for horizontal images (vertical layout)
12 const DEFAULT_DIMENSIONS = { width: 800, height: 450 };
13
14 interface UseImageDimensionsProps {
15 element: RootImageType;
16 slideId?: string;
17 layoutType: string;
18 }
19
20 export interface ImageDimensions {
21 width: number;
22 height: number;
23 scale: number;
24 }
25
26 function computeScale(
27 dimensions: { width: number; height: number },
28 layoutType: string,
29 ) {
30 const maxHeight = window.innerHeight * MAX_HEIGHT_RATIO_WITH_WINDOW;
31 let availableWidth = window.innerWidth * 0.45 - TOTAL_PADDING_FROM_SHEET;
32
33 if (layoutType === "vertical") {
34 availableWidth *= 0.85;
35 }
36
37 const heightFits = dimensions.height <= maxHeight;
38 const widthFits = dimensions.width <= availableWidth;
39
40 if (heightFits && widthFits) {
41 return 1;
42 }
43
44 const heightScale = maxHeight / dimensions.height;
45 const widthScale = availableWidth / dimensions.width;
46 return Math.min(heightScale, widthScale, 1);
47 }
48
49 /**
50 * Custom hook to calculate image dimensions and scale for the presentation image editor.
51 * Handles both vertical (horizontal images) and horizontal (vertical images) layouts.
52 */
53 export function useImageDimensions({
54 element,
55 slideId,
56 layoutType,
57 }: UseImageDimensionsProps): ImageDimensions {
58 const [dimensions, setDimensions] = useState<{
59 width: number;
60 height: number;
61 }>(DEFAULT_DIMENSIONS);
62 const [scale, setScale] = useState(1);
63
64 useEffect(() => {
65 if (typeof window === "undefined" || typeof document === "undefined") {
66 return;
67 }
68
69 let resizeObserver: ResizeObserver | null = null;
70 let animationFrameId: number | null = null;
71 let retryTimeoutId: number | null = null;
72 let observedContainer: Element | null = null;
73
74 const selector = slideId ? `.slide-container-${slideId}` : null;
75 const supportsResizeObserver = typeof window.ResizeObserver !== "undefined";
76
77 const getDimensionsFromContainer = (
78 container: Element,
79 ): { width: number; height: number } => {
80 const parentRect = container.getBoundingClientRect();
81 const parentWidth = parentRect.width;
82 const parentHeight = parentRect.height;
83
84 const BASE_WIDTH_PERCENTAGE_NUMERICAL =
85 parseFloat(BASE_WIDTH_PERCENTAGE) / 100;
86 let actualWidth: number = parentWidth * BASE_WIDTH_PERCENTAGE_NUMERICAL;
87 let actualHeight: number = BASE_HEIGHT;
88
89 if (layoutType === "vertical") {
90 actualHeight = element.size?.h ?? BASE_HEIGHT;
91 actualWidth = Math.max(parentWidth - HORIZONTAL_PADDING, 0);
92 } else {
93 actualWidth =
94 parentWidth *
95 (parseFloat(element.size?.w ?? BASE_WIDTH_PERCENTAGE) / 100);
96 actualHeight = parentHeight;
97 }
98
99 return { width: actualWidth, height: actualHeight };
100 };
101
102 const updateMeasurements = () => {
103 if (animationFrameId !== null) {
104 cancelAnimationFrame(animationFrameId);
105 }
106
107 animationFrameId = requestAnimationFrame(() => {
108 const slideContainer = selector
109 ? document.querySelector(selector)
110 : null;
111
112 if (!slideContainer) {
113 setDimensions(DEFAULT_DIMENSIONS);
114 setScale(computeScale(DEFAULT_DIMENSIONS, layoutType));
115
116 if (retryTimeoutId !== null) {
117 clearTimeout(retryTimeoutId);
118 }
119 retryTimeoutId = window.setTimeout(() => {
120 retryTimeoutId = null;
121 updateMeasurements();
122 }, 200);
123
124 return;
125 }
126
127 if (supportsResizeObserver && observedContainer !== slideContainer) {
128 resizeObserver?.disconnect();
129 resizeObserver = new ResizeObserver(() => updateMeasurements());
130 resizeObserver.observe(slideContainer);
131 observedContainer = slideContainer;
132 }
133
134 const nextDimensions = getDimensionsFromContainer(slideContainer);
135
136 setDimensions((prev) =>
137 prev.width === nextDimensions.width &&
138 prev.height === nextDimensions.height
139 ? prev
140 : nextDimensions,
141 );
142 setScale(computeScale(nextDimensions, layoutType));
143 });
144 };
145
146 updateMeasurements();
147 window.addEventListener("resize", updateMeasurements);
148
149 return () => {
150 window.removeEventListener("resize", updateMeasurements);
151 resizeObserver?.disconnect();
152
153 if (animationFrameId !== null) {
154 cancelAnimationFrame(animationFrameId);
155 }
156
157 if (retryTimeoutId !== null) {
158 clearTimeout(retryTimeoutId);
159 }
160 };
161 }, [slideId, layoutType, element.size?.h, element.size?.w]);
162
163 return {
164 width: dimensions.width,
165 height: dimensions.height,
166 scale,
167 };
168 }
169
169 lines TYPESCRIPT