Commit d29419bc authored by Grant's avatar Grant
Browse files

replace client with client-next

parent b1b4fdff
Loading
Loading
Loading
Loading

packages/client-next/package.json

deleted100644 → 0
+0 −52
Original line number Diff line number Diff line
{
  "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"
  }
}
+0 −272
Original line number Diff line number Diff line
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());
  }
}
+0 −146
Original line number Diff line number Diff line
@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";
+0 −3
Original line number Diff line number Diff line
{
  "extends": "@tsconfig/vite-react/tsconfig.json",
}
Loading