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’,
valley: ‘
https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&q=80’,
cottage1: ‘
https://images.unsplash.com/photo-1518780664697-55e3ad937233?w=400&q=80’,
cottage2: ‘
https://images.unsplash.com/photo-1449158743715-0a90ebb6d2d8?w=400&q=80’,
cottage3: ‘
https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?w=400&q=80’,
cottage4: ‘
https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=400&q=80’,
leaves: ‘
https://images.unsplash.com/photo-1502082553048-f009c37129b9?w=400&q=80’
};
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);
})();