Играем в сапера в фотошопе



    По роду своей деятельности мне периодически приходится автоматизировать свою работу в фотошопе. Точнее я мог бы этого не делать, но природная лень не оставляет шансов в борьбе с рутиной, как говорится «лучше час потерять, зато потом за 5 минут долететь». Все бы наверное так и оставалось на уровне отдельных разрозненных скриптов если бы не пост от enotus. Благодаря ему я узнал, что к фотошопу (как впрочем и другим продуктам от Adobe) можно писать расширения на HTML+JS. И пошло, поехало.

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

    Как уже описано в статье моего предшественника, нам понадобится Brackets с установленным расширением ну и, собственно, сам Photoshop.

    Также рекомендую сразу запастись документацией со страницы Adobe, наиболее вероятно, что потребуется Photoshop CC JavaScript Reference и, возможно, Photoshop CC Scripting Guide. Они не имеют прямого отношения к написанию расширений, но потребуются для взаимодействия с фотошопом.

    Приступаем. Создадим проект в Brackets при помощи расширения:



    У нас появится папка с полным набором файлов для работы расширения:


    В папку CSS я закинул дополнительно файл для светлой темы topcoat т.к. предпочитаю работать со светлым интерфейсом, но это не обязательно. Кстати, о Topcoat: скачать и почитать документацию можно тут.

    Manifest можно и не трогать, для последней версии фотошопа там все настроено. Если потребуется изменить иконку расширения или разрешить работу в фотошопе более ранней версии, уточните эти моменты в документации от производителя.

    В папке js нас интересует main.js, собственно, основной файл нашего проекта. Но прежде чем переходить к нему нам нужно создать интерфейс нашего расширения. Это делается с помощью HTML в файле index.html.

    Остался еще один важный файл, о котором я не упомянул: hostscript.jsx. JSX скрипт это как раз то, что использовали уже довольно давно для автоматизации различных процессов в фотошопе и других проектах от Adobe. У них даже есть специальная утилитка ExtendScript Toolkit, она тоже может пригодится, например, для отладки скриптов jsx.

    index.html


    В этом файле тоже уже все подготовлено, подключена темная тема topcoat, добавлены все необходимые скрипты. Собственно, все что требуется — это заполнить div#content:

        <div id="content">         
            <div>
                Поле:<br>
                <input type="text" id="w" class="topcoat-text-input" placeholder="width" value="6"> x <input type="text" id="h" class="topcoat-text-input" placeholder="height" value="6"><br>
                Количество мин:<br>
                <input type="text" id="m" class="topcoat-text-input" placeholder="width" value="8"><br>
                <button id="btn_start" class="topcoat-button--large hostFontSize">New game</button>
            </div>
            <hr>
            <div>            
                <h2>Осталось открыть: <span id="res">-</span></h2>
                <h3><span id="timer">0</span> sec.</h3>
                <button id="btn_check" class="topcoat-button--large hostFontSize" disabled>Check cell</button>               
            </div>   
            <div>            
                <button id="btn_mark" class="topcoat-button--large hostFontSize" disabled>Mark</button> <button id="btn_unmark" class="topcoat-button--large hostFontSize" disabled>UnMark</button>    
            </div>         
        </div>
    


    В результате мы получим следующую картину:



    Можно переходить к JS.

    main.js


    По сути, тут уже есть пример самого простого использования:

    /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
    /*global $, window, location, CSInterface, SystemPath, themeManager*/
    
    (function () {
        'use strict';
        var csInterface = new CSInterface();
            
        function init() {                
            themeManager.init();                
            $("#btn_test").click(function () {
                csInterface.evalScript('sayHello()');
            });
        }
            
        init();
    }());
    

    Самая главная тут — это строчка csInterface.evalScript('sayHello()');, именно таким образом происходит взаимодействие с приложением. Дело в том, что механизм расширений универсальный и не зависит от приложения, в котором выполняется, но по этой же причине ни чего не знает от функционале — ему все равно фотошоп это иллюстратор. Именно для этого и используются скрипты JSX. А sayHello() лишь один из методов реализованных в hostscript.jsx.

    Но, нам же нужно не только передавать данные в jsx, но и получать результаты. Поэтому можно использовать callBack функцию, в том числе анонимную:

    csInterface.evalScript('checkCell()', function(result){
                    if(result<=0)
                    {
                        stopGame();
                    }
                    else
                    {
                        $("#res").html(result); //вывод количества закрытых клеток без мин
                    }
                });
    

    Вот примерно так я проверяю клетки на мины: result содержит количество клеток, которые еще нужно открыть для победы. Мина соответствует -1. Если вы обратили внимание, среди скриптов сразу подключен jQuery. Так что взаимодействовать с интерфейсом проще простого.

    Единственный момент, требующий отдельного упоминания в рамках main.js — это реакция на события, происходящие в приложении за пределами нашего расширения. С одной стороны, тут все просто, с другой хуже, чем хотелось бы. Первым делом с попытался поймать клик мышкой в документе — не тут то было, такого события просто нет. Как выяснилось позже, события относятся, скорее, ко всему приложению, а не к документу и имеют характер «использован фильтр», «отменено последнее действие» и тому подобное. Помимо этого у всех событий есть 4-х буквенный идентификатор и длинный цифровой и в нашем случае нужен именно длинный цифровой, а в документации указаны лишь короткие коды. Благо у каждого приложения есть метод конвертации одного идентификатор в другой, но он работает в рамках jsx-скрипта.

    В общем я не буду сейчас вас грузить деталями, воспользуемся примером из документации:

        function registerPhotoshopEvent(in_eventId) {
            var event = new CSEvent("com.adobe.PhotoshopRegisterEvent", "APPLICATION");
            event.extensionId = csInterface.getExtensionID();
            event.appId = csInterface.getApplicationID();
            event.data = in_eventId
            csInterface.dispatchEvent(event);
        }
    
        csInterface.addEventListener("PhotoshopCallback" , function(event) {
            csInterface.evalScript("return (app.documents.length > 0)", stopGame)
        });
    
        var closeEventid = "1131180832"; //идентификатор события закрытия документа
        registerPhotoshopEvent(closeEventid);
    


    hostscript.jsx


    Как я уже писал ранее, это основной способ взаимодействия с приложением, в данном случае с Photoshop CC. Вот тут то и пригодится скаченная в самом начале Photoshop CC JavaScript Reference. Там содержится описание свойств и методов различных объектов фотошопа.

    Я приведу здесь лишь пару примеров. Начнем с создания документа:

    var startRulerUnits = app.preferences.rulerUnits; // сохраняем текущие единицы измерения 
    preferences.rulerUnits = Units.PIXELS; //Заменяем единицы измерения на пикселы
        
    //создаем документ
    mainDoc = app.documents.add(width,  height, dpi, "Game", NewDocumentMode.RGB);
    

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

    Обращаю внимание, что есть исключения. Например, выделенная область всегда задается и измеряется только в PIXELS. А например все векторные объекты меряются исключительно в POINTS.

    Для конвертации единиц измерения я использовал небольшой метод подсмотренный где-то на просторах интернета:

    function convertValue(value, from, to)
    {
        output = new UnitValue(value, from);    
        return output.as(to);
    }
    

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

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

    function DrawBomb(layer, x, y, colorHEX)
    {
        var startRulerUnits = app.preferences.rulerUnits; // сохраняем единицы измерения 
        preferences.rulerUnits = Units.PIXELS; //Заменяем единицы измерения на пикселы
        
        mainDoc.activeLayer = layer;
        mainDoc.selection.select([
            [(x+0.2)*cellSize+padding, (y+0.2)*cellSize+padding],
            [(x+0.8)*cellSize+padding, (y+0.2)*cellSize+padding],
            [(x+0.8)*cellSize+padding, (y+0.8)*cellSize+padding],
            [(x+0.2)*cellSize+padding, (y+0.8)*cellSize+padding],
            [(x+0.2)*cellSize+padding, (y+0.2)*cellSize+padding]
        ]);
        var color = new SolidColor;
        color.rgb.hexValue = colorHEX;
        mainDoc.selection.fill(color);
        mainDoc.selection.deselect();    
        preferences.rulerUnits = startRulerUnits; //восстанавливаем единицы измерения
    }
    


    Можно и поиграть


    Ну вот, интерфейс нарисован, обработчики в js написаны, логика в jsx реализована, можно и поиграть.
    Не забываем включить Debug Mode, чтобы мы увидели свое не подписанное расширение в фотошопе.



    Теперь можно запускать Photoshop и активировать наше расширение:



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



    Ну и видео процесса:



    Напоследок скажу пару слов про подписывание ваших расширений.
    Не забываейте удалять все скрытые файлы перед подписыванием.
    В Windows путь к папке с расширением нужно начинать с точки, примерно так:
    ZXPSignCmd.exe -sign ./ru.mobak.habrasample ru.mobak.habrasample.zxp cert.p12 pass

    И прошу, не судите строго мой код, все таки я не программист.

    Файлы


    Исходники расширения можно скачать тут.

    Готовое расширение которое можно установить через Adobe Extension Manager лежит тут. Подписано самодельным сертификатом, так что матюкается маленько.

    P.S. Вспомнил, что забыл написать про один отличный ресурс, правда на английском, но с кучей примеров: http://www.davidebarranca.com/

    P.P.S. Еще один момент, который хотел сказать. Раньше, чтобы обновить расширение, нужно было закрывать и перезапускать фотошоп. Но прогресс не стоит на месте, сейчас достаточно свернуть и развернуть панель расширения. Таким образом можно смотреть результат буквально хоть после каждого изменения.
    Поделиться публикацией

    Похожие публикации

    Комментарии 15

      +4
      Чего только не придумают, лишь бы не работать =).
      Шутка, сам в 2048 рублю в шопе…
      0
      Очень круто!
      +3
      Всем огромное спасибо за оценку моей работы, очень приятно.
      Рад, что тема заинтересовала.
      Есть ли потребность в более подробном описании взаимодействия с фотошопом? Еще могу рассказать как писал подобные штуки в CorelDraw.
        –2
        *Картинка с буханкой хлеба и троллейбусом*

        Хотя на самом деле классно и интересно :)
          +1
          Спасибо, посмеялся. Не знал этого мема.
          На самом деле есть гораздо более практические задачи. Перед этим баловством был создан вполне себе серьезный проект по интеграции чертежей автокада и макетов фотошопа. Задачи которые требовали примерно 2-3 часа работы дизайнера решаются сейчас скриптом за 2-3 минуты.
            0
            Да, я прекрасно понимаю что скриптование может дать очень и очень значительные удобства, под буханкой и троллейбусом я имел ввиду именно игры :)
              +2
              Ну да. Хотелось придумать что-то бесполезное, но интересное. При этом не писать слишком долго.
          0
          Следующий шаг — мультиплеер!
            0
            Для мультиплеера есть идея Морской бой написать :-)
            0
            Дожили. Вы теперь в минёре будете фотки править?
              0
              Ну может и не фотки, но пиксель-арт точно получится :)

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое