Lulucat

দুটি ইরেজার, আর কেন মুছে দেওয়া স্ট্রোকেরই মালিকানায়

Gaoge ZhangGaoge Zhang

ইরেজার স্ট্রোককে আলাদা উপাদান হিসেবে রাখা স্বাভাবিক নকশা। কিন্তু অবজেক্ট সরানো সেটিকে ভেঙে দেয়, তাই Lulucat Notes-এ যে স্ট্রোক থেকে কালি মুছে যায়, মোছার কাজটি সেই স্ট্রোকেরই মালিকানায় থাকে।

হালনাগাদ

নোটবুকে কিছু মুছতে গেলে মানুষ আসলে দুটি আলাদা বিষয় বোঝায়। কখনও একটি mark ভুল হয় এবং পুরো mark-টাই চলে যাওয়া উচিত: একটি guide line, কেটে দেওয়া একটি শব্দ, অথবা বিশ্রামরত হাতের তালু থেকে পড়ে যাওয়া একটি stray stroke। কখনও কেবল mark-এর একটি অংশ ভুল: অর্ধেক character, diagram-এর একটি branch, অথবা sentence পেরিয়ে চলে যাওয়া highlighter sweep-এর tail। Lulucat Notes-এ এখন দুটিই আছে, দুটি আলাদা tool হিসেবে।

কঠিনতর design প্রশ্নটি হল, erasure-টি কোথায় থাকে।

প্রথমে এসেছিল stroke eraser

প্রথম eraser পুরো stroke মুছে দিত। একটি touch প্রতিটি stroke পরীক্ষা করে: bounding box প্রায় সবগুলিকে বাতিল করে, আর বেঁচে যাওয়াগুলির ক্ষেত্রে eraser radius এবং ওই segment বরাবর interpolated stroke radius যোগ করে point-to-segment squared distance test করা হয়। Pressure-এর সঙ্গে width বদলায়, তাই একটি মাত্র nominal width ব্যবহার করলে ব্যবহারকারী না-ছোঁয়া পাতলা stroke মুছে যেত, আর ছোঁয়া মোটা stroke বাদ পড়ত।

একটি gesture একটিমাত্র undo entry-তে মিশে যায়, এবং entry-তে মুছে যাওয়া প্রতিটি stroke তার original index-সহ রাখা হয়। Undo সেগুলিকে উল্টো ক্রমে আবার ঢোকায়, ফলে stacking order হুবহু আগের অবস্থায় ফিরে আসে।

এই eraser দ্রুত, অনুমানযোগ্য, কিন্তু একটি শব্দের অর্ধেক মুছতে পারে না।

MaLiang eraser-কে brush বানায়

MaLiang, MIT-licensed drawing framework যেখান থেকে আমরা আগেই highlighter stamp model ধার নিয়েছি, partial erasing-এর উত্তর দেয় 24টি লাইনে। এর Eraser হল Brush-এর একটি subclass, যা একটি জিনিস—blend options—override করে, যোগ করার বদলে বিয়োগ করে। একটি eraser stroke অন্য সব stroke-এর মতো একই list-এর একটি element। Undo ওই element সরিয়ে দেয়। কোনও existing stroke বদলানো হয় না, কারণ erasure কেবল replay-এর সময়, list order অনুযায়ী ঘটে।

আমরা সেই model ship করলাম, আর দুটি device session সেটিকে ভেঙে দিল।

Object move অনুপস্থিত owner-কে প্রকাশ করে

প্রথম report: lasso দিয়ে stroke select করে drag করলে hole পেছনে পড়ে থাকে।

Stroke সরানোর আগে ও পরে দুটি eraser model-এর তুলনা করা চারটি panel। Eraser-কে নিজের element হিসেবে রাখলে gap পুরনো জায়গায় থাকে; erasure-এর মালিক stroke হলে gap ink-এর সঙ্গে সরে যায়।

একই erased stroke ডান দিকে সরানো হয়েছে। উপরে: eraser নিজস্ব element। নিচে: erasure stroke-এর মালিকানায়।

Selection বিষয়টি আরও খারাপ করল। Selected stroke bitmap থেকে overlay-তে উঠে যায়, যেখানে eraser element ছাড়া সরাসরি draw করা হয়; তাই erased stroke select করার মুহূর্তে সেটি আবার পুরো দেখা যেত।

দ্বিতীয় report: eraser highlighter-এর ওপর ভেসে থাকত। আমাদের renderer দুটি pass-এ replay করে, highlighter আগে এবং pen পরে, কারণ highlighter ink-কে ink-এর নিচে থাকতে হয়। একটি eraser element-কে দু’টি pass-এই অংশ নিতে হয়, নইলে এটি দুই ধরনেরই মুছতে পারে না। Pen pass-এ অংশ নেওয়ার অর্থ, প্রতিটি highlighter draw হওয়ার পরে চালানো; ফলে সময়ের হিসাবে পরে আঁকা highlighter stroke-ও eraser কেটে দিত।

দুটি failure-এর উৎস এক। MaLiang-এ lasso, move বা brush-গুলির মধ্যে layer order নেই, তাই owner-বিহীন element সেখানে যথেষ্ট। আমাদের canvas-এ তিনটিই আছে, এবং owner-বিহীন erasure-এর “ink সরে গেলে কী হয়” বা “এটি কোন layer-এ কাজ করছে”—এই প্রশ্নগুলির নির্দিষ্ট উত্তর নেই।

Stroke-এর মালিকানায় erasure

এখন প্রতিটি Stroke নিজের erase paths বহন করে। Eraser যে stroke-গুলিতে স্পর্শ করে, সেগুলি পরীক্ষা করে এবং প্রতিটির সঙ্গে canvas coordinates-এ একটি path যোগ করে। একটি stroke render করার সময় তার ink এবং নিজের erasure-গুলি একটি transparency layer-এর ভিতরে আঁকা হয়:

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 alpha-র অনুপাতে সেটি সরিয়ে দেয়:

একটি transparency layer-এর ভিতরে destination হল সেই একটিমাত্র stroke, তাই operation paper, নিচের stroke, বা পরে draw করা stroke পর্যন্ত পৌঁছাতে পারে না। যেসব stroke-এ erasure নেই, তাদের জন্য layer-এর কোনও খরচ নেই—এমন stroke-ই প্রায় সব: তারা আগের path ব্যবহার করে।

Ownership দুটি failure-এর সরাসরি উত্তর দেয়। Translation sample point-এর সঙ্গে erase path সরায়, তাই সরানো stroke তার gap সঙ্গে নিয়ে যায়। Selection overlay একই stroke object draw করে, তাই selected stroke একই gap দেখায়। Layer order আর গুরুত্বপূর্ণ নয়, কারণ erasure নিজের stroke ছাড়া অন্য কিছুতে কখনও প্রয়োগ হয় না।

Undo gesture-ভিত্তিক: একটি entry-তে gesture তৈরি করা প্রতিটি (stroke, path) pair থাকে; undo সেই path-গুলি সরায়, redo আবার append করে। Erasure-এর নিচের ink কখনও edit করা হয় না, তাই কিছু reconstruct করতে হয় না।

Live feedback tile bitmap দিয়ে যায়

যে stroke আঁকা হচ্ছে, সেটি সাধারণত composited page-এর ওপর overlay হিসেবে render হয়। সেখানে erasure কাজ করতে পারে না, কারণ overlay paper এবং ink—দুটির ওপরেই বসে, আর destination-out paper-এও গর্ত করে দিত।

তাই eraser সঙ্গে সঙ্গে stroke-এর মধ্যে লেখে, এবং নতুন sample-এর প্রতিটি batch কেবল affected rectangle-এর local replay শুরু করে। Tile pipeline আগে থেকেই একটি rectangle clear করে এবং সেটিকে intersect করা stroke-গুলি replay করে; eraser সেই পথই পুনর্ব্যবহার করে এবং দ্বিতীয় কোনও drawing semantic যোগ করে না।

পাশাপাশি দুটি eraser

Toolbar-এ একটি hidden mode-সহ একটিমাত্র button নয়, দুটি eraser button আছে। Partial eraser একটি stroke-এর অংশ সরায়। Stroke eraser স্পর্শ করলেই পুরো stroke সরায়। তাদের radius আলাদা constant, দুটিই বর্তমানে 4 পয়েন্ট।

একটি পুরো stroke delete করলে তার erase paths-ও সঙ্গে মুছে যায়, কারণ সেগুলি ওই stroke-এর অংশ; আর undo দুটিকে একসঙ্গে ফিরিয়ে আনে। Erase paths saved file-এরও অংশ, এবং বাকি সবকিছুর মতো quantize করা হয়: radius ও coordinates পয়েন্টের দশমাংশ হিসেবে।

একটি limitation রয়ে গেছে। সম্পূর্ণ মুছে ফেলা stroke ডেটা হিসেবে থেকে যায়। Render করলে কিছু দেখা যায় না, কিন্তু lasso এখনও সেটিকে select করতে পারে, stroke eraser এখনও সেটিকে delete করতে পারে, আর document-এ এটি এখনও জায়গা দখল করে। Fully erased stroke সরিয়ে ফেলতে একটি coverage test দরকার, যা আমরা এখনও লিখিনি; তাই আপাতত তারা থেকে যায়।

অন্য project-এর একটি model যখন আপনার project-এ কাজ করা বন্ধ করে, সাধারণত কারণ আপনার copy করা code-এ নয়। কারণ হল, ওই project-এ যে feature-গুলি নেই, আপনার project-এ সেগুলি আছে।