replace client with client-next
This commit is contained in:
parent
b1b4fdffb4
commit
d29419bcf7
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite serve",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@nextui-org/react": "^2.2.9",
|
||||
"@sc07-canvas/lib": "^1.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"framer-motion": "^11.0.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-zoom-pan-pinch": "^3.4.1",
|
||||
"socket.io-client": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/vite-react": "^3.0.0",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.35",
|
||||
"sass": "^1.70.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.1.1",
|
||||
"vite-plugin-simple-html": "^0.1.2"
|
||||
}
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import { ClientConfig, IPalleteContext, IPosition, Pixel } from "../types";
|
||||
import Network from "./network";
|
||||
import {
|
||||
ClickEvent,
|
||||
HoverEvent,
|
||||
PanZoom,
|
||||
} from "@sc07-canvas/lib/src/renderer/PanZoom";
|
||||
|
||||
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 _destroy = false;
|
||||
private config: ClientConfig;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private PanZoom: PanZoom;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
private cursor = { x: -1, y: -1 };
|
||||
private pixels: {
|
||||
[x_y: string]: { color: number; type: "full" | "pending" };
|
||||
} = {};
|
||||
private lastPlace: number | undefined;
|
||||
|
||||
constructor(
|
||||
config: ClientConfig,
|
||||
canvas: HTMLCanvasElement,
|
||||
PanZoom: PanZoom
|
||||
) {
|
||||
super();
|
||||
Canvas.instance = this;
|
||||
|
||||
this.config = config;
|
||||
this.canvas = canvas;
|
||||
this.PanZoom = PanZoom;
|
||||
this.ctx = canvas.getContext("2d")!;
|
||||
|
||||
canvas.width = config.canvas.size[0];
|
||||
canvas.height = config.canvas.size[1];
|
||||
|
||||
this.PanZoom.addListener("hover", this.handleMouseMove.bind(this));
|
||||
this.PanZoom.addListener("click", this.handleMouseDown.bind(this));
|
||||
|
||||
Network.waitFor("canvas").then(([pixels]) => this.handleBatch(pixels));
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroy = true;
|
||||
|
||||
this.PanZoom.removeListener("hover", this.handleMouseMove.bind(this));
|
||||
this.PanZoom.removeListener("click", this.handleMouseDown.bind(this));
|
||||
|
||||
Network.off("canvas", this.handleBatch.bind(this));
|
||||
}
|
||||
|
||||
handleMouseDown(e: ClickEvent) {
|
||||
const [x, y] = this.screenToPos(e.clientX, e.clientY);
|
||||
this.place(x, y);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.emit("cursorPos", this.cursor);
|
||||
}
|
||||
|
||||
handleBatch(pixels: string[]) {
|
||||
pixels.forEach((hex, index) => {
|
||||
const x = index % this.config.canvas.size[0];
|
||||
const y = index / this.config.canvas.size[1];
|
||||
const color = this.Pallete.getColorFromHex(hex);
|
||||
|
||||
this.pixels[x + "_" + y] = {
|
||||
color: color ? color.id : -1,
|
||||
type: "full",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handlePixel({ x, y, color }: Pixel) {
|
||||
this.pixels[x + "_" + y] = {
|
||||
color,
|
||||
type: "full",
|
||||
};
|
||||
}
|
||||
|
||||
palleteCtx: IPalleteContext = {};
|
||||
Pallete = {
|
||||
getColor: (colorId: number) => {
|
||||
return this.config.pallete.colors.find((c) => c.id === colorId);
|
||||
},
|
||||
|
||||
getSelectedColor: () => {
|
||||
if (!this.palleteCtx.color) return undefined;
|
||||
|
||||
return this.Pallete.getColor(this.palleteCtx.color);
|
||||
},
|
||||
|
||||
getColorFromHex: (hex: string) => {
|
||||
return this.config.pallete.colors.find((c) => c.hex === hex);
|
||||
},
|
||||
};
|
||||
|
||||
updatePallete(pallete: IPalleteContext) {
|
||||
this.palleteCtx = pallete;
|
||||
}
|
||||
|
||||
place(x: number, y: number) {
|
||||
if (!this.Pallete.getSelectedColor()) return;
|
||||
|
||||
if (this.lastPlace) {
|
||||
if (this.lastPlace + this.config.pallete.pixel_cooldown > Date.now()) {
|
||||
console.log("cannot place; cooldown");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPlace = Date.now();
|
||||
|
||||
Network.socket
|
||||
.emitWithAck("place", {
|
||||
x,
|
||||
y,
|
||||
color: this.Pallete.getSelectedColor()!.id,
|
||||
})
|
||||
.then((ack) => {
|
||||
if (ack.success) {
|
||||
this.handlePixel(ack.data);
|
||||
} else {
|
||||
// TODO: handle undo pixel
|
||||
alert("error: " + ack.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
panZoomTransformToCanvas() {
|
||||
const { x, y, scale: zoom } = this.PanZoom.transform;
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
|
||||
let canvasX = 0;
|
||||
let canvasY = 0;
|
||||
|
||||
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";
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return [output.x, output.y];
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
|
||||
const bezier = (n: number) => n * n * (3 - 2 * n);
|
||||
|
||||
this.ctx.globalAlpha = 1;
|
||||
|
||||
this.ctx.fillStyle = "#fff";
|
||||
this.ctx.fillRect(
|
||||
0,
|
||||
0,
|
||||
this.config.canvas.size[0],
|
||||
this.config.canvas.size[1]
|
||||
);
|
||||
|
||||
for (const [x_y, pixel] of Object.entries(this.pixels)) {
|
||||
const [x, y] = x_y.split("_").map((a) => parseInt(a));
|
||||
|
||||
this.ctx.globalAlpha = pixel.type === "full" ? 1 : 0.5;
|
||||
this.ctx.fillStyle =
|
||||
pixel.color > -1
|
||||
? "#" + this.Pallete.getColor(pixel.color)!.hex
|
||||
: "transparent";
|
||||
this.ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
if (this.palleteCtx.color && this.cursor.x > -1 && this.cursor.y > -1) {
|
||||
const color = this.config.pallete.colors.find(
|
||||
(c) => c.id === this.palleteCtx.color
|
||||
);
|
||||
|
||||
let t = ((Date.now() / 100) % 10) / 10;
|
||||
this.ctx.globalAlpha = t < 0.5 ? bezier(t) : -bezier(t) + 1;
|
||||
this.ctx.fillStyle = "#" + color!.hex;
|
||||
this.ctx.fillRect(this.cursor.x, this.cursor.y, 1, 1);
|
||||
}
|
||||
|
||||
if (!this._destroy) window.requestAnimationFrame(() => this.draw());
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: contain;
|
||||
touch-action: none;
|
||||
|
||||
background-color: #ddd !important;
|
||||
}
|
||||
|
||||
header#main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
touch-action: none;
|
||||
pointer-events: none;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 10px;
|
||||
touch-action: initial;
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 5px 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
|
||||
&--overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
|
||||
span:first-of-type {
|
||||
font-size: 130%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
background-color: #aaa;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
#cursor {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 3px;
|
||||
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#canvas-meta {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
transform: translateY(-100%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.canvas-meta--cursor-pos {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
z-index: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.pixelate {
|
||||
image-rendering: optimizeSpeed;
|
||||
-ms-interpolation-mode: nearest-neighbor;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: -o-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./components/Pallete.scss";
|
||||
@import "./board.scss";
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"extends": "@tsconfig/vite-react/tsconfig.json",
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
var gulp = require("gulp");
|
||||
var ts = require("gulp-typescript");
|
||||
const webpack = require("webpack-stream");
|
||||
const sass = require("gulp-sass")(require("sass"));
|
||||
|
||||
gulp.task("css", function () {
|
||||
return gulp
|
||||
.src("src/**/*.scss")
|
||||
.pipe(sass().on("error", sass.logError))
|
||||
.pipe(gulp.dest("./public/css"));
|
||||
});
|
||||
|
||||
gulp.task("js", function () {
|
||||
return gulp
|
||||
.src("src/**/*.ts")
|
||||
.pipe(webpack(require("./webpack.config.js")))
|
||||
.pipe(gulp.dest("public"));
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"watch",
|
||||
gulp.series("js", "css", function () {
|
||||
gulp.watch("src/**/*.ts", gulp.series("js"));
|
||||
gulp.watch("src/**/*.scss", gulp.series("css"));
|
||||
})
|
||||
);
|
|
@ -1,26 +1,52 @@
|
|||
{
|
||||
"name": "@sc07-canvas/client",
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "gulp watch"
|
||||
"build": "vite build",
|
||||
"dev": "vite serve",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"type": "module",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@nextui-org/react": "^2.2.9",
|
||||
"@sc07-canvas/lib": "^1.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-cli": "^2.3.0",
|
||||
"gulp-typescript": "^2.12.2",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"ts-loader": "^9.4.4"
|
||||
"eventemitter3": "^5.0.1",
|
||||
"framer-motion": "^11.0.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-zoom-pan-pinch": "^3.4.1",
|
||||
"socket.io-client": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.22.10",
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"babel-loader": "^9.1.3",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"sass": "^1.65.1",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-stream": "^7.0.0"
|
||||
"@tsconfig/vite-react": "^3.0.0",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.35",
|
||||
"sass": "^1.70.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"vite": "^5.1.1",
|
||||
"vite-plugin-simple-html": "^0.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title>Document</title>
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div></div>
|
||||
<div class="spacer"></div>
|
||||
<div>
|
||||
<div class="user-card">
|
||||
<div class="user-card--overview">
|
||||
<div class="user-name"></div>
|
||||
<div class="user-instance"></div>
|
||||
</div>
|
||||
<img src="#" alt="User Avatar" class="user-avatar" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="board-zoom">
|
||||
<div id="board-move" style="width: 100px; height: 100px">
|
||||
<canvas
|
||||
id="board"
|
||||
width="1000"
|
||||
height="1000"
|
||||
class="pixelate"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div id="cursor" style="display: none"></div>
|
||||
|
||||
<div id="pallete">
|
||||
<div id="canvas-meta">
|
||||
<!-- canvas is in charge of the metabox -->
|
||||
<span>
|
||||
Pixels:
|
||||
<span class="canvas-meta--pixels"></span>
|
||||
</span>
|
||||
<span>
|
||||
Users Online:
|
||||
<span class="canvas-meta--online"></span>
|
||||
</span>
|
||||
<button id="test">test button</button>
|
||||
</div>
|
||||
|
||||
<div class="pallete-colors"></div>
|
||||
<div class="pallete-user-overlay">
|
||||
you are not logged in
|
||||
<a href="/api/login" class="user-login">login</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,11 +0,0 @@
|
|||
import "./lib/net";
|
||||
import Pallete from "./lib/pallete";
|
||||
import Canvas from "./lib/canvas";
|
||||
|
||||
window.addEventListener("mousemove", (e) => {
|
||||
const cursor = document.getElementById("cursor");
|
||||
cursor!.style.transform = `translate(${e.pageX}px, ${e.pageY}px)`;
|
||||
});
|
||||
|
||||
Canvas.setup();
|
||||
Canvas.draw();
|
|
@ -1,32 +0,0 @@
|
|||
import { SUserPacket, AuthSession } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
const $puseroverlay = document.querySelector(
|
||||
"#pallete .pallete-user-overlay"
|
||||
)! as HTMLDivElement;
|
||||
const $usercard = document.querySelector(".user-card")! as HTMLDivElement;
|
||||
const $usercard_name = $usercard.querySelector(".user-name")! as HTMLDivElement;
|
||||
const $usercard_instance = $usercard.querySelector(
|
||||
".user-instance"
|
||||
)! as HTMLDivElement;
|
||||
const $usercard_avatar = $usercard.querySelector(
|
||||
"img.user-avatar"
|
||||
)! as HTMLImageElement;
|
||||
|
||||
class Auth {
|
||||
private user: AuthSession | undefined;
|
||||
|
||||
login() {
|
||||
window.location.href = "/api/login";
|
||||
}
|
||||
|
||||
handleAuth(data: SUserPacket) {
|
||||
this.user = data.user;
|
||||
$puseroverlay.style.display = "none";
|
||||
|
||||
$usercard_name.innerText = data.user.user.username;
|
||||
$usercard_instance.innerHTML = data.user.service.instance.hostname;
|
||||
$usercard_avatar.setAttribute("src", data.user.user.profile);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Auth();
|
|
@ -1,407 +1,99 @@
|
|||
import pallete from "./pallete";
|
||||
import Network from "./net";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { ClientConfig, IPalleteContext, IPosition, Pixel } from "../types";
|
||||
import Network from "./network";
|
||||
import {
|
||||
CPixelPacket,
|
||||
SCanvasPacket,
|
||||
SPixelPacket,
|
||||
} from "@sc07-canvas/lib/src/net";
|
||||
|
||||
const $moveContainer = document.querySelector("main")!;
|
||||
const $canvas = document.getElementById("board")! as HTMLCanvasElement;
|
||||
const $zoom = document.getElementById("board-zoom")!;
|
||||
const $move = document.getElementById("board-move")!;
|
||||
const $container = document.querySelector("main")!;
|
||||
|
||||
const $metabox = document.getElementById("canvas-meta")! as HTMLDivElement;
|
||||
const $metabox_pixels = $metabox.querySelector(
|
||||
".canvas-meta--pixels"
|
||||
) as HTMLSpanElement;
|
||||
const $metabox_online = $metabox.querySelector(
|
||||
".canvas-meta--online"
|
||||
) as HTMLSpanElement;
|
||||
|
||||
const $testbtn = document.getElementById("test")! as HTMLButtonElement;
|
||||
|
||||
export type CanvasConfig = {
|
||||
size: [number, number];
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
const ZOOM_SPEED = 0.1;
|
||||
|
||||
class Canvas {
|
||||
private config: CanvasConfig = { size: [0, 0], zoom: 1 };
|
||||
ClickEvent,
|
||||
HoverEvent,
|
||||
PanZoom,
|
||||
} from "@sc07-canvas/lib/src/renderer/PanZoom";
|
||||
|
||||
interface CanvasEvents {
|
||||
/**
|
||||
* Last pixel place date
|
||||
*
|
||||
* TODO: Support pixel stacking
|
||||
* Cursor canvas position
|
||||
* (-1, -1) is not on canvas
|
||||
* @param position Canvas position
|
||||
* @returns
|
||||
*/
|
||||
private lastPlace: Date | undefined;
|
||||
private zoom = 1;
|
||||
private move = { x: 0, y: 0 };
|
||||
cursorPos: (position: IPosition) => void;
|
||||
}
|
||||
|
||||
export class Canvas extends EventEmitter<CanvasEvents> {
|
||||
static instance: Canvas | undefined;
|
||||
|
||||
private _destroy = false;
|
||||
private config: ClientConfig;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private PanZoom: PanZoom;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
private cursor = { x: -1, y: -1 };
|
||||
private ctx: CanvasRenderingContext2D | null = null;
|
||||
// private pixels: {
|
||||
// x: number;
|
||||
// y: number;
|
||||
// color: number;
|
||||
// type: "full" | "pending";
|
||||
// }[] = [];
|
||||
private pixels: {
|
||||
[x_y: string]: { color: number; type: "full" | "pending" };
|
||||
} = {};
|
||||
private lastPlace: number | undefined;
|
||||
|
||||
private mouseDown = false;
|
||||
private dragOrigin = { x: 0, y: 0 };
|
||||
private downTime: number | undefined;
|
||||
constructor(
|
||||
config: ClientConfig,
|
||||
canvas: HTMLCanvasElement,
|
||||
PanZoom: PanZoom
|
||||
) {
|
||||
super();
|
||||
Canvas.instance = this;
|
||||
|
||||
load(config: CanvasConfig) {
|
||||
this.config = config;
|
||||
$canvas.setAttribute("width", config.size[0] + "");
|
||||
$canvas.setAttribute("height", config.size[1] + "");
|
||||
this.setZoom(config.zoom);
|
||||
this.canvas = canvas;
|
||||
this.PanZoom = PanZoom;
|
||||
this.ctx = canvas.getContext("2d")!;
|
||||
|
||||
canvas.width = config.canvas.size[0];
|
||||
canvas.height = config.canvas.size[1];
|
||||
|
||||
this.PanZoom.addListener("hover", this.handleMouseMove.bind(this));
|
||||
this.PanZoom.addListener("click", this.handleMouseDown.bind(this));
|
||||
|
||||
Network.waitFor("canvas").then(([pixels]) => this.handleBatch(pixels));
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
setZoom(newzoom: number, mouse?: { x: number; y: number }) {
|
||||
if (newzoom < 0.5) newzoom = 0.5;
|
||||
const oldzoom = this.zoom;
|
||||
this.zoom = newzoom;
|
||||
destroy() {
|
||||
this._destroy = true;
|
||||
|
||||
$zoom.style.transform = `scale(${newzoom})`;
|
||||
this.PanZoom.removeListener("hover", this.handleMouseMove.bind(this));
|
||||
this.PanZoom.removeListener("click", this.handleMouseDown.bind(this));
|
||||
|
||||
if (mouse) {
|
||||
const dx = mouse.x - $container.clientWidth / 2;
|
||||
const dy = mouse.y - $container.clientHeight / 2;
|
||||
this.move.x -= dx / oldzoom;
|
||||
this.move.x += dx / newzoom;
|
||||
this.move.y -= dy / oldzoom;
|
||||
this.move.y += dy / newzoom;
|
||||
$move.style.transform = `translate(${this.move.x}px, ${this.move.y}px)`;
|
||||
}
|
||||
Network.off("canvas", this.handleBatch.bind(this));
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.ctx = $canvas.getContext("2d");
|
||||
handleMouseDown(e: ClickEvent) {
|
||||
const [x, y] = this.screenToPos(e.clientX, e.clientY);
|
||||
this.place(x, y);
|
||||
}
|
||||
|
||||
let pinching = false;
|
||||
let pinchInit = 0;
|
||||
|
||||
$testbtn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
$canvas.classList.toggle("pixelate");
|
||||
});
|
||||
|
||||
window.addEventListener("wheel", (e) => {
|
||||
const oldzoom = this.zoom;
|
||||
let newzoom = (this.zoom += ZOOM_SPEED * (e.deltaY > 0 ? -1 : 1));
|
||||
this.setZoom(newzoom, {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
});
|
||||
});
|
||||
|
||||
const handleMouseStart = (e: { pageX: number; pageY: number }) => {
|
||||
this.mouseDown = true;
|
||||
this.dragOrigin = { x: e.pageX, y: e.pageY };
|
||||
this.downTime = Date.now();
|
||||
};
|
||||
|
||||
$moveContainer.addEventListener(
|
||||
"touchstart",
|
||||
(e) => {
|
||||
if (e.changedTouches.length === 2) {
|
||||
pinching = true;
|
||||
pinchInit = Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY
|
||||
);
|
||||
} else {
|
||||
handleMouseStart(e.touches[0]);
|
||||
}
|
||||
|
||||
$testbtn.innerText = e.changedTouches.length + "";
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
$moveContainer.addEventListener("mousedown", handleMouseStart);
|
||||
|
||||
const update = (x: number, y: number, update: boolean) => {
|
||||
const deltaX = (x - this.dragOrigin.x) / this.zoom;
|
||||
const deltaY = (y - this.dragOrigin.y) / this.zoom;
|
||||
const newX = this.move.x + deltaX;
|
||||
const newY = this.move.y + deltaY;
|
||||
if (update) {
|
||||
this.move.x += deltaX;
|
||||
this.move.y += deltaY;
|
||||
}
|
||||
$move.style.transform = `translate(${newX}px, ${newY}px)`;
|
||||
};
|
||||
|
||||
const handleMouseEnd = (e: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}) => {
|
||||
if (!this.mouseDown) return;
|
||||
|
||||
this.mouseDown = false;
|
||||
|
||||
const downDelta = Date.now() - this.downTime!;
|
||||
const delta = [
|
||||
Math.abs(this.dragOrigin.x - e.clientX),
|
||||
Math.abs(this.dragOrigin.y - e.clientY),
|
||||
];
|
||||
if (downDelta < 500) {
|
||||
// mouse was down for less than 500ms
|
||||
|
||||
if (delta[0] < 5 && delta[1] < 5) {
|
||||
const [x, y] = this.screenToPos(e.clientX, e.clientY);
|
||||
this.place(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
update(e.pageX, e.pageY, true);
|
||||
};
|
||||
|
||||
window.addEventListener("touchend", (e) => {
|
||||
if (pinching) {
|
||||
console.log("pinching end");
|
||||
pinching = false;
|
||||
} else {
|
||||
handleMouseEnd(e.changedTouches[0]);
|
||||
}
|
||||
});
|
||||
window.addEventListener("mouseup", (e) => {
|
||||
if (pinching) {
|
||||
pinching = false;
|
||||
} else {
|
||||
handleMouseEnd(e);
|
||||
}
|
||||
});
|
||||
|
||||
const handleMouseMove = (e: {
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}) => {
|
||||
if (this.mouseDown) {
|
||||
update(e.pageX, e.pageY, false);
|
||||
} else {
|
||||
const canvasRect = $canvas.getBoundingClientRect();
|
||||
if (
|
||||
canvasRect.left <= e.pageX &&
|
||||
canvasRect.right >= e.pageX &&
|
||||
canvasRect.top <= e.pageY &&
|
||||
canvasRect.bottom >= e.pageY
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"touchmove",
|
||||
(e) => {
|
||||
if (pinching) {
|
||||
let initDiff = Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY
|
||||
);
|
||||
let diff = (pinchInit - initDiff) / (10 / this.zoom);
|
||||
|
||||
let newzoom = (this.zoom += ZOOM_SPEED * -diff);
|
||||
this.setZoom(newzoom, {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
});
|
||||
|
||||
pinchInit = initDiff;
|
||||
} else {
|
||||
handleMouseMove(e.touches[0]);
|
||||
}
|
||||
},
|
||||
{ passive: false }
|
||||
);
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
$canvas.addEventListener("touchmove", (e) => {
|
||||
if (!pinching) {
|
||||
const [x, y] = this.screenToPos(
|
||||
e.touches[0].clientX,
|
||||
e.touches[0].clientY
|
||||
);
|
||||
this.cursor.x = x;
|
||||
this.cursor.y = y;
|
||||
}
|
||||
});
|
||||
$canvas.addEventListener("mousemove", (e) => {
|
||||
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;
|
||||
|
||||
// window.requestAnimationFrame(() => this.draw());
|
||||
});
|
||||
}
|
||||
|
||||
screenToPos(x: number, y: number) {
|
||||
const rect = $canvas.getBoundingClientRect();
|
||||
const scale = [$canvas.width / rect.width, $canvas.height / rect.height];
|
||||
return [x - rect.left, y - rect.top]
|
||||
.map((v, i) => v * scale[i])
|
||||
.map((v) => v >> 0);
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.ctx) {
|
||||
window.requestAnimationFrame(() => this.draw());
|
||||
return;
|
||||
}
|
||||
|
||||
/* @ts-ignore */
|
||||
this.ctx.mozImageSmoothingEnabled =
|
||||
/* @ts-ignore */
|
||||
this.ctx.webkitImageSmoothingEnabled =
|
||||
/* @ts-ignore */
|
||||
this.ctx.msImageSmoothingEnabled =
|
||||
this.ctx.imageSmoothingEnabled =
|
||||
false;
|
||||
|
||||
this.ctx.canvas.width = window.innerWidth;
|
||||
this.ctx.canvas.height = window.innerHeight;
|
||||
|
||||
const bezier = (n: number) => n * n * (3 - 2 * n);
|
||||
|
||||
this.ctx.globalAlpha = 1;
|
||||
|
||||
this.ctx.fillStyle = "#fff";
|
||||
this.ctx.fillRect(0, 0, this.config.size[0], this.config.size[1]);
|
||||
|
||||
for (const [x_y, pixel] of Object.entries(this.pixels)) {
|
||||
const [x, y] = x_y.split("_").map((a) => parseInt(a));
|
||||
|
||||
this.ctx.globalAlpha = pixel.type === "full" ? 1 : 0.5;
|
||||
this.ctx.fillStyle =
|
||||
pixel.color > -1
|
||||
? "#" + pallete.getPalleteColor(pixel.color)!.hex
|
||||
: "transparent";
|
||||
this.ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
if (pallete.getColor() && this.cursor.x > -1 && this.cursor.y > -1) {
|
||||
let t = ((Date.now() / 100) % 10) / 10;
|
||||
this.ctx.globalAlpha = t < 0.5 ? bezier(t) : -bezier(t) + 1;
|
||||
this.ctx.fillStyle = "#" + pallete.getColor()!.hex;
|
||||
this.ctx.fillRect(this.cursor.x, this.cursor.y, 1, 1);
|
||||
}
|
||||
|
||||
this.renderMetabox();
|
||||
|
||||
window.requestAnimationFrame(() => this.draw());
|
||||
}
|
||||
|
||||
/**
|
||||
* update canvas metabox with latest information
|
||||
*
|
||||
* executed by #draw()
|
||||
*/
|
||||
renderMetabox() {
|
||||
if (this.lastPlace) {
|
||||
let cooldown =
|
||||
this.lastPlace.getTime() +
|
||||
pallete.getPixelCooldown() -
|
||||
new Date().getTime();
|
||||
$metabox_pixels.innerText = cooldown > 0 ? cooldown / 1000 + "s" : "none";
|
||||
} else {
|
||||
$metabox_pixels.innerText = "no cooldown";
|
||||
this.cursor.x = -1;
|
||||
this.cursor.y = -1;
|
||||
}
|
||||
|
||||
$metabox_online.innerText = Network.getOnline() + "";
|
||||
this.emit("cursorPos", this.cursor);
|
||||
}
|
||||
|
||||
place(x: number, y: number) {
|
||||
if (!pallete.getColor()) return;
|
||||
|
||||
if (this.lastPlace) {
|
||||
if (
|
||||
this.lastPlace.getTime() + pallete.getPixelCooldown() >
|
||||
new Date().getTime()
|
||||
) {
|
||||
console.log("cannot place, cooldown in place");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPlace = new Date();
|
||||
|
||||
const color = pallete.getColor()!;
|
||||
|
||||
// this.pixels.push({
|
||||
// x,
|
||||
// y,
|
||||
// color: color.id,
|
||||
// type: "pending",
|
||||
// });
|
||||
this.pixels[x + "_" + y] = {
|
||||
color: color.id,
|
||||
type: "pending",
|
||||
};
|
||||
|
||||
Network.sendAck<CPixelPacket, SPixelPacket>("place", {
|
||||
x,
|
||||
y,
|
||||
color: color.id,
|
||||
}).then((ack) => {
|
||||
// remove pending pixel at coord
|
||||
// can remove regardless of success or failure, as success places pixel at coord from server
|
||||
// this.pixels.splice(
|
||||
// this.pixels.indexOf(
|
||||
// this.pixels.find(
|
||||
// (pxl) =>
|
||||
// pxl.x === x &&
|
||||
// pxl.y === y &&
|
||||
// pxl.color === color.id &&
|
||||
// pxl.type === "pending"
|
||||
// )!
|
||||
// ),
|
||||
// 1
|
||||
// );
|
||||
|
||||
if (ack.success) {
|
||||
this.handlePixel(ack.data);
|
||||
} else {
|
||||
// TODO: handle undo pixel
|
||||
alert("Error: " + ack.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handlePixel({ x, y, color, ...rest }: SPixelPacket) {
|
||||
// this.pixels.push({
|
||||
// x,
|
||||
// y,
|
||||
// color,
|
||||
// type: "full",
|
||||
// });
|
||||
this.pixels[x + "_" + y] = {
|
||||
color,
|
||||
type: "full",
|
||||
};
|
||||
}
|
||||
|
||||
handleBatch(batch: SCanvasPacket) {
|
||||
batch.pixels.forEach((hex, index) => {
|
||||
const x = index / this.config.size[0];
|
||||
const y = index % this.config.size[0];
|
||||
const color = pallete.getPalleteFromHex(hex);
|
||||
handleBatch(pixels: string[]) {
|
||||
pixels.forEach((hex, index) => {
|
||||
const x = index % this.config.canvas.size[0];
|
||||
const y = index / this.config.canvas.size[1];
|
||||
const color = this.Pallete.getColorFromHex(hex);
|
||||
|
||||
this.pixels[x + "_" + y] = {
|
||||
color: color ? color.id : -1,
|
||||
|
@ -409,6 +101,172 @@ class Canvas {
|
|||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Canvas();
|
||||
handlePixel({ x, y, color }: Pixel) {
|
||||
this.pixels[x + "_" + y] = {
|
||||
color,
|
||||
type: "full",
|
||||
};
|
||||
}
|
||||
|
||||
palleteCtx: IPalleteContext = {};
|
||||
Pallete = {
|
||||
getColor: (colorId: number) => {
|
||||
return this.config.pallete.colors.find((c) => c.id === colorId);
|
||||
},
|
||||
|
||||
getSelectedColor: () => {
|
||||
if (!this.palleteCtx.color) return undefined;
|
||||
|
||||
return this.Pallete.getColor(this.palleteCtx.color);
|
||||
},
|
||||
|
||||
getColorFromHex: (hex: string) => {
|
||||
return this.config.pallete.colors.find((c) => c.hex === hex);
|
||||
},
|
||||
};
|
||||
|
||||
updatePallete(pallete: IPalleteContext) {
|
||||
this.palleteCtx = pallete;
|
||||
}
|
||||
|
||||
place(x: number, y: number) {
|
||||
if (!this.Pallete.getSelectedColor()) return;
|
||||
|
||||
if (this.lastPlace) {
|
||||
if (this.lastPlace + this.config.pallete.pixel_cooldown > Date.now()) {
|
||||
console.log("cannot place; cooldown");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPlace = Date.now();
|
||||
|
||||
Network.socket
|
||||
.emitWithAck("place", {
|
||||
x,
|
||||
y,
|
||||
color: this.Pallete.getSelectedColor()!.id,
|
||||
})
|
||||
.then((ack) => {
|
||||
if (ack.success) {
|
||||
this.handlePixel(ack.data);
|
||||
} else {
|
||||
// TODO: handle undo pixel
|
||||
alert("error: " + ack.error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
panZoomTransformToCanvas() {
|
||||
const { x, y, scale: zoom } = this.PanZoom.transform;
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
|
||||
let canvasX = 0;
|
||||
let canvasY = 0;
|
||||
|
||||
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";
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return [output.x, output.y];
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
|
||||
const bezier = (n: number) => n * n * (3 - 2 * n);
|
||||
|
||||
this.ctx.globalAlpha = 1;
|
||||
|
||||
this.ctx.fillStyle = "#fff";
|
||||
this.ctx.fillRect(
|
||||
0,
|
||||
0,
|
||||
this.config.canvas.size[0],
|
||||
this.config.canvas.size[1]
|
||||
);
|
||||
|
||||
for (const [x_y, pixel] of Object.entries(this.pixels)) {
|
||||
const [x, y] = x_y.split("_").map((a) => parseInt(a));
|
||||
|
||||
this.ctx.globalAlpha = pixel.type === "full" ? 1 : 0.5;
|
||||
this.ctx.fillStyle =
|
||||
pixel.color > -1
|
||||
? "#" + this.Pallete.getColor(pixel.color)!.hex
|
||||
: "transparent";
|
||||
this.ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
|
||||
if (this.palleteCtx.color && this.cursor.x > -1 && this.cursor.y > -1) {
|
||||
const color = this.config.pallete.colors.find(
|
||||
(c) => c.id === this.palleteCtx.color
|
||||
);
|
||||
|
||||
let t = ((Date.now() / 100) % 10) / 10;
|
||||
this.ctx.globalAlpha = t < 0.5 ? bezier(t) : -bezier(t) + 1;
|
||||
this.ctx.fillStyle = "#" + color!.hex;
|
||||
this.ctx.fillRect(this.cursor.x, this.cursor.y, 1, 1);
|
||||
}
|
||||
|
||||
if (!this._destroy) window.requestAnimationFrame(() => this.draw());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
import io from "socket.io-client";
|
||||
import Pallete from "./pallete";
|
||||
import Canvas from "./canvas";
|
||||
import {
|
||||
CPixelPacket,
|
||||
ClientPacket,
|
||||
PacketAck,
|
||||
SCanvasPacket,
|
||||
SPixelPacket,
|
||||
SUserPacket,
|
||||
ServerPacket,
|
||||
} from "@sc07-canvas/lib/src/net";
|
||||
import Auth from "./auth";
|
||||
|
||||
class Network {
|
||||
private socket;
|
||||
private online_count = 0;
|
||||
|
||||
constructor() {
|
||||
this.socket = io();
|
||||
|
||||
this.socket.on("config", (config) => {
|
||||
Pallete.load(config.pallete);
|
||||
Canvas.load(config.canvas);
|
||||
});
|
||||
|
||||
this.socket.on("pixel", (data: SPixelPacket) => {
|
||||
Canvas.handlePixel(data);
|
||||
});
|
||||
|
||||
this.socket.on("user", (data: SUserPacket) => {
|
||||
Auth.handleAuth(data);
|
||||
});
|
||||
|
||||
this.socket.on("canvas", (data: SCanvasPacket) => {
|
||||
Canvas.handleBatch(data);
|
||||
});
|
||||
|
||||
this.socket.on("online", (data: { count: number }) => {
|
||||
this.online_count = data.count;
|
||||
});
|
||||
}
|
||||
|
||||
send<T extends ClientPacket>(
|
||||
type: T["type"],
|
||||
data: Omit<T, "type" | "_direction">
|
||||
) {
|
||||
// @ts-ignore
|
||||
data._direction = "client->server";
|
||||
|
||||
this.socket.emit(type, data);
|
||||
}
|
||||
|
||||
sendAck<O extends ClientPacket, I extends ServerPacket>(
|
||||
type: O["type"],
|
||||
data: Omit<O, "type" | "_direction">
|
||||
) {
|
||||
return new Promise<PacketAck<I>>((res) => {
|
||||
this.socket.emit(type, data, (data: PacketAck<I>) => {
|
||||
res(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get online user count
|
||||
* @returns online users count
|
||||
*/
|
||||
getOnline() {
|
||||
return this.online_count;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Network();
|
|
@ -1,80 +0,0 @@
|
|||
import { SUserPacket } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
export type PalleteColor = {
|
||||
id: number;
|
||||
hex: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const $pallete = document.getElementById("pallete")!;
|
||||
const $pcolors = $pallete.querySelector(".pallete-colors")!;
|
||||
const $puseroverlay = $pallete.querySelector(
|
||||
".pallete-user-overlay"
|
||||
)! as HTMLDivElement;
|
||||
const $cursor = document.getElementById("cursor")!;
|
||||
|
||||
class Pallete {
|
||||
private pallete: PalleteColor[] = [];
|
||||
private active: PalleteColor | undefined;
|
||||
private pixel_cooldown = 0;
|
||||
|
||||
load({
|
||||
colors,
|
||||
pixel_cooldown,
|
||||
}: {
|
||||
colors: PalleteColor[];
|
||||
pixel_cooldown: number;
|
||||
}) {
|
||||
$pcolors.innerHTML = "";
|
||||
|
||||
this.pallete = colors;
|
||||
this.pixel_cooldown = pixel_cooldown;
|
||||
|
||||
colors.forEach((color) => {
|
||||
const $color = document.createElement("a");
|
||||
$color.href = "#";
|
||||
$color.classList.add("pallete-color");
|
||||
$color.style.backgroundColor = "#" + color.hex;
|
||||
$color.addEventListener("click", () => {
|
||||
this.pick(color);
|
||||
});
|
||||
$pcolors.append($color);
|
||||
});
|
||||
|
||||
this.active = undefined;
|
||||
}
|
||||
|
||||
pick(color?: PalleteColor) {
|
||||
this.active = color;
|
||||
|
||||
$cursor.style.display = color ? "block" : "none";
|
||||
|
||||
if (color) $cursor.style.backgroundColor = "#" + color.hex;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
getPallete() {
|
||||
return this.pallete;
|
||||
}
|
||||
|
||||
getPalleteColor(id: number) {
|
||||
return this.pallete.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
getPalleteFromHex(hex: string) {
|
||||
return this.pallete.find((p) => p.hex === hex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pixel cooldown
|
||||
* @returns Pixel cooldown in ms
|
||||
*/
|
||||
getPixelCooldown() {
|
||||
return this.pixel_cooldown;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Pallete();
|
|
@ -1,14 +1,20 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: contain;
|
||||
touch-action: none;
|
||||
|
||||
background-color: #ddd !important;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
header {
|
||||
header#main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
@ -16,13 +22,20 @@ header {
|
|||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
touch-action: none;
|
||||
pointer-events: none;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.box {
|
||||
padding: 10px;
|
||||
touch-action: initial;
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.user-card {
|
||||
|
@ -70,59 +83,20 @@ header {
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
#pallete {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
#canvas-meta {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
transform: translateY(-100%);
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
.pallete-colors {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pallete-color {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pallete-user-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#canvas-meta {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
transform: translateY(-100%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.canvas-meta--cursor-pos {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,3 +127,20 @@ main {
|
|||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./components/Pallete.scss";
|
||||
@import "./board.scss";
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true
|
||||
}
|
||||
"extends": "@tsconfig/vite-react/tsconfig.json",
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/index.ts",
|
||||
devtool: "inline-source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "public"),
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue