const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let offsetMultiplier = -0.0; // Initial value let offsetAnimationDirection = 1; // 1 for increasing, -1 for decreasing const offsetMin = -0.1; const offsetMax = 0.1; const offsetSpeed = 0.00000; // Adjust this value to control the speed of the animation const fontFace = new FontFace('JSCodecPro', 'url(/fonts/CodecProRegular/9_CodecPro-Regular.woff2)'); const images = { stuttgart: new Image(), carWash1: new Image(), carWash2: new Image() }; images.stuttgart.src = '/images/stuttgart-map.webp'; // Function to get a random image from the mainIllustrations array function getRandomImage(half) { const midPoint = Math.floor(mainIllustrations.length / 2); let start, end; if (half === 1) { // First half of the array start = 0; end = midPoint; } else { // Second half of the array start = midPoint; end = mainIllustrations.length; } const index = Math.floor(Math.random() * (end - start)) + start; return `/call/image.resize?w=1600&src=/uploads/main-illustrations/${mainIllustrations[index]}`; } // Set initial random images images.carWash1.src = getRandomImage(1); do { images.carWash2.src = getRandomImage(2); } while (images.carWash2.src === images.carWash1.src); function preloadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = src; }); } let lastDisplayedImage = ''; let imageToRefresh = 'carWash2'; let isRefreshing = false; let currentImageKey = 'carWash1'; // Track which image should be refreshed async function refreshImage(imageKey) { let newImageSrc; let newImage; do { newImageSrc = getRandomImage((imageKey == 'carWash1' ? 1 : 2) ); if (newImageSrc !== images.carWash1.src && newImageSrc !== images.carWash2.src && newImageSrc !== lastDisplayedImage) { try { newImage = await preloadImage(newImageSrc); break; } catch (error) { console.error('Failed to load image:', newImageSrc, error); } } } while (true); if (imageKey === 'carWash1') { images.carWash1.src = newImage.src; resetImageAnimation('carWash1'); } else { images.carWash2.src = newImage.src; resetImageAnimation('carWash2'); } lastDisplayedImage = newImageSrc; } async function refreshImageLoop() { while (true) { await refreshImage(currentImageKey); resetImageAnimation(currentImageKey); // Switch to the other image for the next refresh currentImageKey = (currentImageKey === 'carWash1') ? 'carWash2' : 'carWash1'; // Wait 15 seconds before refreshing the next image await new Promise(resolve => setTimeout(resolve, 15000)); } } // Start the image refresh loop //refreshImageLoop(); // Modify the animate function function animate(timestamp) { updateImageAnimation(); drawCanvas(); requestAnimationFrame(animate); } // Set up the interval to refresh images every 15 seconds //setInterval(refreshImage, 15000); function resizeCanvas() { const dpr = window.devicePixelRatio || 1; const maxWidth = 1600; // Set max-width for the canvas const canvasWidth = Math.min(window.innerWidth, maxWidth); // Limit the canvas width to 1600px // Adjust canvas height proportionally, or you can use a fixed height calculation const canvasHeight = (window.innerHeight - 118); canvas.width = canvasWidth * dpr; canvas.height = canvasHeight * dpr; canvas.style.width = `${canvasWidth}px`; canvas.style.height = `${canvasHeight}px`; // Apply the device pixel ratio scaling ctx.scale(dpr, dpr); drawCanvas(); // Redraw the canvas with the updated size } function drawCanvas() { const width = canvas.width / (window.devicePixelRatio || 1); const height = canvas.height / (window.devicePixelRatio || 1); ctx.clearRect(0, 0, width, height); if (window.innerWidth < 700) { // Mobile layout: Two-area design with equal height sections const topImage = images.carWash1; const bottomImage = images.stuttgart; // Ensure exact equal heights for top and bottom sections const sectionHeight = Math.floor(height / 2); // Explicitly use floor to ensure equal division const targetWidth = width; let topImageWidth, topImageHeight, offsetX = 0, offsetY = 0; // Check if the top image is wider or taller const topAspectRatio = topImage.naturalWidth / topImage.naturalHeight; if (topAspectRatio > targetWidth / sectionHeight) { // Image is wider than the space - fit to height and animate horizontally topImageHeight = sectionHeight; topImageWidth = topImageHeight * topAspectRatio; const maxOffsetX = topImageWidth - targetWidth; offsetX = -maxOffsetX * (animationProgress.carWash1 || 0); } else { // Image is taller than the space - fit to width and animate vertically topImageWidth = targetWidth; topImageHeight = topImageWidth / topAspectRatio; const maxOffsetY = topImageHeight - sectionHeight; offsetY = -maxOffsetY * (animationProgress.carWash1 || 0); } // Draw the top image with animation ctx.drawImage( topImage, offsetX, 0, topImageWidth, topImageHeight ); // Draw the bottom image (map) without distortion, ensuring it fills the bottom half const bottomAspectRatio = bottomImage.naturalWidth / bottomImage.naturalHeight; const scaleFactor = Math.max(width / bottomImage.naturalWidth, sectionHeight / bottomImage.naturalHeight); const bottomDrawWidth = bottomImage.naturalWidth * scaleFactor; const bottomDrawHeight = bottomImage.naturalHeight * scaleFactor; // Adjust the Y-coordinate to start drawing where the top section ends const bottomOffsetX = (width - bottomDrawWidth) / 2; const bottomOffsetY = sectionHeight; // Ensures it starts right where the top section ends ctx.drawImage( bottomImage, bottomOffsetX, bottomOffsetY, bottomDrawWidth, bottomDrawHeight ); // Handle text switching at the bottom (as before) const switchInterval = 15000; const elapsed = Date.now() % switchInterval; const showStars = elapsed < switchInterval / 2; let line1, line2; if (showStars) { line1 = `${theBestText} ★★★★★`; line2 = `${usedCarsText}`; } else { line1 = 'Hafenbahnstraße 22a'; line2 = '70329 Stuttgart'; } // Calculate font sizes and positions (as before) const maxWidth = Math.min(width * 0.6, 400); let fontSize1 = calculateFontSize(line1, maxWidth, 'bold'); let fontSize2 = calculateFontSize(line2, maxWidth, 'bold'); ctx.font = `bold ${fontSize1}px "JSCodecPro"`; const metrics1 = ctx.measureText(line1); ctx.font = `bold ${fontSize2}px "JSCodecPro"`; const metrics2 = ctx.measureText(line2); const line1Height = metrics1.actualBoundingBoxAscent + metrics1.actualBoundingBoxDescent; const line2Height = metrics2.actualBoundingBoxAscent + metrics2.actualBoundingBoxDescent; const lineSpacing = 8; const totalTextHeight = line1Height + lineSpacing + line2Height; const bottomHalfStart = sectionHeight; const bottomHalfHeight = sectionHeight; const centerY = bottomHalfStart + (bottomHalfHeight - totalTextHeight) / 2; ctx.fillStyle = 'black'; ctx.textAlign = 'center'; ctx.font = `bold ${fontSize1}px "JSCodecPro"`; ctx.fillText(line1, width / 2, centerY + line1Height); ctx.font = `bold ${fontSize2}px "JSCodecPro"`; ctx.fillText(line2, width / 2, centerY + line1Height + lineSpacing + line2Height); // Draw a black line to split the two images exactly in the middle ctx.strokeStyle = '#757575'; ctx.lineWidth = 8; ctx.beginPath(); ctx.moveTo(0, sectionHeight); // This ensures the line is exactly in the middle ctx.lineTo(width, sectionHeight); ctx.stroke(); } else { // Desktop layout: Original 4-area design remains unchanged const intersectionX = width * 0.5; const intersectionY = height * 0.5; ctx.fillStyle = '#f1f1f1'; ctx.fillRect(0, 0, width, height); drawClippedImage(images.carWash2, [0, 0, intersectionX + (height * offsetMultiplier), 0, intersectionX, intersectionY, 0, intersectionY - (intersectionX * offsetMultiplier)]); drawClippedImage(images.carWash1, [intersectionX, intersectionY, width, intersectionY + (intersectionX * offsetMultiplier), width, height, intersectionX - (height * offsetMultiplier), height]); drawClippedImage(images.stuttgart, [intersectionX + (height * offsetMultiplier), 0, width, 0, width, intersectionY + (intersectionX * offsetMultiplier), intersectionX, intersectionY]); // Berechne die Startpunkte der roten Linien const topRedLineStartX = intersectionX + (height * offsetMultiplier); const bottomRedLineEndX = intersectionX - (height * offsetMultiplier); // Oberes rechtes Viereck const upperRightQuarterWidth = width - topRedLineStartX; const upperRightQuarterHeight = height / 2; const upperRightQuarterCenterX = (topRedLineStartX + width) / 2; const upperRightQuarterCenterY = height * 0.25; ctx.fillStyle = '#f1f1f1'; ctx.fillRect(0, 0, width, height); drawClippedImage(images.carWash2, [0, 0, intersectionX + (height * offsetMultiplier), 0, intersectionX, intersectionY, 0, intersectionY - (intersectionX * offsetMultiplier)]); drawClippedImage(images.carWash1, [intersectionX, intersectionY, width, intersectionY + (intersectionX * offsetMultiplier), width, height, intersectionX - (height * offsetMultiplier), height]); drawClippedImage(images.stuttgart, [intersectionX + (height * offsetMultiplier), 0, width, 0, width, intersectionY + (intersectionX * offsetMultiplier), intersectionX, intersectionY]); let maxTextWidth; if (width < 600) { maxTextWidth = width * 0.3; // 25% of total width for desktop } else if (width < 900) { maxTextWidth = width * 0.25; // Increase to 30% of total width for mobile } else { maxTextWidth = width * 0.2; // 20% of total width for desktop } // Berechne Schriftgrößen const inStuttgartFontSize = calculateFontSize(inStuttgartText, maxTextWidth, 'bold'); const addressFontSize = calculateFontSize('Hafenbahnstraße 22a', maxTextWidth); // Messe Textbreiten ctx.font = `bold ${inStuttgartFontSize}px "JSCodecPro"`; const inStuttgartWidth = ctx.measureText(inStuttgartText).width; ctx.font = `${addressFontSize}px "JSCodecPro"`; const addressWidth = ctx.measureText('Hafenbahnstraße 22a').width; const plzWidth = ctx.measureText('70329 Stuttgart').width; // Bestimme die maximale Textbreite const actualTextWidth = Math.max(inStuttgartWidth, addressWidth, plzWidth); // Berechne Gesamthöhe des Textblocks const lineSpacing = 5; // Abstand zwischen den Zeilen const totalTextHeight = inStuttgartFontSize + addressFontSize * 2 + lineSpacing * 2; // Berechne Startposition für zentrierten Text im oberen rechten Bereich const startX = upperRightQuarterCenterX - actualTextWidth / 2; const startY = upperRightQuarterCenterY - totalTextHeight / 2; // Zeichne obere Texte ctx.fillStyle = 'black'; ctx.textAlign = 'left'; ctx.font = `bold ${inStuttgartFontSize}px "JSCodecPro"`; ctx.fillText(inStuttgartText, startX, startY + inStuttgartFontSize); ctx.font = `${addressFontSize}px "JSCodecPro"`; ctx.fillText('Hafenbahnstraße 22a', startX, startY + inStuttgartFontSize + addressFontSize + lineSpacing); ctx.fillText('70329 Stuttgart', startX, startY + inStuttgartFontSize + addressFontSize * 2 + lineSpacing * 2); // Unteres linkes Viereck const lowerLeftQuarterWidth = bottomRedLineEndX; const lowerLeftQuarterHeight = height / 2; const lowerLeftQuarterCenterX = bottomRedLineEndX / 2; const lowerLeftQuarterCenterY = height * 0.75; // Berechne Schriftgrößen für unteren Text und vergrößere sie um 30% let bestFontSize = calculateFontSize(theBestText+' ★★★★★', maxTextWidth, 'bold'); let gebrauchtwagenFontSize = calculateFontSize(usedCarsText, maxTextWidth, 'bold'); bestFontSize = Math.round(bestFontSize * 1.3); gebrauchtwagenFontSize = Math.round(gebrauchtwagenFontSize * 1.3); // Messe Textbreiten mit den neuen Schriftgrößen ctx.font = `bold ${bestFontSize}px "JSCodecPro"`; let bestWidth = ctx.measureText(theBestText+' ★★★★★').width; ctx.font = `bold ${gebrauchtwagenFontSize}px "JSCodecPro"`; let gebrauchtwagenWidth = ctx.measureText(usedCarsText).width; // Passe die Schriftgrößen an, um gleiche Breite zu erreichen if (bestWidth > gebrauchtwagenWidth) { gebrauchtwagenFontSize = adjustFontSize(usedCarsText, bestWidth, gebrauchtwagenFontSize, 'bold'); } else { bestFontSize = adjustFontSize(theBestText + ' ★★★★★', gebrauchtwagenWidth, bestFontSize, 'bold'); } // Messe die finalen Textbreiten ctx.font = `bold ${bestFontSize}px "JSCodecPro"`; bestWidth = ctx.measureText(theBestText + ' ★★★★★').width; ctx.font = `bold ${gebrauchtwagenFontSize}px "JSCodecPro"`; gebrauchtwagenWidth = ctx.measureText(usedCarsText).width; // Bestimme die maximale Textbreite für unteren Text const lowerActualTextWidth = Math.max(bestWidth, gebrauchtwagenWidth); // Berechne Gesamthöhe des unteren Textblocks const totalLowerTextHeight = bestFontSize + gebrauchtwagenFontSize + lineSpacing; // Berechne Startposition für zentrierten unteren Text const lowerStartX = lowerLeftQuarterCenterX - lowerActualTextWidth / 2; const lowerStartY = lowerLeftQuarterCenterY - totalLowerTextHeight / 2; // Zeichne unteren Text ctx.font = `bold ${bestFontSize}px "JSCodecPro"`; ctx.fillText(theBestText + ' ★★★★★', lowerStartX, lowerStartY + bestFontSize); ctx.font = `bold ${gebrauchtwagenFontSize}px "JSCodecPro"`; ctx.fillText(usedCarsText, lowerStartX, lowerStartY + bestFontSize + gebrauchtwagenFontSize + lineSpacing); // Zeichne rote Linien ctx.strokeStyle = '#757575'; ctx.lineWidth = 8; ctx.beginPath(); // Horizontale Linie bleibt unverändert ctx.moveTo(0, intersectionY - (intersectionX * offsetMultiplier)); ctx.lineTo(width, intersectionY + (intersectionX * offsetMultiplier)); // Vertikale Linie wird 5 Pixel nach oben verschoben const verticalLineOffset = 5; ctx.moveTo(intersectionX + (height * offsetMultiplier), -verticalLineOffset); ctx.lineTo(intersectionX - (height * offsetMultiplier), height + verticalLineOffset); ctx.stroke(); } } // Animation parameters const animationDuration = 30000; // 15 seconds let animationStartTime = {}; let animationProgress = {}; let isFirstAnimation = true; function resetImageAnimation(imageKey) { const currentTime = Date.now(); if (isFirstAnimation && imageKey === 'carWash1') { animationStartTime[imageKey] = currentTime - 15000; // Start halfway through for the first image isFirstAnimation = false; } else { animationStartTime[imageKey] = currentTime; } animationProgress[imageKey] = 0; } function updateImageAnimation() { const currentTime = Date.now(); // Animate the offsetMultiplier offsetMultiplier += offsetSpeed * offsetAnimationDirection; // Reverse direction if it reaches the bounds if (offsetMultiplier >= offsetMax) { offsetMultiplier = offsetMax; offsetAnimationDirection = -1; } else if (offsetMultiplier <= offsetMin) { offsetMultiplier = offsetMin; offsetAnimationDirection = 1; } // Update image animations as usual for (const key of ['carWash1', 'carWash2']) { if (animationStartTime[key]) { const elapsed = currentTime - animationStartTime[key]; animationProgress[key] = (elapsed % animationDuration) / animationDuration; } } } function drawClippedImage(image, clipPath) { ctx.save(); ctx.beginPath(); ctx.moveTo(clipPath[0], clipPath[1]); for (let i = 2; i < clipPath.length; i += 2) { ctx.lineTo(clipPath[i], clipPath[i+1]); } ctx.closePath(); ctx.clip(); const clipMinX = Math.min(...clipPath.filter((_, i) => i % 2 === 0)); const clipMaxX = Math.max(...clipPath.filter((_, i) => i % 2 === 0)); const clipMinY = Math.min(...clipPath.filter((_, i) => i % 2 === 1)); const clipMaxY = Math.max(...clipPath.filter((_, i) => i % 2 === 1)); const clipWidth = clipMaxX - clipMinX; const clipHeight = clipMaxY - clipMinY; const imageAspectRatio = image.naturalWidth / image.naturalHeight; const clipAspectRatio = clipWidth / clipHeight; let drawWidth, drawHeight, maxOffsetX, maxOffsetY; if (imageAspectRatio > clipAspectRatio) { // Image is wider than clip area, so we fit to height drawHeight = clipHeight; drawWidth = drawHeight * imageAspectRatio; maxOffsetX = drawWidth - clipWidth; maxOffsetY = 0; } else { // Image is taller than clip area, so we fit to width drawWidth = clipWidth; drawHeight = drawWidth / imageAspectRatio; maxOffsetX = 0; maxOffsetY = drawHeight - clipHeight; } let imageKey = ''; if (image === images.carWash1) { imageKey = 'carWash1'; } else if (image === images.carWash2) { imageKey = 'carWash2'; } else if (image === images.stuttgart) { imageKey = 'stuttgart'; // Add this line to identify the Stuttgart map } let animationOffsetX = 0; let animationOffsetY = 0; if (imageKey && imageKey !== 'stuttgart' && animationProgress[imageKey] !== undefined) { animationOffsetX = maxOffsetX * animationProgress[imageKey]; animationOffsetY = maxOffsetY * animationProgress[imageKey]; } ctx.drawImage(image, clipMinX - animationOffsetX, clipMinY - animationOffsetY, drawWidth, drawHeight); ctx.restore(); } function animate() { updateImageAnimation(); drawCanvas(); requestAnimationFrame(animate); } function calculateFontSize(text, maxWidth, fontWeight = '', maxFontSize = 100) { let fontSize = maxFontSize; do { ctx.font = `${fontWeight} ${fontSize}px "JSCodecPro"`; fontSize--; } while (ctx.measureText(text).width > maxWidth && fontSize > 1); return fontSize + 1; } function adjustFontSize(text, targetWidth, startFontSize, fontWeight) { let fontSize = startFontSize; ctx.font = `${fontWeight} ${fontSize}px "JSCodecPro"`; let width = ctx.measureText(text).width; if (width < targetWidth) { while (width < targetWidth) { fontSize++; ctx.font = `${fontWeight} ${fontSize}px "JSCodecPro"`; width = ctx.measureText(text).width; } fontSize--; } else { while (width > targetWidth && fontSize > 1) { fontSize--; ctx.font = `${fontWeight} ${fontSize}px "JSCodecPro"`; width = ctx.measureText(text).width; } } return fontSize; } function loadImages() { return Promise.all([ new Promise((resolve, reject) => { images.stuttgart.onload = resolve; images.stuttgart.onerror = reject; if (images.stuttgart.complete) resolve(); }), new Promise((resolve, reject) => { images.carWash1.onload = resolve; images.carWash1.onerror = reject; if (images.carWash1.complete) resolve(); }), new Promise((resolve, reject) => { images.carWash2.onload = resolve; images.carWash2.onerror = reject; if (images.carWash2.complete) resolve(); }) ]); } function waitForFont(fontName, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Font loading timed out: ${fontName}`)); }, timeout); document.fonts.load(`12px "${fontName}"`).then(() => { clearTimeout(timeoutId); resolve(); }).catch(reject); }); } // Starten Sie die Schleife in der init-Funktion // Modify the init function // Initialize the loop in the init function as before function init() { document.fonts.add(fontFace); Promise.all([ fontFace.load().then(() => waitForFont('JSCodecPro')), loadImages(), document.fonts.ready ]).then(() => { window.addEventListener('resize', resizeCanvas); resizeCanvas(); setTimeout(() => { resizeCanvas(); }, 100); // Initialize animation parameters resetImageAnimation('carWash1'); resetImageAnimation('carWash2'); animationStartTime.carWash1 = Date.now(); // animationStartTime.carWash2 = Date.now(); const currentTime = Date.now(); resetImageAnimation('carWash1'); animationStartTime.carWash2 = currentTime - 15000; // Start the second image halfway through its cycle animationProgress.carWash2 = 0.5; // Start animation loop requestAnimationFrame(animate); // Start image refresh loop refreshImageLoop(); }).catch(err => { console.error('Error loading resources:', err); }); } init();