جاوااسکریپت - تغییر اندازه بوم HTML5 برای تناسب با پنجره


چگونه میتوانم عنصر HTML5 <canvas> را بهطور خودکار برای قرار گرفتن در صفحه مقیاسبندی کنم؟

برای مثال، من میتوانم یک <div> را با تنظیم ویژگیهای height و width روی 100٪ مقیاس کنم، اما یک <canvas> مقیاس نمیشود، اینطور نیست؟

پاسخ شماره ۱

من معتقدم که یک راه حل شیک برای این پیدا کردهام:

جاوااسکریپت

/* important! for alignment, you should make things
 * relative to the canvas' current width/height.
 */
function draw() {
  var ctx = (a canvas context);
  ctx.canvas.width  = window.innerWidth;
  ctx.canvas.height = window.innerHeight;
  //...drawing code...
}

سیاساس

html, body {
  width:  100%;
  height: 100%;
  margin: 0;
}

تاکنون برای من هیچ تأثیر منفی بزرگی در عملکرد نداشته است.

پاسخ شماره ۲

راه حل زیر برای من بهترین کار را کرد. از آنجایی که من نسبتاً تازهکار در برنامهنویسی هستم، دوست دارم تأیید بصری داشته باشم که چیزی به شکلی که انتظار دارم کار میکند. من آن را در سایت زیر پیدا کردم: http://htmlcheats.com/html/resize-the-html5-canvas-dyamically/

کد اینجا است:

(function() {
  var
    // Obtain a reference to the canvas element using its id.
    htmlCanvas = document.getElementById('c'),
    // Obtain a graphics context on the canvas element for drawing.
    context = htmlCanvas.getContext('2d');

  // Start listening to resize events and draw canvas.
  initialize();

  function initialize() {
    // Register an event listener to call the resizeCanvas() function 
    // each time the window is resized.
    window.addEventListener('resize', resizeCanvas, false);
    // Draw canvas border for the first time.
    resizeCanvas();
  }

  // Display custom canvas. In this case it's a blue, 5 pixel 
  // border that resizes along with the browser window.
  function redraw() {
    context.strokeStyle = 'blue';
    context.lineWidth = '5';
    context.strokeRect(0, 0, window.innerWidth, window.innerHeight);
  }

  // Runs each time the DOM window resize event fires.
  // Resets the canvas dimensions to match window,
  // then draws the new borders accordingly.
  function resizeCanvas() {
    htmlCanvas.width = window.innerWidth;
    htmlCanvas.height = window.innerHeight;
    redraw();
  }
})();
html,
body {
  width: 100%;
  height: 100%;
  margin: 0px;
  border: 0;
  overflow: hidden;
  /*  Disable scrollbars */
  display: block;
  /* No floating content on sides */
}
<canvas id='c' style='position:absolute; left:0px; top:0px;'></canvas>

حاشیه آبی به شما لبه بوم تغییر اندازه را نشان میدهد و همیشه در کنار لبه پنجره قرار دارد و در هر چهار طرف قابل مشاهده است، که در مورد برخی از پاسخهای دیگر بالا چنین نبود. امیدوارم مفید باشد.

پاسخ شماره ۳

پاسخ ۲۰۲۲

روش پیشنهادی در سال ۲۰۲۲ برای بررسی اینکه آیا یک عنصر تغییر اندازه داده است، استفاده از ResizeObserver است

const observer = new ResizeObserver(myResizeTheCanvasFn);
observer.observe(someCanvasElement);

این بهتر از window.addEventListener('resize', myResizeTheCanvasFn) یا onresize=myResizeTheCanvasFn است چون همه حالتهای تغییر اندازه بوم را مدیریت میکند، حتی زمانی که به تغییر اندازه پنجره مربوط نباشد.

به طور مشابه، استفاده از window.innerWidth و window.innerHeight بیمعنی است. شما اندازه خود بوم را میخواهید، نه اندازه پنجره. به این صورت، فرقی نمیکند بوم را کجا قرار دهید، اندازه صحیح برای وضعیت دریافت خواهید کرد و نیازی به نوشتن مجدد کد اندازهگیری خود نخواهید داشت.

در مورد اینکه چگونه بوم را پر کند تا پنجره را

html, body {
  height: 100%;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;   /* this is IMPORTANT! */
}

دلیلی که نیاز به display: block دارید این است که به طور پیشفرض، عنصر canvas به صورت inline است که به این معنی است که فضای اضافی در انتها دارد. بدون display: block، یک نوار اسکرول خواهید داشت. بسیاری از افراد مشکل نوار اسکرول را با اضافه کردن overflow: hidden به بدنه سند حل میکنند، اما این فقط پنهان کردن این واقعیت است که CSS مربوط به canvas به درستی تنظیم نشده بود. بهتر است با رفع باگ (تنظیم canvas به display: block) مشکل را حل کنید تا با overflow: hidden آن را پنهان کنید.

نمونه کامل

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
let width = 300;
let height = 150;

const observer = new ResizeObserver((entries) => {
  width = canvas.clientWidth;
  height = canvas.clientHeight;
});
observer.observe(canvas)

// not import but draw something just to showcase

const hsla = (h, s, l, a) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`;

function render(time) {
  canvas.width = width;
  canvas.height = height;

  ctx.save();
  ctx.translate(width / 2, height / 2);
  ctx.rotate(time * 0.0001);
  
  const range = Math.max(width, height) * 0.8;
  const size = 64 + Math.sin(time * 0.001) * 50;
  for (let i = 0; i < range; i += size) {
    ctx.fillStyle = hsla(i / range * 0.3 + time * 0.0001, 1, 0.5, 1);
    ctx.fillRect( i, -range, size, range * 2);
    ctx.fillRect(-i, -range, size, range * 2);
  }
  
  ctx.restore();

  requestAnimationFrame(render)
}
requestAnimationFrame(render)
html, body {
  height: 100%;
  margin: 0;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;
}
<canvas></canvas>

شما ممکن است بپرسید، چرا ما عرض و ارتفاع را در بازخورد ResizeObserver ذخیره میکنیم به جای اینکه مستقیماً اندازه بوم را تنظیم کنیم. دلیل آن این است که مشخصات HTML5 میگوید که بازخوردهای ResizeObserver بعد از بازخوردهای requestAnimationFrame اما قبل از ترکیب بندی رخ میدهند. تنظیم عرض یا ارتفاع بوم باعث پاک شدن بوم میشود. این با هم به این معنی است که اگر کاربر اندازه را تغییر دهد، ترتیب عملیات چنین خواهد بود

  1. رسم (requestAnimationFrame)
  2. پاک کردن (ResizeObserver)
  3. ترکیبی

این که در نهایت به چشمک زدن منجر میشود. شما میتوانید آن را در این مثال ببینید که اندازه بوم را مستقیماً داخل resizeobserver تنظیم میکند. آن را اجرا کنید، «تمام صفحه» را انتخاب کنید، سپس اندازه پنجره مرورگر را تغییر دهید.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const observer = new ResizeObserver((entries) => {
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
});
observer.observe(canvas)

// not import but draw something just to showcase

const hsla = (h, s, l, a) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`;

function render(time) {
  const {width, height} = canvas;

  ctx.clearRect(0, 0, width, height);
  ctx.save();
  ctx.translate(width / 2, height / 2);
  ctx.rotate(time * 0.0001);
  
  const range = Math.max(width, height) * 0.8;
  const size = 64 + Math.sin(time * 0.001) * 50;
  for (let i = 0; i < range; i += size) {
    ctx.fillStyle = hsla(i / range * 0.3 + time * 0.0001, 1, 0.5, 1);
    ctx.fillRect( i, -range, size, range * 2);
    ctx.fillRect(-i, -range, size, range * 2);
  }
  
  ctx.restore();

  requestAnimationFrame(render)
}
requestAnimationFrame(render)
html, body {
  height: 100%;
  margin: 0;
}
canvas {
  width: 100%;
  height: 100%;
  display: block;
}
<canvas></canvas>

اگر نیاز دارید چندین بوم را مدیریت کنید، باید روشی برای ذخیره اندازه هر کدام داشته باشید. یکی از راهها استفاده از WeakMap است.

const canvases = document.querySelectorAll('canvas');
const canvasToSize = new WeakMap();

function recordCanvasSize(canvas) {
  canvasToSize.set(canvas, {
    width: canvas.clientWidth,
    height: canvas.clientHeight,
  });
}

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => recordCanvasSize(entry.target));
});

canvases.forEach(canvas => {
  recordCanvasSize(canvas);
  observer.observe(canvas)
});

// not import but draw something just to showcase

const hsla = (h, s, l, a) => `hsla(${h * 360}, ${s * 100}%, ${l * 100}%, ${a})`;

function render(time) {
  canvases.forEach((canvas, n) => {
    const {width, height} = canvasToSize.get(canvas);
    canvas.width = width;
    canvas.height = height;

    const ctx = canvas.getContext('2d');
    ctx.save();
    ctx.translate(width / 2, height / 2);
    ctx.rotate(time * 0.0001 + n);
  
    const range = Math.max(width, height) * 0.8;
    const size = 64 + Math.sin(time * 0.001 + n) * 50;
    for (let i = 0; i < range; i += size) {
      ctx.fillStyle = hsla(i / range * 0.3 + time * 0.0001 + n / 3, 1, 0.5, 1);
      ctx.fillRect( i, -range, size, range * 2);
      ctx.fillRect(-i, -range, size, range * 2);
    }
  
    ctx.restore();
  });

  requestAnimationFrame(render)
}
requestAnimationFrame(render)
html, body {
  height: 100%;
  margin: 0;
}
#outer {
  height: 100%;
  display: flex;
  align-items: stretch;
}
canvas {
  flex: 1 1 auto;
  display: block;
  width: 100%;
  min-width: 0;
}
<div id="outer">
  <canvas></canvas>
  <canvas></canvas>
  <canvas></canvas>
</div>

توجه: مسائل دیگری نیز مربوط به تغییر اندازه بوم وجود دارد. بهویژه اگر بخواهید با تنظیمات مختلف devicePixelRatio کار کنید. برای اطلاعات بیشتر این مقاله را ببینید.

پاسخ شماره ۴

اساساً کاری که باید انجام دهید این است که رویداد onresize را به عنصر body خود متصل کنید، و هنگامی که این رویداد رخ داد، فقط کافی است که اندازه بوم را با استفاده از window.innerWidth و window.innerHeight تغییر دهید.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Canvas Resize</title>

    <script type="text/javascript">
        function resize_canvas(){
            canvas = document.getElementById("canvas");
            if (canvas.width  < window.innerWidth)
            {
                canvas.width  = window.innerWidth;
            }

            if (canvas.height < window.innerHeight)
            {
                canvas.height = window.innerHeight;
            }
        }
    </script>
</head>

<body onresize="resize_canvas()">
        <canvas id="canvas">Your browser doesn't support canvas</canvas>
</body>
</html>

پاسخ شماره ۵

تنظیم عرض و ارتفاع فضای مختصات بوم بر اساس ابعاد کلاینت مرورگر نیازمند این است که هر بار که مرورگر تغییر اندازه میدهد، اندازه آن را تغییر داده و دوباره رسم کنید.

یک راه حل کمتر پیچیده این است که ابعاد قابل رسم را در متغیرهای جاوااسکریپت نگه دارید، اما ابعاد بوم را بر اساس ابعاد screen.width و screen.height تنظیم کنید. از CSS برای سازگار کردن استفاده کنید:

#containingDiv { 
  overflow: hidden;
}
#myCanvas {
  position: absolute; 
  top: 0px;
  left: 0px;
} 

پنجره مرورگر معمولاً هرگز بزرگتر از خود صفحه نمایش نخواهد بود (به جز در مواردی که وضوح صفحه نمایش اشتباه گزارش شود، همانطور که ممکن است با مانیتورهای دوگانه غیر همسان رخ دهد)، بنابراین پسزمینه نمایش داده نمیشود و نسبت پیکسلها تغییر نخواهد کرد. پیکسلهای بوم به طور مستقیم متناسب با وضوح صفحه نمایش خواهند بود مگر اینکه از CSS برای مقیاسبندی بوم استفاده کنید.

پاسخ شماره ۶

یک رویکرد کاملاً CSS که به راه حل @jerseyboy در بالا اضافه میکند.
در فایرفاکس (تست شده در نسخه ۲۹)، کروم (تست شده در نسخه ۳۴) و اینترنت اکسپلورر (تست شده در نسخه ۱۱) کار میکند.

<!DOCTYPE html>
<html>
    <head>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
        }
        canvas {
            background-color: #ccc;
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <canvas id="canvas" width="500" height="500"></canvas>
    <script>
        var canvas = document.getElementById('canvas');
        if (canvas.getContext) {
            var ctx = canvas.getContext('2d');
            ctx.fillRect(25,25,100,100);
            ctx.clearRect(45,45,60,60);
            ctx.strokeRect(50,50,50,50);
        }
    </script>
</body>
</html>

لینک به مثال: http://temporaer.net/open/so/140502_canvas-fit-to-window.html

اما مراقب باشید، همانطور که @jerseyboy در نظر خود بیان میکند:

تغییر اندازه بوم با CSS مشکلساز است. حداقل در مرورگرهای Chrome و Safari، موقعیتهای موس/لمس با موقعیتهای پیکسلی بوم به صورت ۱:۱ مطابقت نخواهند داشت و شما باید سیستمهای مختصات را تبدیل کنید.

پاسخ شماره ۷

function resize() {

    var canvas = document.getElementById('game');
    var canvasRatio = canvas.height / canvas.width;
    var windowRatio = window.innerHeight / window.innerWidth;
    var width;
    var height;

    if (windowRatio < canvasRatio) {
        height = window.innerHeight;
        width = height / canvasRatio;
    } else {
        width = window.innerWidth;
        height = width * canvasRatio;
    }

    canvas.style.width = width + 'px';
    canvas.style.height = height + 'px';
};

window.addEventListener('resize', resize, false);

پاسخ شماره ۸

اندازه اولیه را تنظیم کنید.

const canvas = document.getElementById('canvas');

canvas.width  = window.innerWidth;
canvas.height = window.innerHeight;

بهروزرسانی اندازه هنگام تغییر اندازه پنجره.

function windowResize() {
  canvas.width  = window.innerWidth;
  canvas.height = window.innerHeight;
};

window.addEventListener('resize', windowResize);