画布

我们要定义的第一个组件是界面的一部分,它将图片显示为彩色框的网格。 该组件负责两件事:显示图片并将该图片上的指针事件传给应用的其余部分。

因此,我们可以将其定义为仅了解当前图片,而不是整个应用状态的组件。 因为它不知道整个应用是如何工作的,所以不能直接发送操作。 相反,当响应指针事件时,它会调用创建它的代码提供的回调函数,该函数将处理应用的特定部分。

  1. const scale = 10;
  2. class PictureCanvas {
  3. constructor(picture, pointerDown) {
  4. this.dom = elt("canvas", {
  5. onmousedown: event => this.mouse(event, pointerDown),
  6. ontouchstart: event => this.touch(event, pointerDown)
  7. });
  8. drawPicture(picture, this.dom, scale);
  9. }
  10. setState(picture) {
  11. if (this.picture == picture) return;
  12. this.picture = picture;
  13. drawPicture(this.picture, this.dom, scale);
  14. }
  15. }

我们将每个像素绘制成一个10x10的正方形,由比例常数决定。 为了避免不必要的工作,该组件会跟踪其当前图片,并且仅当将setState赋予新图片时才会重绘。

实际的绘图功能根据比例和图片大小设置画布大小,并用一系列正方形填充它,每个像素一个。

  1. function drawPicture(picture, canvas, scale) {
  2. canvas.width = picture.width * scale;
  3. canvas.height = picture.height * scale;
  4. let cx = canvas.getContext("2d");
  5. for (let y = 0; y < picture.height; y++) {
  6. for (let x = 0; x < picture.width; x++) {
  7. cx.fillStyle = picture.pixel(x, y);
  8. cx.fillRect(x * scale, y * scale, scale, scale);
  9. }
  10. }
  11. }

当鼠标悬停在图片画布上,并且按下鼠标左键时,组件调用pointerDown回调函数,提供被点击图片坐标的像素位置。 这将用于实现鼠标与图片的交互。 回调函数可能会返回另一个回调函数,以便在按下按钮并且将指针移动到另一个像素时得到通知。

  1. PictureCanvas.prototype.mouse = function(downEvent, onDown) {
  2. if (downEvent.button != 0) return;
  3. let pos = pointerPosition(downEvent, this.dom);
  4. let onMove = onDown(pos);
  5. if (!onMove) return;
  6. let move = moveEvent => {
  7. if (moveEvent.buttons == 0) {
  8. this.dom.removeEventListener("mousemove", move);
  9. } else {
  10. let newPos = pointerPosition(moveEvent, this.dom);
  11. if (newPos.x == pos.x && newPos.y == pos.y) return;
  12. pos = newPos;
  13. onMove(newPos);
  14. }
  15. };
  16. this.dom.addEventListener("mousemove", move);
  17. };
  18. function pointerPosition(pos, domNode) {
  19. let rect = domNode.getBoundingClientRect();
  20. return {x: Math.floor((pos.clientX - rect.left) / scale),
  21. y: Math.floor((pos.clientY - rect.top) / scale)};
  22. }

由于我们知道像素的大小,我们可以使用getBoundingClientRect来查找画布在屏幕上的位置,所以可以将鼠标事件坐标(clientXclientY)转换为图片坐标。 它们总是向下取舍,以便它们指代特定的像素。

对于触摸事件,我们必须做类似的事情,但使用不同的事件,并确保我们在"touchstart"事件中调用preventDefault以防止滑动。

  1. PictureCanvas.prototype.touch = function(startEvent,
  2. onDown) {
  3. let pos = pointerPosition(startEvent.touches[0], this.dom);
  4. let onMove = onDown(pos);
  5. startEvent.preventDefault();
  6. if (!onMove) return;
  7. let move = moveEvent => {
  8. let newPos = pointerPosition(moveEvent.touches[0],
  9. this.dom);
  10. if (newPos.x == pos.x && newPos.y == pos.y) return;
  11. pos = newPos;
  12. onMove(newPos);
  13. };
  14. let end = () => {
  15. this.dom.removeEventListener("touchmove", move);
  16. this.dom.removeEventListener("touchend", end);
  17. };
  18. this.dom.addEventListener("touchmove", move);
  19. this.dom.addEventListener("touchend", end);
  20. };

对于触摸事件,clientXclientY不能直接在事件对象上使用,但我们可以在touches属性中使用第一个触摸对象的坐标。