ค้นหาเว็บไซต์

javascript - ปรับขนาดแคนวาส HTML5 ให้พอดีกับหน้าต่าง


ฉันจะปรับขนาดองค์ประกอบ HTML5 <canvas> ให้พอดีกับหน้าต่างโดยอัตโนมัติได้อย่างไร?

ตัวอย่างเช่น ฉันสามารถทำให้ <div> ขยายขนาดได้โดยการตั้งค่าคุณสมบัติ height และ width เป็น 100% แต่ <canvas> จะไม่สามารถขยายขนาดได้ใช่ไหม?

คำตอบ #1

ผมเชื่อว่าผมได้พบวิธีแก้ปัญหาที่สง่างามสำหรับเรื่องนี้แล้ว:

จาวาสคริปต์

/* 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;
}

จนถึงตอนนี้ ยังไม่มีผลกระทบด้านประสิทธิภาพเชิงลบขนาดใหญ่ต่อฉันเลย

คำตอบ #2

วิธีแก้ปัญหาต่อไปนี้ใช้ได้ผลดีที่สุดสำหรับฉัน เนื่องจากฉันค่อนข้างใหม่ต่อการเขียนโค้ด ฉันชอบที่จะมีการยืนยันทางสายตาว่าสิ่งใดกำลังทำงานตามที่ฉันคาดไว้ ฉันพบมันที่เว็บไซต์ดังต่อไปนี้: 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>

ขอบสีน้ำเงินจะแสดงขอบของผืนผ้าใบที่ปรับขนาดได้ และจะอยู่ตามขอบของหน้าต่างเสมอ มองเห็นได้ทั้ง 4 ด้าน ซึ่งไม่ได้เกิดขึ้นกับคำตอบอื่น ๆ ข้างต้นบางคำตอบ หวังว่าจะช่วยได้นะ

คำตอบ #3

คำตอบปี 2022

วิธีที่แนะนำในปี 2022 ในการตรวจสอบว่าองค์ประกอบมีการปรับขนาดหรือไม่คือการใช้ 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 เพราะโดยค่าเริ่มต้นแคนวาสเป็น inline ซึ่งหมายความว่าจะรวมพื้นที่ว่างเพิ่มเติมที่ปลาย หากไม่ใช้ display: block คุณจะได้แถบเลื่อน หลายคนแก้ปัญหาแถบเลื่อนโดยการเพิ่ม overflow: hidden ใน body ของเอกสาร แต่สิ่งนั้นเป็นเพียงการซ่อนความจริงที่ว่า CSS ของแคนวาสไม่ได้ถูกตั้งค่าอย่างถูกต้อง ควรแก้ไขบั๊ก (ตั้งค่าแคนวาสเป็น 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>

คุณอาจถามว่า ทำไมเราต้องบันทึกความกว้างและความสูงใน callback ของ ResizeObserver แทนที่จะตั้งค่าขนาดของ canvas โดยตรง เหตุผลก็คือ สเปคของ HTML5 ระบุว่า callback ของ ResizeObserver จะเกิดขึ้นหลังจาก callback ของ requestAnimationFrame แต่ก่อนการคอมโพสิต การตั้งค่าความกว้างหรือความสูงของ canvas จะล้าง canvas ทั้งหมด รวมกันแล้วหมายความว่า หากผู้ใช้ปรับขนาด ลำดับของการทำงานจะเป็น

  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 ที่แตกต่างกัน ดูบทความนี้สำหรับข้อมูลเพิ่มเติม

คำตอบ #4

พื้นฐานสิ่งที่คุณต้องทำคือการผูกเหตุการณ์ 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>

คำตอบ #5

การตั้งค่าความกว้างและความสูงของพื้นที่พิกัดของผ้าใบตามขนาดของไคลเอนต์เบราว์เซอร์จำเป็นต้องปรับขนาดและวาดใหม่ทุกครั้งที่เบราว์เซอร์ถูกปรับขนาด

วิธีแก้ปัญหาที่ไม่ซับซ้อนมากนักคือการเก็บขนาดของภาพที่สามารถวาดได้ในตัวแปรของ Javascript แต่ตั้งค่าขนาดของ canvas ตามขนาด screen.width และ screen.height ใช้ CSS เพื่อปรับให้พอดี:

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

หน้าต่างเบราว์เซอร์โดยทั่วไปจะไม่ใหญ่กว่าหน้าจอเอง (ยกเว้นในกรณีที่ความละเอียดหน้าจอรายงานผิดพลาด เช่น กับจอภาพคู่ที่ไม่ตรงกัน) ดังนั้นพื้นหลังจะไม่ปรากฏและสัดส่วนพิกเซลจะไม่เปลี่ยนแปลง พิกเซลของแคนวาสจะเป็นสัดส่วนโดยตรงกับความละเอียดหน้าจอเว้นแต่คุณจะใช้ CSS เพื่อขยายหรือย่อแคนวาส

คำตอบ #6

แนวทาง CSS บริสุทธิ์ เพิ่มเติมจากวิธีแก้ของ @jerseyboy ข้างต้น
ใช้งานได้ใน Firefox (ทดสอบในเวอร์ชัน 29), Chrome (ทดสอบในเวอร์ชัน 34) และ Internet Explorer (ทดสอบในเวอร์ชัน 11)

<!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 ตำแหน่งเหตุการณ์เมาส์/การสัมผัสจะไม่ตรงกับตำแหน่งพิกเซลของผืนผ้าใบแบบ 1:1 และคุณจะต้องแปลงระบบพิกัด

คำตอบ #7

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);

คำตอบ #8

ตั้งค่าขนาดเริ่มต้น

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);

บทความที่เกี่ยวข้อง