PerfectVector
โดย Irene Kimอ่าน 4 นาที

SVG กับ Canvas Path2D: ใช้พาธซ้ำในขนาดที่ถูกต้อง

นำข้อมูลพาธ SVG มาใช้บน canvas ด้วยการแปลง viewBox สี ช่องเปิด และความหนาแน่นพิกเซลที่ถูกต้อง ทำตามตัวอย่างที่สร้างขึ้นและเก็บไฟล์หลัก SVG ที่แก้ไขได้

ในหน้านี้

ส่งแอตทริบิวต์ d ของพาธให้ new Path2D(d) แล้ววาดด้วยบริบท canvas หากต้องการให้ตรงกับ SVG ต้องนำการแมปพิกัด การวาดสี และกฎเติมสีมาด้วย ตัวสร้างอินสแตนซ์รับข้อมูลพาธ ไม่ได้นำเข้าเอกสาร SVG หรือใช้ viewBox, CSS หรือการแปลงกลุ่มของเอกสาร (ตัวสร้างอินสแตนซ์ Path2D ของ MDN)

เรื่องนี้สำคัญเมื่อไอคอนที่ออกแบบเองดูถูกต้องใน SVG แต่กลายเป็นเล็ก เยื้อง ดำ หรือช่องเปิดถูกเติมทึบบน canvas ตัวอย่างด้านล่างแก้ความต่างเหล่านั้นด้วยสองพาธและการแปลงเพื่อจัดให้พอดีที่กำหนดชัด เริ่มจากรูปทรงเวกเตอร์เดิมเมื่อมีอยู่ การแปลงเป็นเวกเตอร์มีประโยชน์เฉพาะเมื่องานที่จะนำมาใช้ซ้ำยังมีเพียงพิกเซล

วัตถุ Path2D เก็บอะไรไว้

ให้มองข้อมูลพาธเป็นคำอธิบายเส้นรอบรูป คำสั่ง SVG อื่นอยู่นอกสตริงนั้น และโค้ดวาด canvas ของคุณต้องกำหนดสิ่งที่จำเป็นเอง

อินพุต SVGหน้าที่ฝั่ง canvas
ค่า d ของพาธสร้างรูปทรง Path2D
viewBox และขนาดแสดงผลคำนวณการแปลงสำหรับวาด
fill และ fill-ruleตั้ง fillStyle และส่งกฎให้ fill()
transform ขององค์ประกอบแม่หรือพาธใช้การแปลงที่เทียบเท่าแยกต่างหาก
CSS ไล่สี มาสก์ ข้อความ และรูปภาพทำการเรนเดอร์ที่จำเป็นเองหรือเลือกอีกวิธี

ตัวอย่างใช้สีเติมที่ระบุโดยตรง พาธลูกโดยตรงสองพาธ และไม่มีฟีเจอร์ SVG อื่น ตั้งใจให้เล็กพอจะตรวจได้ การคัดลอกลูปดึงข้อมูลนี้ไปใส่ตัวนำเข้า SVG ทั่วไปจะทำให้สไตล์ที่สืบทอดและองค์ประกอบที่ไม่รองรับหายไป

เปรียบเทียบต้นฉบับกับ canvas ที่ยังไม่ได้ตั้งค่า

ตราสัญลักษณ์ที่เราสร้างนี้มีตัวรูปมนสีเขียวอมฟ้า ปลายสีส้ม และช่องเปิดสี่เหลี่ยม viewBox เริ่มที่ (20, 10) ไม่ใช่ (0, 0) พาธย่อยด้านในเดินทิศเดียวกับขอบด้านนอก กฎ evenodd ที่ประกาศจึงสำคัญ

ใส่ SVG นี้กับ canvas ลงในหน้า HTML:

<svg id="source" xmlns="http://www.w3.org/2000/svg"
     viewBox="20 10 120 80" width="240" height="160">
  <path fill="#126d70" fill-rule="evenodd"
        d="M40 20 H90 Q110 20 110 40 V60 Q110 80 90 80 H40 Q30 80 30 70 V30 Q30 20 40 20 Z M50 35 H80 V60 H50 Z"/>
  <path fill="#f19b38" d="M115 25 L135 50 L115 75 Z"/>
</svg>
<canvas id="result"></canvas>

หากวนค่า d ทั้งสองแล้วเรียก ctx.fill(new Path2D(d)) โดยไม่ตั้งอย่างอื่น รูปทรงจะใช้พิกัด canvas และสีวาดกับกฎเติมปริยาย มันไม่ได้รับรูปลักษณ์ SVG อัตโนมัติ fill() ของ canvas รับทั้งพาธและกฎ evenodd หรือ nonzero ที่ระบุได้ โดย nonzero เป็นค่าปริยาย (MDN fill())

ภาพเปรียบเทียบในเบราว์เซอร์ของตราสัญลักษณ์ SVG ผลวาด Path2D ดิบสีดำขนาดเล็กที่ช่องเปิดถูกเติม และผลวาด canvas สองสีที่จัดขนาดถูกต้อง
พาธเดียวกันที่เขียนเองในเบราว์เซอร์: SVG อ้างอิง Path2D ดิบ และการจัดให้พอดีที่กำหนดชัดพร้อมคืนค่าสีและ evenodd ตารางบันทึกขนาดบิตแมปเบื้องหลังสำหรับอินพุตขนาดและ DPR สี่แบบ

วิธีทดสอบ: เราเรนเดอร์ SVG ที่เขียนเองนี้ในเบราว์เซอร์ วาดสองพาธผ่าน canvas ทั้งสองวิธี และรันโค้ดที่เผยแพร่ซ้ำในหน้า HTML แยก ภาพหน้าจอเป็นผลเบราว์เซอร์จริง แสดงการจัดการพิกัดและสีวาด ไม่ใช่ผลแปลง PerfectVector

แมป viewBox ลงใน canvas

พฤติกรรมรักษาอัตราส่วนปริยายของ SVG จัด viewBox ให้พอดีภายในวิวพอร์ตและจัดกึ่งกลาง ข้อกำหนดระบบพิกัด SVG นิยามการแมปนั้น สำหรับตัวอย่างโครงสร้างไม่ซ้อนนี้ เลียนแบบ xMidYMid meet ด้วยมาตราส่วนเท่ากันหนึ่งค่าและการเลื่อน:

scale = min(cssWidth / viewBoxWidth, cssHeight / viewBoxHeight)
tx = (cssWidth - viewBoxWidth * scale) / 2 - viewBoxX * scale
ty = (cssHeight - viewBoxHeight * scale) / 2 - viewBoxY * scale

พจน์สุดท้ายลบจุดกำเนิดที่ไม่ใช่ศูนย์ ที่ 240 × 160 มาตราส่วนคือ 2, tx คือ -40 และ ty คือ -20 ที่ 300 × 120 มาตราส่วนเป็น 1.5 viewBox ที่จัดพอดีกว้าง 180 พิกเซลมีพื้นที่แนวนอนข้างละ 60 พิกเซล ดังนั้นระยะเลื่อนเป็น (30, -15) หลังรวมจุดกำเนิดต้นฉบับ

ใช้สคริปต์ต่อไปนี้หลัง HTML ด้านบน มันจัดการแอตทริบิวต์โดยตรงบนพาธของตัวอย่างนี้ วาด canvas ใหม่ทั้งหมด และแยกขนาด CSS ออกจากขนาดบิตแมปเบื้องหลัง:

const source = document.querySelector("#source");
const vb = source.viewBox.baseVal;
const layers = [...source.querySelectorAll("path")].map(el => ({
  path: new Path2D(el.getAttribute("d")),
  fill: el.getAttribute("fill"),
  rule: el.getAttribute("fill-rule") || "nonzero"
}));
 
function draw(canvas, cssWidth, cssHeight, dpr = window.devicePixelRatio || 1) {
  canvas.style.width = `${cssWidth}px`;
  canvas.style.height = `${cssHeight}px`;
  canvas.width = Math.round(cssWidth * dpr);
  canvas.height = Math.round(cssHeight * dpr);
  const ctx = canvas.getContext("2d");
  const scale = Math.min(cssWidth / vb.width, cssHeight / vb.height);
  const tx = (cssWidth - vb.width * scale) / 2 - vb.x * scale;
  const ty = (cssHeight - vb.height * scale) / 2 - vb.y * scale;
  const px = canvas.width / cssWidth;
  const py = canvas.height / cssHeight;
  ctx.setTransform(px * scale, 0, 0, py * scale, px * tx, py * ty);
  for (const layer of layers) {
    ctx.fillStyle = layer.fill;
    ctx.fill(layer.path, layer.rule);
  }
}
 
draw(document.querySelector("#result"), 240, 160);
 

การตั้ง canvas.width และ canvas.height ล้างและรีเซ็ตบริบท จากนั้นฟังก์ชันตั้งการแปลงทั้งหมดและวาดทุกชั้นใหม่ การเรียกซ้ำจึงไม่สะสมมาตราส่วน สมมติฐานคือ viewBox ใช้ได้และมีความกว้างสูงเป็นบวก รวมทั้งขนาดที่ขอเป็นบวก

px และ py ใช้ขนาดบิตแมปจริงหลังปัดเศษ ทำให้พิกัด CSS แมปกับพิกเซลที่จัดสรรแม้ความหนาแน่นที่ขอให้ขนาดเป็นเศษส่วน ตัวอย่าง devicePixelRatio ของ MDN อธิบายว่าทำไมขนาดแสดง canvas กับขนาดพิกเซลต้องจัดการแยกกัน

ตรวจขนาดกับความหนาแน่นพิกเซลแยกกัน

เราส่งค่า DPR 1 และ 2 ให้ฟังก์ชันเดียวกันอย่างชัดเจนที่ขนาด CSS สองแบบ นี่เป็นอินพุตความหนาแน่นที่ควบคุม ไม่ใช่การอ้างว่าทดสอบอุปกรณ์จริงสองเครื่อง อัตราส่วนพิกเซลอุปกรณ์ที่เบราว์เซอร์รายงานสำหรับการเรียกปริยายคือ 2

ขนาด CSSอาร์กิวเมนต์ DPRบิตแมป canvas ที่สังเกต
240 × 1601240 × 160
240 × 1602480 × 320
300 × 1201300 × 120
300 × 1202600 × 240

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

หากต้นฉบับผิดก่อนถึง canvas ให้ใช้ การตรวจขนาด SVG และ viewBox หากผิดเฉพาะ canvas ให้เปรียบเทียบการแปลงและการจัดการสีวาดกับ SVG อ้างอิงที่ทำงานถูกต้องก่อน

รักษาต้นฉบับและเลือกวิธีนำเข้าที่เหมาะสม

เก็บไฟล์หลัก SVG และแอตทริบิวต์ไว้คู่กับข้อมูลพาธที่ดึงออกมา ชุดคำสั่งวาด Path2D มีประโยชน์เมื่อคุณควบคุมเส้นรอบรูปและสไตล์จำนวนไม่มาก สำหรับ SVG ซับซ้อนที่ต้องการเพียงรูปลักษณ์ตามที่เบราว์เซอร์เรนเดอร์ การโหลดเป็นรูปภาพแล้ววาดภาพนั้นช่วยเลี่ยงการสร้างทุกฟีเจอร์ใหม่ได้ วิธีนั้นยังวาดลงบิตแมป canvas

การบันทึกเอาต์พุต canvas เป็น PNG สร้างภาพราสเตอร์ แม้การวาดเริ่มจากพาธเวกเตอร์ API ส่งออก canvas สร้างบล็อบรูปภาพ และต้องรองรับ PNG (MDN toBlob()) เก็บ SVG เพื่อแก้พาธภายหลัง เช่นเดียวกัน ไฟล์ SVG อาจมีภาพราสเตอร์แทนข้อมูลพาธที่ใช้ได้ วิธีวินิจฉัยภาพฝัง ช่วยแยกกรณีเหล่านั้น

PerfectVector เหมาะเมื่อเหลือเพียง PNG หรือ JPG ของไอคอนที่ออกแบบเองและต้องการเส้นรอบรูปนำมาใช้ซ้ำ แปลงภาพนั้นเป็น SVG แล้วตรวจขอบรูป สีที่แยกกัน และช่องเปิดเล็กก่อนดึงพาธสำหรับ canvas ไฟล์หลัก SVG ที่สะอาดควรเข้าสู่ขั้นตอนเตรียมโดยตรง การลากพาธไม่ได้ทำการแมป viewBox คัดลอก CSS หรือทำให้ PNG จาก canvas แก้ไขเป็นเวกเตอร์ได้

สำหรับไอคอนทั้งชุด กำหนด กริดและกฎสีร่วมของชุด UI ก่อนเชื่อมชุดคำสั่งวาดแต่ละไอคอน ตัวอย่างเตรียม SVG เพิ่มเติมอยู่ใน บล็อก

ก่อนใช้ภาพวาดในอินเทอร์เฟซ

  • ตรวจ SVG อ้างอิงที่ขนาดแสดงเดียวกับ canvas
  • ยืนยันจุดกำเนิด viewBox รวมทั้งความกว้างและความสูง
  • นำสีวาดและกฎเติมที่ตั้งใจไว้มาด้วย ตรวจทุกช่องเปิด
  • รวมการแปลงกลุ่มแทนการคัดลอก d อย่างเดียว
  • อ่านค่า canvas.width และ canvas.height จริง แล้วทดสอบขนาดแสดงใหญ่ที่สุดที่ตั้งใจใช้
  • เก็บไฟล์หลัก SVG และให้ชื่อที่เข้าถึงได้หรือข้อความรอบภาพที่เหมาะกับอินเทอร์เฟซ

คำถามที่พบบ่อย

Path2D นำเข้าไฟล์ SVG ทั้งไฟล์ได้หรือไม่? ไม่ อินพุตสตริงของมันเป็นข้อมูลพาธ SVG เช่นค่าแอตทริบิวต์ d ของพาธ โครงสร้างเอกสาร การแมป viewBox สไตล์ และการแปลงกลุ่มต้องจัดการแยก

ทำไมช่องเปิดใน SVG จึงถูกเติมบน canvas? ตรวจกฎเติมและทิศทางพาธย่อย ตัวอย่างนี้ต้องใช้ evenodd ขณะที่ canvas fill มีค่าปริยายเป็น nonzero ส่งกฎที่ตั้งใจไว้เมื่อเติมวัตถุ Path2D

วาดพาธ SVG บน canvas แล้วรักษาการส่งออกเวกเตอร์หรือไม่? พาธยังเป็นคำสั่งวาดที่ใช้ซ้ำได้ในโปรแกรม แต่การส่งออก canvas เป็น PNG ให้พิกเซล เก็บ SVG ต้นฉบับสำหรับแก้ไขและส่งมอบเวกเตอร์

แหล่งอ้างอิง

  1. MDN — ตัวสร้างอินสแตนซ์ Path2D — นิยามสตริงข้อมูลพาธที่ตัวสร้างรับ
  2. W3C — ระบบพิกัด SVG — นิยามจุดกำเนิด viewBox การแมปวิวพอร์ต และการจัดแนวรักษาอัตราส่วน
  3. MDN — Canvas fill() — อธิบายการเติมพาธและกฎ evenodd กับ nonzero
  4. MDN — devicePixelRatio — อธิบายขนาดแสดง CSS และความละเอียดบิตแมปเบื้องหลัง canvas
  5. MDN — Canvas toBlob() — อธิบายการส่งออกภาพราสเตอร์จาก canvas

มีเพียงไอคอนราสเตอร์หรือ? เตรียม SVG ด้วย PerfectVector ตรวจเส้นรอบรูปและช่องเปิด แล้วเปรียบเทียบภาพวาด canvas กับ SVG นั้นตามขนาดอินเทอร์เฟซจริง

เพิ่มเติมจากบล็อก

เริ่มต้นด้วย SVG ที่เรียบร้อยขึ้น
และแก้ไขได้ง่ายกว่า