Xin chào các bạn,

Trong bài này mình sẽ hướng dẫn các bạn cách ghép hình bằng Canvas trong HTML5, cụ thể ở đây mình ghép khung vào hình mà mình chọn từ máy lên. Các bạn có thể xem demo ở đây.

Ý tưởng ghép khung vào hình

Ý tưởng cơ bản là mình vẽ lần lượt hai hình lên canvas, sau đó di chuyển, phóng to thu nhỏ hay xoay hình rồi cho tải về là xong. Rất đơn giản phải không?

Vẽ khung hình

Trong file html, các bạn thêm các thẻ <canvas>, <input><img> như sau:

<canvas id="canvas">Xin lỗi, trình duyệt bạn không hỗ trợ.</canvas>
<img style="display: none;" id="source">

Trong đó,

  • Thẻ canvas để mình sử dụng vẽ hình.
  • Thẻ img dùng để chứa hình mà mình tải lên.

Tiếp theo, các bạn tạo file js để code một số xử lý hình ảnh.

// lấy hình, canvas, context của canvas bên html
var userImage = document.getElementById("source");
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
// gán kích thước mặc định cho canvas
var canvasWidth = canvas.width = 500;
var canvasHeight = canvas.height = 500;
// cạnh nhỏ nhất
var minEdge = 0;
var imageScale = 1;
var minScale = 1;
// tọa độ hình
var imagePosX = 0;
var imagePosY = 0;
// góc xoay
var angleInDegrees = 0;
// tạo đối tượng khung hình để ghép
var frame = new Image;
frame.src = "images/avatar_frame.png";
frame.onload = function() {
drawCurrentImage();
}

Mình lấy các đối tượng trong HTML như image, canvascontext và tạo các biến cơ bản. Sau đó tạo đối tượng frame là khung hình muốn ghép.

Ở hàm onload của frame, mình vẽ khung hình lên context.

function drawCurrentImage()
{
// xóa canvas
context.clearRect(0, 0, canvasWidth, canvasHeight);
// vẽ frame
context.drawImage(frame, 0, 0, frame.width, frame.height, 0, 0, canvasWidth, canvasHeight);
};

Ban đầu đơn giản chỉ xóa canvas rồi vẽ frame hình lên trước, các bạn có thể xem thêm hàm drawImage(), clearRect()

Load hình

Tiếp theo mình sẽ load hình lên.

Trong file HTML các bạn thêm thẻ input để up hình.

<form>
<input style="display:none;" type="file" id="imageFile" accept="image/*">
</form>

Viết sự kiện change cho input.

document.getElementById('imageFile').addEventListener('change', function() {
updateImage();
}, false);
document.getElementById('selectImageBtn').addEventListener('click', function() {
$("#imageFile").click();
}, false);
function updateImage() {
// lấy dữ liệu từ input
var input = $("#imageFile")[0];
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
// cập nhật src cho hình từ input
userImage.src = e.target.result;
}
reader.readAsDataURL(input.files[0]);
}
};

Khi hình được load lên, mình sử dụng FileReader để đọc dữ liệu từ input và cập nhật lại source hình từ dữ liệu đó cho đối tượng userImage.

userImage.onload = function() {
// tìm cạnh nhỏ nhất
minEdge = Math.min(userImage.width, userImage.height)
// tính giá trị scale
minScale = canvasWidth / minEdge;
imageScale = minScale;
// tính lại kích thước thật của hình khi scale
imageWidth = userImage.width * imageScale;
imageHeight = userImage.height * imageScale;
// vẽ lại hình
drawCurrentImage();
}

Ở hàm onload của userImage, mình sẽ tìm cạnh nhỏ nhất để scale hình cho vừa với khung. Và lưu giá trị vào minScale để giới hạn giá trị scale. (mình thích không cho người ta scale nhỏ hơn canvas thôi).

Giờ mình có hình rồi nên ta cập nhật thêm cho hàm drawCurrentImage()

function drawCurrentImage() {
// xóa canvas
context.clearRect(0, 0, canvasWidth, canvasHeight);
// save context trước khi thay đổi
context.save();
// di chuyển context đến tọa độ x, y
context.translate(imagePosX, imagePosY);
// xoay hình
context.rotate(angleInDegrees * Math.PI / 180);
// scale hình
context.scale(imageScale, imageScale);
// vẽ hình
context.drawImage(userImage, -userImage.width / 2, -userImage.height / 2);
// trả lại mặc định
context.restore();
// vẽ khung hình
context.drawImage(frame, 0, 0, frame.width, frame.height, 0, 0, canvasWidth, canvasHeight);
};

Các bạn save() context hiện tại trước khi thay đổi để vẽ userImage, rồi nhớ restore() để trả về mặc định cho context trước khi vẽ cái khác.

Để vẽ theo tâm của hình (origin là tâm của hình), mình dùng cách translate() chuyển hình đến tọa độ mong muốn. Sau đó khi vẽ bằng drawImage(), ta vẽ lùi về một nữa kích thước ngang, dọc của hình.

Và mình dùng rotate(), scale() để gán các giá trị tương ứng cho context.

Di chuyển

Ý tưởng: mỗi khi chuột di chuyển mình sẽ tính toán độ thay đổi của chuột so với lần trước rồi cập nhật lại vị trí của hình.

Các bạn thêm các biến như bên dưới và các sự kiện mousedown, mousemove, mouseup, mouseout cho canvas.

// vị trí của canvas
var canvasOffset = $("#canvas").offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
// biến cờ để xác định trạng thái kéo
var isDragging = false;
$("#canvas").mousedown(function(e) {
// vị trí hiện tại chuột theo canvas = vị trí chuột hiện tại - offset của canvas
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// bắt đầu kéo hình
isDragging = true;
// lưu lại vị trí cũ
preX = canMouseX;
preY = canMouseY;
});
$("#canvas").mouseup(function(e) {
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// hết kéo hình
isDragging = false;
preX = canMouseX;
preY = canMouseY;
});
$("#canvas").mouseout(function(e) {
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// chuột ra khỏi canvas rồi nên hết kéo được
isDragging = false;
preX = canMouseX;
preY = canMouseY;
});
$("#canvas").mousemove(function(e) {
canMouseX = parseInt(e.clientX - offsetX);
canMouseY = parseInt(e.clientY - offsetY);
// nếu đang kéo hình thì cập nhật vị trí và vẽ lại
if(isDragging) {
// delta
imagePosX += (canMouseX - preX);
imagePosY += (canMouseY - preY);
drawCurrentImage();
}
preX = canMouseX;
preY = canMouseY;
});

Ở sự kiện cho mousemove, ta cập nhật vị trí hiện tại của hình với độ thay đổi của chuột. Các sự kiện mouseup, mousedownmouseout để xác định trạng thái kéo thả hình.

Mình thêm giới hạn cho tọa đổ để không kéo hình lệch xa quá, chỉ cho nó vừa khung và di chuyển hết hình thôi.

// tính lại phần cho phép di chuyển của hình so với canvas
var deltaChangeX = (imageWidth - canvasWidth) / 2;
var deltaChangeY = (imageHeight - canvasHeight) / 2;
// xét chiều X
if(imagePosX > canvasWidth / 2 + deltaChangeX)
{
imagePosX = canvasWidth / 2 + deltaChangeX;
}
else if (imagePosX < canvasWidth / 2 - deltaChangeX)
{
imagePosX = canvasWidth / 2 - deltaChangeX;
}
// Xét chiều Y
if(imagePosY > canvasHeight / 2 + deltaChangeY)
{
imagePosY = canvasHeight / 2 + deltaChangeY;
}
else if (imagePosY < canvasHeight / 2 - deltaChangeY)
{
imagePosY = canvasHeight / 2 - deltaChangeY;
}

Các bạn thêm vào hàm drawCurrentImage(), mình tính phần dư của hình so với canvas, phần này sẽ là giới hạn di chuyển của tọa độ. Nếu tọa độ thấp hơn hoặc lớn hơn thì mình không cho phép vượt qua.

Thay đổi giá trị scale

Với giá trị scale, ta thêm thanh input range để kéo thay đổi giá trị vào HTML như sau

<input id="scaleBar" type="range" min="1" max="2" step="0.01"/>
<p>Kích thước: <span id="scaleValue">1</span></p>

Mỗi lần thay đổi giá trị, mình cập nhật lại imageScale với giá trị của scaleBar.

scaleBar.oninput = function() {
// tính giá trị scale, imageScale là tỉ lệ tối thiểu mình scale để vừa với khung ảnh
// sau đó nhân với giá trị của thanh scale
imageScale = minScale * scaleBar.value;
// vẽ hình
drawCurrentImage();
// gán giá trị hiển thị trên web
var scaleValue = document.getElementById("scaleValue");
scaleValue.innerHTML = scaleBar.value;
}

Thay đổi góc xoay

Để thay đổi góc xoay, các bạn thêm vào file HTML hai nút xoay trái/phải.

<span>Xoay hình
<a id="clockwise" role="button" class="btn btn-default"></a>
<a id="counterclockwise" role="button" class="btn btn-default"></a>
</span>
var angleInDegrees = 0;
document.getElementById('clockwise').addEventListener('click', function() {
angleInDegrees += 90;
swapWidthHeightImage();
drawCurrentImage();
}, false);
document.getElementById('counterclockwise').addEventListener('click', function() {
angleInDegrees -= 90;
swapWidthHeightImage();
drawCurrentImage();
}, false);
function swapWidthHeightImage()
{
var temp = imageWidth;
imageWidth = imageHeight;
imageHeight = temp;
}

Mỗi lần bấm nút xoay hình, mình cộng / trừ góc xoay cho 90 độ, và đổi width, height của hình cho nhau. Sau đó là vẽ lại hình.

Tải hình

Để tải hình, mình gán sự kiện click cho nút download, dùng hàm toDataURL() để lấy dữ liệu của canvas và tải xuống.

function downloadFile(button, canvasId, filename) {
var dt = document.getElementById(canvasId).toDataURL();
button.href = dt;
button.download = filename;
};
document.getElementById('downloadBtn').addEventListener('click', function() {
downloadFile(this, "canvas", "up_avatar.png");
}, false);

Tổng kết

Vậy là xong các chức năng cơ bản của việc ghép khung vào hình, các bạn có thể phát triển thêm như là xoay hình, phóng to thu nhỏ, chọn frame… Còn lại là các thứ như là css lại giao diện theo ý các bạn thôi.

Full Source code của project.

Bài viết này mình cũng đang tìm hiểu về Javascript, nên nếu có gì sai sót các bạn có thể góp ý thêm nha. Bạn nào có sử dụng lại nhớ báo mình cho mình biết nhé.

Hẹn gặp lại các bạn.

Cập nhật

  • 3/10/2016: cập nhật scale hình ảnh
  • 11/2/2017: cập nhật xoay hình ảnh