import { baseCombo, forEachLiveOne, liveCount } from "./gameStateMutators";
import {
brickCenterX,
brickCenterY,
countBricksAbove,
countBricksBelow,
currentLevelInfo,
isTelekinesisActive,
isYoyoActive,
max_levels,
} from "./game_utils";
import { colorString, GameState } from "./types";
import { t } from "./i18n/i18n";
import { gameState, lastMeasuredFPS } from "./game";
import { isOptionOn } from "./options";
export const gameCanvas = document.getElementById("game") as HTMLCanvasElement;
export const ctx = gameCanvas.getContext("2d", {
alpha: false,
}) as CanvasRenderingContext2D;
export const bombSVG = document.createElement("img");
bombSVG.src =
"data:image/svg+xml;base64," +
btoa(``);
bombSVG.onload = () => (gameState.needsRender = true);
export const background = document.createElement("img");
background.onload = () => (gameState.needsRender = true);
export const backgroundCanvas = document.createElement("canvas");
export function render(gameState: GameState) {
if (!gameState.readyToRender) return;
const level = currentLevelInfo(gameState);
const hasCombo = gameState.combo > baseCombo(gameState);
const { width, height } = gameCanvas;
if (!width || !height) return;
if (gameState.currentLevel || gameState.levelTime) {
menuLabel.innerText = gameState.loop
? t("play.current_lvl_loop", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
loop: gameState.loop,
})
: t("play.current_lvl", {
level: gameState.currentLevel + 1,
max: max_levels(gameState),
});
} else {
menuLabel.innerText = t("play.menu_label");
}
const catchRate = gameState.levelSpawnedCoins
? (gameState.levelSpawnedCoins - gameState.levelLostCoins) /
gameState.levelSpawnedCoins
: 1;
scoreDisplay.innerHTML =
(isOptionOn("show_fps")
? `
${lastMeasuredFPS} FPS
/
`
: "") +
(isOptionOn("show_stats")
? `
0.9 && "good") || ""}" data-tooltip="${t("play.stats.coins_catch_rate")}">
${Math.floor(catchRate * 100)}%
/
${Math.ceil(gameState.levelTime / 1000)}s
/
${gameState.levelWallBounces} B
/
${gameState.levelMisses} M
/
`
: "") +
`$${gameState.score}`;
scoreDisplay.className =
gameState.lastScoreIncrease > gameState.levelTime - 500 ? "active" : "";
// Clear
if (!isOptionOn("basic") && !level.color && level.svg) {
// Without this the light trails everything
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "screen";
ctx.globalAlpha = 0.6;
forEachLiveOne(gameState.coins, (coin) => {
drawFuzzyBall(ctx, coin.color, gameState.coinSize * 2, coin.x, coin.y);
});
gameState.balls.forEach((ball) => {
drawFuzzyBall(
ctx,
gameState.ballsColor,
gameState.ballSize * 2,
ball.x,
ball.y,
);
});
ctx.globalAlpha = 0.5;
gameState.bricks.forEach((color, index) => {
if (!color) return;
const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index);
drawFuzzyBall(
ctx,
color == "black" ? "#666" : color,
gameState.brickWidth,
x,
y,
);
});
ctx.globalAlpha = 1;
forEachLiveOne(gameState.particles, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawFuzzyBall(ctx, color, size * 3, x, y);
});
// Decides how brights the bg black parts can get
ctx.globalAlpha = 0.2;
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
// Decides how dark the background black parts are when lit (1=black)
ctx.globalAlpha = 0.8;
ctx.globalCompositeOperation = "multiply";
if (level.svg && background.width && background.complete) {
if (backgroundCanvas.title !== level.name) {
backgroundCanvas.title = level.name;
backgroundCanvas.width = gameState.canvasWidth;
backgroundCanvas.height = gameState.canvasHeight;
const bgctx = backgroundCanvas.getContext(
"2d",
) as CanvasRenderingContext2D;
bgctx.fillStyle = level.color || "#000";
bgctx.fillRect(0, 0, gameState.canvasWidth, gameState.canvasHeight);
if (gameState.perks.clairvoyant >= 3) {
const pageSource = document.body.innerHTML.replace(/\s+/gi, "");
const lineWidth = Math.ceil(gameState.canvasWidth / 15);
const lines = Math.ceil(gameState.canvasHeight / 20);
const chars = lineWidth * lines;
let start = Math.ceil(Math.random() * (pageSource.length - chars));
for (let i = 0; i < lines; i++) {
bgctx.fillStyle = "white";
bgctx.font = "20px Courier";
bgctx.fillText(
pageSource.slice(
start + i * lineWidth,
start + (i + 1) * lineWidth,
),
0,
i * 20,
gameState.canvasWidth,
);
}
} else {
const pattern = ctx.createPattern(background, "repeat");
if (pattern) {
bgctx.fillStyle = pattern;
bgctx.fillRect(0, 0, width, height);
}
}
}
ctx.drawImage(backgroundCanvas, 0, 0);
} else {
// Background not loaded yes
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, width, height);
}
} else {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = level.color || "#000";
ctx.fillRect(0, 0, width, height);
forEachLiveOne(gameState.particles, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2);
drawBall(ctx, color, size, x, y);
});
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
const lastExplosionDelay = Date.now() - gameState.lastExplosion + 5;
const shaked = lastExplosionDelay < 200 && !isOptionOn("basic");
if (shaked) {
const amplitude =
((gameState.perks.bigger_explosions + 1) * 50) / lastExplosionDelay;
ctx.translate(
Math.sin(Date.now()) * amplitude,
Math.sin(Date.now() + 36) * amplitude,
);
}
if (gameState.perks.bigger_explosions && !isOptionOn("basic") && shaked) {
gameCanvas.style.filter =
"brightness(" + (1 + 100 / (1 + lastExplosionDelay)) + ")";
} else {
gameCanvas.style.filter = "";
}
// Coins
ctx.globalAlpha = 1;
forEachLiveOne(gameState.coins, (coin) => {
ctx.globalCompositeOperation = "source-over";
// ctx.globalCompositeOperation =
// coin.color === "gold" || level.color ? "source-over" : "screen";
drawCoin(
ctx,
coin.color,
coin.size,
coin.x,
coin.y,
(hasCombo && gameState.perks.asceticism && "red") ||
(coin.color === "gold" && "gold") ||
gameState.puckColor,
coin.a,
);
});
// Black shadow around balls
if (!isOptionOn("basic")) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = Math.min(0.8, liveCount(gameState.coins) / 20);
gameState.balls.forEach((ball) => {
drawBall(
ctx,
level.color || "#000",
gameState.ballSize * 6,
ball.x,
ball.y,
);
});
}
ctx.globalCompositeOperation = "source-over";
renderAllBricks();
ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.lights, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.min(1, 2 - (elapsed / duration) * 2) * 0.5;
drawBrick(ctx, color, x, y, -1, gameState.perks.clairvoyant >= 2);
});
ctx.globalCompositeOperation = "screen";
forEachLiveOne(gameState.texts, (flash) => {
const { x, y, time, color, size, duration } = flash;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "source-over";
drawText(ctx, flash.text, color, size, x, y - elapsed / 10);
});
forEachLiveOne(gameState.particles, (particle) => {
const { x, y, time, color, size, duration } = particle;
const elapsed = gameState.levelTime - time;
ctx.globalAlpha = Math.max(0, Math.min(1, 2 - (elapsed / duration) * 2));
ctx.globalCompositeOperation = "screen";
drawBall(ctx, color, size, x, y);
drawFuzzyBall(ctx, color, size, x, y);
});
if (gameState.perks.extra_life) {
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = gameState.puckColor;
for (let i = 0; i < gameState.perks.extra_life; i++) {
ctx.fillRect(
gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown,
gameState.gameZoneHeight - gameState.puckHeight / 2 + 2 * i,
gameState.perks.unbounded
? gameState.canvasWidth
: gameState.gameZoneWidthRoundedUp,
1,
);
}
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
gameState.balls.forEach((ball) => {
const drawingColor = gameState.ballsColor;
// The white border around is to distinguish colored balls from coins/bg
drawBall(
ctx,
drawingColor,
gameState.ballSize,
ball.x,
ball.y,
gameState.puckColor,
);
if (isTelekinesisActive(gameState, ball) || isYoyoActive(gameState, ball)) {
ctx.beginPath();
ctx.moveTo(gameState.puckPosition, gameState.gameZoneHeight);
ctx.strokeStyle = gameState.puckColor;
ctx.bezierCurveTo(
gameState.puckPosition,
gameState.gameZoneHeight,
gameState.puckPosition,
ball.y,
ball.x,
ball.y,
);
ctx.stroke();
ctx.lineWidth = 2;
ctx.setLineDash(emptyArray);
}
if (gameState.perks.clairvoyant && gameState.ballStickToPuck) {
ctx.strokeStyle = gameState.ballsColor;
ctx.beginPath();
ctx.moveTo(ball.x, ball.y);
ctx.lineTo(ball.x + ball.vx * 10, ball.y + ball.vy * 10);
ctx.stroke();
}
});
// The puck
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
drawPuck(
ctx,
gameState.puckColor,
gameState.puckWidth,
gameState.puckHeight,
0,
gameState.perks.concave_puck,
gameState.perks.streak_shots && hasCombo ? getDashOffset(gameState) : -1,
);
if (gameState.combo > 1) {
ctx.globalCompositeOperation = "source-over";
const comboText = "x " + gameState.combo;
const comboTextWidth = (comboText.length * gameState.puckHeight) / 1.8;
const totalWidth = comboTextWidth + gameState.coinSize * 2;
const left = gameState.puckPosition - totalWidth / 2;
if (totalWidth < gameState.puckWidth) {
drawCoin(
ctx,
"gold",
gameState.coinSize,
left + gameState.coinSize / 2,
gameState.gameZoneHeight - gameState.puckHeight / 2,
gameState.puckColor,
0,
);
drawText(
ctx,
comboText,
"#000",
gameState.puckHeight,
left + gameState.coinSize * 1.5,
gameState.gameZoneHeight - gameState.puckHeight / 2,
true,
);
} else {
drawText(
ctx,
comboTextWidth > gameState.puckWidth
? gameState.combo.toString()
: comboText,
"#000",
comboTextWidth > gameState.puckWidth ? 12 : 20,
gameState.puckPosition,
gameState.gameZoneHeight - gameState.puckHeight / 2,
false,
);
}
}
// Borders
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1;
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
ctx.fillStyle =
hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor;
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.left_is_lava &&
!gameState.perks.unbounded &&
"red") ||
"white",
gameState.offsetX - 1,
0,
gameState.offsetX - 1,
height,
gameState.perks.unbounded ? 0.1 : 1,
);
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.right_is_lava &&
!gameState.perks.unbounded &&
"red") ||
"white",
width - gameState.offsetX + 1,
0,
width - gameState.offsetX + 1,
height,
gameState.perks.unbounded ? 0.1 : 1,
);
} else {
ctx.fillStyle = "red";
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.left_is_lava &&
!gameState.perks.unbounded &&
"red") ||
"",
0,
0,
0,
height,
1,
);
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.right_is_lava &&
!gameState.perks.unbounded &&
"red") ||
"",
width - 1,
0,
width - 1,
height,
1,
);
}
ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1;
drawStraightLine(
ctx,
gameState,
(hasCombo && gameState.perks.top_is_lava && "red") || "",
gameState.offsetXRoundedDown,
1,
width - gameState.offsetXRoundedDown,
1,
1,
);
ctx.globalAlpha = 1;
drawStraightLine(
ctx,
gameState,
(hasCombo && gameState.perks.compound_interest && "red") ||
(isOptionOn("mobile-mode") && "white") ||
"",
gameState.offsetXRoundedDown,
gameState.gameZoneHeight,
width - gameState.offsetXRoundedDown,
gameState.gameZoneHeight,
1,
);
if (isOptionOn("mobile-mode") && !gameState.running) {
drawText(
ctx,
t("play.mobile_press_to_play"),
gameState.puckColor,
gameState.puckHeight,
gameState.canvasWidth / 2,
gameState.gameZoneHeight +
(gameState.canvasHeight - gameState.gameZoneHeight) / 2,
);
}
if (shaked) {
ctx.resetTransform();
}
}
function drawStraightLine(
ctx: CanvasRenderingContext2D,
gameState: GameState,
mode: "white" | "" | "red",
x1,
y1,
x2,
y2,
alpha = 1,
) {
ctx.globalAlpha = alpha;
if (!mode) return;
if (mode == "red") {
ctx.strokeStyle = "red";
ctx.lineDashOffset = getDashOffset(gameState);
ctx.lineWidth = 2;
ctx.setLineDash(redBorderDash);
} else {
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
if (mode == "red") {
ctx.setLineDash(emptyArray);
ctx.lineWidth = 1;
}
ctx.globalAlpha = 1;
}
let cachedBricksRender = document.createElement("canvas");
let cachedBricksRenderKey = "";
export function renderAllBricks() {
ctx.globalAlpha = 1;
const hasCombo = gameState.combo > baseCombo(gameState);
const redBorderOnBricksWithWrongColor =
hasCombo && gameState.perks.picky_eater && !isOptionOn("basic");
const redColorOnAllBricks = !!(
gameState.lastPuckMove &&
gameState.perks.passive_income &&
hasCombo &&
gameState.lastPuckMove >
gameState.levelTime - 250 * gameState.perks.passive_income
);
let offset = getDashOffset(gameState);
if (
!(
redBorderOnBricksWithWrongColor ||
redColorOnAllBricks ||
gameState.perks.reach ||
gameState.perks.zen
)
) {
offset = 0;
}
const clairVoyance =
gameState.perks.clairvoyant && gameState.brickHP.reduce((a, b) => a + b, 0);
const newKey =
gameState.gameZoneWidth +
"_" +
gameState.bricks.join("_") +
bombSVG.complete +
"_" +
redBorderOnBricksWithWrongColor +
"_" +
redColorOnAllBricks +
"_" +
gameState.ballsColor +
"_" +
gameState.perks.pierce_color +
"_" +
clairVoyance +
"_" +
offset;
if (newKey !== cachedBricksRenderKey) {
cachedBricksRenderKey = newKey;
cachedBricksRender.width = gameState.gameZoneWidth;
cachedBricksRender.height = gameState.gameZoneWidth + 1;
const canctx = cachedBricksRender.getContext(
"2d",
) as CanvasRenderingContext2D;
canctx.clearRect(0, 0, gameState.gameZoneWidth, gameState.gameZoneWidth);
canctx.resetTransform();
canctx.translate(-gameState.offsetX, 0);
// Bricks
gameState.bricks.forEach((color, index) => {
const x = brickCenterX(gameState, index),
y = brickCenterY(gameState, index);
if (!color) return;
let redBecauseOfReach =
gameState.perks.reach &&
countBricksAbove(gameState, index) &&
!countBricksBelow(gameState, index);
let redBorder =
(gameState.ballsColor !== color &&
color !== "black" &&
redBorderOnBricksWithWrongColor) ||
(hasCombo && gameState.perks.zen && color === "black") ||
redBecauseOfReach ||
redColorOnAllBricks;
canctx.globalCompositeOperation = "source-over";
drawBrick(
canctx,
color,
x,
y,
redBorder ? offset : -1,
gameState.perks.clairvoyant >= 2,
);
if (gameState.brickHP[index] > 1 && gameState.perks.clairvoyant) {
canctx.globalCompositeOperation =
gameState.perks.clairvoyant >= 2 ? "source-over" : "destination-out";
drawText(
canctx,
gameState.brickHP[index].toString(),
color,
gameState.puckHeight,
x,
y,
);
}
if (color === "black") {
canctx.globalCompositeOperation = "source-over";
drawIMG(canctx, bombSVG, gameState.brickWidth, x, y);
}
});
}
ctx.drawImage(cachedBricksRender, gameState.offsetX, 0);
}
let cachedGraphics: { [k: string]: HTMLCanvasElement } = {};
export function drawPuck(
ctx: CanvasRenderingContext2D,
color: colorString,
puckWidth: number,
puckHeight: number,
yOffset = 0,
concave_puck: number,
redBorderOffset: number,
) {
const key =
"puck" +
color +
"_" +
puckWidth +
"_" +
puckHeight +
"_" +
concave_puck +
"_" +
redBorderOffset;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = puckWidth;
can.height = puckHeight * 2;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.fillStyle = color;
canctx.beginPath();
canctx.moveTo(0, puckHeight * 2);
if (concave_puck) {
canctx.lineTo(0, puckHeight * 0.75);
canctx.bezierCurveTo(
puckWidth / 2,
(puckHeight * (2 + concave_puck)) / 3,
puckWidth / 2,
(puckHeight * (2 + concave_puck)) / 3,
puckWidth,
puckHeight * 0.75,
);
canctx.lineTo(puckWidth, puckHeight * 2);
} else {
canctx.lineTo(0, puckHeight * 1.25);
canctx.bezierCurveTo(
0,
puckHeight * 0.75,
puckWidth,
puckHeight * 0.75,
puckWidth,
puckHeight * 1.25,
);
canctx.lineTo(puckWidth, puckHeight * 2);
}
canctx.fill();
if (redBorderOffset !== -1) {
canctx.strokeStyle = "red";
canctx.lineWidth = 4;
canctx.setLineDash(redBorderDash);
canctx.lineDashOffset = redBorderOffset;
canctx.stroke();
}
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
Math.round(gameState.puckPosition - puckWidth / 2),
gameState.gameZoneHeight - puckHeight * 2 + yOffset,
);
}
export function drawBall(
ctx: CanvasRenderingContext2D,
color: colorString,
width: number,
x: number,
y: number,
borderColor = "",
) {
const key = "ball" + color + "_" + width + "_" + borderColor;
const size = Math.round(width);
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.beginPath();
canctx.arc(size / 2, size / 2, Math.round(size / 2) - 1, 0, 2 * Math.PI);
canctx.fillStyle = color;
canctx.fill();
if (borderColor) {
canctx.lineWidth = 2;
canctx.strokeStyle = borderColor;
canctx.stroke();
}
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
Math.round(x - size / 2),
Math.round(y - size / 2),
);
}
const angles = 32;
export function drawCoin(
ctx: CanvasRenderingContext2D,
color: colorString,
size: number,
x: number,
y: number,
borderColor: colorString,
rawAngle: number,
) {
const angle =
((Math.round((rawAngle / Math.PI) * 2 * angles) % angles) + angles) %
angles;
const key =
"coin with halo" +
"_" +
color +
"_" +
size +
"_" +
borderColor +
"_" +
(color === "gold" ? angle : "whatever");
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
// coin
canctx.beginPath();
canctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI);
canctx.fillStyle = color;
canctx.fill();
canctx.strokeStyle = borderColor;
if (borderColor == "red") {
canctx.lineWidth = 2;
canctx.setLineDash(redBorderDash);
}
canctx.stroke();
if (color === "gold") {
// Fill in
canctx.beginPath();
canctx.arc(size / 2, size / 2, (size / 2) * 0.6, 0, 2 * Math.PI);
canctx.fillStyle = "rgba(255,255,255,0.5)";
canctx.fill();
canctx.translate(size / 2, size / 2);
canctx.rotate(angle / 16);
canctx.translate(-size / 2, -size / 2);
canctx.globalCompositeOperation = "multiply";
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
drawText(canctx, "$", color, size - 2, size / 2, size / 2 + 1);
}
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
Math.round(x - size / 2),
Math.round(y - size / 2),
);
}
export function drawFuzzyBall(
ctx: CanvasRenderingContext2D,
color: colorString,
width: number,
x: number,
y: number,
) {
const key = "fuzzy-circle" + color + "_" + width;
if (!color) debugger;
const size = Math.round(width * 3);
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
const gradient = canctx.createRadialGradient(
size / 2,
size / 2,
0,
size / 2,
size / 2,
size / 2,
);
gradient.addColorStop(0, color);
gradient.addColorStop(1, "transparent");
canctx.fillStyle = gradient;
canctx.fillRect(0, 0, size, size);
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
Math.round(x - size / 2),
Math.round(y - size / 2),
);
}
export function drawBrick(
ctx: CanvasRenderingContext2D,
color: colorString,
x: number,
y: number,
offset: number = 0,
borderOnly: boolean,
) {
const tlx = Math.ceil(x - gameState.brickWidth / 2);
const tly = Math.ceil(y - gameState.brickWidth / 2);
const brx = Math.ceil(x + gameState.brickWidth / 2) - 1;
const bry = Math.ceil(y + gameState.brickWidth / 2) - 1;
const width = brx - tlx,
height = bry - tly;
const key =
"brick" +
color +
"_" +
"_" +
width +
"_" +
height +
"_" +
offset +
"_" +
borderOnly;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = width;
can.height = height;
const bord = 4;
const cornerRadius = 2;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.fillStyle = color;
canctx.setLineDash(offset !== -1 ? redBorderDash : emptyArray);
canctx.lineDashOffset = offset;
canctx.strokeStyle = offset !== -1 ? "red" : color;
canctx.lineJoin = "round";
canctx.lineWidth = bord;
roundRect(
canctx,
bord / 2,
bord / 2,
width - bord,
height - bord,
cornerRadius,
);
if (!borderOnly) {
canctx.fill();
}
canctx.stroke();
cachedGraphics[key] = can;
}
ctx.drawImage(cachedGraphics[key], tlx, tly, width, height);
// It's not easy to have a 1px gap between bricks without antialiasing
}
export function roundRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
export function drawIMG(
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
size: number,
x: number,
y: number,
) {
const key = "svg" + img + "_" + size + "_" + img.complete;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = size;
can.height = size;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
const ratio = size / Math.max(img.width, img.height);
const w = img.width * ratio;
const h = img.height * ratio;
canctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
Math.round(x - size / 2),
Math.round(y - size / 2),
);
}
export function drawText(
ctx: CanvasRenderingContext2D,
text: string,
color: colorString,
fontSize: number,
x: number,
y: number,
left = false,
) {
const key = "text" + text + "_" + color + "_" + fontSize + "_" + left;
if (!cachedGraphics[key]) {
const can = document.createElement("canvas");
can.width = fontSize * text.length;
can.height = fontSize;
const canctx = can.getContext("2d") as CanvasRenderingContext2D;
canctx.fillStyle = color;
canctx.textAlign = left ? "left" : "center";
canctx.textBaseline = "middle";
canctx.font = fontSize + "px monospace";
canctx.fillText(text, left ? 0 : can.width / 2, can.height / 2, can.width);
cachedGraphics[key] = can;
}
ctx.drawImage(
cachedGraphics[key],
left ? x : Math.round(x - cachedGraphics[key].width / 2),
Math.round(y - cachedGraphics[key].height / 2),
);
}
export const scoreDisplay = document.getElementById(
"score",
) as HTMLButtonElement;
const menuLabel = document.getElementById("menuLabel") as HTMLButtonElement;
const emptyArray = [];
const redBorderDash = [5, 5];
export function getDashOffset(gameState: GameState) {
if (isOptionOn("basic")) {
return 0;
}
return Math.floor(((gameState.levelTime % 500) / 500) * 10) % 10;
}