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

Michael Deal
Michael Deal

Lý lịch trích ngang của tôi

Tôi biết đến <canvas> vào năm 2006 khi Firefox phiên bản 2.0 được phát hành. 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 ứng dụng web <canvas> đầu tiên; Color Sphere (2007). Điều này đã đưa tôi vào thế giới của màu sắc và những yếu tố đồ hoạ nguyên bản; truyền cảm hứng cho việc tạo ra Sketchpad (2007-2008) với nỗ lực tập hợp một ứng dụng "tốt hơn là Paint" trong 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 bạn bè lâu năm của tôi là Charles Pritchard. Chúng tôi đang phát triển Darkroom trong HTML5 <canvas>. Darkroom là một ứng dụng chia sẻ ảnh không làm hỏng ảnh, kết hợp sức mạnh của các 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ữ in trên vải canvas.

<canvas> mang đến cho lập trình viên Javascript toàn quyền kiểm soát màu sắc, vectơpixel trên màn hình – thành phần hình ảnh của màn hình.

Các ví dụ sau đây xử lý một phần trong <canvas> chưa được chú ý nhiều; tạo hiệu ứng văn bản. Bạn có thể tạo nhiều hiệu ứng văn bản trong <canvas> như bạn có thể tưởng tượng – các bản minh hoạ này chỉ đề cập đến một phần nhỏ những hiệu ứng có thể tạo được. 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ể được á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>.
Các hiệu ứng văn bản giống như CSS trong <canvas> tạo mặt nạ cắt, tìm các chỉ số trong <canvas> và sử dụng thuộc tính bóng.
Cầu vồng neon, phản chiếu sọc – hiệu ứng tạo chuỗi.
Các 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 bên trong và bóng ngoài trong <canvas>
Hiện một tính năng ít được biết đến; sử dụng chiều kim đồng hồ so với chiều kim đồng hồ ngược để tạo bóng đổ ngược (bóng đổ bên trong).
Vũ trụ – hiệu ứng tạo sinh.
Hiệu ứng văn bản
dựa trên tính năng 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 border-radius, web-gradients và các tính năng khác) là khả năng tạo bóng. Bạn cần 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 pháp; box-shadow cho các phần tử hộp, chẳng hạn như div, span, v.v. và text-shadow (bóng văn bản) cho nội dung văn bản.

<canvas> có một loại bóng; đượ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 nhấn vào bốn 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; // số nguyên
Khoảng cách bóng theo chiều ngang, so với văn bản.
ctx.shadowOffsetY = 0; // số nguyên
Khoảng cách theo chiều dọc của bóng, so với văn bản.
ctx.shadowBlur = 10; // số nguyên
Hiệu ứng làm mờ bóng, giá trị càng lớn thì độ mờ càng lớn.

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

Đồ hoạ 3D CSS

Hiệu ứng 3D lập thể (xem Hình ảnh anaglyph để biết thêm thông tin) là ví dụ về một dòng mã đơn giản, được sử dụng hiệu quả. Với dòng CSS sau, chúng tôi có thể tạo ảo ảnh về chiều sâu khi xem bằng kính 3D màu đỏ/lục lam (loại kính họ mang đến cho bạn trong 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ó shadow-blur (giá trị thứ ba), vì vậy, không có lý do gì để thực sự chạy shadow, 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. EM không được hỗ trợ trong <canvas>, vì vậy, bạn sẽ phải chuyển đổi các EM thành PX. Chúng ta có thể tìm 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 ví dụ: để 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 một cách chính xác. Do hiệu ứng Neon dựa vào nhiều bóng, nên chúng ta sẽ gặp sự cố; 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 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 bị răng cưa.

Đồ 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 là vô ích; 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 thay, có một cách để giải quyết vấn đề này, chúng ta có thể vẽ độ lệch bóng khỏi văn bản, tách riêng chúng (để chúng không chồng chéo), từ đó ẩn văn bản ra 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 việc vẽ fillText ở vị trí đầu tiên (trong khi cho phép vẽ bóng) bằng cách thêm một đường cắt. Để tạo đường dẫn cắt xung quanh văn bản, chúng ta cần biết chiều cao của văn bản (trước đây gọi là "em-height" là chiều cao của chữ cái "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 CSS hack-ardry (xem Chỉ số kiểu chữ để biết thêm các cách khắc phục việc triển khai <canvas> cũ 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 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á trong quá trình thực hiện - nếu một bóng không bị làm mờ, FillText có thể được sử dụng với cùng một hiệu ứng, 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ạ; bằng 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ó thể liên kết với nhiều kiểu. Bạn có thể sử dụng các hiệu ứng bóng tương tự trên bất kỳ đối tượng vectơ nào, từ WebFonts đế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

Thời gian gián đoạn (tiếp nối khi đẩy pixel)

Khi viết phần này của bài viết, ví dụ về Kính lúp 3D đã khiến tôi tò mò. Bạn có thể 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 độ hơi khác nhau không? 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 (data) với kênh màu xanh lục lam của hình ảnh thứ hai (data2):

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

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

Kính 3D

Hiệu ứng chuỗi neon-rainbow, zebra-reflection

Việc tạo chuỗi nhiều hiệu ứng trong <canvas> có thể đơ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 (tối hơn) và lighter (sáng hơn) có thể được coi là chế độ kết hợp lớp; 10 thao tác khác được áp dụng cho các lớp dưới dạng mặt nạ alpha (một lớp sẽ xoá các pixel của lớp khác). 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 các lớp đó theo những cách mới mẻ và thú vị:

Đồ hoạ 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 phần lớn phổ màu và nhiều mức độ trong suốt alpha để xem chi tiết những điều cần biết. Bạn nên tham khảo tài liệu tham khảo globalCompositeOperation của Mozilla để biết nội dung mô tả dạ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 toán tử 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". Sáng hơn kết hợp các pixel được nối tương tự như cách kết hợp ánh sáng; khi ánh sáng đỏ, xanh lục và trắng có 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 tốt hơn và các cạnh mượt mà hơn. Lighter đã được 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 để bàn HTML5 có tại http://weavesilk.com/. Một trong các 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 của chế độ này.

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

Hiệu ứng nhấp nháy Neon-Rainbow

Trong bản minh hoạ sau, chúng ta sẽ tạo hiệu ứng giống như Photoshop với ánh sáng cầu vồng neon có đường viền bị giật bằng cách nối các hiệu ứng với nhau bằng globalCompositeOperation (nguồn vào, sáng hơn và tối hơn). Bản minh hoạ này là phần tiếp theo của bản minh hoạ "Hiệu ứng đổ 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):

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

Hiệu ứng phản chiếu Zebra

Hiệu ứng phản chiếu Zebra được lấy cảm hứng từ tài nguyên tuyệt vời của WebDesignerWall về cách làm mới trang bằng CSS. Điều này giúp ý tưởng này đi xa hơn một chút, tạo ra một "hiệu ứng phản chiếu" cho văn bản – chẳng hạn như nội dung 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 (shine); điều này minh hoạ khả năng áp dụng nhiều loại màu tô cho mỗi đối tượng vectơ:

Hiệu ứng 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();
};

Bóng trong/ngoài trong Canvas

Thông số kỹ thuật <canvas> không ảnh hưởng đến đối tượng của bóng "bên trong" so với bóng "bên ngoài". Trên thực tế, ở lần đầu xuất hiện, bạn có thể thấy bóng "bên trong" không được hỗ trợ. Không phải vậy. Việc bật tính năng này hơi phức tạp 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 xoắn theo chiều kim đồng hồ so với quy tắc xoắn ngược chiều kim đồng hồ. Để thực hiện việc này, bạn tạo "bóng bên trong" bằng cách vẽ hình chữ nhật của vùng chứa, sau đó vẽ một hình cắt bỏ theo quy tắc ngược dòng, tức là tạo ra nghịch đảo của hình dạng.

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

Bóng bên trong/bên 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();
};

Bạn có thể thấy từ các ví dụ này, bằng cách sử dụng globalCompositeOperation, chúng ta có thể tạo hiệu ứng chuỗi với nhau, tạo ra các hiệu ứng phức tạp hơn (sử dụng tính năng che và kết hợp). Màn hình là nơi bạn có thể thỏa sức sáng tạo ;)

Spaceage – hiệu ứng tạo sinh

Trong <canvas>, đi từ ký tự Unicode 0x2708:

Unicode đồ hoạ

…thành ví dụ được tô bóng sau:

Ví dụ về vùng được tô bóng

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

Bằng cách ánh xạ vị trí XY của các phần tử với sóng sin/cosin và chuyển đổi qua lạ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ề "nguy hiểm" sau:

Hiệu ứng chuyển đổi HSL

HSL: Hue, Bão hòa, Độ sáng (1978)

HSL là một định dạng mới được hỗ trợ trong thông số kỹ thuật CSS3. Trong trường hợp 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; để luân phiên qua quang phổ màu, chúng ta chỉ cần tăng "hue" (tông màu) từ 360; tông màu được liên kết với quang phổ theo hình trụ. Độ sáng kiểm soát độ đậm/nhạt của màu; 0% cho biết một pixel màu đen, trong khi 100% cho biết một pixel màu trắng. Độ bão hoà kiểm soát độ sáng hoặc độ sống động của màu sắc; màu xám được tạo với độ bão hoà là 0% và màu sống động đượ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ể tiếp tục hỗ trợ các trình duyệt cũ thông qua việc chuyển đổi không gian màu. Mã sau đây chấp nhận một đối tượng HSL { H: 360, S: 100, L: 100} và xuất 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 chuyên sâu 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: setTimeoutsetInterval.

window.requestAnimationFrame, là tiêu chuẩn mới ở đây để thay thế cả hai; giúp tiết kiệm điện năng trên thế giới (và cho máy tính của bạn một vài nhịp) 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, ả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 có giới hạn là 60 khung hình/giây. Lý do là 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 ở tốc độ 30 khung hình/giây sẽ thấy ảnh động "mượt mà").

Tại thời điểm viết bài, bạn cần có tiền tố cụ thể của nhà cung cấp để sử dụng requestAnimationFrame. Paul Irish đã tạo một lớp đệm có hỗ trợ đa nhà cung cấp, trong requestAnimationFrame để tạo ả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);
        };
})();

Mở rộng một chút, bạn có thể kết hợp điều này với một tính năng lấp đầy nhiều lần như requestAnimationFrame.js (có một số 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);
})();
Hình ảnh làm mờ ghi chú
Đồ hoạ ảnh động
Đồ hoạ ma trận

Mã nguồn

Với sự hỗ trợ của toàn bộ 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 trên iPhone/Android/Máy tính để bàn bằng PhoneGap, hoặc

Titan.