Ecobot_Scoala_Verde/frontend/js/character.js
Stefan Caramizoiu d7a7d2cafd Initial commit
2026-04-01 11:14:26 +03:00

290 lines
8.5 KiB
JavaScript

class Character {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.width = this.canvas.width;
this.height = this.canvas.height;
// Character state
this.state = 'idle'; // idle, listening, thinking, talking
this.mouthOpen = 0;
this.eyeBlink = 0;
this.blinkTimer = 0;
this.bounceOffset = 0;
this.bounceSpeed = 0.02;
this.time = 0;
// Leaf particles
this.leaves = [];
for (let i = 0; i < 5; i++) {
this.leaves.push({
x: Math.random() * this.width,
y: Math.random() * this.height,
size: 5 + Math.random() * 8,
speed: 0.3 + Math.random() * 0.5,
wobble: Math.random() * Math.PI * 2,
});
}
this.animate();
}
setState(state) {
this.state = state;
}
setMouthOpen(value) {
this.mouthOpen = Math.max(0, Math.min(1, value));
}
animate() {
this.time += 1;
this.update();
this.draw();
requestAnimationFrame(() => this.animate());
}
update() {
// Bounce
this.bounceOffset = Math.sin(this.time * this.bounceSpeed) * 5;
// Blink
this.blinkTimer++;
if (this.blinkTimer > 150 + Math.random() * 100) {
this.eyeBlink = 1;
this.blinkTimer = 0;
}
if (this.eyeBlink > 0) {
this.eyeBlink -= 0.15;
if (this.eyeBlink < 0) this.eyeBlink = 0;
}
// Mouth animation when talking
if (this.state === 'talking') {
this.mouthOpen = 0.3 + Math.sin(this.time * 0.3) * 0.3 + Math.sin(this.time * 0.17) * 0.2;
} else if (this.state !== 'talking') {
this.mouthOpen *= 0.9;
}
// Update leaves
this.leaves.forEach(leaf => {
leaf.y += leaf.speed;
leaf.x += Math.sin(leaf.wobble + this.time * 0.02) * 0.5;
leaf.wobble += 0.01;
if (leaf.y > this.height + 10) {
leaf.y = -10;
leaf.x = Math.random() * this.width;
}
});
}
draw() {
const ctx = this.ctx;
const cx = this.width / 2;
const cy = this.height / 2 + 30;
ctx.clearRect(0, 0, this.width, this.height);
// Draw falling leaves
this.drawLeaves(ctx);
const by = cy + this.bounceOffset;
// Body - a friendly tree trunk shape
this.drawBody(ctx, cx, by);
// Eyes
this.drawEyes(ctx, cx, by);
// Mouth
this.drawMouth(ctx, cx, by);
// Leaves on top (hair)
this.drawCrown(ctx, cx, by);
// Status indicator
this.drawStatusGlow(ctx, cx, by);
}
drawBody(ctx, cx, cy) {
// Tree trunk body
ctx.fillStyle = '#8B6914';
ctx.beginPath();
ctx.moveTo(cx - 40, cy + 60);
ctx.quadraticCurveTo(cx - 50, cy - 20, cx - 30, cy - 60);
ctx.quadraticCurveTo(cx, cy - 75, cx + 30, cy - 60);
ctx.quadraticCurveTo(cx + 50, cy - 20, cx + 40, cy + 60);
ctx.closePath();
ctx.fill();
// Lighter bark detail
ctx.fillStyle = '#A07828';
ctx.beginPath();
ctx.moveTo(cx - 25, cy + 60);
ctx.quadraticCurveTo(cx - 30, cy - 10, cx - 15, cy - 50);
ctx.quadraticCurveTo(cx, cy - 60, cx + 15, cy - 50);
ctx.quadraticCurveTo(cx + 30, cy - 10, cx + 25, cy + 60);
ctx.closePath();
ctx.fill();
// Small arms (branches)
ctx.strokeStyle = '#8B6914';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
// Left arm
const armWave = Math.sin(this.time * 0.05) * 5;
ctx.beginPath();
ctx.moveTo(cx - 35, cy - 10);
ctx.quadraticCurveTo(cx - 65, cy - 25 + armWave, cx - 70, cy - 40 + armWave);
ctx.stroke();
// Right arm
ctx.beginPath();
ctx.moveTo(cx + 35, cy - 10);
ctx.quadraticCurveTo(cx + 65, cy - 25 - armWave, cx + 70, cy - 40 - armWave);
ctx.stroke();
// Small leaves on arm tips
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.ellipse(cx - 73, cy - 43 + armWave, 8, 5, -0.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + 73, cy - 43 - armWave, 8, 5, 0.5, 0, Math.PI * 2);
ctx.fill();
// Feet (roots)
ctx.fillStyle = '#8B6914';
ctx.beginPath();
ctx.ellipse(cx - 20, cy + 65, 18, 8, -0.2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + 20, cy + 65, 18, 8, 0.2, 0, Math.PI * 2);
ctx.fill();
}
drawEyes(ctx, cx, cy) {
const eyeY = cy - 25;
const eyeSpacing = 18;
const eyeHeight = this.eyeBlink > 0.5 ? 2 : 10;
// White of eyes
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.ellipse(cx - eyeSpacing, eyeY, 10, eyeHeight, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + eyeSpacing, eyeY, 10, eyeHeight, 0, 0, Math.PI * 2);
ctx.fill();
if (this.eyeBlink <= 0.5) {
// Pupils
ctx.fillStyle = '#2E7D32';
ctx.beginPath();
ctx.ellipse(cx - eyeSpacing + 1, eyeY + 1, 5, 6, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx + eyeSpacing + 1, eyeY + 1, 5, 6, 0, 0, Math.PI * 2);
ctx.fill();
// Pupil shine
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(cx - eyeSpacing + 3, eyeY - 2, 2, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx + eyeSpacing + 3, eyeY - 2, 2, 0, Math.PI * 2);
ctx.fill();
}
}
drawMouth(ctx, cx, cy) {
const mouthY = cy + 5;
const openAmount = this.mouthOpen * 12;
if (openAmount > 1) {
// Open mouth
ctx.fillStyle = '#5D2906';
ctx.beginPath();
ctx.ellipse(cx, mouthY + openAmount / 2, 12, openAmount, 0, 0, Math.PI * 2);
ctx.fill();
// Tongue
ctx.fillStyle = '#E57373';
ctx.beginPath();
ctx.ellipse(cx, mouthY + openAmount, 6, 3, 0, 0, Math.PI);
ctx.fill();
} else {
// Smile
ctx.strokeStyle = '#5D2906';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.arc(cx, mouthY - 5, 12, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.stroke();
}
}
drawCrown(ctx, cx, cy) {
const crownY = cy - 65;
const sway = Math.sin(this.time * 0.03) * 3;
// Main crown
const greens = ['#2E7D32', '#4CAF50', '#66BB6A', '#388E3C'];
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const r = 35 + Math.sin(this.time * 0.02 + i) * 3;
const lx = cx + Math.cos(angle) * r + sway;
const ly = crownY + Math.sin(angle) * 25 - 5;
ctx.fillStyle = greens[i % greens.length];
ctx.beginPath();
ctx.ellipse(lx, ly, 20, 15, angle * 0.3, 0, Math.PI * 2);
ctx.fill();
}
// Center top
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.ellipse(cx + sway, crownY - 25, 18, 14, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#66BB6A';
ctx.beginPath();
ctx.ellipse(cx + sway + 5, crownY - 30, 12, 10, 0.3, 0, Math.PI * 2);
ctx.fill();
}
drawLeaves(ctx) {
ctx.fillStyle = 'rgba(76, 175, 80, 0.4)';
this.leaves.forEach(leaf => {
ctx.save();
ctx.translate(leaf.x, leaf.y);
ctx.rotate(leaf.wobble + this.time * 0.02);
ctx.beginPath();
ctx.ellipse(0, 0, leaf.size, leaf.size * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
}
drawStatusGlow(ctx, cx, cy) {
if (this.state === 'idle') return;
const colors = {
listening: 'rgba(244, 67, 54, 0.3)',
thinking: 'rgba(255, 193, 7, 0.3)',
talking: 'rgba(76, 175, 80, 0.3)',
};
const color = colors[this.state] || 'transparent';
const pulse = Math.sin(this.time * 0.1) * 10 + 20;
ctx.beginPath();
ctx.arc(cx, cy - 20, 80 + pulse, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
}