Ghép khung vào hình với Canvas trong HTML5

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>
và <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
, canvas
và context
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
, mousedown
và mouseout
để 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.
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