Создание плагинов

Paper.js предоставляет гибкую систему расширения за счёт прототипного наследования и динамического добавления функциональности. Плагины в контексте Paper.js — это наборы методов, свойств или модификаций, расширяющих стандартные классы библиотеки: Path, Group, Item, View, Project и другие.

Расширение может происходить на нескольких уровнях:

  • добавление методов в прототипы классов
  • создание обёрток над существующими объектами
  • внедрение пользовательских утилит
  • интеграция с внешними библиотеками

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


Расширение прототипов

Paper.js построен на прототипной системе JavaScript, что позволяет напрямую добавлять методы в классы.

Добавление метода в Path

paper.Path.prototype.toCenter = function () {
    this.position = paper.view.center;
    return this;
};

Теперь любой объект Path получает новый метод:

var circle = new paper.Path.Circle({
    center: [50, 50],
    radius: 20
});

circle.toCenter();

Расширение Item

Все графические элементы наследуются от Item, поэтому добавление методов сюда делает их доступными для всех типов объектов:

paper.Item.prototype.fadeOut = function (duration = 60) {
    let frame = 0;
    let item = this;

    function animate() {
        if (frame < duration) {
            item.opacity -= 1 / duration;
            frame++;
            requestAnimationFrame(animate);
        }
    }

    animate();
};

Создание пространства имён плагина

Чтобы избежать конфликтов, плагины оформляются через собственное пространство имён.

var MyPlugin = {};

Или с привязкой к Paper.js:

paper.MyPlugin = {};

Пример:

paper.MyPlugin.randomColor = function () {
    return new paper.Color(Math.random(), Math.random(), Math.random());
};

Инкапсуляция логики

Хороший плагин не просто добавляет функции, а структурирует поведение.

Пример: генератор фигур

paper.MyPlugin.ShapeFactory = {
    createStar: function (center, points, radius1, radius2) {
        return new paper.Path.Star(center, points, radius1, radius2);
    },

    createCircle: function (center, radius) {
        return new paper.Path.Circle(center, radius);
    }
};

Плагины как модули

Современный подход предполагает использование модульной структуры.

ES6 модуль

export function addPathExtensions(paper) {
    paper.Path.prototype.doubleScale = function () {
        this.scale(2);
        return this;
    };
}

Импорт:

import { addPathExtensions } from './plugin.js';

addPathExtensions(paper);

Работа с событиями

Плагины могут вмешиваться в систему событий Paper.js.

Расширение обработки кликов

paper.Item.prototype.onClickHighli ght = function () {
    this.onCl ick = function () {
        this.fillColor = 'red';
    };
};

Модификация поведения View

View отвечает за отрисовку и взаимодействие с canvas.

paper.View.prototype.enableGrid = function (size = 20) {
    var grid = new paper.Group();

    for (let x = 0; x < this.size.width; x += size) {
        let line = new paper.Path.Line({
            from: [x, 0],
            to: [x, this.size.height],
            strokeColor: '#eee'
        });
        grid.addChild(line);
    }

    for (let y = 0; y < this.size.height; y += size) {
        let line = new paper.Path.Line({
            from: [0, y],
            to: [this.size.width, y],
            strokeColor: '#eee'
        });
        grid.addChild(line);
    }

    return grid;
};

Добавление утилит для работы с геометрией

Плагины часто расширяют математические возможности.

paper.Point.prototype.distanceTo = function (point) {
    return this.getDistance(point);
};

Или более сложный пример:

paper.Path.prototype.getBoundingCircle = function () {
    var bounds = this.bounds;
    var center = bounds.center;
    var radius = Math.max(bounds.width, bounds.height) / 2;

    return new paper.Path.Circle(center, radius);
};

Создание цепочек методов (Fluent API)

Удобство использования плагинов повышается за счёт возврата this.

paper.Path.prototype.setRed = function () {
    this.fillColor = 'red';
    return this;
};

paper.Path.prototype.moveRight = function (value) {
    this.position.x += value;
    return this;
};

Использование:

circle.setRed().moveRight(50).scale(1.5);

Работа с проектами (Project)

Project управляет сценой.

paper.Project.prototype.clearAll = function () {
    this.activeLayer.removeChildren();
};

Переопределение существующих методов

Иногда требуется изменить стандартное поведение.

Пример: логирование создания объектов

const originalCircle = paper.Path.Circle;

paper.Path.Circle = function (...args) {
    console.log('Создан круг:', args);
    return new originalCircle(...args);
};

Важно сохранять оригинальные методы, чтобы не ломать библиотеку.


Интеграция с внешними библиотеками

Плагин может выступать адаптером между Paper.js и сторонними инструментами.

Пример: интеграция с анимационной библиотекой

paper.Item.prototype.animatePosition = function (to, duration = 1000) {
    let start = this.position.clone();
    let startTime = Date.now();
    let item = this;

    function animate() {
        let elapsed = Date.now() - startTime;
        let progress = Math.min(elapsed / duration, 1);

        item.position = start.add(to.subtract(start).multiply(progress));

        if (progress < 1) {
            requestAnimationFrame(animate);
        }
    }

    animate();
};

Структура полноценного плагина

Хорошо организованный плагин включает:

  • инициализацию
  • расширения прототипов
  • утилиты
  • конфигурацию
var MyPlugin = (function () {

    function init(paper) {
        extendPath(paper);
        extendItem(paper);
    }

    function extendPath(paper) {
        paper.Path.prototype.blink = function () {
            let visible = true;
            let item = this;

            setInterval(() => {
                item.visible = visible = !visible;
            }, 300);
        };
    }

    function extendItem(paper) {
        paper.Item.prototype.centerIt = function () {
            this.position = paper.view.center;
        };
    }

    return {
        init: init
    };

})();

Использование:

MyPlugin.init(paper);

Защита от конфликтов

При разработке плагинов важно учитывать:

  • проверку существования методов
  • нейминг (уникальные имена)
  • минимальное вмешательство в ядро
if (!paper.Path.prototype.rotateAround) {
    paper.Path.prototype.rotateAround = function (point, angle) {
        this.rotate(angle, point);
    };
}

Документирование плагинов

Каждое расширение должно сопровождаться описанием:

  • назначения метода
  • входных параметров
  • возвращаемых значений
  • примеров использования
/**
 * Перемещает объект в центр холста
 * @returns {Item}
 */
paper.Item.prototype.toCenter = function () {
    this.position = paper.view.center;
    return this;
};

Тестирование

Проверка плагинов включает:

  • корректность работы с разными типами объектов
  • совместимость с обновлениями Paper.js
  • отсутствие утечек памяти
  • корректную работу анимаций

Распространение плагинов

Плагины могут распространяться:

  • как отдельные JS-файлы
  • через npm-пакеты
  • как часть larger framework

Пример структуры npm-пакета:

paperjs-plugin/
├── index.js
├── package.json
├── README.md

Производительность

При разработке плагинов необходимо учитывать:

  • минимизацию операций в циклах
  • использование кэширования
  • избегание лишних пересчётов геометрии

Плохая практика:

for (let i = 0; i < 1000; i++) {
    path.bounds; // дорого
}

Хорошая практика:

let bounds = path.bounds;
for (let i = 0; i < 1000; i++) {
    bounds;
}

Расширение через композицию

Вместо изменения прототипов можно использовать композицию:

function FancyPath(path) {
    this.path = path;
}

FancyPath.prototype.highlight = function () {
    this.path.strokeColor = 'yellow';
};

Создание цепочек обработки

Плагины могут формировать pipeline обработки объектов:

paper.Path.prototype.process = function (...steps) {
    steps.forEach(step => step(this));
    return this;
};

Использование:

circle.process(
    p => p.scale(2),
    p => p.rotate(45),
    p => p.fillColor = 'blue'
);

Расширение системы слоёв

paper.Layer.prototype.lockAll = function () {
    this.children.forEach(child => child.locked = true);
};

Управление состоянием

Плагин может хранить состояние:

paper.MyPlugin.state = {
    selectedItems: []
};

Ленивые вычисления

paper.Path.prototype.getCachedLength = function () {
    if (!this._cachedLength) {
        this._cachedLength = this.length;
    }
    return this._cachedLength;
};

Декораторы поведения

function withShadow(item) {
    item.shadowColor = new paper.Color(0, 0, 0, 0.5);
    item.shadowBlur = 10;
    return item;
}

Переиспользуемые пресеты

paper.MyPlugin.presets = {
    button: {
        fillColor: '#3498db',
        radius: 8
    }
};

Применение:

Object.assign(path, paper.MyPlugin.presets.button);