Пиксели, Excel, Kotlin и немного ностальгии…
Всем привет! Идея для этой статьи пришла еще месяц назад, но в силу занятости на работе времени катастрофически не хватало. Однажды вечером в YouTube я наткнулся на ролик о создании игры-платформера в стиле пиксельной графики. И тут мне вспомнились мои первые уроки информатики в школе, где мы «рисовали на Бейсике» и играли в «ворона ест буквы».
Предисловие
На дворе стоял 2000-й год. Кризис 98 года остался позади. Я учился в 8 классе местной школы, в небольшом городке. С началом учебного года всех ждало небольшое событие — ввели урок информатики. Многие отнеслись к этому, как к еще одному предмету который надо учить, но были и те, у кого загорелись глаза. В числе последних оказался и я.
Надо отметить, что информатику хоть и ввели, но «ввести новые компьютеры» забыли, потому что денег на эти цели не было. На службе у нашей школы тогда стояли машины made in USSR — »Электроника МС 0511» и несколько их чуть более современных аналогов. Работали они только по им самим ведомым законам, или после прихода некоего «Николая Владимировича» — местного мастера.
фото с сайта — red-innovations.suВести предмет как водится поставили молодого и «горячего» преподавателя — девушку 26 лет, которая кстати очень старалась. Мы учили системы счисления и переводили письменно числа из одной в другую. Читали про общее устройство ПК и конечно был Бейсик. У каждого тетрадка была в прочной прозрачной обложке, сзади которой была нарисована система координат. Это был своего рода холст для эскизов фигур, которые мы потом старательно переносили в код.
Именно эту тетрадь, с фигурами, нарисованными шариковой ручкой мне и напомнил ролик. Нахлынули воспоминания и захотелось сделать что-то похожее, пусть и без Бейсика, тем более что выдалась пара свободных вечеров.
Рисуем первое изображение
Для своих целей я взял BufferedImage. Начал с простой функции, которая рисует пиксель в заданных координатах и с определенным цветом.
fun drawPixel(
x:Int, y:Int, red:Int, green:Int, blue: Int,
image: BufferedImage
) {
image.setRGB(x, y, Color(red,green,blue).rgb)
}
Чтобы проверить работу набросал метод, который выводит картинку с пикселями рандомного цвета. В функции можно понизить значение каждого из каналов цвета, задав диапазон — красного redRng, зеленого greenRng и синего blueRng цвета.
fun drawRandImage(
image: BufferedImage, stepSize: Int = 1,
redRng: Int = 255, greenRng: Int = 255, blueRng: Int = 255
) {
for(posX in 0 until image.width step stepSize){
for (posY in 0 until image.height step stepSize) {
val r = if (redRng <= 0) 0 else Random.nextInt(0, redRng)
val g = if (greenRng <= 0) 0 else Random.nextInt(0, greenRng)
val b = if (blueRng <= 0) 0 else Random.nextInt(0, blueRng)
drawPixel(posX, posY, r, g, b, image)
}
}
}
Если поставить в цикле шаг stepSize отличный от единицы и занизить один из каналов, то можно получить интересный эффект.
рандомное изображение 1.) step 3, RGB (11, 238, 229) 2.) step 2, RGB (181, 19, 227)Вроде что-то вырисовывается. Теперь надо сохранить результат. Роль по записи изображения была героически возложена на ImageIO. Насколько я знаю — он блокирующий, поэтому я его от греха подальше обернул в Thread.
fun writeImage(img: BufferedImage, file: String) {
val imgthread = Thread(Runnable {
ImageIO.write(img, File(file).extension, File(file))
})
try {
imgthread.start()
} catch (ex: Exception) {
ex.printStackTrace()
imgthread.interrupt()
}
}
Останавливаться на этом было глупо, поэтому следующим шагом решил сделать »рисовалку» на базе двумерного списка.
Пиксельное сердце
Координаты для отрисовки решил сделать в виде двумерного списка ArrayList>. Получить «пиксельный» эффект мне помогла функция drawTitle, которая «дергает» в цикле drawPixel, рисуя «big pixel» в виде плитки.
fun drawTile(
startX: Int, startY: Int, size: Int,
red: Int, green: Int, blue: Int, image: BufferedImage
) {
for (posX in startX until startX+size) {
for (posY in startY until startY+size) {
drawPixel(posX,posY,red,green,blue,image)
}
}
}
Настала очередь обработать массив с числами. Сказано-сделано. Добавив с помощью оператора when обработку 4 цветов…
fun drawImage(pixels: ArrayList>, image: BufferedImage) {
pixels.forEachIndexed { posY, row ->
row.forEachIndexed { posX, col ->
when(col) {
1 -> drawTile(posX*10,posY*10,10,255,2,0,image)
2 -> drawTile(posX*10,posY*10,10,156,25,31,image)
3 -> drawTile(posX*10,posY*10,10,255,255,255,image)
else -> drawTile(posX*10,posY*10,10,23,0,44,image)
}
}
}
}
…и создав список в виде двумерного массива, где каждая цифра соответствует своему цвету (1 = красный, 2 = темно-красный, 3 = белый, 4 = фиолетовый)
val map = arrayListOf(
listOf(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
listOf(0,0,0,1,1,1,0,0,0,1,2,2,0,0,0),
listOf(0,0,1,3,3,1,1,0,1,1,1,2,2,0,0),
listOf(0,1,3,3,1,1,1,1,1,1,1,1,2,2,0),
listOf(0,1,3,1,1,1,1,1,1,1,1,1,2,2,0),
listOf(0,1,1,1,1,1,1,1,1,1,1,1,2,2,0),
listOf(0,1,1,1,1,1,1,1,1,1,1,1,2,2,0),
listOf(0,0,1,1,1,1,1,1,1,1,1,2,2,0,0),
listOf(0,0,0,1,1,1,1,1,1,1,2,2,0,0,0),
listOf(0,0,0,0,1,1,1,1,1,2,2,0,0,0,0),
listOf(0,0,0,0,0,1,1,1,2,2,0,0,0,0,0),
listOf(0,0,0,0,0,0,1,2,2,0,0,0,0,0,0),
listOf(0,0,0,0,0,0,0,2,0,0,0,0,0,0,0),
listOf(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
)
…на выходе получил такую красоту. Мой внутренний «школьник» был очень доволен.
pixel heartИ хотя все получилось как я ожидал, но «рисовать цифрами» то еще удовольствие, да и хотелось на выходе получать что-то посложнее в плане цвета и детализации, поэтому я задумался о визуальном редакторе. Но запасы чая таяли на глазах, а вечер постепенно перетекал в ночь, поэтому решено было отложить задачу до завтра.
Excel как холст
Следующим вечером я продолжил. Сперва подумал о JS (Resct JS), но тут нужно было переписывать все полностью на нем, да и JavaScript я пробовал слишком давно. Хотелось взять что-то простое…
По работе часто приходится работать с таблицами, поэтому само собой выбор остановился на Excel. Привел строки столбцы к виду квадратной сетки и вуаля — наш холст готов к работе с цифровыми красками. Осталось лишь только получить данные из ячеек.»Цифровая бумага все стерпит» — подумал я, и взял Apache POI— библиотеку для работы файлами word, excel, pdf. Документация у нее написана хорошо, но некоторые примеры кода там явно требуют корректировки.
Для начала набросал простую лямбду для преобразования hex в rgba, которая отдает стандартный джавовский класс Color.
val toRGBA = { hex: String ->
val red = hex.toLong(16) and 0xff0000 shr 16
val green = hex.toLong(16) and 0xff00 shr 8
val blue = hex.toLong(16) and 0xff
val alpha = hex.toLong(16) and 0xff000000 shr 24
Color(red.toInt(),green.toInt(),blue.toInt(),alpha.toInt())
}
Теперь оставалось пройтись по листу и собрать все ячейки в массив, попутно извлекая цвет у закрашенной ячейки и проставляя его в пустых.
fun getPixelColors(file: String, listName: String): ArrayList> {
val table = FileInputStream(file)
val sheet = WorkbookFactory.create(table).getSheet(listName)
val rowIterator: Iterator = sheet.iterator()
val rowArray: ArrayList = ArrayList()
val cellArray: ArrayList = ArrayList()
while (rowIterator.hasNext()) {
val row: Row = rowIterator.next()
rowArray.add(row.rowNum)
val cellIterator = row.cellIterator()
while (cellIterator.hasNext()) {
val cell = cellIterator.next()
cellArray.add(cell.address.column)
}
}
val rowSize = rowArray.maxOf { el->el }
//...проходим по листу
//...и формируем массив
return pixelMatrix
}
Функция немаленькая и всю ее приводить я не буду (ссылка на код в конце статьи). Конечно, ее можно сократить, но ради читаемости я оставил все как есть. И тут хотелось бы остановиться на одном моменте.
Чтобы создать двумерный массив с пикселями, нужно узнать количество строк и столбцов, в которых есть закрашенные ячейки. И если следовать примеру из документации и сделать так…
val rows = sheet.lastRowNum
val cells = sheet.getRow(rows).lastCellNum // + rows
val pixArray = Array(rows+1) {Array(ccc+1) {""} }
…то Вы получите ошибку OutOfBounds. Количество строк (row) получается всегда правильным, но количество ячеек порой то меньше, то больше чем нужно. Я так и не понял, почему результат «скачет», причем проявляется это рандомно. Исправить это можно при помощи iterator.hasNext (), который реально возвращает последнюю ячейку.
Редактор пикселей в ExcelДело сталось за малым — преобразовать нашу «пиксельную матрицу» в картинку и вернуть в качестве результата BufferedImage. В отличии от начала статьи, тип картинки у нас изменился на — TYPE_INT_ARGB, чтобы не закрашенные ячейки таковыми и оставались.
fun renderImage(pixels: ArrayList>): BufferedImage {
val resultImage = BufferedImage(
pixels[0].size*10,
pixels.size*10,
BufferedImage.TYPE_INT_ARGB
)
pixels.forEachIndexed { posY, row ->
row.forEachIndexed { posX, col ->
drawTile(
(posX)*10,(posY)*10, 10,
toRGBA(col).red, toRGBA(col).green,toRGBA(col).blue,
toRGBA(col).alpha, resultImage
)
}
}
return resultImage
}
Теперь, запасшись малиновым чаем и любимой музыкой можно придаться ностальгии и творить.
отрисованная картина в Excel. за основу взята работа Mockingjay1701Выводы
Весь код доступен по ссылке на github. Что дальше? В планах добавить поддержку svg, может добавить несколько фильтров (blur, glitch, glow, etc…), переписать все с «индусского кода» на человеческий, добавить поддержку xls (HSSF Color) и возможно набросать пару тестов. Чего-то больше добавлять не имеет смысла, так как это скорее интересная задача с легким налетом ностальгии, чем какой-то проект.
Послесловие
Конечно, можно было ограничиться лишь «Фотошопом и Экселем» (ctrl+c, ctrl+v), но цель была не просто получить пиксельный «шедевр» в пару кликов. Хотелось вспомнить школьные уроки информатики, ту теплую атмосферу: Бейсик, старые компьютеры, пиксельные рисунки на экране черно-белого монитора «Электроника МС». Да черт побери, в конечном счете это хоть и простая, но интересная задача, потратить на которую пару вечеров просто приятно.
И раз уж текст скорее всего выйдет накануне 14 февраля, то пусть он будет своеобразным признанием в любви к технологиям, которыми я с того самого дня и по настоящее время увлечен.
Пусть через пару лет «Электронику МС» сменили современные аналоги на базе Pentium, те первые занятия на старых компьютерах навсегда останутся со мной, ведь именно они вложили в меня любовь к компьютерам и всему что с ними связано…
А с чего начиналась информатика у Вас в школе?
Всем спасибо! Всем пока!