Arbeiten mit Canvas: Erstellen eines Diagrammwerkzeugs in ReactJS

Von Soumyajit Pathak

Arbeiten mit Canvas: Erstellen eines Diagrammwerkzeugs in ReactJS

Von Soumyajit Pathak

Unter ein früherer Artikelsprachen wir darüber, wie wichtig es ist, die Landschaft eines Integrationsprojekts mithilfe von Diagrammen zu entwerfen. Die in diesem Artikel gezeigten Diagramme wurden erstellt mit INTEGRTR Digitaler Transformator - eine spezialisierte Software zur Erstellung von Diagrammen, die die Visualisierung komplexer Unternehmensintegrationslandschaften einfacher denn je macht.

Bei der Entwicklung dieses Tools haben wir mit zahlreichen Frameworks und Technologien experimentiert, bevor wir uns für ReactJS und HTML5 Canvas entschieden haben. In diesem praxisorientierten Blog möchten wir unsere Erkenntnisse und Erfahrungen aus dem Projekt mit allen teilen, die ähnliche Tools erstellen oder lernen möchten, wie HTML5 Canvas funktioniert. Wir beginnen mit den Grundlagen, indem wir versuchen, ein minimales Diagramm-Authoring-Tool zu erstellen, um Ihnen den Einstieg in diese Reise zu erleichtern.

Wir werden Folgendes verwenden reagieren für die Verwaltung unserer DOM-View-Schicht, konva.js (und seine React-Bindungen react-konva) für die Verwaltung unserer Canvas-bezogenen Logik und eine kleine Zustandsverwaltungsbibliothek namens @halka/state um unseren Anwendungsstatus zu verwalten.

HINWEIS: Wir gehen davon aus, dass Sie mit React und React Hooks einigermaßen vertraut sind. Machen Sie sich keine Sorgen über die anderen Bibliotheken, wir werden ihre Anwendungsfälle im Laufe der Arbeit erklären.

Wenn Sie nur wegen des Quellcodes hier sind, können Sie gleich loslegen - Github Repo

Bevor wir anfangen, wollen wir uns ansehen, was wir bauen werden -

Eigenschaften

  • Ein linkes Bedienfeld mit Formen, die wir auf die Leinwand in der Mitte ziehen und ablegen können.
  • Wir können die Formen auf der Leinwand auswählen, ihre Größe ändern und sie drehen.
  • Wir ändern die Farbe der Konturen und die Füllfarbe der Formen im rechten Bedienfeld.
  • Speichern Sie den Zustand der Leinwand / des Diagramms im LocalStorage des Browsers.

Das war's für diese Runde.

HINWEIS: Ich werde Folgendes verwenden Garn durch den Artikel für die Ausführung von Skripten, aber Sie können auch die npm auch. Ersetzen Sie einfach Garn durch npm gleichwertige Befehle.

Erste Schritte

Um loszulegen -

  • Klonen Sie das Repository - git clone https://github.com/integrtr/integrtr_diagrams.git
  • Installieren Sie die Abhängigkeiten - Garn
  • Prüfen Sie die Stufe 0 Zweigstelle - git checkout step-0

Zu diesem Zeitpunkt ist dies eine einfache React-App, die mit create-react-app. Wir haben alle Dateien entfernt, die wir nicht brauchen, wie Tests, serviceWorker usw., und einige Goodies für eine schnellere Entwicklung wie HMR aktiviert. Sie müssen sich nicht darum kümmern, irgendetwas einzurichten.

Wenn Sie den Dev-Server starten, indem Sie Garnanfang sehen Sie eine React-App auf localhost:3000 mit dem Text "Let's get started".

Paneele und Leinwand hinzufügen

Wir fügen die Palette Komponente auf der linken Seite, Segeltuch in der Mitte und Inspektor auf der rechten Seite.

Wir werden alle drei Komponenten zur Basis hinzufügen App Komponente.

// App.js
importiere React von "react";

import { Palette } from "./Palette";;
import { Canvas } aus "./Canvas";
import { Inspector } from "./Inspector";

function App() {
  return (
    <div classname="app">
      <palette />
      <canvas />
      <inspector />
    </div>
  );
}

export default App;
// Palette.js
import React from "react";

import { DRAG_DATA_KEY, SHAPE_TYPES } from "./constants";

const handleDragStart = (Ereignis) =&gt; {
  const type = event.target.dataset.shape;

  if (type) {
    // x,y-Koordinaten des Mauszeigers relativ zur Position des Padding Edge des Zielknotens
    const offsetX = event.nativeEvent.offsetX;
    const offsetY = event.nativeEvent.offsetY;

    // Abmessungen des Knotens im Browser
    const clientWidth = event.target.clientWidth;
    const clientHeight = event.target.clientHeight;

    const dragPayload = JSON.stringify({
      type,
      offsetX,
      offsetY,
      clientBreite,
      clientHeight,
    });

    event.nativeEvent.dataTransfer.setData(DRAG_DATA_KEY, dragPayload);
  }
};

export function Palette() {
  return (
    <aside classname="palette">
      <h2>Formen</h2>
      <div
        classname="shape rectangle"
        data-shape="{SHAPE_TYPES.RECT}"        draggable
 ondragstart="{handleDragStart}"      />
      <div
        classname="shape circle"
        data-shape="{SHAPE_TYPES.CIRCLE}"        draggable
 ondragstart="{handleDragStart}"      />
    </aside>
  );
}

Fügen wir nun zwei Formen in der Palette Komponente - ein Rechteck und ein Kreis. Wir machen sie ziehbar und fügen Sie außerdem Datenform Attribut auf jedem der div um die richtige Form, die gezogen wird, in den Event-Handlern identifizieren zu können.

Schließlich fügen Sie die onDragStart Ereignisbehandler auf der div Elemente. In unserem handleDragStart Handler verwenden wir die HTML5 Drag and Drop API, um die dataTransfer Wert, um die notwendigen Daten zu übergeben, die vom Drop-Ereignis-Listener auf dem Canvas-Containerelement verwendet werden. Die Datenpunkte, die wir übergeben, sind Typ der Form, offsetX und offsetY Werte des Ereignisses und die Abmessungen des Palettenknotens. Wir können nur String-Werte übergeben, also müssen wir unser Payload-Objekt stringifizieren.

HINWEIS: Wir verwenden ausdrücklich das nativeEvent des Browsers, um die dataTransfer API, denn React verwendet Synthetische Ereignisse.

Jetzt fügen wir unsere Segeltuch Komponente.

// Canvas.js
importiere React von "react";
importiere { Ebene, Stufe } von "react-konva";

export function Canvas() {

  return (
    
); }

Die Bühne Komponente von react-konva ist verantwortlich für die Darstellung der Leinwand Element in das DOM ein. Wir setzen es Höhe und Breite auf der Grundlage der inneren Dimension Werte des Fensters. Wir versetzen 400px, um das Panel auf beiden Seiten zu berücksichtigen.

Zum Schluss fügen wir das rechte Panel hinzu, d.h. die Inspektor Komponente. Wir fügen hier erst einmal nur einen Platzhaltertext ein.

// PropertiesPanel.js
import React from "react";

exportiere Funktion PropertiesPanel() {
  return (
    <aside classname="panel">
      <h2>Eigenschaften</h2>
      <div classname="properties">
        <div classname="no-data">Nichts ist ausgewählt</div>
      </div>
    </aside>
  );
}

Mit der einige CSS im Hintergrund (Sie können überprüfen, dass in der Repo). Unsere wird beginnen, unser Endziel zu ähneln. Aber es ist noch nicht alles tun.

Ziehen und Ablegen von Formen aus der Palette auf die Leinwand

Wir haben die Formen bereits so eingerichtet, dass sie ziehbar im Palette. Jetzt müssen wir zulassen, dass sie fallen gelassen und zu den Segeltuch. Dazu benötigen wir zunächst einen Status, um zu verfolgen, welche Formen sich zu einem bestimmten Zeitpunkt im Canvas befinden und welche Eigenschaften sie haben, wie z. B. Formtyp, Position, Farbe usw. Wir werden diesen Zustand in Zukunft auch in anderen Komponenten benötigen, wie zum Beispiel in unserer Inspektor Komponente, um die Attribute zu aktualisieren. Wir werden diesen Zustand also global machen. Dafür werden wir eine kleine Zustandsverwaltungsbibliothek namens @halka/state.

Legen wir eine Datei an, die unseren globalen Zustand und die Handler zur Aktualisierung dieses Zustands enthält.

// state.js
import { createStore } aus "@halka/state";
importiere produce von "immer";

import { SHAPE_TYPES, DEFAULTS } from "./constants";

const baseState = {
  ausgewählt: null,
  shapes: {},
};

export const useShapes = createStore(baseState);
const setState = (fn) => useShapes.set(produce(fn));

@halka/state exportiert nur eine Funktion, d.h. createStore um, wie der Name schon sagt, einen globalen Zustandsspeicher zu erstellen. Wir übergeben ihm den Anfangszustand, der ein Objekt mit ausgewählt = null die später verwendet wird, um die ausgewählte Form auf der Leinwand zu verfolgen und Formen ist ein Objekt (das als mapähnliche Datenstruktur verwendet wird), das alle Daten für alle Formen auf der Leinwand enthält.

createStore gibt einen React-Hook zurück, benannt als useShapes die in jeder React-Funktionskomponente verwendet werden kann, um den Status zu abonnieren. useShapes Haken wird auch mit einem einstellen. Methode, die eine Statusaktualisierungsfunktion ähnlich der von React ist setState die zur Aktualisierung des Zustands verwendet werden können. Wir erstellen eine setState eine eigene Funktion, die mit immer um das Schreiben von verschachtelten / tiefgehenden Aktualisierungen zu erleichtern.

HINWEIS: Wir verwenden immer Bibliothek, um uns die Aktualisierung von verschachtelten Objekt-/Array-Zuständen mit veränderbaren APIs zu erleichtern und gleichzeitig die Vorteile von unveränderlichen Zustandsaktualisierungen zu erhalten. Erfahren Sie mehr darüber.

// state.js
import { nanoid } from "nanoid";

.
.
.

export const createRectangle = ({ x, y }) => {
  setState((state) => {
    state.shapes[nanoid()] = {
      type: SHAPE_TYPES.RECT, // Rechteck
      Breite: DEFAULTS.RECT.WIDTH, // 150
      height: DEFAULTS.RECT.HEIGHT, // 100
      fill: DEFAULTS.RECT.FILL, // #ffffff
      stroke: DEFAULTS.RECT.STROKE, // #000000,
      rotation: DEFAULTS.RECT.ROTATION, // 0
      x,
      y,
    };
  });
};

Anschließend schreiben wir die Funktion, die dem Zustand ein Rechteck hinzufügt, wenn sie ein Paar von x und y Koordinatenwerte. Wir verwenden nanoid um eindeutige Schlüssel für jede Form zu erzeugen und die x, y Werte, die zusammen mit dem Typ und Standardwerte für andere visuelle Attribute wie Breite, Höhe, füllen. und Schlaganfall.

Wir können dasselbe für den Kreis tun und durch Hinzufügen von Radius anstelle von Breite und Höhe.

// state.js
export const createCircle = ({ x, y }) => {
  setState((state) => {
    state.shapes[nanoid()] = {
      type: SHAPE_TYPES.CIRCLE, // Kreis
      radius: DEFAULTS.CIRCLE.RADIUS, // 50
      fill: DEFAULTS.CIRCLE.FILL, // weiß
      stroke: DEFAULTS.CIRCLE.STROKE, // schwarz
      x,
      y,
    };
  });
};

Als Nächstes wollen wir das Hinzufügen der Formen in der Segeltuch Bestandteil -

Um das Herausfallen des Elements aus dem Element zu behandeln, müssen wir eine onDrop Ereignishörer.

Schauen wir uns an, wie wir das Hinzufügen von Formen zum Zustand in handleDrop Griff-Funktion. Wir greifen auf die Typ der gezogenen Form durch Zugriff auf die DRAG_DATA_KEY Nutzlastwert auf der dataTransfer Attribut des nativeEvent.

Zur Erinnerung: Wir setzen die Nutzlast des stringifizierten Objekts in unserem Drag-Start-Ereignishandler

Wir müssen auch wissen, welche x und y Position des Drop-Ereignisses, die die Konva-Instanz verstehen kann, damit wir die Form an der Stelle des Drops hinzufügen können. Dazu müssen wir auf die Konva-Instanz des Canvas zugreifen. Wir können dies tun, indem wir einen ref auf die Bühne Komponente. Jetzt können wir auf die Konva-Instanz der Leinwand unter stageRef.current.

Um die richtige x und y Koordinate der Drop-Position im Canvas zu bestimmen, müssen wir zunächst die Zeigerposition in der Konva-Instanz manuell so einstellen, als ob das Ereignis im Canvas selbst ausgelöst worden wäre. Dies kann durch folgenden Aufruf geschehen setPointersPositions auf die Konva-Instanz und übergibt ihr die Veranstaltung Wert. Dann erhalten wir die abgeleitete Leinwandposition zurück, indem wir getPointersPositions auf der konva-Instanz.

Auf der Grundlage der Typ gezogen wird, rufen wir die createRectangle oder createCircle Zustandsaktualisierungsfunktion, die wir oben erstellt haben. Der Versatz des Mausereignisses im Verhältnis zu den Abmessungen des Knotens, der in der Palette gezogen wurde, muss ebenfalls berücksichtigt werden.

Um den Drop nahtlos zu gestalten und den Knoten genau an der Stelle hinzuzufügen, an der das Drop-Ereignis stattfindet, müssen wir einige Offset-Berechnungen durchführen. Wie im obigen Diagramm erwähnt, gilt für Rechtecke x und y Werte zeigen auf die linke obere Ecke. Daher können wir direkt die offsetX und offsetY Werte aus dem x und y Werte, die wir aus Konva zurückbekommen.

// state.js
createRectangle({
  x: coords.x - offsetX,
  y: coords.y - offsetY,
});
// state.js
createCircle({
  x: coords.x - (offsetX - clientBreite / 2),
  y: coords.y - (offsetY - clientHeight / 2),
});

Für den Kreis,  x und y Werte weisen auf den Mittelpunkt hin. Daher müssen wir zunächst die x und y in Bezug auf den Mittelpunkt des Knotens, d. h. clientBreite / 2 oder clientHöhe / 2. Schließlich subtrahieren wir die resultierenden Werte von den x und y Werte, die wir aus Konva zurückbekommen.

// Canvas.js
import React, { useRef, useCallback } from "react";
import { Layer, Stage } from "react-konva";

import { useShapes, createCircle, createRectangle } from "./state";
import { DRAG_DATA_KEY, SHAPE_TYPES } from "./constants";
import { Shape } from "./Shape";

const handleDragOver = (event) => event.preventDefault();

export function Canvas() {
  const shapes = useShapes((state) => Object.entries(state.shapes));

  const stageRef = useRef();

  const handleDrop = useCallback((event) => {
    const draggedData = event.nativeEvent.dataTransfer.getData(DRAG_DATA_KEY);

    if (draggedData) {
      const { offsetX, offsetY, type, clientHeight, clientWidth } = JSON.parse(
        draggedData
      );

      stageRef.current.setPointersPositions(event);

      const coords = stageRef.current.getPointerPosition();

      if (type === SHAPE_TYPES.RECT) {
        // rectangle x, y is at the top,left corner
        createRectangle({
          x: coords.x - offsetX,
          y: coords.y - offsetY,
        });
      } else if (type === SHAPE_TYPES.CIRCLE) {
        // circle x, y is at the center of the circle
        createCircle({
          x: coords.x - (offsetX - clientWidth / 2),
          y: coords.y - (offsetY - clientHeight / 2),
        });
      }
    }
  }, []);

  return (
    <main className="canvas" onDrop={handleDrop} onDragOver={handleDragOver}>
      <div className="buttons">
        <button onClick={saveDiagram}>Save</button>
        <button onClick={reset}>Reset</button>
      </div>
      <Stage
        ref={stageRef}
        width={window.innerWidth - 400}
        height={window.innerHeight}
        onClick={clearSelection}
      >
        <Layer>
          {shapes.map(([key, shape]) => (
            <Shape key={key} shape={{ ...shape, id: key }} />
          ))}
        </Layer>
      </Stage>
    </main>
  );
}

In der Segeltuch Komponente greifen wir auf alle Shape-Einträge aus unserem Speicher zu, indem wir die useShapes Haken wieder. Dann iterieren wir über das Array, um alle Formen als Kinder der Ebene Komponente. Wir übergeben die Schlüssel als id zusammen mit allen formbezogenen Daten als Form Stütze zum Form Komponente.

// Shape.js
import React, { useCallback } from "react";

import { SHAPE_TYPES } from "./constants";
import { useShapes } from "./state";
import { Circle } from "./Circle";
import { Rectangle } from "./Rectangle";

export function Shape({ shape }) {
  if (shape.type === SHAPE_TYPES.RECT) {
    return <Rectangle {...shape} />;
  } else if (shape.type === SHAPE_TYPES.CIRCLE) {
    return <Circle {...shape} />;
  }

  return null;
}

Wir müssen nun eine generische Form Komponente, die die richtige Komponente auf der Grundlage der ihr übergebenen ID wiedergibt.

Nun müssen wir die Komponenten erstellen, die die eigentlichen Formen im Canvas darstellen.

// Rectangle.js
importiere React von "react";
import { Rect as KonvaRectangle } from "react-konva";

export function Rectangle({ type, id, ...shapeProps }) {
  return (
     
  );
}
// Circle.js
importiere React von "react";
import { Circle as KonvaCircle } from "react-konva";

export function Circle({ type, id, ...shapeProps }) {
  return (
    
  );
}

Im Moment befassen wir uns nur mit der shapeProps Werte (wie Schlaganfall, füllen., Breite, Höhe und Radius).

Das war's, jetzt können wir Formen per Drag&Drop aus dem Palette und sie werden in die Segeltuch.

Auswählen, Verschieben, Ändern der Größe und Drehen von Formen

Wir beginnen mit dem Hinzufügen der Zustandsaktualisierungsfunktion zum Auswählen und Verschieben von Formen.

// state.js
export const selectShape = (id) => {
  setState((state) => {
    state.selected = id;
  });
};

export const clearSelection = () => {
  setState((state) => {
    state.selected = null;
  });
};

export const moveShape = (id, event) => {
  setState((state) => {
    const shape = state.shapes[id];

    if (shape) {
      shape.x = event.target.x();
      shape.y = event.target.y();
    }
  });
};

Für selectShape Handler, setzen wir einfach die id der Form als die ausgewählte Eigenschaft in unserem Zustand. Die clearSelection Handler setzt das Ausgewählte zurück auf null. Für moveShape Handler überprüfen wir zunächst, ob eine Form ausgewählt ist und aktualisieren dann die x und y Koordinatenwerte der Form.

// state.js
export const transformRectangleShape = (node, id, event) => {
  // Transformator ändert den Maßstab des Knotens
  // und NICHT seine Breite oder Höhe
  // aber im Speicher haben wir nur Breite und Höhe
  // um die Daten besser anzupassen, wird die Skalierung am Ende der Transformation zurückgesetzt
  const scaleX = node.scaleX();
  const scaleY = node.scaleY();

  // wir setzen den Maßstab zurück
  node.scaleX(1);
  node.scaleY(1);

  setState((state) => {
    const shape = state.shapes[id];

    if (shape) {
      shape.x = node.x();
      shape.y = node.y();
      
      shape.rotation = node.rotation();
      
       shape.width = clamp(
        // Vergrößerung der Breite in der Reihenfolge der Skalierung
        node.width() * scaleX,
        // sollte nicht kleiner als die Mindestbreite sein
        LIMITS.RECT.MIN,
        // sollte nicht größer als die maximale Breite sein
        LIMITS.RECT.MAX
      );
      shape.height = clamp(
        node.height() * scaleY,
        LIMITS.RECT.MIN,
        GRENZEN.REKT.MAX
      );
    }
  });
};

Dann erstellen wir die transformRectangleShape Funktion zur Statusaktualisierung. Diese Funktion ist für die Handhabung der Ereignisse zur Größenänderung und Drehung einer Rechteckform im Canvas verantwortlich. Wir werden die Funktion Konva.Transformator für die Handhabung dieser Transformationen im Canvas. Standardmäßig wird der Maßstab der umgewandelten Form geändert, aber wir müssen Breite und Höhe erhöhen, damit andere Eigenschaften (wie die Strichstärke) nicht beeinträchtigt werden. Um die Maßstabsänderung in eine Dimensionsänderung umzuwandeln, wird zunächst die Reihenfolge der Maßstabsänderungen zwischengespeichert und der Maßstab des Knotens auf 1 (Standard) zurückgesetzt. Dann setzen wir den neuen aktuellen Dimensionswert gleich dem aktuellen Dimensionswert multipliziert mit der Skalenreihenfolge. Die Breite sollte nicht über die Mindest- und Höchstgrenzen hinausgehen.

Schauen wir uns nun die Einrichtung in der Rechteck Komponente unter Verwendung der Transformator Komponente von react-konva.

// Rectangle.js
import React, { useRef, useEffect, useCallback } from "react";
import { Rect as KonvaRectangle, Transformer } from "react-konva";

import { LIMITS } from "./constants";
import { selectShape, transformRectangleShape, moveShape } from "./state";

const boundBoxCallbackForRectangle = (oldBox, newBox) => {
  // Größenänderung begrenzen
  if (
    newBox.width < LIMITS.RECT.MIN ||
    newBox.height  LIMITS.RECT.MAX ||
    neueBox.Höhe > LIMITS.RECT.MAX
  ) {
    return oldBox;
  }
  return newBox;
};

export function Rectangle({ id, isSelected, type, ...shapeProps }) {
  const shapeRef = useRef();
  const transformerRef = useRef();

  useEffect(() => {
    if (isSelected) {
      transformerRef.current.nodes([shapeRef.current]);
      transformerRef.current.getLayer().batchDraw();
    }
  }, [isSelected]);

  const handleSelect = useCallback(
    (Ereignis) => {
      event.cancelBubble = true;

      selectShape(id);
    },
    [id]
  );

  const handleDrag = useCallback(
    (Ereignis) => {
      moveShape(id, event);
    },
    [id]
  );

  const handleTransform = useCallback(
    (Ereignis) => {
      transformRectangleShape(shapeRef.current, id, event);
    },
    [id]
  );

  return (
    
      
      {isSelected && (
        
      )}
    
  );
}

Wir fügen die handleSelect Handler auf anklicken., tippen Sie auf und dragStart Ereignis auf der KonvaRectangle Komponente, die die selectShape Funktion mit der ID der Form. Es muss verhindert werden, dass das Ereignis auch auf den übergeordneten Kontext übergreift. Das liegt daran, dass wir die Funktion clearSelection Handler auf der Segeltuch Klick-Ereignis. Wenn wir nicht verhindern, dass dieses Ereignis auf den Canvas überspringt, wird es den Click-Handler auf Segeltuch auch, wodurch die Auswahl sofort nach dem Setzen gelöscht wird. Wir fügen die handleDrag Handler auf dragEnd Ereignis, das die Funktion moveShape Funktion mit der id. Wir müssen auch die ziehbar boolesche Eigenschaft, um auf die Drag-Ereignisse zu warten.

Nun müssen wir uns ansehen, wie wir auf die Größenänderungs- und Drehungsereignisse reagieren können. Dazu müssen wir zunächst eine ref über die KonvaRectangle Komponente, um auf die Konva-Knoteninstanz der Form zugreifen zu können. Außerdem müssen wir eine Transformator Komponente mit einer ref damit verbunden. Wir wollen nur die Transformator aktiv sein soll, wenn die Form ausgewählt ist. Wir rendern sie also bedingt und fügen außerdem eine useEffect Aufruf, der ausgeführt wird, wenn isSelected Stützenänderungen. Wenn isSelected wahr ist, fügen wir die Instanz des Formknotens hinzu (Zugriff über shapeRef.current) zu den Knoten der Transformatorinstanz (Zugriff über transformerRef.current) und rufen die batchDraw Methode auf den Transformator, um ihn auf die Leinwandebene zu zeichnen.

Wir fügen auch eine boundBoxFunc auf dem Transformator, die verhindert, dass die Form in eine ungültige Dimension transformiert wird (d. h. innerhalb der Min-/Max-Dimensionsgrenzen bleibt).

// Circle.js

Wir haben eine ähnliche Einrichtung in der Kreis Komponente. Die einzigen größeren Änderungen betreffen die Einrichtung der Transformator Komponente. Wir haben rotateEnabled eingestellt auf falsch weil es nicht notwendig ist, einen Kreis zu drehen, und die Anker zur Größenänderung nur an den vier Ecken verfügbar sind, so dass das Verhältnis von Höhe und Breite immer intakt ist. Sie können die benutzerdefinierte enabledAnchors einrichten, wenn der Kreis in eine Ellipse umgeformt werden soll.

Werfen wir nun einen Blick darauf, wie die neue isSelected wird an die Komponente übergeben.

// Shape.js
import React, { useCallback } from "react";

import { SHAPE_TYPES } from "./constants";
import { useShapes } from "./state";
import { Circle } from "./Circle";
import { Rectangle } from "./Rectangle";

export function Shape({ shape }) {
  const isSelectedSelector = useCallback(
    (state) => state.selected === shape.id,
    [shape]
  );
  const isSelected = useShapes(isSelectedSelector);

  if (shape.type === SHAPE_TYPES.RECT) {
    return <Rectangle {...shape} isSelected={isSelected} />;
  } else if (shape.type === SHAPE_TYPES.CIRCLE) {
    return <Circle {...shape} isSelected={isSelected} />;
  }

  return null;
}

In der Form Komponente verwenden wir einen benutzerdefinierten Selektor, um zu prüfen, ob die ausgewählt id-Wert im Status ist gleich dem id der Form und geben das boolesche Ergebnis an die Komponente weiter, die die Form rendert.

Um die Aufhebung der Auswahl beim Anklicken des leeren Bereichs einer Leinwand zu behandeln, fügen wir die clearSelection Handler auf der anklicken. Veranstaltung des Bühne Komponente.

// Canvas.js

  
    {shapes.map(([key, shape]) => (
      
    ))}

Aktualisieren der Kontur- und Füllfarbe von Shapes

Die Schlaganfall und füllen. Die Farbe der Formen wird auf schwarz und weiß bzw. standardmäßig. Wir möchten jedoch dem Benutzer die Möglichkeit geben, diese Werte zu aktualisieren. Der Benutzer sollte in der Lage sein, diese Werte für die ausgewählte Form über das rechte Bedienfeld zu aktualisieren.

// state.js
export const updateAttribute = (attr, value) => {
  setState((state) => {
    const shape = state.shapes[state.selected];

    if (shape) {
      shape[attr] = value;
    }
  });
};

Zunächst erstellen wir die updateAttribute Zustandsfunktion, die einen bestimmten Attributwert der ausgewählten Form aktualisiert.

Dann, in der PropertiesPanel Komponente werden zunächst alle Eigenschaften dargestellt, d. h. Schlaganfall und füllen. zusammen mit dem Typ. Und bei Änderung eines der Farbeingänge rufen wir die updateAttribute Funktion mit dem Attributschlüssel und dem Aktualisierungswert.

// PropertiesPanel.js
import React, { useCallback } from "react";

import { useShapes, updateAttribute } from "./state";

const shapeSelector = (state) =&gt; state.shapes[state.selected];

export function PropertiesPanel() {
  const selectedShape = useShapes(shapeSelector);

  const updateAttr = useCallback((event) =&gt; {
    const attr = event.target.name;

    updateAttribute(attr, event.target.value);
  }, []);

  return (
    <aside classname="panel">
      <h2>Eigenschaften</h2>
      <div classname="properties">
        {selectedShape ? (
          <>
            <div classname="key">
              Typ <span classname="value">{selectedShape.type}</span>
            </div>

            <div classname="key">
              Schlaganfall{" "}
              <input
                classname="value"
                name="stroke"
                type="color"
                value="{selectedShape.stroke}"                onchange="{updateAttr}"              />
            </div>

            <div classname="key">
              Ausfüllen{" "}
              <input
                classname="value"
                name="fill"
                type="color"
                value="{selectedShape.fill}"                onchange="{updateAttr}"              />
            </div>
          </>
        ) : (
          <div classname="no-data">Nichts ist ausgewählt</div>
        )}
      </div>
    </aside>
  );
}

Persistieren des Zustands in LocalStorage

Zu guter Letzt bauen wir die Persistenz zu LocalStorage auf. Dies ist wahrscheinlich das einfachste von allen.

// state.js
const APP_NAMESPACE = "__integrtr_diagrams__";

const baseState = {
  ausgewählt: null,
  shapes: {},
};

export const useShapes = createStore(() => {
  const initialState = JSON.parse(localStorage.getItem(APP_NAMESPACE));

  return { ...baseState, shapes: initialState ?? {} };
});
const setState = (fn) => useShapes.set(produce(fn));

export const saveDiagram = () => {
  const state = useShapes.get();

  localStorage.setItem(APP_NAMESPACE, JSON.stringify(state.shapes));
};

export const reset = () => {
  localStorage.removeItem(APP_NAMESPACE);

  useShapes.set(baseState);
};

Wir müssen den Code für die Initialisierung des Zustands unseres Shops aktualisieren. Anstatt den Anfangszustand direkt zu setzen, übergeben wir eine Funktion an die createStore Funktion, die prüft, ob der Zustand der Formen im LocalStorage gespeichert ist, und die den Anfangszustand auf der Grundlage dieses Zustands generiert.

Wir schreiben auch die saveDiagram Funktion, die lediglich die Formen aus dem Zustand in LocalStorage und dem zurücksetzen Funktion, die den aktuellen Zustand sowie den LocalStorage bereinigt.

Schließlich fügen wir zwei Schaltflächen hinzu, die die Aktionen Speichern und Zurücksetzen auslösen.

// App.js
<div classname="buttons">
  <button onclick="{saveDiagram}">Speichern Sie</button>
  <button onclick="{reset}">Zurücksetzen</button>
</div>

Das ist alles!

Wir haben unser Diagrammerstellungstool einsatzbereit. Es mag in Bezug auf die Funktionen minimal sein, aber es ist definitiv eine solide Basis, auf der wir aufbauen können.

Nun, da Sie mit den Grundlagen von HTML5 Canvas vertraut sind, versuchen Sie, darauf aufzubauen und Ihre coolen Projekte mit uns zu teilen.

Je effizienter die Digitalisierung und der Datenfluss sind, desto höher sind der Unternehmenswert und die Wettbewerbsfähigkeit.

Möchten Sie ein INTEGRTR werden?

DE