This document explains how Lychee's dynamic photo layout system works, including the four different layout algorithms and their implementation details.
Lychee provides a sophisticated layout system that dynamically arranges photo thumbnails in different visual patterns. The system supports four distinct layout modes, each optimized for different use cases and visual preferences:
- Square Layout - Uniform grid with square thumbnails
- Justified Layout - Flickr-style rows with consistent heights
- Masonry Layout - Pinterest-style columns with variable heights
- Grid Layout - Regular grid preserving aspect ratios
The layout system is built around several key files:
PhotoLayout.ts- Main layout orchestrator and configuration managementuseSquare.ts- Square grid layout implementationuseJustify.ts- Justified row layout implementationuseMasonry.ts- Masonry column layout implementationuseGrid.ts- Regular grid layout implementationgetWidth.ts- Dynamic width calculation utilitiestypes.d.ts- TypeScript interfaces and type definitions
The layout system uses a factory pattern to select and activate the appropriate layout algorithm:
function activateLayout() {
switch (layout.value) {
case "square": return useSquare(...)
case "justified": return useJustify(...)
case "masonry": return useMasonry(...)
case "grid": return useGrid(...)
}
}Purpose: Creates a uniform grid where all photos are displayed as perfect squares.
Key Characteristics:
- All thumbnails have identical square dimensions
- Photos are cropped to fit square aspect ratio
- Regular grid pattern with consistent spacing
- Optimal for clean, organized appearance
Algorithm Details:
// Calculate grid dimensions
const perChunk = Math.floor((max_width + grid_gap) / target_width_height)
const grid_width = target_width_height + spread
// Position each item in a regular grid
e.style.width = grid_width + "px"
e.style.height = grid_width + "px" // Square aspect ratio
e.style.left = column.left + "px"Configuration Parameters:
photo_layout_square_column_width- Target width/height for square thumbnailsphoto_layout_gap- Spacing between thumbnails
Use Cases:
- Instagram-style photo grids
- Portfolio presentations
- Clean, minimalist interfaces
- Equal visual weight for all photos
Purpose: Creates Flickr-style rows where photos maintain their aspect ratios while keeping row heights consistent.
Key Characteristics:
- Preserves original photo aspect ratios
- Consistent row heights across the grid
- Photos scaled to fit perfectly within rows
- Uses the
justified-layoutlibrary for optimal spacing
Algorithm Details:
// Calculate aspect ratios for all photos
const ratio: number[] = justifiedItems.map(photo => {
return height > 0 ? width / height : 1
})
// Use justified-layout library to calculate optimal positioning
const layoutGeometry = createJustifiedLayout(ratio, {
containerWidth: width,
containerPadding: 0,
targetRowHeight: photoDefaultHeight,
})
// Apply calculated dimensions and positions
e.style.width = layoutGeometry.boxes[i].width + "px"
e.style.height = layoutGeometry.boxes[i].height + "px"Configuration Parameters:
photo_layout_justified_row_height- Target height for each row (default: 320px)
Use Cases:
- Professional photo galleries
- Showcasing photography with varied aspect ratios
- Optimal space utilization
- Maintaining photo composition integrity
Purpose: Creates a Pinterest-style layout with columns of varying heights, preserving aspect ratios.
Key Characteristics:
- Preserves original photo aspect ratios
- Variable column heights create organic flow
- Photos placed in shortest available column
- Optimal for diverse photo dimensions
Algorithm Details:
// Calculate aspect ratios
const ratio = gridItems.map(photo => width / height)
// Find shortest column for placement
idx = findSmallestIdx(columns)
const column = columns[idx]
const height = grid_width / ratio[i]
// Position photo in shortest column
e.style.height = height + "px"
e.style.top = column.height + "px"
column.height = column.height + height + grid_gapColumn Selection Strategy:
function findSmallestIdx(columns: Column[]) {
// Find column with minimum height
return columns.reduce((minIdx, col, i) =>
col.height < columns[minIdx].height ? i : minIdx
, 0)
}Configuration Parameters:
photo_layout_masonry_column_width- Target width for columnsphoto_layout_gap- Spacing between photos
Use Cases:
- Pinterest-style browsing
- Mixed media galleries
- Varied photo dimensions
- Organic, flowing layouts
Purpose: Creates a regular grid where photos maintain aspect ratios within column constraints.
Key Characteristics:
- Fixed column widths with variable heights
- Preserves aspect ratios within columns
- Regular row alignment across columns
- Balanced between uniformity and aspect ratio preservation
Algorithm Details:
// Calculate photo dimensions preserving aspect ratio
const ratio = gridItems.map(photo => width / height)
const height = Math.floor(grid_width / ratio[i])
// Align photos in rows across columns
if (idx % perChunk === 0) {
const newTop = Math.max(...columns.map(column => column.height))
columns.forEach(column => column.height = newTop)
}
e.style.width = grid_width + "px"
e.style.height = height + "px"Row Synchronization: The grid layout ensures photos are aligned in rows by synchronizing column heights at the start of each new row.
Configuration Parameters:
photo_layout_grid_column_width- Target width for grid columnsphoto_layout_gap- Spacing between photos
Use Cases:
- Traditional photo galleries
- Balanced visual presentation
- Consistent column structure
- Professional portfolios
The getWidth.ts utility calculates available container width considering various UI factors:
export function getWidth(timelineData: TimelineData, route: RouteLocationNormalizedLoaded): number {
const baseWidth = window.innerWidth
const paddingLeftRight = 2 * 18
let scrollBarWidth = 15
if (isTouchDevice()) {
scrollBarWidth = 0 // Touch devices hide scrollbars
}
// Account for timeline border if visible
let timeLineBorder = 0
if (timelineData.isTimeline.value && timelineData.isLeftBorderVisible.value) {
timeLineBorder = 50
}
return baseWidth - paddingLeftRight - scrollBarWidth - timeLineBorder
}Width Factors Considered:
- Window inner width
- Left/right padding (36px total)
- Scrollbar width (15px on desktop, 0px on touch devices)
- Timeline border width (50px when timeline is active)
- Route-specific adjustments
Layout configurations are loaded dynamically from the server:
export function useGetLayoutConfig() {
const layoutConfig = ref<App.Http.Resources.GalleryConfigs.PhotoLayoutConfig>()
function loadLayoutConfig(): Promise<void> {
return AlbumService.getLayout().then((data) => {
layoutConfig.value = data.data
})
}
return { layoutConfig, loadLayoutConfig }
}Visual feedback for layout selection is provided through dynamic CSS classes:
export function useLayoutClass(layout: Ref<App.Enum.PhotoLayoutType>) {
const BASE = "my-0 w-5 h-5 mr-0 ml-0 transition-all duration-300 group-hover:scale-150"
const squareClass = computed(() =>
BASE + (layout.value === "square" ? "stroke-primary-400" : "stroke-neutral-400")
)
// Similar for justified, masonry, grid...
}All layouts support timeline mode, which affects:
- Width Calculation: Timeline border reduces available width
- Layout Positioning: Photos positioned relative to timeline border
- Visual Indicators: Timeline-specific UI elements
Timeline data structure:
export type TimelineData = {
isTimeline: Ref<boolean>
isLeftBorderVisible: Ref<boolean>
}The layout system supports right-to-left languages:
const { isLTR } = useLtRorRtL()
const align = isLTR() ? "left" : "right"
// Apply positioning based on text direction
e.style[align] = column.left + "px"All layouts use direct DOM manipulation for optimal performance:
// Filter to only element nodes (nodeType === 1)
const gridItems = [...el.childNodes].filter(gridItem => gridItem.nodeType === 1)
// Direct style property assignment
e.style.top = column.height + "px"
e.style.width = grid_width + "px"Masonry and square layouts use column-based algorithms for O(n) complexity:
// Efficient column tracking
const columns: Column[] = Array.from({ length: perChunk }, (_, idx) => ({
height: 0,
left: (grid_gap + grid_width) * idx
}))- Layouts reuse existing DOM elements
- Minimal object allocation during layout calculations
- Efficient array operations for positioning
Layouts automatically recalculate when:
- Window resizes
- Layout mode changes
- Timeline visibility toggles
- Container width changes
- Touch device detection for scrollbar width
- Responsive column counts based on available width
- Optimized touch targets for mobile interaction
Layouts work with photo thumbnail components that provide:
data-widthanddata-heightattributes for aspect ratio calculation- Absolute positioning support
- Responsive image loading
Timeline layouts coordinate with:
- Timeline border visibility
- Date separator positioning
- Scroll synchronization
- Square: Use for uniform, clean presentations
- Justified: Best for professional photo galleries
- Masonry: Ideal for varied content dimensions
- Grid: Good balance between structure and flexibility
- Layouts are applied after DOM elements are rendered
- Batch DOM updates for better performance
- Use
requestAnimationFramefor smooth transitions
- Maintain logical tab order regardless of visual layout
- Ensure adequate spacing for touch targets
- Support keyboard navigation patterns
- Frontend Architecture - Overall frontend architecture, Vue3 patterns, and state management
- Frontend Gallery Views - Gallery interface and viewing modes
- Coding Conventions - Coding standards including Vue3/TypeScript conventions
Last updated: December 22, 2025