Отрисовка сетки на canvas и событие по клику

Здравствуйте! Ребят, может кто-то подсказать как можно решить проблему. Суть в том, что есть отрисованная сетка на Canvas. Вопрос такой:

  1. Как сделать чтобы эта сетка представляла собой массив данных (где пустая клетка = 0, а закрашенная = 1)
  2. Как на эту сетку (с данными) повесить события по клику мышки. Где клик или кликнул и провел мышью закрашивали бы клетки.
    Разобрался пока только в отрисовке сетки линиями (Это вообще хорошо или плохо? Может подобная сетка именно на canvas реализуется по другому?)

Приветствия.

Во-первых оформляй код текстом, а не скриншотом. Так будет проще с ним работать, и ты с большей вероятностью получишь ответ на свой вопрос. Пример хорошо оформленного вопроса - Подсчет среднего значения ячеек столбца

Во-вторых. То, что делает твой код - чисто отрисовка линий. Хоть визуально мы видим ячейки, в программе нет сущностей, которые представляли собой ячейки.

Задача решается так: ты создаешь структуру данных, которая представляет набор ячеек. Для представления ячеек лучше использовать объект. Набор ячеек представить массивом. Можно представить все ячейки как массивы массивов. Можно как один массив. Разница в выборе в том, на сколько будет просто обрабатывать полученную структуру данных для твоих задач.

Вот код https://jsfiddle.net/yyjoazuy/

var canvas = document.querySelector('canvas')
var context = canvas.getContext('2d')

// Представление всех ячеек
var cells = []
var canvasWidth = 300
var canvasHeight = 300
canvas.width = canvasWidth
canvas.height = canvasHeight
var cellWidth = 10
var cellHeight = 10
var cellsInRow = Math.floor(canvasWidth / cellWidth)
var cellsInColumn = Math.floor(canvasHeight / cellHeight)

for (var top = 0; top < canvasWidth; top += cellWidth) {
	for (var left = 0; left < canvasHeight; left += cellHeight) {
		var cell = {
			top: top,
			left: left,
			solid: false,
			// аргумент говорит о том каким цветом закрашивать клетку. Предполагается что у клетки может быть 2 цвета. 
			fill: function (solid) {
				// запоминаем состояние закрашенности клетки
				this.solid = solid
				context.fillStyle = solid ? '#000' : '#fff';
				context.fillRect(this.top, this.left, cellWidth,cellHeight);
			},
			drawBorder: function () {
				context.beginPath();
				context.strokeStyle = 'yellow';
				// magic. According to http://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry
				context.moveTo(this.top - 0.5, this.left - 0.5)
				context.lineTo(this.top - 0.5, this.left + cellWidth - 0.5)
				context.lineTo(this.top + cellHeight - 0.5, this.left + cellWidth - 0.5)
				context.lineTo(this.top + cellHeight - 0.5, this.left - 0.5)
				context.lineTo(this.top - 0.5, this.left - 0.5)
				context.stroke()
			},
			getTop: function () {
				return this.top
			},
			getLeft: function () {
				return this.left
			}
		}
		cells.push(cell)
		cell.fill(true)
		cell.drawBorder()
	}
}

function getCellByPosition(top, left) {
	var topIndex = Math.floor(top / cellHeight) * cellsInRow
	var leftIndex = Math.floor(left / cellWidth)
	return cells[topIndex + leftIndex]
}

// Взаимодействие
var filling = false

function fillCellAtPositionIfNeeded(x, y, fillingMode) {
	var cellUnderCursor = getCellByPosition(x, y)
	if (cellUnderCursor.solid !== fillingMode) {
		cellUnderCursor.fill(fillingMode)
	}
	cell.drawBorder()
}
function handleMouseDown(event) {
	// нужно вычислить координаты клика относительно верхнего левого края canvas
	// это делается с использованием вычисления координат канваса и кроссбраузерных свойств объекта event
	// я использую некроссбраузерные свойства объекта событий
	filling = !getCellByPosition(event.layerX, event.layerY).solid
	fillCellAtPositionIfNeeded(event.layerX, event.layerY, filling)

	canvas.addEventListener('mousemove', handleMouseMove, false)
}

function handleMouseUp() {
	canvas.removeEventListener('mousemove', handleMouseMove)
}

function handleMouseMove(event) {
	fillCellAtPositionIfNeeded(event.layerX, event.layerY, filling)
}

canvas.addEventListener('mousedown', handleMouseDown, false)
canvas.addEventListener('mouseup', handleMouseUp, false)

По коду:

  1. Каждый объект ячейки хранит информацию о заполненности ячейки solid. Эта иформация используется как снаружи чтобы принять решение как обрабатывать клик по ячейке. Так же хранит метод, которые знает как закрасить ячейку.
  2. Отрисовка границ ячеек не идеальна. Границы ячеек совпадают, и когда закрашивается одна ячейка, она перекрывает границу ячейки рядом. Я упростил задачу вычисления положения ячейки.
  3. Не все ячейки под мышкой закрашиваются, когда водишь мышкой быстро. Это особенность работы браузера (положение мыши считывается раз в 16мс).
  4. Канвас не дает API, которое позволило бы работать с элементами канваса как с объектами. С точки зрения использования канвас - это набор точек с состоянием каждой точки. Поэтому чтобы работать на канвасе с объектами, нужно самому вести учет где объект расположен, какую имеет форму. В этой задаче я, исходя из предпосылок, что все ячейки имеют одинаковый размер, и плотно подогнаны друг к другу, вычисляю объект ячейки по координатам клика. Есть библиотеки, которые представляют объекты, отрисованные на канвасе в виде объектов. С их использованием работа с элементами канваса сводится к манипуляции объектами. Рекомендую http://fabricjs.com/

Благодарю за ответ. Да, прошу прощения, последующие посты будут оформлены по правилам и с примерами кода.

Что я смог реализовать по задаче:

Есть некоторое поле с набором ячеек (аля “клеток”), которые представлены как массив массивов (Вы это упомянули в своем примере)
Каждый элемент массива хранит информацию

Есть функция (она временная, пока только для того, чтобы понять как закрашивать клетки) которая отслеживает координаты положения мыши в текущий момент. Пока не совсем понимаю как отследить попадают ли эти координаты в область клетки для того чтобы ее закрасить по клику. Когда заполняется массив, он рисует клетку следующим образом: элементы массива и их перебор устанавливают положение верхнего левого угла прямоугольника. Например: Элемент [0, 0] = видимая область 8x8px, Элемент [1,1], [2,1], [3,1] = видимая область 24х8px. Какой проверкой можно связать координаты и элементы?

Мне нужно немного времени чтобы переваривать Ваш код (чтобы что-то из него почерпнуть, или задать вопрос почему именно так). Потому что не все функции (порой даже синтаксис) мне понятны и я обращаюсь к литературе по JS.

Удобно выводить общий подход из частности. Посмотрим на матрицу 4*4.

var matrix = [
	[0, 1, 0, 0],
	[1, 0, 0, 0],
	[1, 0, 0, 0],
	[1, 0, 0, 0]
]

Каждой ячейке будет соответствовать квадрат 8*8 на канвасе. Будем думать о координатах начиная с верхнего левого угла канваса (где бы он ни был на странице). Можно представить картинку канваса себе в голове. А еще лучше нарисовать на бумаге и тыкать в нее ручкой. Далее.
Ячейке с какими координатами будет соответствовать координата 1 (ось x), 5 (ось y)? [0, 0]
Ячейке с какими координатами будет соответствовать координата 12 (ось x), 7 (ось y)? [1, 0]
Ячейке с какими координатами будет соответствовать координата 20 (ось x), 7 (ось y)? [2, 0]

Для горизонтальной координаты - колько ширин ячейки (с округлением в большую сторону) находится в значении горизонтальной координаты мыши, столько ячеек в длине. Зная ширину ячейки, получаем ее последовательный индекс.

Для вертикальной координаты - сколько высот ячейки в значении в вертикальной координаты, такому ряду ячейки соответствует координата.
координата 5 (ось x), 3 (ось y) [0, 0]
координата 5 (ось x), 15 (ось y) [0, 1]
координата 5 (ось x), 17 (ось y) [0, 2]

Я теоретически понимаю, как Вы предлагаете действовать, Дмитрий. То есть если я возьму любую координату, то смогу определить какому элементу она будет соответствовать, рассчитав это где-то на листке. Но никак не могу вывести эту формулу в общем виде для любого элемента массива, уже непосредственно в программном коде. (Т.е не понимаю до конца, как грамотно высчитывать, в какую из сторон округлять и т.п, и по какому принципу это именно так должно работать)

Для примера смотри как я решал подобную в примере выше.

function getCellByPosition(top, left) {
	var topIndex = Math.floor(top / cellHeight) * cellsInRow
	var leftIndex = Math.floor(left / cellWidth)
	return cells[topIndex + leftIndex]
}

Для примера будем работать с полем 4x3. (4 ряда, 3 колонки). Клетки поля представлены массивом. Для поля 4x3 массив будет состоять из 12 клеток. У каждой клетки есть индекс - ее место в массиве.
На поле у каждой клетки есть свое положение. Отобразим индексы в местах положения клеток на поле 4x3

0 1 2  3
4 5 6  7
8 9 10 11

Для примера будем работать только с первым рядом клеток. Клетки имеют ширину и мы ее знаем. Клетки стоят вплотную друг к другу. Где заканчивается одна клетка, начинается вторая. Допустим ширина клетки 10px.

Пусть горизонтальная координата меньше 10px (считаю от верхего левого угла поля). Эта координата находится на первой клеткой в ряду. Над клеткой с индексом 0. Если горизонтальная координата - 12px, это соответствует одной целой ширине клетки (отступаем 1 клетку) + 2px. Индекс клетки, которой соответствует координата - 1. Если горизонтальная координата - 27px, это соответствует 2 ширинам клетки (отступаем 2 клетки в ряду) + 7px. Горизонтальный индекс клетки - то, сколько целых ширин клетки в значении координаты. Следовательно, целая часть от деления горизонтальной координаты на ширину клетки, и есть индекс клетки.

Чтобы получить целую часть от деления в js, надо использовать Math.floor.


Для подсчета индекса клетки по вертикальной координате (считаю из верхнего левого угла) нужно использовать количество клеток в ряду.


Конкретная реализация поиска клетки будет зависеть от того как ты представляешь клетки. Напомню, что в примере выше, я описал как искал координаты клеток для случая когда все поле представленно плоским (без вложенностей) массивом.

Спасибо, Дмитрий. Вы объяснили очень доступным языком. Теперь разобрался как по координатам определить индекс клетки.

Получается в Вашем примере, массив с клетками - он одномерный. Каждый раз клетка добавляется в конец массива. А поле с клетками мы видим только за счет отрисовки на канве.

Еще хотел задать такой вопрос:
Опять же, в Вашем примере клетка - это объект. Все поле представлено массивом объектов. Можно ли объекту задать поле, например, чтобы объект хранил информацию об этом значении в виде числа. Изначально - пустое поле. По клику они меняются. Повторный клик привод к исходному состоянию. И собственно вопрос состоит вот в чем. Как можно реализовать переключение между этими состояниями.

Чтобы было удобнее закрашивать клетки, будем хранить функцию закрашивания в самом объекте клетки.
В том же объекте клетки будем хранить информацию о текущем состоянии.

// ...

              var cell = {
			top: top,
			left: left,
			solid: false,
			// аргумент говорит о том каким цветом закрашивать клетку. Предполагается что у клетки может быть 2 цвета. 
			fill: function (solid) {
				// запоминаем состояние закрашенности клетки
				this.solid = solid
				context.fillStyle = solid ? '#000' : '#fff';
				context.fillRect(this.top, this.left, cellWidth,cellHeight);
			},

// ...

Вот так реализовано хранение двух состояний (solid) клетки. А так же функция закрашивания клетки fill.

Получается, мы можем взять любую клетку из массива (массив мы назвали cells) и закрасить ее. Например так мы закрасим самую первую клетку cells[0].fill().

Тогда твоя задача сводится к тому чтобы при клике по клетке находить объект, который этой клетке соответствует. И вызывать метод, который закрасит клетку. Если модифицируешь мой пример, то можешь передавать в клетку любой цвет. Состояние solid (и имя лучше тоже поменять) хранить не в более удобном формате.

А то, в какой цвет закрашивать, храни в общедоступной переменной. Примерный код:

var fillingColor = '#f00'


button1.onclick = function () {
    fillingColor = '#f00'
}

button2.onclick = function () {
    fillingColor = '#0f0'
}

canvas.onclick = function (event) {
    var cell = getCell(cells, event)
    cell.fill(fillingColor)
}