เอฟเฟ็กต์ของแบบตัวอักษรใน Canvas

ภูมิหลังของฉัน

<canvas> เป็นที่รู้จักของฉันในปี 2006 เมื่อมีการเปิดตัว Firefox v2.0 บทความใน Ajaxian ที่อธิบายเกี่ยวกับเมทริกซ์การเปลี่ยนรูปแบบเป็นแรงบันดาลใจให้ฉันสร้างเว็บแอป <canvas> รายการแรกอย่าง Color Sphere (2007) ซึ่งทำให้ฉันได้สัมผัสกับโลกแห่งสีสันและรูปภาพพื้นฐานต่างๆ จนเกิดแรงบันดาลใจในการสร้าง Sketchpad (2007-2008) เพื่อพยายามสร้างแอปพลิเคชันที่ "ดีกว่า Paint" ในเบราว์เซอร์ การทดลองเหล่านี้นำไปสู่การสร้างสตาร์ทอัพ Mugtug ร่วมกับ Charles Pritchard เพื่อนเก่า เรากำลังพัฒนา Darkroom ใน HTML5 <canvas> Darkroom เป็นแอปการแชร์รูปภาพที่ไม่มีการทำลายข้อมูล ซึ่งรวมประสิทธิภาพของฟิลเตอร์แบบพิกเซลเข้ากับแบบอักษรและภาพวาดแบบเวกเตอร์

บทนำ

กราฟิกแบนเนอร์ภาพพิมพ์แคนวาส

<canvas> ช่วยให้โปรแกรมเมอร์ JavaScript ควบคุมสี เวกเตอร์ และพิกเซลบนหน้าจอได้อย่างเต็มที่ ซึ่งก็คือองค์ประกอบภาพบนจอภาพ

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

เงาข้อความใน <canvas>
เอฟเฟกต์ข้อความที่คล้าย CSS ใน <canvas> การสร้างมาสก์การตัด ค้นหาเมตริกใน <canvas> และใช้พร็อพเพอร์ตี้เงา
เอฟเฟกต์นีออนเรนโบว์ และเอฟเฟกต์แสงสะท้อนจากม้าลาย
เอฟเฟกต์ข้อความที่คล้าย Photoshop ใน <canvas> ตัวอย่างการใช้ GlobalCompositeOperation, createLinearGradient, createPattern
เงาด้านในและด้านนอกใน <canvas>
เผยฟีเจอร์ที่ไม่ค่อยมีคนรู้จัก โดยใช้การเลี้ยวตามเข็มนาฬิกากับการเลี้ยวทวนเข็มนาฬิกาเพื่อสร้างเงาที่ตรงข้ามกับเงาตก (เงาด้านใน)
Spaceage - generative effect.
เอฟเฟกต์ข้อความแบบ Generative ใน <canvas> โดยใช้การเปลี่ยนสี hsl() และ window.requestAnimationFrame เพื่อสร้างความรู้สึกเคลื่อนไหว

เงาข้อความใน Canvas

หนึ่งในสิ่งที่ฉันชอบที่สุดที่เพิ่มเข้ามาในข้อกำหนด CSS3 (นอกเหนือจาก border-radius, web-gradients และอื่นๆ) คือความสามารถในการสร้างเงา คุณควรทราบความแตกต่างระหว่างเงา CSS และเงา <canvas> โดยเฉพาะอย่างยิ่ง

CSS ใช้ 2 วิธี ได้แก่ box-shadow สำหรับองค์ประกอบกล่อง เช่น div, span และอื่นๆ และ text-shadow สำหรับเนื้อหาข้อความ

<canvas> มีเงา 1 ประเภท ซึ่งใช้กับออบเจ็กต์เวกเตอร์ทั้งหมด เช่น ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText และอื่นๆ หากต้องการสร้างเงาใน <canvas> ให้แตะพร็อพเพอร์ตี้ 4 รายการต่อไปนี้

ctx.shadowColor = "red" // string
สีของเงา: อินพุต RGB, RGBA, HSL, HEX และอื่นๆ ใช้ได้
ctx.shadowOffsetX = 0; // integer
ระยะห่างในแนวนอนของเงาเมื่อเทียบกับข้อความ
ctx.shadowOffsetY = 0; // จำนวนเต็ม
ระยะห่างในแนวตั้งของเงาเมื่อเทียบกับข้อความ
ctx.shadowBlur = 10; // integer
เอฟเฟกต์เบลอเงา ยิ่งค่ายิ่งมาก เบลอก็จะยิ่งมาก

เริ่มต้นกันด้วยมาดูว่า <canvas> จำลองเอฟเฟกต์ CSS ได้อย่างไร การค้นหา "css text-shadow" ใน Google Images นำเราไปพบกับตัวอย่างที่ดี 2-3 รายการที่เราสามารถลอกเลียนแบบได้ ได้แก่ Line25, Stereoscopic และ Shadow 3D

กราฟิก 3 มิติ CSS

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

text-shadow: -0.06em 0 0 red, 0.06em 0 0 cyan;

สิ่งที่ควรทราบ 2 ประการเมื่อแปลงสตริงนี้เป็น <canvas>

  1. ไม่มีการเบลอเงา (ค่าที่ 3) จึงไม่มีเหตุผลที่จะเรียกใช้เงาจริง เนื่องจากfillText จะสร้างผลลัพธ์ที่เหมือนกัน
var text = "Hello world!"
ctx.fillStyle = "#000"
ctx.fillText(text, -7, 0);
ctx.fillStyle = "red"
ctx.fillText(text, 0, 0);
ctx.fillStyle = "cyan"
ctx.fillText(text, 7, 0);</pre>
  1. <canvas> ไม่รองรับ EM จึงต้องแปลงเป็น PX เราหาอัตราส่วนการเปลี่ยนค่าสำหรับแปลงระหว่าง PT, PC, EM, EX, PX และอื่นๆ ได้โดยใช้การสร้างองค์ประกอบที่มีพร็อพเพอร์ตี้แบบอักษรเดียวกันใน DOM และตั้งค่าความกว้างเป็นรูปแบบที่จะวัด เช่น หากต้องการจับการเปลี่ยนค่า EM เป็น PX เราจะวัดองค์ประกอบ DOM ที่มี "height: 1em" ค่า offsetHeight ที่ได้จะเป็นจำนวน PX ในแต่ละ EM
var font = "20px sans-serif"
var d = document.createElement("span");
d.style.cssText = "font: " + font + " height: 1em; display: block"
// the value to multiply PX 's by to convert to EM 's
var EM2PX = 1 / d.offsetHeight;</pre>

การป้องกันอัลฟาคูณ

ในตัวอย่างที่ซับซ้อนมากขึ้น เช่น ผลนีออนที่พบในบรรทัด 25 ต้องใช้พร็อพเพอร์ตี้ shadowBlur เพื่อจำลองเอฟเฟกต์อย่างถูกต้อง เนื่องจากเอฟเฟกต์นีออนใช้เงาหลายรายการ เราจึงพบปัญหาใน <canvas> เนื่องจากออบเจ็กต์เวกเตอร์แต่ละรายการมีเงาได้เพียงรายการเดียว ดังนั้น หากต้องการวาดเงาหลายรายการ คุณต้องวาดข้อความหลายเวอร์ชันซ้อนกัน ซึ่งส่งผลให้เกิดการคูณอัลฟา และท้ายที่สุดจะมีขอบขรุขระ

กราฟิกนีออน

ฉันลองเรียกใช้ ctx.fillStyle = "rgba(0,0,0,0)" หรือ "transparent" เพื่อซ่อนข้อความขณะที่แสดงเงา... แต่ความพยายามนี้ก็ไม่มีประโยชน์ เนื่องจากเงาเป็นการทวีคูณของอัลฟ่าแบบfillStyle เงาก็จะไม่ทึบแสงมากกว่าการเติมสไตล์

แต่โชคดีที่เรามีวิธีหลีกเลี่ยงปัญหานี้ เราสามารถวาดออฟเซ็ตเงาจากข้อความโดยแยกส่วนออกจากกัน (เพื่อไม่ให้ซ้อนทับกัน) และซ่อนข้อความไว้นอกหน้าจอได้ด้วย

var text = "Hello world!"
var blur = 10;
var width = ctx.measureText(text).width + blur * 2;
ctx.textBaseline = "top"
ctx.shadowColor = "#000"
ctx.shadowOffsetX = width;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = blur;
ctx.fillText(text, -width, 0);

เลื่อยรอบบล็อกข้อความ

หากต้องการทำให้เรียบร้อยขึ้นเล็กน้อย เราสามารถป้องกันไม่ให้ระบบวาด fillText ตั้งแต่แรก (ขณะที่อนุญาตให้วาดเงา) ได้โดยการเพิ่มเส้นทางการครอบตัด หากต้องการสร้างเส้นทางการครอบตัดรอบข้อความ เราจำเป็นต้องทราบความสูงของข้อความ (เรียกว่า "ความสูง em" ซึ่งเดิมคือความสูงของตัวอักษร "M" ในเครื่องพิมพ์) และกว้างของข้อความ เรารับความกว้างได้โดยใช้ ctx.measureText().width แต่ ctx.measureText().height นั้นไม่มีอยู่

โชคดีที่จากการแฮ็กด้วย CSS (ดูเมตริกการพิมพ์ สำหรับวิธีอื่นๆ ในการแก้ไขการใช้งาน <canvas> แบบเก่าโดยใช้การวัด CSS) เราจะพบความสูงของข้อความผ่านการวัด offsetHeight ของ <span> ที่มีพร็อพเพอร์ตี้แบบอักษรเดียวกัน

var d = document.createElement("span");
d.font = "20px arial"
d.textContent = "Hello world!"
var emHeight = d.offsetHeight;

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

ctx.rect(0, 0, width, emHeight);
ctx.clip();

สรุปทั้งหมดและเพิ่มประสิทธิภาพไปพร้อมๆ กัน หากเงาไม่มีเบลอ คุณสามารถใช้ fillText เพื่อทำให้เกิดเอฟเฟกต์เดียวกันได้ ซึ่งจะช่วยเราไม่ต้องตั้งค่ามาสก์การตัด

var width = ctx.measureText(text).width;
var style = shadowStyles[text];
// add a background to the current effect
ctx.fillStyle = style.background;
ctx.fillRect(0, offsetY, ctx.canvas.width, textHeight - 1)
// parse text-shadows from css
var shadows = parseShadow(style.shadow);
// loop through the shadow collection
var n = shadows.length; while(n--) {
var shadow = shadows[n];
var totalWidth = width + shadow.blur * 2;
ctx.save();
ctx.beginPath();
ctx.rect(offsetX - shadow.blur, offsetY, offsetX + totalWidth, textHeight);
ctx.clip();
if (shadow.blur) { // just run shadow (clip text)
    ctx.shadowColor = shadow.color;
    ctx.shadowOffsetX = shadow.x + totalWidth;
    ctx.shadowOffsetY = shadow.y;
    ctx.shadowBlur = shadow.blur;
    ctx.fillText(text, -totalWidth + offsetX, offsetY + metrics.top);
} else { // just run pseudo-shadow
    ctx.fillStyle = shadow.color;
    ctx.fillText(text, offsetX + (shadow.x||0), offsetY - (shadow.y||0) + metrics.top);
}
ctx.restore();
}
// drawing the text in the foreground
if (style.color) {
ctx.fillStyle = style.color;
ctx.fillText(text, offsetX, offsetY + metrics.top);
}
// jump to next em-line
ctx.translate(0, textHeight);

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

เงาข้อความในเอฟเฟกต์ภาพพิมพ์แคนวาส

ระยะพัก (แทนเจนต์ในการพุชพิกเซล)

ในการเขียนส่วนนี้ของบทความ ตัวอย่างภาพสเตอริโอสโคปทำให้ฉันสงสัย การสร้างเอฟเฟกต์หน้าจอภาพยนตร์ 3 มิติโดยใช้ <canvas> และรูปภาพ 2 รูปที่ถ่ายจากมุมมองที่ต่างกันเล็กน้อยนั้นยากง่ายเพียงใด ดูเหมือนว่าจะไม่ใช่เรื่องยาก เคอร์เนลต่อไปนี้รวมช่องสีแดงของรูปภาพแรก (ข้อมูล) ที่มีช่องสีน้ำเงินของรูปภาพที่สอง (data2)

data[i] = data[i] * 255 / 0xFF;
data[i+1] = 255 * data2[i+1] / 0xFF;
data[i+2] = 255 * data2[i+2] / 0xFF;

ตอนนี้ก็แค่เอาเทปพันสายไฟติด iPhone 2 เครื่องไว้ที่หน้าผาก แล้วคลิก "บันทึกวิดีโอ" พร้อมกัน เราก็สร้างภาพยนตร์ 3 มิติของเราเองใน HTML5 ได้แล้ว มีใครอยากอาสาไหม

แว่นตา 3 มิติ

เอฟเฟกต์นีออนเรนโบว์ แสงสะท้อนจากม้าลาย

การต่อเอฟเฟกต์หลายรายการใน <canvas> อาจเป็นเรื่องง่าย แต่ต้องมีความรู้พื้นฐานเกี่ยวกับ globalCompositeOperation (GCO) วิธีเปรียบเทียบการดำเนินการกับ GIMP (หรือ Photoshop) มี GCO 12 รายการที่<canvas> เข้มกว่าและสีอ่อนถือเป็นโหมดผสมผสานเลเยอร์ ส่วนการดำเนินการอีก 10 รายการจะใช้กับเลเยอร์เหมือนมาสก์อัลฟ่า (เลเยอร์หนึ่งจะนำพิกเซลของอีกเลเยอร์ออก) globalCompositeOperation จะเชื่อมโยง "เลเยอร์" (หรือในกรณีของเราคือสตริงโค้ด) เข้าด้วยกัน โดยรวมเข้าด้วยกันด้วยวิธีใหม่ๆ ที่น่าตื่นเต้น ดังนี้

การเชื่อมต่อกราฟิกเอฟเฟกต์

แผนภูมิ globalCompositeOperation แสดงการทำงานของโหมด GCO โดยแผนภูมินี้ใช้ช่วงสีส่วนใหญ่และความโปร่งแสงอัลฟ่าหลายระดับเพื่อให้เห็นรายละเอียดของสิ่งที่จะเกิดขึ้น เราขอแนะนําให้ดูข้อมูลอ้างอิง globalCompositeOperation ของ Mozilla สำหรับคำอธิบายที่เป็นข้อความ หากต้องการค้นคว้าเพิ่มเติม คุณสามารถดูวิธีการทำงานของการดำเนินการในหนังสือ Compositing Digital Images ของ Porter Duff

โหมดที่ฉันชอบคือ globalCompositeOperation="lighter" ที่สว่างขึ้นจะผสมผสานพิกเซลที่ต่อท้ายซึ่งคล้ายกับการผสมแสงของแสง เมื่อแสงสีแดง เขียว และขาวมีความเข้มเต็มที่ เราจะเห็นแสงสีขาว ฟีเจอร์นี้น่าสนุกสนานเมื่อนำมาใช้งาน โดยเฉพาะเมื่อตั้งค่า <canvas> เป็น globalAlpha ต่ำ ซึ่งจะช่วยให้ควบคุมได้ละเอียดยิ่งขึ้นและขอบเรียบเนียนขึ้น แอป Lighter มีประโยชน์มากมายหลายอย่าง ล่าสุดผมชอบเป็นผู้สร้างพื้นหลังบนเดสก์ท็อป HTML5 ที่ http://weavesilk.com/ การแสดงตัวอย่างเพลง Breathing Galaxies (JS1k) รายการหนึ่งใช้โหมดที่สว่างกว่าเช่นกัน คุณสามารถดูรูปแบบจากตัวอย่าง 2 รายการนี้เพื่อเริ่มเห็นผลลัพธ์ของโหมดนี้

การจัดการเบราว์เซอร์ globalCompositeOperation

เอฟเฟกต์นีออนรุ้ง

ในการแสดงตัวอย่างต่อไปนี้ เราจะสร้างเอฟเฟกต์แสงนีออนสีรุ้งเหมือนใน Photoshop ที่มีขอบที่กระจัดกระจาย โดยเชื่อมเอฟเฟกต์เข้าด้วยกันโดยใช้ globalCompositeOperation (source-in, lighter และ darker) การสาธิตนี้ต่อยอดมาจาก "เงาข้อความใน <canvas>" โดยจะใช้กลยุทธ์เดียวกันในการแยกเงาออกจากข้อความ (ดูส่วนก่อนหน้า)

Jitter แบบรุ้ง
function neonLightEffect() {
var text = "alert('"+String.fromCharCode(0x2665)+"')";
var font = "120px Futura, Helvetica, sans-serif";
var jitter = 25; // the distance of the maximum jitter
var offsetX = 30;
var offsetY = 70;
var blur = getBlurValue(100);
// save state
ctx.save();
ctx.font = font;
// calculate width + height of text-block
var metrics = getMetrics(text, font);
// create clipping mask around text-effect
ctx.rect(offsetX - blur/2, offsetY - blur/2,
        offsetX + metrics.width + blur, metrics.height + blur);
ctx.clip();
// create shadow-blur to mask rainbow onto (since shadowColor doesn't accept gradients)
ctx.save();
ctx.fillStyle = "#fff";
ctx.shadowColor = "rgba(0,0,0,1)";
ctx.shadowOffsetX = metrics.width + blur;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = blur;
ctx.fillText(text, -metrics.width + offsetX - blur, offsetY + metrics.top);
ctx.restore();
// create the rainbow linear-gradient
var gradient = ctx.createLinearGradient(0, 0, metrics.width, 0);
gradient.addColorStop(0, "rgba(255, 0, 0, 1)");
gradient.addColorStop(0.15, "rgba(255, 255, 0, 1)");
gradient.addColorStop(0.3, "rgba(0, 255, 0, 1)");
gradient.addColorStop(0.5, "rgba(0, 255, 255, 1)");
gradient.addColorStop(0.65, "rgba(0, 0, 255, 1)");
gradient.addColorStop(0.8, "rgba(255, 0, 255, 1)");
gradient.addColorStop(1, "rgba(255, 0, 0, 1)");
// change composite so source is applied within the shadow-blur
ctx.globalCompositeOperation = "source-atop";
// apply gradient to shadow-blur
ctx.fillStyle = gradient;
ctx.fillRect(offsetX - jitter/2, offsetY,
            metrics.width + offsetX, metrics.height + offsetY);
// change composite to mix as light
ctx.globalCompositeOperation = "lighter";
// multiply the layer
ctx.globalAlpha = 0.7
ctx.drawImage(ctx.canvas, 0, 0);
ctx.drawImage(ctx.canvas, 0, 0);
ctx.globalAlpha = 1
// draw white-text ontop of glow
ctx.fillStyle = "rgba(255,255,255,0.95)";
ctx.fillText(text, offsetX, offsetY + metrics.top);
// created jittered stroke
ctx.lineWidth = 0.80;
ctx.strokeStyle = "rgba(255,255,255,0.25)";
var i = 10; while(i--) { 
    var left = jitter / 2 - Math.random() * jitter;
    var top = jitter / 2 - Math.random() * jitter;
    ctx.strokeText(text, left + offsetX, top + offsetY + metrics.top);
}    
ctx.strokeStyle = "rgba(0,0,0,0.20)";
ctx.strokeText(text, offsetX, offsetY + metrics.top);
ctx.restore();
};

เอฟเฟกต์การสะท้อนของม้าลาย

เอฟเฟกต์ภาพสะท้อนของ Zebra ได้รับแรงบันดาลใจจากแหล่งข้อมูลที่ยอดเยี่ยมของ WebDesignerWall ในการเพิ่มสีสันให้กับหน้าเว็บด้วย CSS ฟีเจอร์นี้พัฒนาไอเดียไปอีกขั้นด้วยการสร้าง "ภาพสะท้อน" ของข้อความ เช่น ภาพที่อาจเห็นใน iTunes เอฟเฟ็กต์นี้รวมการเติมสี (สีขาว), createPattern (zebra.png) และ LinearGradient (shine) สิ่งนี้แสดงให้เห็นถึงความสามารถในการใช้ประเภทการเติมหลายแบบ กับออบเจ็กต์เวกเตอร์แต่ละรายการ

เอฟเฟกต์ Zebra
function sleekZebraEffect() {
// inspired by - http://www.webdesignerwall.com/demo/css-gradient-text/
var text = "Sleek Zebra...";
var font = "100px Futura, Helvetica, sans-serif";

// save state
ctx.save();
ctx.font = font;

// getMetrics calculates:
// width + height of text-block
// top + middle + bottom baseline
var metrics = getMetrics(text, font);
var offsetRefectionY = -20;
var offsetY = 70;
var offsetX = 60;

// throwing a linear-gradient in to shine up the text
var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
gradient.addColorStop(0.1, '#000');
gradient.addColorStop(0.35, '#fff');
gradient.addColorStop(0.65, '#fff');
gradient.addColorStop(1.0, '#000');
ctx.fillStyle = gradient
ctx.fillText(text, offsetX, offsetY + metrics.top);

// draw reflected text
ctx.save();
ctx.globalCompositeOperation = "source-over";
ctx.translate(0, metrics.height + offsetRefectionY)
ctx.scale(1, -1);
ctx.font = font;
ctx.fillStyle = "#fff";
ctx.fillText(text, offsetX, -metrics.height - offsetY + metrics.top);
ctx.scale(1, -1);

// cut the gradient out of the reflected text 
ctx.globalCompositeOperation = "destination-out";
var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
gradient.addColorStop(0.0, 'rgba(0,0,0,0.65)');
gradient.addColorStop(1.0, '#000');
ctx.fillStyle = gradient;
ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height);

// restore back to original transform state
ctx.restore();

// using source-atop to allow the transparent .png to show through to the gradient
ctx.globalCompositeOperation = "source-atop";

// creating pattern from <image> sourced.
ctx.fillStyle = ctx.createPattern(image, 'repeat');

// fill the height of two em-boxes, to encompass both normal and reflected state
ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height * 2);
ctx.restore();
};

เงาด้านใน/ด้านนอกใน Canvas

ข้อกำหนด <canvas> ไม่ได้กล่าวถึงเรื่องเงา "ด้านใน" เทียบกับเงา "ด้านนอก" อันที่จริงแล้ว เมื่อเห็นครั้งแรก คุณอาจคิดว่าระบบไม่รองรับเงา "ด้านใน" ไม่เป็นเช่นนั้น การเปิดใช้นั้นค่อนข้างยุ่งยากนิดหน่อย ;) ตามที่เสนอไว้ในโพสต์ล่าสุดจาก F1LT3R คุณสามารถสร้างเงาภายในได้โดยใช้พร็อพเพอร์ตี้ที่ไม่ซ้ำกันของกฎการเลี้ยวตามเข็มนาฬิกาและทวนเข็มนาฬิกา โดยสร้าง "เงาภายใน" ด้วยการวาดสี่เหลี่ยมผืนผ้าของคอนเทนเนอร์ จากนั้นใช้กฎการเลี้ยวสวนทางเพื่อวาดรูปร่างที่ตัดออก ซึ่งจะสร้างรูปร่างที่กลับกัน

ตัวอย่างต่อไปนี้ช่วยให้คุณจัดรูปแบบ inner-shadow และ fillStyle ได้โดยใช้สี+ไล่ระดับสี+ลายพร้อมกัน คุณสามารถระบุการหมุนของลายทีละรายการได้ โปรดสังเกตว่าตอนนี้ลายทางของม้าลายตั้งฉากกัน ระบบจะใช้มาสก์การครอบตัดขนาดของขอบเขต ซึ่งทำให้ไม่ต้องใช้คอนเทนเนอร์ขนาดใหญ่มากเพื่อล้อมรอบรูปร่างที่ตัดออก ซึ่งจะช่วยเพิ่มความเร็วด้วยการไม่ประมวลผลส่วนที่ไม่จำเป็นต้องใช้ของเงา

เงาด้านใน/ด้านนอก
function innerShadow() {

function drawShape() { // draw anti-clockwise
ctx.arc(0, 0, 100, 0, Math.PI * 2, true); // Outer circle
ctx.moveTo(70, 0);
ctx.arc(0, 0, 70, 0, Math.PI, false); // Mouth
ctx.moveTo(-20, -20);
ctx.arc(30, -30, 10, 0, Math.PI * 2, false); // Left eye
ctx.moveTo(140, 70);
ctx.arc(-20, -30, 10, 0, Math.PI * 2, false); // Right eye
};

var width = 200;
var offset = width + 50;
var innerColor = "rgba(0,0,0,1)";
var outerColor = "rgba(0,0,0,1)";

ctx.translate(150, 170);

// apply inner-shadow
ctx.save();
ctx.fillStyle = "#000";
ctx.shadowColor = innerColor;
ctx.shadowBlur = getBlurValue(120);
ctx.shadowOffsetX = -15;
ctx.shadowOffsetY = 15;

// create clipping path (around blur + shape, preventing outer-rect blurring)
ctx.beginPath();
ctx.rect(-offset/2, -offset/2, offset, offset);
ctx.clip();

// apply inner-shadow (w/ clockwise vs. anti-clockwise cutout)
ctx.beginPath();
ctx.rect(-offset/2, -offset/2, offset, offset);
drawShape();
ctx.fill();
ctx.restore();

// cutout temporary rectangle used to create inner-shadow
ctx.globalCompositeOperation = "destination-out";
ctx.fill();

// prepare vector paths
ctx.beginPath();
drawShape();

// apply fill-gradient to inner-shadow
ctx.save();
ctx.globalCompositeOperation = "source-in";
var gradient = ctx.createLinearGradient(-offset/2, 0, offset/2, 0);
gradient.addColorStop(0.3, '#ff0');
gradient.addColorStop(0.7, '#f00');
ctx.fillStyle = gradient;
ctx.fill();

// apply fill-pattern to inner-shadow
ctx.globalCompositeOperation = "source-atop";
ctx.globalAlpha = 1;
ctx.rotate(0.9);
ctx.fillStyle = ctx.createPattern(image, 'repeat');
ctx.fill();
ctx.restore();

// apply fill-gradient
ctx.save();
ctx.globalCompositeOperation = "destination-over";
var gradient = ctx.createLinearGradient(-offset/2, -offset/2, offset/2, offset/2);
gradient.addColorStop(0.1, '#f00');
gradient.addColorStop(0.5, 'rgba(255,255,0,1)');
gradient.addColorStop(1.0, '#00f');
ctx.fillStyle = gradient
ctx.fill();

// apply fill-pattern
ctx.globalCompositeOperation = "source-atop";
ctx.globalAlpha = 0.2;
ctx.rotate(-0.4);
ctx.fillStyle = ctx.createPattern(image, 'repeat');
ctx.fill();
ctx.restore();

// apply outer-shadow (color-only without temporary layer)
ctx.globalCompositeOperation = "destination-over";
ctx.shadowColor = outerColor;
ctx.shadowBlur = 40;
ctx.shadowOffsetX = 15;
ctx.shadowOffsetY = 10;
ctx.fillStyle = "#fff";
ctx.fill();
};

จากตัวอย่างเหล่านี้ คุณจะเห็นว่าการใช้ globalCompositeOperation ช่วยให้เราสามารถใช้เอฟเฟกต์แบบเชนร่วมกันเพื่อสร้างเอฟเฟกต์ที่ซับซ้อนยิ่งขึ้น (โดยใช้การมาสก์และการผสม) หน้าจอคือโลกใบใหม่ของคุณ ;)

Spaceage - เอฟเฟกต์ Generative

ใน <canvas> เริ่มจากอักขระ Unicode 0x2708:

Gfaphic แบบ Unicode

...ในตัวอย่างที่มีแรเงานี้:

ตัวอย่างที่มีเงา

…ทำได้โดยการเรียกใช้ ctx.strokeText() หลายครั้งโดยใช้ lineWidth แบบบาง (0.25) ในขณะที่ค่อยๆ ลดค่า x-offset และค่าอัลฟ่า ซึ่งทำให้องค์ประกอบเวกเตอร์ของเราดูเคลื่อนไหว

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

เอฟเฟกต์การเปลี่ยนสี HSL

HSL: สี ความอิ่มตัว ความสว่าง (1978)

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

การแสดงภาพความง่ายของ HSL หากต้องการวนดูสเปกตรัมสี เราเพียงแค่ต้องเพิ่ม "สี" จาก 360 โดยระบบจะแมปสีกับสเปกตรัมในลักษณะทรงกระบอก ความสว่างจะควบคุมความเข้ม/ความสว่างของสี โดย 0% หมายถึงพิกเซลสีดำ ส่วน 100% หมายถึงพิกเซลสีขาว ความอิ่มตัวจะควบคุมความสว่างหรือความสดของสี โดยระบบจะสร้างสีเทาที่มีความอิ่มตัว 0% และสร้างสีสันสดใสโดยใช้ค่า 100%

กราฟิก HSL

เนื่องจาก HSL เป็นมาตรฐานล่าสุด คุณจึงอาจต้องรองรับเบราว์เซอร์รุ่นเก่าต่อไป ซึ่งทำได้ผ่านการแปลงพื้นที่สี โค้ดต่อไปนี้จะยอมรับออบเจ็กต์ HSL { H: 360, S: 100, L: 100} และแสดงผลออบเจ็กต์ RGB { R: 255, G: 255, B: 255 } จากนั้น คุณจะใช้ค่าเหล่านั้นเพื่อสร้างสตริง rgb หรือ rgba ได้ ดูข้อมูลเชิงลึกเพิ่มเติมได้ในบทความที่เป็นประโยชน์ของ Wikipedia เกี่ยวกับ HSL

// HSL (1978) = H: Hue / S: Saturation / L: Lightness
HSL_RGB = function (o) { // { H: 0-360, S: 0-100, L: 0-100 }
var H = o.H / 360,
    S = o.S / 100,
    L = o.L / 100,
    R, G, B, _1, _2;

function Hue_2_RGB(v1, v2, vH) {
if (vH < 0) vH += 1;
if (vH > 1) vH -= 1;
if ((6 * vH) < 1) return v1 + (v2 - v1) * 6 * vH;
if ((2 * vH) < 1) return v2;
if ((3 * vH) < 2) return v1 + (v2 - v1) * ((2 / 3) - vH) * 6;
return v1;
}

if (S == 0) { // HSL from 0 to 1
R = L * 255;
G = L * 255;
B = L * 255;
} else {
if (L < 0.5) {
    _2 = L * (1 + S);
} else {
    _2 = (L + S) - (S * L);
}
_1 = 2 * L - _2;

R = 255 * Hue_2_RGB(_1, _2, H + (1 / 3));
G = 255 * Hue_2_RGB(_1, _2, H);
B = 255 * Hue_2_RGB(_1, _2, H - (1 / 3));
}

return {
R: R,
G: G,
B: B
};
};

การสร้างภาพเคลื่อนไหวด้วย requestAnimationFrame

ก่อนหน้านี้ หากต้องการสร้างภาพเคลื่อนไหวใน JavaScript คุณจะมี 2 ตัวเลือก ได้แก่ setTimeout และ setInterval

window.requestAnimationFrame เป็นมาตรฐานใหม่ที่จะมาแทนที่ทั้ง 2 รายการนี้ ซึ่งจะช่วยประหยัดไฟฟ้า (และทำให้คอมพิวเตอร์ทำงานช้าลงเพียงไม่กี่วินาที) โดยการอนุญาตให้เบราว์เซอร์ควบคุมภาพเคลื่อนไหวตามทรัพยากรที่มีอยู่ ฟีเจอร์สําคัญบางอย่างมีดังนี้

  • เมื่อผู้ใช้ออกจากเฟรม ภาพเคลื่อนไหวอาจช้าลงหรือหยุดไปเลย เพื่อไม่ให้ใช้ทรัพยากรที่ไม่จำเป็น
  • อัตราเฟรมสูงสุดคือ 60 FPS เพราะเรื่องนี้ถือว่าอยู่เหนือระดับที่มนุษย์สังเกตเห็นได้ (มนุษย์ส่วนใหญ่ที่มี 30FPS จะเห็นภาพเคลื่อนไหวที่ "ลื่นไหล")

ณ เวลาที่เขียน ต้องใช้คำนำหน้าเฉพาะของผู้ให้บริการเพื่อใช้ requestAnimationFrame Paul Ireland สร้างเลเยอร์ shim ที่รองรับการใช้งานข้ามแพลตฟอร์มใน requestAnimationFrame สำหรับ สร้างภาพเคลื่อนไหวอัจฉริยะ โดยมีรายละเอียดดังนี้

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
return  window.requestAnimationFrame       || 
        window.webkitRequestAnimationFrame || 
        window.mozRequestAnimationFrame    || 
        window.oRequestAnimationFrame      || 
        window.msRequestAnimationFrame     || 
        function(/* function */ callback, /* DOMElement */ element){
        window.setTimeout(callback, 1000 / 60);
        };
})();

แนวคิดนี้ยังอาจพัฒนาไปอีกขั้นด้วยการใช้ Polyfill เช่น requestAnimationFrame.js (มีฟีเจอร์บางอย่างที่ต้องดำเนินการ) ซึ่งจะรองรับเบราว์เซอร์รุ่นเก่าได้มากขึ้นขณะเปลี่ยนไปใช้มาตรฐานใหม่นี้

(function animate() {
var i = 50;
while(i--) {
    if (n > endpos) return;

    n += definition;
    ctx.globalAlpha = (0.5 - (n + startpos) / endpos) * alpha;
    if (doColorCycle) {
        hue = n + color;
        ctx.strokeStyle = "hsl(" + (hue % 360) + ",99%,50%)"; // iterate hue
    }
    var x = cos(n / cosdiv) * n * cosmult; // cosine
    var y = sin(n / sindiv) * n * sinmult; // sin
    ctx.strokeText(text, x + xoffset, y + yoffset); // draw rainbow text
}
timeout = window.requestAnimationFrame(animate, 0);
})();
กราฟิกโน้ตเบลอ
กราฟิกภาพเคลื่อนไหว
กราฟิกเมทริกซ์

แหล่งที่มา

การสนับสนุนจากผู้ให้บริการเบราว์เซอร์ต่างๆ ช่วยให้มั่นใจได้ว่า<canvas>จะมีอนาคตอย่างแน่นอน โดยสามารถพอร์ตไปยังไฟล์ปฏิบัติการของ iPhone/Android/เดสก์ท็อปได้โดยใช้ PhoneGap หรือ

Titanium