Lulucat

Hai công cụ tẩy, và vì sao phần xóa thuộc về nét vẽ

Gaoge ZhangGaoge Zhang

Việc tách nét tẩy thành một phần tử riêng là một thiết kế hiển nhiên. Di chuyển đối tượng phá vỡ thiết kế đó, nên trong Lulucat Notes, phần xóa thuộc về nét mà nó xóa mực.

Đã cập nhật

Khi xóa trong sổ tay, người ta có thể đang nói đến hai việc khác nhau. Đôi khi một dấu bị sai và cần bỏ cả dấu: đường hướng dẫn, một từ bị gạch bỏ, hoặc một nét thừa do lòng bàn tay tì xuống. Đôi khi chỉ một phần của dấu bị sai: nửa ký tự, một nhánh của sơ đồ, hoặc phần đuôi của một nét quét bút đánh dấu vượt quá câu. Lulucat Notes hiện cung cấp cả hai dưới dạng hai công cụ riêng.

Vấn đề thiết kế khó hơn là phần xóa nằm ở đâu.

Công cụ tẩy nét xuất hiện trước

Công cụ tẩy đầu tiên xóa toàn bộ nét. Mỗi lần chạm sẽ kiểm tra mọi nét: các hộp bao loại gần như tất cả chúng, còn những nét còn lại được kiểm tra bằng point-to-segment squared distance, so với tổng bán kính của tẩy và bán kính nét được nội suy dọc theo đoạn đó. Bề rộng thay đổi theo áp lực, vì vậy dùng một bề rộng danh nghĩa duy nhất sẽ xóa những nét mảnh mà người dùng không chạm vào và bỏ sót những nét dày đã bị chạm.

Một cử chỉ được gộp thành một mục hoàn tác, và mục đó lưu mỗi nét bị xóa cùng với chỉ mục ban đầu của nó. Hoàn tác chèn chúng trở lại theo thứ tự ngược, nên thứ tự xếp chồng được khôi phục chính xác như cũ.

Công cụ tẩy này nhanh, có hành vi dễ đoán và không thể xóa nửa từ.

MaLiang biến tẩy thành cọ

MaLiang, framework vẽ được cấp phép MIT mà trước đó chúng tôi đã mượn mô hình stamp của bút đánh dấu, giải quyết việc xóa một phần chỉ trong 24 dòng. Eraser của nó là một lớp con của Brush, chỉ ghi đè một thứ là các tùy chọn hòa trộn để trừ thay vì cộng. Một nét tẩy là một phần tử trong cùng danh sách với mọi nét khác. Hoàn tác loại bỏ phần tử đó. Không nét hiện có nào bị sửa đổi, vì việc xóa chỉ diễn ra khi phát lại theo thứ tự trong danh sách.

Chúng tôi đã phát hành mô hình đó, và hai phiên làm việc trên thiết bị đã làm nó hỏng.

Di chuyển đối tượng làm lộ ra phần xóa không có chủ sở hữu

Báo cáo đầu tiên: chọn các nét bằng vùng chọn tự do, kéo chúng đi, nhưng khoảng trống vẫn nằm lại.

Bốn khung so sánh hai mô hình tẩy trước và sau khi di chuyển các nét. Khi tẩy là phần tử riêng, khoảng trống nằm ở vị trí cũ; khi phần xóa thuộc về nét, khoảng trống di chuyển cùng mực vẽ.

Cùng những nét đã bị xóa, được di chuyển sang phải. Phía trên: tẩy là phần tử riêng. Phía dưới: phần xóa thuộc về các nét.

Việc chọn còn khiến mọi thứ tệ hơn. Các nét được chọn được đưa ra khỏi bitmap vào một lớp phủ để vẽ trực tiếp, không có phần tử tẩy; vì vậy vừa chọn một nét đã bị xóa, nét đó lập tức hiện nguyên vẹn trở lại.

Báo cáo thứ hai: tẩy nổi phía trên bút đánh dấu. Bộ kết xuất của chúng tôi phát lại theo hai lượt: bút đánh dấu trước, bút sau, vì mực bút đánh dấu phải nằm bên dưới mực. Một phần tử tẩy phải tham gia cả hai lượt, nếu không nó không thể xóa cả hai loại nét. Tham gia lượt bút có nghĩa là chạy sau khi mọi bút đánh dấu đã được vẽ, nên tẩy cũng cắt vào các nét bút đánh dấu được vẽ muộn hơn về thời gian.

Hai lỗi đều có cùng một nguyên nhân. MaLiang không có vùng chọn tự do, không có thao tác di chuyển và không có thứ tự lớp giữa các cọ, nên một phần tử không có chủ thể sở hữu vẫn đủ dùng trong đó. Canvas của chúng tôi có cả ba, còn một phần xóa không có chủ thể sở hữu thì không có câu trả lời xác định cho việc mực di chuyển sẽ ra sao hoặc thao tác này đang tác động lên lớp nào.

Phần xóa thuộc về nét

Giờ đây mỗi Stroke đều mang các đường dẫn xóa riêng. Tẩy kiểm tra các nét mà nó chạm vào rồi thêm một đường dẫn vào từng nét, theo tọa độ canvas. Khi kết xuất một nét, mực của nét đó và phần xóa riêng của nó được vẽ trong một lớp trong suốt:

context.beginTransparencyLayer(auxiliaryInfo: nil)
drawInk(stroke, in: context, clip: clip, colorOverride: colorOverride)
context.setBlendMode(.destinationOut)
for path in stroke.erasePaths { fillErasePath(path, in: context) }
context.endTransparencyLayer()

Destination-out giữ đích và loại bỏ một phần đích theo tỷ lệ alpha của nguồn:

Trong một lớp trong suốt, đích chính là nét đó, nên thao tác không thể chạm tới giấy, các nét bên dưới hoặc các nét được vẽ sau. Những nét không có phần xóa không chịu chi phí của lớp, mà đó là gần như tất cả các nét: chúng đi theo đường dẫn ban đầu.

Việc gắn quyền sở hữu giải quyết trực tiếp hai lỗi đó. Phép tịnh tiến di chuyển các đường dẫn xóa cùng với các điểm mẫu, nên một nét được di chuyển sẽ mang theo các khoảng trống của nó. Lớp phủ lựa chọn vẽ cùng một đối tượng nét, nên nét được chọn hiển thị các khoảng trống giống hệt. Thứ tự lớp không còn quan trọng, vì phần xóa không bao giờ tác động lên thứ gì ngoài nét của chính nó.

Hoàn tác hoạt động theo cử chỉ: một mục liệt kê mọi cặp (stroke, path) mà cử chỉ đó tạo ra, hoàn tác xóa các đường dẫn đó, còn lệnh làm lại thêm chúng vào lần nữa. Mực bên dưới phần xóa chưa bao giờ bị chỉnh sửa, nên không cần dựng lại gì.

Phản hồi trực tiếp đi qua bitmap tile

Nét đang được vẽ thường được kết xuất thành một lớp phủ phía trên trang đã hợp thành. Phần xóa không thể hoạt động ở đó, vì lớp phủ nằm trên cả giấy lẫn mực, và destination-out sẽ đục thủng cả giấy.

Vì vậy công cụ tẩy ghi ngay vào các nét, và mỗi lô mẫu mới sẽ kích hoạt việc phát lại cục bộ chỉ trong hình chữ nhật bị ảnh hưởng. Pipeline tile vốn đã xóa một hình chữ nhật rồi phát lại các nét giao với nó; tẩy tái sử dụng đường đi đó và không bổ sung một cơ chế vẽ thứ hai.

Cả hai công cụ tẩy, đặt cạnh nhau

Thanh công cụ có hai nút tẩy thay vì một nút có chế độ ẩn. Tẩy một phần loại bỏ một phần của nét. Tẩy nét xóa toàn bộ nét khi chạm vào. Bán kính của chúng là các hằng số riêng, hiện đều là 4 point.

Xóa toàn bộ một nét cũng xóa các đường dẫn xóa của nó, vì chúng là một phần của nét đó, và hoàn tác khôi phục cả hai cùng nhau. Các đường dẫn xóa cũng là một phần của tệp đã lưu, được lượng tử hóa như mọi thứ khác: bán kính và tọa độ được lưu theo phần mười point.

Vẫn còn một hạn chế. Một nét bị xóa đến mức không còn gì vẫn tồn tại dưới dạng dữ liệu. Nó được kết xuất như không có gì, nhưng vùng chọn tự do vẫn có thể chọn nó và tẩy nét vẫn có thể xóa nó, đồng thời nó vẫn chiếm chỗ trong tài liệu. Việc thu gom các nét bị xóa hoàn toàn cần một phép kiểm tra độ phủ mà chúng tôi chưa viết, nên hiện tại chúng vẫn được giữ lại.

Khi một mô hình từ dự án khác ngừng hoạt động trong dự án của bạn, vấn đề thường không nằm ở đoạn mã bạn sao chép. Nó nằm ở các tính năng mà dự án kia không có.