holodepth

HTML5 Canvas · Girdi & etkileşim

Sürükleme mantığı: tuval üzerinde taşıma ve durum yönetimi

Canvas 2D’de bir şekli, saplamayı veya aracı «sürüklemek», tarayıcının ürettiği olay dizisini dar bir durum makinesine indirgemek demektir: basıldı mı, hangi nesne seçildi, imleç tuvale göre nereye kaydı, bırakıldı mı veya işlem iptal mi edildi? Olayları doğrudan çizime bağlamak yerine önce oturum durumunu güncellemek, hem okunurluğu hem de çoklu girdi ( fare, dokunma, kalem ) için tek kod yolu sağlar — düşük seviye olay sözleşmesi Pointer sistemi ve Fare girdisi sayfalarında; burada bunların tuval hedefli sürükle–bırak bileşimine odaklanılır.

Koordinat köprüsü Event koordinatları ile aynı ilkeleri paylaşır; kare tabanlı çizim ve kirli bayrak deseni Update vs render · kirli bayrak ile aynı hizada düşünülmelidir — sürükleme sırasında sahneyi her pointermove’da komple yeniden kurmak yerine konumu veya modeli güncelleyip tek çizim yoluna aktarmak üretimdedir.

Özet: sürükleme aşamaları

Aşama Tipik olay Canvas’ta yapılacak iş
Seçim / basınç pointerdown İsabet sınaması, tutma ofsetini kaydet, gerekirse yakalama başlat
Taşıma pointermove Durumu güncelle; çizimi rAF ile tekilleştir veya hafif ön izleme
Commit pointerup Final konumu sabitle, seçimi bırak, oturumu kapat
İptal pointercancel, lostpointercapture Ön izlemeyi geri al veya yarım taşımayı yoksay

Durum modeli: seçili nesne ve sürükleme oturumu

Üretimde yayılan hata, «her olayda her şeyi» yapmaktır: kod yolu karmaşıklaşır, çift çizim yapılır ve mobilde pointercancel geldiğinde durum ortada asılı kalır. Bunun yerine küçük bir oturum nesnesi ( örn. { pointerId, hedefId, grabDx, grabDy } ) tutun; yalnız oturum aktifken taşıma mantığını çalıştırın. İdle durumda olan tuvale gelen hareket olayları erken dönüş ile ucuz kalmalıdır — bu, özellikle çok katmanlı arayüzlerde CPU maliyetini belirgin düşürür.

Aynı anda tek nesneyi taşımak çoğu araç için yeterlidir; çoklu seçim veya çok parmaklı jest gerekiyorsa pointerId başına ayrı oturum ( Map ) düşünün — Pointer sistemi · kimlik ve oturum bu modeli destekler. Oturum kapanırken hem yerel bayrakları sıfırlayın hem de üst uygulama durumuna «commit / abort» sinyali verin — örneğin «snap» sonrası geçmişe tek kayıt eklemek gibi.

Tutma ofseti ve tek köprü: nesne neden «zıplamamalı»

Kullanıcı dairenin kenarından tuttuğunda, yeni konum doğrudan imleç koordinatına eşitlenirse şekil imlece «sıçrar». Çözüm: basınç anında çıpa ( anchor ) ile imleç arasındaki farkı ( grabDx, grabDy ) saklamak ve her karede nesne merkezini veya sol–üst köşesini imleç − tutma vektörü ile güncellemektir. Bu vektör tuval piksel uzayında tutulmalıdır; CSS ölçeği ve DPR dönüşümü tek bir client → canvas yardımcısında toplanmalıdır.

Aynı yardımcı hem seçim hem taşıma hem gömülü ölçüm çizgilerinde paylaşılmalıdır — aksi halde 1–2 piksel sapmalar bile kullanıcı güvenini kırar. Genel çerçeve için Event koordinatları sayfasına dönün; burada yine de kısa bir toCanvas örneği verilir ki sürükleme modülü tek dosyada okunabilir kalsın.

/** İstemci pikseli → tuval bitmap pikseli (CSS ölçek + iç boyut oranı). */
function clientToCanvasPixel(canvas, clientX, clientY) {
  const r = canvas.getBoundingClientRect();
  const sx = canvas.width / r.width;
  const sy = canvas.height / r.height;
  return { x: (clientX - r.left) * sx, y: (clientY - r.top) * sy };
}

Yakalama ve iptal: setPointerCapture ile güvenli taşıma

Sürükleme sırasında imleç tuval dışına çıkınca olaylar kesilirse deneyim kırılır. setPointerCapture(pointerId) genelde pointerdown zincirinde, gerçek bir kullanıcı hareketinden sonra çağrılmalıdır. pointerup ve pointercancel yakalamayı düşürür; yine de lostpointercapture ile uygulama içi oturumu kapatmak, beklenmedik sistem müdahalelerinde sızıntıyı önler — Pointer sistemi · yakalama ile aynı çizgidir.

pointercancel geldiğinde «bırakıldı» varsaymak hatalıdır: kullanıcı jesti tarayıcıya kaptırmıştır. Ön izleme varsa geri alın; kalıcı modeli güncellemeyin veya işi yarım bırakan bir «taslak» bayrağı ile işaretleyin. Dokunmatik kaydırma politikası için Pointer sistemi · dokunma davranışı sayfasındaki touch-action önerileriyle birlikte test edin.

/**
 * toCanvas: clientToCanvasPixel gibi. hitTest(p, event) → seçilebilir mi.
 * onMove: kare başına delta (dx,dy) ve basılan noktadan bu yana toplam (totalDx,totalDy).
 * Nesne köşesine göre tutma ofsetini onStart içinde modele yazın (grabDx = p.x - obj.x).
 */
function attachCanvasPointerDrag(canvas, options) {
  const {
    toCanvas,
    hitTest = () => true,
    onStart,
    onMove,
    onEnd,
  } = options;

  const ac = new AbortController();
  const { signal } = ac;
  let session = null;

  function finish(reason, e) {
    if (!session) return;
    if (e && 'pointerId' in e && e.pointerId !== session.pointerId) return;
    const s = session;
    session = null;
    onEnd?.(s, reason);
  }

  canvas.addEventListener(
    'pointerdown',
    (e) => {
      const p = toCanvas(canvas, e.clientX, e.clientY);
      if (!hitTest(p, e)) return;
      canvas.setPointerCapture(e.pointerId);
      session = {
        pointerId: e.pointerId,
        start: { ...p },
        last: { ...p },
      };
      onStart?.(session, e);
    },
    { signal },
  );

  canvas.addEventListener(
    'pointermove',
    (e) => {
      if (!session || e.pointerId !== session.pointerId) return;
      const p = toCanvas(canvas, e.clientX, e.clientY);
      const dx = p.x - session.last.x;
      const dy = p.y - session.last.y;
      session.last = { ...p };
      onMove?.(
        {
          ...session,
          point: p,
          dx,
          dy,
          totalDx: p.x - session.start.x,
          totalDy: p.y - session.start.y,
        },
        e,
      );
    },
    { signal },
  );

  for (const type of ['pointerup', 'pointercancel', 'lostpointercapture']) {
    canvas.addEventListener(type, (e) => finish(e.type, e), { signal });
  }

  return () => {
    finish('dispose');
    ac.abort();
  };
}

İsabet sınaması: yol, dolgu ve basit geometri

Taşıyacağınız nesne bir Path2D ile çiziliyorsa, bağlamı geçici olarak kaydedip isPointInPath(p.x, p.y) veya isPointInStroke ile tıklama bölgesini sorgulayabilirsiniz. Dönüşüm ( translate/rotate ) aktifse sınamayı aynı matriste yapın — aksi halde «tıklanmıyor» hissi oluşur. isPointInPath için o anki fillRule ( nonzero / evenodd ) çizimle aynı olmalıdır; bileşik delikli formlarda kural uyumsuzluğu «içi boş sanılan» yanlış seçim üretir. isPointInStroke güncel çizgi kalınlığı, uçlar ve birleşim moduna duyarlıdır — ince çizgi ile dar vuruş alanı, kalın kontur ile geniş vuruş alanı demektir.

Pratik alternatif: model dünyasında dönüşümü yapıp noktayı ters matrisle «model uzayına» taşımak ve orada sınamak — böylece bağlam üzerinde ekstra save / restore döngüsü azaltılır; fakat matris ters çevrilemezse ( tekil durumlar ) yine bağlam kopyası yolu tercih edilir. Kullanıcı dostu dokunma için görsel çizgiden birkaç piksel daha geniş gizli vuruş ( hit slop ) uygulamak mobilde bariz rahatlık sağlar; bu bölge sadece sınamada kullanılmalı, çizimle karışmamalıdır.

Basit dünya modelinizde daire veya dikdörtgen listesi tutuyorsanız analitik mesafe / sınırlı kutu kontrolü daha ucuz olabilir; sahne yüzlerce nesneye çıkınca uzamsal indeks ( ızgara, dört ağaç ) ayrı optimizasyon konusudur, burada yalnızca doğruluk ilkesi hatırlatılır. attachCanvasPointerDrag örneğindeki hitTest(p, e) ( Yakalama ve iptal ) bu sınamayı tek noktadan bağlar; yanlış uzayda ( CSS pikseli sanılarak ) yazılmış hitTest, en sık görülen sessiz hatadır.

Seçim önceliği: üstte çizilen nesne altta kalanın olayını «yutmalıdır». Tuvalde genelde çizim sırası z sırasıdır; ters sırada isabet testi yapmak veya her nesneye ayrı tuval katmanı kullanmak mimari tercihtir — ikinci yol etkileşimi kolaylaştırır fakat bellek ve birleştirme maliyetini artırır. Çoklu katmanda üst tuvalde «yalnız ön yüzeyi şeffaf bırakıp» altı göstermek isterken, üst katman tüm dikdörtgeni opak alırsa alttaki hiç seçilmez — pointer-events ve şeffaf bölgeler Fare girdisi · CSS odak çizgisinde ele alınır; burada yalnızca «çizim sırası = yutma sırası» kuralı sabittir.

Ön izleme, sınır ve snap: commit ayrımı

Kullanıcı sürüklerken iki katman düşünün: taslak konum ( ön izleme ) ve kayıtlı model ( commit sonrası ). Taslak yalnızca oturum aktifken çizilir; bırakma veya iptalde model ya yeni konuma yazılır ya da eski haline döner. Bu ayrım, geri alma ( undo ) yığınını basitleştirir ve yarım bırakılmış taşımayı güvenle yok saymanızı sağlar. İptal yolunun hem pointerup hem pointercancel ile aynı «ön izlemeyi düşür» kodunu çağırdığından emin olun — aksi halde Anti-kalıplar bölümündeki hayalet ön izleme oluşur.

Ön izleme görseli genelde yarı saydam doldurma, kesik çizgi kontur veya gölge ofseti ile ayakta kalır; kalıcı çizimde kullandığınız gölgelendirme yığınını kopyalamak zorunda değilsiniz — kullanıcıya yeterli ipucu veren hafif bir katman, kare başına maliyeti düşürür. Taslağı tuval sınırları içinde kısıtlamak ( clamp ) ile serbest bırakmak ürün kararıdır: keskin kenarda «yapışmış» his için sınırda sürtünme ( yumuşak clamp veya birkaç piksel içeri itme ) eklenebilir.

Tuval sınırları veya ızgara hizalama genelde commit anında uygulanır: sürükleme boyunca yumuşak manyetik his için taslak konuma toleranslı snap ekleyebilirsiniz. Canlı snap ile commit snap’i karıştırmayın — ilki yönlendirme hissi verir, ikincisi modeli gerçekten ızgara düğümüne kilitleyen tek yazmadır. İnce toleranslı manyetizma için eşik değerini cihazın DPR ve ölçeğine göre piksel cinsinden kalibre etmek sapmayı azaltır.

Klavyeyle ince ayar ( ok tuşları ) için Klavye girdisi odağı ile birlikte tasarlanmalıdır — aksi halde tuval odaksızken oklar beklenmedik davranır. Sürüklerken Esc ile iptal gibi klavye kısayolları ön izlemeyi düşürüp modeli eski konuma döndürmeli; bu, pointer oturumu kapalıyken de tutarlı bir UX sağlar.

Performans ve kiralık dinleyici: mouse yedek yolu

pointermove yoğunlaştığında tam sahneyi olay içinde çizmek pahalıdır; konumu güncelleyip requestAnimationFrame ile tekilleştirin — Fare girdisi · yoğun mousemove ile aynı üretim felsefesi. Olay yolunu ince tutup modeli ( taslak dahil ) güncelledikten sonra tek çizim kanalına aktarmak, Update vs render · kirli bayrak düşüncesiyle örtüşür: sürükleme bitene kadar aynı karede gereksiz ikinci tam boyalı geçişten kaçının.

Kare planlı çizimde, her karede yalnız kirli ( dirty ) dikdörtgeni yeniden çizmek mümkünse ( arka plan statikse ) maliyet ciddi düşer — bu, sayfa kapsamı dışı bir optimizasyon başlığıdır; yine de sürükleme ön izlemesini tam tuvale yaymak yerine küçük bir üst katman veya sınırlı bölge stratejisini değerlendirmek üretimdedir. Zaman adımı hassasiyeti gerekiyorsa Delta time ile uyumlu güncelleme ( örn. animasyonlu snap ) ayrı katman olarak kalabilir.

Nadiren yalnız klasik mouse olaylarına güvenirseniz, window üzerinde geçici mousemove kiralamak mümkündür — fakat mouseup ile mutlaka sökün; kalıcı dinleyici hem CPU hem hata ayıklama maliyetidir. Mümkün olan her yerde Pointer Events ve setPointerCapture tercih edin; bu sayfa mouse yolunu yedek strateji olarak not düşer. Sekme görünürlüğü veya pencere blur olduğunda yarım oturumu Klavye girdisi · basılı tuş durumu sayfasındaki gibi sıfırlama düşüncesine yakın duran bir «güvenlik supabı» ile kapatmak, sürüklemeyi ekran dışında sürdüren hayalet durumları engeller.

Anti-kalıplar: yakalamayı unutmak ve yanlış ofset

İmleç = nesne köşesi: Tutma ofseti olmadan doğrudan konum atamak. Küçük şekillerde bariz, büyük görsellerde kullanıcı güvenini kırar. Düzeltme: basınç anında çıpa ile model köşesi ( veya merkezi ) arasındaki vektörü saklayıp her karede çıkarın ( Tutma ofseti ).

pointercancel yok saymak: Yarım ön izleme ekranda kalır; model ile görsel uyuşmaz. İptalde hem ön izleme katmanını hem seçim vurgusunu, oturum bayraklarını aynı işlevde düşürün.

Yakalama olmadan sınır dışı: Taşıma yarıda kesilir; mobilde sık görülür. Çözüm: setPointerCapture ( Yakalama bölümü ).

Hit test ile dönüşüm uyumsuzluğu: Çizim matrisi ile sınamanın matrisi farklı. Aynı model transformu yoksa «bazen seçiliyor bazen değil» sapması üretir — özellikle döndürülmüş dikdörtgenlerde iç/dış testi kayar.

Çift iş yükü: Aynı hedefte hem pointer* hem mouse* ile aynı taşıma mantığını koşulsuz çalıştırmak, uyumluluk katmanında çift adım riski taşır. Bir aile seçin veya merkezi birleştiriciden geçirin ( Pointer sistemi · birleşik girdi ).

Oturumu kapatmadan bileşeni sökmek: Dispose yalnız dinleyiciyi değil, yarım kalan taslak çizimini de temizlemeli; aksi halde sonraki mount’ta eski hayalet ön izleme görülebilir.

Bu sayfanın sınırı

HTML5 sürükle–bırak ( drag and drop ) dosya API’si ve tuval içi model taşıması farklı problemlerdir; burada yalnızca canvas çizim modelinin içinde konum güncellemesi ele alınır. Tam özellikli düzenleyici ( hizalama rehberleri, çoklu seçim, bağlantı çizgileri ) kendi ürün katmanınızdadır.

  • Oturum nesnesi her çıkış yolunda (up, cancel, lostpointercapture, unmount) kapatılıyor mu?
  • Tutma ofseti ve toCanvas tek yardımcıda mı?
  • Canlı snap ile commit snap ayrımı ve iptal ( cancel ) yolu ön izlemeyi düşürüyor mu?
  • isPointInPath / stroke sınaması çizimdeki dolgu kuralı ve konturla uyumlu mu?
  • Ön izleme ile kalıcı model ayrıldı mı?
  • Taşıma çizimi rAF ile sınırlandı mı?