Xây dựng trình tạo khung hồ sơ với React Konva

Nếu bạn đã dành thời gian trên mạng xã hội, chắc hẳn bạn đã thấy một trong những người bạn của mình thay đổi ảnh đại diện Facebook của họ để đưa vào một khung hình ủng hộ đội thể thao yêu thích của họ. Hoặc, bạn có thể đã xem hồ sơ LinkedIn của một đồng nghiệp với khung nói về sự cởi mở với công việc. Trong bài viết này, chúng tôi sẽ xây dựng trình tạo khung ảnh hồ sơ của riêng mình trong React bằng cách sử dụng React Konva.

Ví dụ, chúng tôi sẽ sử dụng bộ tạo khung mà tôi đã giúp xây dựng và duy trì cho cuộc thi Hackathon của trường đại học của tôi. Vào cuối hướng dẫn này, bạn sẽ có thể tạo một trình tạo khung hồ sơ để những người tham gia sự kiện trực tuyến của bạn sử dụng. Đầu ra cuối cùng của chúng tôi sẽ giống như hình dưới đây:

Cuối cùng của React Frame Genrator

Bạn cũng có thể kiểm tra ví dụ trực tiếp. Bắt đầu nào!

React Konva là gì?

Các API canvas HTML5 là một công cụ mạnh mẽ để kết hợp hình ảnh động, hiệu quả vào các ứng dụng web. Canvas hỗ trợ nhiều loại trình duyệt và bao gồm các API tích hợp để thêm hình dạng, hình ảnh và văn bản. Một loạt các thư viện tiện ích như Konva.jsp5.js đã được tạo trên Canvas để hỗ trợ xử lý sự kiện và hoạt ảnh.

Canvas API về cơ bản là bắt buộc và nó được xây dựng để tương tác với JavaScript vani bằng các phương thức như ctx.rect(x, y, width, height). Cú pháp này hoàn toàn xa lạ với các nhà phát triển React và không tương thích với các mẫu React hiện đại. React Konva, một trình bao bọc React cho Konva.js, cho phép bạn sử dụng các giao diện thành phần React để kết nối với API Konva.

Điều kiện tiên quyết

Để làm theo bài viết này, bạn nên làm quen với React và Tạo ứng dụng React. Bạn cũng nên hiểu về các công cụ quản lý trạng thái như Redux hoặc React Context API. Ngoài ra, kiến ​​thức trước đây về React Konva cũng rất hữu ích.

Trong hướng dẫn này, chúng tôi sẽ sử dụng các công nghệ sau: React, Tạo ứng dụng React, các thành phần được tạo kiểu, API ngữ cảnh của React, React Konva và Netlify. Bắt đầu nào!

Bắt đầu

Đầu tiên, chúng tôi sẽ cài đặt Yarn để làm cho quá trình phát triển của chúng tôi trơn tru hơn nhiều:

# Globally install yarn
npm install -g yarn

Tiếp theo, chúng ta sẽ tạo một ứng dụng React cơ bản bằng cách sử dụng Create React App. Để chạy lệnh này, bạn cần cài đặt Node.js. Bạn có thể kiểm tra xem Node.js đã được cài đặt hay chưa bằng cách sử dụng lệnh node --version. Cài đặt Node.js sẽ tự động bật npmnpx lệnh:

# Using npm
npx create-react-app profile-frames

# Using yarn
yarn create react-app profile-frames

Hãy thiết lập một cấu trúc thư mục sạch; xóa tất cả các tệp đi kèm với Create React App và tạo các thư mục và tệp cần thiết như hình dưới đây:

Cấu trúc thư mục sạch

  • public: Các tệp trong thư mục này sẽ có thể truy cập trực tiếp cho người dùng
  • src: Chứa các tệp mà chúng tôi sẽ tải lên và tạo
    • assets: Chứa tất cả hình ảnh và khung
    • components: Chứa các thành phần
    • pages: Chứa các màn hình khác nhau cho ứng dụng của chúng tôi
    • store: Chứa các tệp xử lý quản lý trạng thái toàn cầu của ứng dụng

Để chạy ứng dụng React của bạn, hãy tạo index.jsApp.jsx các tập tin trong src thư mục, sau đó thêm mã sau:

// App.jsx
import React from "react";

const App = () => {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  );
};

export default App;

// index.js
import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.querySelector("#root")
);

Cuối cùng, chúng tôi sẽ cài đặt các thành phần được tạo kiểu và React Konva cho các thao tác tạo kiểu CSS và canvas, tương ứng:

yarn add styled-components react-konva konva

Trong bài viết này, chúng tôi sẽ không tập trung chi tiết vào kiểu CSS. Bạn có thể tìm thấy chi tiết của việc triển khai cụ thể trong kho lưu trữ ở trên.

Liệt kê các tính năng của ứng dụng của chúng tôi

Trước khi bắt đầu viết mã, bạn nên lập danh sách chi tiết các tính năng mà chúng tôi muốn đưa vào. Ứng dụng của chúng tôi sẽ bao gồm các tính năng sau và chúng tôi sẽ xây dựng các thành phần của mình dựa trên danh sách này:

  • MỘT Stage thành phần nơi người dùng có thể tương tác với các phần tử Konva khác nhau
  • Một băng chuyền bao gồm các tùy chọn khung hình khác nhau để người dùng lựa chọn
  • Một bảng điều khiển bao gồm các phần sau:
    • Một upload image cái nút
    • NameGuild đầu vào
    • MỘT Download cái nút

Kiến trúc thành phần dưới đây sẽ phù hợp với dự án của chúng tôi:

Cấu trúc thư mục dự án React Konva

Tạo Canvas thành phần

React Konva có một thành phần được gọi là Stage mà bạn có thể sử dụng để hiển thị các hình dạng và mục khác nhau. Thành phần con trực tiếp của Stage cần phải là một Layer thành phần, sau đó có thể chứa các thành phần khác nhau như hình ảnh, văn bản, hình dạng, v.v. Trong phần trình diễn của chúng tôi, một lớp duy nhất sẽ hoàn thành công việc.

Tạo một tệp mới có tên Canvas.jsx bên trong components thư mục và thêm mã sau:

import React, { useRef } from "react";

// Libraries
import { Layer, Stage, Image, Text } from "react-konva";

// Assets
import frame1 from "../assets/frame1";
import profile from "../assets/frame1";

const Canvas = () => {
  /**
   * A list of variables containing the various details that we
   * would require for all the different elements.
   * 
   * It is better to separate them now as we will be shifting 
   * them to their global states later
   */
  const stageRef = useRef();
  const stageDetails = {
    width: 350,
    height: 350,
    x: 0,
    y: 0,
  };

  const imageDetails = {
    originalDimensions: {
      width: 0,
      height: 0,
    },
    renderDimensions: {
      width: 300,
      height: 300,
    },
    position: {
      x: 0,
      y: 0,
    },
  };

  const textDetails = {
    name: {
      value: "name field",
      id: "user-name",
      dimensions: {
        width: 100,
        height: 50,
      },
      position: {
        x: 50,
        y: 50,
      },
    },
    guild: {
      value: "guild field",
      id: "user-guild",
      dimensions: {
        x: 100,
        y: 50,
      },
      position: {
        x: 100,
        y: 100,
      },
    },
  };

  return (
    <Stage
      ref={stageRef}
      width={stageDetails.width}
      height={stageDetails.height}
      style={{ position: "relative" }}
    >
      <Layer>
        <Image
          image={profile}
          width={imageDetails.renderDimensions.width}
          height={imageDetails.renderDimensions.height}
          x={imageDetails.position.x}
          y={imageDetails.position.y}
        />

        <Text
          text={textDetails.name.value}
          width={textDetails.name.dimensions.width}
          height={textDetails.name.dimensions.height}
          x={textDetails.name.position.x}
          y={textDetails.name.position.y}
        />
        <Text
          text={textDetails.guild.value}
          width={textDetails.guild.dimensions.width}
          height={textDetails.guild.dimensions.height}
          x={textDetails.guild.position.x}
          y={textDetails.guild.position.y}
        />

        <Image
          image={frame1}
          width={stageDetails.width}
          height={stageDetails.height}
          x={0}
          y={0}
          style={{ position: "absolute", top: 0, left: 0, zIndex: 100 }}
        />
      </Layer>
    </Stage>
  );
};

export default Canvas;

Có rất nhiều điều xảy ra trong đoạn mã này. Hãy xem lại các đạo cụ khác nhau đang được chuyển cho tất cả các phần tử.

Mỗi canvas phần tử có các đạo cụ sau:

  • widthheight, chỉ định kích thước của thành phần cụ thể
  • xy, xác định vị trí của thành phần

xy có liên quan đến Stage , bắt đầu từ góc trên bên trái của Stage thành phần. Các Stage thành phần yêu cầu một tham chiếu, được tạo bằng cách sử dụng useRef Cái móc.

Có hai thành phần hình ảnh, một cho ảnh đại diện và một cho khung. Những hình ảnh này phải có độ phân giải cao, vì vậy chúng tôi sẽ cần tính toán kích thước hiển thị cho mỗi hình ảnh để vừa với Stage thành phần. Ngoài ra, có hai thành phần văn bản đều có text đề xuất hiển thị nội dung, tức là văn bản mà chúng tôi cung cấp.

Tại thời điểm này, bước tiếp theo của bạn sẽ là lấy Stage thành phần đã sẵn sàng, nhưng bạn sẽ nhận thấy một số vấn đề. Đối với một, văn bản xuất hiện không có nền. Chúng tôi sẽ cần phải sửa nó để nó trông khác biệt với hình ảnh. Ngoài ra, không có phần tử nào của chúng tôi có thể biến đổi, có nghĩa là chúng tôi không thể thay đổi kích thước hoặc xoay các phần tử. Cuối cùng, ứng dụng của chúng tôi là tĩnh, nhưng chúng tôi muốn làm cho nó động và tương tác.

Tạo một tùy chỉnh text thành phần

Tích hợp sẵn text thành phần không bao gồm bất kỳ nền nào. Chúng tôi sẽ tạo thành phần tùy chỉnh của riêng mình bằng cách sử dụng text thành phần được đặt trên đầu trang của một Rect, Hình chữ nhật, thành phần. React Konva cung cấp một group thành phần, cung cấp một bộ đạo cụ, như kích thước và vị trí, cho chỉ một thành phần thay vì nhiều thành phần.

Tạo một tệp mới có tên CustomText.jsx và thêm mã sau:

import React from "react";

// Libraries
import { Rect, Text, Group } from "react-konva";

const CustomText = ({ dimensions, position, name }) => {
  const shapeRef = React.useRef(null);

  /**
   * As with other konva components, group also takes in
   * width, height and positions x and y.
   *
   * In addition to this, the properities of offsetX and offsetY
   * prop which shifts its coordinate system origin to the center instead
   * of the top-left corner are also added.
   *
   * This would help in positioning both the rectangle and the
   * text element at the center of the group.
   */
  const groupProps = {
    width: dimensions.width,
    height: dimensions.height,
    offsetX: dimensions.width / 2,
    offsetY: dimensions.height / 2,
    x: position.x,
    y: position.y,
  };

  return (
    <Group ref={shapeRef} {...groupProps}>
      {/* The width of both the elements are kept same */}
      {/* Not passing any positions defaults them to x=0 and y=0 */}
      <Rect
        width={dimensions.width}
        height={dimensions.height}
        cornerRadius={[7, 7, 7, 7]}
                                fill="lightblue"
      />
      <Text
        width={dimensions.width}
        height={dimensions.height}
        align='center'
        verticalAlign='middle'
        text={name}
        fontSize={20}
      />
    </Group>
  );
};

export default CustomText;

Bây giờ, chúng tôi có một tùy chỉnh text thành phần rõ ràng hơn và dễ phân biệt hơn với hình nền.

Chia tỷ lệ các thành phần của bạn với Transformer

Tại thời điểm này, chúng tôi đã chuẩn bị phần lớn các thành phần của mình. Tuy nhiên, chúng tôi đang thiếu một tính năng chính sẽ mang lại một cấp độ tùy chỉnh hoàn toàn mới cho ứng dụng của chúng tôi.

Hãy bắt đầu với hình ảnh. Trước khi chúng tôi có thể tạo tùy chỉnh của mình image , chúng ta cần cấu trúc lại Canvas thành phần để bao gồm các trạng thái, cho phép chúng tôi chuyển sang một ứng dụng năng động hơn với các tương tác:

import profile from "../assets/frame1";

const Canvas = () => {
        /* Existing Code */

        /**
   * Okay so the imageDetails variables are removed and 
   * shifted to a state. Not only this but also 2 new properties of 
         * scale defaulted to 1 which would determine
   * the size of our shape/element and id are added
   * 
   * In addition to that, a new state called selectedElement is also
   * selectedElement. This element stores an id or unique field which
   * showcases which element is currently selected.
   */
        const [selectedElement, setSelectedElement] = useState(null);
  const [imageDetails, setImageDetails] = useState({
    originalDimensions: {
      width: 0,
      height: 0,
    },
    renderDimensions: {
      width: 300,
      height: 300,
    },
    position: {
      x: 0,
      y: 0,
    },
    scale: 1,
                id: 'user-profile-image',
                image: profile
  });

        /* Existing code */
}

export default Canvas;

Với Stage thiết lập để bao gồm các trạng thái, hãy xây dựng thành phần hình ảnh có thể biến đổi của chúng tôi. Để giảm mã trong một thành phần duy nhất và tránh bất kỳ loại lặp lại nào, hãy tạo một tệp mới có tên CustomImage.jsx.

React Konva cung cấp một Transformer thành phần tạo ra một hộp xung quanh hình dạng, vì vậy người dùng có thể dễ dàng thay đổi kích thước hoặc xoay nó. Thêm mã sau vào CustomImage.jsx tập tin:

import React from "react";

// Components
import { Image, Transformer } from "react-konva";

const CustomImage = ({
  imageDetails,
  setImageDetails,
  isSelected,
  onSelect,
}) => {
  /**
   * Create references to the shape which needs to be transformed
   * and to the transformer component itself.
   */
  const shapeRef = React.useRef();
  const transformerRef = React.useRef();

  /**
   * This effect runs whenever the isSelected variable is toggled
   * The isSelected variable is set from the parent element which indicates
   * that the current element is selected and is to be transformed.
   */
  React.useEffect(() => {
    if (isSelected) {
      /**
       * Here you are instructing the transformer component via its ref to
       * enable the specified component i.e. the image is to be transformed
       * and then create the transformer box around it.
                         * This code will run everytime the isSelected variable is updated.
       */
      transformerRef.current?.nodes([shapeRef.current]);
      transformerRef.current?.getLayer().batchDraw();
    }
  }, [isSelected]);

  const {
    renderDimensions: { width, height },
    position: { x, y },
    image,
  } = imageDetails;

  /**
   * The most important handler functions for transformations
   * You need to handle 2 things -
   *    Change in Dimensions on transform and
   *    Change in Positions on drag
   */

  /**
   * This function handles the dimension changes of the shape
   * If you recall, you have set a property named scale equal to 1 on
   * initialisation.
   * Using this handler, you need to update the scale property of this
   * shape which can be obtained from the shapeRef
   */
  const onTransformEnd = () => {
    if (shapeRef.current) {
      const node = shapeRef.current;
      setImageDetails((current) => ({ ...current, scale: node.scale() }));
    }
  };

  /**
   * This function handles the positional changes of the shape
   * You have positions (x and y) properties in the state which you
   * will update through this handler, similar to the onTransformEnd
   * function.
   */
  const onDragEnd = () => {
    if (shapeRef.current) {
      const node = shapeRef.current;
      setImageDetails((current) => ({ ...current, x: node.x(), y: node.y() }));
    }
  };

  return (
    <>
      <Image
        ref={shapeRef}
        image={image}
        width={width}
        height={height}
        x={x}
        y={y}

        /**
        onSelect is a function that toggles the isSelected
        variable. This function is called when image is
        clicked or tapped.
        */
        onClick={onSelect}
        onTap={onSelect}

        /** Transformation Handlers Explained above */
        onTransformEnd={onTransformEnd}
        onDragEnd={onDragEnd}
      />

      {isSelected && (
        <Transformer
          ref={transformerRef}
          boundBoxFunc={(oldBox, newBox) => {
            /**
              this function handles the sizing of the box
              Essentially what it does is adding a check
              to avoid reduction of size to 0
              if the newBox dimensions are less than 5 units,
              it returns the oldBox dimensions
             */
            if (newBox.width < 5 || newBox.height < 5) {
              return oldBox;
            }
            return newBox;
          }}
        />
      )}
    </>
  );
};

export default CustomImage;

Bây giờ bạn đã thiết lập thành phần hình ảnh có thể biến đổi, hãy cập nhật mã của chúng tôi với thành phần mới. bên trong Canvas.jsx , thay thế thành phần hình ảnh bằng mã sau:

Lưu ý: Vì bạn không muốn khung có thể biến đổi, nên chỉ thay thế thành phần hình ảnh được sử dụng cho hồ sơ người dùng.

                                <CustomImage
          imageDetails={imageDetails}
          setImageDetails={setImageDetails}
          isSelected={selectedElement === imageDetails.id}
          onSelect={() => setSelectedElement(imageDetails.id)}
        />

Bạn cũng sẽ muốn cấu trúc lại CustomText để làm cho nó có thể biến đổi.

Đã đến lúc tái cấu trúc

Với thiết lập hiện tại, chúng tôi có rất nhiều mục liên quan đến trạng thái được lưu trữ trong chính thành phần, như chi tiết hình ảnh, chi tiết văn bản và chi tiết giai đoạn cũng như các chức năng xử lý. Với tốc độ này, mã của bạn sẽ nhanh chóng trở nên lộn xộn và không thể đọc được.

Ngoài ra, chỉ với ba thành phần trong mã của chúng tôi, có rất nhiều lỗ khoan chống đỡ xảy ra, đây không phải là phương pháp hay. Chúng tôi sẽ cần cải thiện một số trạng thái được yêu cầu bởi các thành phần như đầu vào và nút tải lên.

Hãy thiết lập quản lý nhà nước toàn cầu. Chúng tôi sẽ sử dụng API ngữ cảnh cùng với useReducer Cái móc. Tôi tin rằng cốt lõi của nó, React là một thư viện quản lý trạng thái và do đó bất kỳ thư viện bên ngoài nào như Redux đều không cần thiết.

bên trong store/actions thư mục, tạo một tệp mới có tên là frames.action.js và thêm mã sau:

/**
 * the useReducer hook from react takes the initialState as
 * one of its parameters. If no param is passed, the initial state
 * would be considered as null which not necessarily wrong but not at
 * all a better practice. It can lead to unknown undefined errors during
 * build time.
 * As defined below, this is the initial state structure considering all
 * the required fields related to the user profile image.
 */
export const initialState = {
  imageDetails: {
    originalDimensions: {
      width: 0,
      height: 0,
    },
    renderDimensions: {
      width: 0,
      height: 0,
    },
    position: {
      x: 0,
      y: 0,
    },
    scale: 1,
    id: "user-profile-image",
    image: null,
  },
};

/**
 * Similar to redux, define all the different types of
 * actions related to image state changes to avoid any errors down
 * the line.
 */
export const CANVAS_ACTIONS = Object.freeze({
  UPLOAD_IMAGE: "IMAGE/UPDATE_IMAGE_DETAILS",
  UPDATE_IMAGE_DIMENSIONS: "IMAGE/UPDATE_IMAGE_RENDER_DIMENSIONS",
  UPDATE_IMAGE_POSITION: "IMAGE/UPDATE_IMAGE_POSITIONS",
});

Với các loại hành động và trạng thái ban đầu của chúng tôi đã thiết lập, hãy thực hiện tương tự đối với bộ giảm tốc. Tạo một tệp có tên frames.reducer.js phía dưới cái store/reducers thư mục và thêm mã sau:

import { CANVAS_ACTIONS } from "../actions/compose.action";

/**
 * Similar to Redux, canvasReducer handles all the different
 * actions and the changes to be made to the state depending
 * on the action type.
 *
 * For now, each case returns the default state. You'll start
 * writing cases after the context API is setup
 */
export default function canvasReducer(state, action) {
  switch (action.type) {
    case CANVAS_ACTIONS.UPLOAD_IMAGE:
      return state;

    case CANVAS_ACTIONS.UPDATE_IMAGE_DIMENSIONS:
      return state;

    case CANVAS_ACTIONS.UPDATE_IMAGE_POSITIONS:
      return state;

    default:
      return state;
  }
}

Trong một ứng dụng React tiêu chuẩn, các đạo cụ được sử dụng để gửi dữ liệu từ trên xuống từ cấp độ gốc đến cấp độ con. Tuy nhiên, luồng dữ liệu này có thể gây bất tiện cho một số loại đạo cụ được yêu cầu bởi nhiều thành phần trong một ứng dụng. API ngữ cảnh giúp bạn có thể chia sẻ các giá trị như thế này giữa các thành phần mà không cần phải chuyển một cách rõ ràng một phần mềm hỗ trợ qua mỗi cấp của cây thành phần.

Tạo một tệp mới có tên canvas.context.js phía dưới cái store/contexts thư mục và thêm mã sau:

import React, { useReducer, useMemo, createContext, useContext } from "react";

// Reducer, Initial State, Types
import canvasReducer from "../reducers/frames.reducer";
import { initialState } from "../actions/frames.action";

/**
* use the createContext function from react to create a context component
*/
const FramesContext = createContext(initialState);

export function FramesCtxProvider(props) {
  /**
   * The useReducer hook provided by React enables you to create
   * global states. Similar to the useState hook, useReducer provides
   * access to the state through is first destructured variable and a
   * function - dispatch to which you pass an object consisting of 2 properites -
   *
   * dispatch({
   *         type: one of the types from CANVAS_ACTIONS,
   *         payload: data that would be sent to reducer function to update the state,
   * })
   */
  const [state, dispatch] = useReducer(canvasReducer, initialState);
  const value = useMemo(() => [state, dispatch], [state]);
  return <FramesContext.Provider value={value} {...props} />;
}

/**
 * A very handy custom hook to easily get access to the state and dispatch functions
 * in any component
 *
 * This avoids quite a few steps where you would have to import the above context,
 * useContext hook from react and bunch of other steps.
 *
 * Instead, all you have to do now is import this hook and call it inside a component!
 */
export function useFrames() {
  const context = useContext(FramesContext);
  if (!context)
    throw new Error("useFrames must be used within a FramesCtxProvider");

  const [state, dispatch] = context;
  return [state, dispatch];
}

Trước khi tiếp tục với trình giảm bớt, bạn cần phải bọc ứng dụng của mình bằng trình cung cấp ngữ cảnh. Hướng tới App.jsx và cập nhật nó bằng mã sau:

import React from "react";

// Components
import { FramesCtxProvider } from "./store/contexts/frames.context";
import Frames from "./pages/Frame";

const App = () => {
  return (
    <FramesCtxProvider>
      <Frames />
    </FramesCtxProvider>
  );
};

export default App;

/* ==================== Inside pages/Frames.jsx ==================== */
import React from "react";

// Components
import Canvas from "../components/Canvas";

const Frames = () => {
  return (
    <div>
      <Canvas />
    </div>
  );
};

export default Frames;

Bây giờ, chúng tôi sẽ cập nhật reducer chức năng xử lý các bản cập nhật khác nhau và gọi chức năng điều phối với các loại hành động thích hợp. Refactor frames.reducer.js tệp với mã sau:

export default function canvasReducer(state, action) {
  switch (action.type) {
    case CANVAS_ACTIONS.UPLOAD_IMAGE:
      return {
        ...state,
        originalDimensions: {
          width: action.payload.width,
          height: action.payload.height,
        },
        image: action.payload.image,
      };

    case CANVAS_ACTIONS.UPDATE_IMAGE_DIMENSIONS:
      return {
        ...state,
        scale: action.payload.scale,
      };

    case CANVAS_ACTIONS.UPDATE_IMAGE_POSITIONS:
      return {
        ...state,
        position: {
          x: action.payload.x,
          y: action.payload.y,
        },
      };

    default:
      return state;
  }
}

Cập nhật các thành phần để sử dụng công văn

Trong của chúng tôi Image , chúng tôi cần cập nhật tỷ lệ hình ảnh và vị trí hình ảnh khi onTransformEndonDragEnd được kích hoạt, tương ứng:

// State Handlers
import { useFrames } from "../store/contexts/frames.context";
import { CANVAS_ACTIONS } from "../store/actions/frames.action";

/* Remove the rest of the destructured props */
const CustomImage = ({ isSelected, onSelect }) => {
        /* Rest of code */

        const [state, dispatch] = useFrames();

        /* Update the destructured element to use the state */
  const {
    renderDimensions: { width, height },
    position: { x, y },
    image,
  } = state.imageDetails;

        /* Replace the setImageDetails with the following dispatch code */
  const onTransformEnd = () => {
    if (shapeRef.current) {
      const node = shapeRef.current;
      dispatch({
        type: CANVAS_ACTIONS.UPDATE_IMAGE_DIMENSIONS,
        payload: {
          scale: node.scale(),
        },
      });
    }
  };

        /* Replace the setImageDetails with the following dispatch code */
  const onDragEnd = () => {
    if (shapeRef.current) {
      const node = shapeRef.current;
      dispatch({
        type: CANVAS_ACTIONS.UPDATE_IMAGE_POSITIONS,
        payload: {
          x: node.x(),
          y: node.y(),
        },
      });
    }
  };

        /* Rest of code */

}

Chúng tôi cũng cần cập nhật chi tiết hình ảnh khi hình ảnh được tải lên, nhưng chúng tôi sẽ thực hiện việc này sau.

Tạo ra một upload image thành phần

Để tải hình ảnh lên trình tạo khung hồ sơ của chúng tôi, chúng tôi sẽ tạo một thành phần khá đơn giản với đầu vào là loại image và một onChange xử lý với chức năng điều phối của loại upload image.

Tạo một tệp có tên UploadImage.jsx trong thư mục thành phần:

import React from "react";

// State Handlers
import { CANVAS_ACTIONS } from "../store/actions/frames.action";
import { useFrames } from "../store/contexts/frames.context";

const UploadImage = () => {
  /**
   * Following is a destructuring way to get only dispatch
   */
  const [, dispatch] = useFrames();

  const handleInputChange = (e) => {
    /**
     * The following code is to get the image data and
     * the dimensions of the uploaded image. In order to get this
     * use the FileReader class.
     */
    if (e.target.files.length > 0) {
      const file = e.target.files[0];
      const i = new Image();
      i.src = URL.createObjectURL(file);

      i.onload = () => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => {
          dispatch({
            type: CANVAS_ACTIONS.UPLOAD_IMAGE,
            payload: {
              image: i.src,
              originalDimensions: {
                width: i.width,
                height: i.height,
              },
            },
          });
        };
      };
    }
  };

  return (
    <div>
      <label htmlFor="contained-button-file">
        <button>Upload Image</button>
      </label>

      <input
        type="file"
        accept="image/*"
        style={{ display: "none" }}
        id='contained-button-file'
        maxFiles={1}
        onChange={handleInputChange}
      />
    </div>
  );
};

export default UploadImage;

Bây giờ, chức năng tải lên đã sẵn sàng, nhưng chúng tôi có một vấn đề nhỏ khác cần giải quyết. Hiện tại, chúng tôi đã mã hóa sai các kích thước của CustomImage thành phần. Điều gì sẽ xảy ra nếu hình ảnh được tải lên có tỷ lệ khung hình khác 1: 1?

Các renderedDimensions thuộc tính ở trạng thái ban đầu của hình ảnh và bạn đang nhận được kích thước ban đầu của hình ảnh tại thời điểm tải lên. Thay vào đó, chúng tôi sẽ tính toán tỷ lệ co của hình ảnh, sau đó tùy thuộc vào kích thước của stage, chúng tôi sẽ tính toán các kích thước hiển thị của hình ảnh.

Thêm mã sau vào handleImageInput hoạt động ngay sau công văn đầu tiên:

                                        const aspectRatio = i.width / i.height;
          const stageHeight = state.stageDetails.height;
          const stageWidth = state.stageDetails.width;
          dispatch({
            type: CANVAS_ACTIONS.UPDATE_IMAGE_RENDERED_DIMENSIONS,
            payload: {
              width: aspectRatio > 1 ? stageWidth : stageHeight * aspectRatio,
              height: aspectRatio > 1 ? stageWidth / aspectRatio : stageHeight,
            },
          });

Tải xuống khung

React Konva cung cấp một phương pháp để chuyển đổi canvas thành hình ảnh thông qua tham chiếu mà chúng tôi đã chuyển đến Stage thành phần:

const downloadURI = (uri, name) => {
  const link = document.createElement('a');
  link.download = name;
  link.href = uri;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};

const handleDownload = () => {
  if (stageRef.current) {
    const uri = stageRef.current.toDataURL();
    downloadURI(uri, 'certificate.png');
  }
};

Bạn cần chuẩn bị sẵn sàng các thành phần đầu vào và hiển thị giá trị trong text thành phần của sân khấu, CustomText thành phần. Phương pháp để đạt được điều này hoàn toàn giống với các bước chúng tôi đã làm cho hình ảnh. Đây là một bản tóm tắt:

Phương pháp tải xuống thành phần văn bản

Sự kết luận

Trong bài viết này, chúng ta đã học cách xây dựng trình tạo khung ảnh hồ sơ bằng React Konva. Chúng tôi đã tuân theo một mô hình quản lý nhà nước phức tạp mà không cần bất kỳ gói bên ngoài nào. Chúng tôi cũng đã học cách thiết lập môi trường canvas và thao tác các khía cạnh khác nhau của canvas trong ứng dụng một trang dựa trên React.

Nếu bạn muốn làm cho dự án của mình tương tác hơn, bạn có thể tích hợp các thành phần React Konva khác vào ứng dụng của mình. Hãy chắc chắn kiểm tra tài liệu chính thức để biết thêm chi tiết. Chúc bạn viết mã vui vẻ!

Hiển thị đầy đủ các ứng dụng React sản xuất

Gỡ lỗi các ứng dụng React có thể khó khăn, đặc biệt là khi người dùng gặp các sự cố khó tái tạo. Nếu bạn quan tâm đến việc giám sát và theo dõi trạng thái Redux, tự động hiển thị các lỗi JavaScript và theo dõi các yêu cầu mạng chậm và thời gian tải thành phần, thử LogRocket.

LogRocket giống như một DVR cho các ứng dụng web, ghi lại mọi thứ diễn ra trên ứng dụng React của bạn theo đúng nghĩa đen. Thay vì đoán lý do tại sao sự cố xảy ra, bạn có thể tổng hợp và báo cáo về trạng thái ứng dụng của bạn đang ở trạng thái nào khi sự cố xảy ra. LogRocket cũng theo dõi hiệu suất ứng dụng của bạn, báo cáo với các chỉ số như tải CPU của máy khách, mức sử dụng bộ nhớ máy khách, v.v.

Gói phần mềm trung gian LogRocket Redux bổ sung thêm một lớp khả năng hiển thị vào các phiên người dùng của bạn. LogRocket ghi lại tất cả các hành động và trạng thái từ các cửa hàng Redux của bạn.

Hiện đại hóa cách bạn gỡ lỗi các ứng dụng React của mình – .

Thanks for Reading

Enjoyed this post? Share it with your networks.

Get more stuff

Subscribe to our mailing list and get interesting stuff and updates to your email inbox.

Thank you for subscribing.

Something went wrong.

Leave a Feedback!