Lulucat

Два ластики — і чому стирання належить штриху

Gaoge ZhangGaoge Zhang

Окремий штрих ластика — очевидне рішення. Переміщення об’єктів його ламає, тому в Lulucat Notes стирання належить штриху, з якого воно прибирає чорнило.

Оновлено

Коли люди стирають у зошиті, вони можуть мати на увазі дві різні речі. Іноді позначка неправильна, і треба прибрати її всю: напрямну лінію, закреслене слово або випадковий штрих від долоні, що лежала на сторінці. Іноді неправильна лише частина позначки: половина символу, одна гілка діаграми або хвіст мазка маркера, що вийшов за межі речення. Тепер Lulucat Notes підтримує обидва варіанти як два окремі інструменти.

Складніше питання дизайну — де зберігати стирання.

Ластик для штрихів з’явився першим

Перший ластик видаляв цілі штрихи. Дотик перевіряє кожен штрих: обмежувальні прямокутники відсіюють майже всі, а для решти виконується перевірка квадрата відстані від точки до відрізка з порогом, що дорівнює сумі радіуса ластика та радіуса штриха, інтерпольованого вздовж цього відрізка. Ширина змінюється залежно від тиску, тож одна номінальна ширина видаляла б тонкі штрихи, яких користувач не торкнувся, і пропускала б товсті, яких торкнувся.

Один жест об’єднується в один запис скасування, і в цьому записі кожен видалений штрих зберігається разом із його початковим індексом. Скасування вставляє їх назад у зворотному порядку, тому порядок накладання відновлюється точно таким, яким був.

Цей ластик швидкий, передбачуваний і не здатний видалити половину слова.

MaLiang перетворює ластик на пензель

MaLiang — фреймворк для малювання під ліцензією MIT, з якого ми вже запозичили модель відбитків маркера. Він відповідає на проблему часткового стирання у 24 рядках. Його Eraser — підклас Brush, який перевизначає лише параметри змішування, щоб віднімати, а не додавати. Штрих ластика — елемент у тому самому списку, що й усі інші штрихи. Скасування видаляє цей елемент. Жоден наявний штрих не змінюється, бо стирання відбувається лише під час повторного відтворення, у порядку списку.

Ми випустили цю модель, але під час двох сеансів на пристроях вона зламалася.

Переміщення об’єктів виявляє відсутнього власника

Перший звіт: виділіть штрихи за допомогою ласо, перетягніть їх — і отвір залишиться позаду.

Чотири панелі порівнюють дві моделі ластика до та після переміщення штрихів. Коли ластик є окремим елементом, проміжок залишається на старому місці; коли стирання належить штриху, проміжок переміщується разом із чорнилом.

Ті самі стерті штрихи, переміщені праворуч. Вгорі: ластик — окремий елемент. Унизу: стирання належить штрихам.

Виділення погіршило ситуацію. Виділені штрихи піднімаються з растрового зображення в оверлей, який малює їх безпосередньо, без елемента ластика, тож щойно ви виділяєте стертий штрих, він знову здається цілим.

Другий звіт: ластик зависав над маркером. Наш рендерер відтворює зображення у два проходи: спочатку маркери, потім пера, бо чорнило маркера має лежати під чорнилом пера. Елемент ластика мусить брати участь в обох проходах, інакше не зможе стирати обидва види. Участь у проході пера означає виконання після того, як намальовано всі маркери, тож ластик також вирізав штрихи маркера, намальовані пізніше в часі.

Обидві помилки мають одне джерело. У MaLiang немає ласо, переміщення й порядку шарів між пензлями, тому елемента без власника там достатньо. Наше полотно має всі три, а стирання без власника не має визначеної відповіді на питання, що станеться, коли чорнило переміститься, і з яким шаром працює цей елемент.

Стирання, що належить штриху

Тепер кожен Stroke має власні шляхи стирання. Ластик перевіряє, яких штрихів він торкається, і додає до кожного з них шлях у координатах полотна. Відтворення одного штриха малює його чорнило й власні стирання всередині шару прозорості:

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 зберігає destination і видаляє його пропорційно до альфи source:

Усередині шару прозорості destination — лише цей штрих, тому операція не може дістатися паперу, штрихів під ним або штрихів, намальованих пізніше. Шар нічого не додає для штрихів без стирань, а це майже всі: для них використовується початковий шлях.

Належність безпосередньо вирішує обидві проблеми. Трансляція переміщує шляхи стирання разом із точками вибірки, тож переміщений штрих несе свої проміжки. Оверлей виділення малює той самий об’єкт штриха, тому виділений штрих показує ті самі проміжки. Порядок шарів перестає мати значення, бо стирання ніколи не застосовується ні до чого, крім власного штриха.

Скасування працює на рівні жесту: один запис перелічує кожну пару (stroke, path), створену цим жестом; скасування видаляє ці шляхи, повторення додає їх знову. Чорнило під стиранням ніколи не редагується, тож нічого не потрібно відновлювати.

Поточний відгук проходить через растрове зображення тайлів

Штрих, який малюється, зазвичай відтворюється як оверлей над скомпонованою сторінкою. Стирання там не працює, бо цей оверлей лежить поверх і паперу, і чорнила, а destination-out пробив би папір наскрізь.

Тож ластик одразу записує дані в штрихи, а кожна порція нових зразків запускає локальне повторне відтворення лише зачепленого прямокутника. Конвеєр тайлів уже очищає прямокутник і повторно відтворює штрихи, що його перетинають; ластик повторно використовує цей шлях і не додає іншої семантики малювання.

Обидва ластики поруч

На панелі інструментів дві кнопки ластика, а не одна кнопка з прихованим режимом. Частковий ластик прибирає частину штриха. Ластик штрихів видаляє цілі штрихи при контакті. Їхні радіуси — окремі константи, обидва наразі дорівнюють 4 пунктам.

Видалення цілого штриха забирає із собою і його шляхи стирання, оскільки вони є частиною цього штриха, а скасування повертає обидва разом. Шляхи стирання також є частиною збереженого файлу й квантуються так само, як і все інше: радіус і координати з точністю до десятої частини пункта.

Одне обмеження залишається. Штрих, стертий до нуля, усе ще існує як дані. Він нічого не відтворює, але ласо все ще може його виділити, а ластик штрихів — видалити; він також і далі займає місце в документі. Для збирання повністю стертих штрихів потрібна перевірка покриття, якої ми ще не написали, тому поки що вони залишаються.

Коли модель з іншого проєкту перестає працювати у вашому, різниця зазвичай не в коді, який ви скопіювали. Вона в можливостях, яких немає в тому проєкті.