使用 Web Workers - Web API 接口参考

使用 Web Workers
==============

Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外，它们可以使用 `XMLHttpRequest`（尽管 `responseXML` 和 `channel` 属性总是为空）或 `fetch`（没有这些限制）执行 I/O。一旦创建，一个 worker 可以将消息发送到创建它的 JavaScript 代码，通过将消息发布到该代码指定的事件处理器（反之亦然）。

本文详细介绍了如何使用 web worker。

Web Workers API
---------------

一个 worker 是使用一个构造函数创建的一个对象（例如 `Worker()`）运行一个命名的 JavaScript 文件——这个文件包含将在 worker 线程中运行的代码; worker 运行在另一个全局上下文中，不同于当前的`window`。因此，在 `Worker` 内通过 `window` 获取全局作用域（而不是`self`）将返回错误。

在专用 worker 的情况下，`DedicatedWorkerGlobalScope` 对象代表了 worker 的上下文（专用 worker 是指标准 worker 仅在单一脚本中被使用；共享 worker 的上下文是 `SharedWorkerGlobalScope` (en-US) 对象）。一个专用 worker 仅能被首次生成它的脚本使用，而共享 worker 可以同时被多个脚本使用。

**备注：** 参见 Web Workers API 落地页以获取 worker 的参考文档和更多指引。

在 worker 线程中你可以运行任何你喜欢的代码，不过有一些例外情况。比如：在 worker 内，不能直接操作 DOM 节点，也不能使用 `window` 对象的默认方法和属性。但是你可以使用大量 `window` 对象之下的东西，包括 WebSockets，以及 IndexedDB 等数据存储机制。查看 Web Workers 可以使用的函数和类 获取详情。

workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用 `postMessage()` 方法发送各自的消息，使用 `onmessage` 事件处理函数来响应消息（消息被包含在 `message` 事件的 data 属性中）。这个过程中数据并不是被共享而是被复制。

只要运行在同源的父页面中，worker 可以依次生成新的 worker；并且可以使用 `XMLHttpRequest` 进行网络 I/O，但是 `XMLHttpRequest` 的 `responseXML` 和 `channel` 属性总会返回 `null`。

专用 worker
---------

如前文所述，一个专用 worker 仅能被生成它的脚本所使用。这一部分将探讨专用 worker 基础示例（运行专用 worker）中的 JavaScript 代码：将你输入的 2 个数字作乘法。输入的数字会发送给一个专用 worker，由专用 worker 作乘法后，再返回给页面进行展示。

这个例子很小，但是我们决定在保持简单的同时向你介绍基础的 worker 概念。更多的细节会在之后的文章中进行讲解。

### worker 特性检测

为了更好的错误处理控制以及向下兼容，将你的 worker 运行代码包裹在以下代码中是一个很好的想法（main.js）：

```js
if (window.Worker) {
  // …
}
```

### 生成一个专用 worker

创建一个新的 worker 很简单。你需要做的是调用 `Worker()` 构造器，指定一个脚本的 URI 来执行 worker 线程（main.js）：

```js
const myWorker = new Worker("worker.js");
```

### 专用 worker 中消息的接收和发送

你可以通过 `postMessage()` 方法和 `onmessage` 事件处理函数触发 worker 的方法。当你想要向一个 worker 发送消息时，你只需要这样做（main.js）：

```js
first.onchange = () => {
  myWorker.postMessage([first.value, second.value]);
  console.log("Message posted to worker");
};

second.onchange = () => {
  myWorker.postMessage([first.value, second.value]);
  console.log("Message posted to worker");
};
```

这段代码中变量 `first` 和 `second` 代表 2 个 `<input>` 元素；它们当中任意一个的值发生改变时，`myWorker.postMessage([first.value,second.value])` 会将这 2 个值组成数组发送给 worker。你可以在消息中发送许多你想发送的东西。

在 worker 中接收到消息后，我们可以写这样一个事件处理函数代码作为响应（worker.js）：

```js
onmessage = (e) => {
  console.log("Message received from main script");
  const workerResult = `Result: ${e.data[0] \* e.data[1]}`;
  console.log("Posting message back to main script");
  postMessage(workerResult);
};
```

`onmessage` 处理函数允许我们在收到消息时运行一些代码，消息本身在 `message` 事件 `data` 属性进行使用。这里我们简单的对这 2 个数字作乘法处理并再次使用 `postMessage()` 方法，将结果回传给主线程。

回到主线程，我们再次使用 `onmessage` 以响应 worker 回传的消息：

```js
myWorker.onmessage = (e) => {
  result.textContent = e.data;
  console.log("Message received from worker");
};
```

在这里我们获取消息事件的数据，并且将它设置为显示结果段落的 `textContent`，所以用户可以直接看到运算的结果。

**备注：** 在主线程中使用时，`onmessage` 和 `postMessage()` 必须挂在 `worker` 对象上，而在 worker 中使用时不用这样做。原因是，在 worker 内部，worker 是有效的全局作用域。

**备注：** 当一个消息在主线程和 worker 之间传递时，它被复制或者转移了，而不是共享。请参阅worker 中数据的接收与发送：详细介绍 获取更详尽的解释。

### 终止 worker

如果你需要从主线程中立刻终止一个运行中的 worker，可以调用 worker 的 `terminate` 方法：

```js
myWorker.terminate();
```

worker 线程会被立即终止。

### 处理错误

当 worker 出现运行中错误时，它的 `onerror` 事件处理函数会被调用。它会收到一个扩展了 `ErrorEvent` 接口的名为 `error` 的事件。

该事件不会冒泡并且可以被取消；为了防止触发默认动作，worker 可以调用错误事件的 `preventDefault()` 方法。

错误事件有以下三个用户关心的字段：

`message`

可读性良好的错误消息。

`filename`

发生错误的脚本文件名。

`lineno`

发生错误时所在脚本文件的行号。

### 生成 subworker

如果需要的话，worker 能够生成更多的 worker，这就是所谓的 subworker，它们必须托管在同源的父页面内。而且，subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。这使得 worker 更容易记录它们之间的依赖关系。

### 引入脚本与库

Worker 线程能够访问一个全局函数 `importScripts()` 来引入脚本，该函数接受 0 个或者多个 URI 作为参数来引入资源；以下例子都是合法的：

```js
importScripts(); /\* 什么都不引入 \*/
importScripts("foo.js"); /\* 只引入 "foo.js" \*/
importScripts("foo.js", "bar.js"); /\* 引入两个脚本 \*/
importScripts("//example.com/hello.js"); /\* 你可以从其他来源导入脚本 \*/
```

浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。如果脚本无法加载，将抛出 `NETWORK_ERROR` 异常，接下来的代码也无法执行。而之前执行的代码（包括使用 `setTimeout()` 异步执行的代码）依然能够运行。`importScripts()` **之后**的函数声明依然会被保留，因为它们始终会在其他代码之前运行。

**备注：** 脚本的下载顺序不固定，但执行时会按照传入 `importScripts()` 中的文件名顺序进行。这个过程是同步完成的；直到所有脚本都下载并运行完毕，`importScripts()` 才会返回。

共享 worker
---------

一个共享 worker 可以被多个脚本使用——即使这些脚本正在被不同的 window、iframe 或者 worker 访问。这一部分，我们会讨论共享 worker 基础示例（运行共享 worker）中的 JavaScript 代码：该示例与专用 worker 基础示例非常相像，只是有 2 个可用函数被存放在不同脚本文件中：两数相乘函数，以及求平方函数。这两个脚本使用同一个 worker 来完成实际需要的运算。

这里，我们关注一下专用 worker 和共享 worker 之间的区别。在这个示例中有 2 个 HTML 页面，每个页面所包含的 JavaScript 代码使用的是同一个 worker。

**备注：** 如果共享 worker 可以被多个浏览上下文调用，所有这些浏览上下文必须属于同源（相同的协议，主机和端口号）。

**备注：** 在 Firefox 中，共享 worker 不能被私有和非私有 window 对象的 document 所共享（Firefox bug 1177621）。

### 生成一个共享 worker

生成一个新的共享 worker 与生成一个专用 worker 非常相似，只是构造器的名字不同（查看 index.html 和 index2.html）——生成共享 worker 的代码如下：

```js
const myWorker = new SharedWorker("worker.js");
```

一个非常大的区别在于，与一个共享 worker 通信必须通过 `port` 对象——一个确切的打开的端口供脚本与 worker 通信（在专用 worker 中这一部分是隐式进行的）。

在传递消息之前，端口连接必须被显式的打开，打开方式是使用 `onmessage` 事件处理函数或者 `start()` 方法。只有一种情况下需要调用 `start()` 方法，那就是 `message` 事件被 `addEventListener()` 方法使用。

**备注：** 在使用 `start()` 方法打开端口连接时，如果父级线程和 worker 线程需要双向通信，那么它们都需要调用该方法。

### 共享 worker 中消息的接收和发送

现在，消息可以像之前那样发送到 worker 了，但是 `postMessage()` 方法必须被端口对象调用（你会再一次看到 multiply.js 和 square.js 中相似的结构）：

```js
squareNumber.onchange = () => {
  myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
  console.log("Message posted to worker");
};
```

回到 worker 中，这里也有些复杂（worker.js）:

```js
onconnect = (e) => {
  const port = e.ports[0];

  port.onmessage = (e) => {
    const workerResult = `Result: ${e.data[0] \* e.data[1]}`;
    port.postMessage(workerResult);
  };
};
```

首先，当一个端口连接被创建时（例如：在父级线程中，设置 `onmessage` 事件处理函数，或者显式调用 `start()` 方法时），使用 `onconnect` 事件处理函数来执行代码。

使用事件的 `ports` 属性来获取端口并存储在变量中。

然后，为端口添加一个 `onmessage` 处理函数用来做运算并回传结果给主线程。在 worker 线程中设置此 `onmessage` 处理函数也会隐式的打开与主线程的端口连接，因此这里跟前文一样，对 `port.start()` 的调用也是不必要的。

最后，回到主脚本，我们处理消息（你会又一次看到 multiply.js 和 square.js中相似的结构）：

```js
myWorker.port.onmessage = (e) => {
  result2.textContent = e.data;
  console.log("Message received from worker");
};
```

当一条消息通过端口回到 worker，我们将运算结果放入结果段落中合适的地方。

关于线程安全
------

`Worker` 接口会生成真正的操作系统级别的线程，如果你不太小心，那么并发会对你的代码产生有趣的影响。

然而，对于 web worker 来说，与其他线程的通信点会被很小心的控制，这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM，此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿，还真搞不出错误来。

内容安全策略
------

有别于创建它的 document 对象，worker 有它自己的执行上下文。因此普遍来说，worker 并不受限于创建它的 document（或者父级 worker）的内容安全策略。我们来举个例子，假设一个 document 有如下头部声明：

```http
Content-Security-Policy: script-src 'self'
```

这个声明有一部分作用在于，禁止它内部包含的脚本代码使用 `eval()` 方法。然而，如果脚本代码创建了一个 worker，在 worker 上下文中执行的代码却是可以使用 `eval()` 的。

为了给 worker 指定内容安全策略，必须为发送 worker 代码的请求本身设置 Content-Security-Policy 响应标头。

有一个例外情况，即 worker 脚本的源如果是一个全局性的唯一的标识符（例如，它的 URL 协议为 data 或 blob），worker 则会继承创建它的 document 或者 worker 的 CSP。

worker 中数据的接收与发送：详细介绍
---------------------

在主页面与 worker 之间传递的数据是通过**拷贝**，而不是共享来完成的。传递给 `worker` 的对象需要经过序列化，接下来在另一端还需要反序列化。页面与 `worker` **不会共享同一个实例**，最终的结果就是在每次通信结束时生成了数据的**一个副本**。大部分浏览器使用结构化克隆来实现该特性。

在往下进行之前，出于教学的目的，让我们创建一个名为 `emulateMessage()` 的函数，它将模拟在从 `worker` 到主页面（反之亦然）的通信过程中，变量的“*拷贝而非共享*”行为：

```js
function emulateMessage(vVal) {
  return eval(`(${JSON.stringify(vVal)})`);
}

// Tests

// test #1
const example1 = new Number(3);
console.log(typeof example1); // object
console.log(typeof emulateMessage(example1)); // number

// test #2
const example2 = true;
console.log(typeof example2); // boolean
console.log(typeof emulateMessage(example2)); // boolean

// test #3
const example3 = new String("Hello World");
console.log(typeof example3); // object
console.log(typeof emulateMessage(example3)); // string

// test #4
const example4 = {
  name: "Carina Anand",
  age: 43,
};
console.log(typeof example4); // object
console.log(typeof emulateMessage(example4)); // object

// test #5
function Animal(type, age) {
  this.type = type;
  this.age = age;
}
const example5 = new Animal("Cat", 3);
console.log(example5.constructor); // Animal
console.log(emulateMessage(example5).constructor); // Object
```

拷贝而并非共享的那个值称为*消息*（message）。再来谈谈 `worker`，你可以使用 `postMessage()` 将*消息*传递给主线程或从主线程传送回来。`message` 事件的 `data` (en-US) 属性就包含了从 worker 传回来的数据。

**example.html**（主页面）：

```js
const myWorker = new Worker("my\_task.js");

myWorker.onmessage = (event) => {
  console.log(`Worker said : ${event.data}`);
};

myWorker.postMessage("ali");
```

**my\_task.js**（worker 文件）：

```js
postMessage("I'm working before postMessage('ali').");

onmessage = (event) => {
  postMessage(`Hi, ${event.data}`);
};
```

结构化克隆算法可以接收 JSON 数据以及一些 JSON 不能表示的数据——比如循环引用。

### 传递数据的例子

#### 例子 1：传输 JSON 的高级方式和创建一个交换系统

如果你需要传输非常复杂的数据，还要同时在主页与 Worker 内调用多个方法，那么可以考虑创建一个类似下面的系统。

首先，我们创建一个 `QueryableWorker` 的类，它接收 worker 的 URL、一个默认侦听函数和一个错误处理函数作为参数，这个类将会记录所有的侦听的列表并且帮助我们与 worker 进行通信。

```js
function QueryableWorker(url, defaultListener, onError) {
  const instance = this;
  const worker = new Worker(url);
  const listeners = {};

  this.defaultListener = defaultListener ?? (() => {});

  if (onError) {
    worker.onerror = onError;
  }

  this.postMessage = (message) => {
    worker.postMessage(message);
  };

  this.terminate = () => {
    worker.terminate();
  };
}
```

紧接着，我们写出新增和删除侦听的方法。

```js
this.addListeners = (name, listener) => {
  listeners[name] = listener;
};

this.removeListeners = (name) => {
  delete listeners[name];
};
```

这里我们让 worker 处理 2 个这样的简单操作：区别 2 个数字并在 3 秒后弹框提示。为了完成这个操作，我们首先实现一个 `sendQuery` 方法，该方法可以查询 worker 是否真正有我们所需要的对应方法。

```js
// 该函数至少需要一个参数，即我们想要查询的方法名称。
// 然后我们可以传入方法所需的参数。
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
  if (!queryMethod) {
    throw new TypeError(
      "QueryableWorker.sendQuery takes at least one argument",
    );
  }
  worker.postMessage({
    queryMethod,
    queryMethodArguments,
  });
};
```

我们以 `onmessage` 方法作为 QueryableWorker 的结尾。如果 worker 有我们所需要的对应的方法，它就会返回相对应的侦听方法的名字以及所需要的参数，我们只需要在侦听列表 `listeners` 中找到它：

```js
worker.onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethodListener") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    listeners[event.data.queryMethodListener].apply(
      instance,
      event.data.queryMethodArguments,
    );
  } else {
    this.defaultListener.call(instance, event.data);
  }
};
```

现在回到 worker 中。首先我们需要一个能够完成这 2 个操作的方法：

```js
const queryableFunctions = {
  getDifference(a, b) {
    reply("printStuff", a - b);
  },
  waitSomeTime() {
    setTimeout(() => {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

function reply(queryMethodListener, ...queryMethodArguments) {
  if (!queryMethodListener) {
    throw new TypeError("reply - takes at least one argument");
  }
  postMessage({
    queryMethodListener,
    queryMethodArguments,
  });
}

// 当主页面直接调用 QueryWorker 的 postMessage 方法时，该方法被调用。
function defaultReply(message) {
  // 做点什么
}
```

`onmessage` 方法也就很简单了：

```js
onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethod") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    queryableFunctions[event.data.queryMethod].apply(
      self,
      event.data.queryMethodArguments,
    );
  } else {
    defaultReply(event.data);
  }
};
```

接下来给出一个完整的实现：

**example.html**（主页面）：

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>MDN Example - Queryable worker</title>
    <script type="text/javascript">
 // QueryableWorker 实例的方法：
 // \* sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc.): 调用一个 Worker 的可查询函数
 // \* postMessage(string or JSON Data): 见 Worker.prototype.postMessage()
 // \* terminate(): 终止 Worker
 // \* addListener(name, function): 添加一个监听器
 // \* removeListener(name): 移除一个监听器
 // QueryableWorker 实例的属性：
 // \* defaultListener: 默认监听器只在 Worker 直接调用 postMessage() 函数时执行
 function QueryableWorker(url, defaultListener, onError) {
 const instance = this;
 const worker = new Worker(url);
 const listeners = {};

 this.defaultListener = defaultListener ?? (() => {});

 if (onError) {
 worker.onerror = onError;
 }

 this.postMessage = (message) => {
 worker.postMessage(message);
 };

 this.terminate = () => {
 worker.terminate();
 };

 this.addListener = (name, listener) => {
 listeners[name] = listener;
 };

 this.removeListener = (name) => {
 delete listeners[name];
 };

 // 这个函数至少需要一个参数，即我们想要查询的方法名称。
 // 然后我们可以传入方法所需的参数。
 this.sendQuery = (queryMethod, ...queryMethodArguments) => {
 if (!queryMethod) {
 throw new TypeError(
 "QueryableWorker.sendQuery takes at least one argument",
 );
 }
 worker.postMessage({
 queryMethod,
 queryMethodArguments,
 });
 };

 worker.onmessage = (event) => {
 if (
 event.data instanceof Object &&
 Object.hasOwn(event.data, "queryMethodListener") &&
 Object.hasOwn(event.data, "queryMethodArguments")
 ) {
 listeners[event.data.queryMethodListener].apply(
 instance,
 event.data.queryMethodArguments,
 );
 } else {
 this.defaultListener.call(instance, event.data);
 }
 };
 }

 // 你自定义的 "queryable" worker
 const myTask = new QueryableWorker("my\_task.js");

 // 你自定义的 "listeners"
 myTask.addListener("printStuff", (result) => {
 document
 .getElementById("firstLink")
 .parentNode.appendChild(
 document.createTextNode(`The difference is ${result}!`),
 );
 });

 myTask.addListener("doAlert", (time, unit) => {
 alert(`Worker waited for ${time} ${unit} :-)`);
 });
 </script>
  </head>
  <body>
    <ul>
      <li>
        <a
 id="firstLink"
 href="javascript:myTask.sendQuery('getDifference', 5, 3);"
 >What is the difference between 5 and 3?</a
 >
      </li>
      <li>
        <a href="javascript:myTask.sendQuery('waitSomeTime');"
 >Wait 3 seconds</a
 >
      </li>
      <li>
        <a href="javascript:myTask.terminate();">terminate() the Worker</a>
      </li>
    </ul>
  </body>
</html>
```

**my\_task.js**（worker 文件）：

```js
const queryableFunctions = {
  // 示例 1：得到两个数字的差值：
  getDifference(minuend, subtrahend) {
    reply("printStuff", minuend - subtrahend);
  },

  // 示例 2：等待三秒
  waitSomeTime() {
    setTimeout(() => {
      reply("doAlert", 3, "seconds");
    }, 3000);
  },
};

// 系统函数

function defaultReply(message) {
  // 你的默认 PUBLIC 函数只在主页面直接调用 queryableWorker.postMessage() 方法时执行。
  // 做点什么
}

function reply(queryMethodListener, ...queryMethodArguments) {
  if (!queryMethodListener) {
    throw new TypeError("reply - not enough arguments");
  }
  postMessage({
    queryMethodListener,
    queryMethodArguments,
  });
}

onmessage = (event) => {
  if (
    event.data instanceof Object &&
    Object.hasOwn(event.data, "queryMethod") &&
    Object.hasOwn(event.data, "queryMethodArguments")
  ) {
    queryableFunctions[event.data.queryMethod].apply(
      self,
      event.data.queryMethodArguments,
    );
  } else {
    defaultReply(event.data);
  }
};
```

这个实例中，可以对从主页面到 worker、以及 worker 到主页面之间传递的消息内容进行切换。而且属性名 "queryMethod"、"queryMethodListeners" 和 "queryMethodArguments" 可以是任何东西，只要它们在 `QueryableWorker` 和 `worker` 中保持一致。

### 通过转让所有权（可转移对象）来传递数据

现代浏览器包含另一种性能更高的方法来将特定类型的对象传递给一个 worker 或从 worker 传回。可转移对象从一个上下文转移到另一个上下文而不会经过任何拷贝操作。这意味着当传递大型数据集时会获得极大的性能提升。

例如，当你将一个 `ArrayBuffer` 对象从主应用转让到 Worker 中，原始的 `ArrayBuffer` 被清除并且无法使用。它包含的内容会（完整无差的）传递给 Worker 上下文。

```js
// 创建一个 32MB 的“文件”，用从 0 到 255 的连续数值填充它——32MB = 1024 \* 1024 \* 32
const uInt8Array = new Uint8Array(1024 \* 1024 \* 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
```

**备注：** 获取更多该方法相关的可转让对象、性能及特性检测等方法，请参阅 HTML5 Rocks 中的Transferable Objects: Lightning Fast! 。

嵌入式 worker
----------

目前没有一种“官方”的方法能够像 `<script>` 元素一样将 worker 的代码嵌入道到网页中。但是如果一个 `<script>` 元素没有 `src` 属性，并且它的 `type` 属性没有指定成一个可运行的 MIME type，那么它就会被认为是一个数据块元素，并且能够被 JavaScript 使用。“数据块”是 HTML5 中一个十分常见的特性，它可以携带几乎任何文本类型的数据。所以，你能够以如下方式嵌入一个 worker：

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>MDN Example - Embedded worker</title>
    <script type="text/js-worker">
 // 该脚本不会被 JS 引擎解析，因为它的 mime-type 是 text/js-worker。
 const myVar = 'Hello World!';
 // 剩下的 worker 代码写到这里。
 </script>
    <script>
 // 该脚本会被 JS 引擎解析，因为它的 mime-type 是 text/javascript。
 function pageLog(sMsg) {
 // 使用 fragment：这样浏览器只会进行一次渲染/重排。
 const frag = document.createDocumentFragment();
 frag.appendChild(document.createTextNode(sMsg));
 frag.appendChild(document.createElement("br"));
 document.querySelector("#logDisplay").appendChild(frag);
 }
 </script>
    <script type="text/js-worker">
 // 该脚本不会被 JS 引擎解析，因为它的 mime-type 是 text/js-worker。
 onmessage = (event) => {
 postMessage(myVar);
 };
 // 剩下的 worker 代码写到这里。
 </script>
    <script>
 // 该脚本会被 JS 引擎解析，因为它的 mime-type 是 text/javascript。

 // 过去存在 blob builder，但现在我们使用 Blob
 const blob = new Blob(
 Array.prototype.map.call(
 document.querySelectorAll("script[type='text\/js-worker']"),
 (script) => script.textContent,
 { type: "text/javascript" },
 ),
 );

 // 创建一个新的 document.worker 属性，包含所有 "text/js-worker" 脚本。
 document.worker = new Worker(window.URL.createObjectURL(blob));

 document.worker.onmessage = (event) => {
 pageLog(`Received: ${event.data}`);
 };

 // 启动 worker。
 window.onload = () => {
 document.worker.postMessage("");
 };
 </script>
  </head>
  <body>
    <div id="logDisplay"></div>
  </body>
</html>
```

现在，嵌入式 worker 已经嵌套进了一个自定义的 `document.worker` 属性中。

这样也不足为奇，你仍然可以将一个函数转换为 blob，然后为这个 blob 生成 URL 对象。比如：

```js
function fn2workerURL(fn) {
  const blob = new Blob([`(${fn.toString()})()`], { type: "text/javascript" });
  return URL.createObjectURL(blob);
}
```

更多示例
----

本节提供了几个如何使用 web worker 的例子。

### 在后台执行运算

worker 的一个优势在于能够执行处理器密集型的运算而不会阻塞 UI 线程。在下面的例子中，worker 用于计算斐波那契数。

#### JavaScript 代码

下面的 JavaScript 代码保存在“fibonacci.js”文件中，与下一节的 HTML 文件关联。

```js
self.onmessage = (e) => {
  const userNum = Number(e.data);
  fibonacci(userNum);
};

function fibonacci(num) {
  let a = 1;
  let b = 0;
  while (num >= 0) {
    [a, b] = [a + b, a];
    num--;
  }

  self.postMessage(b);
}
```

worker 将属性 `onmessage` 设置为一个函数，当 worker 对象调用 `postMessage()` 时该函数会接收到发送过来的信息（注意，这么使用并不等同定义一个同名的*函数*。`var onmessage`、`let onmessage` 与 `function onmessage` 将会定义与该名字相同的全局属性，但是它们不会注册能够接收从创建 worker 的网页发送过来的消息的函数）。这将执行数学运算，并最终将结果返回到主线程。

#### HTML 代码

```html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>Fibonacci number generator</title>
    <style>
 body {
 width: 500px;
 }

 div,
 p {
 margin-bottom: 20px;
 }
 </style>
  </head>
  <body>
    <form>
      <div>
        <label for="number"
 >Enter a number that is an index position in the fibonacci sequence to
          see what number is in that position (e.g. enter 5 and you'll get a
          result of 8 — fibonacci index position 5 is 8).</label
 >
        <input type="number" id="number" />
      </div>
      <div>
        <input type="submit" />
      </div>
    </form>

    <p id="result"></p>

    <script>
 const form = document.querySelector("form");
 const input = document.querySelector('input[type="number"]');
 const result = document.querySelector("p#result");
 const worker = new Worker("fibonacci.js");

 worker.onmessage = (event) => {
 result.textContent = event.data;
 console.log(`Got: ${event.data}`);
 };

 worker.onerror = (error) => {
 console.log(`Worker error: ${error.message}`);
 throw error;
 };

 form.onsubmit = (e) => {
 e.preventDefault();
 worker.postMessage(input.value);
 input.value = "";
 };
 </script>
  </body>
</html>
```

网页创建了一个 ID 为 `result` 的用于显示运算结果的 `<div>` 元素，然后生成 worker。在生成 worker 后，`onmessage` 处理函数配置为通过设置 `div` 元素的内容来显示运算结果，然后 `onerror` 处理函数被设置为将错误消息记录到 devtools 控制台。

最后，向 worker 发送一条消息来启动它。

运行这个示例。

### 划分任务给多个 worker

当多核系统流行开来，将复杂的运算任务分配给多个 worker 来运行已经变得十分有用，这些 worker 会在多处理器内核上运行这些任务。

其他类型的 worker
------------

除了专用和共享的 web worker，还有一些其他类型的 worker：

* ServiceWorkers 基本上是作为代理服务器，位于 web 应用程序、浏览器和网络（如果可用）之间。它们的目的是（除开其他方面）创建有效的离线体验，拦截网络请求，以及根据网络是否可用采取合适的行动并更新驻留在服务器上的资源。它们还将允许访问推送通知和后台同步 API。
* Audio Worklet 提供了在 worklet（轻量级的 web worker）上下文中直接完成脚本化音频处理的可能性。
调试 worker 线程
------------

大多数浏览器都允许你在 JavaScript 调试器中调试 Web Worker，其方式与调试主线程*完全相同*！例如，Firefox 和 Chrome 都列出了主线程和活动 worker 线程的 JavaScript 源文件，所有这些文件都可以打开以设置断点和日志点。

要了解如何调试 Web Worker，请参阅每个浏览器的 JavaScript 调试器的文档：

* Chrome Sources panel
* Firefox JavaScript Debugger
worker 中可用的函数和接口
----------------

你可以在 web worker 中使用大多数的标准 JavaScript 特性，包括：

* `Navigator`
* `XMLHttpRequest`
* `Array`、`Date`、`Math` 和 `String`
* `setTimeout()` 和 `setInterval()`

在一个 worker 中最主要的你*不能*做的事情就是直接影响父页面。包括操作父页面的节点以及使用页面中的对象。你只能间接地实现，通过 `DedicatedWorkerGlobalScope.postMessage` (en-US) 回传消息给主脚本，然后从主脚本那里执行操作或变化。

**备注：** 你可以使用网站测试一个方法是否对 worker 可用： https://worker-playground.glitch.me/。例如，如果你在 Firefox 84 的网站上输入 EventSource，你会发现在 service worker 不支持这个方法，但在专用和共享 worker 中支持。

**备注：** 获取 worker 中完整的方法列表，请参阅 worker 可用的方法和接口 (en-US)。

规范
--

| Specification |
| --- |
| HTML Standard # workers |

相关链接
----

* `Worker` 接口
* `SharedWorker` 接口
* worker 可用的方法 (en-US)
* `OffscreenCanvas` 接口


可转移对象 - Web API 接口参考

可转移对象
=====

**可转移的对象**（Transferable object）是拥有属于自己的资源的对象，这些资源可以从一个上下文*转移*到另一个，确保资源一次仅在一个上下文可用。传输后，原始对象不再可用；它不再指向转移后的资源，并且任何读取或者写入该对象的尝试都将抛出异常。

*可转移对象*通常用于共享资源，该资源一次仅能安全地暴露在一个 JavaScript 线程中。例如，`ArrayBuffer` 是一个拥有内存块的可转移对象。当此类缓冲区（buffer）在线程之间传输时，相关联的内存资源将从原始的缓冲区分离出来，并且附加到新线程创建的缓冲区对象中。原始线程中的缓冲区对象不再可用，因为它不再拥有属于自己的内存资源了。

使用 `structuredClone()` 创建对象的深层拷贝时，也可以使用转移。克隆操作后，传输的资源将被移动到克隆的对象，而不是复制。

使用转移对象资源的机制取决于对象自身。例如，当 `ArrayBuffer` 在线程之间转移时，它指向的内存资源*实际上*以快速且高效的零拷贝操作在上下文之间移动。其他对象可以通过拷贝关联的资源，然后将它从旧的上下文中删除来转移它。

并不是所有的对象都是可转移的。下面提供了可转移对象的列表。

在线程之间传输对象
---------

以下代码演示了当消息从主线程发送到 web worker 线程时，传输是如何工作的。`Uint8Array` 在其缓冲区被转移时，被拷贝到 worker 中。传输后，任何尝试从主线程读或者写 `uInt8Array` 都将抛出错误，但是你仍然可以检查 `byteLength` 以确定它现在是 0。

```js
// Create an 8MB "file" and fill it. 8MB = 1024 \* 1024 \* 8 B
const uInt8Array = new Uint8Array(1024 \* 1024 \* 8).map((v, i) => i);
console.log(uInt8Array.byteLength); // 8388608

// Transfer the underlying buffer to a worker
worker.postMessage(uInt8Array, [uInt8Array.buffer]);
console.log(uInt8Array.byteLength); // 0
```

**备注：** 像 `Int32Array` 和 `Uint8Array` 等类型化数组是可序列化的，但是不能转移。然而，它们的底层缓冲区是一个 `ArrayBuffer`，它是一个可转移对象。我们可以在数据参数中发送 `uInt8Array.buffer`，但是不能在传输数组中发送 `uInt8Array`。

### 在进行克隆操作时转移

以下代码展示了 `structuredClone()` 操作，将底层缓冲区从原始对象复制到克隆对象（`clone`）。

```js
const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024

original[0] = 1;
console.log(clone[0]); // 0

// Transferring the Uint8Array would throw an exception as it is not a transferable object
// const transferred = structuredClone(original, {transfer: [original]});

// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1

// After transferring Uint8Array.buffer cannot be used.
console.log(original.byteLength); // 0
```

支持的对象
-----

以下是可以被*转移*的不同规范的对象：

* `ArrayBuffer`
* `MessagePort`
* `ReadableStream`
* `WritableStream`
* `TransformStream`
* `AudioData` (en-US)
* `ImageBitmap`
* `VideoFrame` (en-US)
* `OffscreenCanvas`
* `RTCDataChannel`

在各自对象的兼容性信息中，如果拥有 `transferable` 子特性，浏览器的支持应该被展示（示例请参阅 `RTCDataChannel`）。在撰写本文时，并非所有可转移对象都已更新此信息。

**备注：** 可转移的对象在 Web IDL 文件中用属性 `[Transferable]` 标记。

参见
--

* 可转移对象：像闪电一样快！
* 使用 Web Worker
* HTML 规范中的可转移对象
* `DedicatedWorkerGlobalScope.postMessage()` (en-US)


Web Workers 可以使用的函数和类 - Web API 接口参考

Web Workers 可以使用的函数和类
=====================

除了标准的 JavaScript 函数集（例如 `String`、`Array`、`Object`、`JSON` 等）DOM 中的许多函数都可在 worker 中使用。本文列出了这些函数。

Worker 上下文和函数
-------------

**Worker 在另一个全局上下文中运行，与当前的 window 不同！**`Window` 并不直接在 worker 中可用，其中的很多方法都通过共享的混入（`WindowOrWorkerGlobalScope`）定义，并通过 worker 派生的 `WorkerGlobalScope` (en-US) 上下文提供这些方法：

* `DedicatedWorkerGlobalScope` 用于专用 worker
* `SharedWorkerGlobalScope` (en-US) 用于共享 worker
* `ServiceWorkerGlobalScope` 用于 service worker

一些函数在所有的 worker 和主线程中均可用（来自 `WindowOrWorkerGlobalScope`）：

* `atob()`
* `btoa()`
* `clearInterval()`
* `clearTimeout()`
* `dump()`
非标准
* `queueMicrotask()`
* `setInterval()`
* `setTimeout()`
* `structuredClone()`
* `window.requestAnimationFrame`（仅专用 worker）
* `window.cancelAnimationFrame`（仅专用 worker）

以下函数**仅**在 worker 中可用：

* `WorkerGlobalScope.importScripts()`（所有的 worker）
* `DedicatedWorkerGlobalScope.postMessage` (en-US)（仅专用 worker）
Worker 中可用的 Web API
-------------------

**备注：** 如果列出的 API 被某一平台的特定版本所支持，那么一般可以认为它在 web worker 中是可用的。你也可以使用网站来测试对某个特定对象/函数的支持：https://worker-playground.glitch.me/

以下 Web API 在 worker 中可用：

* `Barcode Detection API`
* `Broadcast Channel API`
* `Cache API`
* `Channel Messaging API`
* `Console API`
* Web Crypto API（例如 `Crypto`）
* CSS Font Loading API
* `CustomEvent`
* `Encoding API`（例如 `TextEncoder`、`TextDecoder`）
* `Fetch API`
* `FileReader`
* `FileReaderSync`（仅在 worker 中有效！）
* `FormData`
* `ImageBitmap`
* `ImageData`
* `IndexedDB`
* Media Source Extensions API（仅限专用 worker）
* Network Information API
* `Notifications API`
* `OffscreenCanvas`（和所有的 canvas context API）
* `Performance API`，包括：
    + `Performance`
    + `PerformanceEntry`
    + `PerformanceMeasure` (en-US)
    + `PerformanceMark` (en-US)
    + `PerformanceObserver`
    + `PerformanceResourceTiming`
* Server-sent 事件
* `ServiceWorkerRegistration` (en-US)
* `URL API`（例如 `URL`）
* WebCodecs\_API
* `WebSocket`
* `XMLHttpRequest`

worker 也可以派生其他 worker，所以这些 API 也是可用的：

* `Worker`
* `WorkerGlobalScope` (en-US)
* `WorkerLocation` (en-US)
* `WorkerNavigator` (en-US)
参见
--

* 使用 web worker
* `Worker`


Canvas 教程 - Web API 接口参考

Canvas 教程
=========

**`<canvas>`**是一个可以使用脚本 (通常为JavaScript) 来绘制图形的 HTML 元素。例如，它可以用于绘制图表、制作图片构图或者制作简单的动画。右边的图片展示了一些 `<canvas>` 的实现示例，在这个教程后面我们将看到。

本篇教程从一些基础开始，描述了如何使用<canvas>元素来绘制 2D 图形。教程中提供的例子，会让你明白可以用 canvas 做什么，也会提供一些代码片段来帮助你开始构建自己的内容。

`<canvas>` 最早由 Apple 引入 WebKit，用于 Mac OS X 的 Dashboard，随后被各个浏览器实现。如今，所有主流的浏览器都支持它。

开始之前
----

使用 `<canvas>` 元素不是非常难，但你需要一些基本的HTML和JavaScript知识。除一些过时的浏览器不支持`<canvas>` 元素外，所有的新版本主流浏览器都支持它。Canvas 的默认大小为 300 像素 ×150 像素（宽 × 高，像素的单位是 px）。但是，可以使用 HTML 的高度和宽度属性来自定义 Canvas 的尺寸。为了在 Canvas 上绘制图形，我们使用一个 JavaScript 上下文对象，它能动态创建图像（creates graphics on the fly）。

本教程包含
-----

* 基本用法
* 绘制图形
* 使用样式与颜色
* 绘制文本
* 使用图像
* 变形
* 合成和剪辑
* 基本动画
* 高级动画
* 像素处理
* 优化 canvas
* 终曲
参见
--

* Canvas 专题页
* Adobe Illustrator to Canvas plug-in
* HTML5 Canvas 教程
* 下一页 »


使用 canvas 处理视频 - Web API 接口参考

使用 canvas 处理视频
==============

通过在一个 canvas（画布）上结合 video 元素功能，你可以实时地操纵视频数据来合成各种视觉特效到正在呈现的视频画面中。本教程示范如何使用 JavaScript 代码执行色度键控（也被称为“绿屏效果”）。

查看该实例。

文档内容
----

以下是用于渲染该内容的 XHTML 文档。

```html
<!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>
    <style>
 body {
 background: black;
 color: #cccccc;
 }
 #c2 {
 background-image: url(foo.png);
 background-repeat: no-repeat;
 }
 div {
 float: left;
 border: 1px solid #444444;
 padding: 10px;
 margin: 10px;
 background: #3b3b3b;
 }
 </style>
    <script type="text/javascript;" src="main.js"></script>
  </head>

  <body onload="processor.doLoad()">
    <div>
      <video id="video" src="video.ogv" controls="true" />
    </div>
    <div>
      <canvas id="c1" width="160" height="96" />
      <canvas id="c2" width="160" height="96" />
    </div>
  </body>
</html>
```

从这里带出的关键有：

1. 这篇文档布置了两个 `canvas` 元素，分别带着 ID 为 `c1` 和 `c2` 的属性。Canvas `c1` 用于显示原视频的当前帧，而 `c2` 用于显示执行了色度键控效果后的视频；`c2` 预先加载将被用于替换视频中绿幕（绿色背景）的静态图像。
2. JavaScript 代码从一个名为 `main.js` 的脚本引入；这个脚本使用 JavaScript 1.8 特性，所以在 22 行引入脚本时它的 version（版本）是指定的。
3. 当文档加载时，在 `main.js` 里的 `processor.doLoad()` 方法开始执行。
JavaScript 代码
-------------

`main.js` 里的 JavaScript 代码由三个方法组成。

### 初始化色键播放器

当 XHTML 文档最初加载时 `doLoad()` 方法被调用。这个方法的工作是准备色键处理代码所需的变量，并设置一个监听事件以便我们可以监测用户何时开始播放视频。

```js
  var processor;

  processor.doLoad = function doLoad() {
    this.video = document.getElementById('video');
    this.c1 = document.getElementById('c1');
    this.ctx1 = this.c1.getContext('2d');
    this.c2 = document.getElementById('c2');
    this.ctx2 = this.c2.getContext('2d');
    let self = this;
    this.video.addEventListener('play', function() {
        self.width = self.video.videoWidth / 2;
        self.height = self.video.videoHeight / 2;
        self.timerCallback();
      }, false);
  },
```

这段代码抓取 XHTML 文档中关键元素的引用，即 `video` 元素和两个 `canvas` 元素。它也获取了两个 canvas 各自的图形上下文引用。这些将在我们真正做色键控制效果的时候被用到。

然后 `addEventListener()` 被调用来开始监视 `video` 元素以便我们在用户按下视频上的播放按钮时获得通知。为响应用户开始回放，这段代码获取了视频的宽度和高度，并各自减半（当我们执行色度键控效果的时候我们将减半视频的尺寸），然后调用 `timerCallback()` 方法来开始监视视频以及计算视觉效果。

### 计时器回调

计时器回调最初在视频开始播放时（当“播放”事件发生时）被调用，然后负责设定自身定期被调用以便为每一帧启用键控效果。

```js
  processor.timerCallback = function timerCallback() {
    if (this.video.paused || this.video.ended) {
      return;
    }
    this.computeFrame();
    let self = this;
    setTimeout(function() {
        self.timerCallback();
      }, 0);
  },
```

回调做的第一件事情是检查视频是否正好在播放；如果不是，回调立即返回并不会做任何事情。

然后（如果视频正在播放）它调用 `computeFrame()` 方法，该方法对当前视频帧执行色度键控效果。

回调做的最后一件事是调用 `setTimeout()` 来设定它自身被尽快地再次调用。在真实环境中，你可能会根据视频的帧速率情况来计划实现这种调用。

### 操作视频帧数据

下方展示的 `computeFrame()` 方法负责真实获取一帧数据并执行色度键控效果。

```js
processor.computeFrame = function computeFrame() {
  this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
  let frame = this.ctx1.getImageData(0, 0, this.width, this.height);
  let l = frame.data.length / 4;

  for (let i = 0; i < l; i++) {
    let r = frame.data[i \* 4 + 0];
    let g = frame.data[i \* 4 + 1];
    let b = frame.data[i \* 4 + 2];
    if (g > 100 && r > 100 && b < 43) frame.data[i \* 4 + 3] = 0;
  }
  this.ctx2.putImageData(frame, 0, 0);
  return;
};
```

当这段例行程序被调用时，video 元素正显示最新的视频数据帧，就像这样：

![video.png](/zh-CN/docs/Web/API/Canvas_API/Manipulating_video_using_canvas/video.png)

在第 2 行，视频帧被复制到第一个 canvas 的图形上下文 `ctx1` 中，并指定了和我们之前保存的值一样的宽度和高度来绘制一半大小的帧。注意这点，你可以简单地把 video 元素放到上下文的 `drawImage()` 方法当中来绘制当前的视频帧到上下文里。效果如下：

![sourcectx.png](/zh-CN/docs/Web/API/Canvas_API/Manipulating_video_using_canvas/sourcectx.png)

第 3 行通过在第一个上下文里调用 `getImageData()` 方法获取到视频当前帧的原始图形数据的拷贝。它提供了原始的 32 位像素的图像数据使我们可以继续操作。第 4 行通过用帧的图像数据的总大小除以四来计算图像中的像素数。

从第 6 行开始 `for` 循环扫描帧的像素，取出每个像素的红、绿和蓝的值，并和用于检测绿幕的预设的数对比。其中绿幕将用从 `foo.png` 导入的静态背景图像替换。

每一个（数值）范围内得到的被认为是绿幕一部分的帧图像数据里的像素具有的 alpha 值被替换为零，以表示该像素完全透明。因此，最终的图像里的整个绿幕的区域 100% 透明，于是在 13 行当它被绘制到目标上下文中时，效果是作为一层遮罩覆于静态背景上面。

形成的图像像这样：

![output.png](/zh-CN/docs/Web/API/Canvas_API/Manipulating_video_using_canvas/output.png)

这随视频播放而被反复实现，所以一帧接着一帧被处理并带有色键效果被显示出来。

查看该实例。

参见
--

* 使用音频和视频 (en-US)


终曲 - Web API 接口参考

终曲
==

* « 上一页

恭喜你！你已经结束了 Canvas 的教程！这些知识将帮助你在 web 中创建优秀的 2D 图形。

更多的例子以及教程
---------

下面这些网站里有关于 canvas 的各种各样的演示以及更进一步的说明：

Codepen.io

前端开发人员操练场和浏览器内的代码编辑器。

Canvasdemos.com(域名已失效)

关于 HTML5 canvas 元素的应用程序，游戏，工具和教程。

HTML5CanvasTutorials

包括大多数 canvas API 的示例。

31 days of Canvas tutorials

JavaScript 可视化编程入门的不错选择。

Game development

玩游戏是最热门的计算机活动之一。新技术不断诞生，以使开发更好更强、且能在任何符合标准的浏览器上运行的游戏变得可能。

其他的 Web API
-----------

当和 canvas 和图形进一步打交道时，这些 API 可能会排上用场。

WebGL

用于渲染交互式 3D 图形的 API.

SVG

可缩放矢量图形（简称 SVG）允许你使用矢量线，矢量图形，确保无论图片大小都可以比例平滑地显示。

Web Audio

Web Audio API 提供了 Web 上的音频控制——个强大和灵活的系统，允许开发人员选择的音频源，添加效果的音频，创建音频可视化，应用空间效应（如平移）等等。

解惑
--

Stackoverflow

到"canvas"标签来提问

Comments about this tutorial – the MDN documentation community

如果你对本教程有任何意见，或是想感谢我们，在此畅所欲言吧！

* « 上一页


像素操作 - Web API 接口参考

像素操作
====

* « 上一页
* 下一页 »

到目前为止，我们尚未深入了解 Canvas 画布真实像素的原理，事实上，你可以直接通过 ImageData 对象操纵像素数据，直接读取或将数据数组写入该对象中。稍后我们也将深入了解如何控制图像使其平滑（反锯齿）以及如何从 Canvas 画布中保存图像。

ImageData 对象
------------

`ImageData`对象中存储着 canvas 对象真实的像素数据，它包含以下几个只读属性：

`width`

图片宽度，单位是像素

`height`

图片高度，单位是像素

`data`

`Uint8ClampedArray` 类型的一维数组，包含着 RGBA 格式的整型数据，范围在 0 至 255 之间（包括 255）。

data 属性返回一个 `Uint8ClampedArray`，它可以被使用作为查看初始像素数据。每个像素用 4 个 1bytes 值 (按照红，绿，蓝和透明值的顺序; 这就是"RGBA"格式) 来代表。每个颜色值部份用 0 至 255 来代表。每个部份被分配到一个在数组内连续的索引，左上角像素的红色部份在数组的索引 0 位置。像素从左到右被处理，然后往下，遍历整个数组。

`Uint8ClampedArray` 包含 `height` × `width` × 4 字节数据，索引值从 0 到 (`height`× `width` × 4)-1

例如，要读取图片中位于第 50 行，第 200 列的像素的蓝色部份，你会写以下代码：
```

blueComponent = imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 2];
```

根据行、列读取某像素点的 R/G/B/A 值的公式：
```

imageData.data[((50 * (imageData.width * 4)) + (200 * 4)) + 0/1/2/3];
```

你可能用会使用 `Uint8ClampedArray.length` 属性来读取像素数组的大小（以字节为单位）：

```js
var numBytes = imageData.data.length;
```

创建一个 ImageData 对象
-----------------

去创建一个新的，空白的 `ImageData` 对象，你应该会使用`createImageData()` 方法。有 2 个版本的 `createImageData()` 方法。

```js
var myImageData = ctx.createImageData(width, height);
```

上面代码创建了一个新的具体特定尺寸的 `ImageData` 对象。所有像素被预设为透明黑。

你也可以创建一个被 `anotherImageData` 对象指定的相同像素的 `ImageData` 对象。这个新的对象像素全部被预设为透明黑。这个并非复制了图片数据。

```js
var myImageData = ctx.createImageData(anotherImageData);
```

得到场景像素数据
--------

为了获得一个包含画布场景像素数据的 `ImageData` 对象，你可以用 `getImageData()` 方法：

```js
var myImageData = ctx.getImageData(left, top, width, height);
```

这个方法会返回一个 `ImageData` 对象，它代表了画布区域的对象数据，此画布的四个角落分别表示为 (`left`, `top`), (`left + width`, `top`), (`left`, `top + height`), 以及 (`left + width`, `top + height`) 四个点。这些坐标点被设定为画布坐标空间元素。

**备注：** 任何在画布以外的元素都会被返回成一个透明黑的 `ImageData` 对象。

这个方法也会在文章用画布操作视频中展示。

### 颜色选择器

在这个例子里面，我们会使用 `getImageData()` 去展示鼠标光标下的颜色。为此，我们要当前鼠标的位置，记为 layerX 和 layerY，然后我们去查询 `getImageData()` 给我们提供的在那个位置的像数数组里面的像素数据。最后我们使用数组数据去设置背景颜色和 `<div>` 的文字去展示颜色值。
```

<canvas id="canvas" width="300" height="227" style="float:left"></canvas>
<div id="color" style="width:200px;height:50px;float:left"></div>
```

```
var img = new Image();
img.src = "rhino.jpg";
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
img.onload = function () {
  ctx.drawImage(img, 0, 0);
  img.style.display = "none";
};
var color = document.getElementById("color");
function pick(event) {
  var x = event.layerX;
  var y = event.layerY;
  var pixel = ctx.getImageData(x, y, 1, 1);
  var data = pixel.data;
  var rgba =
    "rgba(" +
    data[0] +
    "," +
    data[1] +
    "," +
    data[2] +
    "," +
    data[3] / 255 +
    ")";
  color.style.background = rgba;
  color.textContent = rgba;
}
canvas.addEventListener("mousemove", pick);
```

```js
var img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/rhino.jpg";
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
img.onload = function () {
  ctx.drawImage(img, 0, 0);
  img.style.display = "none";
};
var hoveredColor = document.getElementById("hovered-color");
var selectedColor = document.getElementById("selected-color");

function pick(event, destination) {
  var x = event.layerX;
  var y = event.layerY;
  var pixel = ctx.getImageData(x, y, 1, 1);
  var data = pixel.data;

  const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
  destination.style.background = rgba;
  destination.textContent = rgba;

  return rgba;
}

canvas.addEventListener("mousemove", function (event) {
  pick(event, hoveredColor);
});
canvas.addEventListener("click", function (event) {
  pick(event, selectedColor);
});
```

在场景中写入像素数据
----------

你可以用 `putImageData()` 方法去对场景进行像素数据的写入。

```js
ctx.putImageData(myImageData, dx, dy);
```

dx 和 dy 参数表示你希望在场景内左上角绘制的像素数据所得到的设备坐标。

例如，为了在场景内左上角绘制 `myImageData` 代表的图片，你可以写如下的代码：

```js
ctx.putImageData(myImageData, 0, 0);
```

### 图片灰度和反相颜色

在这个例子里，我们遍历所有像素以改变他们的数值。然后我们将被修改的像素数组通过putImageData() 放回到画布中去。invert 函数仅仅是去减掉颜色的最大色值 255。grayscale 函数仅仅是用红绿和蓝的平均值。你也可以用加权平均，例如 `x = 0.299r + 0.587g + 0.114b` 这个公式。更多资料请参考维基百科的Grayscale。
```

<canvas id="canvas" width="300" height="227"></canvas>
<div>
  <input id="grayscalebtn" value="Grayscale" type="button" />
  <input id="invertbtn" value="Invert" type="button" />
</div>
```

```
var img = new Image();
img.src = "rhino.jpg";
img.onload = function () {
  draw(this);
};

function draw(img) {
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  img.style.display = "none";
  var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  var data = imageData.data;

  var invert = function () {
    for (var i = 0; i < data.length; i += 4) {
      data[i] = 255 - data[i]; // red
      data[i + 1] = 255 - data[i + 1]; // green
      data[i + 2] = 255 - data[i + 2]; // blue
    }
    ctx.putImageData(imageData, 0, 0);
  };

  var grayscale = function () {
    for (var i = 0; i < data.length; i += 4) {
      var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
      data[i] = avg; // red
      data[i + 1] = avg; // green
      data[i + 2] = avg; // blue
    }
    ctx.putImageData(imageData, 0, 0);
  };

  var invertbtn = document.getElementById("invertbtn");
  invertbtn.addEventListener("click", invert);
  var grayscalebtn = document.getElementById("grayscalebtn");
  grayscalebtn.addEventListener("click", grayscale);
}
```

```js
var img = new Image();
img.crossOrigin = "anonymous";
img.src = "./assets/rhino.jpg";

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img.onload = function () {
  ctx.drawImage(img, 0, 0);
};

var original = function () {
  ctx.drawImage(img, 0, 0);
};

var invert = function () {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (var i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i]; // red
    data[i + 1] = 255 - data[i + 1]; // green
    data[i + 2] = 255 - data[i + 2]; // blue
  }
  ctx.putImageData(imageData, 0, 0);
};

var grayscale = function () {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (var i = 0; i < data.length; i += 4) {
    var avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = avg; // red
    data[i + 1] = avg; // green
    data[i + 2] = avg; // blue
  }
  ctx.putImageData(imageData, 0, 0);
};

const inputs = document.querySelectorAll("[name=color]");
for (const input of inputs) {
  input.addEventListener("change", function (evt) {
    switch (evt.target.value) {
      case "inverted":
        return invert();
      case "grayscale":
        return grayscale();
      default:
        return original();
    }
  });
}
```

缩放和反锯齿
------

在`drawImage()` 方法，第二个画布和`imageSmoothingEnabled` 属性的帮助下，我们可以放大显示我们的图片及看到详情内容。

我们得到鼠标的位置并裁剪出距左和上 5 像素，距右和下 5 像素的图片。然后我们将这幅图复制到另一个画布然后将图片调整到我们想要的大小。在缩放画布里，我们将 10×10 像素的对原画布的裁剪调整为 200×200。

```js
zoomctx.drawImage(
  canvas,
  Math.abs(x - 5),
  Math.abs(y - 5),
  10,
  10,
  0,
  0,
  200,
  200,
);
```

因为反锯齿默认是启用的，我们可能想要关闭它以看到清楚的像素。你可以通过切换勾选框来看到 `imageSmoothingEnabled` 属性的效果（不同浏览器需要不同前缀）。

###### Zoom example

```html
<canvas id="canvas" width="300" height="227"></canvas>
<canvas id="zoom" width="300" height="227"></canvas>
<div>
  <label for="smoothbtn">
    <input type="checkbox" name="smoothbtn" checked="checked" id="smoothbtn" />
    Enable image smoothing
  </label>
</div>
```

```js
var img = new Image();
img.src = "rhino.jpg";
img.onload = function () {
  draw(this);
};

function draw(img) {
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0);
  img.style.display = "none";
  var zoomctx = document.getElementById("zoom").getContext("2d");

  var smoothbtn = document.getElementById("smoothbtn");
  var toggleSmoothing = function (event) {
    zoomctx.imageSmoothingEnabled = this.checked;
    zoomctx.mozImageSmoothingEnabled = this.checked;
    zoomctx.webkitImageSmoothingEnabled = this.checked;
    zoomctx.msImageSmoothingEnabled = this.checked;
  };
  smoothbtn.addEventListener("change", toggleSmoothing);

  var zoom = function (event) {
    var x = event.layerX;
    var y = event.layerY;
    zoomctx.drawImage(
      canvas,
      Math.abs(x - 5),
      Math.abs(y - 5),
      10,
      10,
      0,
      0,
      200,
      200,
    );
  };

  canvas.addEventListener("mousemove", zoom);
}
```

保存图片
----

`HTMLCanvasElement` 提供一个 `toDataURL()` 方法，此方法在保存图片的时候非常有用。它返回一个包含被类型参数规定的图像表现格式的数据链接。返回的图片分辨率是 96 dpi。

`canvas.toDataURL('image/png')`

默认设定。创建一个 PNG 图片。

`canvas.toDataURL('image/jpeg', quality)`

创建一个 JPG 图片。你可以有选择地提供从 0 到 1 的品质量，1 表示最好品质，0 基本不被辨析但有比较小的文件大小。

当你从画布中生成了一个数据链接，例如，你可以将它用于任何 `<image>` 元素，或者将它放在一个有 download 属性的超链接里用于保存到本地。

你也可以从画布创建一个 `Blob` 对象。

`canvas.toBlob(callback, type, encoderOptions)`

这个创建了一个在画布中的代表图片的 `Blob` 对象。

参见
--

* `ImageData`
* Manipulating video using canvas
* Canvas, images and pixels – by Christian Heilmann
* « 上一页
* 下一页 »


绘制文本 - Web API 接口参考

绘制文本
====

* « 上一页
* 下一页 »

在前一个章节中看过 应用样式和颜色 之后，我们现在来看一下如何在 canvas 中绘制文本

绘制文本
----

canvas 提供了两种方法来渲染文本：

`fillText(text, x, y [, maxWidth])`

在指定的 (x,y) 位置填充指定的文本，绘制的最大宽度是可选的。

`strokeText(text, x, y [, maxWidth])`

在指定的 (x,y) 位置绘制文本边框，绘制的最大宽度是可选的。

### 一个填充文本的示例

文本用当前的填充方式被填充：

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.font = "48px serif";
  ctx.fillText("Hello world", 10, 50);
}
```

```
<canvas id="canvas" width="300" height="100"></canvas>
```

```
draw();
```

### 一个文本边框的示例

文本用当前的边框样式被绘制：

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.font = "48px serif";
  ctx.strokeText("Hello world", 10, 50);
}
```

```
<canvas id="canvas" width="300" height="100"></canvas>
```

```
draw();
```

有样式的文本
------

在上面的例子用我们已经使用了 `font` 来使文本比默认尺寸大一些。还有更多的属性可以让你改变 canvas 显示文本的方式：

`font = value`

当前我们用来绘制文本的样式。这个字符串使用和 CSS `font` 属性相同的语法。默认的字体是 `10px sans-serif`。

`textAlign = value`

文本对齐选项。可选的值包括：`start`, `end`, `left`, `right` or `center`. 默认值是 `start`。

`textBaseline = value`

基线对齐选项。可选的值包括：`top`, `hanging`, `middle`, `alphabetic`, `ideographic`, `bottom`。默认值是 `alphabetic`。

`direction = value`

文本方向。可能的值包括：`ltr`, `rtl`, `inherit`。默认值是 `inherit`。

如果你之前使用过 CSS，那么这些选项你会很熟悉。

下面的图片（from the WHATWG）展示了 textBaseline 属性支持的不同的基线情况：

![The top of the em square is
roughly at the top of the glyphs in a font, the hanging baseline is
where some glyphs like आ are anchored, the middle is half-way
between the top of the em square and the bottom of the em square,
the alphabetic baseline is where characters like Á, ÿ,
f, and Ω are anchored, the ideographic baseline is
where glyphs like 私 and 達 are anchored, and the bottom
of the em square is roughly at the bottom of the glyphs in a
font. The top and bottom of the bounding box can be far from these
baselines, due to glyphs extending far outside the em square.](http://www.whatwg.org/specs/web-apps/current-work/images/baselines.png)

### textBaseline 例子

编辑下面的代码，看看它们在 canvas 中的变化：

```js
ctx.font = "48px serif";
ctx.textBaseline = "hanging";
ctx.strokeText("Hello world", 0, 100);
```

```
<canvas id="canvas" width="400" height="200" class="playable-canvas"></canvas>
<div class="playable-buttons">
  <input id="edit" type="button" value="Edit" />
  <input id="reset" type="button" value="Reset" />
</div>
<textarea id="code" class="playable-code">
ctx.font = "48px serif";
ctx.textBaseline = "hanging";
ctx.strokeText("Hello world", 0, 100);</textarea
>
```

```
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var textarea = document.getElementById("code");
var reset = document.getElementById("reset");
var edit = document.getElementById("edit");
var code = textarea.value;

function drawCanvas() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  eval(textarea.value);
}

reset.addEventListener("click", function () {
  textarea.value = code;
  drawCanvas();
});

edit.addEventListener("click", function () {
  textarea.focus();
});

textarea.addEventListener("input", drawCanvas);
window.addEventListener("load", drawCanvas);
```

预测量文本宽度
-------

当你需要获得更多的文本细节时，下面的方法可以给你测量文本的方法。

`measureText()`

将返回一个 `TextMetrics`对象的宽度、所在像素，这些体现文本特性的属性。

下面的代码段将展示如何测量文本来获得它的宽度：

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var text = ctx.measureText("foo"); // TextMetrics object
  text.width; // 16;
}
```

Geoko 特性说明
----------

在 Geoko（Firefox，Firefox OS 及基于 Mozilla 的应用的渲染引擎）中，曾有一些版本较早的 API 实现了在 canvas 上对文本作画的功能，但它们现在已不再使用。

* « 上一页
* 下一页 »


基本的动画 - Web API 接口参考

基本的动画
=====

* « 上一页
* 下一页 »

由于我们是用 JavaScript 去操控 `<canvas>` 对象，这样要实现一些交互动画也是相当容易的。在本章中，我们将看看如何做一些基本的动画。

可能最大的限制就是图像一旦绘制出来，它就是一直保持那样了。如果需要移动它，我们不得不对所有东西（包括之前的）进行重绘。重绘是相当费时的，而且性能很依赖于电脑的速度。

动画的基本步骤
-------

你可以通过以下的步骤来画出一帧：

1. **清空 canvas**
 除非接下来要画的内容会完全充满 canvas（例如背景图），否则你需要清空所有。最简单的做法就是用 `clearRect` 方法。
2. **保存 canvas 状态**
 如果你要改变一些会改变 canvas 状态的设置（样式，变形之类的），又要在每画一帧之时都是原始状态的话，你需要先保存一下。
3. **绘制动画图形（animated shapes）**
 这一步才是重绘动画帧。
4. **恢复 canvas 状态**
 如果已经保存了 canvas 的状态，可以先恢复它，然后重绘下一帧。
操控动画
----

在 canvas 上绘制内容是用 canvas 提供的或者自定义的方法，而通常，我们仅仅在脚本执行结束后才能看见结果，比如说，在 for 循环里面做完成动画是不太可能的。

因此，为了实现动画，我们需要一些可以定时执行重绘的方法。有两种方法可以实现这样的动画操控。首先可以通过 `setInterval` 和 `setTimeout` 方法来控制在设定的时间点上执行重绘。

### 有安排地更新画布

首先，可以用`window.setInterval()`, `window.setTimeout()`,和`window.requestAnimationFrame()`来设定定期执行一个指定函数。

`setInterval(function, delay)` (en-US)

当设定好间隔时间后，function 会定期执行。

`setTimeout(function, delay)` (en-US)

在设定好的时间之后执行函数

`requestAnimationFrame(callback)`

告诉浏览器你希望执行一个动画，并在重绘之前，请求浏览器执行一个特定的函数来更新动画。

如果你并不需要与用户互动，你可以使用 setInterval() 方法，它就可以定期执行指定代码。如果我们需要做一个游戏，我们可以使用键盘或者鼠标事件配合上 setTimeout() 方法来实现。通过设置事件监听，我们可以捕捉用户的交互，并执行相应的动作。

**备注：** 下面的例子，采用 `window.requestAnimationFrame()`实现动画效果。这个方法提供了更加平缓并更加有效率的方式来执行动画，当系统准备好了重绘条件的时候，才调用绘制动画帧。一般每秒钟回调函数执行 60 次，也有可能会被降低。想要了解更多关于动画循环的信息，尤其是游戏，可以在Game development zone 参考这篇文章 Anatomy of a video game。

太阳系的动画
------

这个例子里面，我会做一个小型的太阳系模拟动画。

```js
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init() {
  sun.src = "canvas\_sun.png";
  moon.src = "canvas\_moon.png";
  earth.src = "canvas\_earth.png";
  window.requestAnimationFrame(draw);
}

function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  ctx.globalCompositeOperation = "destination-over";
  ctx.clearRect(0, 0, 300, 300); // clear canvas

  ctx.fillStyle = "rgba(0,0,0,0.4)";
  ctx.strokeStyle = "rgba(0,153,255,0.4)";
  ctx.save();
  ctx.translate(150, 150);

  // Earth
  var time = new Date();
  ctx.rotate(
    ((2 \* Math.PI) / 60) \* time.getSeconds() +
      ((2 \* Math.PI) / 60000) \* time.getMilliseconds(),
  );
  ctx.translate(105, 0);
  ctx.fillRect(0, -12, 50, 24); // Shadow
  ctx.drawImage(earth, -12, -12);

  // Moon
  ctx.save();
  ctx.rotate(
    ((2 \* Math.PI) / 6) \* time.getSeconds() +
      ((2 \* Math.PI) / 6000) \* time.getMilliseconds(),
  );
  ctx.translate(0, 28.5);
  ctx.drawImage(moon, -3.5, -3.5);
  ctx.restore();

  ctx.restore();

  ctx.beginPath();
  ctx.arc(150, 150, 105, 0, Math.PI \* 2, false); // Earth orbit
  ctx.stroke();

  ctx.drawImage(sun, 0, 0, 300, 300);

  window.requestAnimationFrame(draw);
}

init();
```

```
<canvas id="canvas" width="300" height="300"></canvas>
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

动画时钟
----

这个例子实现一个动态时钟，可以显示当前时间。

```js
function clock() {
  var now = new Date();
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.save();
  ctx.clearRect(0, 0, 150, 150);
  ctx.translate(75, 75);
  ctx.scale(0.4, 0.4);
  ctx.rotate(-Math.PI / 2);
  ctx.strokeStyle = "black";
  ctx.fillStyle = "white";
  ctx.lineWidth = 8;
  ctx.lineCap = "round";

  // Hour marks
  ctx.save();
  for (var i = 0; i < 12; i++) {
    ctx.beginPath();
    ctx.rotate(Math.PI / 6);
    ctx.moveTo(100, 0);
    ctx.lineTo(120, 0);
    ctx.stroke();
  }
  ctx.restore();

  // Minute marks
  ctx.save();
  ctx.lineWidth = 5;
  for (i = 0; i < 60; i++) {
    if (i % 5 != 0) {
      ctx.beginPath();
      ctx.moveTo(117, 0);
      ctx.lineTo(120, 0);
      ctx.stroke();
    }
    ctx.rotate(Math.PI / 30);
  }
  ctx.restore();

  var sec = now.getSeconds();
  var min = now.getMinutes();
  var hr = now.getHours();
  hr = hr >= 12 ? hr - 12 : hr;

  ctx.fillStyle = "black";

  // write Hours
  ctx.save();
  ctx.rotate(
    hr \* (Math.PI / 6) + (Math.PI / 360) \* min + (Math.PI / 21600) \* sec,
  );
  ctx.lineWidth = 14;
  ctx.beginPath();
  ctx.moveTo(-20, 0);
  ctx.lineTo(80, 0);
  ctx.stroke();
  ctx.restore();

  // write Minutes
  ctx.save();
  ctx.rotate((Math.PI / 30) \* min + (Math.PI / 1800) \* sec);
  ctx.lineWidth = 10;
  ctx.beginPath();
  ctx.moveTo(-28, 0);
  ctx.lineTo(112, 0);
  ctx.stroke();
  ctx.restore();

  // Write seconds
  ctx.save();
  ctx.rotate((sec \* Math.PI) / 30);
  ctx.strokeStyle = "#D40000";
  ctx.fillStyle = "#D40000";
  ctx.lineWidth = 6;
  ctx.beginPath();
  ctx.moveTo(-30, 0);
  ctx.lineTo(83, 0);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(0, 0, 10, 0, Math.PI \* 2, true);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(95, 0, 10, 0, Math.PI \* 2, true);
  ctx.stroke();
  ctx.fillStyle = "rgba(0,0,0,0)";
  ctx.arc(0, 0, 3, 0, Math.PI \* 2, true);
  ctx.fill();
  ctx.restore();

  ctx.beginPath();
  ctx.lineWidth = 14;
  ctx.strokeStyle = "#325FA2";
  ctx.arc(0, 0, 142, 0, Math.PI \* 2, true);
  ctx.stroke();

  ctx.restore();

  window.requestAnimationFrame(clock);
}

window.requestAnimationFrame(clock);
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

循环全景照片
------

在这个例子中，会有一个自左向右滑动的全景图。我们使用了在维基百科中找到的尤塞米提国家公园的图片，当然你可以随意找一张任何尺寸大于 canvas 的图片。

```js
var img = new Image();

// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.

img.src = "capitan\_meadows,\_yosemite\_national\_park.jpg";
var CanvasXSize = 800;
var CanvasYSize = 200;
var speed = 30; // lower is faster
var scale = 1.05;
var y = -4.5; // vertical offset

// Main program

var dx = 0.75;
var imgW;
var imgH;
var x = 0;
var clearX;
var clearY;
var ctx;

img.onload = function () {
  imgW = img.width \* scale;
  imgH = img.height \* scale;

  if (imgW > CanvasXSize) {
    // image larger than canvas
    x = CanvasXSize - imgW;
  }
  if (imgW > CanvasXSize) {
    // image width larger than canvas
    clearX = imgW;
  } else {
    clearX = CanvasXSize;
  }
  if (imgH > CanvasYSize) {
    // image height larger than canvas
    clearY = imgH;
  } else {
    clearY = CanvasYSize;
  }

  // get canvas context
  ctx = document.getElementById("canvas").getContext("2d");

  // set refresh rate
  return setInterval(draw, speed);
};

function draw() {
  ctx.clearRect(0, 0, clearX, clearY); // clear the canvas

  // if image is <= Canvas Size
  if (imgW <= CanvasXSize) {
    // reset, start from beginning
    if (x > CanvasXSize) {
      x = -imgW + x;
    }
    // draw additional image1
    if (x > 0) {
      ctx.drawImage(img, -imgW + x, y, imgW, imgH);
    }
    // draw additional image2
    if (x - imgW > 0) {
      ctx.drawImage(img, -imgW \* 2 + x, y, imgW, imgH);
    }
  }

  // image is > Canvas Size
  else {
    // reset, start from beginning
    if (x > CanvasXSize) {
      x = CanvasXSize - imgW;
    }
    // draw aditional image
    if (x > CanvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }
  // draw image
  ctx.drawImage(img, x, y, imgW, imgH);
  // amount to move
  x += dx;
}
```

```
<canvas id="canvas" width="800" height="200"></canvas>
```

下方就是是图片在其中滑动的 `<canvas>`。需要注意的是这里定义的 width 和 height 必须与 JavaScript 代码中的变量值`CanvasXZSize`和`CanvasYSize`保持一致。
```

<canvas id="canvas" width="800" height="200"></canvas>
```

鼠标追踪动画
------

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script>
 var cn;
 //= document.getElementById('cw');
 var c;
 var u = 10;
 const m = {
 x: innerWidth / 2,
 y: innerHeight / 2,
 };
 window.onmousemove = function (e) {
 m.x = e.clientX;
 m.y = e.clientY;
 };
 function gc() {
 var s = "0123456789ABCDEF";
 var c = "#";
 for (var i = 0; i < 6; i++) {
 c += s[Math.ceil(Math.random() \* 15)];
 }
 return c;
 }
 var a = [];
 window.onload = function myfunction() {
 cn = document.getElementById("cw");
 c = cn.getContext("2d");

 for (var i = 0; i < 10; i++) {
 var r = 30;
 var x = Math.random() \* (innerWidth - 2 \* r) + r;
 var y = Math.random() \* (innerHeight - 2 \* r) + r;
 var t = new ob(
 innerWidth / 2,
 innerHeight / 2,
 5,
 "red",
 Math.random() \* 200 + 20,
 2,
 );
 a.push(t);
 }
 //cn.style.backgroundColor = "#700bc8";

 c.lineWidth = "2";
 c.globalAlpha = 0.5;
 resize();
 anim();
 };
 window.onresize = function () {
 resize();
 };
 function resize() {
 cn.height = innerHeight;
 cn.width = innerWidth;
 for (var i = 0; i < 101; i++) {
 var r = 30;
 var x = Math.random() \* (innerWidth - 2 \* r) + r;
 var y = Math.random() \* (innerHeight - 2 \* r) + r;
 a[i] = new ob(
 innerWidth / 2,
 innerHeight / 2,
 4,
 gc(),
 Math.random() \* 200 + 20,
 0.02,
 );
 }
 // a[0] = new ob(innerWidth / 2, innerHeight / 2, 40, "red", 0.05, 0.05);
 //a[0].dr();
 }
 function ob(x, y, r, cc, o, s) {
 this.x = x;
 this.y = y;
 this.r = r;
 this.cc = cc;
 this.theta = Math.random() \* Math.PI \* 2;
 this.s = s;
 this.o = o;
 this.t = Math.random() \* 150;

 this.o = o;
 this.dr = function () {
 const ls = {
 x: this.x,
 y: this.y,
 };
 this.theta += this.s;
 this.x = m.x + Math.cos(this.theta) \* this.t;
 this.y = m.y + Math.sin(this.theta) \* this.t;
 c.beginPath();
 c.lineWidth = this.r;
 c.strokeStyle = this.cc;
 c.moveTo(ls.x, ls.y);
 c.lineTo(this.x, this.y);
 c.stroke();
 c.closePath();
 };
 }
 function anim() {
 requestAnimationFrame(anim);
 c.fillStyle = "rgba(0,0,0,0.05)";
 c.fillRect(0, 0, cn.width, cn.height);
 a.forEach(function (e, i) {
 e.dr();
 });
 }
 </script>
    <style>
 #cw {
 position: fixed;
 z-index: -1;
 }

 body {
 margin: 0;
 padding: 0;
 background-color: rgba(0, 0, 0, 0.05);
 }
 </style>
  </head>
  <body>
    <canvas id="cw"></canvas>
    qwerewr
  </body>
</html>
```

### Output

其他例子
----

Advanced animations

我们将在下一章看到一些先进的动画技术和物理现象。

* « 上一页
* 下一页 »


使用样式和颜色 - Web API 接口参考

使用样式和颜色
=======

 * « 上一页
* 下一页 »

在绘制图形的章节里，我只用到默认的线条和填充样式。而在这一章里，我们将会探讨 canvas 全部的可选项，来绘制出更加吸引人的内容。色彩 Colors
---------

到目前为止，我们只看到过绘制内容的方法。如果我们想要给图形上色，有两个重要的属性可以做到：`fillStyle` 和 `strokeStyle`。

`fillStyle = color`

设置图形的填充颜色。

`strokeStyle = color`

设置图形轮廓的颜色。

`color` 可以是表示 CSS 颜色值的字符串，渐变对象或者图案对象。我们迟些再回头探讨渐变和图案对象。默认情况下，线条和填充颜色都是黑色（CSS 颜色值 `#000000`）。

**备注：** 一旦你设置了 `strokeStyle` 或者 `fillStyle` 的值，那么这个新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色，你需要重新设置 `fillStyle` 或 `strokeStyle` 的值。

你输入的应该是符合 CSS3 颜色值标准 的有效字符串。下面的例子都表示同一种颜色。

```js
// 这些 fillStyle 的值均为 '橙色'
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";
```

### `fillStyle` 示例

在本示例里，我会再度用两层 `for` 循环来绘制方格阵列，每个方格不同的颜色。结果如右图，但实现所用的代码却没那么绚丽。我用了两个变量 i 和 j 来为每一个方格产生唯一的 RGB 色彩值，其中仅修改红色和绿色通道的值，而保持蓝色通道的值不变。你可以通过修改这些颜色通道的值来产生各种各样的色板。通过增加渐变的频率，你还可以绘制出类似 Photoshop 里面的那样的调色板。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 6; i++) {
    for (var j = 0; j < 6; j++) {
      ctx.fillStyle =
        "rgb(" +
        Math.floor(255 - 42.5 \* i) +
        "," +
        Math.floor(255 - 42.5 \* j) +
        ",0)";
      ctx.fillRect(j \* 25, i \* 25, 25, 25);
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

结果如下：

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `strokeStyle` 示例

这个示例与上面的有点类似，但这次用到的是 `strokeStyle` 属性，画的不是方格，而是用 `arc` 方法来画圆。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 6; i++) {
    for (var j = 0; j < 6; j++) {
      ctx.strokeStyle =
        "rgb(0," +
        Math.floor(255 - 42.5 \* i) +
        "," +
        Math.floor(255 - 42.5 \* j) +
        ")";
      ctx.beginPath();
      ctx.arc(12.5 + j \* 25, 12.5 + i \* 25, 10, 0, Math.PI \* 2, true);
      ctx.stroke();
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

结果如下：

| Screenshot | Live sample |
| --- | --- |
|  |  |

透明度 Transparency
----------------

除了可以绘制实色图形，我们还可以用 canvas 来绘制半透明的图形。通过设置 `globalAlpha` 属性或者使用一个半透明颜色作为轮廓或填充的样式。

`globalAlpha = transparencyValue`

这个属性影响到 canvas 里所有图形的透明度，有效的值范围是 0.0（完全透明）到 1.0（完全不透明），默认是 1.0。

`globalAlpha` 属性在需要绘制大量拥有相同透明度的图形时候相当高效。不过，我认为下面的方法可操作性更强一点。

因为 `strokeStyle` 和 `fillStyle` 属性接受符合 CSS 3 规范的颜色值，那我们可以用下面的写法来设置具有透明度的颜色。

```js
// 指定透明颜色，用于描边和填充样式
ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)";
```

`rgba()` 方法与 `rgb()` 方法类似，就多了一个用于设置色彩透明度的参数。它的有效范围是从 0.0（完全透明）到 1.0（完全不透明）。

### `globalAlpha` 示例

在这个例子里，将用四色格作为背景，设置 `globalAlpha` 为 `0.2` 后，在上面画一系列半径递增的半透明圆。最终结果是一个径向渐变效果。圆叠加得越更多，原先所画的圆的透明度会越低。通过增加循环次数，画更多的圆，从中心到边缘部分，背景图会呈现逐渐消失的效果。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  // 画背景
  ctx.fillStyle = "#FD0";
  ctx.fillRect(0, 0, 75, 75);
  ctx.fillStyle = "#6C0";
  ctx.fillRect(75, 0, 75, 75);
  ctx.fillStyle = "#09F";
  ctx.fillRect(0, 75, 75, 75);
  ctx.fillStyle = "#F30";
  ctx.fillRect(75, 75, 75, 75);
  ctx.fillStyle = "#FFF";

  // 设置透明度值
  ctx.globalAlpha = 0.2;

  // 画半透明圆
  for (var i = 0; i < 7; i++) {
    ctx.beginPath();
    ctx.arc(75, 75, 10 + 10 \* i, 0, Math.PI \* 2, true);
    ctx.fill();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `rgba()` 示例

第二个例子和上面那个类似，不过不是画圆，而是画矩形。这里还可以看出，rgba() 可以分别设置轮廓和填充样式，因而具有更好的可操作性和使用灵活性。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // 画背景
  ctx.fillStyle = "rgb(255,221,0)";
  ctx.fillRect(0, 0, 150, 37.5);
  ctx.fillStyle = "rgb(102,204,0)";
  ctx.fillRect(0, 37.5, 150, 37.5);
  ctx.fillStyle = "rgb(0,153,255)";
  ctx.fillRect(0, 75, 150, 37.5);
  ctx.fillStyle = "rgb(255,51,0)";
  ctx.fillRect(0, 112.5, 150, 37.5);

  // 画半透明矩形
  for (var i = 0; i < 10; i++) {
    ctx.fillStyle = "rgba(255,255,255," + (i + 1) / 10 + ")";
    for (var j = 0; j < 4; j++) {
      ctx.fillRect(5 + i \* 14, 5 + j \* 37.5, 14, 27.5);
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

线型 Line styles
--------------

可以通过一系列属性来设置线的样式。

`lineWidth = value`

设置线条宽度。

`lineCap = type`

设置线条末端样式。

`lineJoin = type`

设定线条与线条间接合处的样式。

`miterLimit = value`

限制当两条线相交时交接处最大长度；所谓交接处长度（斜接长度）是指线条交接处内角顶点到外角顶点的长度。

`getLineDash()`

返回一个包含当前虚线样式，长度为非负偶数的数组。

`setLineDash(segments)`

设置当前虚线样式。

`lineDashOffset = value`

设置虚线样式的起始偏移量。

通过以下的样例可能会更加容易理解。

#### `lineWidth` 属性的例子

这个属性设置当前绘线的粗细。属性值必须为正数。默认值是 1.0。

线宽是指给定路径的中心到两边的粗细。换句话说就是在路径的两边各绘制线宽的一半。因为画布的坐标并不和像素直接对应，当需要获得精确的水平或垂直线的时候要特别注意。

在下面的例子中，用递增的宽度绘制了 10 条直线。最左边的线宽 1.0 单位。并且，最左边的以及所有宽度为奇数的线并不能精确呈现，这就是因为路径的定位问题。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 10; i++) {
    ctx.lineWidth = 1 + i;
    ctx.beginPath();
    ctx.moveTo(5 + i \* 14, 5);
    ctx.lineTo(5 + i \* 14, 140);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

想要获得精确的线条，必须对线条是如何描绘出来的有所理解。见下图，用网格来代表 canvas 的坐标格，每一格对应屏幕上一个像素点。在第一个图中，填充了 (2,1) 至 (5,5) 的矩形，整个区域的边界刚好落在像素边缘上，这样就可以得到的矩形有着清晰的边缘。

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas-grid.png)

如果你想要绘制一条从 (3,1) 到 (3,5)，宽度是 1.0 的线条，你会得到像第二幅图一样的结果。实际填充区域（深蓝色部分）仅仅延伸至路径两旁各一半像素。而这半个像素又会以近似的方式进行渲染，这意味着那些像素只是部分着色，结果就是以实际笔触颜色一半色调的颜色来填充整个区域（浅蓝和深蓝的部分）。这就是上例中为何宽度为 1.0 的线并不准确的原因。

要解决这个问题，你必须对路径施以更加精确的控制。已知粗 1.0 的线条会在路径两边各延伸半像素，那么像第三幅图那样绘制从 (3.5,1) 到 (3.5,5) 的线条，其边缘正好落在像素边界，填充出来就是准确的宽为 1.0 的线条。

**备注：** 在这个竖线的例子中，其 Y 坐标刚好落在网格线上，否则端点上同样会出现半渲染的像素点（但还要注意，这种行为的表现取决于当前的 lineCap 风格，它默认为 butt；你可能希望通过将 lineCap 样式设置为 square 正方形，来得到与奇数宽度线的半像素坐标相一致的笔画，这样，端点轮廓的外边框将被自动扩展以完全覆盖整个像素格）。

还请注意，只有路径的起点和终点受此影响：如果一个路径是通过 closePath() 来封闭的，它是没有起点和终点的；相反的情况下，路径上的所有端点都与上一个点相连，下一段路径使用当前的 lineJoin 设置（默认为 miter），如果相连路径是水平和/或垂直的话，会导致相连路径的外轮廓根据相交点自动延伸，因此渲染出的路径轮廓会覆盖整个像素格。接下来的两个小节将展示这些额外的行样式。

对于那些宽度为偶数的线条，每一边的像素数都是整数，那么你想要其路径是落在像素点之间 (如那从 (3,1) 到 (3,5)) 而不是在像素点的中间。同样，注意到那个例子的垂直线条，其 Y 坐标刚好落在网格线上，如果不是的话，端点上同样会出现半渲染的像素点。

虽然开始处理可缩放的 2D 图形时会有点小痛苦，但是及早注意到像素网格与路径位置之间的关系，可以确保图形在经过缩放或者其他任何变形后都可以保持看上去蛮好：线宽为 1.0 的垂线在放大 2 倍后，会变成清晰的线宽为 2.0，并且出现在它应该出现的位置上。

#### `lineCap` 属性的例子

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_linecap.png)属性 `lineCap` 的值决定了线段端点显示的样子。它可以为下面的三种的其中之一：`butt`，`round` 和 `square`。默认是 `butt`。

在这个例子里面，我绘制了三条直线，分别赋予不同的 `lineCap` 值。还有两条辅助线，为了可以看得更清楚它们之间的区别，三条线的起点终点都落在辅助线上。

最左边的线用了默认的 `butt` 。可以注意到它是与辅助线齐平的。中间的是 `round` 的效果，端点处加上了半径为一半线宽的半圆。右边的是 `square` 的效果，端点处加上了等宽且高度为一半线宽的方块。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var lineCap = ["butt", "round", "square"];

  // 创建路径
  ctx.strokeStyle = "#09f";
  ctx.beginPath();
  ctx.moveTo(10, 10);
  ctx.lineTo(140, 10);
  ctx.moveTo(10, 140);
  ctx.lineTo(140, 140);
  ctx.stroke();

  // 画线条
  ctx.strokeStyle = "black";
  for (var i = 0; i < lineCap.length; i++) {
    ctx.lineWidth = 15;
    ctx.lineCap = lineCap[i];
    ctx.beginPath();
    ctx.moveTo(25 + i \* 50, 10);
    ctx.lineTo(25 + i \* 50, 140);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

#### `lineJoin` 属性的例子

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_linejoin.png)`lineJoin` 的属性值决定了图形中两线段连接处所显示的样子。它可以是这三种之一：`round`, `bevel` 和 `miter`。默认是 `miter`。

这里我同样用三条折线来做例子，分别设置不同的 `lineJoin` 值。最上面一条是 `round` 的效果，边角处被磨圆了，圆的半径等于线宽。中间和最下面一条分别是 bevel 和 miter 的效果。当值是 `miter` 的时候，线段会在连接处外侧延伸直至交于一点，延伸效果受到下面将要介绍的 `miterLimit` 属性的制约。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var lineJoin = ["round", "bevel", "miter"];
  ctx.lineWidth = 10;
  for (var i = 0; i < lineJoin.length; i++) {
    ctx.lineJoin = lineJoin[i];
    ctx.beginPath();
    ctx.moveTo(-5, 5 + i \* 40);
    ctx.lineTo(35, 45 + i \* 40);
    ctx.lineTo(75, 5 + i \* 40);
    ctx.lineTo(115, 45 + i \* 40);
    ctx.lineTo(155, 5 + i \* 40);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

#### `miterLimit` 属性的演示例子

就如上一个例子所见的应用 `miter` 的效果，线段的外侧边缘会被延伸交汇于一点上。线段之间夹角比较大时，交点不会太远，但随着夹角变小，交点距离会呈指数级增大。

`miterLimit` 属性就是用来设定外延交点与连接点的最大距离，如果交点距离大于此值，连接效果会变成了 bevel。注意，最大斜接长度（即交点距离）是当前坐标系测量线宽与此`miterLimit`属性值（HTML `<canvas>`默认为 10.0）的乘积，所以`miterLimit`可以单独设置，不受显示比例改变或任何仿射变换的影响：它只影响线条边缘的有效绘制形状。

更准确的说，斜接限定值（`miterLimit`）是延伸长度（在 HTML Canvas 中，这个值是线段外连接点与路径中指定的点之间的距离）与一半线宽的最大允许比值。它也可以被等效定义为线条内外连接点距离（`miterLength`）与线宽（`lineWidth`）的最大允许比值（因为路径点是内外连接点的中点）。这等同于相交线段最小内夹角（*θ*）的一半的余割值，小于此角度的斜接将不会被渲染，而仅渲染斜边连接：

* `miterLimit` = **max** `miterLength` / `lineWidth` = 1 / **sin** ( **min** *θ* / 2 )
* 斜接限定值默认为 10.0，这将会去除所有小于大约 11 度的斜接。
* 斜接限定值为 √2 ≈ 1.4142136（四舍五入）时，将去除所有锐角的斜接，仅保留钝角或直角。
* 1.0 是合法的斜接限定值，但这会去除所有斜接。
* 小于 1.0 的值不是合法的斜接限定值。

在下面的小示例中，你可以动态的设置`miterLimit`的值并查看它对画布中图形的影响。蓝色线条指出了锯齿图案中每个线条的起点与终点（同时也是不同线段之间的连接点）。

在此示例中，当你设定`miterLimit`的值小于 4.2 时，图形可见部分的边角不会延伸相交，而是在蓝色线条边呈现斜边连接效果；当`miterLimit`的值大于 10.0 时，此例中大部分的边角都会在远离蓝线的位置相交，且从左至右，距离随着夹角的增大而减小；而介于上述值之间的值所呈现的效果，也介于两者之间。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // 清空画布
  ctx.clearRect(0, 0, 150, 150);

  // 绘制参考线
  ctx.strokeStyle = "#09f";
  ctx.lineWidth = 2;
  ctx.strokeRect(-5, 50, 160, 50);

  // 设置线条样式
  ctx.strokeStyle = "#000";
  ctx.lineWidth = 10;

  // 检查输入
  if (document.getElementById("miterLimit").value.match(/\d+(\.\d+)?/)) {
    ctx.miterLimit = parseFloat(document.getElementById("miterLimit").value);
  } else {
    alert("Value must be a positive number");
  }

  // 绘制线条
  ctx.beginPath();
  ctx.moveTo(0, 100);
  for (i = 0; i < 24; i++) {
    var dy = i % 2 == 0 ? 25 : -25;
    ctx.lineTo(Math.pow(i, 1.5) \* 2, 75 + dy);
  }
  ctx.stroke();
  return false;
}
```

```
<table>
  <tr>
    <td><canvas id="canvas" width="150" height="150"></canvas></td>
    <td>
      在输入框中输入<code>miterLimit</code>的值，并点击重绘按钮查看效果。<br /><br />
      <form onsubmit="return draw();">
        <label>Miter limit</label>
        <input type="number" size="3" id="miterLimit" />
        <input type="submit" value="重绘" />
      </form>
    </td>
  </tr>
</table>
```

```
document.getElementById("miterLimit").value = document
  .getElementById("canvas")
  .getContext("2d").miterLimit;
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### 使用虚线

用 `setLineDash` 方法和 `lineDashOffset` 属性来制定虚线样式。`setLineDash` 方法接受一个数组，来指定线段与间隙的交替；`lineDashOffset` 属性设置起始偏移量。

在这个例子中，我们要创建一个蚂蚁线的效果。它往往应用在计算机图形程序选区工具动效中。它可以帮助用户通过动画的边界来区分图像背景选区边框。在本教程的后面部分，你可以学习如何实现这一点和其他基本的动画。
```

<canvas id="canvas" width="110" height="110"></canvas>
```

```js
var ctx = document.getElementById("canvas").getContext("2d");
var offset = 0;

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.setLineDash([4, 2]);
  ctx.lineDashOffset = -offset;
  ctx.strokeRect(10, 10, 100, 100);
}

function march() {
  offset++;
  if (offset > 16) {
    offset = 0;
  }
  draw();
  setTimeout(march, 20);
}

march();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

渐变 Gradients
------------

就好像一般的绘图软件一样，我们可以用线性或者径向的渐变来填充或描边。我们用下面的方法新建一个 `canvasGradient` 对象，并且赋给图形的 `fillStyle` 或 `strokeStyle` 属性。

`createLinearGradient(x1, y1, x2, y2)`

createLinearGradient 方法接受 4 个参数，表示渐变的起点 (x1,y1) 与终点 (x2,y2)。

`createRadialGradient(x1, y1, r1, x2, y2, r2)`

createRadialGradient 方法接受 6 个参数，前三个定义一个以 (x1,y1) 为原点，半径为 r1 的圆，后三个参数则定义另一个以 (x2,y2) 为原点，半径为 r2 的圆。

```js
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
var radialgradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 100);
```

创建出 `canvasGradient` 对象后，我们就可以用 `addColorStop` 方法给它上色了。

`gradient.addColorStop(position, color)`

addColorStop 方法接受 2 个参数，`position` 参数必须是一个 0.0 与 1.0 之间的数值，表示渐变中颜色所在的相对位置。例如，0.5 表示颜色会出现在正中间。`color` 参数必须是一个有效的 CSS 颜色值（如 #FFF，rgba(0,0,0,1)，等等）。

你可以根据需要添加任意多个色标（color stops）。下面是最简单的线性黑白渐变的例子。

```js
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
lineargradient.addColorStop(0, "white");
lineargradient.addColorStop(1, "black");
```

#### `createLinearGradient` 的例子

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_lineargradient.png)

本例中，我弄了两种不同的渐变。第一种是背景色渐变，你会发现，我给同一位置设置了两种颜色，你也可以用这来实现突变的效果，就像这里从白色到绿色的突变。一般情况下，色标的定义是无所谓顺序的，但是色标位置重复时，顺序就变得非常重要了。所以，保持色标定义顺序和它理想的顺序一致，结果应该没什么大问题。

第二种渐变，我并不是从 0.0 位置开始定义色标，因为那并不是那么严格的。在 0.5 处设一黑色色标，渐变会默认认为从起点到色标之间都是黑色。

你会发现，`strokeStyle` 和 `fillStyle` 属性都可以接受 `canvasGradient` 对象。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // Create gradients
  var lingrad = ctx.createLinearGradient(0, 0, 0, 150);
  lingrad.addColorStop(0, "#00ABEB");
  lingrad.addColorStop(0.5, "#fff");
  lingrad.addColorStop(0.5, "#26C000");
  lingrad.addColorStop(1, "#fff");

  var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95);
  lingrad2.addColorStop(0.5, "#000");
  lingrad2.addColorStop(1, "rgba(0,0,0,0)");

  // assign gradients to fill and stroke styles
  ctx.fillStyle = lingrad;
  ctx.strokeStyle = lingrad2;

  // draw shapes
  ctx.fillRect(10, 10, 130, 130);
  ctx.strokeRect(50, 50, 50, 50);
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `createRadialGradient` 的例子

这个例子，我定义了 4 个不同的径向渐变。由于可以控制渐变的起始与结束点，所以我们可以实现一些比（如在 Photoshop 中所见的）经典的径向渐变更为复杂的效果。（经典的径向渐变是只有一个中心点，简单地由中心点向外围的圆形扩张）

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // 创建渐变
  var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
  radgrad.addColorStop(0, "#A7D30C");
  radgrad.addColorStop(0.9, "#019F62");
  radgrad.addColorStop(1, "rgba(1,159,98,0)");

  var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50);
  radgrad2.addColorStop(0, "#FF5F98");
  radgrad2.addColorStop(0.75, "#FF0188");
  radgrad2.addColorStop(1, "rgba(255,1,136,0)");

  var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40);
  radgrad3.addColorStop(0, "#00C9FF");
  radgrad3.addColorStop(0.8, "#00B5E2");
  radgrad3.addColorStop(1, "rgba(0,201,255,0)");

  var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90);
  radgrad4.addColorStop(0, "#F4F201");
  radgrad4.addColorStop(0.8, "#E4C700");
  radgrad4.addColorStop(1, "rgba(228,199,0,0)");

  // 画图形
  ctx.fillStyle = radgrad4;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad3;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad2;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad;
  ctx.fillRect(0, 0, 150, 150);
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

这里，我让起点稍微偏离终点，这样可以达到一种球状 3D 效果。但最好不要让里圆与外圆部分交叠，那样会产生什么效果就真是不得而知了。

4 个径向渐变效果的最后一个色标都是透明色。如果想要两色标直接的过渡柔和一些，只要两个颜色值一致就可以了。代码里面看不出来，是因为我用了两种不同的颜色表示方法，但其实是相同的，`#019F62 = rgba(1,159,98,1)`。

| Screenshot | Live sample |
| --- | --- |
|  |  |

图案样式 Patterns
-------------

上一节的一个例子里面，我用了循环来实现图案的效果。其实，有一个更加简单的方法：`createPattern`。

`createPattern(image, type)`

该方法接受两个参数。Image 可以是一个 `Image` 对象的引用，或者另一个 canvas 对象。`Type` 必须是下面的字符串值之一：`repeat`，`repeat-x`，`repeat-y` 和 `no-repeat`。

**备注：** 用 canvas 对象作为 `Image` 参数在 Firefox 1.5 (Gecko 1.8) 中是无效的。

图案的应用跟渐变很类似的，创建出一个 pattern 之后，赋给 `fillStyle` 或 `strokeStyle` 属性即可。

```js
var img = new Image();
img.src = "someimage.png";
var ptrn = ctx.createPattern(img, "repeat");
```

**备注：** 与 drawImage 有点不同，你需要确认 image 对象已经装载完毕，否则图案可能效果不对的。

### `createPattern` 的例子

在最后的例子中，我创建一个图案然后赋给了 `fillStyle` 属性。唯一要注意的是，使用 Image 对象的 `onload` handler 来确保设置图案之前图像已经装载完毕。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // 创建新 image 对象，用作图案
  var img = new Image();
  img.src = "canvas\_createpattern.png";
  img.onload = function () {
    // 创建图案
    var ptrn = ctx.createPattern(img, "repeat");
    ctx.fillStyle = ptrn;
    ctx.fillRect(0, 0, 150, 150);
  };
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

The result looks like this:

| Screenshot | Live sample |
| --- | --- |
|  |  |

阴影 Shadows
----------

`shadowOffsetX = float`

`shadowOffsetX` 和 `shadowOffsetY` 用来设定阴影在 X 和 Y 轴的延伸距离，它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸，正值则表示会往下或右延伸，它们默认都为 `0`。

`shadowOffsetY = float`

shadowOffsetX 和 `shadowOffsetY` 用来设定阴影在 X 和 Y 轴的延伸距离，它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸，正值则表示会往下或右延伸，它们默认都为 `0`。

`shadowBlur = float`

shadowBlur 用于设定阴影的模糊程度，其数值并不跟像素数量挂钩，也不受变换矩阵的影响，默认为 `0`。

`shadowColor = color`

shadowColor 是标准的 CSS 颜色值，用于设定阴影颜色效果，默认是全透明的黑色。

### 文字阴影的例子

这个例子绘制了带阴影效果的文字。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  ctx.shadowOffsetX = 2;
  ctx.shadowOffsetY = 2;
  ctx.shadowBlur = 2;
  ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

  ctx.font = "20px Times New Roman";
  ctx.fillStyle = "Black";
  ctx.fillText("Sample String", 5, 30);
}
```

```
<canvas id="canvas" width="150" height="80"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

我们可以通过下一章来了解文字属性和 fillText 方法相关的内容。

Canvas 填充规则
-----------

当我们用到 `fill`（或者 `clip`和`isPointinPath` ）你可以选择一个填充规则，该填充规则根据某处在路径的外面或者里面来决定该处是否被填充，这对于自己与自己路径相交或者路径被嵌套的时候是有用的。

两个可能的值：

`nonzero`

non-zero winding rule, 默认值。

`evenodd`

even-odd winding rule.

这个例子，我们用填充规则 `evenodd`

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.beginPath();
  ctx.arc(50, 50, 30, 0, Math.PI \* 2, true);
  ctx.arc(50, 50, 15, 0, Math.PI \* 2, true);
  ctx.fill("evenodd");
}
```

```
<canvas id="canvas" width="100" height="100"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

* « 上一页
* 下一页 »


使用 canvas 来绘制图形 - Web API 接口参考

使用 canvas 来绘制图形
===============

* « 上一页
* 下一页 »

既然我们已经设置了 canvas 环境，我们可以深入了解如何在 canvas 上绘制。到本文的最后，你将学会如何绘制矩形，三角形，直线，圆弧和曲线，变得熟悉这些基本的形状。绘制物体到 Canvas 前，需掌握路径，我们看看到底怎么做。

栅格
--

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes/canvas_default_grid.png)

在我们开始画图之前，我们需要了解一下画布栅格（canvas grid）以及坐标空间。上一页中的 HTML 模板中有个宽 150px, 高 150px 的 canvas 元素。如右图所示，canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。栅格的起点为左上角（坐标为（0,0））。所有元素的位置都相对于原点定位。所以图中蓝色方形左上角的坐标为距离左边（X 轴）x 像素，距离上边（Y 轴）y 像素（坐标为（x,y））。在课程的最后我们会平移原点到不同的坐标上，旋转网格以及缩放。现在我们还是使用原来的设置。

绘制矩形
----

不同于 SVG，`<canvas>` 只支持两种形式的图形绘制：矩形和路径（由一系列点连成的线段）。所有其他类型的图形都是通过一条或者多条路径组合而成的。不过，我们拥有众多路径生成的方法让复杂图形的绘制成为了可能。

首先，我们回到矩形的绘制中。canvas 提供了三种方法绘制矩形：

`fillRect(x, y, width, height)`

绘制一个填充的矩形

`strokeRect(x, y, width, height)`

绘制一个矩形的边框

`clearRect(x, y, width, height)`

清除指定矩形区域，让清除部分完全透明。

上面提供的方法之中每一个都包含了相同的参数。x 与 y 指定了在 canvas 画布上所绘制的矩形的左上角（相对于原点）的坐标。width 和 height 设置矩形的尺寸。

下面的 draw() 函数是前一页中取得的，现在就来使用上面的三个函数。

### 矩形（Rectangular）例子
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  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);
  }
}
```

该例子的输出如下图所示。

| Screenshot | Live sample |
| --- | --- |
|  |  |

`fillRect()`函数绘制了一个边长为 100px 的黑色正方形。`clearRect()`函数从正方形的中心开始擦除了一个 60\*60px 的正方形，接着`strokeRect()`在清除区域内生成一个 50\*50 的正方形边框。

接下来我们能够看到 clearRect() 的两个可选方法，然后我们会知道如何改变渲染图形的填充颜色及描边颜色。

不同于下一节所要介绍的路径函数（path function），以上的三个函数绘制之后会马上显现在 canvas 上，即时生效。

绘制路径
----

图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径，甚至一个子路径，都是闭合的。使用路径绘制图形需要一些额外的步骤。

1. 首先，你需要创建路径起始点。
2. 然后你使用画图命令去画出路径。
3. 之后你把路径封闭。
4. 一旦路径生成，你就能通过描边或填充路径区域来渲染图形。

以下是所要用到的函数：

`beginPath()`

新建一条路径，生成之后，图形绘制命令被指向到路径上生成路径。

`closePath()`

闭合路径之后图形绘制命令又重新指向到上下文中。

`stroke()`

通过线条来绘制图形轮廓。

`fill()`

通过填充路径的内容区域生成实心的图形。

生成路径的第一步叫做 beginPath()。本质上，路径是由很多子路径构成，这些子路径都是在一个列表中，所有的子路径（线、弧形、等等）构成图形。而每次这个方法调用之后，列表清空重置，然后我们就可以重新绘制新的图形。

**备注：** 当前路径为空，即调用 beginPath() 之后，或者 canvas 刚建的时候，第一条路径构造命令通常被视为是 moveTo（），无论实际上是什么。出于这个原因，你几乎总是要在设置路径之后专门指定你的起始位置。

第二步就是调用函数指定绘制路径，本文稍后我们就能看到了。

第三，就是闭合路径 closePath(),不是必需的。这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的，即当前点为开始点，该函数什么也不做。

**备注：** 当你调用 fill() 函数时，所有没有闭合的形状都会自动闭合，所以你不需要调用 closePath() 函数。但是调用 stroke() 时不会自动闭合。

### 绘制一个三角形

例如，绘制三角形的代码如下：
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="100" height="100"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    ctx.beginPath();
    ctx.moveTo(75, 50);
    ctx.lineTo(100, 75);
    ctx.lineTo(100, 25);
    ctx.fill();
  }
}
```

输出看上去如下：

| Screenshot | Live sample |
| --- | --- |
|  |  |

### 移动笔触

一个非常有用的函数，而这个函数实际上并不能画出任何东西，也是上面所描述的路径列表的一部分，这个函数就是`moveTo()`。或者你可以想象一下在纸上作业，一支钢笔或者铅笔的笔尖从一个点到另一个点的移动过程。

`moveTo(x, y)`

将笔触移动到指定的坐标 x 以及 y 上。

当 canvas 初始化或者`beginPath()`调用后，你通常会使用`moveTo()`函数设置起点。我们也能够使用`moveTo()`绘制一些不连续的路径。看一下下面的笑脸例子。我将用到`moveTo()`方法（红线处）的地方标记了。

你可以尝试一下，使用下边的代码片。只需要将其复制到之前的`draw()`函数即可。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    ctx.beginPath();
    ctx.arc(75, 75, 50, 0, Math.PI \* 2, true); // 绘制
    ctx.moveTo(110, 75);
    ctx.arc(75, 75, 35, 0, Math.PI, false); // 口 (顺时针)
    ctx.moveTo(65, 65);
    ctx.arc(60, 65, 5, 0, Math.PI \* 2, true); // 左眼
    ctx.moveTo(95, 65);
    ctx.arc(90, 65, 5, 0, Math.PI \* 2, true); // 右眼
    ctx.stroke();
  }
}
```

结果看起来是这样的：

| Screenshot | Live sample |
| --- | --- |
|  |  |

如果你想看到连续的线，你可以移除调用的 moveTo()。

**备注：** 需要学习更多关于 arc() 函数的内容，请看下面的圆弧

### 线

绘制直线，需要用到的方法`lineTo()`。

`lineTo(x, y)`

绘制一条从当前位置到指定 x 以及 y 位置的直线。

该方法有两个参数：x 以及 y，代表坐标系中直线结束的点。开始点和之前的绘制路径有关，之前路径的结束点就是接下来的开始点，等等。。。开始点也可以通过`moveTo()`函数改变。

下面的例子绘制两个三角形，一个是填充的，另一个是描边的。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    // 填充三角形
    ctx.beginPath();
    ctx.moveTo(25, 25);
    ctx.lineTo(105, 25);
    ctx.lineTo(25, 105);
    ctx.fill();

    // 描边三角形
    ctx.beginPath();
    ctx.moveTo(125, 125);
    ctx.lineTo(125, 45);
    ctx.lineTo(45, 125);
    ctx.closePath();
    ctx.stroke();
  }
}
```

这里从调用`beginPath()`函数准备绘制一个新的形状路径开始。然后使用`moveTo()`函数移动到目标位置上。然后下面，两条线段绘制后构成三角形的两条边。

| Screenshot | Live sample |
| --- | --- |
|  |  |

你会注意到填充与描边三角形步骤有所不同。正如上面所提到的，因为路径使用填充（fill）时，路径自动闭合，使用描边（stroke）则不会闭合路径。如果没有添加闭合路径`closePath()`到描边三角形函数中，则只绘制了两条线段，并不是一个完整的三角形。

### 圆弧

绘制圆弧或者圆，我们使用`arc()`方法。当然可以使用`arcTo()`，不过这个的实现并不是那么的可靠，所以我们这里不作介绍。

`arc(x, y, radius, startAngle, endAngle, anticlockwise)`

画一个以（x,y）为圆心的以 radius 为半径的圆弧（圆），从 startAngle 开始到 endAngle 结束，按照 anticlockwise 给定的方向（默认为顺时针）来生成。

`arcTo(x1, y1, x2, y2, radius)`

根据给定的控制点和半径画一段圆弧，再以直线连接两个控制点。

这里详细介绍一下 arc 方法，该方法有六个参数：`x,y`为绘制圆弧所在圆上的圆心坐标。`radius`为半径。`startAngle`以及`endAngle`参数用弧度定义了开始以及结束的弧度。这些都是以 x 轴为基准。参数`anticlockwise`为一个布尔值。为 true 时，是逆时针方向，否则顺时针方向。

**备注：** `arc()` 函数中表示角的单位是弧度，不是角度。角度与弧度的 js 表达式：

**弧度=(Math.PI/180)\*角度。**

下面的例子比上面的要复杂一下，下面绘制了 12 个不同的角度以及填充的圆弧。

下面两个`for`循环，生成圆弧的行列（x,y）坐标。每一段圆弧的开始都调用`beginPath()`。代码中，每个圆弧的参数都是可变的，实际编程中，我们并不需要这样做。

x,y 坐标是可变的。半径（radius）和开始角度（startAngle）都是固定的。结束角度（endAngle）在第一列开始时是 180 度（半圆）然后每列增加 90 度。最后一列形成一个完整的圆。

`clockwise`语句作用于第一、三行是顺时针的圆弧，`anticlockwise`作用于二、四行为逆时针圆弧。`if`语句让一、二行描边圆弧，下面两行填充路径。

**备注：** 这个示例所需的画布大小略大于本页面的其他例子：150 x 200 像素。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="200"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    for (var i = 0; i < 4; i++) {
      for (var j = 0; j < 3; j++) {
        ctx.beginPath();
        var x = 25 + j \* 50; // x 坐标值
        var y = 25 + i \* 50; // y 坐标值
        var radius = 20; // 圆弧半径
        var startAngle = 0; // 开始点
        var endAngle = Math.PI + (Math.PI \* j) / 2; // 结束点
        var anticlockwise = i % 2 == 0 ? false : true; // 顺时针或逆时针

        ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

        if (i > 1) {
          ctx.fill();
        } else {
          ctx.stroke();
        }
      }
    }
  }
}
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### 二次贝塞尔曲线及三次贝塞尔曲线

下一个十分有用的路径类型就是贝塞尔曲线。二次及三次贝塞尔曲线都十分有用，一般用来绘制复杂有规律的图形。

`quadraticCurveTo(cp1x, cp1y, x, y)`

绘制二次贝塞尔曲线，`cp1x,cp1y` 为一个控制点，`x,y` 为结束点。

`bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)`

绘制三次贝塞尔曲线，`cp1x,cp1y`为控制点一，`cp2x,cp2y`为控制点二，`x,y`为结束点。

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes/canvas_curves.png)右边的图能够很好的描述两者的关系，二次贝塞尔曲线有一个开始点（蓝色）、一个结束点（蓝色）以及一个控制点（红色），而三次贝塞尔曲线有两个控制点。

参数 x、y 在这两个方法中都是结束点坐标。`cp1x,cp1y`为坐标中的第一个控制点，`cp2x,cp2y`为坐标中的第二个控制点。

使用二次以及三次贝塞尔曲线是有一定的难度的，因为不同于像 Adobe Illustrators 这样的矢量软件，我们所绘制的曲线没有给我们提供直接的视觉反馈。这让绘制复杂的图形变得十分困难。在下面的例子中，我们会绘制一些简单有规律的图形，如果你有时间以及更多的耐心，很多复杂的图形你也可以绘制出来。

下面的这些例子没有多少困难。这两个例子中我们会连续绘制贝塞尔曲线，最后形成复杂的图形。

#### 二次贝塞尔曲线

这个例子使用多个贝塞尔曲线来渲染对话气泡。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    // 二次贝塞尔曲线
    ctx.beginPath();
    ctx.moveTo(75, 25);
    ctx.quadraticCurveTo(25, 25, 25, 62.5);
    ctx.quadraticCurveTo(25, 100, 50, 100);
    ctx.quadraticCurveTo(50, 120, 30, 125);
    ctx.quadraticCurveTo(60, 120, 65, 100);
    ctx.quadraticCurveTo(125, 100, 125, 62.5);
    ctx.quadraticCurveTo(125, 25, 75, 25);
    ctx.stroke();
  }
}
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

#### 三次贝塞尔曲线

这个例子使用贝塞尔曲线绘制心形。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    //三次贝塞尔曲线
    ctx.beginPath();
    ctx.moveTo(75, 40);
    ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
    ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
    ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
    ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
    ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
    ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
    ctx.fill();
  }
}
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### 矩形

直接在画布上绘制矩形的三个额外方法，正如我们开始所见的绘制矩形，同样，也有 rect() 方法，将一个矩形路径增加到当前路径上。

`rect(x, y, width, height)`

绘制一个左上角坐标为（x,y），宽高为 width 以及 height 的矩形。

当该方法执行的时候，moveTo() 方法自动设置坐标参数（0,0）。也就是说，当前笔触自动重置回默认坐标。

### 组合使用

目前为止，每一个例子中的每个图形都只用到一种类型的路径。然而，绘制一个图形并没有限制使用数量以及类型。所以在最后的一个例子里，让我们组合使用所有的路径函数来重现一款著名的游戏。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    roundedRect(ctx, 12, 12, 150, 150, 15);
    roundedRect(ctx, 19, 19, 150, 150, 9);
    roundedRect(ctx, 53, 53, 49, 33, 10);
    roundedRect(ctx, 53, 119, 49, 16, 6);
    roundedRect(ctx, 135, 53, 49, 33, 10);
    roundedRect(ctx, 135, 119, 25, 49, 10);

    ctx.beginPath();
    ctx.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7, false);
    ctx.lineTo(31, 37);
    ctx.fill();

    for (var i = 0; i < 8; i++) {
      ctx.fillRect(51 + i \* 16, 35, 4, 4);
    }

    for (i = 0; i < 6; i++) {
      ctx.fillRect(115, 51 + i \* 16, 4, 4);
    }

    for (i = 0; i < 8; i++) {
      ctx.fillRect(51 + i \* 16, 99, 4, 4);
    }

    ctx.beginPath();
    ctx.moveTo(83, 116);
    ctx.lineTo(83, 102);
    ctx.bezierCurveTo(83, 94, 89, 88, 97, 88);
    ctx.bezierCurveTo(105, 88, 111, 94, 111, 102);
    ctx.lineTo(111, 116);
    ctx.lineTo(106.333, 111.333);
    ctx.lineTo(101.666, 116);
    ctx.lineTo(97, 111.333);
    ctx.lineTo(92.333, 116);
    ctx.lineTo(87.666, 111.333);
    ctx.lineTo(83, 116);
    ctx.fill();

    ctx.fillStyle = "white";
    ctx.beginPath();
    ctx.moveTo(91, 96);
    ctx.bezierCurveTo(88, 96, 87, 99, 87, 101);
    ctx.bezierCurveTo(87, 103, 88, 106, 91, 106);
    ctx.bezierCurveTo(94, 106, 95, 103, 95, 101);
    ctx.bezierCurveTo(95, 99, 94, 96, 91, 96);
    ctx.moveTo(103, 96);
    ctx.bezierCurveTo(100, 96, 99, 99, 99, 101);
    ctx.bezierCurveTo(99, 103, 100, 106, 103, 106);
    ctx.bezierCurveTo(106, 106, 107, 103, 107, 101);
    ctx.bezierCurveTo(107, 99, 106, 96, 103, 96);
    ctx.fill();

    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.arc(101, 102, 2, 0, Math.PI \* 2, true);
    ctx.fill();

    ctx.beginPath();
    ctx.arc(89, 102, 2, 0, Math.PI \* 2, true);
    ctx.fill();
  }
}

// 封装的一个用于绘制圆角矩形的函数。

function roundedRect(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x, y + radius);
  ctx.lineTo(x, y + height - radius);
  ctx.quadraticCurveTo(x, y + height, x + radius, y + height);
  ctx.lineTo(x + width - radius, y + height);
  ctx.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
  ctx.lineTo(x + width, y + radius);
  ctx.quadraticCurveTo(x + width, y, x + width - radius, y);
  ctx.lineTo(x + radius, y);
  ctx.quadraticCurveTo(x, y, x, y + radius);
  ctx.stroke();
}
```

结果画面如下：

| Screenshot | Live sample |
| --- | --- |
|  |  |

我们不会很详细地讲解上面的代码，因为事实上这很容易理解。重点是绘制上下文中使用到了 fillStyle 属性，以及封装函数（例子中的`roundedRect()`）。使用封装函数对于减少代码量以及复杂度十分有用。

在稍后的课程里，我们会讨论`fillStyle`样式的更多细节。这章节中，我们对`fillStyle`样式所做的仅是改变填充颜色，由默认的黑色到白色，然后又是黑色。

Path2D 对象
---------

正如我们在前面例子中看到的，你可以使用一系列的路径和绘画命令来把对象“画”在画布上。为了简化代码和提高性能，`Path2D`对象已可以在较新版本的浏览器中使用，用来缓存或记录绘画命令，这样你将能快速地回顾路径。

怎样产生一个 Path2D 对象呢？

`Path2D()`

`Path2D()`会返回一个新初始化的 Path2D 对象（可能将某一个路径作为变量——创建一个它的副本，或者将一个包含 SVG path 数据的字符串作为变量）。

```js
new Path2D(); // 空的 Path 对象
new Path2D(path); // 克隆 Path 对象
new Path2D(d); // 从 SVG 建立 Path 对象
```

所有的路径方法比如`moveTo`, `rect`, `arc`或`quadraticCurveTo`等，如我们前面见过的，都可以在 Path2D 中使用。

Path2D API 添加了 `addPath`作为将`path`结合起来的方法。当你想要从几个元素中来创建对象时，这将会很实用。比如：

**`Path2D.addPath(path [, transform])`**

添加了一条路径到当前路径（可能添加了一个变换矩阵）。

### Path2D 示例

在这个例子中，我们创造了一个矩形和一个圆。它们都被存为 Path2D 对象，后面再派上用场。随着新的 Path2D API 产生，几种方法也相应地被更新来使用 Path2D 对象而不是当前路径。在这里，带路径参数的`stroke`和`fill`可以把对象画在画布上。
```

<html>
  <body onload="draw();">
    <canvas id="canvas" width="130" height="100"></canvas>
  </body>
</html>
```

```js
function draw() {
  var canvas = document.getElementById("canvas");
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d");

    var rectangle = new Path2D();
    rectangle.rect(10, 10, 50, 50);

    var circle = new Path2D();
    circle.moveTo(125, 35);
    circle.arc(100, 35, 25, 0, 2 \* Math.PI);

    ctx.stroke(rectangle);
    ctx.fill(circle);
  }
}
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### 使用 SVG paths

新的 Path2D API 有另一个强大的特点，就是使用 SVG path data 来初始化 canvas 上的路径。这将使你获取路径时可以以 SVG 或 canvas 的方式来重用它们。

这条路径将先移动到点 `(M10 10)` 然后再水平移动 80 个单位`(h 80)`，然后下移 80 个单位 `(v 80)`，接着左移 80 个单位 `(h -80)`，再回到起点处 (`z`)。你可以在Path2D constructor (en-US) 查看这个例子。

```js
var p = new Path2D("M10 10 h 80 v 80 h -80 Z");
```

* « 上一页
* 下一页 »


高级动画 - Web API 接口参考

高级动画
====

* « 上一页
* 下一页 »

在上一章，我们制作了基本动画以及逐步了解了让物件移动的方法。在这一部分，我们将会对运动有更深的了解并学会添加一些符合物理的运动以让我们的动画更加高级。

绘制小球
----

我们将会画一个小球用于动画学习，所以首先在画布上画一个球。下面的代码帮助我们建立画布。

```html
<canvas id="canvas" width="600" height="300"></canvas>
```

跟平常一样，我们需要先画一个 context（画布场景）。为了画出这个球，我们又会创建一个包含一些相关属性以及 `draw()` 函数的 `ball` 对象，来完成绘制。

```js
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

var ball = {
  x: 100,
  y: 100,
  radius: 25,
  color: "blue",
  draw: function () {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

ball.draw();
```

这里并没有什么特别的。小球实际上是一个简单的圆形，在`arc()` 函数的帮助下画出。

添加速率
----

现在我们有了一个小球，正准备添加一些基本动画，正如我们上一章所学的。又是这样，`window.requestAnimationFrame()` 再一次帮助我们控制动画。小球依旧依靠添加速率矢量进行移动。在每一帧里面，我们依旧用clear 清理掉之前帧里旧的圆形。

```js
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var raf;

var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: "blue",
  draw: function () {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;
  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener("mouseover", function (e) {
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener("mouseout", function (e) {
  window.cancelAnimationFrame(raf);
});

ball.draw();
```

边界
--

若没有任何的碰撞检测，我们的小球很快就会超出画布。我们需要检查小球的 x 和 y 位置是否已经超出画布的尺寸以及是否需要将速度矢量反转。为了这么做，我们把下面的检查代码添加进 `draw` 函数：

```js
if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
  ball.vy = -ball.vy;
}
if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
  ball.vx = -ball.vx;
}
```

### 首个示例

让我们看看现今它变得如何。

#### HTML

```html
<canvas id="canvas" style="border: 1px solid" width="600" height="300"></canvas>
```

#### JavaScript

```js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;

const ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: "blue",
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener("mouseover", (e) => {
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener("mouseout", (e) => {
  window.cancelAnimationFrame(raf);
});

ball.draw();
```

#### 结果

移动你的鼠标到画布里开启动画。

加速度
---

为了让动作更真实，你可以像这样处理速度，例如：

```js
ball.vy \*= 0.99;
ball.vy += 0.25;
```

这会逐帧减少垂直方向的速度，所以小球最终将只会在地板上弹跳。

### 第二个示例

#### HTML

```html
<canvas id="canvas" style="border: 1px solid" width="600" height="300"></canvas>
```

#### JavaScript

```js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;

const ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: "blue",
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;
  ball.vy \*= 0.99;
  ball.vy += 0.25;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener("mouseover", (e) => {
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener("mouseout", (e) => {
  window.cancelAnimationFrame(raf);
});

ball.draw();
```

#### 结果

长尾效果
----

现在，我们使用的是 `clearRect` 函数帮我们清除前一帧动画。若用一个半透明的 `fillRect` 函数取代之，就可轻松制作长尾效果。

```js
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
```

### 第三个示例

#### HTML

```html
<canvas id="canvas" style="border: 1px solid" width="600" height="300"></canvas>
```

#### JavaScript

```js
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;

const ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: "blue",
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

function draw() {
  ctx.fillStyle = "rgba(255, 255, 255, 0.3)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;
  ball.vy \*= 0.99;
  ball.vy += 0.25;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener("mouseover", (e) => {
  raf = window.requestAnimationFrame(draw);
});

canvas.addEventListener("mouseout", (e) => {
  window.cancelAnimationFrame(raf);
});

ball.draw();
```

#### 结果

添加鼠标控制
------

为了更好地控制小球，我们可以用 `mousemove` (en-US)事件让它跟随鼠标活动。下面例子中，click 事件会释放小球然后让它重新跳起。
```

<canvas id="canvas" style="border: 1px solid" width="600" height="300"></canvas>
```

```js
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var raf;
var running = false;

var ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 1,
  radius: 25,
  color: "blue",
  draw: function () {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI \* 2, true);
    ctx.closePath();
    ctx.fillStyle = this.color;
    ctx.fill();
  },
};

function clear() {
  ctx.fillStyle = "rgba(255,255,255,0.3)";
  ctx.fillRect(0, 0, canvas.width, canvas.height);
}

function draw() {
  clear();
  ball.draw();
  ball.x += ball.vx;
  ball.y += ball.vy;

  if (ball.y + ball.vy > canvas.height || ball.y + ball.vy < 0) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width || ball.x + ball.vx < 0) {
    ball.vx = -ball.vx;
  }

  raf = window.requestAnimationFrame(draw);
}

canvas.addEventListener("mousemove", function (e) {
  if (!running) {
    clear();
    ball.x = e.offsetX;
    ball.y = e.offsetY;
    ball.draw();
  }
});

canvas.addEventListener("click", function (e) {
  if (!running) {
    raf = window.requestAnimationFrame(draw);
    running = true;
  }
});

canvas.addEventListener("mouseout", function (e) {
  window.cancelAnimationFrame(raf);
  running = false;
});

ball.draw();
```

用你的鼠标移动小球，点击可以释放。

Breakout
--------

本短文仅仅解释了一小部分的创建高级动画的技巧。其实还有更多！试试添加一个球拍，一些砖块，然后将这个例子弄成一个 Breakout（译者注：打砖块游戏）如何？查看我们的游戏开发区去查阅更多相关文章。

参考
--

* `window.requestAnimationFrame()`
* 网页动画高效开发 (en-US)
* « 上一页
* 下一页 »


组合 Compositing - Web API 接口参考

组合 Compositing
==============

* « 上一页
* 下一页 »

在之前的例子里面，我们总是将一个图形画在另一个之上，对于其他更多的情况，仅仅这样是远远不够的。比如，对合成的图形来说，绘制顺序会有限制。不过，我们可以利用 `globalCompositeOperation` 属性来改变这种状况。此外，`clip`属性允许我们隐藏不想看到的部分图形。

`globalCompositeOperation`
--------------------------

我们不仅可以在已有图形后面再画新图形，还可以用来遮盖指定区域，清除画布中的某些部分（清除区域不仅限于矩形，像`clearRect()`方法做的那样）以及更多其他操作。

`globalCompositeOperation = type`

这个属性设定了在画新图形时采用的遮盖策略，其值是一个标识 12 种遮盖方式的字符串。

裁切路径
----

![](/zh-CN/docs/Web/API/Canvas_API/Tutorial/Compositing/canvas_clipping_path.png)裁切路径和普通的 canvas 图形差不多，不同的是它的作用是遮罩，用来隐藏不需要的部分。如右图所示。红边五角星就是裁切路径，所有在路径以外的部分都不会在 canvas 上绘制出来。

如果和上面介绍的 `globalCompositeOperation` 属性作一比较，它可以实现与 `source-in` 和 `source-atop`差不多的效果。最重要的区别是裁切路径不会在 canvas 上绘制东西，而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。

在 绘制图形 一章中，我只介绍了 `stroke` 和 `fill` 方法，这里介绍第三个方法`clip`。

`clip()`

将当前正在构建的路径转换为当前的裁剪路径。

我们使用 `clip()`方法来创建一个新的裁切路径。

默认情况下，canvas 有一个与它自身一样大的裁切路径（也就是没有裁切效果）。

### `clip` 的例子

这个例子，我会用一个圆形的裁切路径来限制随机星星的绘制区域。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.fillRect(0, 0, 150, 150);
  ctx.translate(75, 75);

  // Create a circular clipping path
  ctx.beginPath();
  ctx.arc(0, 0, 60, 0, Math.PI \* 2, true);
  ctx.clip();

  // draw background
  var lingrad = ctx.createLinearGradient(0, -75, 0, 75);
  lingrad.addColorStop(0, "#232256");
  lingrad.addColorStop(1, "#143778");

  ctx.fillStyle = lingrad;
  ctx.fillRect(-75, -75, 150, 150);

  // draw stars
  for (var j = 1; j < 50; j++) {
    ctx.save();
    ctx.fillStyle = "#fff";
    ctx.translate(
      75 - Math.floor(Math.random() \* 150),
      75 - Math.floor(Math.random() \* 150),
    );
    drawStar(ctx, Math.floor(Math.random() \* 4) + 2);
    ctx.restore();
  }
}
function drawStar(ctx, r) {
  ctx.save();
  ctx.beginPath();
  ctx.moveTo(r, 0);
  for (var i = 0; i < 9; i++) {
    ctx.rotate(Math.PI / 5);
    if (i % 2 == 0) {
      ctx.lineTo((r / 0.525731) \* 0.200811, 0);
    } else {
      ctx.lineTo(r, 0);
    }
  }
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

首先，我画了一个与 canvas 一样大小的黑色方形作为背景，然后移动原点至中心点。然后用 `clip` 方法创建一个弧形的裁切路径。裁切路径也属于 canvas 状态的一部分，可以被保存起来。如果我们在创建新裁切路径时想保留原来的裁切路径，我们需要做的就是保存一下 canvas 的状态。

裁切路径创建之后所有出现在它里面的东西才会画出来。在画线性渐变时我们就会注意到这点。然后会绘制出 50 颗随机位置分布（经过缩放）的星星，当然也只有在裁切路径里面的星星才会绘制出来。

| Screenshot | Live sample |
| --- | --- |
|  |  |

* « 上一页
* 下一页 »


Canvas 的基本用法 - Web API 接口参考

Canvas 的基本用法
============

* « 上一页
* 下一页 »

让我们通过了解 `<canvas>` HTML 元素本身开始本教程。在本页结束时，你会了解到如何去设置一个 canvas 2D 上下文以及如何在你的浏览器上创建第一个例子。

`<canvas>` 元素
-------------

```html
<canvas id="tutorial" width="150" height="150"></canvas>
```

`<canvas>` 看起来和 `<img>` 元素很相像，唯一的不同就是它并没有 src 和 alt 属性。实际上，`<canvas>` 标签只有两个属性\*\*——\*\* `width`和`height`。这些都是可选的，并且同样利用 DOM properties 来设置。当没有设置宽度和高度的时候，canvas 会初始化宽度为 300 像素和高度为 150 像素。该元素可以使用CSS来定义大小，但在绘制时图像会伸缩以适应它的框架尺寸：如果 CSS 的尺寸与初始画布的比例不一致，它会出现扭曲。

**备注：** 如果你绘制出来的图像是扭曲的，尝试用 width 和 height 属性为<canvas>明确规定宽高，而不是使用 CSS。

`id`属性并不是<canvas>元素所特有的，而是每一个 HTML 元素都默认具有的属性（比如 class 属性）。给每个标签都加上一个 id 属性是个好主意，因为这样你就能在我们的脚本中很容易的找到它。

<canvas>元素可以像任何一个普通的图像一样（有`margin`，`border`，`background`等等属性）被设计。然而，这些样式不会影响在 canvas 中的实际图像。我们将会在一个专门的章节里看到这是如何解决的。当开始时没有为 canvas 规定样式规则，其将会完全透明。

### 替换内容

<canvas>元素与`<img>`标签的不同之处在于，就像`<video>`，`<audio>`，或者 `<picture>`元素一样，很容易定义一些替代内容。由于某些较老的浏览器（尤其是 IE9 之前的 IE 浏览器）或者文本浏览器不支持 HTML 元素"canvas"，在这些浏览器上你应该总是能展示替代内容。

这非常简单：我们只是在<canvas>标签中提供了替换内容。不支持<canvas>的浏览器将会忽略容器并在其中渲染后备内容。而支持<canvas>的浏览器将会忽略在容器中包含的内容，并且只是正常渲染 canvas。

举个例子，我们可以提供对 canvas 内容的文字描述或者是提供动态生成内容相对应的静态图片，如下所示：

```html
<canvas id="stockGraph" width="150" height="150">
  current stock price: $3.15 +0.15
</canvas>

<canvas id="clock" width="150" height="150">
  <img src="images/clock.png" width="150" height="150" alt="" />
</canvas>
```

### `</canvas>` 标签不可省

与 `<img>` 元素不同，`<canvas>` 元素**需要**结束标签 (`</canvas>`)。如果结束标签不存在，则文档的其余部分会被认为是替代内容，将不会显示出来。

如果不需要替代内容，一个简单的 `<canvas id="foo" ...></canvas>` 在所有支持 canvas 的浏览器中都是完全兼容的。

渲染上下文（The rendering context）
----------------------------

`<canvas>` 元素创造了一个固定大小的画布，它公开了一个或多个**渲染上下文**，其可以用来绘制和处理要展示的内容。我们将会将注意力放在 2D 渲染上下文中。其他种类的上下文也许提供了不同种类的渲染方式；比如， WebGL 使用了基于OpenGL ES的 3D 上下文 ("experimental-webgl") 。

canvas 起初是空白的。为了展示，首先脚本需要找到渲染上下文，然后在它的上面绘制。`<canvas>` 元素有一个叫做 `getContext()` 的方法，这个方法是用来获得渲染上下文和它的绘画功能。`getContext()`接受一个参数，即上下文的类型。对于 2D 图像而言，如本教程，你可以使用 `CanvasRenderingContext2D`。

```js
var canvas = document.getElementById("tutorial");
var ctx = canvas.getContext("2d");
```

代码的第一行通过使用 `document.getElementById()` 方法来为 `<canvas>` 元素得到 DOM 对象。一旦有了元素对象，你可以通过使用它的 getContext() 方法来访问绘画上下文。

检查支持性
-----

替换内容是用于在不支持 `<canvas>` 标签的浏览器中展示的。通过简单的测试 `getContext()` 方法的存在，脚本可以检查编程支持性。上面的代码片段现在变成了这个样子：

```js
var canvas = document.getElementById("tutorial");

if (canvas.getContext) {
  var ctx = canvas.getContext("2d");
  // drawing code here
} else {
  // canvas-unsupported code here
}
```

一个模板骨架
------

这里的是一个最简单的模板，我们之后就可以把它作为之后的例子的起点。

**备注：** 为了简洁，我们在 HTML 中内嵌了 script 元素，但并不推荐这种做法。

```html
<html>
  <head>
    <title>Canvas tutorial</title>
    <script type="text/javascript">
 function draw() {
 var canvas = document.getElementById("tutorial");
 if (canvas.getContext) {
 var ctx = canvas.getContext("2d");
 }
 }
 </script>
    <style type="text/css">
 canvas {
 border: 1px solid black;
 }
 </style>
  </head>
  <body onload="draw();">
    <canvas id="tutorial" width="150" height="150"></canvas>
  </body>
</html>
```

上面的脚本中包含一个叫做 draw() 的函数，当页面加载结束的时候就会执行这个函数。通过使用在文档上加载事件来完成。只要页面加载结束，这个函数，或者像是这个的，同样可以使用 `window.setTimeout()` (en-US)， `window.setInterval()` (en-US)，或者其他任何事件处理程序来调用。

模板看起来会是这样。如这里所示，它最初是空白的。

一个简单例子
------

一开始，让我们来看个简单的例子，我们绘制了两个有趣的长方形，其中的一个有着 alpha 透明度。我们将在接下来的例子里深入探索一下这是如何工作的。

```html
<html>
  <head>
    <script type="application/javascript">
 function draw() {
 var canvas = document.getElementById("canvas");
 if (canvas.getContext) {
 var ctx = canvas.getContext("2d");

 ctx.fillStyle = "rgb(200,0,0)";
 ctx.fillRect(10, 10, 55, 50);

 ctx.fillStyle = "rgba(0, 0, 200, 0.5)";
 ctx.fillRect(30, 30, 55, 50);
 }
 }
 </script>
  </head>
  <body onload="draw();">
    <canvas id="canvas" width="150" height="150"></canvas>
  </body>
</html>
```

例子看起来像是这样：:

| Screenshot | Live sample |
| --- | --- |
|  |  |

* « 上一页
* 下一页 »