হাইলাইটারের তিনটি জ্যামিতি ইঞ্জিনকে স্ট্যাম্প দিয়ে কেন প্রতিস্থাপন করলাম
বাস্তব Apple Pencil-এর ডেটা সিন্থেটিক পরীক্ষায় ধরা না-পড়া ত্রুটি প্রকাশ করেছে; Lulucat Notes তিনটি আউটলাইন ইঞ্জিন থেকে MaLiang-এর স্ট্যাম্প মডেলে চলে গেছে।
Lulucat Notes শুরু হয়েছিল একটি প্রচলিত highlighter renderer দিয়ে: sampled path-এর চারপাশে geometric outline তৈরি করা, তারপর সেই outline-কে translucent colour দিয়ে একবার fill করা। একবারের fill translucent segment overlap হওয়ার জায়গায় তৈরি হওয়া গাঢ় band এড়ায়। একই সঙ্গে প্রতিটি join, self-intersection, cap এবং short stroke-কে একই geometry problem-এর মধ্যে নিয়ে আসে।
Custom geometry-এর তিনটি generation প্রত্যেকেই একটি defect-এর শ্রেণি সরিয়েছে এবং আরেকটি প্রকাশ করেছে। এখন আমরা যে renderer ship করি, সেটি MaLiang-এর ভিত্তিতে তৈরি soft circular stamp ব্যবহার করে; MaLiang হল Harley-xk-এর MIT-licensed open-source drawing framework। Outline failure-এর একটি সম্পূর্ণ শ্রেণি সরানোর বিনিময়ে আমরা darker self-intersection এবং pressure-tapered end মেনে নিয়েছি।

228% zoom-এ device-এর আসল screenshot। কয়েকটি stroke-এর দুই প্রান্তেই ছোট hook দেখা যায়।
বাস্তব handwriting synthetic scene-এ না-দেখা hook তৈরি করেছিল
আমাদের synthetic scene-এ straight line, arc, short tap, backtrack, pressure change এবং sharp turn ছিল, এবং সবগুলিই পরিষ্কারভাবে render হয়েছিল। তবু বাস্তব Chinese handwriting অনেক stroke-এর শুরু ও শেষে প্রায় 1–3 point লম্বা hook তৈরি করেছিল।
তাই আমরা app-এ একটি save button যোগ করে 29টি সম্পূর্ণ stroke record করি। প্রতিটি sample তার position, radius, pressure, azimuth, altitude, timestamp এবং UIKit estimation flag ধরে রেখেছিল। একটি offline program সেই JSON decode করে এবং app যে একই Stroke.swift, StrokeRenderer.swift এবং HighlighterStrokeBuilder.swift file ব্যবহার করে, সেগুলিই compile করে। তার output hook-সহ device screenshot-এর সঙ্গে stroke-by-stroke মিলে যায়।
Offline renderer কেবল recorded input ব্যবহার করেই defect-টি পুনরুৎপাদন করতে পেরেছিল। তাই কারণ display behaviour-এ নয়, sample এবং geometry-তে ছিল। এখন renderer-এ ঢোকা exact sample-গুলি আমরা পড়তে পারি।
আমাদের প্রথম hypothesis touchdown এবং lift-off-এর সময় estimated Pencil orientation-কে দোষ দিয়েছিল। ডেটা সেটি প্রত্যাখ্যান করে। সব 29টি stroke-এ, প্রথম ও শেষ আটটি sample-এর মধ্যে azimuth সর্বোচ্চ 0.09 radians এবং altitude সর্বোচ্চ 0.02 radians বদলেছিল, আর সেই iPad-এ প্রতিটি estimation flag false ছিল। Pressure এবং direction তীব্রভাবে বদলালেও orientation স্থির ছিল।
ডেটার একটি পরিষ্কার gap থেকেই pressure floor এসেছিল
প্রতিটি recorded stroke-এর শেষ sample-এর pressure ছিল 0.000। Writing sample-গুলির pressure ছিল 0.01 বা তার বেশি, আর Pencil surface ছেড়ে যাওয়ার পর record করা sample-গুলির pressure ছিল 0.002 বা তার কম। সেই airborne sample-গুলি প্রায়ই আরও 2–7 point চলতে থাকে এবং 0.8–9.0 point পথ অতিক্রম করে; তাদের direction লেখা stroke থেকে প্রায় 90 degrees পর্যন্ত সরে যেতে পারে।
Stroke 2 এই mechanism-টি দেখায়। তার body 143 থেকে 180 degrees-এর মধ্যে শেষ হয়েছিল। এরপর low-pressure tail −90 থেকে −67 degrees-এর দিকে ঘুরে আরও 8.5 point এগিয়ে যায়। Outline renderer শেষ airborne point-কে authoritative endpoint হিসেবে ধরেছিল, তাই সেটি ওই point-এর দিকে geometry-র একটি সরু অংশ টেনে নিয়েছিল।
Measured pressure writing এবং airborne movement-এর মধ্যে প্রায় এক order of magnitude-এর ব্যবধান রাখে, তাই আমরা সেই gap-এর মধ্যেই cutoff বসিয়েছি:
private static let pressureFloor: CGFloat = 0.005
শুধু দুই প্রান্তের low-pressure sample বাদ দেওয়া হয়। Stroke-এর মাঝখানে থাকা low-pressure sample তার অংশই থাকে। এই নিয়ম measured event boundary অনুসরণ করে এবং লেখা path-কে অপরিবর্তিত রাখে।

Trimming-এর আগে renderer cap-কে airborne sample-এ anchor করে। Green dot হল raw sample; red line হল generated outline।

0.005-এর নিচের sample trim করার পর cap শেষ written point-এ শেষ হয়। এই pressure floor বর্তমান stamp renderer-এও রয়ে গেছে।
এই floor দীর্ঘ airborne hook সরিয়ে দেয়। যেসব ছোট touchdown drag এবং lift-off arc-এর pressure 0.005-এর ওপরে ছিল, সেগুলি থেকে যায়, কারণ সেগুলি written motion। Outline model-এর ভেতরে এই motion সামলাতে গিয়েই geometry-র পরের কয়েকটি round শুরু হয়।
তিনটি geometry engine defect-কে শুধু এক জায়গা থেকে আরেক জায়গায় সরিয়েছিল
Generation one: একটি outline, একটি fill
প্রথম pressure-sensitive engine একটি পরিচিত pipeline অনুসরণ করেছিল। এটি একটি spine smooth করে, left ও right offset হিসাব করে, সেগুলিকে একটি polygon-এ জোড়ে এবং polygon-টি একবার fill করে। Nib ছিল Apple Pencil-এর azimuth এবং altitude দ্বারা নিয়ন্ত্রিত একটি ellipse, তাই writing direction-এর সঙ্গে তার cross-section বদলাত। Short tap-এর জন্য আলাদা shape ছিল, আর flat cap-এ zero-winding pocket এড়াতে ছোট asymmetry ব্যবহার করা হত।
Single fill stroke-এর ভেতরে opacity accumulation-এর সমস্যার সমাধান করেছিল, কিন্তু প্রতিটি turn-এ একটি valid polygon প্রয়োজন ছিল। Touchdown drag cap এবং body-র মধ্যে wedge তৈরি করত। Backtrack white crescent pocket তৈরি করতে পারত। Sharp turn inner outline-কে self-intersect করিয়ে একটি unfilled notch রেখে যেতে পারত।
Generation two: প্রান্তের কাছে outline মেরামত করা
দ্বিতীয় engine outline রেখে তার সঙ্গে local repair যোগ করেছিল: backtrack trimming, প্রথম দিকের estimated property-র জন্য damping, endpoint straightening, transient-direction trimming এবং একটি ছোট cap extension। প্রতিটি patch একটি পুনরুৎপাদিত failure সমাধান করেছিল। একসঙ্গে এগুলি endpoint behaviour-কে একাধিক threshold এবং window-এর ওপর নির্ভরশীল করে তোলে।
এরপর real writing সেই window-এর বাইরের case তৈরি করল। একটি fixed 24-point straightening window short stroke-এর ইচ্ছাকৃত hook মুছে দিল। একটি adaptive window সেই hook বাঁচিয়ে রাখল, কিন্তু lift-off arc তবুও দৃশ্যমান corner হয়ে উঠতে পারত। Direction-aware trimming আরও বেশি transient motion সরিয়ে দিল; বিনিময়ে একটি recorded stroke-এর সত্যিকারের curved start থেকে প্রায় 2.5 point বাদ পড়েছিল।
প্রতিটি patch তার নির্দিষ্ট case-টি সমাধান করেছিল। কিন্তু তাদের নিচের model-টি এখনও noisy, curved input-এর মধ্যেও valid থাকা একটি hard outline-এর ওপর নির্ভরশীল ছিল।
Generation three: clipped square cap-সহ coverage union
তৃতীয় engine একটি single contour তৈরি করা বন্ধ করল। এটি grayscale mask-এ প্রতিটি segment-এর জন্য একটি trapezoid এবং প্রতিটি interior sample-এ একটি disc আঁকল, তারপর তাদের coverage union নিল এবং highlighter colour একবার apply করল। Backtrack crescent, self-intersection darkening এবং zero-winding hole অদৃশ্য হয়ে গেল, কারণ renderer আর polygon winding-এর ওপর নির্ভর করছিল না।
Union প্রান্তগুলিকে round করে দিয়েছিল, তাই আমরা দুইটি endpoint half-plane-এর বিরুদ্ধে coverage clip করে square cap ফিরিয়ে আনলাম। সেই clip constant-pressure straight line ঠিক করল, কিন্তু আরও গুরুতর একটি defect তৈরি করল: একটি curved stroke-এর body বৈধভাবেই endpoint half-plane অতিক্রম করতে পারে, আর cap clip তখন stroke-এর মাঝখানের অংশ সরিয়ে ফেলত। Live input-এর সময় endpoint direction বদলাত, তাই clipped area সরে যেত এবং stroke flicker করত।

Global cap constraint একটি curved stroke-এর body কেটে দেয়।

Cap clipping-কে endpoint থেকে এক half-width-এর মধ্যে থাকা primitive-এ সীমাবদ্ধ করলেই এই stroke পুনরুদ্ধার হয়।
একটি arc-length gate cap clipping-কে local করে এবং recorded প্রতিটি case ঠিক করে। তবু একটি extreme synthetic hook-এ low-pressure tip clipped cap-এর সঙ্গে যেখানে মিলেছিল, সেখানে একটি ছোট white notch দেখা গিয়েছিল। তৃতীয় engine আবার একটি geometry failure-এর বদলে অন্য একটি geometry failure এনেছিল।
MaLiang hard boundary সরিয়ে দেয়
MaLiang একটি closed outline-এর বদলে repeated texture stamp ব্যবহার করে stroke তৈরি করে। এর path generator quadratic Bézier segment ব্যবহার করে, যা adjacent sample midpoint-এর মধ্য দিয়ে যায়, আর এর line renderer সেই path বরাবর regular step-এ stamp বসায়। Stroke বরাবর stamp size, rotation, colour এবং opacity—সবই বদলাতে পারে।
একটি soft stamp-এর আলাদা cap, join বা winding rule নেই। Direction-এ তীব্র পরিবর্তন overlapping mark-এর sequence হিসেবেই থেকে যায়। Backtrack কোনও empty polygon pocket তৈরি করতে পারে না, আর endpoint half-plane stroke-এর মাঝখান কাটতে পারে না, কারণ model-এ endpoint clip-ই নেই।
আমরা code নয়, model গ্রহণ করেছি। বর্তমান HighlighterStrokeBuilder চারটি operation করে:
- যেসব endpoint sample-এর pressure
0.005-এর নিচে, শুধু সেগুলি সরানো। - Midpoint quadratic Bézier segment দিয়ে position smooth করা।
- Arc length-এর প্রতি point-এ একটি circular stamp বসানো।
- Normalized pressure থেকে stamp diameter নির্ধারণ করা এবং source-over alpha দিয়ে stamp-গুলি composite করা।
Builder 686 line থেকে 190 line-এ নেমে এসেছে। Azimuth, altitude, outline construction, union mask, endpoint direction window, cap extension এবং cap clipping—সবই highlighter path থেকে বাদ গেছে। Input model এটি সমর্থন করে বলে আমরা এখনও Pencil orientation record করি, কিন্তু highlighter নিজে আর এই field-গুলি পড়ে না।
MaLiang-এর pressure formula-র জন্য device calibration দরকার ছিল
MaLiang একটি formula-র সমতুল্য পদ্ধতিতে stamp-এর size নির্ধারণ করে
যেখানে ধরে নেওয়া হয় force 0 থেকে 1 range-এর একটি কার্যকর অংশ ব্যবহার করে। আমাদের device data-তে তা সত্যি ছিল না। প্রায় 3,400টি Apple Pencil sample-এ median pressure ছিল 0.047 এবং maximum ছিল 0.178। এই value-গুলি সরাসরি formula-তে বসালে normal writing nominal brush size-এর তুলনায় অনেক বেশি পাতলা হয়ে যেত।
তাই আমরা observed median-কে কেন্দ্র করে normalize করি, তারপর exponent প্রয়োগ করে result clamp করি:
এখন median written pressure nominal 12-point diameter-এ map করে। খুব হালকা contact nominal size-এর 35%-এ দৃশ্যমান থাকে, আর heavy pressure 120%-এ পৌঁছে বাড়া বন্ধ করে।
এই calibration MaLiang-এর নয়, input device এবং brush behaviour-এর বিষয়। নিজের pressure distribution না মেপে original formula copy করলে architecture একই থাকত, কিন্তু brush ভুল হত।
জানা overlap count থেকেই per-stamp alpha এসেছিল
MaLiang overlapping translucent stamp-এর জন্য empirical expression alpha ÷ overlapping × 2.5 ব্যবহার করে compensation করে। আমাদের brush-এর fixed nominal diameter 12 point এবং stamp step 1 point, তাই তার centreline প্রায় 12 layer পায়। আমাদের ক্ষেত্রে overlap count জানা, তাই compensation অনুমান না করে solve করা যায়।
Target stroke opacity
তাই একটি stamp-এর জন্য দরকার

চূড়ান্ত renderer recorded device path replay করছে। Hook, diagonal cut এবং white pocket নেই।
আমরা যে দুটি খরচ মেনে নিয়েছি
Self-intersection আরও গাঢ় হয়। Source-over composition প্রতিটি stamp গোনে, যার মধ্যে একই area-র ওপর আগের pass-এ বসানো stamp-ও থাকে; coverage-union engine এটি এড়িয়েছিল। আমরা এই darker overlap মেনে নিয়েছি, কারণ এটি predictable এবং stroke shape অক্ষুণ্ণ রাখে।
Pressure-এর সঙ্গে endpoint সরু হয়। বাস্তব lift-off pressure কমিয়ে দেয়, ফলে stamp diameter কমে; তাই stroke full-width square cap-এ শেষ না হয়ে taper হয়। তিনটি geometry engine জুড়েই আমরা সেই square cap-কে requirement হিসেবে ধরে নিয়েছিলাম। সেটি বাদ দেওয়ায় ওপরের একাধিক failure-এর পেছনে থাকা clipping এবং endpoint machinery সরিয়ে দেওয়া গেছে, তাই tapered end মেনে নিয়েছি।
সুতরাং আমরা যে model ship করি, সেটি আগের model-গুলির তুলনায় কম কাজ করে। এটি measured writing event সংরক্ষণ করে, device calibration-এর পর pressure-এ সাড়া দেয় এবং globally valid outline-এর প্রয়োজন ছাড়াই curved ও self-crossing path render করে। Chisel-shaped nib, perfectly square cap এবং raw sample-এর মধ্যকার exact polyline—এই feature-গুলি আমরা তার বিনিময়ে ছেড়ে দিয়েছি।
এই তদন্তের ফলগুলির মধ্যে pressure floor-ই অন্য project-এ সবচেয়ে সরাসরি প্রয়োগ করা যায়। 0.005 value-টি real input-এর দুই cluster-এর মধ্যকার gap থেকে এসেছে: writing 0.01 এবং তার বেশি, আর airborne tracking 0.002 এবং তার কম। সেই একই recorded stroke-গুলি দেখিয়েছে প্রতিটি geometry repair কোথায় ব্যর্থ হয়েছে। সেই failure-গুলিই stamp model এবং এই model-এর সঙ্গে থাকা দুটি খরচের দিকে নিয়ে গেছে।