# UI Adaptivity System Comprehensive documentation for the Tour Builder Platform's responsive canvas scaling and UI adaptivity system. ## Overview The platform uses a **Canvas Units** system to ensure UI elements scale proportionally across all viewport sizes while maintaining a consistent design. This system allows content authored at a design resolution (e.g., 1920×1080) to display correctly on any screen size. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ UI Adaptivity Architecture │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Configuration Layer │ │ │ │ │ │ │ │ canvas.config.ts │ │ │ │ ├── defaults: { width: 1920, height: 1080 } │ │ │ │ ├── scaling: { mode: 'fit', minScale: 0.1, maxScale: 4.0 } │ │ │ │ └── cssVars: { scale: '--canvas-scale', unit: '--cu', ... } │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Calculation Layer │ │ │ │ │ │ │ │ canvasScale.ts (utilities) │ │ │ │ ├── calculateCanvasScale(viewport, design) → scale factor │ │ │ │ ├── toCU(designPixels) → "calc(N * var(--cu, 1px))" │ │ │ │ ├── normalizeToCanvasUnits(value) → canvas unit expression │ │ │ │ └── getCanvasCssVars(scale) → CSS custom properties object │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Hook/Context Layer │ │ │ │ │ │ │ │ useCanvasScale (hook) │ CanvasScaleContext (context) │ │ │ │ ├── scale │ ├── Same properties │ │ │ │ ├── cssVars │ ├── Provider pattern │ │ │ │ ├── letterboxStyles │ └── Optional hook variant │ │ │ │ └── showRotatePrompt │ │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Application Layer │ │ │ │ │ │ │ │ RuntimePresentation │ Constructor │ │ │ │ ├── cssVars on root │ ├── cssVars on canvas │ │ │ │ ├── letterboxStyles │ ├── letterboxStyles │ │ │ │ └── Elements use --cu │ └── Elements use --cu │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## Core Concepts ### Canvas Units (--cu) The `--cu` CSS custom property is the foundation of the adaptivity system. It represents "1 design pixel" that scales with the viewport. ``` At design resolution (1920×1080 on 1920×1080 viewport): --cu = 1px At 4K (1920×1080 on 3840×2160 viewport): --cu = 2px (elements render 2x larger) At half-resolution (1920×1080 on 960×540 viewport): --cu = 0.5px (elements render at half size) ``` **Usage:** ```css /* Direct CSS usage */ .element { font-size: calc(24 * var(--cu, 1px)); /* 24px at design scale */ padding: calc(16 * var(--cu, 1px)); border-radius: calc(8 * var(--cu, 1px)); } ``` ```typescript // JavaScript usage via toCU() import { toCU } from '../lib/canvasScale'; const style = { fontSize: toCU(24), // "calc(24 * var(--cu, 1px))" padding: toCU(16), // "calc(16 * var(--cu, 1px))" borderRadius: toCU(8), // "calc(8 * var(--cu, 1px))" }; ``` ### Scale Factor Calculation The scale factor is calculated to fit the design canvas within the viewport while maintaining aspect ratio: ```typescript // canvasScale.ts export function calculateCanvasScale( viewportWidth: number, viewportHeight: number, designWidth: number = 1920, designHeight: number = 1080, ): number { const scaleX = viewportWidth / designWidth; const scaleY = viewportHeight / designHeight; // Use min() to fit content within viewport (letterbox/pillarbox) const scale = Math.min(scaleX, scaleY); // Clamp to configured min/max (0.1 - 4.0) return Math.max(0.1, Math.min(4.0, scale)); } ``` ### Letterbox Mode When the viewport aspect ratio doesn't match the design aspect ratio, the system creates black bars (letterbox for horizontal bars, pillarbox for vertical bars) to maintain content proportions. ```typescript // Generated letterbox styles const letterboxStyles: CSSProperties = { width: designWidth * scale, // Actual canvas width height: designHeight * scale, // Actual canvas height position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', // Center in viewport }; ``` **Visual Example:** ``` ┌─────────────────────────────────────────┐ │░░░░░░░░░░░░░ Letterbox Bar ░░░░░░░░░░░░│ ← Black bar (wider viewport) ├─────────────────────────────────────────┤ │ │ │ Canvas Content │ ← Design content (1920×1080) │ (maintains 16:9 ratio) │ │ │ ├─────────────────────────────────────────┤ │░░░░░░░░░░░░░ Letterbox Bar ░░░░░░░░░░░░│ ← Black bar └─────────────────────────────────────────┘ ``` ### Global UI Controls Fullscreen, global sound, and offline controls are positioned against the same visible canvas rectangle as page elements. Their coordinates are stored as `xPercent`/`yPercent`. Their dimensions are stored as canvas-width-relative percentages (`buttonSizePercent`, `iconSizePercent`, `borderRadiusPercent`), so they scale with the displayed canvas instead of remaining fixed CSS pixels. Vertical clamping accounts for the canvas aspect ratio because button height is also derived from canvas width. This keeps controls fully inside the canvas for 16:9, 4:3, ultra-wide, and custom project ratios. ## File Reference ### Configuration **File:** `frontend/src/config/canvas.config.ts` ```typescript export const CANVAS_CONFIG = { // Default design dimensions defaults: { width: 1920, height: 1080, }, // Common presets for project settings presets: [ { name: 'HD 16:9', width: 1920, height: 1080 }, { name: '4K 16:9', width: 3840, height: 2160 }, { name: 'HD 4:3', width: 1440, height: 1080 }, { name: 'Ultra-wide 21:9', width: 2560, height: 1080 }, ], // Scaling behavior scaling: { mode: 'fit' as const, // Fit within viewport, may letterbox minScale: 0.1, maxScale: 4.0, }, // Portrait orientation handling orientation: { showRotatePrompt: true, minAspectRatioForPrompt: 0.8, }, // CSS custom property names cssVars: { scale: '--canvas-scale', unit: '--cu', designWidth: '--design-width', designHeight: '--design-height', }, }; ``` ### Utilities **File:** `frontend/src/lib/canvasScale.ts` | Function | Purpose | |----------|---------| | `calculateCanvasScale(vw, vh, dw, dh)` | Calculate scale factor for viewport | | `toCU(designPixels)` | Convert design pixels to `calc()` expression | | `isLegacyUnit(value)` | Check if value uses px/rem/vw/vh units | | `normalizeToCanvasUnits(value, property)` | Convert legacy units to canvas units | | `getCanvasCssVars(scale, dw, dh)` | Generate CSS custom properties object | | `vwToDesignPx(vw)` | Convert viewport width to design pixels | | `vhToDesignPx(vh)` | Convert viewport height to design pixels | | `remToDesignPx(rem)` | Convert rem to design pixels (16px base) | ### Hook **File:** `frontend/src/hooks/useCanvasScale.ts` ```typescript interface UseCanvasScaleOptions { designWidth?: number; // From project.design_width designHeight?: number; // From project.design_height } interface CanvasScaleResult { scale: number; // Current scale factor (1.0 = design size) designWidth: number; // Design canvas width designHeight: number; // Design canvas height canvasWidth: number; // Calculated width at current scale canvasHeight: number; // Calculated height at current scale isPortrait: boolean; // Viewport is portrait orientation showRotatePrompt: boolean; // Should show "rotate device" prompt cssVars: CSSProperties; // CSS custom properties object letterboxStyles: CSSProperties; // Styles for letterbox container } // Usage const { scale, cssVars, letterboxStyles, isPortrait, showRotatePrompt, } = useCanvasScale({ designWidth: project.design_width, designHeight: project.design_height, }); ``` ### Context (Optional) **File:** `frontend/src/context/CanvasScaleContext.tsx` For deeply nested components that need access to canvas scale without prop drilling: ```typescript // Provider setup {children} // Consumer hook const { scale, cssVars } = useCanvasScaleContext(); // Optional variant (returns null if no provider) const scaleContext = useCanvasScaleContextOptional(); ``` ## Element Styling Integration ### elementStyles.ts **File:** `frontend/src/lib/elementStyles.ts` The element styles library automatically converts values to canvas units: ```typescript // Normalization functions normalizePixelValue('24') // → "calc(24 * var(--cu, 1px))" normalizePixelValue('24px') // → "calc(24 * var(--cu, 1px))" normalizeViewportWidth('50vw') // → "calc(960 * var(--cu, 1px))" (50% of 1920) normalizeViewportHeight('25vh')// → "calc(270 * var(--cu, 1px))" (25% of 1080) // Build complete style object const style = buildElementStyle({ width: '200', height: '100', fontSize: '16', borderRadius: '8', }); // Result: all values converted to calc() expressions with --cu ``` ### useElementWrapperStyle Hook **File:** `frontend/src/components/UiElements/shared/useElementWrapperStyle.ts` Provides consistent styling for UI elements across constructor and runtime: ```typescript const { className, style } = useElementWrapperStyle({ element, isSelected: false, isEditMode: false, }); // Returns: // - className: Tailwind classes for appearance // - style: CSSProperties with canvas unit values ``` ## Usage Patterns ### RuntimePresentation ```typescript // RuntimePresentation.tsx const { cssVars, letterboxStyles } = useCanvasScale({ designWidth: project.design_width, designHeight: project.design_height, }); return (
{/* Inner canvas: maintains aspect ratio */}
{/* All child elements use --cu for sizing */} {elements.map(el => )}
); ``` ### Constructor ```typescript // constructor.tsx const { cssVars, letterboxStyles } = useCanvasScale({ designWidth: project?.design_width, designHeight: project?.design_height, }); return (
{elements.map(el => )}
); ``` ### Individual Elements ```typescript // Element component using canvas units import { toCU } from '../../lib/canvasScale'; const ButtonElement = ({ element }) => { return ( ); }; ``` ## CSS Custom Properties The system sets these CSS custom properties on the canvas container: | Property | Description | Example Value | |----------|-------------|---------------| | `--cu` | Canvas unit (1 design pixel) | `calc(1px * 0.75)` | | `--canvas-scale` | Current scale factor | `0.75` | | `--design-width` | Design canvas width | `1920` | | `--design-height` | Design canvas height | `1080` | **CSS Usage:** ```css .element { /* Use --cu for scalable dimensions */ font-size: calc(18 * var(--cu, 1px)); /* Use --canvas-scale for transforms */ transform: scale(var(--canvas-scale, 1)); /* Use design dimensions for calculations */ width: calc(var(--design-width) * 0.5 * var(--cu, 1px)); } ``` ## Legacy Unit Migration When encountering legacy units (px, vw, vh, rem), use the normalization utilities: ```typescript import { normalizeToCanvasUnits } from '../lib/canvasScale'; // Convert various legacy formats normalizeToCanvasUnits('24px', 'fontSize'); // → "calc(24 * var(--cu, 1px))" normalizeToCanvasUnits('50vw', 'width'); // → "calc(960 * var(--cu, 1px))" normalizeToCanvasUnits('25vh', 'height'); // → "calc(270 * var(--cu, 1px))" normalizeToCanvasUnits('1.5rem', 'fontSize'); // → "calc(24 * var(--cu, 1px))" normalizeToCanvasUnits(100, 'width'); // → "calc(100 * var(--cu, 1px))" ``` ## Orientation Handling The system detects portrait orientation and can prompt users to rotate their device: ```typescript const { isPortrait, showRotatePrompt } = useCanvasScale({ designWidth: 1920, designHeight: 1080, }); // showRotatePrompt is true when: // 1. Device is in portrait mode (height > width) // 2. orientation.showRotatePrompt is enabled in config // 3. Aspect ratio < minAspectRatioForPrompt (0.8) {showRotatePrompt && ( )} ``` ## Integration Points | Component | Uses | Purpose | |-----------|------|---------| | `RuntimePresentation` | `useCanvasScale` | Full-screen tour playback | | `constructor.tsx` | `useCanvasScale` | Tour editing canvas | | `TransitionPreviewOverlay` | `letterboxStyles` prop | Transition videos within canvas | | `CanvasBackground` | Inherits from parent | Background media display | | `RuntimeElement` | `buildElementStyle` | Element rendering | | `CanvasElement` | `useElementWrapperStyle` | Element editing | | `CarouselElement` | `toCU()` | Gallery styling | | `gallerySectionStyles` | `toCU()` | Gallery section dimensions | ## Performance Considerations 1. **Resize listener** - Uses single `resize` event listener, debounced via React state 2. **Memoization** - Scale calculations wrapped in `useMemo` to prevent recalculation 3. **CSS calc()** - Browser handles scaling efficiently via CSS custom properties 4. **No layout thrashing** - Scale updates don't trigger element-by-element recalculation ## Troubleshooting ### Elements Not Scaling 1. Verify `cssVars` is applied to a parent container 2. Check that values use `toCU()` or normalization functions 3. Ensure `--cu` is not overridden by other styles ### Letterbox Not Appearing 1. Parent container must have `overflow: hidden` and `bg-black` 2. `letterboxStyles` must be applied to inner container 3. Check that viewport dimensions are being detected (resize listener) ### Scale Factor Too Small/Large 1. Check `minScale` and `maxScale` in canvas.config.ts 2. Verify project's `design_width` and `design_height` are set correctly 3. Test with different viewport sizes to confirm scaling behavior