Hiệu ứng kiểu chữ trong canvas

Michael Deal
Michael Deal

Nền tảng của tôi

<canvas> bắt đầu được tôi chú ý vào năm 2006 khi Firefox phiên bản 2.0 ra mắt. Một bài viết trên Ajaxian mô tả ma trận biến đổi đã truyền cảm hứng cho tôi tạo ra ứng dụng web <canvas> đầu tiên của mình; Color Sphere (2007). Điều này đã đưa tôi vào thế giới của màu sắc và các hình khối đồ hoạ cơ bản, truyền cảm hứng cho tôi tạo ra Sketchpad (2007-2008) nhằm nỗ lực xây dựng một ứng dụng "tốt hơn Paint" trên trình duyệt. Những thử nghiệm này cuối cùng đã dẫn đến việc thành lập công ty khởi nghiệp Mugtug cùng với người bạn lâu năm của tôi, Charles Pritchard. Chúng tôi đang phát triển Darkroom bằng HTML5 <canvas>. Darkroom là một ứng dụng chia sẻ ảnh không phá huỷ, kết hợp sức mạnh của bộ lọc dựa trên pixel với kiểu chữ và bản vẽ dựa trên vectơ.

Giới thiệu

Hình ảnh biểu ngữ Canvas.

<canvas> giúp các lập trình viên Javascript kiểm soát hoàn toàn màu sắc, vectơ và pixel trên màn hình – cấu trúc hình ảnh của màn hình.

Các ví dụ sau đây đề cập đến một khía cạnh trong <canvas> chưa được chú ý nhiều: tạo hiệu ứng văn bản. Bạn có thể tạo ra nhiều hiệu ứng văn bản trong <canvas> theo trí tưởng tượng của mình. Những bản minh hoạ này chỉ trình bày một phần nhỏ trong số những hiệu ứng có thể tạo ra. Mặc dù chúng ta đang xử lý "văn bản" trong bài viết này, nhưng các phương thức này có thể áp dụng cho bất kỳ đối tượng vectơ nào; tạo hình ảnh thú vị trong trò chơi và các ứng dụng khác:

Bóng văn bản trong <canvas>.
Hiệu ứng văn bản giống CSS trong <canvas> tạo mặt nạ cắt, tìm chỉ số trong <canvas> và sử dụng thuộc tính bóng.
Cầu vồng neon, phản chiếu ngựa vằn – hiệu ứng chuỗi.
Hiệu ứng văn bản giống như Photoshop trong <canvas> ví dụ về cách sử dụng globalCompositeOperation, createLinearGradient, createPattern.
Bóng trong và bóng ngoài trong <canvas>
Tiết lộ một tính năng ít người biết; sử dụng hướng cuộn theo chiều kim đồng hồ so với ngược chiều kim đồng hồ để tạo hiệu ứng ngược lại của bóng đổ (bóng bên trong).
Spaceage – hiệu ứng tạo sinh.
Hiệu ứng văn bản dựa trên AI tạo sinh trong <canvas> sử dụng chu kỳ màu hsl() và window.requestAnimationFrame để tạo cảm giác chuyển động.

Đổ bóng văn bản trong canvas

Một trong những tính năng bổ sung mà tôi yêu thích nhất trong thông số kỹ thuật CSS3 (cùng với bán kính đường viền, dải màu web và các tính năng khác) là khả năng tạo bóng. Bạn cần phải nhận ra sự khác biệt giữa bóng CSS và bóng <canvas>, cụ thể là:

CSS sử dụng hai phương thức: box-shadow cho các phần tử hộp, chẳng hạn như div, span, v.v.; và text-shadow cho nội dung văn bản.

<canvas> có một loại bóng; loại bóng này được dùng cho tất cả các đối tượng vectơ; ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText, v.v. Để tạo bóng trong <canvas>, hãy khai thác 4 thuộc tính sau:

ctx.shadowColor = "red" // string
Màu của bóng; RGB, RGBA, HSL, HEX và các giá trị đầu vào khác đều hợp lệ.
ctx.shadowOffsetX = 0; // integer
Khoảng cách theo chiều ngang của bóng, liên quan đến văn bản.
ctx.shadowOffsetY = 0; // integer
Khoảng cách theo chiều dọc của bóng, tương ứng với văn bản.
ctx.shadowBlur = 10; // integer
Hiệu ứng làm mờ cho bóng, giá trị càng lớn thì độ mờ càng cao.

Để bắt đầu, hãy xem cách <canvas> có thể mô phỏng hiệu ứng CSS. Khi tìm kiếm trên Google Hình ảnh cho "css text-shadow", chúng tôi tìm thấy một vài bản minh hoạ tuyệt vời để mô phỏng: Line25, Stereoscopic và Shadow 3D.

Đồ hoạ 3D của CSS

Hiệu ứng 3D lập thể (xem Ảnh nổi để biết thêm thông tin) là một ví dụ về một dòng mã đơn giản nhưng có tác dụng rất lớn. Với dòng CSS sau, chúng ta có thể tạo ảo giác về chiều sâu khi xem bằng kính 3D màu đỏ/lục lam (loại kính bạn được phát khi xem phim 3D):

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

Có hai điều cần lưu ý khi chuyển đổi chuỗi này thành <canvas>:

  1. Không có độ mờ bóng (giá trị thứ ba), vì vậy không có lý do gì để thực sự chạy bóng, vì fillText sẽ tạo ra kết quả tương tự:
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> không hỗ trợ EM nên bạn sẽ phải chuyển đổi EM thành PX. Chúng ta có thể tìm thấy tỷ lệ chuyển đổi để chuyển đổi giữa PT, PC, EM, EX, PX, v.v. bằng cách tạo một phần tử có cùng thuộc tính phông chữ trong DOM và đặt chiều rộng thành định dạng cần đo lường; hoặc để ghi lại lượt chuyển đổi EM -> PX, chúng ta sẽ đo lường phần tử DOM bằng "height: 1em", offsetHeight thu được sẽ là số PX trong mỗi 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>

Ngăn chặn phép nhân alpha

Trong một ví dụ phức tạp hơn, chẳng hạn như hiệu ứng Neon trên Line25, bạn phải sử dụng thuộc tính shadowBlur để mô phỏng hiệu ứng đúng cách. Vì hiệu ứng Neon dựa vào nhiều bóng, nên chúng ta gặp phải một vấn đề: trong <canvas>, mỗi đối tượng vectơ chỉ có thể có một bóng. Vì vậy, để vẽ nhiều bóng, bạn phải vẽ nhiều phiên bản của văn bản chồng lên nhau. Điều này dẫn đến việc nhân alpha và cuối cùng là các cạnh lởm chởm.

Đồ hoạ neon

Tôi đã thử chạy ctx.fillStyle = "rgba(0,0,0,0)" hoặc "transparent" để ẩn văn bản trong khi hiển thị bóng đổ… tuy nhiên, nỗ lực này không thành công; vì bóng đổ là phép nhân của alpha fillStyle, nên bóng đổ không bao giờ có thể mờ hơn fillStyle.

May mắn là có một cách để giải quyết vấn đề này: chúng ta có thể vẽ bóng lệch khỏi văn bản, tách biệt chúng (để chúng không chồng chéo lên nhau) và từ đó ẩn văn bản khỏi màn hình:

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

Cắt xung quanh một khối văn bản

Để dọn dẹp một chút, chúng ta có thể ngăn không cho fillText được vẽ ngay từ đầu (trong khi vẫn cho phép vẽ bóng) bằng cách thêm một đường dẫn cắt. Để tạo đường dẫn cắt bao quanh văn bản, chúng ta cần biết chiều cao của văn bản (gọi là "em-height", trước đây là chiều cao của chữ "M" trên máy in) và chiều rộng của văn bản. Chúng ta có thể lấy chiều rộng bằng ctx.measureText().width, tuy nhiên, ctx.measureText().height không tồn tại.

May mắn thay, thông qua thủ thuật CSS (xem Chỉ số kiểu chữ để biết thêm cách khắc phục các phương thức triển khai <canvas> cũ hơn bằng cách sử dụng các phép đo CSS), chúng ta có thể tìm thấy chiều cao của văn bản thông qua việc đo lường offsetHeight của <span> có cùng thuộc tính phông chữ:

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

Từ đó, chúng ta có thể tạo một hình chữ nhật để dùng làm đường dẫn cắt; bao quanh "bóng" trong khi xoá hình dạng giả.

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

Kết hợp tất cả lại với nhau và tối ưu hoá khi thực hiện – nếu bóng không bị mờ, bạn có thể sử dụng fillText để đạt được hiệu ứng tương tự, giúp chúng ta không phải thiết lập mặt nạ cắt:

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

Vì bạn sẽ không muốn nhập tất cả các lệnh <canvas> này theo cách thủ công, nên tôi đã đưa một trình phân tích cú pháp bóng văn bản đơn giản vào nguồn minh hoạ; theo cách này, bạn có thể cung cấp cho trình phân tích cú pháp này các lệnh CSS và yêu cầu trình phân tích cú pháp này tạo các lệnh <canvas>. Giờ đây, các phần tử <canvas> của chúng ta có một loạt kiểu mà chúng có thể liên kết. Bạn có thể sử dụng các hiệu ứng đổ bóng tương tự này trên mọi đối tượng vectơ, từ WebFont đến các hình dạng phức tạp được nhập từ SVG, đến các hình dạng vectơ tạo sinh, v.v.

Đổ bóng văn bản trong hiệu ứng canvas

Tạm nghỉ (tiếp tuyến khi đẩy pixel)

In writing this section of the article, the Stereoscopic example made me curious. Sẽ khó đến mức nào để tạo hiệu ứng màn hình phim 3D bằng <canvas> và hai hình ảnh được chụp từ các góc nhìn hơi khác nhau? Rõ ràng là không quá khó. Hạt nhân sau đây kết hợp kênh màu đỏ của hình ảnh đầu tiên (dữ liệu) với kênh màu lục lam của hình ảnh thứ hai (dữ liệu2):

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

Giờ đây, chỉ cần dán hai chiếc iPhone lên trán, nhấp vào "quay video" cùng lúc, là chúng ta có thể tạo phim 3D của riêng mình bằng HTML5. Có ai xung phong không?

Kính 3D

Cầu vồng neon, phản chiếu ngựa vằn – hiệu ứng chuỗi

Việc liên kết nhiều hiệu ứng trong <canvas> có thể rất đơn giản, nhưng bạn cần có kiến thức cơ bản về globalCompositeOperation (GCO). Để so sánh các thao tác với GIMP (hoặc Photoshop): có 12 GCO trong <canvas> darker và lighter có thể được coi là chế độ hoà trộn lớp; 10 thao tác còn lại được áp dụng cho các lớp dưới dạng mặt nạ alpha (một lớp xoá các pixel của lớp kia). globalCompositeOperation liên kết các "lớp" (hoặc trong trường hợp của chúng ta là các chuỗi mã) với nhau, kết hợp chúng theo những cách mới và thú vị:

Đồ hoạ về hiệu ứng tạo chuỗi

Biểu đồ globalCompositeOperation cho thấy các chế độ GCO đang hoạt động; biểu đồ này sử dụng một phần lớn quang phổ màu và nhiều mức độ trong suốt alpha để xem chi tiết những gì sẽ xảy ra. Bạn nên xem tài liệu tham khảo globalCompositeOperation của Mozilla để biết nội dung mô tả bằng văn bản. Để nghiên cứu thêm, bạn có thể tìm hiểu cách hoạt động của thao tác này trong bài viết Kết hợp hình ảnh kỹ thuật số của Porter Duff.

Chế độ yêu thích của tôi là globalCompositeOperation="lighter". Lighter kết hợp các pixel được thêm vào tương tự như cách ánh sáng kết hợp; khi ánh sáng đỏ, xanh lục và trắng ở cường độ tối đa, chúng ta sẽ thấy ánh sáng trắng. Đây là một tính năng thú vị để thử nghiệm, đặc biệt là khi <canvas> được đặt thành globalAlpha thấp; cho phép kiểm soát chi tiết hơn và các cạnh mượt mà hơn. Lighter đã được đưa vào sử dụng cho nhiều mục đích, gần đây nhất là một trình tạo hình nền máy tính HTML5 tại http://weavesilk.com/. Một trong những bản minh hoạ của tôi, Breathing Galaxies (JS1k), cũng sử dụng chế độ sáng hơn – vẽ các mẫu từ hai ví dụ này, bạn sẽ bắt đầu thấy hiệu ứng mà chế độ này mang lại.

Xử lý trình duyệt globalCompositeOperation.

Hiệu ứng cầu vồng neon rung lắc

Trong bản minh hoạ sau, chúng ta sẽ tạo hiệu ứng phát sáng cầu vồng neon giống như Photoshop với đường viền rung, bằng cách liên kết các hiệu ứng với nhau bằng globalCompositeOperation (source-in, sáng hơn và tối hơn). Bản minh hoạ này là một bước tiến của bản minh hoạ "Bóng văn bản trong <canvas>", sử dụng cùng một chiến lược để tách bóng khỏi văn bản (xem phần trước):

Cầu vồng rung lắc
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();
};

Hiệu ứng phản chiếu sọc vằn

Hiệu ứng Zebra Reflection được lấy cảm hứng từ tài nguyên tuyệt vời của WebDesignerWall về cách làm cho trang của bạn trở nên thú vị hơn bằng CSS. Điều này đưa ý tưởng đi xa hơn một chút, tạo ra một "hình ảnh phản chiếu" cho văn bản – chẳng hạn như những gì bạn có thể thấy trong iTunes. Hiệu ứng này kết hợp fillColor (màu trắng), createPattern (zebra.png) và linearGradient (độ bóng); điều này minh hoạ khả năng áp dụng nhiều kiểu tô cho mỗi đối tượng vectơ:

Hiệu ứng sọc vằn
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();
};

Bóng trong/ngoài trong canvas

Thông số kỹ thuật của <canvas> không đề cập đến chủ đề bóng "bên trong" so với bóng "bên ngoài". Trên thực tế, khi mới nhìn, bạn có thể cho rằng bóng "bên trong" không được hỗ trợ. Điều này không đúng. Chỉ là hơi khó bật một chút ;) Như đã đề xuất trong một bài đăng gần đây của F1LT3R, bạn có thể tạo bóng bên trong bằng cách sử dụng các thuộc tính riêng biệt của quy tắc cuộn theo chiều kim đồng hồ và ngược chiều kim đồng hồ. Để làm việc này, bạn tạo một "bóng bên trong" bằng cách vẽ hình chữ nhật vùng chứa, sau đó sử dụng các quy tắc cuộn ngược lại để vẽ hình dạng cắt ra – tạo hình dạng đảo ngược.

Ví dụ sau đây cho phép tạo kiểu cho inner-shadow và fillStyle bằng màu+chuyển màu+mẫu cùng một lúc. Bạn có thể chỉ định chế độ xoay mẫu riêng lẻ; hãy lưu ý rằng các sọc ngựa vằn hiện vuông góc với nhau. Mặt nạ cắt có kích thước bằng hộp giới hạn được dùng để loại bỏ nhu cầu về một vùng chứa siêu lớn để bao quanh hình dạng cắt, giúp cải thiện tốc độ bằng cách ngăn các phần không cần thiết của bóng đổ được xử lý.

Bóng trong/ngoài
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();
};

Qua các ví dụ này, bạn có thể thấy rằng bằng cách sử dụng globalCompositeOperation, chúng ta có thể kết hợp các hiệu ứng với nhau, tạo ra các hiệu ứng phức tạp hơn (sử dụng mặt nạ và hiệu ứng hoà trộn). Màn hình là của bạn ;)

Spaceage – hiệu ứng tạo sinh

Trong <canvas>, chuyển từ ký tự unicode 0x2708:

Ký tự đồ hoạ Unicode

…sang ví dụ có bóng đổ này:

Ví dụ về bóng đổ

…có thể đạt được bằng cách gọi nhiều lần đến ctx.strokeText() với lineWidth mỏng (0,25), đồng thời giảm dần độ lệch x và alpha; mang lại cảm giác chuyển động cho các phần tử vectơ.

Bằng cách liên kết vị trí XY của các phần tử với sóng sin/cosin và chuyển đổi giữa các màu bằng thuộc tính HSL, chúng ta có thể tạo ra các hiệu ứng thú vị hơn, chẳng hạn như ví dụ về "mối nguy hiểm sinh học" này:

Hiệu ứng xoay vòng HSL

HSL: Sắc độ, độ rực màu, độ sáng (1978)

HSL là một định dạng mới được hỗ trợ trong thông số kỹ thuật CSS3. Trong khi HEX được thiết kế cho máy tính, thì HSL được thiết kế để con người có thể đọc được.

Để minh hoạ sự dễ dàng của HSL; để chuyển qua phổ màu, chúng ta chỉ cần tăng "màu sắc" từ 360; màu sắc được liên kết với phổ màu theo kiểu hình trụ. Độ sáng kiểm soát mức độ tối/sáng của màu; 0% cho biết pixel màu đen, còn 100% cho biết pixel màu trắng. Độ rực màu kiểm soát độ sáng hoặc độ rực rỡ của màu; màu xám được tạo bằng độ rực màu 0% và màu rực rỡ được tạo bằng giá trị 100%.

Đồ hoạ HSL

Vì HSL là một tiêu chuẩn mới, nên bạn có thể muốn tiếp tục hỗ trợ các trình duyệt cũ. Bạn có thể thực hiện việc này thông qua tính năng chuyển đổi không gian màu sắc. Đoạn mã sau đây chấp nhận một đối tượng HSL { H: 360, S: 100, L: 100} và xuất ra một đối tượng RGB { R: 255, G: 255, B: 255 }. Từ đó, bạn có thể sử dụng các giá trị đó để tạo chuỗi rgb hoặc rgba. Để biết thêm thông tin chi tiết, hãy xem bài viết sâu sắc của Wikipedia về 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
};
};

Tạo ảnh động bằng requestAnimationFrame

Trước đây, để tạo ảnh động trong Javascript, bạn có hai lựa chọn: setTimeout và setInterval.

window.requestAnimationFrame, là tiêu chuẩn mới để thay thế cả hai; tiết kiệm điện năng cho thế giới (và máy tính của bạn một vài nhịp tim) bằng cách cho phép trình duyệt điều chỉnh ảnh động dựa trên tài nguyên có sẵn. Một số tính năng quan trọng bao gồm:

  • Khi người dùng thoát khỏi khung hình, ảnh động có thể chậm lại hoặc dừng hoàn toàn để tránh sử dụng tài nguyên không cần thiết.
  • Tốc độ khung hình bị giới hạn ở mức 60 khung hình/giây. Lý do là vì tốc độ này cao hơn nhiều so với mức mà con người có thể nhận thấy (hầu hết mọi người đều thấy ảnh động "mượt mà" ở tốc độ 30 khung hình/giây).

Tại thời điểm viết bài, bạn cần có tiền tố dành riêng cho nhà cung cấp để sử dụng requestAnimationFrame. Paul Irish đã tạo một lớp shim có hỗ trợ nhiều nhà cung cấp, trong requestAnimationFrame cho ảnh động thông minh:

// 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);
        };
})();

Tiến xa hơn một chút, những người có tham vọng hơn có thể kết hợp điều này với một poly-fill như requestAnimationFrame.js (có một vài tính năng cần được xử lý) để hỗ trợ các trình duyệt cũ hơn ở mức độ cao hơn, đồng thời chuyển sang tiêu chuẩn mới này.

(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);
})();
Đồ hoạ mờ ghi chú
Đồ hoạ ảnh động
Đồ hoạ ma trận

Mã nguồn

Với sự hỗ trợ từ các nhà cung cấp trình duyệt, không có câu hỏi nào về tương lai của <canvas>, nó có thể được chuyển sang các tệp thực thi iPhone/Android/Máy tính bằng PhoneGap hoặc

Titanium.