Show description
CALC 2: The Game - Interactive Grapher
CALC 2: The Game - Interactive Grapher
CALC 2: THE GAME
PLAYER: LALO
LEVEL SELECT
Custom Input
Level 1: Limaçon's Loop
Level 2: Clash of Curves
Level 3: Infinite Spiral
Level 4: Heart of the Matter
Level 5: Rose Petal Puzzle
Curve 1 (r=)
Curve 2 (r=)
START LEVEL
CALC 2: The Game - Interactive Grapher
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CALC 2: The Game - Interactive Grapher</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=Press+Start+2P&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Press Start 2P', cursive;
background-color: #1a1a1a;
color: #ffffff;
image-rendering: pixelated; /* Ensures sharp pixels for the 8-bit feel */
}
.game-container {
display: grid;
grid-template-columns: 1fr;
lg:grid-template-columns: 2fr 1fr;
gap: 2rem;
max-width: 1200px;
margin: auto;
padding: 1rem;
}
#graph-container {
border: 4px solid #fff;
background-color: #000;
box-shadow: 8px 8px 0 #5d64f5;
position: relative;
aspect-ratio: 1/1;
}
#graph-canvas {
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
}
.controls-container {
background-color: #2c2c2c;
border: 4px solid #fff;
padding: 1.5rem;
box-shadow: 8px 8px 0 #f5a623;
}
.input-8bit, .select-8bit {
background-color: #1a1a1a;
border: 2px solid #fff;
padding: 10px;
color: #fff;
font-family: 'Press Start 2P', cursive;
width: 100%;
margin-bottom: 1rem;
}
.btn-8bit {
background-color: #5d64f5;
border: none;
padding: 12px 24px;
color: white;
text-align: center;
font-size: 16px;
cursor: pointer;
position: relative;
box-shadow: inset -4px -4px 0px 0px #3a3d99;
transition: all 0.1s ease-in-out;
width: 100%;
}
.btn-8bit:hover, .btn-8bit:focus {
background-color: #7178ff;
box-shadow: inset -6px -6px 0px 0px #3a3d99;
}
.btn-8bit:active {
box-shadow: inset 4px 4px 0px 0px #3a3d99;
}
</style>
</head>
<body class="p-4 md:p-8">
<header class="text-center mb-8">
<h1 class="text-3xl md:text-4xl text-yellow-400 tracking-wider">CALC 2: THE GAME</h1>
<p class="text-green-400 mt-2">PLAYER: LALO</p>
</header>
<div class="game-container">
<div id="graph-container">
<canvas id="graph-canvas"></canvas>
</div>
<div class="controls-container">
<h2 class="text-2xl text-center mb-4 text-orange-400">LEVEL SELECT</h2>
<select id="level-select" class="select-8bit">
<option value="custom">Custom Input</option>
<option value="level1">Level 1: Limaçon's Loop</option>
<option value="level2">Level 2: Clash of Curves</option>
<option value="level3">Level 3: Infinite Spiral</option>
<option value="level4">Level 4: Heart of the Matter</option>
<option value="level5">Level 5: Rose Petal Puzzle</option>
</select>
<label for="r1-input" class="mt-4 block">Curve 1 (r=)</label>
<input type="text" id="r1-input" class="input-8bit" value="5-5*sin(theta)">
<label for="r2-input" class="mt-4 block">Curve 2 (r=)</label>
<input type="text" id="r2-input" class="input-8bit" value="">
<button id="plot-button" class="btn-8bit mt-4">START LEVEL</button>
</div>
</div>
<script>
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('graph-container');
const r1Input = document.getElementById('r1-input');
const r2Input = document.getElementById('r2-input');
const plotButton = document.getElementById('plot-button');
const levelSelect = document.getElementById('level-select');
// --- Level Data ---
const levels = {
level1: { r1: '11*cos(theta)-8', r2: '' },
level2: { r1: '6*sin(theta)', r2: '4' },
level3: { r1: '9*exp(-theta)', r2: '' },
level4: { r1: '5-5*sin(theta)', r2: '' },
level5: { r1: '7*sin(2*theta)', r2: '7*sin(theta)' },
custom: { r1: '5-5*sin(theta)', r2: ''}
};
// --- Event Listeners ---
plotButton.addEventListener('click', draw);
levelSelect.addEventListener('change', (e) => {
const level = levels[e.target.value];
r1Input.value = level.r1;
r2Input.value = level.r2;
draw();
});
window.addEventListener('resize', draw);
// --- Drawing Logic ---
function draw() {
// Resize canvas to fit container
const size = container.clientWidth;
canvas.width = size;
canvas.height = size;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Find max r to scale the graph
let maxR = 0;
try {
// We need to evaluate the functions to find a good scale.
// This is a simplified approach. A more robust solution would parse the function.
// For now, we'll use a fixed scale or a simple heuristic.
const r1Func = new Function('theta', `with(Math) { return ${r1Input.value || 0}; }`);
const r2Func = new Function('theta', `with(Math) { return ${r2Input.value || 0}; }`);
for (let i = 0; i < 360; i++) {
const theta = i * Math.PI / 180;
if (r1Input.value) maxR = Math.max(maxR, Math.abs(r1Func(theta)));
if (r2Input.value) maxR = Math.max(maxR, Math.abs(r2Func(theta)));
}
} catch (e) {
console.error("Error parsing function:", e);
maxR = 10; // Default scale on error
}
if (maxR === 0) maxR = 10; // Default scale if no input
const scale = size / (maxR * 2.2);
// Clear canvas and draw axes
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawAxes(centerX, centerY, scale, maxR);
// Plot curves
if (r1Input.value) {
plot(r1Input.value, '#ff6347', centerX, centerY, scale); // Tomato Red
}
if (r2Input.value) {
plot(r2Input.value, '#1e90ff', centerX, centerY, scale); // Dodger Blue
}
}
function drawAxes(cx, cy, scale, maxR) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 2;
// X and Y axes
ctx.beginPath();
ctx.moveTo(0, cy);
ctx.lineTo(canvas.width, cy);
ctx.moveTo(cx, 0);
ctx.lineTo(cx, canvas.height);
ctx.stroke();
// Axis labels
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.font = '12px "Press Start 2P"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const numTicks = 5;
for(let i = 1; i <= numTicks; i++) {
const tickVal = (maxR / numTicks * i).toFixed(1);
// Positive X
ctx.fillText(tickVal, cx + i * (maxR * scale / numTicks), cy + 15);
// Positive Y
ctx.fillText(tickVal, cx + 15, cy - i * (maxR * scale / numTicks));
}
}
function plot(funcStr, color, cx, cy, scale) {
ctx.strokeStyle = color;
ctx.lineWidth = 4;
ctx.beginPath();
let func;
try {
// Using 'with(Math)' to allow direct use of Math functions like sin, cos, etc.
func = new Function('theta', `with(Math) { return ${funcStr}; }`);
} catch (e) {
console.error("Could not plot function:", funcStr, e);
return; // Exit if function is invalid
}
let firstPoint = true;
for (let i = 0; i <= 360 * 4; i++) { // Increased resolution
const theta = (i / 4) * (Math.PI / 180);
let r;
try {
r = func(theta);
} catch(e) {
break; // Stop plotting if function fails
}
if (typeof r !== 'number' || !isFinite(r)) continue;
const x = cx + r * Math.cos(theta) * scale;
const y = cy - r * Math.sin(theta) * scale; // Y is inverted in canvas
if (firstPoint) {
ctx.moveTo(x, y);
firstPoint = false;
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
// Initial draw on page load
draw();
</script>
</body>
</html>