pollen-robotics/Reachy_Mini_LITE_Assembly_Guide
10
1// Assembly Steps Data2const YOUTUBE_VIDEO_ID = "PC5Yx950nMY";3 4const stepsData = [5 { timestamp: "00:14", title: "Set all parts aside" },6 { timestamp: "00:30", title: "Stick Foot Pads" },7 { timestamp: "01:18", title: "Fix the USB Extension Cable and add the protective sleeve" },8 { timestamp: "02:58", title: "Insert the Power Board" },9 { timestamp: "03:17", title: "Fix the Power Board" },10 { timestamp: "04:06", title: "Connect the cables to the Power Board" },11 { timestamp: "05:26", title: "Position the Bottom Assembly" },12 { timestamp: "07:20", title: "Fix the Foot Assembly to the Bottom Assembly" },13 { timestamp: "08:15", title: "Connect the Foot Motor" },14 { timestamp: "09:15", title: "Screw the link rods onto the motor arms" },15 { timestamp: "12:22", title: "Connect Motor 1 to 2" },16 { timestamp: "13:11", title: "Connect Motor 2 to 3" },17 { timestamp: "13:36", title: "Connect Motor 4 to 5" },18 { timestamp: "13:55", title: "Connect Motor 5 to 6" },19 { timestamp: "14:24", title: "Insert all motors into the Bottom Assembly" },20 { timestamp: "15:54", title: "Clip the motor's cables into the Bottom Assembly" },21 { timestamp: "19:00", title: "Position the Tricap" },22 { timestamp: "19:19", title: "Route the cables" },23 { timestamp: "19:51", title: "Connect Motor 3 to 4" },24 { timestamp: "20:40", title: "Route the cables" },25 { timestamp: "22:02", title: "Check rotation" },26 { timestamp: "22:24", title: "Screw the Tricap" },27 { timestamp: "23:56", title: "Screw the Bottom Head onto the link rods" },28 { timestamp: "27:50", title: "Route the cables through the Bottom Head" },29 { timestamp: "28:45", title: "Connect the flexible camera cable (step for reference only)" },30 { timestamp: "28:51", title: "Route the cables through the Head PCB" },31 { timestamp: "29:16", title: "Screw the Head PCB" },32 { timestamp: "30:19", title: "Position the Top Shell" },33 { timestamp: "30:53", title: "Screw the Top Shell" },34 { timestamp: "32:18", title: "Place the lenses in the Glasses Holder" },35 { timestamp: "32:30", title: "Insert the Fisheye Lenses into the caps" },36 { timestamp: "32:54", title: "Snap the Fisheye Lenses" },37 { timestamp: "33:39", title: "Screw the Glasses Assembly onto the Front Head Shell" },38 { timestamp: "34:40", title: "Attach the cases to Antenna's motors" },39 { timestamp: "34:59", title: "Fix Antenna's Assembly to Back Head Shell" },40 { timestamp: "36:30", title: "Connect the Antenna's motors" },41 { timestamp: "36:52", title: "Slide the Back Head Assembly onto the Reachy Mini body" },42 { timestamp: "37:14", title: "Screw the Back Head" },43 { timestamp: "38:00", title: "Fix the Cable Holder" },44 { timestamp: "39:45", title: "Connect Speaker and Motor Cables" },45 { timestamp: "40:07", title: "Connect Power and USB Extension Cables" },46 { timestamp: "40:25", title: "Slide the Top Head Assembly onto the Back Head" },47 { timestamp: "40:39", title: "Connect the Flexible Printed Cable to the Head PCB" },48 { timestamp: "40:50", title: "Connect the Flexible Camera Cable on the Front Head" },49 { timestamp: "41:42", title: "Fix the Front Head" },50 { timestamp: "42:56", title: "Fix the Antennas" },51 { timestamp: "43:42", title: "Turn it on!" },52];53 54// Parse timestamp to seconds (MM:SS format)55function parseTimestamp(ts) {56 const parts = ts.split(':').map(Number);57 return parts[0] * 60 + parts[1];58}59 60// Create assembly steps with parsed timestamps61const assemblySteps = stepsData.map((step, index) => ({62 id: index + 1,63 title: step.title,64 timestamp: step.timestamp,65 timestampSeconds: parseTimestamp(step.timestamp)66}));67 68const TOTAL_STEPS = assemblySteps.length;69 70// Available step images71const availableImages = {72 1: "assets/step1.jpg",73 2: "assets/step2.jpg",74 3: "assets/step3.jpg",75 4: "assets/step4.jpg",76 5: "assets/step5.jpg",77 6: "assets/step6.jpg",78 7: "assets/step7.jpg",79 8: "assets/step8.jpg",80 9: "assets/step9.jpg",81 10: "assets/step10.jpg",82 11: "assets/step11.jpg",83 12: "assets/step12.jpg",84 13: "assets/step13.jpg",85 14: "assets/step14.jpg",86 15: "assets/step15.jpg",87 16: "assets/step16.jpg",88 17: "assets/step17.jpg",89 18: "assets/step18.jpg",90 19: "assets/step19.jpg",91 20: "assets/step20.jpg",92 21: "assets/step21.jpg",93 22: "assets/step22.jpg",94 23: "assets/step23.jpg",95 24: "assets/step24.jpg",96 25: "assets/step25.jpg",97 26: "assets/step26.jpg",98 27: "assets/step27.jpg",99 28: "assets/step28.jpg",100 29: "assets/step29.jpg",101 30: "assets/step30.jpg",102 31: "assets/step31.jpg",103 32: "assets/step32.jpg",104 33: "assets/step33.jpg",105 34: "assets/step34.jpg",106 35: "assets/step35.jpg",107 36: "assets/step36.jpg",108 37: "assets/step37.jpg",109 38: "assets/step38.jpg",110 39: "assets/step39.jpg",111 40: "assets/step40.jpg",112 41: "assets/step41.jpg",113 42: "assets/step42.jpg",114 43: "assets/step43.jpg",115 44: "assets/step44.jpg",116 45: "assets/step45.jpg",117 46: "assets/step46.jpg",118 47: "assets/step47.jpg",119};120 121function getStepImage(stepId) {122 return availableImages[stepId] || null;123}124 125// App State126let currentStep = 1;127let isFullscreen = false;128let scale = 1;129let position = { x: 0, y: 0 };130let isDragging = false;131let dragStart = { x: 0, y: 0 };132 133// DOM Elements134const stepCounterText = document.getElementById('step-counter-text');135const stepImage = document.getElementById('step-image');136const placeholder = document.getElementById('placeholder');137const placeholderNumber = document.getElementById('placeholder-number');138const imageWrapper = document.getElementById('image-wrapper');139const youtubeIframeDesktop = document.getElementById('youtube-iframe-desktop');140const youtubeIframeMobile = document.getElementById('youtube-iframe-mobile');141const prevBtn = document.getElementById('prev-btn');142const nextBtn = document.getElementById('next-btn');143const stepIndicators = document.getElementById('step-indicators');144const progressBar = document.getElementById('progress-bar');145const fullscreenBtn = document.getElementById('fullscreen-btn');146const fullscreenModal = document.getElementById('fullscreen-modal');147const closeFullscreenBtn = document.getElementById('close-fullscreen-btn');148const fullscreenStepTitle = document.getElementById('fullscreen-step-title');149const fullscreenImage = document.getElementById('fullscreen-image');150const fullscreenPlaceholder = document.getElementById('fullscreen-placeholder');151const fullscreenPlaceholderNumber = document.getElementById('fullscreen-placeholder-number');152const fullscreenImageContainer = document.getElementById('fullscreen-image-container');153const fullscreenYoutubeIframeDesktop = document.getElementById('fullscreen-youtube-iframe-desktop');154const fullscreenYoutubeIframeMobile = document.getElementById('fullscreen-youtube-iframe-mobile');155const fullscreenPrevBtn = document.getElementById('fullscreen-prev-btn');156const fullscreenNextBtn = document.getElementById('fullscreen-next-btn');157const fullscreenStepIndicators = document.getElementById('fullscreen-step-indicators');158const fullscreenProgressBar = document.getElementById('fullscreen-progress-bar');159const zoomInBtn = document.getElementById('zoom-in-btn');160const zoomOutBtn = document.getElementById('zoom-out-btn');161const zoomLevel = document.getElementById('zoom-level');162 163// Update YouTube embed164function updateYouTubeEmbed(timestampSeconds) {165 const embedUrl = `https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}?start=${timestampSeconds}&rel=0&autoplay=1&mute=1`;166 youtubeIframeDesktop.src = embedUrl;167 youtubeIframeMobile.src = embedUrl;168 fullscreenYoutubeIframeDesktop.src = embedUrl;169 fullscreenYoutubeIframeMobile.src = embedUrl;170}171 172// Render step indicators with pagination173function renderStepIndicators(containerId, currentStep, onClick) {174 const container = document.getElementById(containerId);175 container.innerHTML = '';176 177 const groupStart = Math.floor((currentStep - 1) / 10) * 10 + 1;178 const groupEnd = Math.min(groupStart + 9, TOTAL_STEPS);179 const canGoPrevGroup = groupStart > 1;180 const canGoNextGroup = groupEnd < TOTAL_STEPS;181 182 // Previous group button183 if (canGoPrevGroup) {184 const prevGroupBtn = document.createElement('button');185 prevGroupBtn.className = 'step-indicator-nav';186 prevGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>';187 prevGroupBtn.setAttribute('aria-label', 'Previous group');188 prevGroupBtn.addEventListener('click', () => onClick(groupStart - 1));189 container.appendChild(prevGroupBtn);190 }191 192 // Step number buttons 193 for (let i = 0; i < 10; i++) {194 const stepNum = groupStart + i;195 if (stepNum > TOTAL_STEPS) break;196 197 const isActive = stepNum === currentStep;198 const button = document.createElement('button');199 button.className = `step-indicator-num ${isActive ? 'step-indicator-num-active' : ''}`;200 button.textContent = stepNum;201 button.setAttribute('aria-label', `Go to step ${stepNum}`);202 button.addEventListener('click', () => onClick(stepNum));203 container.appendChild(button);204 }205 206 // Next group button207 if (canGoNextGroup) {208 const nextGroupBtn = document.createElement('button');209 nextGroupBtn.className = 'step-indicator-nav';210 nextGroupBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';211 nextGroupBtn.setAttribute('aria-label', 'Next group');212 nextGroupBtn.addEventListener('click', () => onClick(groupEnd + 1));213 container.appendChild(nextGroupBtn);214 }215}216 217// Update UI218function updateUI() {219 const step = assemblySteps[currentStep - 1];220 const imageSrc = getStepImage(step.id);221 222 // Update step counter223 stepCounterText.textContent = `Step ${step.id}/${TOTAL_STEPS}`;224 225 // Update image226 if (imageSrc) {227 stepImage.src = imageSrc;228 stepImage.alt = `Assembly step ${step.id}`;229 stepImage.classList.remove('hidden');230 placeholder.classList.add('hidden');231 } else {232 stepImage.classList.add('hidden');233 placeholder.classList.remove('hidden');234 placeholderNumber.textContent = step.id;235 }236 237 // Update YouTube embed238 updateYouTubeEmbed(step.timestampSeconds);239 240 // Update buttons241 prevBtn.disabled = currentStep === 1;242 nextBtn.disabled = currentStep === TOTAL_STEPS;243 244 // Update step indicators245 renderStepIndicators('step-indicators', currentStep, goToStep);246 247 // Update progress bar248 const progress = (currentStep / TOTAL_STEPS) * 100;249 progressBar.style.width = `${progress}%`;250 251 // Update fullscreen UI252 updateFullscreenUI();253}254 255// Update fullscreen UI256function updateFullscreenUI() {257 const step = assemblySteps[currentStep - 1];258 const imageSrc = getStepImage(step.id);259 260 fullscreenStepTitle.textContent = `Step ${step.id}/${TOTAL_STEPS} - ${step.title}`;261 262 if (imageSrc) {263 fullscreenImage.src = imageSrc;264 fullscreenImage.alt = `Step ${step.id}`;265 fullscreenImage.classList.remove('hidden');266 fullscreenPlaceholder.classList.add('hidden');267 } else {268 fullscreenImage.classList.add('hidden');269 fullscreenPlaceholder.classList.remove('hidden');270 fullscreenPlaceholderNumber.textContent = step.id;271 }272 273 fullscreenPrevBtn.disabled = currentStep === 1;274 fullscreenNextBtn.disabled = currentStep === TOTAL_STEPS;275 276 renderStepIndicators('fullscreen-step-indicators', currentStep, goToStepFullscreen);277 278 const progress = (currentStep / TOTAL_STEPS) * 100;279 fullscreenProgressBar.style.width = `${progress}%`;280 281 updateZoomDisplay();282}283 284// Navigation functions285function goToPrevious() {286 if (currentStep > 1) {287 currentStep--;288 updateUI();289 }290}291 292function goToNext() {293 if (currentStep < TOTAL_STEPS) {294 currentStep++;295 updateUI();296 }297}298 299function goToStep(step) {300 if (step >= 1 && step <= TOTAL_STEPS) {301 currentStep = step;302 updateUI();303 }304}305 306function goToStepFullscreen(step) {307 resetZoom();308 goToStep(step);309}310 311function goToPreviousFullscreen() {312 resetZoom();313 goToPrevious();314}315 316function goToNextFullscreen() {317 resetZoom();318 goToNext();319}320 321// Fullscreen functions322function openFullscreen() {323 isFullscreen = true;324 fullscreenModal.classList.remove('hidden');325 document.body.style.overflow = 'hidden';326 updateFullscreenUI();327}328 329function closeFullscreen() {330 isFullscreen = false;331 fullscreenModal.classList.add('hidden');332 document.body.style.overflow = '';333 resetZoom();334}335 336// Zoom functions337function zoomIn() {338 scale = Math.min(scale + 0.5, 4);339 updateZoomDisplay();340}341 342function zoomOut() {343 scale = Math.max(scale - 0.5, 0.5);344 updateZoomDisplay();345}346 347function resetZoom() {348 scale = 1;349 position = { x: 0, y: 0 };350 updateZoomDisplay();351}352 353function updateZoomDisplay() {354 zoomLevel.textContent = `${Math.round(scale * 100)}%`;355 fullscreenImage.style.transform = `translate(${position.x}px, ${position.y}px) scale(${scale})`;356}357 358// Drag functions for fullscreen image359function handleMouseDown(e) {360 if (scale > 1) {361 isDragging = true;362 dragStart = {363 x: e.clientX - position.x,364 y: e.clientY - position.y365 };366 fullscreenImageContainer.style.cursor = 'grabbing';367 }368}369 370function handleMouseMove(e) {371 if (isDragging && scale > 1) {372 position = {373 x: e.clientX - dragStart.x,374 y: e.clientY - dragStart.y375 };376 updateZoomDisplay();377 }378}379 380function handleMouseUp() {381 isDragging = false;382 fullscreenImageContainer.style.cursor = 'grab';383}384 385function handleWheel(e) {386 e.preventDefault();387 const delta = e.deltaY > 0 ? -0.2 : 0.2;388 scale = Math.max(0.5, Math.min(4, scale + delta));389 updateZoomDisplay();390}391 392// Event Listeners393prevBtn.addEventListener('click', goToPrevious);394nextBtn.addEventListener('click', goToNext);395fullscreenBtn.addEventListener('click', openFullscreen);396stepImage.addEventListener('click', openFullscreen);397closeFullscreenBtn.addEventListener('click', closeFullscreen);398fullscreenPrevBtn.addEventListener('click', goToPreviousFullscreen);399fullscreenNextBtn.addEventListener('click', goToNextFullscreen);400zoomInBtn.addEventListener('click', zoomIn);401zoomOutBtn.addEventListener('click', zoomOut);402 403fullscreenImageContainer.addEventListener('mousedown', handleMouseDown);404fullscreenImageContainer.addEventListener('mousemove', handleMouseMove);405fullscreenImageContainer.addEventListener('mouseup', handleMouseUp);406fullscreenImageContainer.addEventListener('mouseleave', handleMouseUp);407fullscreenImageContainer.addEventListener('wheel', handleWheel, { passive: false });408 409// Keyboard navigation410document.addEventListener('keydown', (e) => {411 if (isFullscreen) {412 if (e.key === 'Escape') closeFullscreen();413 if (e.key === '+' || e.key === '=') zoomIn();414 if (e.key === '-') zoomOut();415 if (e.key === '0') resetZoom();416 if (e.key === 'ArrowLeft' && currentStep > 1) goToPreviousFullscreen();417 if (e.key === 'ArrowRight' && currentStep < TOTAL_STEPS) goToNextFullscreen();418 } else {419 if (e.key === 'ArrowRight') goToNext();420 if (e.key === 'ArrowLeft') goToPrevious();421 }422});423 424// Initialize425updateUI();426 