Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Canvas'da animatsiya
- 2.1 Har kadrda: tozalash, yangilash, chizish
- 2.2 O'yin sikli
- 2.3 «Bahor»: somsani ushla
- 3. Rasmni qayta ishlash
- 3.1 Kichraytirish: yuklashdan oldin
- 3.2 Qirqish: rasmning bo'lagi
- 3.3 Piksellar bilan ishlash: getImageData
- 3.4 Ifloslangan canvas
- 4. Eksport: canvas'dan faylga
- 4.1 toBlob va toDataURL
- 4.2 Format va sifat
- 4.3 «Bahor» cheki: chizish va yuklab olish
- 5. OffscreenCanvas
- 6. Ko'p uchraydigan xatolar
- 6.1 Kadrni tozalamaslik
- 6.2 Tugma "yopishib qolishi"
- 6.3 Ifloslangan canvas
- 6.4 Shaffof fonni JPEG qilish
- 6.5 toDataURL ni hamma joyda ishlatish
- 6.6 Piksellarni bittalab o'qish
- 7. Mashqlar
- 1-mashq (oson): Sakrayotgan koptok
- 2-mashq (o'rta): Suv belgisi
- 3-mashq (qiyin): Hajm chegarasigacha siqish
- 4-mashq: Amaliy tajriba — vazifalar ro'yxati rasm bo'lib
- 8. Real ishda
- Xulosa
- Manbalar
Canvas amaliyotda: animatsiya, o'yin sikli, rasmni qayta ishlash va toBlob
Qisqacha: Canvas'dagi animatsiya — har kadrda uch ish: tozalash (
clearRect), holatni yangilash va qayta chizish; kadrlarrequestAnimationFramebilan, harakat delta time bilan. O'yin sikli shunga klaviatura holati va to'qnashuv tekshiruvini qo'shadi. Rasmni kichraytirish va qirqish —drawImagening o'zi, piksellar bilan ishlash —getImageData/putImageData. Tayyor chizmacanvas.toBlob(callback, tur, sifat)bilan faylga aylanadi va<a download>bilan yuklab olinadi.OffscreenCanvas— sahifaga qo'yilmaydigan, alohida thread'da ham ishlaydigan canvas.
Bu darsda
- Canvas'da animatsiya va oddiy o'yin siklini (kirish → yangilash → chizish) yoza olasiz.
- Katta rasmni yuklashdan oldin kichraytirasiz va markazidan kvadrat qilib qirqasiz.
getImageDatabilan piksellarni o'qib, rasmni oq-qora qilasiz.- Chizmani
toBlobbilan PNG, JPEG yoki WebP faylga aylantirib, yuklab berasiz. - "Ifloslangan" canvas xatosini va
OffscreenCanvasnima uchun kerakligini tushuntira olasiz.
Oldin bilishingiz kerak: Canvas 2D asoslari, requestAnimationFrame, File va Blob, Faylni o'qish va oldindan ko'rish, Typed arrays va binar ma'lumot.
1. Nega bu kerak?
O'tgan darsda canvas'da grafik chizdik. U bir marta chizildi va qotib qoldi. Jasur akaning esa «Bahor» uchun yangi uchta istagi bor:
- O'yin. Buyurtma kutayotgan bolalar uchun saytda kichik o'yin: tepadan somsa tushadi, savat bilan ushlaysiz. Bu — har soniyada o'nlab marta qayta chiziladigan rasm.
- Rasmlar. Sardor menyu uchun telefonda taom suratga oladi. Har surat 4 000 piksel enida va 5 MB. Saytga shundayligicha yuklansa, mehmonning telefoni sekin ochadi. Rasmni yuklashdan oldin, brauzerning o'zida kichraytirish kerak.
- Chek. Mehmon buyurtmasining chekini rasm qilib saqlab, Telegram'da yuborishni so'raydi. Canvas'da chizilgan chekni faylga aylantirish kerak.
Uchalasi ham shu darsda. Hammasi o'tgan darsdagi asboblar ustiga quriladi — yangi metodlar oz, asosiysi ularni qanday birlashtirish.
2. Canvas'da animatsiya
2.1 Har kadrda: tozalash, yangilash, chizish
O'tgan darsda aytgan edik: canvas'da chizilgan narsani "surib" bo'lmaydi, faqat qayta chizish mumkin. Demak, animatsiya — tez-tez qayta chizilgan rasmlar ketma-ketligi, xuddi multfilm kadrlari kabi. Har kadrda uch qadam:
- Tozalash —
clearRect(0, 0, eni, bo'yi): eski kadrni o'chirish. - Yangilash — holatni o'zgartirish:
x += tezlik × dt. - Chizish — yangi holatni matoga chizish.
Kadrlarni requestAnimationFrame beradi, tezlikni esa delta time boshqaradi — o'sha darsdagi kuryer misoli. Endi kuryer canvas'da. «Izni qoldirish» katakchasi birinchi qadamni — tozalashni — o'chiradi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { border: 1px solid #c8d3c8; max-width: 100%; }
</style>
<canvas id="road" width="320" height="100">
Kuryer yo'l bo'ylab chapdan o'ngga yuradi.
</canvas>
<p><label><input type="checkbox" id="trail"> Izni qoldirish
(clearRect'siz)</label></p>
<script>
const canvas = document.getElementById("road");
const ctx = canvas.getContext("2d");
const trail = document.getElementById("trail");
const speed = 120; // px soniyasiga
let x = 0;
let previous = null;
function drawCourier(x) {
ctx.fillStyle = "#2e7d32";
ctx.fillRect(x, 40, 40, 24); // quti
ctx.fillStyle = "#1b1b1b";
for (const wheelX of [x + 8, x + 32]) { // ikki g'ildirak
ctx.beginPath();
ctx.arc(wheelX, 70, 7, 0, Math.PI * 2);
ctx.fill();
}
}
function frame(time) {
if (previous === null) previous = time;
const dt = Math.min((time - previous) / 1000, 0.1);
previous = time;
if (!trail.checked) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 1
}
x = (x + speed * dt) % (canvas.width + 40); // 2
drawCourier(x - 40); // 3
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>Katakchani belgilang: kuryer orqasida yashil "lenta" qoladi. Har kadr oldingi kadrlar ustiga chizildi — tozalanmagani uchun. Bu canvas animatsiyasidagi eng ko'p uchraydigan xato. Ba'zan esa ataylab ishlatiladi: chizish ilovasida cho'tka izi aynan shunday qoladi.
x - 40 — kuryer chap chetdan, ko'rinmas joydan "kirib" kelishi uchun. % (canvas.width + 40) esa u o'ng chetdan to'liq chiqib ketgach, boshiga qaytaradi.
Maslahat: Fon har kadrda bir xil bo'lsa (masalan, murakkab manzara), uni har safar qayta chizish qimmat. Ikkita canvas'ni CSS bilan ustma-ust qo'ying: pastkisida fon — bir marta chiziladi, ustkisida harakatlanuvchi narsalar — har kadrda. Bu usul qatlamlar (layers) deyiladi.
2.2 O'yin sikli
O'yin — animatsiyaning o'zi, faqat foydalanuvchi aralashadi. Shuning uchun har kadrga yana ikki qadam qo'shiladi: kirish (qaysi tugmalar bosilgan?) va qoidalar (somsa savatga tushdimi?). Bu tuzilma o'yin sikli (game loop) deyiladi:
flowchart LR
R["rAF: yangi kadr"] --> I["Kirish:<br/>bosilgan tugmalar"]
I --> U["update(dt):<br/>harakat, to'qnashuv, ball"]
U --> D["render():<br/>tozalash va chizish"]
D --> Q{"O'yin<br/>tugadimi?"}
Q -- "yo'q" --> R
Q -- "ha" --> E["Natija"]Nimaga qarang: update faqat holatni (raqamlarni) o'zgartiradi, render faqat holatni chizadi. Bu Holatdan UI chizish darsidagi "state → render" g'oyasining o'zi. Ikkalasi ajratilgani uchun o'yinni pauza qilish oson: update chaqirilmaydi, holat qotadi.
2.3 «Bahor»: somsani ushla
Endi o'yin. Savatni ← va → tugmalari yoki barmoq bilan (telefonda) boshqarasiz. Somsa savatga tushsa — ball, yerga tushsa — imkoniyat kamayadi. Uchta imkoniyat tugasa — o'yin tugaydi. Avval o'yin maydoniga bosing yoki Tab bilan unga o'ting — shunda klaviatura ishlaydi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { border: 1px solid #c8d3c8; touch-action: none;
max-width: 100%; }
canvas:focus-visible { outline: 3px solid #2e7d32; }
button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<canvas id="game" width="320" height="240" tabindex="0"
aria-label="O'yin: somsani savat bilan ushlang, strelkalar bilan">
</canvas>
<p>
<button id="pause" type="button">Pauza</button>
<button id="restart" type="button">Yangi o'yin</button>
<span id="score" aria-live="polite"></span>
</p>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreOut = document.getElementById("score");
const keys = new Set(); // hozir bosib turilgan tugmalar
let state;
let previous = null;
function newGame() {
state = {
basket: { x: 130, y: 214, w: 60, h: 16, speed: 260 },
items: [], // tushayotgan somsalar
spawnIn: 0, // keyingi somsagacha (soniya)
score: 0,
lives: 3,
paused: false,
};
showScore();
}
function showScore() {
scoreOut.textContent =
`Ball: ${state.score} · Imkoniyat: ${state.lives}`;
}
function update(dt) {
const b = state.basket;
if (keys.has("ArrowLeft")) b.x -= b.speed * dt;
if (keys.has("ArrowRight")) b.x += b.speed * dt;
b.x = Math.max(0, Math.min(canvas.width - b.w, b.x));
state.spawnIn -= dt;
if (state.spawnIn <= 0) {
const x = 10 + Math.random() * (canvas.width - 20);
state.items.push({ x, y: -10, speed: 90 + state.score * 6 });
state.spawnIn = 1; // har soniyada bitta somsa
}
const rest = [];
for (const item of state.items) {
item.y += item.speed * dt;
const caught = item.y >= b.y && item.y <= b.y + b.h
&& item.x >= b.x && item.x <= b.x + b.w;
if (caught) {
state.score++;
showScore();
} else if (item.y > canvas.height) {
state.lives--;
showScore();
} else {
rest.push(item); // hali tushib kelyapti
}
}
state.items = rest;
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const b = state.basket;
ctx.fillStyle = "#8d6e63";
ctx.fillRect(b.x, b.y, b.w, b.h);
ctx.fillStyle = "#e0a458";
for (const item of state.items) {
ctx.beginPath(); // somsa — uchburchak
ctx.moveTo(item.x, item.y - 10);
ctx.lineTo(item.x + 10, item.y + 8);
ctx.lineTo(item.x - 10, item.y + 8);
ctx.closePath();
ctx.fill();
}
}
function frame(time) {
if (previous === null) previous = time;
const dt = Math.min((time - previous) / 1000, 0.1);
previous = time;
if (!state.paused) {
update(dt);
render();
}
if (state.lives > 0) {
requestAnimationFrame(frame);
} else {
console.log(`O'yin tugadi. Ball: ${state.score}`);
}
}
canvas.addEventListener("keydown", (event) => {
if (event.key.startsWith("Arrow")) event.preventDefault();
keys.add(event.key);
});
canvas.addEventListener("keyup", (event) => keys.delete(event.key));
canvas.addEventListener("blur", () => keys.clear());
canvas.addEventListener("pointermove", (event) => {
state.basket.x = event.offsetX - state.basket.w / 2;
});
document.getElementById("pause").addEventListener("click", () => {
state.paused = !state.paused;
});
document.getElementById("restart").addEventListener("click", () => {
const wasOver = state.lives <= 0;
newGame();
if (wasOver) {
previous = null;
requestAnimationFrame(frame);
}
canvas.focus();
});
newGame();
requestAnimationFrame(frame);
</script>O'yinni tahlil qilamiz.
Kirish — holat sifatida. keydown da tugma keys to'plamiga (Set) qo'shiladi, keyup da olib tashlanadi. update esa har kadrda "hozir nima bosib turilgan?" deb so'raydi. Nega keydown ichida to'g'ridan-to'g'ri x -= 10 qilmaymiz? Chunki keydown ning takrorlanishi tizim sozlamasiga bog'liq: odatda birinchi bosishdan keyin qisqa pauza, keyin tez-tez takrorlanish. Harakat "turtinib" boshlanadi. Holat sifatida o'qilgan tugma esa savatni birinchi kadrdanoq silliq suradi.
blur da tozalash. O'yinchi → ni bosib turgan holda boshqa oynaga o'tsa, keyup o'yinga kelmaydi. Tugma "yopishib" qoladi — savat o'zi ketaveradi. blur hodisasida to'plamni tozalash bunga qarshi.
event.preventDefault(). Strelkalar sahifani aylantiradi. O'yin maydonida bu kerak emas — standart harakatni bekor qilamiz (Event obyekti va standart harakatlar).
To'qnashuv (collision). Somsa nuqtasi savat to'rtburchagi ichidami? Buning uchun to'rtta taqqoslash yetadi: y savatning tepasi va pasti orasida, x chap va o'ng cheti orasida. Bu eng oddiy to'qnashuv tekshiruvi. Ikki to'rtburchak uchun ham shunday: o'qlar bo'yicha "ustma-ust tushadimi?" deb tekshiriladi. O'yin dasturchilari uni AABB (axis-aligned bounding box — o'qlarga parallel chegaraviy quti) deyishadi.
Murakkablik. speed: 90 + state.score * 6 — har ushlangan somsa keyingisini tezroq qiladi. O'yin asta-sekin qiyinlashadi.
Bobur va Malika. Bobur faqat klaviatura bilan ishlaydi: tabindex="0" maydonni Tab bilan tanlanadigan qiladi, :focus-visible esa uni ko'rinadigan ramka bilan ajratadi. Ball aria-live="polite" maydonida — ekran o'quvchi o'zgarishni o'qiydi. Shuning uchun showScore har kadrda emas, faqat ball o'zgarganda chaqiriladi: aks holda Malika soniyasiga 60 marta bir xil gapni eshitardi. Lekin tan olaylik: harakatli o'yinni ko'zi ojiz mehmon o'ynay olmaydi. Bunday narsa saytning asosiy funksiyasi bo'lmasin.
Biz o'yinni Chrome 154 da sinadik: ← bosib turilganda savat 0,5 soniyada chap chetga yetdi va to'xtadi (x = 0). Hech narsa bosilmasa, uchta somsa yerga tushadi va konsolga O'yin tugadi. Ball: 0 chiqadi. Somsalar tasodifiy joydan tushgani uchun ba'zan bittasi o'zi savatga tushib qolishi mumkin — unda ball 1 bo'ladi.
Tekshirib ko'ring: Pauza paytida tab yashirilib, 10 soniyadan keyin qaytildi. Pauza olib tashlanganda somsalar bir zumda pastga "sakraydimi"?
Javob
Yo'q. Ikki himoya bor. Birinchisi: yashirin tabda rAF umuman chaqirilmaydi (requestAnimationFrame darsi). Ikkinchisi: dt Math.min(..., 0.1) bilan cheklangan — kadrlar orasida qancha vaqt o'tmasin, bir kadrda somsa ko'pi bilan 0,1 soniyalik yo'l yuradi. Pauza paytida ham previous yangilanib turadi, shuning uchun dt katta bo'lmaydi.
3. Rasmni qayta ishlash
3.1 Kichraytirish: yuklashdan oldin
Sardorning 4 000 pikselli surati. Menyu kartasida u ko'pi bilan 800 piksel enida ko'rinadi. Qolgan piksellar — mehmon telefoni yuklab oladigan, lekin ko'rmaydigan bayt.
Reja: faylni rasmga aylantiramiz, kichik canvas'ga kichraytirib chizamiz, canvas'ni yana faylga aylantiramiz. Kichraytirish uchun yangi metod kerak emas — o'tgan darsdagi drawImage(rasm, x, y, eni, bo'yi) rasmni berilgan o'lchamga siqadi.
Faylni rasmga aylantirishning zamonaviy usuli — createImageBitmap(fayl). U Blob yoki File ni oladi va Promise qaytaradi. Natija — ImageBitmap: chizishga tayyor, ochilgan (decode qilingan) rasm. <img> va load hodisasi kerak emas. Faylni o'qish darsidagi URL.createObjectURL + <img> usuli ham ishlaydi, lekin bu qisqaroq.
Natija oynasida o'z rasmingizni tanlashingiz mumkin. Yoki «Namuna rasm» ni bosing — u 2 400 × 1 600 pikselli "surat" yasab beradi:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { max-width: 100%; border: 1px solid #c8d3c8; }
button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<p><input type="file" id="file" accept="image/*"></p>
<p><button id="sample" type="button">Namuna rasm</button></p>
<p id="info">Rasm tanlang yoki namunani bosing.</p>
<canvas id="preview" width="0" height="0"></canvas>
<script>
const MAX_SIDE = 800;
const info = document.getElementById("info");
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Rasmni faylga aylantirib bo'lmadi"));
}, type, quality);
});
}
async function resizeImage(file) {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1,
MAX_SIDE / Math.max(bitmap.width, bitmap.height));
const width = Math.round(bitmap.width * scale);
const height = Math.round(bitmap.height * scale);
const canvas = document.getElementById("preview");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(bitmap, 0, 0, width, height);
console.log(`Asl: ${bitmap.width} × ${bitmap.height}`
+ ` → yangi: ${width} × ${height}`);
bitmap.close(); // xotirani bo'shatish
const blob = await canvasToBlob(canvas, "image/jpeg", 0.8);
const kb = (size) => Math.round(size / 1024);
info.textContent =
`Fayl: ${kb(file.size)} KB → ${kb(blob.size)} KB (JPEG, 0.8)`;
return blob;
}
// Namuna: katta "surat" yasab, uni JPEG faylga aylantiramiz
async function makeSamplePhoto() {
const big = document.createElement("canvas");
big.width = 2400;
big.height = 1600;
const ctx = big.getContext("2d");
const sky = ctx.createLinearGradient(0, 0, 0, 1600);
sky.addColorStop(0, "#81d4fa");
sky.addColorStop(1, "#fff3e0");
ctx.fillStyle = sky;
ctx.fillRect(0, 0, 2400, 1600);
ctx.fillStyle = "#ffffff"; // likopcha
ctx.beginPath();
ctx.arc(1200, 900, 600, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#e0a458"; // osh
ctx.beginPath();
ctx.arc(1200, 900, 420, 0, Math.PI * 2);
ctx.fill();
return canvasToBlob(big, "image/jpeg", 0.95);
}
document.getElementById("file").addEventListener("change",
(event) => {
const file = event.target.files[0];
if (file) resizeImage(file);
});
document.getElementById("sample").addEventListener("click",
async () => resizeImage(await makeSamplePhoto()));
</script>«Namuna rasm» ni bosing. Biz natija oynasida bosib sinadik — konsolga Asl: 2400 × 1600 → yangi: 800 × 533 chiqdi. Sahifada esa fayl hajmi qanchaga kamaygani ko'rinadi. Aniq KB rasmning o'ziga bog'liq, lekin piksellar soni 9 baravar kamaygani uchun fayl ham bir necha baravar kichrayadi.
Asosiy qator — masshtab:
Math.max(eni, bo'yi)— rasmning uzun tomoni. Unga qarab hisoblaymiz: rasm yotiq ham, tik ham bo'lishi mumkin.MAX_SIDE / uzun_tomon— necha baravar kichraytirish kerak: 800 / 2400 = 1/3.Math.min(1, ...)— rasm allaqachon kichik bo'lsa (masalan 500 piksel), uni kattalashtirmaymiz. Kattalashtirish sifat qo'shmaydi, faqat hajm.- Eni ham, bo'yi ham bitta son bilan ko'paytiriladi — nisbat saqlanadi, rasm cho'zilmaydi. 1600 / 3 = 533,3 —
Math.roundbilan 533.
Tekshirib ko'ring: Mehmon 600 × 400 lik rasm tanladi.
scalenimaga teng bo'ladi va yangi rasm qanday o'lchamda chiqadi?
Javob
MAX_SIDE / 600 = 800 / 600 ≈ 1,33. Lekin Math.min(1, 1.33) — 1. Demak, scale = 1 va rasm o'z o'lchamida qoladi: 600 × 400. Kichik rasm kattalashtirilmaydi — bu sifat qo'shmaydi, faqat faylni og'irlashtiradi.
Maslahat: Juda katta rasmni (masalan 8 000 → 400) bir qadamda kichraytirsangiz, ingichka chiziqlar "tishli" chiqishi mumkin. Brauzer silliqlash sifatini oshirish mumkin:
ctx.imageSmoothingQuality = "high". Natija yetarli bo'lmasa, bir necha bosqichda ikki baravardan kichraytiriladi.
3.2 Qirqish: rasmning bo'lagi
Profil rasmi (avatar) kvadrat bo'ladi. Surat esa to'rtburchak. Markazdan kvadrat bo'lak kesib olish kerak. Buning uchun drawImage ning 9 argumentli shakli bor:
ctx.drawImage(manba, sx, sy, sw, sh, dx, dy, dw, dh);Argumentlar ikki guruh:
sx, sy, sw, sh— manbadan qaysi to'rtburchak olinadi (source — manba): boshlanish nuqtasi va o'lchami.dx, dy, dw, dh— matoga qayerga va qanday o'lchamda chiziladi (destination — manzil).
Bir o'xshatish: qaychi bilan suratdan bo'lak kesib (s...), uni daftarga yopishtiryapsiz (d...). Yopishtirganda bo'lakni kattalashtirish yoki kichraytirish ham mumkin.
Markazdagi kvadrat hisobi:
- kvadrat tomoni — rasmning qisqa tomoni:
side = Math.min(eni, bo'yi); - chap chekka:
sx = (eni - side) / 2— ortiqcha joyning yarmi chapdan, yarmi o'ngdan qoladi; - yuqori chekka:
sy = (bo'yi - side) / 2.
Bo'sh joyni to'ldiring: 400 × 300 suratdan markaziy kvadrat qirqilsa, side = 300 va sx =
3.3 Piksellar bilan ishlash: getImageData
Ba'zan rasmning har pikseli bilan ishlash kerak: oq-qora qilish, yorqinlikni o'zgartirish, rang tanlash vositasi. Canvas piksellarni massiv qilib beradi:
ctx.getImageData(x, y, eni, bo'yi)— sohaning piksellarini ImageData obyekti qilib qaytaradi:width,heightvadata.data—Uint8ClampedArray: har piksel uchun to'rtta son, ketma-ket: qizil, yashil, ko'k va shaffoflik (R, G, B, A), har biri 0–255.ctx.putImageData(imageData, x, y)— o'zgartirilgan piksellarni matoga qaytaradi.
Demak, 200 × 200 rasmda data.length = 200 × 200 × 4 = 160 000. Birinchi piksel — data[0..3], ikkinchisi — data[4..7]. Sikl qadami doim 4.
"Clamped" so'zi — "qisilgan": massivga 300 yozsangiz, 255 saqlanadi; −20 yozsangiz, 0; 92,8 yozsangiz — yaxlitlanib 93. Rang hisoblarida chegaradan chiqib ketish xavfi yo'q.
Oq-qora qilish formulasi: uchala rangni bitta "yorqinlik" soniga aylantiramiz. Ko'z yashil rangni eng yorqin, ko'kni eng xira sezadi, shuning uchun rang ulushlari har xil: 0.299 × R + 0.587 × G + 0.114 × B. Bu televideniye standartidan kelgan klassik formula.
Endi qirqish va piksellar birga. Manba — 300 × 200 "surat": chapda qizil, o'rtada yashil doira, o'ngda ko'k:
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { border: 1px solid #c8d3c8; max-width: 100%; }
button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<canvas id="photo" width="300" height="200">Manba surat</canvas>
<canvas id="avatar" width="200" height="200">Avatar</canvas>
<p><button id="gray" type="button">Oq-qora qilish</button></p>
<script>
const photo = document.getElementById("photo");
const p = photo.getContext("2d");
p.fillStyle = "#c0392b";
p.fillRect(0, 0, 100, 200);
p.fillStyle = "#1565c0";
p.fillRect(200, 0, 100, 200);
p.fillStyle = "#2e7d32";
p.beginPath();
p.arc(150, 100, 70, 0, Math.PI * 2);
p.fill();
// Markazdan kvadrat qirqish
const side = Math.min(photo.width, photo.height);
const sx = (photo.width - side) / 2;
const sy = (photo.height - side) / 2;
const avatar = document.getElementById("avatar");
const ctx = avatar.getContext("2d");
ctx.drawImage(photo, sx, sy, side, side, 0, 0, 200, 200);
console.log(`Qirqildi: x = ${sx}, y = ${sy}, tomoni ${side}`);
function toGrayscale(ctx) {
const { width, height } = ctx.canvas;
const image = ctx.getImageData(0, 0, width, height);
const d = image.data;
for (let i = 0; i < d.length; i += 4) {
const gray = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
d[i] = d[i + 1] = d[i + 2] = gray; // A (d[i + 3]) o'zgarmaydi
}
ctx.putImageData(image, 0, 0);
return image;
}
document.getElementById("gray").addEventListener("click", () => {
const before = ctx.getImageData(100, 100, 1, 1).data.join(",");
const image = toGrayscale(ctx);
const after = ctx.getImageData(100, 100, 1, 1).data.join(",");
console.log(`data.length: ${image.data.length}`);
console.log(`Markaz: ${before} → ${after}`);
});
</script>Konsolda:
Qirqildi: x = 50, y = 0, tomoni 200O'ngdagi avatar — suratning o'rta qismi: qizil va ko'k chetlardan 50 pikseldan, yashil doira esa to'liq. «Oq-qora qilish» bosilgandan keyin konsolga yana ikki qator qo'shiladi:
data.length: 160000
Markaz: 46,125,50,255 → 93,93,93,255Biz buni natija oynasida bosib tekshirdik. Hisobni ham tekshiring: #2e7d32 — bu (46, 125, 50). 0.299 × 46 + 0.587 × 125 + 0.114 × 50 = 92,83 — massiv uni 93 ga yaxlitladi. Shaffoflik (255) o'zgarmadi.
Diqqat: Canvas'ni ko'p o'qisangiz (har kadrda
getImageData), kontekstni shunday oling:getContext("2d", { willReadFrequently: true }). Bu brauzerga "piksellarni tez-tez o'qiyman" deydi va o'qish tezlashadi. Bayroqsiz ko'p o'qisangiz, Chrome konsolda ogohlantiradi:Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true— "getImageData bilan ko'p o'qish willReadFrequently bilan tezroq".
CSS'dagi filtrlarni canvas'da ham ishlatish mumkin: ctx.filter = "grayscale(1)" dan keyin chizilgan hamma narsa oq-qora. Bu qisqaroq, lekin Safari hali qo'llamaydi (web-features 3.40.0 bo'yicha Baseline emas). getImageData esa hamma joyda ishlaydi va istalgan formulani yozishga imkon beradi.
3.4 Ifloslangan canvas
O'tgan darsda va'da qilgan edik. Canvas'ga boshqa saytdan kelgan rasm chizilsa, canvas "ifloslangan" (tainted) bo'ladi. Shundan keyin piksel o'qish va eksport taqiqlanadi. Biz Chrome 154 da sinadik: localhost:5601 dagi sahifa 127.0.0.1:5602 dagi rasmni chizdi. Brauzer uchun bu ikki xil origin:
Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.Tarjimasi: "getImageData ni bajarib bo'lmadi: canvas boshqa origin ma'lumoti bilan ifloslangan". toBlob va toDataURL da esa: Tainted canvases may not be exported — "ifloslangan canvas'ni eksport qilib bo'lmaydi".
Nega bunday qoida? Tasavvur qiling: yomon sayt sizning bankingiz sahifasidagi rasmni (masalan, hisob ko'chirmasi) o'z canvas'iga chizadi va piksellarni o'qib, serverga yuboradi. Siz bankka kirgan bo'lganingiz uchun brauzer rasmni cookie bilan olib keladi. Bu qoida aynan shunday o'g'irlikning oldini oladi — CORS darsidagi g'oyaning rasm uchun ko'rinishi.
Rasm egasi ruxsat bersa, cheklov olinadi. Ikki shart kerak:
- Rasm serveri
Access-Control-Allow-Originsarlavhasini yuborishi. - Sizning kodingiz rasmni CORS rejimida so'rashi:
img.crossOrigin = "anonymous"(srcdan oldin).
Ikkalasi bajarilganda sinovimizda uchala amal ham ishladi. Bitta shart yetmasa — yana xato. Foydalanuvchi o'zi tanlagan fayl (<input type="file">) va o'zingiz chizgan narsa canvas'ni hech qachon ifloslantirmaydi.
4. Eksport: canvas'dan faylga
4.1 toBlob va toDataURL
Canvas'dagi rasmni faylga aylantirishning ikki yo'li bor:
| Metod | Natija | Qachon |
|---|---|---|
canvas.toBlob(callback, tur, sifat) |
Blob — callback'ga keladi |
Deyarli doim |
canvas.toDataURL(tur, sifat) |
"data:image/png;base64,..." satri |
Juda kichik rasm, tez sinov |
toDataURL sinxron va qulay, lekin u rasmni base64 matnga aylantiradi — asl fayldan taxminan uchdan bir katta. Katta rasmda bu megabaytlab satr, sahifa qotadi. toBlob asinxron ishlaydi va baytlarni xotirada ixcham saqlaydi.
toBlob Promise qaytarmaydi — callback oladi. Shuning uchun uni bir marta Promise'ga o'raymiz (Promise yordamchilari va promisify darsidagi usul). Bu funksiya "Kichraytirish" misolida bor edi:
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Rasmni faylga aylantirib bo'lmadi"));
}, type, quality);
});
}Nega blob null bo'lishi mumkin? Canvas o'lchami 0 bo'lsa — eksport qiladigan piksel yo'q. Biz sinadik: width = 0 da callback'ga null keldi, toDataURL() esa "data:," qaytardi.
4.2 Format va sifat
Ikkinchi argument — format (MIME turi), uchinchisi — sifat (0 dan 1 gacha, faqat JPEG va WebP uchun):
"image/png"— standart. Siqish yo'qotishsiz: har piksel aynan saqlanadi. Matn, chizma, chek, logotip uchun."image/jpeg"— fotosuratlar uchun. Sifatni pasaytirsangiz, ko'z sezmaydigan detallar tashlanadi va fayl kichrayadi. Shaffoflik yo'q — shaffof joylar qora bo'ladi."image/webp"— zamonaviy format: fotoda JPEG darajasida, shaffoflikni ham saqlaydi.
Brauzer qo'llamaydigan format so'ralsa (masalan "image/gif"), xato chiqmaydi — PNG qaytadi. Biz sinadik: Chrome 154 da "image/gif" va "image/avif" so'rovlariga blob.type — image/png. Shuning uchun natijaning blob.type sini tekshirish foydali.
Qancha farq qiladi? Biz Chrome 154 da 1 200 × 900 pikselli sinov rasmini (fotoga o'xshash: gradient, minglab dog' va shovqin) har xil formatda saqladik:
- PNGyo'qotishsiz2 392 KB
- JPEG 0.92standart sifat517 KB
- JPEG 0.8296 KB
- WebP 0.8300 KB
- JPEG 0.5143 KB
- 400×300 JPEG 0.8kichraytirilgan44 KB
Manba: O'lchandi: Chrome 154, canvas.toBlob, 1200×900 sinov rasmi, 2026-10-05
Nimaga qarang: fotoga o'xshash rasm uchun PNG — eng og'ir, sakkiz baravar katta. Sifatni 0.92 dan 0.8 ga tushirish faylni deyarli ikki baravar kichraytirdi. Lekin eng katta yutuq — kichraytirish: 400 × 300 rasm 517 KB o'rniga 44 KB. Shuning uchun tartib doim: avval o'lcham, keyin sifat. Sifatni berilmasa, Chrome JPEG uchun 0.92 ishlatadi — sinovda argumentsiz va 0.92 bilan hajm bir xil chiqdi.
Bu raqamlar faqat bitta rasm uchun. Oddiy rangli chizmada (chek, grafik) aksincha — PNG kichikroq va tiniqroq bo'ladi.
4.3 «Bahor» cheki: chizish va yuklab olish
Mehmon chekini rasm qilib saqlaydi. Chekni 2 baravar zichlikda chizamiz (SCALE = 2) — Telegram'da kattalashtirganda ham tiniq bo'lsin. Chiziqcha chiziq uchun yangi metod — setLineDash([uzunlik, oraliq]):
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { border: 1px solid #c8d3c8; }
button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<canvas id="receipt" role="img"
aria-label="Chek 1042: jami 108 000 so'm"></canvas>
<p><button id="download" type="button">PNG yuklab olish</button>
<span id="size"></span></p>
<script>
const order = {
id: 1042,
table: 5,
time: "05.10.2026 12:30",
items: [
{ name: "Osh", qty: 2, price: 35000 },
{ name: "Lag'mon", qty: 1, price: 28000 },
{ name: "Ko'k choy", qty: 2, price: 5000 },
],
};
const SCALE = 2;
const WIDTH = 280;
const height = 150 + order.items.length * 24;
const canvas = document.getElementById("receipt");
canvas.width = WIDTH * SCALE;
canvas.height = height * SCALE;
canvas.style.width = `${WIDTH}px`;
const ctx = canvas.getContext("2d");
ctx.scale(SCALE, SCALE);
function formatSom(n) {
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function dashedLine(y) {
ctx.beginPath();
ctx.setLineDash([4, 3]);
ctx.moveTo(16, y);
ctx.lineTo(WIDTH - 16, y);
ctx.stroke();
ctx.setLineDash([]); // keyingi chiziqlar uchun qaytarish
}
ctx.fillStyle = "#ffffff"; // PNG shaffof bo'lmasin
ctx.fillRect(0, 0, WIDTH, height);
ctx.fillStyle = "#1b1b1b";
ctx.strokeStyle = "#9e9e9e";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.font = "700 18px system-ui, sans-serif";
ctx.fillText("«Bahor» oshxonasi", WIDTH / 2, 26);
ctx.font = "13px system-ui, sans-serif";
ctx.fillText(`Buyurtma #${order.id} · Stol ${order.table}`,
WIDTH / 2, 48);
ctx.fillText(order.time, WIDTH / 2, 66);
dashedLine(82);
let y = 100;
let total = 0;
for (const item of order.items) {
const sum = item.qty * item.price;
total += sum;
ctx.textAlign = "left";
ctx.fillText(`${item.name} × ${item.qty}`, 16, y);
ctx.textAlign = "right";
ctx.fillText(formatSom(sum), WIDTH - 16, y);
y += 24;
}
dashedLine(y - 6);
ctx.font = "700 15px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.fillText("Jami", 16, y + 14);
ctx.textAlign = "right";
ctx.fillText(`${formatSom(total)} so'm`, WIDTH - 16, y + 14);
console.log(`Chek #${order.id}: jami ${formatSom(total)} so'm`);
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Rasmni faylga aylantirib bo'lmadi"));
}, type, quality);
});
}
document.getElementById("download").addEventListener("click",
async () => {
const blob = await canvasToBlob(canvas, "image/png");
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `bahor-chek-${order.id}.png`;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
document.getElementById("size").textContent =
`${Math.round(blob.size / 1024)} KB`;
console.log(`Yuklab olindi: ${link.download} (${blob.type})`);
});
</script>Konsolda:
Chek #1042: jami 108 000 so'm«PNG yuklab olish» bosilganda konsolga Yuklab olindi: bahor-chek-1042.png (image/png) qo'shiladi va brauzer faylni saqlaydi. Natija oynasi yuklab olishga ruxsat beradi. Yuklab berish qismi — File va Blob darsidagi usulning aynan o'zi: Blob → vaqtinchalik blob: URL → <a download> bosiladi → URL bo'shatiladi. Yangi narsa faqat Blob ning manbasi: endi u matndan emas, canvas'dan.
Uchta kichik detal:
- Oq fon.
fillRectbilan fon chizilmasa, PNG shaffof bo'ladi. Telegram'ning qorong'i mavzusida qora matn qora fonda ko'rinmay qoladi. setLineDash([]). Chiziqcha rejim ham kontekst holati — qaytarilmasa, keyingi hamma chiziqlar chiziqcha bo'ladi.save/restoreham ishlatsa bo'ladi.- Pul hisobi. Narxlar butun so'mda — kasr yo'q, yaxlitlash xatosi ham yo'q.
Tekshirib ko'ring: Chekni
"image/jpeg"bilan saqlab, oq fon chizish qatorini olib tashlasak, rasm qanday chiqadi?
Javob
Fon qora bo'ladi va qora matn ko'rinmay qoladi. JPEG shaffoflikni saqlay olmaydi: shaffof piksellar (0, 0, 0, 0) shaffoflik olib tashlanib, qora (0, 0, 0) bo'lib qoladi. Chek kabi chizmalar uchun PNG va doim aniq fon tanlang.
5. OffscreenCanvas
Ba'zan canvas sahifada ko'rinishi shart emas: masalan, faqat kichraytirish uchun. Bunday holatda document.createElement("canvas") o'rniga new OffscreenCanvas(eni, bo'yi) ishlatish mumkin. U DOM'ga bog'lanmagan canvas: konteksti va metodlari bir xil. Farqlari:
- Eksport —
convertToBlob({ type, quality }), va u Promise qaytaradi. O'rov yozish shart emas. - U Web Worker ichida ham ishlaydi. Worker — sahifadan alohida thread'da bajariladigan skript. Og'ir rasm ishlovini asosiy thread'dan chiqarib, sahifani qotirmaslik uchun aynan shu kerak. Worker'larni uch dars keyin, Web Workers darsida o'rganamiz, hozir bilish shart emas.
- Sahifadagi
<canvas>ni ham worker'ga "berish" mumkin:canvas.transferControlToOffscreen(). Shunda chizish butunlay boshqa thread'da bo'ladi. Bu ham o'sha darsda.
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
</style>
<img id="banner" alt="Aksiya: ko'k choy bepul">
<script type="module">
const off = new OffscreenCanvas(320, 80);
const ctx = off.getContext("2d");
ctx.fillStyle = "#2e7d32";
ctx.fillRect(0, 0, 320, 80);
ctx.fillStyle = "#ffffff";
ctx.font = "700 20px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Aksiya: ko'k choy bepul", 160, 40);
const blob = await off.convertToBlob({ type: "image/webp" });
document.getElementById("banner").src = URL.createObjectURL(blob);
console.log(`${blob.type}, ${off.width} × ${off.height}`);
</script>Konsolda:
image/webp, 320 × 80Canvas sahifada yo'q, lekin uning natijasi oddiy <img> da ko'rinadi. await yuqori darajada ishlatilgani uchun skript — type="module" (top-level await). OffscreenCanvas 2023-yil martdan beri hamma asosiy brauzerlarda bor (web-features: Baseline 2023).
6. Ko'p uchraydigan xatolar
6.1 Kadrni tozalamaslik
Har kadr oldingisi ustiga chiziladi — "iz" qoladi. Tuzatish: kadr boshida ctx.clearRect(0, 0, canvas.width, canvas.height) yoki butun fonni qayta chizish.
6.2 Tugma "yopishib qolishi"
keyup kelmay qolsa (oyna almashtirilganda), harakat to'xtamaydi. Tuzatish: blur hodisasida keys.clear().
6.3 Ifloslangan canvas
Boshqa domendagi rasm chizilgach getImageData yoki toBlob — SecurityError. Tuzatish: server CORS sarlavhasini yuborsin va img.crossOrigin = "anonymous" ni src dan oldin yozing; bo'lmasa, rasmni o'z serveringizdan bering.
6.4 Shaffof fonni JPEG qilish
Shaffof joylar qora bo'lib chiqadi. Tuzatish: avval oq (yoki kerakli) fon bilan fillRect, keyin chizish; yoki PNG/WebP tanlang.
6.5 toDataURL ni hamma joyda ishlatish
Katta rasmning base64 satri megabaytlab bo'ladi; uni localStorage ga yozish kvotani bir zumda to'ldiradi (localStorage darsidagi QuotaExceededError). Tuzatish: toBlob va kerak bo'lsa IndexedDB.
6.6 Piksellarni bittalab o'qish
// ❌ har piksel uchun alohida getImageData — juda sekin
for (let y = 0; y < 600; y++) {
for (let x = 0; x < 800; x++) {
const pixel = ctx.getImageData(x, y, 1, 1).data;
}
}480 000 ta chaqiruv, har biri canvas'dan nusxa oladi. Tuzatish: bitta getImageData bilan butun sohani oling va data massivi bo'ylab yuring.
7. Mashqlar
1-mashq (oson): Sakrayotgan koptok
320 × 200 canvas'da radiusi 12 bo'lgan koptok vx = 150, vy = 110 px/soniya tezlikda harakatlansin. Devorga tegsa — qaytsin: o'ng yoki chap devorda vx = -vx, yuqori yoki pastkisida vy = -vy. Har kadrda tozalash, yangilash, chizish; dt bilan.
Ishora: koptok markazi x bo'lsa, o'ng devorga tegish sharti — x + radius > canvas.width.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
canvas { border: 1px solid #c8d3c8; }
</style>
<canvas id="box" width="320" height="200">Sakrayotgan koptok</canvas>
<script>
const canvas = document.getElementById("box");
const ctx = canvas.getContext("2d");
const ball = { x: 60, y: 50, r: 12, vx: 150, vy: 110 };
let previous = null;
function update(dt) {
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
if (ball.x + ball.r > canvas.width || ball.x - ball.r < 0) {
ball.vx = -ball.vx;
}
if (ball.y + ball.r > canvas.height || ball.y - ball.r < 0) {
ball.vy = -ball.vy;
}
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fillStyle = "#e0a458";
ctx.fill();
}
function frame(time) {
if (previous === null) previous = time;
const dt = Math.min((time - previous) / 1000, 0.1);
previous = time;
update(dt);
render();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>Bu tuzilma — o'yin siklining eng kichik ko'rinishi: update faqat raqamlarni, render faqat chizishni bajaradi. Bitta nozik joy: tez koptok bir kadrda devordan biroz "o'tib" ketishi mumkin. Mukammal o'yinlarda koptok devor chetiga qaytarib qo'yiladi: ball.x = canvas.width - ball.r.
2-mashq (o'rta): Suv belgisi
Menyu suratlariga «Bahor» belgisi (watermark) qo'shilsin. "Qirqish" misolidagi kabi 300 × 200 "surat" chizing (istalgan shakl). Uning o'ng pastki burchagiga globalAlpha = 0.6 bilan oq «Bahor» yozuvini qo'ying (textAlign = "right", textBaseline = "bottom", chetlardan 10 px). «JPEG yuklab olish» tugmasi rasmni bahor-menyu.jpg nomi bilan, 0.85 sifatda yuklab bersin. Konsolga blob.type ni chiqaring.
Ishora: globalAlpha ni yozuvdan keyin 1 ga qaytaring. Yuklab berish — "«Bahor» cheki" misolidagi to'rt qator.
Yechim
<style>
body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
button { font: inherit; padding: 0.3rem 0.7rem; }
</style>
<canvas id="photo" width="300" height="200">Menyu surati</canvas>
<p><button id="save" type="button">JPEG yuklab olish</button></p>
<script>
const canvas = document.getElementById("photo");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#8d6e63";
ctx.fillRect(0, 0, 300, 200);
ctx.fillStyle = "#e0a458";
ctx.beginPath();
ctx.arc(150, 100, 70, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 0.6;
ctx.fillStyle = "#ffffff";
ctx.font = "700 20px system-ui, sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "bottom";
ctx.fillText("«Bahor»", canvas.width - 10, canvas.height - 10);
ctx.globalAlpha = 1;
document.getElementById("save").addEventListener("click", () => {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "bahor-menyu.jpg";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
console.log(blob.type);
}, "image/jpeg", 0.85);
});
</script>Tugma bosilganda konsolda image/jpeg chiqadi (biz natija oynasida sinadik). textAlign = "right" va textBaseline = "bottom" bilan berilgan nuqta yozuvning o'ng pastki burchagi bo'ladi — shuning uchun chetdan 10 px hisobi oddiy.
3-mashq (qiyin): Hajm chegarasigacha siqish
Telegram bot rasmni 200 KB dan oshmasa qabul qiladi deylik. compressToLimit(canvas, maxBytes) funksiyasini yozing. U JPEG sifatini 0.9 dan boshlab 0.1 qadam bilan pasaytirsin va hajm maxBytes dan kichik bo'lgan birinchi natijani qaytarsin: { blob, quality }. 0.3 da ham sig'masa — null. Uni "Kichraytirish" misolidagi makeSamplePhoto dan olingan rasmning 1 200 pikselli nusxasida sinang.
Ishora: siklda await canvasToBlob(...). Sifatni yaxlitlang: Math.round(q * 10) / 10 — aks holda 0.1 qadamlar 0.7999999 beradi (Arifmetik operatorlar darsidagi 0.1 + 0.2 kasr xatosi).
Yechim
function canvasToBlob(canvas, type, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Rasmni faylga aylantirib bo'lmadi"));
}, type, quality);
});
}
async function compressToLimit(canvas, maxBytes) {
for (let q = 0.9; q >= 0.3; q = Math.round((q - 0.1) * 10) / 10) {
const blob = await canvasToBlob(canvas, "image/jpeg", q);
if (blob.size <= maxBytes) return { blob, quality: q };
}
return null; // past sifatda ham sig'madi — o'lchamni kamaytiring
}
// canvas — namuna rasmning 1 200 px li nusxasi chizilgan mato
// ("Kichraytirish" misolidagi resizeImage, MAX_SIDE = 1200)
const result = await compressToLimit(canvas, 200 * 1024);
const kb = (size) => Math.round(size / 1024);
console.log(result
? `Sifat ${result.quality}: ${kb(result.blob.size)} KB`
: "Sig'madi");Sikl har qadamda yangi JPEG yasaydi — bu og'ir ish, lekin ko'pi bilan 7 marta. Agar 0.3 da ham sig'masa, sifatni yana pasaytirish rasmni xunuk qiladi: to'g'ri yo'l — o'lchamni kichraytirish ("Format va sifat" grafigidagi xulosa). Tezroq usul ham bor — ikkilik qidiruv (0.5 dan boshlab oraliqni ikkiga bo'lish); uni algoritmlar qismida (14) o'rganamiz.
4-mashq: Amaliy tajriba — vazifalar ro'yxati rasm bo'lib
vazifalar ro'yxatini rasm qilib do'stga yuborish istagi bor. Kanon kodga tegmasdan tajriba qiling: kurs/mashqlar/13/16-royxat-rasm/index.html — massivda besh vazifa ({ id, matn, bajarildi }). Ularni 360 px enli canvas'ga oq fonda, har qatorga bittadan chizing: bajarilganlari oldida ✓ va kulrang, qolganlari oldida ○. «Rasm qilib saqlash» tugmasi vazifalar.png ni yuklab bersin. Canvas balandligi vazifalar soniga qarab hisoblansin. Live Server bilan oching.
Yechim
<canvas id="rasm"></canvas>
<p><button id="saqla" type="button">Rasm qilib saqlash</button></p>
<script>
const vazifalar = [
{ id: 1, matn: "Non olish", bajarildi: true },
{ id: 2, matn: "Hisobot yozish", bajarildi: false },
{ id: 3, matn: "Mijozga qo'ng'iroq", bajarildi: true },
{ id: 4, matn: "Menyuni yangilash", bajarildi: false },
{ id: 5, matn: "Kassa hisobi", bajarildi: false },
];
const SCALE = 2;
const WIDTH = 360;
const ROW = 32;
const height = 56 + vazifalar.length * ROW;
const canvas = document.getElementById("rasm");
canvas.width = WIDTH * SCALE;
canvas.height = height * SCALE;
canvas.style.width = `${WIDTH}px`;
const ctx = canvas.getContext("2d");
ctx.scale(SCALE, SCALE);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, WIDTH, height);
ctx.textBaseline = "middle";
ctx.fillStyle = "#1b1b1b";
ctx.font = "700 18px system-ui, sans-serif";
ctx.fillText("Vazifalarim", 16, 28);
ctx.font = "16px system-ui, sans-serif";
vazifalar.forEach((v, i) => {
const y = 56 + i * ROW + ROW / 2;
ctx.fillStyle = v.bajarildi ? "#9e9e9e" : "#1b1b1b";
ctx.fillText(`${v.bajarildi ? "✓" : "○"} ${v.matn}`, 16, y);
});
document.getElementById("saqla").addEventListener("click", () => {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "vazifalar.png";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 0);
}, "image/png");
});
</script>Balandlik formulasi 56 + soni × 32 — sarlavha uchun joy va har qatorga 32 px. Vazifa qo'shilsa, rasm o'zi uzayadi. Matn — oddiy satr, textContent kabi xavfsiz: canvas HTML'ni talqin qilmaydi, shuning uchun vazifa matnidagi <script> ham faqat harflar bo'lib chiziladi.
git add 13/16-royxat-rasm/index.html
git commit -m "13/16: vazifalar ro'yxatini PNG rasm qilib saqlash"8. Real ishda
- Rasm yuklash formalari. Ijtimoiy tarmoqlar, marketplace'lar va CRM'lar suratni brauzerda kichraytirib, keyin serverga yuboradi — trafik va server diski tejaladi. Avatar uchun qirqish oynasi ham canvas'da ishlaydi. Faylni serverga yuborish — Fayl yuklash va progress darsida.
- Grafik va chek generatsiyasi. Hisobotni PNG qilib eksport qilish, QR kodli chek, sertifikat —
toBlob+ yuklab berish. - Brauzer o'yinlari. Phaser va PixiJS kabi o'yin kutubxonalari aynan shu sikl ustiga qurilgan: kirish,
update(dt),render, to'qnashuv. - Rasm muharrirlari. Filtrlar, yorqinlik, "rang tanlash" vositasi —
getImageDatapiksellari. Og'ir filtrlar worker ichidaOffscreenCanvasbilan ishlaydi. - Intervyu. "Rasmni yuklashdan oldin qanday kichraytirasiz?", "Tainted canvas nima?", "
toBlobvatoDataURLfarqi?" — tez-tez so'raladi.
Xulosa
- Animatsiya — har kadrda tozalash, yangilash va chizish; kadr — rAF, tezlik — delta time.
- O'yin sikli: kirish (bosilgan tugmalar to'plami) →
update(dt)→render(); holat va chizish alohida. - Kichraytirish va qirqish —
drawImage: 5 argumentli (o'lcham) va 9 argumentli (manbadan bo'lak). - Piksellar —
getImageData: har piksel 4 son (RGBA),Uint8ClampedArray; qaytarish —putImageData. - Eksport —
toBlob(callback, tur, sifat); avval o'lcham, keyin sifat. Boshqa origin rasmi canvas'ni ifloslantiradi. OffscreenCanvas— DOM'siz canvas,convertToBlobPromise qaytaradi va worker'da ishlaydi.
Keyingi dars: Audio va videoni JS'dan boshqarish — <audio> va <video> ni JavaScript'dan boshqaramiz: play() Promise'i, pauza, vaqtni surish, tovush va tezlik, media hodisalari, avtomatik ijro siyosati va subtitrlar.
Manbalar
- HTML Living Standard: "The canvas element" (
toBlob, tainted canvas), "OffscreenCanvas" — html.spec.whatwg.org - MDN: "Pixel manipulation with canvas",
createImageBitmap(),HTMLCanvasElement.toBlob()— developer.mozilla.org - MDN: "Allowing cross-origin use of images and canvas" — developer.mozilla.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!