summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRebecca Worledge <rebecca.worledge@qt.io>2020-02-14 20:13:34 -0800
committerRebecca Worledge <rebecca.worledge@qt.io>2020-02-14 20:29:30 -0800
commit03f115744229f841d42c37b1a699e9b8da7873fc (patch)
tree08af6c0c1638e663d070b7ab7e7c562a6904faa7 /src
parent5074794ebd6c1b062500e0cb05464860906625f6 (diff)
Add Image support to Qt Lottie
Adds image parsing, loading, and rendering to Qt Lottie as per the Bodymovin spec. Change-Id: I71950227155b9fbe030fabcfc9c4ae5b8fc1fb6a Reviewed-by: Rebecca Worledge <rebecca.worledge@theqtcompany.com>
Diffstat (limited to 'src')
-rw-r--r--src/bodymovin/bmimage.cpp120
-rw-r--r--src/bodymovin/bmimage_p.h84
-rw-r--r--src/bodymovin/bmimagelayer.cpp158
-rw-r--r--src/bodymovin/bmimagelayer_p.h77
-rw-r--r--src/bodymovin/bmlayer.cpp5
-rw-r--r--src/bodymovin/bodymovin.pro4
-rw-r--r--src/bodymovin/lottierenderer_p.h2
-rw-r--r--src/imports/lottieanimation.cpp3
-rw-r--r--src/imports/rasterrenderer/batchrenderer.cpp22
-rw-r--r--src/imports/rasterrenderer/lottierasterrenderer.cpp17
-rw-r--r--src/imports/rasterrenderer/lottierasterrenderer.h1
11 files changed, 490 insertions, 3 deletions
diff --git a/src/bodymovin/bmimage.cpp b/src/bodymovin/bmimage.cpp
new file mode 100644
index 0000000..8035edf
--- /dev/null
+++ b/src/bodymovin/bmimage.cpp
@@ -0,0 +1,120 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmimage_p.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QJsonObject>
+
+#include "bmtrimpath_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMImage::BMImage(const BMImage &other)
+ : BMBase(other)
+{
+ m_position = other.m_position;
+ m_radius = other.m_radius;
+ m_image = other.m_image;
+}
+
+BMImage::BMImage(const QJsonObject &definition, BMBase *parent)
+{
+ setParent(parent);
+ construct(definition);
+}
+
+BMBase *BMImage::clone() const
+{
+ return new BMImage(*this);
+}
+
+void BMImage::construct(const QJsonObject &definition)
+{
+ BMBase::parse(definition);
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMImage::construct():" << m_name;
+
+ QJsonObject asset = definition.value(QLatin1String("asset")).toObject();
+ QString assetString = asset.value(QLatin1String("p")).toString();
+
+ if (assetString.startsWith(QLatin1String("data:image"))) {
+ QStringList assetsDataStringList = assetString.split(QLatin1String(","));
+ if (assetsDataStringList.length() > 1) {
+ QByteArray assetData = QByteArray::fromBase64(assetsDataStringList[1].toLatin1());
+ m_image.loadFromData(assetData);
+ }
+ }
+ else {
+ QFileInfo info(asset.value(QLatin1String("fileSource")).toString());
+ QString url = info.path() + QDir::separator() + asset.value(QLatin1String("u")).toString() + assetString;
+ QString path = QUrl(url).toLocalFile();
+ m_image.load(path);
+ if (m_image.isNull()) {
+ qWarning() << "Unable to load file " << path;
+ }
+ }
+
+ QJsonObject position = definition.value(QLatin1String("p")).toObject();
+ position = resolveExpression(position);
+ m_position.construct(position);
+
+ QJsonObject radius = definition.value(QLatin1String("r")).toObject();
+ radius = resolveExpression(radius);
+ m_radius.construct(radius);
+}
+
+void BMImage::updateProperties(int frame)
+{
+ m_position.update(frame);
+ m_radius.update(frame);
+
+ m_center = QPointF(m_position.value().x() - m_radius.value() / 2,
+ m_position.value().y() - m_radius.value() / 2);
+}
+
+void BMImage::render(LottieRenderer &renderer) const
+{
+ renderer.render(*this);
+}
+
+QPointF BMImage::position() const
+{
+ return m_position.value();
+}
+
+qreal BMImage::radius() const
+{
+ return m_radius.value();
+}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmimage_p.h b/src/bodymovin/bmimage_p.h
new file mode 100644
index 0000000..2b988df
--- /dev/null
+++ b/src/bodymovin/bmimage_p.h
@@ -0,0 +1,84 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMIMAGE_P_H
+#define BMIMAGE_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QImage>
+#include <QPointF>
+
+#include <QtBodymovin/private/bmproperty_p.h>
+#include <QtBodymovin/private/bmspatialproperty_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class BODYMOVIN_EXPORT BMImage : public BMBase
+{
+public:
+ BMImage() = default;
+ explicit BMImage(const BMImage &other);
+ BMImage(const QJsonObject &definition, BMBase *parent = nullptr);
+
+ BMBase *clone() const override;
+
+ void construct(const QJsonObject &definition);
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &renderer) const override;
+
+ QPointF position() const;
+ qreal radius() const;
+
+ QPointF getCenter() const { return m_center; }
+ QImage getImage() const { return m_image; }
+
+protected:
+ BMSpatialProperty m_position;
+ BMProperty<qreal> m_radius;
+
+ QImage m_image;
+ QPointF m_center;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMIMAGE_P_H
diff --git a/src/bodymovin/bmimagelayer.cpp b/src/bodymovin/bmimagelayer.cpp
new file mode 100644
index 0000000..f228d1c
--- /dev/null
+++ b/src/bodymovin/bmimagelayer.cpp
@@ -0,0 +1,158 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "bmimagelayer_p.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+
+
+#include "bmbase_p.h"
+#include "bmimage_p.h"
+#include "bmshape_p.h"
+#include "bmtrimpath_p.h"
+#include "bmbasictransform_p.h"
+#include "lottierenderer_p.h"
+
+QT_BEGIN_NAMESPACE
+
+BMImageLayer::BMImageLayer(const BMImageLayer &other)
+ : BMLayer(other)
+{
+ m_maskProperties = other.m_maskProperties;
+ m_layerTransform = new BMBasicTransform(*other.m_layerTransform);
+ m_appliedTrim = other.m_appliedTrim;
+}
+
+BMImageLayer::BMImageLayer(const QJsonObject &definition)
+{
+ m_type = BM_LAYER_IMAGE_IX;
+
+ BMLayer::parse(definition);
+
+ BMImage *image = new BMImage(definition, this);
+ appendChild(image);
+
+ if (m_hidden)
+ return;
+
+ qCDebug(lcLottieQtBodymovinParser) << "BMImageLayer::BMImageLayer()"
+ << m_name;
+
+ QJsonArray maskProps = definition.value(QLatin1String("maskProperties")).toArray();
+ QJsonArray::const_iterator propIt = maskProps.constBegin();
+ while (propIt != maskProps.constEnd()) {
+ m_maskProperties.append((*propIt).toVariant().toInt());
+ ++propIt;
+ }
+
+ QJsonObject trans = definition.value(QLatin1String("ks")).toObject();
+ m_layerTransform = new BMBasicTransform(trans, this);
+
+ QJsonArray items = definition.value(QLatin1String("shapes")).toArray();
+ QJsonArray::const_iterator itemIt = items.constEnd();
+ while (itemIt != items.constBegin()) {
+ itemIt--;
+ BMShape *shape = BMShape::construct((*itemIt).toObject(), this);
+ if (shape)
+ appendChild(shape);
+ }
+
+ if (m_maskProperties.length())
+ qCWarning(lcLottieQtBodymovinParser)
+ << "BM Image Layer: mask properties found, but not supported"
+ << m_maskProperties;
+}
+
+BMImageLayer::~BMImageLayer()
+{
+ if (m_layerTransform)
+ delete m_layerTransform;
+}
+
+BMBase *BMImageLayer::clone() const
+{
+ return new BMImageLayer(*this);
+}
+
+void BMImageLayer::updateProperties(int frame)
+{
+ BMLayer::updateProperties(frame);
+
+ m_layerTransform->updateProperties(frame);
+
+ for (BMBase *child : children()) {
+ if (child->hidden())
+ continue;
+
+ BMShape *shape = dynamic_cast<BMShape*>(child);
+
+ if (!shape)
+ continue;
+
+ if (shape->type() == BM_SHAPE_TRIM_IX) {
+ BMTrimPath *trim = static_cast<BMTrimPath*>(shape);
+ if (m_appliedTrim)
+ m_appliedTrim->applyTrim(*trim);
+ else
+ m_appliedTrim = trim;
+ } else if (m_appliedTrim) {
+ if (shape->acceptsTrim())
+ shape->applyTrim(*m_appliedTrim);
+ }
+ }
+}
+
+void BMImageLayer::render(LottieRenderer &renderer) const
+{
+ renderer.saveState();
+
+ renderEffects(renderer);
+
+ // In case there is a linked layer, apply its transform first
+ // as it affects transforms of this layer too
+ if (BMLayer *ll = linkedLayer())
+ renderer.render(*ll->transform());
+
+ renderer.render(*this);
+
+ m_layerTransform->render(renderer);
+
+ for (BMBase *child : children()) {
+ if (child->hidden())
+ continue;
+ child->render(renderer);
+ }
+
+ if (m_appliedTrim && !m_appliedTrim->hidden())
+ m_appliedTrim->render(renderer);
+
+ renderer.restoreState();}
+
+QT_END_NAMESPACE
diff --git a/src/bodymovin/bmimagelayer_p.h b/src/bodymovin/bmimagelayer_p.h
new file mode 100644
index 0000000..f7a4fd5
--- /dev/null
+++ b/src/bodymovin/bmimagelayer_p.h
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the lottie-qt module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 or (at your option) any later version
+** approved by the KDE Free Qt Foundation. The licenses are as published by
+** the Free Software Foundation and appearing in the file LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BMIMAGELAYER_P_H
+#define BMIMAGELAYER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtBodymovin/private/bmlayer_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QJsonObject;
+
+class LottieRenderer;
+class BMShape;
+class BMTrimPath;
+class BMBasicTransform;
+
+class BODYMOVIN_EXPORT BMImageLayer : public BMLayer
+{
+public:
+ BMImageLayer() = default;
+ explicit BMImageLayer(const BMImageLayer &other);
+ BMImageLayer(const QJsonObject &definition);
+ ~BMImageLayer() override;
+
+ BMBase *clone() const override;
+
+ void updateProperties(int frame) override;
+ void render(LottieRenderer &render) const override;
+
+protected:
+ QList<int> m_maskProperties;
+
+private:
+ BMTrimPath *m_appliedTrim = nullptr;
+};
+
+QT_END_NAMESPACE
+
+#endif // BMIMAGELAYER_P_H
diff --git a/src/bodymovin/bmlayer.cpp b/src/bodymovin/bmlayer.cpp
index 57a88ff..12878b0 100644
--- a/src/bodymovin/bmlayer.cpp
+++ b/src/bodymovin/bmlayer.cpp
@@ -34,6 +34,7 @@
#include <QJsonValue>
#include <QLoggingCategory>
+#include "bmimagelayer_p.h"
#include "bmshapelayer_p.h"
#include "bmfilleffect_p.h"
#include "bmbasictransform_p.h"
@@ -79,6 +80,10 @@ BMLayer *BMLayer::construct(QJsonObject definition)
BMLayer *layer = nullptr;
int type = definition.value(QLatin1String("ty")).toInt();
switch (type) {
+ case 2:
+ qCDebug(lcLottieQtBodymovinParser) << "Parse image layer";
+ layer = new BMImageLayer(definition);
+ break;
case 4:
qCDebug(lcLottieQtBodymovinParser) << "Parse shape layer";
layer = new BMShapeLayer(definition);
diff --git a/src/bodymovin/bodymovin.pro b/src/bodymovin/bodymovin.pro
index 5661c4a..1dc4d34 100644
--- a/src/bodymovin/bodymovin.pro
+++ b/src/bodymovin/bodymovin.pro
@@ -31,10 +31,12 @@ SOURCES += \
bmlayer.cpp \
bmshape.cpp \
bmshapelayer.cpp \
+ bmimagelayer.cpp \
bmrect.cpp \
bmfill.cpp \
bmgfill.cpp \
bmgroup.cpp \
+ bmimage.cpp \
bmstroke.cpp \
bmbasictransform.cpp \
bmshapetransform.cpp \
@@ -61,6 +63,7 @@ HEADERS += \
bmfreeformshape_p.h \
bmgfill_p.h \
bmgroup_p.h \
+ bmimage_p.h \
bmlayer_p.h \
bmproperty_p.h \
bmrect_p.h \
@@ -69,6 +72,7 @@ HEADERS += \
bmround_p.h \
bmshape_p.h \
bmshapelayer_p.h \
+ bmimagelayer_p.h \
bmshapetransform_p.h \
bmspatialproperty_p.h \
bmstroke_p.h \
diff --git a/src/bodymovin/lottierenderer_p.h b/src/bodymovin/lottierenderer_p.h
index 4089e8a..27490f5 100644
--- a/src/bodymovin/lottierenderer_p.h
+++ b/src/bodymovin/lottierenderer_p.h
@@ -52,6 +52,7 @@ class BMLayer;
class BMRect;
class BMFill;
class BMGFill;
+class BMImage;
class BMStroke;
class BMBasicTransform;
class BMLayerTransform;
@@ -84,6 +85,7 @@ public:
virtual void render(const BMRound &round) = 0;
virtual void render(const BMFill &fill) = 0;
virtual void render(const BMGFill &fill) = 0;
+ virtual void render(const BMImage &image) = 0;
virtual void render(const BMStroke &stroke) = 0;
virtual void render(const BMBasicTransform &trans) = 0;
virtual void render(const BMShapeTransform &trans) = 0;
diff --git a/src/imports/lottieanimation.cpp b/src/imports/lottieanimation.cpp
index 6d72151..27fe96f 100644
--- a/src/imports/lottieanimation.cpp
+++ b/src/imports/lottieanimation.cpp
@@ -695,9 +695,6 @@ int LottieAnimation::parse(QByteArray jsonSource)
++markerIt;
}
- if (rootObj.value(QLatin1String("assets")).toArray().count())
- qCWarning(lcLottieQtBodymovinParser) << "assets not supported";
-
if (rootObj.value(QLatin1String("chars")).toArray().count())
qCWarning(lcLottieQtBodymovinParser) << "chars not supported";
diff --git a/src/imports/rasterrenderer/batchrenderer.cpp b/src/imports/rasterrenderer/batchrenderer.cpp
index 84e5a2f..e1ad3c5 100644
--- a/src/imports/rasterrenderer/batchrenderer.cpp
+++ b/src/imports/rasterrenderer/batchrenderer.cpp
@@ -32,6 +32,7 @@
#include <QImage>
#include <QPainter>
#include <QHash>
+#include <QMap>
#include <QMutexLocker>
#include <QLoggingCategory>
#include <QThread>
@@ -41,6 +42,7 @@
#include <QtBodymovin/private/bmconstants_p.h>
#include <QtBodymovin/private/bmbase_p.h>
+#include <QtBodymovin/private/bmimagelayer_p.h>
#include <QtBodymovin/private/bmlayer_p.h>
#include "lottieanimation.h"
@@ -92,6 +94,10 @@ void BatchRenderer::registerAnimator(LottieAnimation *animator)
<< static_cast<void*>(animator);
Entry *&entry = m_animData[animator];
+ if (entry) {
+ delete (entry);
+ entry = nullptr;
+ }
Q_ASSERT(entry == nullptr);
entry = new Entry;
entry->animator = animator;
@@ -232,11 +238,27 @@ int BatchRenderer::parse(BMBase *rootElement, const QByteArray &jsonSource) cons
if (rootObj.empty())
return -1;
+ QMap<QString, QJsonObject> assets;
QJsonArray jsonLayers = rootObj.value(QLatin1String("layers")).toArray();
+ QJsonArray jsonAssets = rootObj.value(QLatin1String("assets")).toArray();
+ QJsonArray::const_iterator jsonAssetsIt = jsonAssets.constBegin();
+ while (jsonAssetsIt != jsonAssets.constEnd()) {
+ QJsonObject jsonAsset = (*jsonAssetsIt).toObject();
+
+ jsonAsset.insert(QLatin1String("fileSource"), QJsonValue::fromVariant(m_animData.keys().last()->source()));
+ QString id = jsonAsset.value(QLatin1String("id")).toString();
+ assets.insert(id, jsonAsset);
+ jsonAssetsIt++;
+ }
+
QJsonArray::const_iterator jsonLayerIt = jsonLayers.constEnd();
while (jsonLayerIt != jsonLayers.constBegin()) {
jsonLayerIt--;
QJsonObject jsonLayer = (*jsonLayerIt).toObject();
+ if (jsonLayer.value("ty").toInt() == 2) {
+ QString refId = jsonLayer.value("refId").toString();
+ jsonLayer.insert("asset", assets.value(refId));
+ }
BMLayer *layer = BMLayer::construct(jsonLayer);
if (layer) {
layer->setParent(rootElement);
diff --git a/src/imports/rasterrenderer/lottierasterrenderer.cpp b/src/imports/rasterrenderer/lottierasterrenderer.cpp
index 4224027..12790b5 100644
--- a/src/imports/rasterrenderer/lottierasterrenderer.cpp
+++ b/src/imports/rasterrenderer/lottierasterrenderer.cpp
@@ -39,6 +39,7 @@
#include <QtBodymovin/private/bmshape_p.h>
#include <QtBodymovin/private/bmfill_p.h>
#include <QtBodymovin/private/bmgfill_p.h>
+#include <QtBodymovin/private/bmimage_p.h>
#include <QtBodymovin/private/bmbasictransform_p.h>
#include <QtBodymovin/private/bmshapetransform_p.h>
#include <QtBodymovin/private/bmrect_p.h>
@@ -154,6 +155,22 @@ void LottieRasterRenderer::render(const BMEllipse &ellipse)
m_painter->restore();
}
+void LottieRasterRenderer::render(const BMImage &image)
+{
+ m_painter->save();
+
+ for (int i = 0; i < m_repeatCount; i++) {
+ qCDebug(lcLottieQtBodymovinRender) << "Image" << image.name();
+
+ applyRepeaterTransform(i);
+ QPointF center = image.getCenter();
+ m_painter->drawImage(center.x(), center.y(), image.getImage());
+ }
+
+ m_painter->restore();
+}
+
+
void LottieRasterRenderer::render(const BMRound &round)
{
m_painter->save();
diff --git a/src/imports/rasterrenderer/lottierasterrenderer.h b/src/imports/rasterrenderer/lottierasterrenderer.h
index 557762a..92254b3 100644
--- a/src/imports/rasterrenderer/lottierasterrenderer.h
+++ b/src/imports/rasterrenderer/lottierasterrenderer.h
@@ -56,6 +56,7 @@ public:
void render(const BMRound &round) override;
void render(const BMFill &fill) override;
void render(const BMGFill &shape) override;
+ void render(const BMImage &image) override;
void render(const BMStroke &stroke) override;
void render(const BMBasicTransform &transform) override;
void render(const BMShapeTransform &transform) override;