Làm ứng dụng Paintbrush - Javascript (phần 1)

14 tháng 12, 2021 - 4140 lượt xem

Chào các bạn, trong bài viết này chúng ta sẽ cùng nhau làm ứng dụng Paint brush. Nếu các bạn chưa biết Paint brush là gì thì đây là ứng dụng cho phép chúng ta vẽ các đường nét trên 1 vùng không gian nhất định tương tự như ứng dụng Paint trong window. Các bạn có thể mở ứng dụng Paint để tham khảo

Ứng dụng mà chúng ta làm lần này sẽ không phức tạp như Paint mà sẽ đơn giản hơn bao gồm các chức năng sau:

  • Vẽ các nét
  • Reset nét vẽ
  • Chọn màu cho nét vẽ
  • Chọn kích thước cho nét vẽ

Phần tạo giao diện cho ứng dụng, mình để sources code ở đây để các bạn tham khảo, hoặc các bạn có thể tự tạo giao diện cho ứng dụng của mình

Link tham khảo: https://github.com/buihien0109/buihien0109.github.io/tree/master/HTML5-Games/game/paint/part-1

Đây là giao diện ban đầu của chúng ta

demo giao diện

1. Ý tưởng thực hiện

1. Thành phần

Trong ứng dụng Paint có 2 thành phần:

  • Canvas : Khu vực thực hiện công việc vẽ
  • Memory canvas : Ghi nhớ nét vẽ từ canvas khi kết thúc 1 nét vẽ, và render lại tất cả các nét vẽ sang canvas để thực hiện nét vẽ tiếp theo

2. Quá trình thực hiện

  • Đầu tiên chúng ta sẽ có mảng points để lưu giá trị tọa độ của các điểm
  • Nhấn chuột xuống để bắt đầu vẽ, chúng ta tiến hành xóa toàn bộ canvas, sau đó copy các nét vẽ từ bên memory canvas ➡➡ canvas, sau đó tiến hành vẽ nét mới, trong quá trình di chuyển chuột để vẽ nét mới lấy tọa độ của các điểm sau đó thêm vào mảng points
  • Từ mảng point chúng ta sẽ vẽ nên các nét trên bề mặt canvas
  • Khi nhả chuột ra để kết thúc nét vẽ, lúc này xóa toàn bộ các nét vẽ trên memory canvas đồng thời đưa tất cả các nét trên bề mặt canvas ➡➡ canvas memory, sau đó cho points = [];
  • Chúng ta sẽ sẽ lặp đi lặp lại quá trình này

2. Xử lý khi vẽ

Đầu tiên chúng ta sẽ truy cập vào canvas thông qua id của nó

const canvas = document.getElementById("canvas");

Tiếp theo chúng ta định nghĩa class FreeHand, với constructor đầu vào là canvas element

class FreeHand {
    constructor(canvas) {
        this.canvas = canvas;

        // Lưu context để vẽ 2D
        this.context = canvas.getContext("2d");

        // Kiểm tra khi nào được draw, khi nào không
        this.isDraw = false;

        // Lưu lại tọa độ các point đã vẽ
        this.points = [];

        // Style nét vẽ. Mặc định nét vẽ có chiều rộng 2px, màu trắng
        this.context.lineWidth = 2;
        this.context.lineJoin = "round";
        this.context.lineCap = "round";
        this.context.strokeStyle = "#FFF";

        // Memmory canvas, dùng để ghi nhớ các nét vẽ
        this.memCanvas = document.createElement("canvas");
        this.memCanvas.width = canvas.width;
        this.memCanvas.height = canvas.height;
        this.memCtx = this.memCanvas.getContext("2d");
    }
}

Để vẽ 1 nét chúng ta có các thao tác sau:

  • Nhấn chuột xuống để bắt đầu vẽ
  • Vừa nhấn chuột và di chuyển chuột để thực hiện vẽ
  • Nhả chuột ra để kết thúc nét vẽ

Với mỗi thao tác trên chúng ta sẽ định nghĩa method trong class FreeHand để thực hiên

1. Nhấn chuột xuống để bắt đầu vẽ

Chúng ta thực hiện thao tác này bằng các định nghĩa method onmousedown

Chúng ta sẽ lưu lại tọa độ tại vị trí nhấn chuột xuống và push vào mảng this.points để lưu lại, đồng thời this.isDraw = true để đặt trạng thái bắt đầu vẽ

onmousedown(event) {
    // Lấy tạo độ x,y của chuột tạo vị trí bắt đầu vẽ
    this.x = event.offsetX;
    this.y = event.offsetY;

    // Lưu cặp tọa độ vào mảng points
    this.points.push({
        x: this.x,
        y: this.y,
    });

    // Đặt trạng thái bắt đầu vẽ
    this.isDraw = true;
}

2. Vừa nhấn chuột và di chuyển chuột để thực hiện vẽ

Trong lúc vẽ, chúng ta sẽ xóa hết this.canvas thông qua method this.context.clearRect. Đồng thời vẽ lại các nét từ this.memCtx sang this.canvas

Tọa độ của các điểm trên đường đi của bút vẽ sẽ được push vào mảng this.points để lưu lại

Cuối cùng gọi methods this.drawPoints để nối các điểm trong mảng this.points lại và hiển thị lên thành nét vẽ

Phần tiếp theo chúng ta sẽ định nghĩa method this.drawPoints để tạo nét vẽ từ mảng this.points

onmousemove(event) {
    if(this.isDraw) {
        // Xóa tất cả trên canvas
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);

        // Vẽ memory canvas lên trên canvas
        this.context.drawImage(this.memCanvas, 0, 0);

        // Lưu tọa độ khi di chuyển chuột vào mảng points
        this.points.push({
                x: event.offsetX,
                y: event.offsetY,
        });

        // Nối các tọa độ trong mảng points lại thành 1 đường
        this.drawPoints();
    }
}

3. Nhả chuột ra để kết thúc nét vẽ

Khi nhả chuột ra chúng ta set giá trị this.isDraw = false để kết thúc 1 chu trình vẽ

Đồng thời clear this.memCtx và vẽ lại các nét từ this.canvas sang this.memCtx

Clear this.points = [] để tiếp tục cho lần vẽ tiếp theo

onmouseup() {
    if (this.isDraw) {
        // Đặt lại trạng vẽ => kết thúc nét vẽ
        this.isDraw = false;

        // Xóa toàn bộ memory canvas
        this.memCtx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        // Vẽ canvas lên trên memory canvas
        this.memCtx.drawImage(this.canvas, 0, 0);

        // Clear mảng point cho nét vẽ khác
        this.points = [];
    }
}

3. Làm mượt nét vẽ

Để tạo nét vẽ chúng ta sẽ nối lần lượt tọa độ của các điểm trong mảng this.points lại với nhau. Nhưng nếu đơn thuần chỉ nối thì các nét vẽ này sẽ không mượt và bị gấp khúc

Để làm mượt nét vẽ chúng ta sẽ sử dụng method quadraticCurveTo()

Cụ thể phương thức này như thế nào, các bạn có thể tham khảo tại đây: https://www.w3schools.com/tags/canvas_quadraticcurveto.asp

drawPoints() {
    let ctx = this.context;

    // Nếu mảng point có 1 điểm -> bỏ qua không vẽ
    if(this.points.length <= 1) {
        return
    }

    // Nếu mảng point có 2 điểm -> nối 2 điểm đó với nhau
    if(this.points.length == 2) {
        ctx.beginPath();
        ctx.moveTo(this.points[0].x, this.points[0].y);
        ctx.lineTo(this.points[1].x, this.points[1].y);
        ctx.stroke();
        return
    }

    // Trường hợp mảng points có nhiều điểm -> sử dụng quadraticCurveTo để nối các điểm lại
    ctx.beginPath();
    ctx.moveTo(this.points[0].x, this.points[0].y);
    for (var i = 1; i < this.points.length - 2; i++) {
        var c = (this.points[i].x + this.points[i + 1].x) / 2;
        var d = (this.points[i].y + this.points[i + 1].y) / 2;
        ctx.quadraticCurveTo(this.points[i].x, this.points[i].y, c, d);
    }

    ctx.quadraticCurveTo(
        this.points[i].x,
        this.points[i].y,
        this.points[i + 1].x,
        this.points[i + 1].y
    );
    ctx.stroke();
}

4. Khởi tạo đối tượng và lắng nghe sự kiện

Sau khi định nghĩa class FreeHand với 1 số phương thức cơ bản để có thể vẽ các nét. Bây giờ chúng ta sẽ thực hiện khởi tạo đối tượng từ class FreeHand và lắng nghe các sự kiện của đối tượng canvas và gọi ra method tương ứng

  • onmousedown event ➡➡ gọi method onmousedown
  • onmousemove event ➡➡ gọi method onmousemove
  • onmouseup event ➡➡ gọi method onmouseup
let freehand = new FreeHand(canvas);

// Lắng nghe sự kiện onmousedown (ấn chuột xuống) của đối tượng canvas để bắt đầu vẽ
canvas.onmousedown = (event) => {
    freehand.onmousedown(event);
};

// Lắng nghe sự kiện onmousemove (di chuyển chuột) của đối tượng canvas để vẽ nét
canvas.onmousemove = (event) => {
    freehand.onmousemove(event);
};

// Lắng nghe sự kiện onmouseup (nhả chuột ra) của đối tượng canvas để kết thúc nét vẽ
canvas.onmouseup = () => {
    freehand.onmouseup();
};

và đây là kết quả của chúng ta

Kết quả

Ở bài viết tiếp theo chúng ta sẽ thực hiện các chức năng còn lại của ứng dụng Paintbrush

  • Chọn nét vẽ
  • Chọn màu vẽ
  • Xóa toàn bộ nét vẽ

Sources phần này các bạn có thể tham khảo tại đây: https://github.com/buihien0109/buihien0109.github.io/tree/master/HTML5-Games/game/paint/part-2

Các bạn có thể tham khảo thêm khóa học này nhé:

  • Javascript căn bản - Tổng hợp 12 game huyền thoại - tại đây.
  • Lộ trình Frontend & React.js (5 tháng) - tại đây.

Bình luận

avatar
Nguyễn Trần Nhật Đức 2022-03-21 03:36:46.155586 +0000 UTC

test

Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  +1 Thích
+1