Главная страница «Первого сентября»Главная страница журнала «Информатика»Содержание №8/2008


Семинар

Язык Java и его возможности

Окончание. Начало см. в № 6, 7/2008

Часть III. Динамические изображения

В предыдущих публикациях мы уже неоднократно говорили, что главными достоинствами программ-аплетов следует назвать их интерактивность и динамичность. Сегодня мы рассмотрим подробнее, как именно создаются на Java динамически меняющиеся изображения.

Известно, что создание любой анимационной картинки опирается на средства статической графики и эффективный механизм замены отдельных кадров.
С наиболее важными принципами организации графики в Java мы уже знакомы из части I. Что касается второй составляющей проблемы, т.е. реализации замены картинок с течением времени, то для этой цели (кстати, и не только для нее одной!) в Java существует очень мощный механизм — потоки. Понимание базовых идей организации потоков в современной информатике трудно переоценить, поскольку одновременное параллельное исполнение нескольких процессов есть одно из магистральных направлений совершенствования как software, так и hardware. Вспомним хотя бы о многопоточности современных операционных систем или о многоядерных микропроцессорах, где каждое отдельное ядро занято решением некоторой подзадачи. Язык Java, с самого начала созданный в качестве многопоточного (см. часть I), предоставляет нам широкое поле для изучения потоков. В большинстве примеров мы ограничимся одним потоком, но последняя весьма эффектная учебная задача даст нам повод поэкспериментировать с несколькими самостоятельными потоками.

Как читатели уже, наверное, догадались, разделение свойств интерактивности и динамичности (в терминах данной публикации это части II и III) удается осуществить благодаря относительной независимости средств их реализации. Именно поэтому приводимые ниже примеры сознательно лишены интерактивности: общепринятая в педагогике точка зрения советует при первоначальном изучении явления по возможности сконцентрироваться на его наиболее характерных чертах. Зато, освоив по отдельности интерактивность и динамичность, читатели смогут легко объединить 1 все преимущества этих мощных технологий и создать на основе Java-аплетов учебные материалы нового поколения, возможности которых существенно выходят за рамки традиционных педагогических средств.

1. Принцип создания динамического изображения

Общая идея получения изменяющихся (движущихся) изображений довольно проста и интуитивно понятна: динамический процесс разбивают на отдельные фазы (кадры), которые в ходе демонстрации сменяются с необходимой скоростью. Сразу заметим, что представление непрерывного изменения в виде последовательности статических кадров есть не что иное, как дискретизация, поэтому такая технология прекрасно подходит для компьютера.

Конкретные способы получения кадров и их демонстрации весьма разнообразны. Например, не так давно, когда съемка кинофильмов производилась механическими камерами на специальную светочувствительную пленку, большое внимание уделялось технике транспортировки киноленты вдоль объектива. Сейчас, когда многие люди уже не встречались с подобным оборудованием, стоит напомнить, что движение киноленты реализовывалось весьма специфическим неравномерным способом: быстрая протяжка на один кадр 2 сменялась остановкой для фиксации кадра на пленке (или его демонстрации). Помимо автоматически записанного изображения, зафиксировавшего некоторую натурную съемку, в “докомпьютерном” кинематографе кадры создавались и вручную — художники-мультипликаторы мастерски рисовали различные фазы движения своих героев, а затем оператор снимал их рисунки в режиме специальной покадровой съемки. Демонстрация художественных и мультипликационных фильмов происходила совершенно одинаково.

Многочисленные эксперименты показали, что иллюзия непрерывного движения возникает при смене как минимум 10–12 кадров за секунду. В любительских киноаппаратах обычно применялась скорость 16 кадров/сек., а в профессиональных — 24.

В телевидении используются совсем другие технологические принципы создания движущихся изображений, но и там речь идет о кратковременной демонстрации на экране статических кадров. Частота смены кадров составляет 25 или 30 кадров, что очевидным образом связано с частотой питающей электросети, которая принята в тех или иных странах.

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

Таким образом, мы видели, что для создания на экране дисплея изменяющегося изображения очень важно иметь механизм быстрой смены статических картинок. Посмотрим, как обстоит с этим дело в языке Java.

2. Потоки как средство организации динамической графики в Java

Что такое потоки

Очевидно, что для получения динамических изображений, которые будут воспроизводиться с одинаковым эффектом на разных компьютерах, необходимы специальные средства, “привязанные” не к скоростным характеристикам аппаратуры, а к реальному времени. Иными словами, если мы хотим реализовать с помощью языка универсальную не зависящую от характеристик компьютера картинку, мы должны в операторах этого языка использовать не тактовую частоту или аналогичные технические величины, а обычные секунды и их доли.

В Java имеется специальный механизм, позволяющий организовать функционирование программы в реальном времени, — это поток. Хочется подчеркнуть, что потоки, хотя и служат прекрасным средством для организации динамических изображений, созданы для гораздо более общих целей. Рассмотрим этот аспект немного подробнее.

Большинство читателей хорошо понимают, что такое многозадачность, ибо современные операционные системы существенным образом приспособлены к одновременному функционированию нескольких приложений: аудиопроигрыватель, менеджер закачек из Интернета, текстовой редактор, калькулятор и т.д. плюс сама операционная система и ее внешняя оболочка для манипуляции файлами. Но и внутри каждого крупного приложения можно также распределять работу между заданиями, которые выполнять параллельно, например, в редакторе печатать документ, вести в нем поиск и производить форматирование. Описанный способ исполнения заданий настолько важен, что его вполне можно увидеть стандартными средствами Windows. Щелкните правой кнопкой мыши по панели задач и в появившемся меню выберите пункт “Диспетчер задач”; в результате вы увидите картину, похожую на ту, что приведена на рис. 1.

Рис. 1. Диспетчер задач Windows

В окне Диспетчера отображаются как работающие приложения, так и их процессы (а также процессы, запущенные в результате работы самой ОС). Наибольший интерес представляет собой вкладка “Процессы”, которая отражает десятки протекающих в системе процессов 3. Мы воочию видим, насколько важную роль играет многопоточность в современном компьютере.

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

Бытует мнение (возможно, “подогретое” агрессивной рекламой многоядерных процессоров), что многопоточные программы работают быстрее. На самом деле можно смело утверждать обратное. Действительно, для переключения между задачами требуется дополнительное время, а если бы процессор, “не отвлекаясь”, выполнял задачи последовательно, то он завершил бы их быстрее 4. Видимо, преимущества надо искать в чем-то другом.

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

Рис. 2. Основные принципы функционрования потока

Следующий пример — активные приложения, где необходимо одновременно опрашивать клавиатуру и другие устройства ввода, чтобы реагировать на действия пользователя, и в то же время рассчитывать и перерисовывать изменяющееся состояние экрана.

Еще одно преимущество проистекает из того, что компьютер состоит не только из одного или нескольких процессоров. Вычислительное устройство — лишь один из ресурсов, необходимых для выполнения задач. Всегда есть оперативная память, дисковая подсистема, сетевые подключения, периферия и т.д. Здесь существенно, что все перечисленные устройства работают медленнее (а многие — существенно медленнее), чем процессор и, кроме того, имеют самостоятельные элементы управления (контроллеры). В результате задачи обмена данными требуют совсем незначительного участия центрального процессора, а значит, он легко справится с параллельным обслуживанием нескольких задач. Напротив, когда задачи в основном загружают процессор (например, математические расчеты), то их одновременное исполнение займет в лучшем случае столько же времени, что и последовательное, а то и больше.

Характерной чертой Java является то, что в ней изначально предусмотрена поддержка многопоточного программирования (multithread programming). Далеко не каждый язык может этим похвастаться, например, в Паскале или Бейсике ничего подобного просто нет.

Примечание. При переводе терминов Java на русский язык имеется некоторая специфическая проблема. В Java существует два разных термина — thread и stream, первый из которых фактически является потоком команд, а второй — потоком данных. Тем не менее оба этих термина обычно переводятся как “поток”, что может приводить к смешению этих довольно разных понятий. В данной статье речь везде идет только о потоках типа thread.

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

Новый механизм порождает и принципиально новую проблему — взаимные блокировки (deadlock), тем более что Java не имеет встроенных средств для определения такой ситуации. Суть проблемы в том, что при неудачном стечении обстоятельств может возникнуть состояние, в котором один процесс ожидает завершения другого и наоборот. Обычно эту проблему образно представляют в виде аналогии, когда два барана встретились на узком мосту и не уступают друг другу дорогу 5. Ситуация осложняется тем, что некоторые принципы, заложенные в первые реализации Java-машины, сейчас объявлены крайне неудачными, например, методы “такие, как thread.stop и thread.resume, осуждены, поскольку они с самого начала были плохой идеей и по-настоящему опасны” [1]. Еще более устрашающе выглядит оценка метода destroy “(который никогда не был реализован), который вы не должны вызывать, если можно этого избежать” [2].

Как использовать поток для динамической графики

Рассмотрим теперь, как можно использовать потоковый механизм для создания в окне аплета динамически меняющегося изображения. Обратимся к рис. 2, на котором изображены основные принципы функционирования потока. Подчеркнем, что данная схема составлена на основе систематического описания в классической книге [3]; особенности версии Java 2 будут рассмотрены по ходу изложения примеров.

Любой аплет имеет некоторый набор стандартных процедур, каждая из которых играет вполне определенную роль и вызывается Java-машиной в определенное время. Названия наиболее важных процедур (init, start и т.д.) выписаны на рис. 2; дополнительная информация о них приведена в табл. 1.

Особую роль играет процедура run, определяющая все действия, которые будет выполнять поток. Последний, согласно классическим идеям Java 1, создается при инициализации init и запускается процедурой start. В случаях, когда аплет требуется на время приостановить (например, при переходе на другую web-страницу), вызывается процедура stop. Наконец, destroy выгружает аплет из памяти и уничтожает все связанные с ним объекты, в том числе и поток.

Две оставшиеся процедуры не имеют непосредственного отношения к потокам: paint ответственна за рисование изображения на экране, а action обрабатывает воздействия пользователя клавиатурой и мышью. С точки зрения изучения принципов реализации движущихся изображений необычайно важно подчеркнуть, что метод paint всегда автоматически очищает экран перед новым рисованием, поэтому программисту достаточно описывать только процесс рисования очередного кадра, не заботясь об удалении предыдущего.

Примечание. Если окно аплета велико, а реально обновляется лишь небольшая его часть, при вызове метода перерисовки имеет смысл указывать координаты обновляемого прямоугольника — остальная (бо'льшая) часть рисунка при этом сохраняется.

Отметим, что сам аплет также представляет собой некоторый базовый поток, так что создаваемый нами для нужд мультипликации поток будет уже вторым; он является дочерним, т.е. основной поток аплета управляет им и по окончании работы обеспечивает его уничтожение.

Как уже отмечалось выше, заложенная в Java 1 идеология потоков не во всем себя оправдала, так что реальные программы для потоков не столь логичны, как описанная выше картина. Приведенные в данной публикации примеры за образец принимают официальные рекомендации фирмы Sun [4]. Кроме того, если потоки работают кратковременно и по окончании цикла работы завершаются естественным образом, то можно обойтись без дополнительных мер по их остановке.

Важную роль в организации потока для вывода меняющихся изображений играет метод sleep, который в качестве аргумента содержит время приостановки потока в миллисекундах. Это необычайно важное средство позволяет задерживать нарисованный кадр на экране и обеспечивает возможность его рассмотреть.

В языке Java предусмотрено два механизма создания потоков: реализация интерфейса Runnable (обычно у самого аплета) или расширение стандартного класса Thread с последующим созданием экземпляров нового класса.
В примерах 1–3 будет использован более простой первый метод, а в примере 4 продемонстрирован второй.

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

· Подключить интерфейс Runnable путем внесения его описания в заголовок аплета. (Как альтернативное решение, допустимо создавать класс, расширяющий Thread.)

· В методе start аплета создать поток и запустить, вызвав метод start потока.

· В методе run описать построение картинки; вызвать метод repaint, который обеспечит перерисовку изображения.

· Если цикл воспроизведения картинки бесконечен, в методе stop предусмотреть окончание работы потока: в соответствии с рекомендациями Java 2 использовать для этой цели изменение значения переменной (детали см. в примере 1).

3. Примеры динамических изображений

Перейдем теперь к рассмотрению примеров аплетов, создающих на экране динамические изображения. Они разнообразны по реализации, так что автор надеется, что после чтения статьи читатели сумеют выбрать “стартовую точку” для реализации своего собственного аплета.

Пример 1. Реализация простейшего движения

Начнем с простейшего движущегося изображения: опишем на языке Java небольшую тележку, которая перемещается в горизонтальном направлении. В качестве простейшего алгоритма поведения предлагается модельное движение “взад-вперед” между двумя граничными точками. Возможный вид аплета в некоторый фиксированный момент времени изображен на рис. 3.

Рис. 3. Реализация простейшего движения

Обратимся теперь к листингу 1, показывающему, как выглядит решение нашей задачи.

Пропустив стандартные начальные строчки (об их расшифровке мы подробно говорили в части I публикации), отметим только указание после слова implements имени используемого интерфейса Runnable.

Далее следует описание тех переменных, которые являются “связующими” для согласованной работы всех методов: поток mult, указатель на который пока фиктивный — null, и координата x тележки.

Примечание. Модификатор volatile, указанный при описании потока, “сообщает компилятору, что переменная может быть неожиданно изменена другой частью программы” [3]. Он устанавливает более строгие правила работы со значениями переменных. Употребление модификатора рекомендовано в [4].

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

Наиболее интересным является метод run, который собственно и проделывает основную работу по перемещению тележки. Рассмотрим данный метод подробнее.

Листинг 1

        import java.applet.*;
        import java.awt.*;
        public class cart extends java.applet.Applet
        implements Runnable{
        //Описания
        volatile Thread mult = null; //поток для анимации
        int x; //координата
        //============ Динамика аплета ============
        public void start()
        {mult = new Thread(this); mult.start(); //создать поток
        }
        public void run() //работа потока — анимация
        {int h = 1; //шаг по координате x
        Thread threadCopy = Thread.currentThread();
        //"запомнить" поток
        while (mult == threadCopy) //проверка отсутствия
        признака его остановки
        //изменение картинки
        {if (x == 150) h = -1; if (x == 10) h = 1;
        x = x + h;
        repaint(); //отобразить изменения в окне
        аплета
        //ожидание; в sleep() задается его время в
        мсек.
        try {mult.sleep(20);} catch(InterruptedException e) {}
        }
        }
        public synchronized void stop()
        {mult = null; //остановить работу потока
        notify();
        }
        //====== Рисование ======
        public void paint(Graphics g)
        {g.drawLine(0,45,220,45); //рисуем поверхность
        g.drawOval(x,35,10,10); //рисуем заднее колесо
        g.drawOval(x+30,35,10,10); //рисуем переднее колесо
        g.drawRect(x-5,25,50,10); //рисуем корпус
        }
     }

При входе в метод стоят две на первый взгляд очень странные строки: в переменной threadCopy создается копия указателя на наш поток, с которой он зачем-то постоянно сравнивается при каждом входе в цикл. Казалось бы, что изменится? И действительно, внутри самого метода run ничто не может нарушить истинности этого условия; тем не менее воздействовать на него способен метод stop: “испортив” переменную threadCopy, он оказывается в состоянии немедленно прекратить выполнение цикла.

Далее вычисляется новая координата тележки. Прежде всего x проверяется на совпадение с граничными значениями 10 и 150, при которых знак переменной h меняется на противоположный; это гарантирует изменение направления движения тележки. Затем вычисляется следующее значение координаты и вызывается метод repaint, который обеспечивает перерисовку тележки в новом положении. Как именно это делается, будет описано немного позднее при обсуждении метода paint.

После этого расположена необычайно важная для создания изображения строка — вызывается метод sleep(20), обеспечивающий “засыпание” (точнее говоря, прекращение работы потока) на заданное время: в нашем случае это 20 миллисекунд. Именно здесь регулируется скорость демонстрации нашего компьютерного фильма. Именно эта короткая задержка сохраняет изображение неподвижного кадра, что позволяет глазам наблюдателей его зафиксировать. Наличие конструкции try/catch обеспечивает профессиональным программистам возможность принять специальные меры при неожиданном “экстренном пробуждении” потока; для нас это скорее обуза, чем благо.

Примечание. Время 20 мсек. было выбрано экспериментально. Оно соответствует скорости демонстрации 1000/20 = 50 кадров/сек., что надежно обеспечивает плавность движения тележки.

Назначение следующего метода stop мы уже обсудили выше. Некоторые специфические приемы — модификатор synchronized и метод notify, улучшающие согласно [4] взаимодействие методов потока, здесь разбирать не будем.

Наконец, последний метод с весьма очевидным текстом — paint. Подчеркнем, что положение всех составных частей тележки обязательно указывается относительно текущего значения координаты x: именно эта мера позволяет перемещать тележку как единое целое, просто пересчитывая значение координаты.

Листинг 2

import java.applet.*;

import java.awt.*;

import java.util.*;

public class timer extends java.applet.Applet

implements Runnable{

//Описания

volatile Thread mult = null; //поток для анимации

String w; //время в виде строки

public void start()

{mult = new Thread(this); mult.start(); //создать поток

}

public void run() //работа потока — индикация

{Thread threadCopy = Thread.currentThread(); //"запомнить" поток

while (mult == threadCopy) //проверка отсутствия признака его остановки

//изменение картинки

{Date d = new Date();

String s = d.toString();

int p = s.indexOf(":")-2;

w=""; for (int i = p; i < p + 8; i++) {w = w + s.charAt(i);}

repaint(); //отобразить изменения в окне аплета

//ожидание; в sleep() задается его время в мсек.

try {mult.sleep(500);} catch(InterruptedException e) {}

}

}

public synchronized void stop()

{mult = null; //остановить работу потока

notify();

}

public void paint(Graphics g)

{g.drawString(w,120,10); //выводим время

}

}

Пример 2. Индикация времени

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

Рис. 4. Индикация времени

Текст программы аплета приведен в листинге 2.

Сравнение листингов 1 и 2 показывает, что значительная их часть совпадает. Причина очевидна — организация потока в новой программе точно такая же, как и в предыдущей.

Примечание. Если вы решите взять листинг 1 за основу и получить листинг 2 путем внесения в него изменений, не забудьте в начальной части программы добавить строку, импортирующую библиотеку java.util, в которой сосредоточены методы работы с датами и временем.

Рассмотрим теперь, как в аплете формируются показания часов, для чего разберем устройство метода run. Процесс начинается с создания переменной d типа Date. Отметим тот неочевидный факт, что конструктор Date без аргументов создает переменную, в которую заносится полная информация о текущих значениях даты и времени. Следующим шагом программа с помощью метода toString преобразует эти данные в строку. Дальнейшие манипуляции с полученной строкой требуются потому, что приходится извлекать из нее ту часть, в которой записано время.

Примечание. Казалось бы, заявленные методы getHours, getMinutes и getSeconds позволят нам непосредственно выделять данные о времени. Но, как это было у нас уже не раз, мы в очередной раз наталкиваемся на нерекомендуемые (deprecated) методы.

Итак, займемся выделением информации о времени. Для этого, применив метод indexOf(":"), найдем в строке положение символа “двоеточие”, который разделяет часы и минуты. Затем, предварительно сместившись на 2 позиции влево, скопируем из строки s 8 последовательных символов, которые содержат в себе часы, минуты и секунды, разделенные двоеточием. В результате в переменной w оказывается требуемая строка.

Остается вывести ее в окно аплета, что обеспечивает метод paint.

Пример 3. Как сделать мультфильм

Изучив предыдущие примеры, наблюдательный читатель может заметить, что в них динамическое изображение формировалось средствами языка Java. Конечно, это тоже не так уж мало, поскольку позволяет создать строящиеся в реальном времени диаграммы или графики, всевозможные прогресс-индикаторы, динамические схемы, плавный переход от одной картинки к другой, бегущую строку, появляющиеся и исчезающие указатели и многое другое. Тем не менее определенные ограничения на “оживляемые” картинки это все-таки накладывает: попробуйте, например, операторами Java нарисовать лошадь или котенка! Иными словами, хочется иметь возможность использовать картинки, созданные вне среды Java, например, в графическом редакторе. Решению этой задачи и посвящен пример 3.

Общая идея состоит в чтении графических файлов, в которых нарисованы отдельные кадры будущего мультфильма, в специальные объекты типа Image, предназначенные для “внеэкранного” хранения изображений в оперативной памяти и их последовательной демонстрации. Большую помощь при написании алгоритма может оказать “массив” из объектов типа Vector, с которым мы уже встречались в примере 4 части II.

Предположим, что кадры для создаваемого фильма подготовлены в файлах f1.giff12.gif. Попутно обратим внимание на то, что Java ориентируется на принятые в Интернете графические форматы, так что .bmp здесь не подойдет. Алгоритм создания такого 12-кадрового мультфильма записан в листинге 3.

Все имеющиеся в программе описания нам уже знакомы, кроме объекта типа Image. В него будет читаться картинка из файла, и его же планируется использовать в качестве рабочей переменной при воспроизведении очередного кадра. Сам же фильм непосредственно хранится в векторе frames, состоящем из 12 компонентов.

Перейдем к рассмотрению метода init, который обеспечивает подготовку фильма к последующей демонстрации. Процесс этот проделывается следующим образом. Для создания “внеэкранного” рисунка прежде всего необходима основа — “холст” (Canvas). На нем с помощью метода createImage создается рисунок заданного размера, в нашем примере 32 ? 32 пикселя. Далее следует цикл последовательного чтения графических файлов и сохранения прочитанных изображений в векторе frames. Обратим внимание на следующие интересные детали этого цикла. Имя очередного файла формируется характерным для Java способом — выражением "f" + i + ".gif" (см. раздел об особенностях преобразования типов в части I). В итоге формируются строки с именами файлов “f1.gif”, “f2.gif” и т.д. до 12. Как мы уже знаем, все файлы, с которыми способен работать аплет, должны находиться в том же каталоге, что и он сам. Именно поэтому для нахождения необходимого графического файла методу getImage требуется дополнительно указать путь к этому каталогу, что обеспечивает метод getDocumentBase. Прочитанная картинка сохраняется в вектор при помощи метода addElement.

Метод start не содержит особенностей, поэтому сразу перейдем к главному для потока методу run. Особенностью данного аплета является кратковременность его демонстрации, что позволяет исключить все детали, связанные с остановкой потока. Дело в том, что когда значение координаты x достигнет некоторой величины, цикл закончится и поток естественным образом завершится.

Примечание. Выбранное в условии значение 70 получено путем приблизительного деления 400 (горизонтальный размер окна аплета) на 6 (смещение объекта за один кадр — обоснование последнего значения будет дано ниже).

В данном аплете метод run минимально прост, а все детали вывода требуемых кадров помещаются в paint. Сравните это распределение с примером 2, где, напротив, основные операторы помещены в run, а в примере 1 “нагрузка” на рассматриваемые методы была примерно одинакова. Все варианты, по-видимому, одинаково хороши, поскольку, как отчетливо видно из рис. 2, метод paint фактически является продолжением run.

Остается разобраться с текстом метода paint. Как видно из текста программы, переменная x при переходе к следующему кадру изменяется на единицу. Следовательно, выражение x % 12, обозначающее остаток от деления на 12, последовательно порождает индексы от 0 до 11 (именно такие они и будут в нашем векторе frames!). После извлечения нужного кадра в переменную pict методом drawImage он рисуется в окне аплета. При этом в нашем примере объект еще и смещается, поскольку его координата 6*x каждый раз увеличивается на 6 пикселей. Значение коэффициента выбрано автором для конкретного мультфильма — катящегося по поверхности футбольного мяча; примерный расчет приводится ниже.

Листинг 3

import java.applet.*;

import java.awt.*;

import java.util.*;

public class animat extends java.applet.Applet

implements Runnable{

Thread mult = null; //поток для анимации

Vector frames = new Vector(12); //для хранения кадров

Image pict; //рабочий объект для картинки

int x = 0; //координата

public void init()

{setBackground(Color.green); //фон

//Формирование кадров мультфильма

Canvas c = new Canvas(); //"холст" для рисования

pict = c.createImage(32,32); //рисунок 32x32

for(int i = 1; i <= 12; i++)

{String fn = "f" + i + ".gif"; //генерация имен f1.gif - f12.gif

pict = getImage(getDocumentBase(),fn); //чтение кадра из файла

frames.addElement(pict); //сохранение в вектор

}

}

public void start()

{mult = new Thread(this); mult.start(); //создать поток

}

public void run() //работа потока - анимация

{while (x < 70) //проверка остановки

{repaint(); //отобразить изменения в окне аплета

//ожидание; в sleep() задается его время в мсек.

try {mult.sleep(100);} catch(InterruptedException e) {}

}

}

public void paint(Graphics g)

{g.drawLine(0,82,399,82); //поверхность

pict = (Image) frames.elementAt(x%12); //извлекаем очередной кадр (0-11)

g.drawImage(pict,6*x,50,this); //отображаем его

x++; //наращиваем x

}

}

Если не считать отдельных деталей вроде величины смещения и цвета фона, программа в листинге 3 весьма универсальна и годится для любого мультфильма. Автор при тестировании воспользовался картинками, подготовленными в свое время к одной из своих разработок [5]. Там в одном из первых демонстрационных примеров использовались картинки катящегося пятнистого мяча (пятна, как известно, позволяют легко заметить вращение). Изначально мяч был нарисован в редакторе CorelDraw и скопирован 11 раз с поворотами, кратными 360/12 = 30 градусам. В итоге получились 12 фаз (циклически повторяющегося) движения — они приведены на рис. 5.

Рис. 5. Кадры движения катящегося мяча

Теперь можно, наконец, рассчитать, на сколько пикселей в горизонтальном направлении должен сместиться мяч при переходе от одной картинки к другой (т.е. при повороте на 30 градусов): 2R/12 R/2. Для применяемых в аплете картинок получалось значение 7 пикселей, экспериментально картинка казалась оптимальной при шести.

Примечание. Чем меньше смещение, тем “плавнее” катится мяч. Но слишком маленькой эту величину тоже делать нельзя, поскольку мяч начнет “проскальзывать”: вращение будет казаться быстрее, чем горизонтальное перемещение.

На рис. 6 приведен общий вид итогового аплета. Чувствуется, что статическая картинка на бумаге много теряет по сравнению с реальным динамическим изображением!

Рис. 6. Аплет, демонстрирующий простейший мультфильм

Описанный в данном примере способ создания небольших мультфильмов, конечно, требует наличия некоторых художественных способностей, чтобы изобразить нужные фазы движения (к слову, простейшие динамические картинки учебного назначения многие способны сделать в хорошем графическом редакторе и не будучи живописцами!). Но можно поискать и готовые серии рисунков в Интернете. В частности, большой известностью пользуется нарисованный в 1989 году художником по имени Кенджи Готох (Kenji Gotoh) маленький симпатичный котенок Неко 6. Талантливо сделанные рисунки позволяют из относительно небольшого числа картинок (см. [6, 7]) получить весьма занимательные “мультики”, в которых Неко бегает, чешет за ушком и ложится спать. Как это выглядит, можно непосредственно понаблюдать на сайте [8]. Подчеркнем, что если вы умеете читать по-английски, то приведенные выше ссылки [6] и [7], представляющие собой самоучители по Java, объяснят вам некоторые дополнительные приемы анимирования изображений.

Пример 4. Многопотоковый аплет

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

В качестве демонстрационной задачи рассмотрим ход параллельного пучка лучей в собирающей линзе — рис. 7.

Из школьного курса физики известно, что, проходя через собирающую линзу, лучи пересекаются в характерной точке, которая называется фокусом. В нашем аплете параллельный пучок моделируется пятью лучами (считая проходящий вдоль оси x), но изменить это число не составляет никакого труда.

Помимо общей схемы движения лучей, которую мы планируем увидеть по завершении работы аплета, на рис. 7 указаны в пикселях экрана характерные размеры изображения.

Рис. 7. Схема, демонстрирующая ход лучей в собирающей линии

 

Несколько слов о том, откуда берется формула y = y0 * (1 – x/F), описывающая ход луча после прохождения линзы. Из математики известно, что уравнение любой не вертикальной прямой представляется в виде y = kx + b, которое справедливо в любой ее точке. Воспользуемся последним обстоятельством и запишем данное уравнение для двух характерных точек (0, y0) и (F, 0). Благодаря нулевым координатам полученная система из двух уравнений разрешается моментально; автор надеется, что среднестатистический школьник еще в состоянии это сделать.

Примечание. По мнению автора, при решении задач по информатике надо всеми силами привлекать несложные математические выкладки и расчеты (см. также вычисления времени “засыпания” потока в примере 1 и смещения катящегося мяча в примере 3). И не только потому, что классическая педагогика всегда подчеркивала важность межпредметных связей, но еще и потому, что в ходе проводимых реформ образования все чаще провозглашается невозможность постижения основ этого важного предмета учениками, выбирающими нематематические специальности. Аргументация порой доходит до курьезов. Недавно в Интернете прочел в обсуждении одной из статей, посвященных содержанию обучения, следующий пассаж. Женщина (бухгалтер по профессии!) пишет, что в школе люто ненавидела математику и до сих пор не понимает, что такое синус. Но процентную-то меру налогов она как бухгалтер не может не понимать! А синус, который есть отношение величин катета и гипотенузы, по сути мало чем отличается от начисления процентов подоходного налога, ну разве что синус не принято выражать в процентах! Конечно, это частности, но почему-то в бурно развивающейся Юго-Восточной Азии преподаванию математики и естественных наук уделяется все возрастающее внимание, а не наоборот...

Всесторонне подготовившись, можно приступать к написанию программы аплета (см. листинг 4).

Листинг 4

import java.applet.*;

import java.awt.*;

public class lens extends java.applet.Applet {

//########## class Ray - begin ##########

class Ray extends Thread{ //единичный луч

int D = 20; //задержка потока — "скорость" рисования

//размеры изображения

int x0 = -100; int f = 50; int xMax = f + 50;

//координаты луча

int y0,x;

int constY = 100; //ось X в центре: "пиксели" = constY - y

Ray(int p) {//конструктор

x = x0; y0 = p; //установка начальных координат

start(); //запуск потока

}

void drawRay(Graphics g) { //рисование луча

int xk = 0; if (x < 0) xk = x; //xk - конечные координаты линии ДО линзы

int yp0 = constY - y0; //пересчет y "в пиксели"

g.drawLine(0,yp0, xk – x0,yp0); //линия ДО линзы

if (x > 0){

//расчет конечной координаты y ПОСЛЕ линзы

double xd = x; double w = y0 * (1 - xd/f);

int yk = constY - (int)w;

g.drawLine(-x0,yp0, x - x0, yk); //линия ПОСЛЕ линзы

}

}

public void run() { //работа потока

while (x < xMax) { //пока не выходим за рисунок

x++; repaint();

try {Thread.sleep(D);} //пауза

catch (InterruptedException e) { }

}

}

} //########## class Ray - end ##########

Ray r1,r2,r3,r4,r5; //5 лучей

public void start() {

r1 = new Ray(80); r2 = new Ray(40); r3 = new Ray(0);

r4 = new Ray(-40); r5 = new Ray(-80);

}

public void paint(Graphics g) { //Рисование

g.drawLine(100, 0, 100,200); //линза

g.drawString("F",147,120);

//рисуем лучи

r1.drawRay(g); r2.drawRay(g); r3.drawRay(g);

r4.drawRay(g); r5.drawRay(g);

}

}

Особенностью данного аплета является то, что потоки создаются не как реализация у аплета интерфейса Runnable (см. примеры 1–3), а как расширение стандартного класса Thread.

Основой работы аплета является класс Ray, описывающий рисование отдельного луча. Пять экземпляров этого класса и будут одновременно демонстрировать в окне аплета ход параллельного пучка. Обратите внимание на положение описания класса Ray — последний помещен внутри аплета (класс lens), т.е. он является его своеобразным внутренним классом. Не случайно поэтому файл, содержащий байт-код этого класса, автоматически получает имя lens$Ray.

Примечание. Рекомендуем сравнить сказанное выше с расположением описаний классов в примере 3 в части II, где мы также строили иерархию классов, — там все классы размещаются автономно.

Класс Ray содержит в себе целый ряд полей (x0, f, xMax, constY), характеризующих параметры рисунка. Кроме того, величина y0 хранит в себе начальную координату луча, которая однозначно определяет весь его ход, а x — его текущую координату.

Далее записан текст конструктора, задающего начальные координаты луча и запускающего поток, который данный луч будет рисовать. Непосредственно заниматься рисованием будет следующая далее процедура под названием drawRay. Она делит луч на две половинки — до линзы и после нее. Первая описывает горизонтальную прямую, а вторая использует уравнение, выписанное на рис. 7, причем вторая часть линии рисуется только для положительных x.

Примечание. Важно подчеркнуть, что хотя все величины, входящие в формулу y0 * (1 – x/F), являются целыми, наличие деления делает вычисления в классе целых чисел некорректными. Бороться с этим можно единственным, хотя и несколько специфическим способом: надо с самого начала дать понять Java-машине, что вычисления ведутся с вещественными числами. Для этого переменную xd, с которой начинается вычисление, следует объявить как double. В результате расчета получается вещественное число w, которое затем преобразуется обратно к целочисленному значению и присваивается результирующей координате yk типа int. Если вы не верите, что такой тонкий подход к вычислениям важен, проведите эксперимент — исключите из программы все вещественные переменные. Не пожалейте времени, ибо, надеюсь, картина поведения лучей, которую вы увидите, вас удивит. А объяснение ее заключается в том, что в рамках целых чисел отношение x/f сначала для всех значений x < 50 будет давать 0 (луч будет продолжать параллельное движение), для 50 x < 100 будет получаться 1 (картина скачком изменится), и, наконец, в конечной точке x = 100 примет значение 2 (рисунок вновь перестроится), и в итоге только окончательный результат и окажется правильным!

Метод run несложен и после разбора примеров 1–3 труда не составит. Обратите только внимание на то обстоятельство, что при выходе x за пределы окна аплета цикл прекращается и поток завершается.

Мы разобрали внутреннее устройство класса Ray. Посмотрим теперь, как он используется в аплете.

Как следует далее из листинга 4, создается пять экземпляров лучей r1-r5 с начальными координатами y = 80, 40, 0, –40 и –80. В методе paint все они рисуются путем вызова метода drawRay для каждой из переменных.

Подчеркнем, что каждый из лучей r1-r5 является отдельным потоком, причем все они работают параллельно, т.е. мы видим одновременно рисуемые лучи! Результирующая картинка, которая остается после окончания работы аплета, изображена на рис. 8.

Описанный пример весьма красив и вполне достоин завершить наше предварительное знакомство с языком Java. И если хотя бы некоторая часть читателей заинтересовалась им и напишет какие-либо свои аплеты, тем более с учениками, автор будет считать, что время, потраченное на подготовку и написание данной публикации, было потрачено не напрасно.

Рис. 8. Многопотоковый аплет

Ссылки

1. Харольд Э.Р. 10 причин, по которым нам нужен Java 3. http://www.javaportal.ru/articles/10_reasons_Java3.html.

2. Bruce Eckel. Thinking in Java, 2nd Ed. Русский перевод доступен по адресу http://www.uic.rsu.ru/doc/programming/java/TIJ2e.ru/

3. Нотон П., Шилдт Г. Полный справочник по Java. Киев: Диалектика, 1997, 592 с.

4. Java Thread Primitive Deprecation. http://java.sun.com/j2se/1.4.2/docs/guide/misc/threadPrimitiveDeprecation.html.

5. Еремин Е.А. Программное обеспечение для изучения объектного подхода в курсе информатики. // Сборник трудов конференции “Информационные технологии в образовании”. М., 1999. Ч. 2, с. 41–43.

6. Jones P.E. Section 4.11 — Neko example. http://www.csse.uwa.edu.au/~peterj/javalin/tut4-l9.html.

7. Lemay L., Perkins C.L., Morrison M. Teach Yourself Java in 21 Days. http://docs.rinet.ru/J21/ch11.htm (figure 11.3 — рисунки с фазами движений).

8. It's Neko! (3.0). http://webneko.net/


1 В науке возможность наложения явлений принято называть суперпозицией.

2 В момент протяжки для уменьшения негативных визуальных эффектов объектив перекрывался специальной заслонкой.

3 Неудивительно, что если в такой список добавится пара-тройка вирусов, это пройдет незамеченным.

4 Над этим мало кто задумывается, но серия из 100 операторов a[1] := 0; a[2] := 0; …
a[100] := 0; выполнится быстрее, чем стандартный цикл для a[i] := 0.

5 Более современный вариант — встреча громоздких иномарок в узком переулке.

6 Неко по-японски и есть котенок.

Е.. А.. Еремин,
г. Пермь

TopList