holodepth

HTML5 Canvas · Girdi & etkileşim

Pointer sistemi: tuval üzerinde birleşik işaretçi olayları

Pointer Events spesifikasyonu, fare, dokunma ve kalem girdisini tek bir olay ailesi altında toplar: her temas oturumu bir pointerId taşır; tarayıcı «hangi parmak?» sorusunu bu kimlikle ayırır. Canvas 2D uygulamasında çoklu dokunuş, sürükleme ve basınç duyarlı fırça için klasik mouse* yerine ( veya onunla birlikte dikkatli şekilde ) pointer* kullanmak, platformlar arası davranışı öngörülebilir kılar — fakat koordinatların yine istemci/CSS düzeni uzayında geldiğini ve piksele çevirmek için ayrı bir köprü gerektiğini unutmayın: Event koordinatları genel çerçeveyi; fare düğmeleri ve teker ayrıntısı Fare girdisi sayfasında derinleşir.

Bu sayfa Pointer Events’in canvas hedefli sözleşmesini, setPointerCapture güvenli kullanımını ve dokunma ile tarayıcı kaydırmasının nasıl yarıştığını işler; uzun süreli sürükleme durum makineleri için Sürükleme mantığı ve kare tabanlı çizim için requestAnimationFrame ile çapraz okuma önerilir.

Özet: sık pointer olayları

Olay Ne zaman? Canvas’ta tipik kullanım
pointerdown İşaretçi tuvalde veya alt öğede aktif oldu Araç seçimi, yakalama başlatma, ön izleme noktası
pointermove Konum veya basınç değişti Fırça izi; sürüklerken yoğun — rAF ile birleştirme düşünün
pointerup Temas veya düğme bitti Çizgi kapatma, commit, yakalama doğal olarak düşer
pointercancel Tarayıcı jesti ( kaydırma, sistem diyaloğu ) devreye girdi Yarım hareketi geri al; durumu sıfırla
pointerenter / pointerleave Öğe sınırı geçildi HUD, imleç modu, hover ( fare benzeri )
gotpointercapture / lostpointercapture Yakalama kazanıldı veya dışarıdan düştü Sürükleme bayrakları; beklenmedik kayıp için sigorta

Birleşik girdi modeli: neden Pointer Events

Eski modelde tarayıcı hem mousedown hem touchstart üretir; çift dinleme, çift çizim veya «bir yolda unutulan dinleyici» ile üretim hataları sık görülür. Pointer Events ile aynı etkileşim için yalnızca pointerdown / pointermove / pointerup üçlüsünü işlersiniz; dokunmatik cihazda tarayıcı uyumluluk katmanı mouse olaylarını da sentezleyebilir — bu yüzden hem pointer hem mouse dinlerseniz çift tetikleme riskini bilerek yönetmelisiniz. Çoğu yeni canvas arayüzünde yalnız Pointer Events yeterlidir; üst katman ikinci el kütüphaneler mouse varsayıyorsa arada köprü yazmak gerekir.

pointerType alanı "mouse", "pen", "touch" ( vb. ) döndürür; aynı kod yolunda cihaz sınıfına göre ince ayar yapabilirsiniz — örneğin kaleme özel silgi uçu veya dokunuşa daha geniş tolerans. Bu ayrım canvas çizgisel kalitesini doğrudan etkiler: kalemde pressure varken farede sıfıra yakın olabilir.

Kimlik ve oturum: pointerId, isPrimary ve çoklu dokunuş

pointerId sayısal bir tanıtıcıdır; çoklu dokunuşta her parmak için farklıdır. Durumunuzu bir Map<number, …> ile tutmak, hangi noktanın hareket ettiğini çözümlemenin standart yoludur. isPrimary genelde ilk temas eden işaretçiyi ( özellikle fare ve tek dokunuşta ) işaretler; çoğu masaüstü aracında birincil olmayan parmakları yok saymak veya yalnızca yakınlaştırma jestine ayırmak tasarım kararıdır — ikisi bir arada kullanılacaksa önce kullanıcı testi şarttır.

Oturum hep pointerup veya pointercancel ile biter; ara durumda parmak ekrandan kalkmadan başka jest devralırsa pointercancel görebilirsiniz. Haritadan ilgili pointerId girdisini bu iki olayda da mutlaka silin — aksi halde «hayalet parmak» bırakırsınız ve sonraki hareketler yanlış çizilir.

Yaşam döngüsü: pointercancel ve durum geri alma

pointercancel, kullanıcı jestinin tarayıcı tarafından «alındığı» anlamına gelir: sayfa kaydırması, tarayıcı menüsü veya sistemin hızlı hareket tanıyıcısı devreye girdiğinde tuval artık o temas zincirini garanti etmez. İptal geldiğinde pointerup beklemek hatalıdır; yarım çizimi temizleyip araç durumunu kapatan kod yolu şarttır. Bu, klasik mouse modelinde daha seyrek görülen bir farktır — Pointer Events’e geçerken en çok atlanan noktalardan biridir.

pointermove farede çok sık tetiklenebilir; tam sahne çizimini doğrudan olay içinde yapmak CPU’yu yakar. Kural: Fare girdisi · yoğun mousemove ile aynı — olayda yalnız veri güncelle, çizimi requestAnimationFrame ile tekilleştir; kirli bayrak deseni için Update vs render · kirli bayrak sayfasına bakın.

Yakalama: setPointerCapture ve lostpointercapture

Sürüklerken imleç veya parmak tuval sınırının dışına çıkarsa, yakalama olmadan olaylar başka öğeye düşebilir veya kesilebilir. element.setPointerCapture(pointerId) çağrısı ( genelde pointerdown içinde, kullanıcı etkileşimi zincirinde ) sonraki pointermove / pointerup olaylarını hedef öğeye yönlendirir. pointerup ve pointercancel yakalamayı otomatik olarak düşürür; yine de uygulama içi durum makineniz için lostpointercapture dinlemek, beklenmedik sistem müdahalelerinde temizlik yapmanızı sağlar.

Yakalama, güvenlik ve kullanıcı denetimi ile sınırlandırılmıştır; her zaman kötüye kullanıma karşı düşünülerek tasarlanmıştır. Özelleştirilmiş sürükle-bırak akışlarında ( ön izleme, ızgara hizalama, çok adımlı yerleştirme ) Sürükleme mantığı sayfası durum geçişlerini derinleştirir; burada yalnızca API sınırı ve olay eşleşmesi hatırlatılır.

/**
 * pointerdown: yakalama başlatır (aynı pointerId için).
 * Olay işleyicileri AbortController ile sökülür — bellek sızıntısı olmasın.
 */
function attachPointerCaptureForDrag(canvas, { onMove }) {
  const ac = new AbortController();
  const { signal } = ac;

  canvas.addEventListener(
    'pointerdown',
    (e) => {
      canvas.setPointerCapture(e.pointerId);
    },
    { signal },
  );

  canvas.addEventListener(
    'pointermove',
    (e) => {
      if (e.buttons === 0 && e.pointerType === 'mouse') return;
      onMove?.(e);
    },
    { signal },
  );

  function release(/* e */) {
    /* pointerup / cancel sonrası ek temizlik buraya */
  }

  canvas.addEventListener('pointerup', release, { signal });
  canvas.addEventListener('pointercancel', release, { signal });
  canvas.addEventListener('lostpointercapture', release, { signal });

  return () => ac.abort();
}

Dokunma davranışı: touch-action ve passive dinleyiciler

Mobil ve dokunmatik dizüstüde tarayıcı, dikey kaydırmayı önceliklendirir. Tuval üzerinde tek parmakla çizmek istiyorsanız, CSS touch-action: none ( veya seçici olarak pinch-zoom dışlaması ) ile jest ayrımını kontrol etmelisiniz — aksi halde pointercancel ile çiziminiz sık kesilir veya sayfa zıplar. touch-action salt CSS ile çözülebildiği için, her olayda preventDefault ile kaydırmayı öldürmekten daha sürdürülebilir ve erişilebilir dostudur.

Uygulamada tuval genelde bir sarmalayıcı içinde durur: touch-action değerini etkileşim katmanına ( örn. tam boy sarmalayıcı veya şeffaf üst öğe ) vermek, altındaki sayfa alanının kaydırılabilir kalmasına izin verirken yalnız çizim yüzeyinde jestleri kilitlemenize olanak tanır. touch-action: manipulation tek parmak kaydırmayı bırakıp çift parmak yakınlaştırmayı sınırlamak gibi orta yollar sunar; «tuvalde çiz, çevrede sayfa kay» senaryosunda hangi öğenin jest alanı olduğunu net çizmek gerekir — aksi halde kullanıcı beklenmedik şekilde üst katmanda veya tuvale takılı kalır.

Tarayıcı, kendi kaydırma/kirpme animasyonu ile rekabete girdiğinde pointercancel ve durum geri alma yolunun devreye gireceğini unutmayın. Yakınlaştırma veya üst menü açıldığında yarım çizgi bırakmak yerine iptal olayında fırçayı kapatmak, touch ilkesiyle birlikte düşünülecek tek bir davranış seti oluşturur.

Bazı ortamlarda dinleyici passive: true olarak kaydedilir; bu durumda preventDefault() etkisiz kalır. Tuval zoom’unu elle kontrol etmek istediğinizde dinleyici seçeneklerini bilinçli seçin — konu fare tekeri tarafında Fare girdisi · dinleyici yaşam döngüsü ile paraleldir. Çoğu canvas aracında birincil çözüm touch-action değerini CSS ile doğru ayarlamak, ikincil çözüm ise gerçekten passive: false gerektiğinde ( ve kullanıcı etkileşimi sırasında ) sınırlı preventDefault kullanmaktır — ikisini birlikte koşulsuz uygulamak hem performansı hem erişilebilirliği zorlar.

Basınç, iletişim alanı ve stylus: çizgi kalitesi

pressure 0..1 aralığında ( desteklenmediğinde 1 veya 0.5 varsayımlarına düşebilir ) çizgi genişliğini modüle etmek için kullanılır. width ve height iletişim elipsinin boyutunu verir; parmak alanı geniş olduğunda ince «mouse benzeri» nokta yerine doğal fırça hissi çıkar. tiltX / tiltY destekleniyorsa kalem eğimi ile hachure veya kaligrafi efektleri mümkündür — tüm cihazlarda olmayacağını varsayarak kod yolunu dallandırın.

Pratikte en güvenli desen: önce ham değerleri okuyup sonra tek bir yardımcıda «çizim birimi»ne çevirmektir. Örneğin lineWidth için Math.max(minKalem, tabanGenişlik * (0.15 + 0.85 * pressure)) gibi alt ve üst sınırlı eşlemeler, 0 veya 1 uçlarında patlayan imzaları yumuşatır; parmak dokunuşunda width / height çoğu zaman basınçtan daha güvenilir bir «gövde» ipucudur — ikisini birlikte ( ağırlıklı ortalama veya kinematik yumuşatma ) kullanmak fırça karakterini zenginleştirir.

Piksel dünyasına aktarırken yine tek bir clientX/clientY → tuval piksel köprüsü kullanın; basınç ve genişlik değerlerini doğrudan çizim birimine ölçeklemek, DPR ile uyumlu kalınlık üretir. Genel dönüşüm tablosu Event koordinatları ile aynı fikirde tutulmalıdır — burada yalnız Pointer’a özgü ek ölçüler eklenir.

Çizgi örnekleme aralığı düşük kare hızlarında kırılmaya başlarsa, ham pointermove noktalarını doğrudan bağlamak yerine ara nokta ( örn. önceki olay ile lineer ara adım ) üretmek veya Bézier yumuşatması uygulmak gerekir — bu, basınç ile birlikte ele alındığında el yazısı hissi ciddi biçimde iyileşir. Yoğun taşıma olaylarında çizim gövdesini yine rAF ile tekilleştirme düşüncesiyle tutmak, hem stabilite hem pil tüketimi açısından faydalıdır; aşağıdaki örnek olaydan gelen pressure ve konumu tek karede işler.

/**
 * toCanvas: client → tuval pikseli. render: tek karede stroke/emit.
 */
function attachPointerMoveCoalesced(canvas, toCanvas, render) {
  const ac = new AbortController();
  const { signal } = ac;
  let rafId = 0;
  let latest = null;

  function flush() {
    rafId = 0;
    if (!latest) return;
    render(latest);
    latest = null;
  }

  canvas.addEventListener(
    'pointermove',
    (e) => {
      latest = { ...toCanvas(canvas, e.clientX, e.clientY), e };
      if (!rafId) rafId = requestAnimationFrame(flush);
    },
    { signal },
  );

  return () => {
    cancelAnimationFrame(rafId);
    ac.abort();
  };
}

Anti-kalıplar: çift dinleme ve unutulan iptaller

Mouse ve pointer birlikte: Aynı etkileşimde her iki aileyi de koşulsuz dinlemek çoğu cihazda çift işlem üretir. Ya Pointer Events’e geçin ya da köprü katmanında birini tüketin.

pointercancel yok saymak: Yarım polyline veya sürüklenen ön izleme ekranda kalır; kullanıcı tarayıcı kaydırdığında tuval «yapışmış» görünür.

Çoklu pointerId temizliği: pointerup yolunda haritadan silmeyi atlamak çok dokunuşlu testlerde patlar.

Yakalama sonrası kalıcı dinleyici: Bileşen unmount olduğunda AbortController ile hem olayları hem planlı rAF’ı iptal edin ( yukarıdaki örnek desen ).

Bu sayfanın sınırı

Intersection Observer veya kamera tabanlı jestler burada ele alınmaz. Oyun kumandası için klavye ve ileride Gamepad API ayrı başlıktır — Klavye girdisi köprüsü canvas odak akışını tamamlar.

Ham dokunma olayları ( touch* ) ile Pointer Events farklı garanti seviyelerine sahiptir; yeni canvas kodunda Pointer Events tercih edilmesi, üst düzey tutarlılık için önerilir.

  • pointercancel ve lostpointercapture için temizlik yolları yazıldı mı?
  • Çoklu pointerId haritası her çıkış olayında güncelleniyor mu?
  • Mobilde touch-action tuval jestleriyle uyumlu mu; üst katman jest alanını gölgeliyor mu?
  • pressure / width için cihaz düşümü ve çizgi genişliği sınırları tanımlı mı?
  • Yoğun pointermove rAF ile birleştirildi mi?
  • Mouse ve pointer dinleyicileri çift iş yapmıyor mu?