test Page

Loading images…

The Great Birch Valley

A massive birch tree towers over the valley, its trunk and branches home to a thriving community. Click and drag to explore, scroll to zoom.

Reset View Zoom: 100%
(function() { var container = document.getElementById(‘birch-tree-valley-container’); var canvas = document.getElementById(‘btv-canvas’); var ctx = canvas.getContext(‘2d’); var tooltip = document.getElementById(‘btv-tooltip’); var zoomLevel = document.getElementById(‘btv-zoom-level’); var loadingDiv = document.getElementById(‘btv-loading’); var resetBtn = document.getElementById(‘btv-reset-btn’); function resizeCanvas() { var rect = container.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; } resizeCanvas(); var images = { birchBark: ‘https://images.unsplash.com/photo-1709556062089-88a4a88ba03b?w=800&q=80&#8217;, valley: ‘https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&q=80&#8217;, cottage1: ‘https://images.unsplash.com/photo-1518780664697-55e3ad937233?w=400&q=80&#8217;, cottage2: ‘https://images.unsplash.com/photo-1449158743715-0a90ebb6d2d8?w=400&q=80&#8217;, cottage3: ‘https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400&q=80&#8217;, cottage4: ‘https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=400&q=80&#8217;, leaves: ‘https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=400&q=80&#8217; }; var loadedImages = {}; var imagesLoaded = 0; var totalImages = Object.keys(images).length; Object.keys(images).forEach(function(key) { var img = new Image(); img.crossOrigin = ‘anonymous’; img.onload = function() { imagesLoaded++; if (imagesLoaded === totalImages) { loadingDiv.style.display = ‘none’; animate(); } }; img.onerror = function() { imagesLoaded++; if (imagesLoaded === totalImages) { loadingDiv.style.display = ‘none’; animate(); } }; img.src = images[key]; loadedImages[key] = img; }); var camera = { x: 0, y: 0, zoom: 1, targetZoom: 1 }; var isDragging = false; var lastMouseX = 0; var lastMouseY = 0; var buildings = [ { x: -150, y: 500, width: 80, height: 100, label: ‘Root Village Inn’, type: ‘base’, imageKey: ‘cottage1’ }, { x: -80, y: 520, width: 70, height: 80, label: ‘Market Square’, type: ‘base’, imageKey: ‘cottage2’ }, { x: 20, y: 510, width: 90, height: 110, label: ‘Town Hall’, type: ‘base’, imageKey: ‘cottage3’ }, { x: 100, y: 530, width: 65, height: 75, label: ‘Baker Street’, type: ‘base’, imageKey: ‘cottage4’ }, { x: -200, y: 545, width: 60, height: 70, label: ‘Old Mill’, type: ‘base’, imageKey: ‘cottage1’ }, { x: -60, y: 350, width: 70, height: 80, label: ‘Midway Station’, type: ‘trunk’, imageKey: ‘cottage2’ }, { x: 30, y: 360, width: 75, height: 85, label: ‘Skyward Plaza’, type: ‘trunk’, imageKey: ‘cottage3’ }, { x: -300, y: 150, width: 65, height: 70, label: ‘Westwind Hamlet’, type: ‘branch’, imageKey: ‘cottage4’ }, { x: -350, y: 120, width: 55, height: 60, label: ‘Canopy Gardens’, type: ‘branch’, imageKey: ‘cottage1’ }, { x: -280, y: 80, width: 60, height: 65, label: ‘Leaf District’, type: ‘branch’, imageKey: ‘cottage2’ }, { x: 250, y: 170, width: 68, height: 72, label: ‘Eastbranch Heights’, type: ‘branch’, imageKey: ‘cottage3’ }, { x: 300, y: 130, width: 58, height: 62, label: ‘Sunlit Terraces’, type: ‘branch’, imageKey: ‘cottage4’ }, { x: 280, y: 90, width: 62, height: 68, label: ‘Treetop Square’, type: ‘branch’, imageKey: ‘cottage1’ }, { x: -150, y: 50, width: 55, height: 58, label: ‘Birdsong Chapel’, type: ‘branch’, imageKey: ‘cottage2’ }, { x: 100, y: 40, width: 52, height: 56, label: ‘Sky Observatory’, type: ‘branch’, imageKey: ‘cottage3’ }, { x: 0, y: 20, width: 50, height: 55, label: ‘Crown Lookout’, type: ‘branch’, imageKey: ‘cottage4’ } ]; var tree = { trunk: { x: 0, y: 600, width: 200, height: 500 }, branches: [ { startX: -40, startY: 250, endX: -350, endY: 150, width: 60 }, { startX: 40, startY: 250, endX: 320, endY: 170, width: 60 }, { startX: -30, startY: 180, endX: -300, endY: 80, width: 45 }, { startX: 30, startY: 180, endX: 300, endY: 90, width: 45 }, { startX: -20, startY: 120, endX: -180, endY: 40, width: 35 }, { startX: 20, startY: 120, endX: 130, endY: 30, width: 35 }, { startX: 0, startY: 80, endX: -100, endY: -20, width: 25 }, { startX: 0, startY: 80, endX: 100, endY: -30, width: 25 } ] }; function drawImageWithBorder(img, x, y, width, height, borderWidth) { borderWidth = borderWidth || 3; if (img && img.complete) { ctx.shadowColor = ‘rgba(0, 0, 0, 0.5)’; ctx.shadowBlur = 10; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3; ctx.fillStyle = ‘#8B4513’; ctx.fillRect(x – borderWidth, y – borderWidth, width + borderWidth * 2, height + borderWidth * 2); ctx.shadowColor = ‘transparent’; ctx.shadowBlur = 0; ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.drawImage(img, x, y, width, height); ctx.fillStyle = ‘rgba(139, 69, 19, 0.1)’; ctx.fillRect(x, y, width, height); } } function drawBranch(branch) { var img = loadedImages.birchBark; if (img && img.complete) { ctx.save(); var dx = branch.endX – branch.startX; var dy = branch.endY – branch.startY; var angle = Math.atan2(dy, dx); var length = Math.sqrt(dx * dx + dy * dy); ctx.translate(branch.startX, branch.startY); ctx.rotate(angle); ctx.drawImage(img, 0, -branch.width / 2, length, branch.width); var gradient = ctx.createLinearGradient(0, -branch.width / 2, 0, branch.width / 2); gradient.addColorStop(0, ‘rgba(0, 0, 0, 0.3)’); gradient.addColorStop(0.5, ‘rgba(255, 255, 255, 0.1)’); gradient.addColorStop(1, ‘rgba(0, 0, 0, 0.3)’); ctx.fillStyle = gradient; ctx.fillRect(0, -branch.width / 2, length, branch.width); ctx.restore(); } } function drawLeaves() { var img = loadedImages.leaves; tree.branches.forEach(function(branch) { var numLeaves = 15; for (var i = 0; i < numLeaves; i++) { var t = 0.5 + (i / numLeaves) * 0.5; var x = branch.startX + (branch.endX – branch.startX) * t; var y = branch.startY + (branch.endY – branch.startY) * t; var offsetX = (Math.random() – 0.5) * 80; var offsetY = (Math.random() – 0.5) * 80; var size = 30 + Math.random() * 25; ctx.save(); ctx.globalAlpha = 0.7 + Math.random() * 0.3; if (img && img.complete) { ctx.drawImage(img, x + offsetX – size/2, y + offsetY – size/2, size, size); } else { ctx.fillStyle = 'rgba(' + (100 + Math.random() * 50) + ', ' + (200 + Math.random() * 55) + ', ' + (100 + Math.random() * 50) + ', 0.7)'; ctx.beginPath(); ctx.arc(x + offsetX, y + offsetY, size/2, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); } }); } function drawBuilding(building) { var img = loadedImages[building.imageKey]; if (img && img.complete) { drawImageWithBorder(img, building.x, building.y, building.width, building.height); } else { var colors = { base: '#D2691E', trunk: '#BC8F8F', branch: '#8FBC8F' }; ctx.fillStyle = colors[building.type] || '#D2691E'; ctx.fillRect(building.x, building.y, building.width, building.height); ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(building.x, building.y, building.width, building.height); } } function drawValley() { var img = loadedImages.valley; if (img && img.complete) { ctx.save(); ctx.globalAlpha = 0.8; ctx.drawImage(img, -800, 300, 1600, 800); ctx.restore(); var gradient = ctx.createLinearGradient(0, 400, 0, 900); gradient.addColorStop(0, 'rgba(144, 238, 144, 0)'); gradient.addColorStop(1, 'rgba(144, 238, 144, 0.6)'); ctx.fillStyle = gradient; ctx.fillRect(-1000, 400, 2000, 500); } else { ctx.fillStyle = '#8B7D6B'; ctx.beginPath(); ctx.moveTo(-500, 700); ctx.lineTo(-300, 400); ctx.lineTo(-100, 500); ctx.lineTo(100, 450); ctx.lineTo(300, 500); ctx.lineTo(500, 450); ctx.lineTo(800, 700); ctx.closePath(); ctx.fill(); } ctx.fillStyle = '#90EE90'; ctx.fillRect(-1000, 600, 2000, 400); } function screenToWorld(x, y) { return { x: (x – canvas.width / 2) / camera.zoom + camera.x, y: (y – canvas.height / 2) / camera.zoom + camera.y }; } function draw() { ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.translate(canvas.width / 2, canvas.height / 2); ctx.scale(camera.zoom, camera.zoom); ctx.translate(-camera.x, -camera.y); drawValley(); var barkImg = loadedImages.birchBark; if (barkImg && barkImg.complete) { ctx.save(); ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'; ctx.shadowBlur = 20; ctx.shadowOffsetX = 5; ctx.shadowOffsetY = 5; ctx.drawImage(barkImg, tree.trunk.x – tree.trunk.width / 2, tree.trunk.y – tree.trunk.height, tree.trunk.width, tree.trunk.height); ctx.restore(); } tree.branches.forEach(drawBranch); drawLeaves(); buildings.forEach(drawBuilding); } function animate() { camera.zoom += (camera.targetZoom – camera.zoom) * 0.1; zoomLevel.textContent = Math.round(camera.zoom * 100) + '%'; draw(); requestAnimationFrame(animate); } canvas.addEventListener('mousedown', function(e) { isDragging = true; lastMouseX = e.clientX; lastMouseY = e.clientY; }); canvas.addEventListener('mousemove', function(e) { if (isDragging) { var dx = e.clientX – lastMouseX; var dy = e.clientY – lastMouseY; camera.x -= dx / camera.zoom; camera.y -= dy / camera.zoom; lastMouseX = e.clientX; lastMouseY = e.clientY; } else { var rect = canvas.getBoundingClientRect(); var worldPos = screenToWorld(e.clientX – rect.left, e.clientY – rect.top); var hoveredBuilding = null; for (var i = 0; i = building.x && worldPos.x = building.y && worldPos.y <= building.y + building.height) { hoveredBuilding = building; break; } } if (hoveredBuilding) { tooltip.textContent = hoveredBuilding.label; tooltip.style.left = e.clientX + 10 + 'px'; tooltip.style.top = e.clientY + 10 + 'px'; tooltip.style.opacity = '1'; canvas.style.cursor = 'pointer'; } else { tooltip.style.opacity = '0'; canvas.style.cursor = isDragging ? 'grabbing' : 'grab'; } } }); canvas.addEventListener('mouseup', function() { isDragging = false; }); canvas.addEventListener('mouseleave', function() { isDragging = false; tooltip.style.opacity = '0'; }); canvas.addEventListener('wheel', function(e) { e.preventDefault(); var zoomSpeed = 0.001; camera.targetZoom *= (1 – e.deltaY * zoomSpeed); camera.targetZoom = Math.max(0.3, Math.min(3, camera.targetZoom)); }); resetBtn.addEventListener('click', function() { camera.x = 0; camera.y = 0; camera.targetZoom = 1; }); resetBtn.addEventListener('mouseenter', function() { resetBtn.style.background = '#45a049'; }); resetBtn.addEventListener('mouseleave', function() { resetBtn.style.background = '#4CAF50'; }); window.addEventListener('resize', resizeCanvas); })();