Всем привет! Идея для этой статьи пришла еще месяц назад, но в силу занятости на работе времени катастрофически не хватало. Однажды вечером в 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<List<Int>>. Получить "пиксельный" эффект мне помогла функция 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<List<Int>>, 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<List<String>> { val table = FileInputStream(file) val sheet = WorkbookFactory.create(table).getSheet(listName) val rowIterator: Iterator<Row> = sheet.iterator() val rowArray: ArrayList<Int> = ArrayList() val cellArray: ArrayList<Int> = 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.lastRowNumval cells = sheet.getRow(rows).lastCellNum // + rowsval pixArray = Array(rows+1) {Array(ccc+1) {""} }
...то Вы получите ошибку OutOfBounds. Количество строк (row) получается всегда правильным, но количество ячеек порой то меньше, то больше чем нужно. Я так и не понял, почему результат "скачет", причем проявляется это рандомно. Исправить это можно при помощи iterator.hasNext(), который реально возвращает последнюю ячейку.
Редактор пикселей в ExcelДело сталось за малым - преобразовать нашу "пиксельную матрицу" в картинку и вернуть в качестве результата BufferedImage. В отличии от начала статьи, тип картинки у нас изменился на - TYPE_INT_ARGB, чтобы не закрашенные ячейки таковыми и оставались.
fun renderImage(pixels: ArrayList<List<String>>): 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, те первые занятия на старых компьютерах навсегда останутся со мной, ведь именно они вложили в меня любовь к компьютерам и всему что с ними связано...
А с чего начиналась информатика у Вас в школе?
Всем спасибо! Всем пока!