Commit 1a5df335 authored by Mykeyy's avatar Mykeyy
Browse files

fix(client): align grid overlay and fix pan zoom pixel rounding

parent 9d5cfe22
Loading
Loading
Loading
Loading
Loading
+12 −5
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ const KEYBOARD_ZOOM_STEP = 1;
const KEYBOARD_ZOOM_SMALL_STEP = 0.35;
const INSPECT_END_ANIMATION_DURATION = 180;
const TOUCH_INSPECT_MOVE_THRESHOLD = 25;
const GRID_LINE_FRACTION = 1 / 8;

export const CanvasWrapper = () => {
  const hasMod = useHasRole("MOD");
@@ -122,7 +123,7 @@ export const CanvasWrapper = () => {
}

const CursorInspectPreview = () => {
  const { cursor } = useAppContext();
  const { cursor, gridOverlay } = useAppContext();
  const PanZoom = useContext(RendererContext);
  const selectedColor =
    typeof cursor.color === "number"
@@ -342,6 +343,8 @@ const CursorInspectPreview = () => {
  const inspectScreenRect = (() => {
    if (
      (inspectHold === "idle" && !hasSelectedColor) ||
      typeof previewPosition.x !== "number" ||
      typeof previewPosition.y !== "number" ||
      previewPosition.x < 0 ||
      previewPosition.y < 0
    ) {
@@ -362,6 +365,10 @@ const CursorInspectPreview = () => {
  const inspectBorderSize = inspectScreenRect
    ? Math.min(inspectScreenRect.width, inspectScreenRect.height)
    : 1;
  const gridInset =
    inspectScreenRect && gridOverlay.enabled && hasSelectedColor
      ? inspectBorderSize * GRID_LINE_FRACTION
      : 0;

  const preview = (
    <div
@@ -382,10 +389,10 @@ const CursorInspectPreview = () => {
          left: previewPosition.x,
          ...(inspectScreenRect && {
            position: "fixed",
            top: inspectScreenRect.top,
            left: inspectScreenRect.left,
            width: inspectScreenRect.width,
            height: inspectScreenRect.height,
            top: inspectScreenRect.top + gridInset,
            left: inspectScreenRect.left + gridInset,
            width: Math.max(0, inspectScreenRect.width - gridInset),
            height: Math.max(0, inspectScreenRect.height - gridInset),
            zIndex: 1,
            "--inspect-border-start-inner": inspectBorderSize * 0.02 + "px",
            "--inspect-border-start-outer": inspectBorderSize * 0.04 + "px",
+7 −3
Original line number Diff line number Diff line
@@ -28,7 +28,10 @@ export const GridOverlay = () => {
    if (!ctx) return;

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "black"; // full black
    // Keep grid lines neutral instead of tinting them with placed pixel colors.
    const opacity = Math.min(Math.max(gridOverlay.opacity ?? 1, 0), 1);
    const channel = Math.round(255 - (255 - 128) * opacity);
    ctx.fillStyle = `rgb(${channel}, ${channel}, ${channel})`;

    for (let x = 0; x <= canvas.width; x += GRID_SPACING) {
      ctx.fillRect(x, 0, 1, canvas.height); // vertical line as 1px wide column
@@ -37,7 +40,7 @@ export const GridOverlay = () => {
    for (let y = 0; y <= canvas.height; y += GRID_SPACING) {
      ctx.fillRect(0, y, canvas.width, 1); // horizontal line as 1px tall row
    }
  }, [gridOverlay.enabled]);
  }, [gridOverlay.enabled, gridOverlay.opacity]);

  return (
    <canvas
@@ -53,8 +56,9 @@ export const GridOverlay = () => {
        width: config.canvas.size[0],
        height: config.canvas.size[1],
        display: gridOverlay.enabled ? "block" : "none",
        opacity: gridOverlay.opacity?.toFixed(1) ?? "1",
        opacity: 1,
        pointerEvents: "none",
        zIndex: 1,
      }}
    />
  );
+2 −2
Original line number Diff line number Diff line
@@ -881,8 +881,8 @@ export class CanvasCore extends EventEmitter<CanvasEvents> {
      canvasY += this.canvas.height;
    }

    canvasX >>= 0;
    canvasY >>= 0;
    canvasX = Math.round(canvasX);
    canvasY = Math.round(canvasY);

    return { canvasX, canvasY, zoom };
  }
+1 −1
Original line number Diff line number Diff line
@@ -99,7 +99,7 @@ class _Router extends EventEmitter<RouterEvents> {
    params.set(CLIENT_PARAMS.canvas_y, position.canvasY + "");
    params.set(
      CLIENT_PARAMS.canvas_zoom,
      (this.PanZoom!.transform.scale >> 0) + "",
      Math.round(this.PanZoom!.transform.scale) + "",
    );

    if (this.templateState.enabled && this.templateState.url) {
+50 −2
Original line number Diff line number Diff line
@@ -152,6 +152,8 @@ interface PanZoomEvents {

export class PanZoom extends EventEmitter<PanZoomEvents> {
  private initialized = false;
  private resizeFrame: number | undefined;
  private devicePixelOrigin = { x: 0, y: 0 };

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  public $wrapper: HTMLDivElement = null as any;
@@ -221,6 +223,7 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {

    this.registerMouseEvents();
    this.registerTouchEvents();
    window.addEventListener("resize", this.handleResize);

    this.initialized = true;

@@ -233,12 +236,24 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
      this.transform.x = x;
      this.transform.y = y;
      this.transform.scale = scale;
      this.update({ suppressEmit: true });
    }

    this.update({ suppressEmit: true });

    this.emit("initialize");
  }

  private handleResize = () => {
    if (this.resizeFrame !== undefined) {
      cancelAnimationFrame(this.resizeFrame);
    }

    this.resizeFrame = requestAnimationFrame(() => {
      this.resizeFrame = undefined;
      if (this.initialized) this.update({ suppressEmit: true });
    });
  };

  /**
   * Get scale that would fit the zoom element in the viewport
   * @returns
@@ -859,9 +874,17 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
      );
    }

    const zoomRect = this.$zoom.getBoundingClientRect();
    this.devicePixelOrigin.x = zoomRect.left;
    this.devicePixelOrigin.y = zoomRect.top;

    const position = this.alignToDevicePixel(
      this.transform.x,
      this.transform.y,
    );
    this.$move.style.setProperty(
      "transform",
      `translate(${this.transform.x}px, ${this.transform.y}px)`,
      `translate(${position.x}px, ${position.y}px)`,
    );

    if (!suppressEmit) {
@@ -879,10 +902,35 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
    this.endInspectHold();
    this.unregisterTouchEvents();
    this.unregisterMouseEvents();
    window.removeEventListener("resize", this.handleResize);

    if (this.resizeFrame !== undefined) {
      cancelAnimationFrame(this.resizeFrame);
      this.resizeFrame = undefined;
    }
  }

  // utilities

  alignToDevicePixel(x: number, y: number) {
    const scale = this.transform.scale;
    const dpr = window.devicePixelRatio;
    const screenScale = scale * dpr;

    if (!Number.isFinite(screenScale) || screenScale <= 0) return { x, y };

    return {
      x:
        (Math.round((this.devicePixelOrigin.x + x * scale) * dpr) / dpr -
          this.devicePixelOrigin.x) /
        scale,
      y:
        (Math.round((this.devicePixelOrigin.y + y * scale) * dpr) / dpr -
          this.devicePixelOrigin.y) /
        scale,
    };
  }

  nudgeScale(adj: number) {
    this.transform.scale = checkZoomBounds(
      this.transform.scale * 1.5 ** adj,
Loading