File size: 10,919 Bytes
70ceb8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// AspectRatioUtils.js - Shared utilities for aspect ratio calculations and rendering

/**
 * Utility class for aspect ratio calculations and icon generation
 * Used by AspectRatioSelector and PresetManagerDialog
 */
export class AspectRatioUtils {
    /**
     * Calculate aspect ratio from width and height
     * @param {number} width - Width in pixels
     * @param {number} height - Height in pixels
     * @returns {string} Aspect ratio string (e.g., "16:9")
     */
    static calculateAspectRatio(width, height) {
        const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
        const divisor = gcd(width, height);
        const w = width / divisor;
        const h = height / divisor;
        
        // Round to common aspect ratios
        const ratio = w / h;
        const commonRatios = {
            '21:9': 21/9,
            '16:9': 16/9,
            '3:2': 3/2,
            '4:3': 4/3,
            '5:4': 5/4,
            '1:1': 1,
            '4:5': 4/5,
            '3:4': 3/4,
            '2:3': 2/3,
            '9:16': 9/16,
            '9:21': 9/21
        };
        
        // Find closest common ratio (within 2% tolerance)
        let closestRatio = `${w}:${h}`;
        let minDiff = Infinity;
        
        for (const [name, value] of Object.entries(commonRatios)) {
            const diff = Math.abs(ratio - value);
            if (diff < minDiff && diff < 0.02) {
                minDiff = diff;
                closestRatio = name;
            }
        }
        
        return closestRatio;
    }

    /**
     * Dynamically generate SVG icon for aspect ratio
     * @param {number} width - Width in pixels
     * @param {number} height - Height in pixels
     * @returns {string} SVG markup as string
     */
    static getAspectRatioIcon(width, height) {
        const iconSize = 20;
        const padding = 2;
        const maxDim = iconSize - padding * 2;
        
        const aspect = width / height;
        
        let rectWidth, rectHeight;
        
        if (aspect > 1) {
            rectWidth = maxDim;
            rectHeight = maxDim / aspect;
        } else {
            rectHeight = maxDim;
            rectWidth = maxDim * aspect;
        }
        
        const x = (iconSize - rectWidth) / 2;
        const y = (iconSize - rectHeight) / 2;
        const radius = Math.min(2, rectWidth / 4, rectHeight / 4);
        
        return `<svg width="20" height="20" viewBox="0 0 ${iconSize} ${iconSize}" fill="none">
            <rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${rectWidth.toFixed(2)}" height="${rectHeight.toFixed(2)}" 
                  rx="${radius.toFixed(2)}" stroke="currentColor" stroke-width="1.5" fill="none" opacity="0.9"/>
        </svg>`;
    }

    /**
     * Group presets by aspect ratio
     * @param {Object} presets - Object with preset names as keys and {width, height} as values
     * @returns {Object} Grouped presets by aspect ratio, sorted
     */
    static groupPresetsByAspectRatio(presets) {
        const grouped = {};
        
        for (const [name, dimensions] of Object.entries(presets)) {
            const ratio = this.calculateAspectRatio(dimensions.width, dimensions.height);
            
            if (!grouped[ratio]) {
                grouped[ratio] = [];
            }
            
            grouped[ratio].push({
                name,
                width: dimensions.width,
                height: dimensions.height,
                pixels: dimensions.width * dimensions.height,
                isCustom: dimensions.isCustom || false,
                isHidden: dimensions.isHidden || false
            });
        }
        
        // Sort presets within each group by pixel count (descending)
        for (const ratio in grouped) {
            grouped[ratio].sort((a, b) => b.pixels - a.pixels);
        }
        
        // Sort aspect ratios by their numeric value (landscape to portrait)
        const sortedGrouped = {};
        const sortedRatios = Object.keys(grouped).sort((a, b) => {
            const [aw, ah] = a.split(':').map(Number);
            const [bw, bh] = b.split(':').map(Number);
            return (bw / bh) - (aw / ah);
        });
        
        for (const ratio of sortedRatios) {
            sortedGrouped[ratio] = grouped[ratio];
        }
        
        return sortedGrouped;
    }

    /**
     * Creates a preset column with list of presets for a given aspect ratio
     * Unified method used by both AspectRatioSelector and PresetAddViewRenderer
     * @param {string} ratio - Aspect ratio string (e.g., "16:9")
     * @param {Array} presetList - Array of preset objects
     * @param {Object} options - Configuration options
     * @param {Function} options.renderPresetItem - Function to render individual preset item
     * @returns {HTMLElement} Column element
     */
    static createPresetColumn(ratio, presetList, options = {}) {
        const column = document.createElement('div');
        column.className = 'resolution-master-aspect-ratio-column';

        // Icon at the top
        const firstPreset = presetList[0];
        const iconContainer = document.createElement('div');
        iconContainer.className = 'resolution-master-aspect-ratio-column-icon';
        iconContainer.innerHTML = this.getAspectRatioIcon(firstPreset.width, firstPreset.height);
        column.appendChild(iconContainer);

        // Ratio text below icon
        const ratioText = document.createElement('div');
        ratioText.className = 'resolution-master-aspect-ratio-column-title';
        ratioText.textContent = ratio;
        column.appendChild(ratioText);

        // Preset list (vertical scrollable)
        const presetListContainer = document.createElement('div');
        presetListContainer.className = 'resolution-master-aspect-ratio-column-list';

        // Render each preset using provided function
        presetList.forEach(preset => {
            const presetItem = options.renderPresetItem 
                ? options.renderPresetItem(preset)
                : this.createDefaultPresetItem(preset, options);
            presetListContainer.appendChild(presetItem);
        });

        column.appendChild(presetListContainer);
        return column;
    }

    /**
     * Creates a default preset item (used by AspectRatioSelector)
     * @param {Object} preset - Preset object
     * @param {Object} options - Configuration options
     * @returns {HTMLElement} Preset item element
     */
    static createDefaultPresetItem(preset, options = {}) {
        const { selectedPreset, customPresetIcon, onPresetClick } = options;
        const isSelected = selectedPreset === preset.name;
        
        const presetItem = document.createElement('div');
        presetItem.className = 'resolution-master-aspect-ratio-preset-item' + (isSelected ? ' selected' : '');
        presetItem.setAttribute('data-preset-item', 'true');
        presetItem.setAttribute('data-preset-name', preset.name);
        presetItem.setAttribute('data-preset-dimensions', `${preset.width}×${preset.height}`);

        // Preset name with custom icon if applicable
        const nameDiv = document.createElement('div');
        nameDiv.className = 'resolution-master-aspect-ratio-preset-item-name';
        const customIcon = preset.isCustom && customPresetIcon ? 
            `<img src="${customPresetIcon.src}" class="resolution-master-aspect-ratio-preset-custom-icon">` : '';
        nameDiv.innerHTML = `${preset.name}${customIcon}`;
        
        // Dimensions below name
        const dimensionsDiv = document.createElement('div');
        dimensionsDiv.className = 'resolution-master-aspect-ratio-preset-item-dims';
        dimensionsDiv.textContent = `${preset.width}×${preset.height}`;

        presetItem.appendChild(nameDiv);
        presetItem.appendChild(dimensionsDiv);

        // Click handler
        if (onPresetClick) {
            presetItem.addEventListener('click', () => onPresetClick(preset.name));
        }

        return presetItem;
    }

    /**
     * Updates scroll indicators for columns (adds "↓ Scroll for more" if needed)
     * @param {Array} columns - Array of column elements
     */
    static updateColumnScrollIndicators(columns) {
        columns.forEach(column => {
            // Preset list container is the 3rd child (after icon and ratio text)
            const presetListContainer = column.children[2];
            let scrollIndicator = column.children[3];
            
            const needsIndicator = presetListContainer && 
                                  presetListContainer.scrollHeight > presetListContainer.clientHeight;
            
            if (needsIndicator && !scrollIndicator) {
                scrollIndicator = document.createElement('div');
                scrollIndicator.className = 'resolution-master-aspect-ratio-column-scroll-indicator';
                scrollIndicator.textContent = '↓ Scroll for more';
                column.appendChild(scrollIndicator);
            } else if (!needsIndicator && scrollIndicator) {
                column.removeChild(scrollIndicator);
            }
        });
    }

    /**
     * Creates and manages horizontal scroll indicator ("→ Scroll right for more")
     * @param {HTMLElement} scrollWrapper - The scrollable wrapper element
     * @param {HTMLElement} container - The parent container to append indicator to
     * @param {Object} state - Object to store indicator reference {indicator: null}
     * @returns {Function} Update function to call when checking if indicator is needed
     */
    static createHorizontalScrollManager(scrollWrapper, container, state) {
        const updateIndicator = () => {
            const needsIndicator = scrollWrapper.scrollWidth > scrollWrapper.clientWidth;
            
            if (needsIndicator && !state.indicator) {
                // Create and add indicator
                state.indicator = document.createElement('div');
                state.indicator.className = 'resolution-master-aspect-ratio-horizontal-scroll-indicator';
                state.indicator.textContent = '→ Scroll right for more';
                
                // Enable horizontal scrolling with mouse wheel over the indicator
                state.indicator.addEventListener('wheel', (e) => {
                    e.preventDefault();
                    scrollWrapper.scrollLeft += e.deltaY;
                }, { passive: false });
                
                container.appendChild(state.indicator);
            } else if (!needsIndicator && state.indicator) {
                // Remove indicator (with safety check to prevent errors)
                if (state.indicator.parentNode === container) {
                    container.removeChild(state.indicator);
                }
                state.indicator = null;
            }
        };
        
        return updateIndicator;
    }
}