Untitled
5 hours ago in HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vector Graphing Calculator</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
/* Simple focus state for better accessibility */
input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
</style>
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col items-center justify-center min-h-screen p-4">
<div class="w-full max-w-6xl bg-white rounded-xl shadow-lg p-6 md:p-8">
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900 text-center">Vector Graphing Calculator</h1>
<p class="text-center text-gray-500 mt-2">Visualize vector-based formulas on a customizable graph.</p>
</div>
<!-- Controls Section -->
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6 p-4 bg-gray-50 rounded-lg border">
<!-- Vector Input -->
<div class="col-span-1 md:col-span-3 lg:col-span-1">
<label for="vectorInput" class="block text-sm font-medium text-gray-700 mb-1">Vector V(x)</label>
<input type="text" id="vectorInput" value="66, x" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500" placeholder="e.g., 66, x">
</div>
<!-- X Domain -->
<div class="col-span-1 lg:col-span-2 grid grid-cols-2 gap-4">
<div>
<label for="xMin" class="block text-sm font-medium text-gray-700 mb-1">X Min</label>
<input type="number" id="xMin" value="-10" class="w-full p-2 border border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label for="xMax" class="block text-sm font-medium text-gray-700 mb-1">X Max</label>
<input type="number" id="xMax" value="10" class="w-full p-2 border border-gray-300 rounded-md shadow-sm">
</div>
</div>
<!-- Y Domain -->
<div class="col-span-1 lg:col-span-2 grid grid-cols-2 gap-4">
<div>
<label for="yMin" class="block text-sm font-medium text-gray-700 mb-1">Y Min</label>
<input type="number" id="yMin" value="-5" class="w-full p-2 border border-gray-300 rounded-md shadow-sm">
</div>
<div>
<label for="yMax" class="block text-sm font-medium text-gray-700 mb-1">Y Max</label>
<input type="number" id="yMax" value="15" class="w-full p-2 border border-gray-300 rounded-md shadow-sm">
</div>
</div>
</div>
<!-- Canvas and Legend -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div class="lg:col-span-3">
<div class="aspect-w-4 aspect-h-3 lg:aspect-w-16 lg:aspect-h-9 w-full bg-white border rounded-lg shadow-sm">
<canvas id="graphCanvas" class="w-full h-full"></canvas>
</div>
</div>
<div class="lg:col-span-1 p-4 bg-gray-50 rounded-lg border flex flex-col justify-center">
<h3 class="text-lg font-semibold mb-4 text-center">Formulas</h3>
<div id="error-message" class="text-red-600 text-sm mb-3 font-medium text-center"></div>
<ul class="space-y-3">
<li class="flex items-center">
<div class="w-4 h-4 rounded-full mr-3" style="background-color: #3b82f6;"></div>
<code class="text-sm">y = Sum(V(x)) / Length(V(x)) - x</code>
</li>
<li class="flex items-center">
<div class="w-4 h-4 rounded-full mr-3" style="background-color: #ef4444;"></div>
<code class="text-sm">y = 1 / Length(V(x))</code>
</li>
</ul>
<p class="text-xs text-gray-500 mt-4 text-center">Where <code class="font-mono">Length(V(x))</code> is the number of elements in the vector.</p>
<!-- Coordinate Display -->
<div id="coords-display" class="mt-4 p-3 bg-gray-200 rounded-md text-sm border border-gray-300">
<div class="font-semibold text-gray-700 text-center mb-2">Live Values</div>
<div class="flex justify-between"><span>X:</span> <span id="coord-x" class="font-mono font-medium"></span></div>
<div class="flex justify-between text-blue-600"><span>Y₁:</span> <span id="coord-y1" class="font-mono font-medium"></span></div>
<div class="flex justify-between text-red-600"><span>Y₂:</span> <span id="coord-y2" class="font-mono font-medium"></span></div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Element Selection ---
const canvas = document.getElementById('graphCanvas');
const ctx = canvas.getContext('2d');
const vectorInput = document.getElementById('vectorInput');
const xMinInput = document.getElementById('xMin');
const xMaxInput = document.getElementById('xMax');
const yMinInput = document.getElementById('yMin');
const yMaxInput = document.getElementById('yMax');
const errorMessage = document.getElementById('error-message');
const coordsDisplay = document.getElementById('coords-display');
const coordX = document.getElementById('coord-x');
const coordY1 = document.getElementById('coord-y1');
const coordY2 = document.getElementById('coord-y2');
const padding = { top: 20, right: 20, bottom: 40, left: 50 };
// --- LocalStorage Logic ---
const saveSettings = () => {
const settings = {
vector: vectorInput.value,
xMin: xMinInput.value,
xMax: xMaxInput.value,
yMin: yMinInput.value,
yMax: yMaxInput.value,
};
localStorage.setItem('graphCalculatorSettings', JSON.stringify(settings));
};
const loadSettings = () => {
const savedSettings = localStorage.getItem('graphCalculatorSettings');
if (savedSettings) {
const settings = JSON.parse(savedSettings);
vectorInput.value = settings.vector || '66, x';
xMinInput.value = settings.xMin || '-10';
xMaxInput.value = settings.xMax || '10';
yMinInput.value = settings.yMin || '-5';
yMaxInput.value = settings.yMax || '15';
}
};
// --- Helper Functions ---
const resetCoordsDisplay = () => {
coordX.textContent = '-.---';
coordY1.textContent = '-.---';
coordY2.textContent = '-.---';
};
const parseVectorExpression = (inputString) => {
if (!inputString.trim()) return null;
const parts = inputString.split(',').map(s => s.trim().toLowerCase());
for (const part of parts) {
if (part !== 'x' && isNaN(parseFloat(part))) {
return null;
}
}
return parts;
};
const evaluatePart = (part, xValue) => {
return part === 'x' ? xValue : parseFloat(part);
};
const calculateVectorSumAtX = (vectorParts, xValue) => {
return vectorParts.reduce((sum, part) => sum + evaluatePart(part, xValue), 0);
};
const resizeCanvas = () => {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
drawGraph();
};
// --- Core Drawing Logic ---
const drawGraph = () => {
errorMessage.textContent = '';
const xMin = parseFloat(xMinInput.value);
const xMax = parseFloat(xMaxInput.value);
const yMin = parseFloat(yMinInput.value);
const yMax = parseFloat(yMaxInput.value);
const vectorParts = parseVectorExpression(vectorInput.value);
if (xMin >= xMax || yMin >= yMax) {
errorMessage.textContent = "Min values must be less than max values.";
return;
}
if (!vectorParts) {
errorMessage.textContent = "Invalid vector. Use numbers or 'x', comma-separated.";
return;
}
const vectorLength = vectorParts.length;
if (vectorLength === 0) {
errorMessage.textContent = "Vector cannot be empty.";
return;
}
const canvasWidth = canvas.clientWidth;
const canvasHeight = canvas.clientHeight;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
const graphWidth = canvasWidth - padding.left - padding.right;
const graphHeight = canvasHeight - padding.top - padding.bottom;
const worldToCanvasX = (x) => padding.left + ((x - xMin) / (xMax - xMin)) * graphWidth;
const worldToCanvasY = (y) => (canvasHeight - padding.bottom) - ((y - yMin) / (yMax - yMin)) * graphHeight;
const canvasToWorldX = (px) => {
if (px < padding.left || px > canvasWidth - padding.right) return null;
return xMin + ((px - padding.left) / graphWidth) * (xMax - xMin);
};
drawAxesAndGrid(worldToCanvasX, worldToCanvasY, xMin, xMax, yMin, yMax, graphWidth, graphHeight);
const line1Points = [];
const line2Points = [];
for (let px = padding.left; px <= canvasWidth - padding.right; px++) {
const x = canvasToWorldX(px);
if (x === null) continue;
const vectorSum = calculateVectorSumAtX(vectorParts, x);
const y1 = (vectorSum / vectorLength) - x;
line1Points.push({x: px, y: worldToCanvasY(y1)});
const y2 = 1 / vectorLength;
line2Points.push({x: px, y: worldToCanvasY(y2)});
}
// Clip drawing to the graph area
ctx.save();
ctx.beginPath();
ctx.rect(padding.left, padding.top, graphWidth, graphHeight);
ctx.clip();
// Draw Line 1
ctx.beginPath();
ctx.strokeStyle = '#3b82f6'; // Blue
ctx.lineWidth = 2.5;
line1Points.forEach((point, i) => {
if (i === 0) ctx.moveTo(point.x, point.y);
else ctx.lineTo(point.x, point.y);
});
ctx.stroke();
// Draw Line 2
ctx.beginPath();
ctx.strokeStyle = '#ef4444'; // Red
ctx.lineWidth = 2.5;
line2Points.forEach((point, i) => {
if (i === 0) ctx.moveTo(point.x, point.y);
else ctx.lineTo(point.x, point.y);
});
ctx.stroke();
ctx.restore(); // Remove clipping
};
const drawAxesAndGrid = (toCanvasX, toCanvasY, xMin, xMax, yMin, yMax, graphWidth, graphHeight) => {
const canvasWidth = canvas.clientWidth;
const canvasHeight = canvas.clientHeight;
ctx.font = "12px Inter";
const xTickCount = 10;
const yTickCount = 10;
const xStep = (xMax - xMin) / xTickCount;
const yStep = (yMax - yMin) / yTickCount;
ctx.strokeStyle = '#e5e7eb'; // Light gray for grid
ctx.lineWidth = 1;
// Vertical grid lines and X-axis labels
for (let i = 0; i <= xTickCount; i++) {
const x = xMin + i * xStep;
const px = toCanvasX(x);
ctx.beginPath();
ctx.moveTo(px, padding.top);
ctx.lineTo(px, canvasHeight - padding.bottom);
ctx.stroke();
ctx.fillStyle = '#6b7280';
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(x.toFixed(1), px, canvasHeight - padding.bottom + 8);
}
// Horizontal grid lines and Y-axis labels
for (let i = 0; i <= yTickCount; i++) {
const y = yMin + i * yStep;
const py = toCanvasY(y);
ctx.beginPath();
ctx.moveTo(padding.left, py);
ctx.lineTo(canvasWidth - padding.right, py);
ctx.stroke();
ctx.fillStyle = '#6b7280';
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText(y.toFixed(1), padding.left - 8, py);
}
ctx.strokeStyle = '#9ca3af';
ctx.lineWidth = 1.5;
// X-Axis (y = 0)
if (yMin <= 0 && yMax >= 0) {
const yZero = toCanvasY(0);
ctx.beginPath();
ctx.moveTo(padding.left, yZero);
ctx.lineTo(canvasWidth - padding.right, yZero);
ctx.stroke();
}
// Y-Axis (x = 0)
if (xMin <= 0 && xMax >= 0) {
const xZero = toCanvasX(0);
ctx.beginPath();
ctx.moveTo(xZero, padding.top);
ctx.lineTo(xZero, canvasHeight - padding.bottom);
ctx.stroke();
}
};
// --- Event Listeners ---
[vectorInput, xMinInput, xMaxInput, yMinInput, yMaxInput].forEach(input => {
input.addEventListener('input', () => {
drawGraph();
saveSettings();
});
});
canvas.addEventListener('mousemove', (event) => {
const vectorParts = parseVectorExpression(vectorInput.value);
const xMin = parseFloat(xMinInput.value);
const xMax = parseFloat(xMaxInput.value);
if (xMin >= xMax || !vectorParts || vectorParts.length === 0) {
resetCoordsDisplay();
return;
}
const graphWidth = canvas.clientWidth - padding.left - padding.right;
const worldX = xMin + ((event.offsetX - padding.left) / graphWidth) * (xMax - xMin);
if (event.offsetX < padding.left || event.offsetX > canvas.clientWidth - padding.right ||
event.offsetY < padding.top || event.offsetY > canvas.clientHeight - padding.bottom) {
resetCoordsDisplay();
return;
}
const vectorLength = vectorParts.length;
const vectorSum = calculateVectorSumAtX(vectorParts, worldX);
const worldY1 = (vectorSum / vectorLength) - worldX;
const worldY2 = 1 / vectorLength;
coordX.textContent = worldX.toFixed(3);
coordY1.textContent = worldY1.toFixed(3);
coordY2.textContent = worldY2.toFixed(3);
});
canvas.addEventListener('mouseleave', resetCoordsDisplay);
window.addEventListener('resize', resizeCanvas);
// --- Initial Load ---
loadSettings();
resizeCanvas();
resetCoordsDisplay();
});
</script>
</body>
</html>