Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 262 lines 6.5 kB view raw
1/** 2 * Slides Canvas Engine — fixed-ratio slide rendering and element layout. 3 * 4 * Pure logic module: slide model, element positioning, layer ordering. 5 * DOM/canvas rendering handled by the slides UI layer. 6 */ 7 8export type ElementType = 'text' | 'image' | 'shape' | 'code' | 'chart' | 'embed' | 'table' | 'video' | 'audio'; 9export type ShapeType = 'rectangle' | 'ellipse' | 'triangle' | 'arrow' | 'line'; 10 11export interface SlideElement { 12 id: string; 13 type: ElementType; 14 x: number; 15 y: number; 16 width: number; 17 height: number; 18 rotation: number; 19 /** Layer order (higher = on top) */ 20 zIndex: number; 21 /** Element-specific content */ 22 content: string; 23 /** For shapes */ 24 shapeType?: ShapeType; 25 /** Style properties */ 26 style: Record<string, string>; 27} 28 29export interface Slide { 30 id: string; 31 elements: SlideElement[]; 32 background: string; 33 notes: string; 34} 35 36export interface DeckState { 37 slides: Slide[]; 38 currentSlide: number; 39 /** Fixed aspect ratio (width/height) */ 40 aspectRatio: number; 41 /** Custom master slide definitions stored with deck (optional) */ 42 masters?: import('./master-slides.js').MasterSlide[]; 43 /** Maps slide ID to master ID (optional) */ 44 masterAssignments?: Record<string, string>; 45} 46 47let _slideCounter = 0; 48let _elementCounter = 0; 49 50export const DEFAULT_ASPECT_RATIO = 16 / 9; 51export const SLIDE_WIDTH = 960; 52export const SLIDE_HEIGHT = 540; 53 54/** 55 * Create an empty deck. 56 */ 57export function createDeck(): DeckState { 58 const slide = createSlide(); 59 return { slides: [slide], currentSlide: 0, aspectRatio: DEFAULT_ASPECT_RATIO }; 60} 61 62/** 63 * Create a new slide. 64 */ 65export function createSlide(background = '#ffffff'): Slide { 66 return { 67 id: `slide-${Date.now()}-${++_slideCounter}`, 68 elements: [], 69 background, 70 notes: '', 71 }; 72} 73 74/** 75 * Add a slide at a position. 76 */ 77export function addSlide( 78 state: DeckState, 79 index?: number, 80 background?: string, 81): DeckState { 82 const slide = createSlide(background); 83 const slides = [...state.slides]; 84 const pos = index ?? slides.length; 85 slides.splice(pos, 0, slide); 86 return { ...state, slides }; 87} 88 89/** 90 * Remove a slide. 91 */ 92export function removeSlide(state: DeckState, index: number): DeckState { 93 if (state.slides.length <= 1) return state; 94 const slides = state.slides.filter((_, i) => i !== index); 95 const currentSlide = Math.min(state.currentSlide, slides.length - 1); 96 return { ...state, slides, currentSlide }; 97} 98 99/** 100 * Move a slide from one position to another. 101 */ 102export function moveSlide(state: DeckState, from: number, to: number): DeckState { 103 const slides = [...state.slides]; 104 const [moved] = slides.splice(from, 1); 105 slides.splice(to, 0, moved); 106 return { ...state, slides }; 107} 108 109/** 110 * Duplicate a slide. 111 */ 112export function duplicateSlide(state: DeckState, index: number): DeckState { 113 const original = state.slides[index]; 114 if (!original) return state; 115 const copy: Slide = { 116 ...createSlide(original.background), 117 elements: original.elements.map(e => ({ ...e, id: `el-${Date.now()}-${++_elementCounter}` })), 118 notes: original.notes, 119 }; 120 const slides = [...state.slides]; 121 slides.splice(index + 1, 0, copy); 122 return { ...state, slides }; 123} 124 125/** 126 * Go to a specific slide. 127 */ 128export function goToSlide(state: DeckState, index: number): DeckState { 129 const clamped = Math.max(0, Math.min(index, state.slides.length - 1)); 130 return { ...state, currentSlide: clamped }; 131} 132 133/** 134 * Add an element to the current slide. 135 */ 136export function addElement( 137 state: DeckState, 138 type: ElementType, 139 x: number, 140 y: number, 141 width: number, 142 height: number, 143 content = '', 144 style: Record<string, string> = {}, 145): DeckState { 146 const element: SlideElement = { 147 id: `el-${Date.now()}-${++_elementCounter}`, 148 type, 149 x, y, width, height, 150 rotation: 0, 151 zIndex: currentSlide(state).elements.length, 152 content, 153 style, 154 }; 155 return updateCurrentSlide(state, slide => ({ 156 ...slide, 157 elements: [...slide.elements, element], 158 })); 159} 160 161/** 162 * Remove an element from the current slide. 163 */ 164export function removeElement(state: DeckState, elementId: string): DeckState { 165 return updateCurrentSlide(state, slide => ({ 166 ...slide, 167 elements: slide.elements.filter(e => e.id !== elementId), 168 })); 169} 170 171/** 172 * Move an element. 173 */ 174export function moveElement( 175 state: DeckState, 176 elementId: string, 177 x: number, 178 y: number, 179): DeckState { 180 return updateElement(state, elementId, { x, y }); 181} 182 183/** 184 * Resize an element. 185 */ 186export function resizeElement( 187 state: DeckState, 188 elementId: string, 189 width: number, 190 height: number, 191): DeckState { 192 return updateElement(state, elementId, { 193 width: Math.max(10, width), 194 height: Math.max(10, height), 195 }); 196} 197 198/** 199 * Bring an element to front. 200 */ 201export function bringToFront(state: DeckState, elementId: string): DeckState { 202 const slide = currentSlide(state); 203 const others = slide.elements.filter(e => e.id !== elementId); 204 const maxZ = others.length > 0 ? Math.max(...others.map(e => e.zIndex)) : 0; 205 return updateElement(state, elementId, { zIndex: maxZ + 1 }); 206} 207 208/** 209 * Send an element to back. 210 */ 211export function sendToBack(state: DeckState, elementId: string): DeckState { 212 const slide = currentSlide(state); 213 const others = slide.elements.filter(e => e.id !== elementId); 214 const minZ = others.length > 0 ? Math.min(...others.map(e => e.zIndex)) : 0; 215 return updateElement(state, elementId, { zIndex: minZ - 1 }); 216} 217 218/** 219 * Get the current slide. 220 */ 221export function currentSlide(state: DeckState): Slide { 222 return state.slides[state.currentSlide]; 223} 224 225/** 226 * Get slide count. 227 */ 228export function slideCount(state: DeckState): number { 229 return state.slides.length; 230} 231 232/** 233 * Get element count on current slide. 234 */ 235export function elementCount(state: DeckState): number { 236 return currentSlide(state).elements.length; 237} 238 239/** Internal: update an element's properties */ 240function updateElement( 241 state: DeckState, 242 elementId: string, 243 updates: Partial<SlideElement>, 244): DeckState { 245 return updateCurrentSlide(state, slide => ({ 246 ...slide, 247 elements: slide.elements.map(e => 248 e.id === elementId ? { ...e, ...updates } : e, 249 ), 250 })); 251} 252 253/** Internal: update the current slide */ 254function updateCurrentSlide( 255 state: DeckState, 256 updater: (slide: Slide) => Slide, 257): DeckState { 258 const slides = state.slides.map((s, i) => 259 i === state.currentSlide ? updater(s) : s, 260 ); 261 return { ...state, slides }; 262}