画布
我们要定义的第一个组件是界面的一部分,它将图片显示为彩色框的网格。 该组件负责两件事:显示图片并将该图片上的指针事件传给应用的其余部分。
因此,我们可以将其定义为仅了解当前图片,而不是整个应用状态的组件。 因为它不知道整个应用是如何工作的,所以不能直接发送操作。 相反,当响应指针事件时,它会调用创建它的代码提供的回调函数,该函数将处理应用的特定部分。
const scale = 10;class PictureCanvas {constructor(picture, pointerDown) {this.dom = elt("canvas", {onmousedown: event => this.mouse(event, pointerDown),ontouchstart: event => this.touch(event, pointerDown)});drawPicture(picture, this.dom, scale);}setState(picture) {if (this.picture == picture) return;this.picture = picture;drawPicture(this.picture, this.dom, scale);}}
我们将每个像素绘制成一个10x10的正方形,由比例常数决定。 为了避免不必要的工作,该组件会跟踪其当前图片,并且仅当将setState赋予新图片时才会重绘。
实际的绘图功能根据比例和图片大小设置画布大小,并用一系列正方形填充它,每个像素一个。
function drawPicture(picture, canvas, scale) {canvas.width = picture.width * scale;canvas.height = picture.height * scale;let cx = canvas.getContext("2d");for (let y = 0; y < picture.height; y++) {for (let x = 0; x < picture.width; x++) {cx.fillStyle = picture.pixel(x, y);cx.fillRect(x * scale, y * scale, scale, scale);}}}
当鼠标悬停在图片画布上,并且按下鼠标左键时,组件调用pointerDown回调函数,提供被点击图片坐标的像素位置。 这将用于实现鼠标与图片的交互。 回调函数可能会返回另一个回调函数,以便在按下按钮并且将指针移动到另一个像素时得到通知。
PictureCanvas.prototype.mouse = function(downEvent, onDown) {if (downEvent.button != 0) return;let pos = pointerPosition(downEvent, this.dom);let onMove = onDown(pos);if (!onMove) return;let move = moveEvent => {if (moveEvent.buttons == 0) {this.dom.removeEventListener("mousemove", move);} else {let newPos = pointerPosition(moveEvent, this.dom);if (newPos.x == pos.x && newPos.y == pos.y) return;pos = newPos;onMove(newPos);}};this.dom.addEventListener("mousemove", move);};function pointerPosition(pos, domNode) {let rect = domNode.getBoundingClientRect();return {x: Math.floor((pos.clientX - rect.left) / scale),y: Math.floor((pos.clientY - rect.top) / scale)};}
由于我们知道像素的大小,我们可以使用getBoundingClientRect来查找画布在屏幕上的位置,所以可以将鼠标事件坐标(clientX和clientY)转换为图片坐标。 它们总是向下取舍,以便它们指代特定的像素。
对于触摸事件,我们必须做类似的事情,但使用不同的事件,并确保我们在"touchstart"事件中调用preventDefault以防止滑动。
PictureCanvas.prototype.touch = function(startEvent,onDown) {let pos = pointerPosition(startEvent.touches[0], this.dom);let onMove = onDown(pos);startEvent.preventDefault();if (!onMove) return;let move = moveEvent => {let newPos = pointerPosition(moveEvent.touches[0],this.dom);if (newPos.x == pos.x && newPos.y == pos.y) return;pos = newPos;onMove(newPos);};let end = () => {this.dom.removeEventListener("touchmove", move);this.dom.removeEventListener("touchend", end);};this.dom.addEventListener("touchmove", move);this.dom.addEventListener("touchend", end);};
对于触摸事件,clientX和clientY不能直接在事件对象上使用,但我们可以在touches属性中使用第一个触摸对象的坐标。
