589 lines
17 KiB
JavaScript
589 lines
17 KiB
JavaScript
// Zombie Entities (Normal, Conehead, Buckethead, Flag, PoleVaulter, Newspaper, Football, DuckyTube)
|
|
window.Zombie = class Zombie {
|
|
constructor(row, type = 'normal', startX = 940) {
|
|
this.row = row;
|
|
this.type = type;
|
|
this.x = startX;
|
|
this.y = 0;
|
|
this.width = 60;
|
|
this.height = 90;
|
|
|
|
this.baseSpeed = 22; // default walking speed
|
|
this.eatingDps = 30;
|
|
|
|
this.isEating = false;
|
|
this.targetPlant = null;
|
|
this.eatTimer = 0;
|
|
|
|
this.slowTimer = 0;
|
|
this.freezeTimer = 0;
|
|
this.hitFlashTimer = 0;
|
|
this.headSpawned = false;
|
|
this.animTime = Math.random() * 5;
|
|
this.toRemove = false;
|
|
|
|
// Type specifics
|
|
this.hasPole = type === 'polevaulter';
|
|
this.isVaulting = false;
|
|
this.vaultTimer = 0;
|
|
|
|
this.hasPaper = type === 'newspaper';
|
|
this.paperHealth = 100;
|
|
this.isAngry = false;
|
|
|
|
switch (type) {
|
|
case 'conehead':
|
|
this.maxHealth = 200;
|
|
this.helmetHealth = 100;
|
|
break;
|
|
case 'buckethead':
|
|
this.maxHealth = 350;
|
|
this.helmetHealth = 250;
|
|
break;
|
|
case 'football':
|
|
this.maxHealth = 350;
|
|
this.helmetHealth = 250;
|
|
this.baseSpeed = 42;
|
|
break;
|
|
case 'polevaulter':
|
|
this.maxHealth = 180;
|
|
this.baseSpeed = 44; // Fast until vault
|
|
break;
|
|
case 'newspaper':
|
|
this.maxHealth = 200;
|
|
this.baseSpeed = 18;
|
|
break;
|
|
case 'duckytube':
|
|
this.maxHealth = 120;
|
|
this.baseSpeed = 24;
|
|
break;
|
|
case 'flag':
|
|
this.maxHealth = 140;
|
|
this.baseSpeed = 26;
|
|
break;
|
|
default:
|
|
this.maxHealth = 100;
|
|
this.helmetHealth = 0;
|
|
break;
|
|
}
|
|
|
|
// Apply Zombie Lab Mutations (if in I, Zombie mode or if mutation level passed)
|
|
this.mutationLevel = 1;
|
|
if (window._pvzEngine && window._pvzEngine.zombieLab && window._pvzEngine.currentMode === 'izombie') {
|
|
this.mutationLevel = window._pvzEngine.zombieLab.getZombieLevel(type) || 1;
|
|
}
|
|
|
|
if (this.mutationLevel >= 2) {
|
|
if (type === 'normal') this.baseSpeed *= 1.35;
|
|
if (type === 'conehead') this.helmetHealth += 150;
|
|
if (type === 'polevaulter') this.maxVaults = 2;
|
|
if (type === 'buckethead') this.helmetHealth += 250;
|
|
if (type === 'football') this.helmetHealth += 250;
|
|
}
|
|
if (this.mutationLevel >= 3) {
|
|
if (type === 'normal') this.isVampire = true;
|
|
if (type === 'conehead') { this.helmetHealth += 150; this.reflectDmg = 0.25; }
|
|
if (type === 'polevaulter') { this.maxVaults = 2; this.turboSprint = true; }
|
|
if (type === 'buckethead') { this.helmetHealth += 300; this.iceImmune = true; }
|
|
if (type === 'football') { this.helmetHealth += 300; this.mineImmune = true; }
|
|
}
|
|
|
|
if (window.PvZConfig) {
|
|
const hpMult = window.PvZConfig.getZombieHpMult();
|
|
const speedMult = window.PvZConfig.getZombieSpeedMult();
|
|
const dmgMult = window.PvZConfig.getZombieDamageMult();
|
|
|
|
this.maxHealth = Math.round(this.maxHealth * hpMult);
|
|
if (this.helmetHealth) this.helmetHealth = Math.round(this.helmetHealth * hpMult);
|
|
this.baseSpeed = this.baseSpeed * speedMult;
|
|
this.eatingDps = this.eatingDps * dmgMult;
|
|
}
|
|
|
|
this.speed = this.baseSpeed;
|
|
this.health = this.maxHealth;
|
|
}
|
|
|
|
get isSlowed() {
|
|
return this.slowTimer > 0 || this.freezeTimer > 0;
|
|
}
|
|
|
|
set isSlowed(val) {
|
|
if (val && this.slowTimer <= 0) {
|
|
this.slowTimer = 4.0;
|
|
} else if (!val) {
|
|
this.slowTimer = 0;
|
|
this.freezeTimer = 0;
|
|
}
|
|
}
|
|
|
|
update(dt, gameState) {
|
|
this.animTime += dt;
|
|
|
|
if (this.health <= 0) {
|
|
if (!this.headSpawned && gameState && gameState.combat) {
|
|
this.headSpawned = true;
|
|
gameState.combat.spawnDecapitatedHead(
|
|
this.x,
|
|
this.y - 24,
|
|
this.type,
|
|
this.isAngry,
|
|
this.isSlowed,
|
|
gameState.tree ? gameState.tree.cheats : null
|
|
);
|
|
}
|
|
this.toRemove = true;
|
|
return;
|
|
}
|
|
|
|
if (this.hitFlashTimer > 0) {
|
|
this.hitFlashTimer -= dt;
|
|
if (this.hitFlashTimer < 0) this.hitFlashTimer = 0;
|
|
}
|
|
|
|
if (this.freezeTimer > 0) {
|
|
this.freezeTimer -= dt;
|
|
if (this.freezeTimer < 0) this.freezeTimer = 0;
|
|
}
|
|
|
|
// Freeze / slow effect countdown
|
|
if (this.slowTimer > 0 || this.freezeTimer > 0) {
|
|
if (this.slowTimer > 0) this.slowTimer -= dt;
|
|
this.speed = this.baseSpeed * (this.freezeTimer > 0 ? 0.35 : 0.5);
|
|
} else {
|
|
this.speed = this.baseSpeed;
|
|
}
|
|
|
|
// Pole Vaulter jump logic
|
|
if (this.isVaulting) {
|
|
this.vaultTimer += dt;
|
|
// Arc jump over plant (travels ~90px leftwards in 0.6s)
|
|
this.x -= 150 * dt;
|
|
if (this.vaultTimer >= 0.6) {
|
|
this.isVaulting = false;
|
|
if (this.maxVaults && this.maxVaults > 1) {
|
|
this.maxVaults--;
|
|
this.hasPole = true; // Can vault second time!
|
|
}
|
|
const postSpeed = this.turboSprint ? 35 : 22;
|
|
this.baseSpeed = postSpeed;
|
|
this.speed = postSpeed;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if eating a plant
|
|
if (this.targetPlant) {
|
|
// Pole vaulter vaults over first encountered plant!
|
|
if (this.hasPole && !this.isVaulting) {
|
|
this.hasPole = false;
|
|
this.isVaulting = true;
|
|
this.vaultTimer = 0;
|
|
this.targetPlant = null;
|
|
this.isEating = false;
|
|
if (window.PvZAudio) window.PvZAudio.playVault();
|
|
return;
|
|
}
|
|
|
|
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
|
|
this.targetPlant = null;
|
|
this.isEating = false;
|
|
} else {
|
|
this.isEating = true;
|
|
this.eatTimer += dt;
|
|
if (this.eatTimer >= 0.35) {
|
|
this.eatTimer = 0;
|
|
this.targetPlant.takeDamage(this.eatingDps * 0.35);
|
|
if (this.isVampire) {
|
|
this.health = Math.min(this.maxHealth, this.health + 45);
|
|
}
|
|
if (window.PvZAudio) window.PvZAudio.playChomp();
|
|
if (this.targetPlant && this.targetPlant.type === 'wallnut' && gameState && gameState.achievements) {
|
|
gameState.achievements.trigger('nut_wall');
|
|
}
|
|
if (this.targetPlant.toRemove || this.targetPlant.health <= 0) {
|
|
this.targetPlant = null;
|
|
this.isEating = false;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.isEating = false;
|
|
}
|
|
|
|
// If not eating and not vaulting, walk forward
|
|
if (!this.isEating) {
|
|
this.x -= this.speed * dt;
|
|
}
|
|
}
|
|
|
|
takeDamage(amount, slows = false, source = null) {
|
|
this.hitFlashTimer = 0.08;
|
|
if (slows && !this.iceImmune) {
|
|
this.slowTimer = 4.0;
|
|
}
|
|
if (this.reflectDmg && source && source.takeDamage) {
|
|
source.takeDamage(amount * this.reflectDmg);
|
|
}
|
|
|
|
// Newspaper mechanics
|
|
if (this.type === 'newspaper' && this.hasPaper) {
|
|
this.paperHealth -= amount;
|
|
if (this.paperHealth <= 0) {
|
|
this.hasPaper = false;
|
|
this.isAngry = true;
|
|
this.baseSpeed = 54; // Enraged run!
|
|
this.speed = this.isSlowed ? 27 : 54;
|
|
if (window.PvZAudio) window.PvZAudio.playStrike();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.health -= amount;
|
|
if (this.health <= 0) {
|
|
this.toRemove = true;
|
|
}
|
|
}
|
|
|
|
drawFrostCrystal(ctx, cx, cy, size, pulse) {
|
|
ctx.save();
|
|
ctx.translate(cx, cy);
|
|
ctx.rotate(pulse * 0.4);
|
|
ctx.strokeStyle = 'rgba(224, 247, 250, 0.9)';
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.lineWidth = 1.2;
|
|
for (let i = 0; i < 3; i++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(-size, 0);
|
|
ctx.lineTo(size, 0);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(-size * 0.5, -size * 0.35);
|
|
ctx.lineTo(-size * 0.5, size * 0.35);
|
|
ctx.moveTo(size * 0.5, -size * 0.35);
|
|
ctx.lineTo(size * 0.5, size * 0.35);
|
|
ctx.stroke();
|
|
ctx.rotate(Math.PI / 3);
|
|
}
|
|
ctx.beginPath();
|
|
ctx.arc(0, 0, 1.2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
drawFrostCrystals(ctx) {
|
|
ctx.save();
|
|
const pulse = Math.sin(this.animTime * 3);
|
|
const crys = [
|
|
{ x: -18, y: -26, s: 4.5 },
|
|
{ x: 16, y: -22, s: 5.0 },
|
|
{ x: -16, y: 4, s: 4.0 },
|
|
{ x: 14, y: 0, s: 4.5 },
|
|
{ x: -8, y: 24, s: 3.8 },
|
|
{ x: 10, y: 22, s: 4.0 }
|
|
];
|
|
for (let i = 0; i < crys.length; i++) {
|
|
const c = crys[i];
|
|
const p = pulse + i;
|
|
this.drawFrostCrystal(ctx, c.x, c.y, c.s, p);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
draw(ctx, gameState = null) {
|
|
ctx.save();
|
|
ctx.translate(this.x, this.y);
|
|
|
|
// Ground shadow
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 42, 22, 8, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
const isDancing = gameState && gameState.tree && gameState.tree.cheats && gameState.tree.cheats.dance;
|
|
const danceBob = isDancing ? Math.sin(this.animTime * 12) * 6 : 0;
|
|
const walkBob = (Math.sin(this.animTime * (this.isAngry ? 12 : 5)) * 2) + danceBob;
|
|
const legAngle1 = Math.sin(this.animTime * (this.isAngry ? 10 : (isDancing ? 8 : 4))) * 0.35;
|
|
const legAngle2 = -legAngle1;
|
|
const armAngle = this.isEating
|
|
? (Math.sin(this.animTime * 8) * 0.2 + 0.5)
|
|
: (Math.sin(this.animTime * (this.isAngry ? 8 : (isDancing ? 8 : 3))) * 0.25 + (isDancing ? 0.9 : (this.isAngry ? 0.8 : 0.3)));
|
|
|
|
const isHit = this.hitFlashTimer > 0;
|
|
const isSlow = this.isSlowed;
|
|
|
|
let skinColor = this.isAngry ? '#b71c1c' : '#809973';
|
|
let coatColor = '#5d4037';
|
|
let pantsColor = '#263238';
|
|
|
|
if (this.type === 'football') {
|
|
coatColor = '#b71c1c';
|
|
pantsColor = '#f5f5f5';
|
|
}
|
|
|
|
if (isSlow) {
|
|
skinColor = '#81d4fa';
|
|
coatColor = '#455a64';
|
|
pantsColor = '#263238';
|
|
}
|
|
if (isHit) {
|
|
skinColor = '#ffffff';
|
|
coatColor = '#ffffff';
|
|
pantsColor = '#ffffff';
|
|
ctx.shadowColor = '#ffffff';
|
|
ctx.shadowBlur = 12;
|
|
}
|
|
|
|
// Vault jump vertical offset
|
|
if (this.isVaulting) {
|
|
const jumpProgress = this.vaultTimer / 0.6;
|
|
const jumpY = -Math.sin(jumpProgress * Math.PI) * 65;
|
|
ctx.translate(0, jumpY);
|
|
ctx.rotate(-0.3 + jumpProgress * 0.6);
|
|
} else {
|
|
ctx.translate(0, walkBob);
|
|
}
|
|
|
|
// Water ripple for Ducky Tube
|
|
if (this.type === 'duckytube') {
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 30, 30 + Math.sin(this.animTime * 4) * 4, 12, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Back leg
|
|
ctx.save();
|
|
ctx.translate(6, 15);
|
|
ctx.rotate(legAngle2);
|
|
ctx.fillStyle = pantsColor;
|
|
ctx.fillRect(-5, 0, 10, 24);
|
|
ctx.fillStyle = '#1b1b1b';
|
|
ctx.fillRect(-7, 24, 14, 6);
|
|
ctx.restore();
|
|
|
|
// Front leg
|
|
ctx.save();
|
|
ctx.translate(-6, 15);
|
|
ctx.rotate(legAngle1);
|
|
ctx.fillStyle = pantsColor;
|
|
ctx.fillRect(-5, 0, 10, 24);
|
|
ctx.fillStyle = '#1b1b1b';
|
|
ctx.fillRect(-7, 24, 14, 6);
|
|
ctx.restore();
|
|
|
|
// Torso
|
|
ctx.fillStyle = coatColor;
|
|
ctx.fillRect(-14, -18, 26, 34);
|
|
|
|
// Tie
|
|
ctx.fillStyle = '#b71c1c';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-1, -16);
|
|
ctx.lineTo(3, -16);
|
|
ctx.lineTo(2, -2);
|
|
ctx.lineTo(1, 4);
|
|
ctx.lineTo(0, -2);
|
|
ctx.fill();
|
|
|
|
// Ducky Inner Tube
|
|
if (this.type === 'duckytube') {
|
|
ctx.fillStyle = '#fbc02d';
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 16, 26, 14, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// Duck beak
|
|
ctx.fillStyle = '#ff9800';
|
|
ctx.beginPath();
|
|
ctx.arc(-22, 12, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Arms & Items (Pole / Newspaper)
|
|
ctx.save();
|
|
ctx.translate(-8, -10);
|
|
ctx.rotate(armAngle);
|
|
ctx.fillStyle = coatColor;
|
|
ctx.fillRect(-18, -4, 20, 8);
|
|
ctx.fillStyle = skinColor;
|
|
ctx.beginPath();
|
|
ctx.arc(-20, 0, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Held Item
|
|
if (this.hasPole) {
|
|
// Long vault pole
|
|
ctx.strokeStyle = '#8d6e63';
|
|
ctx.lineWidth = 5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-20, 2);
|
|
ctx.lineTo(-65, -35);
|
|
ctx.stroke();
|
|
} else if (this.hasPaper) {
|
|
// Newspaper shield
|
|
ctx.fillStyle = '#f5f5f5';
|
|
ctx.strokeStyle = '#9e9e9e';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(-34, -18, 16, 32);
|
|
ctx.strokeRect(-34, -18, 16, 32);
|
|
// Newspaper lines
|
|
ctx.fillStyle = '#616161';
|
|
ctx.fillRect(-32, -14, 12, 2);
|
|
ctx.fillRect(-32, -8, 12, 2);
|
|
ctx.fillRect(-32, -2, 12, 2);
|
|
}
|
|
ctx.restore();
|
|
|
|
// Head
|
|
ctx.save();
|
|
ctx.translate(-2, -28);
|
|
const headTilt = this.isEating ? Math.sin(this.animTime * 12) * 0.15 : Math.sin(this.animTime * 3) * 0.05;
|
|
ctx.rotate(headTilt);
|
|
|
|
ctx.fillStyle = skinColor;
|
|
ctx.beginPath();
|
|
ctx.roundRect(-14, -16, 28, 30, [10, 10, 6, 6]);
|
|
ctx.fill();
|
|
|
|
// Eyes
|
|
ctx.fillStyle = this.isAngry ? '#ff5252' : '#fff9c4';
|
|
ctx.beginPath();
|
|
ctx.arc(-8, -4, 4.5, 0, Math.PI * 2);
|
|
ctx.arc(3, -4, 5.5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#000';
|
|
ctx.beginPath();
|
|
ctx.arc(-9, -4, 2, 0, Math.PI * 2);
|
|
ctx.arc(2, -4, 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Mouth
|
|
ctx.fillStyle = '#212121';
|
|
ctx.fillRect(-10, 6, 18, 5);
|
|
ctx.fillStyle = '#fff';
|
|
ctx.fillRect(-9, 6, 3, 3);
|
|
ctx.fillRect(-4, 6, 3, 3);
|
|
ctx.fillRect(1, 8, 3, 3);
|
|
|
|
// Tree of Wisdom Visual Cheats
|
|
if (gameState && gameState.tree && gameState.tree.cheats) {
|
|
// 1. Future Sunglasses
|
|
if (gameState.tree.cheats.future) {
|
|
ctx.fillStyle = '#00e5ff';
|
|
ctx.shadowColor = '#00e5ff';
|
|
ctx.shadowBlur = 6;
|
|
ctx.fillRect(-12, -7, 22, 6);
|
|
ctx.fillStyle = '#18ffff';
|
|
ctx.fillRect(-10, -5, 18, 2);
|
|
ctx.shadowBlur = 0;
|
|
}
|
|
// 2. Mustache
|
|
if (gameState.tree.cheats.mustache) {
|
|
ctx.fillStyle = '#3e2723';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-11, 4);
|
|
ctx.quadraticCurveTo(-4, 0, 0, 3);
|
|
ctx.quadraticCurveTo(4, 0, 9, 4);
|
|
ctx.quadraticCurveTo(0, 8, -11, 4);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Headwear
|
|
if (this.type === 'conehead' && this.health > 100) {
|
|
ctx.fillStyle = isHit ? '#ffffff' : '#ff6d00';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-14, -14);
|
|
ctx.lineTo(14, -14);
|
|
ctx.lineTo(3, -48);
|
|
ctx.lineTo(-3, -48);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-8, -26);
|
|
ctx.lineTo(8, -26);
|
|
ctx.lineTo(6, -34);
|
|
ctx.lineTo(-6, -34);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
} else if (this.type === 'buckethead' && this.health > 100) {
|
|
ctx.fillStyle = isHit ? '#ffffff' : '#90a4ae';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-16, -14);
|
|
ctx.lineTo(16, -14);
|
|
ctx.lineTo(13, -44);
|
|
ctx.lineTo(-13, -44);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.strokeStyle = isHit ? '#ffffff' : '#546e7a';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
} else if (this.type === 'football' && this.health > 100) {
|
|
// Football Helmet
|
|
ctx.fillStyle = isHit ? '#ffffff' : '#b71c1c';
|
|
ctx.beginPath();
|
|
ctx.arc(0, -10, 18, Math.PI, 0);
|
|
ctx.fill();
|
|
// White Stripe
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(-3, -28, 6, 18);
|
|
// Face grill
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 2.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-12, -4);
|
|
ctx.lineTo(-2, 4);
|
|
ctx.lineTo(8, -4);
|
|
ctx.stroke();
|
|
} else if (this.type === 'flag') {
|
|
ctx.strokeStyle = isHit ? '#ffffff' : '#8d6e63';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(10, 10);
|
|
ctx.lineTo(16, -55);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = isHit ? '#ffffff' : '#d32f2f';
|
|
ctx.beginPath();
|
|
ctx.moveTo(16, -55);
|
|
ctx.lineTo(40, -45);
|
|
ctx.lineTo(16, -35);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
// Frost Crystals visual effect around slowed / frozen zombie
|
|
if (isSlow && !isHit) {
|
|
this.drawFrostCrystals(ctx);
|
|
}
|
|
|
|
// Bright white highlight overlay filter when hit
|
|
if (isHit) {
|
|
ctx.save();
|
|
const flashAlpha = Math.min(1, this.hitFlashTimer / 0.08) * 0.75;
|
|
ctx.globalAlpha = flashAlpha;
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, -6, 22, 44, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
// Health bar if damaged (drawn directly above zombie head)
|
|
if (this.health < this.maxHealth) {
|
|
const pct = Math.max(0, this.health / this.maxHealth);
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
|
|
ctx.fillRect(-20, -56, 40, 5);
|
|
ctx.fillStyle = pct > 0.4 ? '#8bc34a' : '#f44336';
|
|
ctx.fillRect(-20, -56, 40 * pct, 5);
|
|
ctx.strokeStyle = '#222';
|
|
ctx.lineWidth = 0.8;
|
|
ctx.strokeRect(-20, -56, 40, 5);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
};
|