Newer
Older
import { ClientConfig, IPosition, Pixel } from "@sc07-canvas/lib/src/net";
ClickEvent,
HoverEvent,
PanZoom,
} from "@sc07-canvas/lib/src/renderer/PanZoom";
import { KeybindManager } from "./keybinds";
import { getRenderer } from "./utils";
import { CanvasPixel } from "./canvasRenderer";
import { CanvasUtils } from "./canvas.utils";
interface CanvasEvents {
/**
* Cursor canvas position
* (-1, -1) is not on canvas
* @param position Canvas position
* @returns
*/
cursorPos: (position: IPosition) => void;
export class Canvas extends EventEmitter<CanvasEvents> {
static instance: Canvas | undefined;
private canvas: HTMLCanvasElement;
private PanZoom: PanZoom;
private cursor: { x: number; y: number; color?: number } = { x: -1, y: -1 };
private pixels: {
[x_y: string]: { color: number; type: "full" | "pending" };
} = {};
// private _delayedLoad: ReturnType<typeof setTimeout>;
constructor(canvas: HTMLCanvasElement, PanZoom: PanZoom) {
getRenderer().startRender();
getRenderer().on("ready", () => this.emit("canvasReady"));
this.PanZoom.addListener("hover", this.handleMouseMove.bind(this));
this.PanZoom.addListener("click", this.handleMouseDown.bind(this));
this.PanZoom.addListener("longPress", this.handleLongPress);
Network.on("pixel", this.handlePixel);
Network.on("pixelLastPlaced", this.handlePixelLastPlaced);
getRenderer().stopRender();
getRenderer().off("ready");
this.PanZoom.removeListener("hover", this.handleMouseMove.bind(this));
this.PanZoom.removeListener("click", this.handleMouseDown.bind(this));
this.PanZoom.removeListener("longPress", this.handleLongPress);
Network.off("pixelLastPlaced", this.handlePixelLastPlaced);
}
/**
* React.Strict remounts the main component, causing a quick remount, which then causes errors related to webworkers
*
* If #useCanvas fails, it's most likely due to that
loadRenderer() {
try {
getRenderer().useCanvas(this.canvas, "main");
} catch (e) {
console.warn(
"[Canvas#loadRenderer] Failed at #useCanvas, this shouldn't be fatal",
e
);
}
}
setSize(width: number, height: number) {
getRenderer().setSize(width, height);
}
loadConfig(config: ClientConfig) {
this.config = config;
this.setSize(config.canvas.size[0], config.canvas.size[1]);
// we want the new one if possible
// (this might cause a timing issue though)
// if we don't clear the old one, if the canvas gets resized we get weird stretching
if (Object.keys(this.pixels).length > 0)
Network.clearPreviousState("canvas");
// Network.waitForState("canvas").then(([pixels]) => {
// console.log("loadConfig just received new canvas data");
// this.handleBatch(pixels);
// });
Network.on("canvas", (start, end, pixels) => {
console.log("[Canvas] received canvas section");
this.handleBatch(start, end, pixels);
const chunks = Network.getCanvasChunks();
console.log(`[Canvas] Received ${chunks.length} chunks to load`);
let loaded = 0;
for (const chunk of chunks) {
console.log(`[Canvas] Loading canvas chunk ${loaded}...`);
this.handleBatch(chunk.start, chunk.end, chunk.pixels);
loaded++;
}
getConfig() {
return this.config;
}
getPanZoom() {
return this.PanZoom;
}
setCooldownBypass(value: boolean) {
this.bypassCooldown = value;
}
getCooldownBypass() {
return this.bypassCooldown;
}
getAllPixels() {
let pixels: {
x: number;
y: number;
color: number;
}[] = [];
for (const [x_y, value] of Object.entries(this.pixels)) {
if (value.type === "pending") continue;
const [x, y] = x_y.split("_").map((v) => parseInt(v));
pixels.push({
x,
y,
color: value.color,
});
}
return pixels;
}
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* Get nearby pixels
* @param x
* @param y
* @param around (x,y) +- around
*/
getSurroundingPixels(x: number, y: number, around: number = 3) {
let pixels = [];
for (let offsetY = 0; offsetY <= around + 1; offsetY++) {
let arr = [];
for (let offsetX = 0; offsetX <= around + 1; offsetX++) {
let targetX = x + (offsetX - around + 1);
let targetY = y + (offsetY - around + 1);
let pixel = this.pixels[targetX + "_" + targetY];
if (pixel) {
arr.push("#" + (this.Pallete.getColor(pixel.color)?.hex || "ffffff"));
} else {
arr.push("transparent");
}
}
pixels.push(arr);
}
return pixels;
}
getPixel(x: number, y: number): { color: number } | undefined {
return this.pixels[x + "_" + y];
}
handleLongPress = (clientX: number, clientY: number) => {
KeybindManager.handleInteraction(
{
key: "LONG_PRESS",
},
{
clientX,
clientY,
}
);
};
previousCanvasClicks: { x: number; y: number }[] = [];
if (!e.alt && !e.ctrl && !e.meta && !e.shift && e.button === "LCLICK") {
const [x, y] = this.screenToPos(e.clientX, e.clientY);
this.place(x, y);
} else {
// KeybindManager.handleInteraction({
// key: e.button,
// alt: e.alt,
// ctrl: e.ctrl,
// meta: e.meta,
// shift: e.meta
// }, )
}
if (e.button === "RCLICK" && !e.alt && !e.ctrl && !e.meta && !e.shift) {
const [x, y] = this.screenToPos(e.clientX, e.clientY);
// keep track of the last X pixels right clicked
// used by the ModModal to determine areas selected
this.previousCanvasClicks.push({ x, y });
this.previousCanvasClicks = this.previousCanvasClicks.slice(-2);
}
handleMouseMove(e: HoverEvent) {
const canvasRect = this.canvas.getBoundingClientRect();
if (
canvasRect.left <= e.clientX &&
canvasRect.right >= e.clientX &&
canvasRect.top <= e.clientY &&
canvasRect.bottom >= e.clientY
) {
const [x, y] = this.screenToPos(e.clientX, e.clientY);
this.cursor.x = x;
this.cursor.y = y;
} else {
this.cursor.x = -1;
this.cursor.y = -1;
}
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
handleSquare = (
start: [x: number, y: number],
end: [x: number, y: number],
color: number
) => {
const palette = this.Pallete.getColor(color);
let serializeBuild: CanvasPixel[] = [];
for (let x = start[0]; x <= end[0]; x++) {
for (let y = start[1]; y <= end[1]; y++) {
// we still store a copy of the pixels in this instance for non-rendering functions
this.pixels[x + "_" + y] = {
type: "full",
color: palette?.id || -1,
};
serializeBuild.push({
x,
y,
hex:
!palette || palette?.hex === "transparent" ? "null" : palette.hex,
});
}
}
getRenderer().usePixels(serializeBuild);
};
handleBatch = (
start: [x: number, y: number],
end: [x: number, y: number],
pixels: string[]
) => {
if (!this.config.canvas) {
throw new Error("handleBatch called with no config");
}
const width = end[0] - start[0];
const height = end[1] - start[1];
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const hex = pixels[width * y + x];
const canvasX = x + start[0];
const canvasY = y + start[1];
// we still store a copy of the pixels in this instance for non-rendering functions
handlePixel = ({ x, y, color }: Pixel) => {
// we still store a copy of the pixels in this instance for non-rendering functions
const palette = this.Pallete.getColor(color);
getRenderer().usePixel({ x, y, hex: palette?.hex || "null" });
handlePixelLastPlaced = (time: number) => {
this.lastPlace = time;
};
Pallete = {
getColor: (colorId: number) => {
return this.config.pallete.colors.find((c) => c.id === colorId);
},
getColorFromHex: (hex: string) => {
return this.config.pallete.colors.find((c) => c.hex === hex);
},
};
/**
* Changes the cursor color as tracked by the Canvas instance
*
* @see Toolbar/Palette.tsx
* @param color
*/
updateCursor(color?: number) {
this.cursor.color = color;
place(x: number, y: number) {
if (!this.Pallete.getSelectedColor()) return;
// TODO: redo this as the server now verifies placements differently
// if (this.lastPlace) {
// if (this.lastPlace + this.config.pallete.pixel_cooldown > Date.now()) {
// console.log("cannot place; cooldown");
// return;
// }
// }
.emitWithAck(
"place",
{
x,
y,
color: this.Pallete.getSelectedColor()!.id,
},
this.bypassCooldown
)
.then((ack) => {
if (ack.success) {
this.handlePixel(ack.data);
console.warn(
"Attempted to place pixel",
{ x, y, color: this.Pallete.getSelectedColor()!.id },
"and got error",
ack
);
switch (ack.error) {
case "invalid_pixel":
toast.error(
"Cannot place, invalid pixel location. Are you even on the canvas?"
);
break;
case "no_user":
toast.error("You are not logged in.");
break;
case "pixel_already_pending":
toast.error("You are already placing a pixel");
break;
case "palette_color_invalid":
toast.error("This isn't a color that you can use...?");
break;
case "pixel_cooldown":
toast.error("You're on pixel cooldown, cannot place");
break;
case "you_already_placed_that":
toast.error("You already placed this color at this location");
break;
default:
toast.error("Error while placing pixel: " + ack.error);
}
canvasToPanZoomTransform(x: number, y: number) {
return CanvasUtils.canvasToPanZoomTransform(
x,
y,
[this.canvas.width, this.canvas.height],
this.PanZoom.flags.useZoom
);
panZoomTransformToCanvas() {
const { x, y, scale: zoom } = this.PanZoom.transform;
const rect = this.canvas.getBoundingClientRect();
if (this.PanZoom.flags.useZoom) {
// css zoom doesn't change the bounding client rect
// therefore dividing by zoom doesn't return the correct output
canvasX = this.canvas.width - (x + rect.width / 2);
canvasY = this.canvas.height - (y + rect.height / 2);
} else {
canvasX = this.canvas.width / 2 - (x + rect.width / zoom);
canvasY = this.canvas.height / 2 - (y + rect.height / zoom);
canvasX += this.canvas.width;
canvasY += this.canvas.height;
}
canvasX >>= 0;
canvasY >>= 0;
return { canvasX, canvasY };
debug(x: number, y: number, id?: string) {
if (document.getElementById("debug-" + id)) {
document.getElementById("debug-" + id)!.style.top = y + "px";
document.getElementById("debug-" + id)!.style.left = x + "px";
let el = document.createElement("div");
if (id) el.id = "debug-" + id;
el.classList.add("debug-point");
el.style.setProperty("top", y + "px");
el.style.setProperty("left", x + "px");
document.body.appendChild(el);
}
/**
* Screen (clientX, clientY) to Canvas position
* @param x
* @param y
* @returns
*/
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
screenToPos(x: number, y: number) {
// the rendered dimentions in the browser
const rect = this.canvas.getBoundingClientRect();
let output = {
x: 0,
y: 0,
};
if (this.PanZoom.flags.useZoom) {
const scale = this.PanZoom.transform.scale;
output.x = x / scale - rect.left;
output.y = y / scale - rect.top;
} else {
// get the ratio
const scale = [
this.canvas.width / rect.width,
this.canvas.height / rect.height,
];
output.x = (x - rect.left) * scale[0];
output.y = (y - rect.top) * scale[1];
}
// floor it, we're getting canvas coords, which can't have decimals
output.x >>= 0;
output.y >>= 0;