Zoom-in zoom-out một điểm cố định trong canvas

Zoom-in zoom-out tại một điểm cố định là một use case rất hay gặp trong các tools design hoặc page builder như Figma. Ở bài blog này, hãy cùng nhau implement tính năng này một cách basic chỉ với JS, HTML và CSS.

Demo

Demonstration

Code step-by-step

1. Create a container and a scalable item


Ở ví dụ này, ta sẽ tạo 2 thẻ div: div.parent – dùng làm container, div.scalable-child – là vật thể cần zoom.

Lưu ý một số thuộc tính css quan trọng:

  • Top, left: 0  là default position của child đối với parent
  • Pointer-event: none, vì ta sẽ add event vào parent, nên sẽ cần disable pointer-event trên child để tính toán dễ dàng hơn trong thuật toán này.
  • Transform-origin: left top, với thuộc tính này ta sẽ coi left và top là 2 cạnh của toạ độ

2. Add wheel event listener

Ta sẽ dùng WheelEvent để xử lý bài toán, event này sẽ được thẻ div.parent lắng nghe và xử lý.

Lưu ý: ở ví dụ này, ta chỉ xử lý bài toán zoom-in zoom-out trên trackpad. Việc zoom-in bằng chuột (mouse) sẽ cần xử lý thêm event cho phần đó, nhưng thuật toán sẽ tương tự.

Trong hàm xử lý handle wheel event, trước tiên ta có 1 biến isZooming để check 2 trường hợp: zooming hoặc moving child div.

Sau đó ta cần tính toán các giá trị mới của left, top, scale trong từng lần zooming/moving. Và lưu lại giá trị hiện tại vào 3 biến global.

3. Calculate on Zooming

  • Khi zooming, wheelEvent sẽ trả về 1 giá trị là deltaY và ta dùng nó để tính giá trị scale mới: newScale
    • deltaY > 0 => zoom-out
    • deltaY < 0 => zoom-in
  • Giá trị deltaY*oldScale*0.01 để điều chỉnh tốc độ scale tương ứng với giá trị scale trước đó.

 

Để hiểu rõ hơn cách tính 2 giá trị mới newLeft, newTop, hãy cùng nhìn vào hình vẽ ở dưới:

Giả sử:

  • Bắt đầu zoom child tại vị trí điểm A (chuột tại điểm A). Khi đó ta sẽ có các giá trị sau:
  • e.offsetX: khoảng cách từ chuột đến cạnh trái của parent
  • e.offsetY: khoảng cách từ chuột đến cạnh trên của parent
  • left: khoảng cách từ cạnh trái của child đến cạnh trái của parent, chính = giá trị của left style property
  • top: khoảng cách từ cạnh trên của child đến cạnh trên của parent, chính = giá trị của top style property

Child được zoom từ tỉ lệ scale lên scale’, và điểm A sẽ dịch đến điểm A’ (vì transition-origin = left top)

Vì vậy yêu cầu đặt ra là cần zoom child sao cho điểm A không di chuyển. Để làm việc này ta cần tính được 2 giá trị deltaX và deltaY mà điểm A đã di chuyển và trừ ngược lại vào 2 giá trị top, left của child.

Từ hình vẽ trên ta công thức dưới đây:

detalX = x’ – x

= x * (scale’ / scale) – x

= x * (scale’ / scale – 1)

= (e.offsetX – left) * (scale’ / scale – 1)

 

detalY = y’ – y

= y * (scale’ / scale) – y

= y * (scale’ / scale – 1)

= (e.offsetY – top) * (scale’ / scale – 1)

 

newLeft = left – detalX

newTop = top – detalY

4. Calculate on Moving

Ở moving event, tính toán newLeft và newTop sẽ đơn giản hơn, chỉ dựa theo 2 giá trị trả về là e.deltaX và e.deltaY.

Đó là tất cả code ta cần làm để giải quyết bài toán này. Tôi mong nó sẽ giúp ích cho bạn.

Bạn có thể tham khảo full source code ở đây.

Comments

Let’s make a great impact together

Be a part of BraveBits to unlock your full potential and be proud of the impact you make.