client-next (react rewrite)
This commit is contained in:
parent
cc94580bbe
commit
2e469e39a9
|
@ -1,6 +1,8 @@
|
|||
packages/server/prisma/dev.db
|
||||
packages/client/public/**/*.css
|
||||
packages/client/public/**/*.js
|
||||
packages/client-next/public/**/*.css
|
||||
packages/client-next/public/**/*.js
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"packages/client",
|
||||
"packages/client-next",
|
||||
"packages/server",
|
||||
"packages/lib"
|
||||
],
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": [
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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/style.scss")
|
||||
.pipe(sass().on("error", sass.logError))
|
||||
.pipe(gulp.dest("./public/css"));
|
||||
});
|
||||
|
||||
gulp.task("js", function () {
|
||||
return gulp
|
||||
.src("src/**/*.tsx?")
|
||||
.pipe(webpack(require("./webpack.config.js")))
|
||||
.pipe(gulp.dest("public"));
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"watch",
|
||||
gulp.series("js", "css", function () {
|
||||
gulp.watch(["src/**/*.ts", "src/**/*.tsx"], gulp.series("js"));
|
||||
gulp.watch("src/**/*.scss", gulp.series("css"));
|
||||
})
|
||||
);
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "client",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"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",
|
||||
"@sc07-canvas/lib": "^1.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"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/core": "^7.23.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^6.9.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-cli": "^2.3.0",
|
||||
"gulp-typescript": "^6.0.0-alpha.1",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"sass": "^1.70.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"ts-loader": "^9.5.1",
|
||||
"webpack": "^5.90.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>webpack-for-react</title>
|
||||
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { Header } from "./Header";
|
||||
import { AppContext } from "../contexts/AppContext";
|
||||
import { CanvasWrapper } from "./CanvasWrapper";
|
||||
import { Pallete } from "./Pallete";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<AppContext>
|
||||
<Header />
|
||||
<CanvasWrapper />
|
||||
<Pallete />
|
||||
</AppContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,77 @@
|
|||
import React, { createRef, useEffect } from "react";
|
||||
import {
|
||||
TransformComponent,
|
||||
TransformWrapper,
|
||||
useControls,
|
||||
useTransformEffect,
|
||||
} from "react-zoom-pan-pinch";
|
||||
import { Canvas } from "../lib/canvas";
|
||||
import { useAppContext } from "../contexts/AppContext";
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
export const CanvasWrapper = () => {
|
||||
// to prevent safari from blurring things, use the zoom css property
|
||||
return (
|
||||
<main>
|
||||
<TransformWrapper
|
||||
centerOnInit
|
||||
limitToBounds={false}
|
||||
centerZoomedOut={false}
|
||||
minScale={0.5}
|
||||
maxScale={50}
|
||||
wheel={{
|
||||
step: 0.05,
|
||||
smoothStep: 0.05,
|
||||
}}
|
||||
initialScale={5}
|
||||
panning={{
|
||||
velocityDisabled: true,
|
||||
}}
|
||||
doubleClick={{
|
||||
disabled: true,
|
||||
}}
|
||||
>
|
||||
<TransformComponent wrapperStyle={{ width: "100%", height: "100%" }}>
|
||||
<CanvasInner />
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
const CanvasInner = () => {
|
||||
const canvasRef = createRef<HTMLCanvasElement>();
|
||||
const { config } = useAppContext();
|
||||
const { centerView } = useControls();
|
||||
|
||||
useTransformEffect(
|
||||
throttle(({ state, instance }) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("x", state.positionX + "");
|
||||
params.set("y", state.positionY + "");
|
||||
params.set("zoom", state.scale + "");
|
||||
window.location.hash = params.toString();
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.canvas || !canvasRef.current) return;
|
||||
const canvas = canvasRef.current!;
|
||||
const canvasInstance = new Canvas(config, canvas);
|
||||
centerView();
|
||||
|
||||
return () => {
|
||||
canvasInstance.destroy();
|
||||
};
|
||||
}, [canvasRef, centerView, config]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
id="board"
|
||||
width="1000"
|
||||
height="1000"
|
||||
className="pixelate"
|
||||
ref={canvasRef}
|
||||
></canvas>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import React from "react";
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header>
|
||||
<div></div>
|
||||
<div className="spacer"></div>
|
||||
<div>
|
||||
<div className="user-card">
|
||||
<div className="user-card--overview">
|
||||
<div className="user-name"></div>
|
||||
<div className="user-instance"></div>
|
||||
</div>
|
||||
<img src="#" alt="User Avatar" className="user-avatar" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
#pallete {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
|
||||
background-color: #fff;
|
||||
|
||||
.pallete-colors {
|
||||
// display: flex;
|
||||
// width: 100%;
|
||||
// justify-content: center;
|
||||
// gap: 10px;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
.pallete-color {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pallete-color--deselect {
|
||||
display: flex-inline;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
|
||||
vertical-align: top;
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
color: #000;
|
||||
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pallete-color {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 2px solid #000;
|
||||
border-radius: 3px;
|
||||
transition: transform 0.25s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useAppContext } from "../contexts/AppContext";
|
||||
import { Canvas } from "../lib/canvas";
|
||||
import { IPalleteContext } from "../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export const Pallete = () => {
|
||||
const { config, user } = useAppContext();
|
||||
const [pallete, setPallete] = useState<IPalleteContext>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!Canvas.instance) return;
|
||||
|
||||
Canvas.instance.emit("pallete", pallete);
|
||||
}, [pallete]);
|
||||
|
||||
return (
|
||||
<div id="pallete">
|
||||
<CanvasMeta />
|
||||
|
||||
<div className="pallete-colors">
|
||||
<button
|
||||
aria-label="Deselect Color"
|
||||
className="pallete-color--deselect"
|
||||
title="Deselect Color"
|
||||
onClick={() => {
|
||||
setPallete(({ color, ...pallete }) => {
|
||||
return pallete;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
{config.pallete.colors.map((color) => (
|
||||
<button
|
||||
key={color.id}
|
||||
aria-label={color.name}
|
||||
className={["pallete-color", color.id === pallete.color && "active"]
|
||||
.filter((a) => a)
|
||||
.join(" ")}
|
||||
style={{
|
||||
backgroundColor: "#" + color.hex,
|
||||
}}
|
||||
title={color.name}
|
||||
onClick={() => {
|
||||
setPallete((pallete) => {
|
||||
return {
|
||||
...pallete,
|
||||
color: color.id,
|
||||
};
|
||||
});
|
||||
}}
|
||||
></button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!user && (
|
||||
<div className="pallete-user-overlay">
|
||||
You are not logged in
|
||||
<a href="/api/login" className="user-login">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CanvasMeta = () => {
|
||||
return (
|
||||
<div id="canvas-meta">
|
||||
<span>
|
||||
Pixels: <span>123</span>
|
||||
</span>
|
||||
<span>
|
||||
Users Online: <span>321</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
export const PanZoomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="test-wrapper">{children}</div>;
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import React, {
|
||||
PropsWithChildren,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { ClientConfig, IAppContext, IPalleteContext } from "../types";
|
||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
||||
import { number } from "prop-types";
|
||||
import Network from "../lib/network";
|
||||
|
||||
const appContext = createContext<IAppContext>({} as any);
|
||||
|
||||
export const useAppContext = () => useContext(appContext);
|
||||
|
||||
export const AppContext = ({ children }: PropsWithChildren) => {
|
||||
const [config, setConfig] = useState<ClientConfig>(undefined as any);
|
||||
const [auth, setAuth] = useState<AuthSession>();
|
||||
|
||||
useEffect(() => {
|
||||
function handleConfig(config: ClientConfig) {
|
||||
console.info("Server sent config", config);
|
||||
setConfig(config);
|
||||
}
|
||||
|
||||
function handleUser(user: AuthSession) {
|
||||
setAuth(user);
|
||||
}
|
||||
|
||||
Network.on("user", handleUser);
|
||||
Network.on("config", handleConfig);
|
||||
|
||||
Network.socket.connect();
|
||||
|
||||
return () => {
|
||||
Network.off("user", handleUser);
|
||||
Network.off("config", handleConfig);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<appContext.Provider value={{ config, user: auth }}>
|
||||
{config ? children : "Loading..."}
|
||||
</appContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./components/App";
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
root.render(<App />);
|
|
@ -0,0 +1,221 @@
|
|||
import EventEmitter from "eventemitter3";
|
||||
import { ClientConfig, IPalleteContext, Pixel } from "../types";
|
||||
import Network from "./network";
|
||||
|
||||
export class Canvas extends EventEmitter {
|
||||
static instance: Canvas | undefined;
|
||||
|
||||
private _destroy = false;
|
||||
private config: ClientConfig;
|
||||
private canvas: HTMLCanvasElement;
|
||||
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) {
|
||||
super();
|
||||
Canvas.instance = this;
|
||||
|
||||
this.config = config;
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d")!;
|
||||
|
||||
canvas.width = config.canvas.size[0];
|
||||
canvas.height = config.canvas.size[1];
|
||||
|
||||
canvas.addEventListener("mousemove", this.handleMouseMove.bind(this));
|
||||
canvas.addEventListener("mouseup", this.handleMouseClick.bind(this));
|
||||
canvas.addEventListener("mousedown", this.handleMouseDown.bind(this));
|
||||
|
||||
this.on("pallete", this.updatePallete.bind(this));
|
||||
|
||||
// Network.on("canvas", this.handleBatch.bind(this));
|
||||
Network.waitFor("canvas").then(([pixels]) => this.handleBatch(pixels));
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroy = true;
|
||||
|
||||
this.canvas.removeEventListener(
|
||||
"mousemove",
|
||||
this.handleMouseMove.bind(this)
|
||||
);
|
||||
this.canvas.removeEventListener(
|
||||
"mouseup",
|
||||
this.handleMouseClick.bind(this)
|
||||
);
|
||||
this.canvas.removeEventListener(
|
||||
"mousedown",
|
||||
this.handleMouseDown.bind(this)
|
||||
);
|
||||
|
||||
Network.off("canvas", this.handleBatch.bind(this));
|
||||
}
|
||||
|
||||
private downTime: number | undefined;
|
||||
private dragOrigin: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
handleMouseClick(e: MouseEvent) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseDown(e: MouseEvent) {
|
||||
this.downTime = Date.now();
|
||||
this.dragOrigin = { x: e.pageX, y: e.pageY };
|
||||
}
|
||||
|
||||
handleMouseMove(e: MouseEvent) {
|
||||
const canvasRect = this.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;
|
||||
}
|
||||
}
|
||||
|
||||
handleBatch(pixels: string[]) {
|
||||
pixels.forEach((hex, index) => {
|
||||
const x = index / this.config.canvas.size[0];
|
||||
const y = index % this.config.canvas.size[0];
|
||||
const color = this.Pallete.getColorFromHex(hex);
|
||||
|
||||
this.pixels[x + "_" + y] = {
|
||||
color: color ? color.id : -1,
|
||||
type: "full",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
handlePixel({ x, y, color, ...pixel }: 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
screenToPos(x: number, y: number) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const scale = [
|
||||
this.canvas.width / rect.width,
|
||||
this.canvas.height / rect.height,
|
||||
];
|
||||
return [x - rect.left, y - rect.top]
|
||||
.map((v, i) => v * scale[i])
|
||||
.map((v) => v >> 0);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { Socket, io } from "socket.io-client";
|
||||
import {
|
||||
ClientConfig,
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
} from "../types";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { AuthSession } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
export interface INetworkEvents {
|
||||
user: (user: AuthSession) => void;
|
||||
config: (user: ClientConfig) => void;
|
||||
canvas: (pixels: string[]) => void;
|
||||
}
|
||||
|
||||
type SentEventValue<K extends keyof INetworkEvents> = EventEmitter.ArgumentMap<
|
||||
Exclude<INetworkEvents, string | symbol>
|
||||
>[Extract<K, keyof INetworkEvents>];
|
||||
|
||||
class Network extends EventEmitter<INetworkEvents> {
|
||||
socket: Socket<ServerToClientEvents, ClientToServerEvents> = io({
|
||||
autoConnect: false,
|
||||
});
|
||||
private online_count = 0;
|
||||
private sentEvents: {
|
||||
[key in keyof INetworkEvents]?: SentEventValue<key>;
|
||||
} = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.socket.on("user", (user: AuthSession) => {
|
||||
this.emit("user", user);
|
||||
});
|
||||
|
||||
this.socket.on("config", (config) => {
|
||||
this.emit("config", config);
|
||||
});
|
||||
|
||||
this.socket.on("canvas", (pixels) => {
|
||||
this._emit("canvas", pixels);
|
||||
});
|
||||
|
||||
// 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("canvas", (data: SCanvasPacket) => {
|
||||
// Canvas.handleBatch(data);
|
||||
// });
|
||||
|
||||
// this.socket.on("online", (data: { count: number }) => {
|
||||
// this.online_count = data.count;
|
||||
// });
|
||||
}
|
||||
|
||||
private _emit: typeof this.emit = (event, ...args) => {
|
||||
this.sentEvents[event] = args;
|
||||
return this.emit(event, ...args);
|
||||
};
|
||||
|
||||
waitFor<Ev extends keyof INetworkEvents & (string | symbol)>(
|
||||
ev: Ev
|
||||
): Promise<SentEventValue<Ev>> {
|
||||
return new Promise((res) => {
|
||||
if (this.sentEvents[ev]) return res(this.sentEvents[ev]!);
|
||||
|
||||
this.once(ev, (...data) => {
|
||||
res(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get online user count
|
||||
* @returns online users count
|
||||
*/
|
||||
getOnline() {
|
||||
return this.online_count;
|
||||
}
|
||||
}
|
||||
|
||||
export default new Network() as Network;
|
|
@ -0,0 +1,118 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: contain;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
z-index: 9999;
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@import "./components/Pallete.scss";
|
|
@ -0,0 +1,52 @@
|
|||
import { AuthSession, PacketAck } from "@sc07-canvas/lib/src/net";
|
||||
|
||||
// socket.io
|
||||
|
||||
export interface ServerToClientEvents {
|
||||
canvas: (pixels: string[]) => void;
|
||||
user: (user: AuthSession) => void;
|
||||
config: (config: ClientConfig) => void;
|
||||
pixel: (pixel: Pixel) => void;
|
||||
}
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
place: (pixel: Pixel, ack: (_: PacketAck<Pixel>) => void) => void;
|
||||
}
|
||||
|
||||
// app context
|
||||
|
||||
export interface IAppContext {
|
||||
config: ClientConfig;
|
||||
user?: AuthSession;
|
||||
}
|
||||
|
||||
export interface IPalleteContext {
|
||||
color?: number;
|
||||
}
|
||||
|
||||
// other
|
||||
|
||||
export type Pixel = {
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
};
|
||||
|
||||
export type PalleteColor = {
|
||||
id: number;
|
||||
name: string;
|
||||
hex: string;
|
||||
};
|
||||
|
||||
export type CanvasConfig = {
|
||||
size: [number, number];
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
pallete: {
|
||||
colors: PalleteColor[];
|
||||
pixel_cooldown: number;
|
||||
};
|
||||
canvas: CanvasConfig;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
},
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
entry: "./src/index.tsx",
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
path: path.resolve(__dirname, "public"),
|
||||
},
|
||||
devtool: "inline-source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
};
|
|
@ -50,7 +50,7 @@ const io = new Server<
|
|||
user: (user: SUserPacket) => void;
|
||||
config: (config: any) => void;
|
||||
pixel: (data: SPixelPacket) => void;
|
||||
canvas: (data: SCanvasPacket) => void;
|
||||
canvas: (pixels: string[]) => void;
|
||||
online: (data: { count: number }) => void;
|
||||
}
|
||||
>(server);
|
||||
|
@ -109,11 +109,7 @@ io.on("connection", (socket) => {
|
|||
});
|
||||
|
||||
Canvas.getPixelsArray().then((pixels) => {
|
||||
socket.emit("canvas", {
|
||||
_direction: "server->client",
|
||||
type: "canvas",
|
||||
pixels,
|
||||
});
|
||||
socket.emit("canvas", pixels);
|
||||
});
|
||||
|
||||
socket.on(
|
||||
|
@ -182,7 +178,7 @@ io.on("connection", (socket) => {
|
|||
});
|
||||
|
||||
app.use(session);
|
||||
app.use(express.static("../client/public"));
|
||||
app.use(express.static("../client-next/public"));
|
||||
app.use("/api", APIRoutes);
|
||||
|
||||
server.listen(parseInt(process.env.PORT!), () => {
|
||||
|
|
Loading…
Reference in New Issue