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
toCanvastek 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ı?