mirror of
https://github.com/cwinfo/matterbridge.git
synced 2024-12-27 11:15:39 +00:00
53cafa9f3d
This commit adds support for go/cgo tgs conversion when building with the -tags `cgo` The default binaries are still "pure" go and uses the old way of converting. * Move lottie_convert.py conversion code to its own file * Add optional libtgsconverter * Update vendor * Apply suggestions from code review * Update bridge/helper/libtgsconverter.go Co-authored-by: Wim <wim@42.be>
627 lines
19 KiB
C++
627 lines
19 KiB
C++
/*
|
|
* Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved.
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
#ifndef LOTTIEITEM_H
|
|
#define LOTTIEITEM_H
|
|
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
#include "lottie_lottiekeypath.h"
|
|
#include "lottie_lottiefiltermodel.h"
|
|
#include "rlottie.h"
|
|
#include "rlottiecommon.h"
|
|
#include "vector_varenaalloc.h"
|
|
#include "vector_vdrawable.h"
|
|
#include "vector_vmatrix.h"
|
|
#include "vector_vpainter.h"
|
|
#include "vector_vpath.h"
|
|
#include "vector_vpathmesure.h"
|
|
#include "vector_vpoint.h"
|
|
|
|
V_USE_NAMESPACE
|
|
|
|
namespace rlottie {
|
|
|
|
namespace internal {
|
|
|
|
template <class T>
|
|
class VSpan {
|
|
public:
|
|
using reference = T &;
|
|
using pointer = T *;
|
|
using const_pointer = T const *;
|
|
using const_reference = T const &;
|
|
using index_type = size_t;
|
|
|
|
using iterator = pointer;
|
|
using const_iterator = const_pointer;
|
|
|
|
VSpan() = default;
|
|
VSpan(pointer data, index_type size) : _data(data), _size(size) {}
|
|
|
|
constexpr pointer data() const noexcept { return _data; }
|
|
constexpr index_type size() const noexcept { return _size; }
|
|
constexpr bool empty() const noexcept { return size() == 0; }
|
|
constexpr iterator begin() const noexcept { return data(); }
|
|
constexpr iterator end() const noexcept { return data() + size(); }
|
|
constexpr const_iterator cbegin() const noexcept { return data(); }
|
|
constexpr const_iterator cend() const noexcept { return data() + size(); }
|
|
constexpr reference operator[](index_type idx) const
|
|
{
|
|
return *(data() + idx);
|
|
}
|
|
|
|
private:
|
|
pointer _data{nullptr};
|
|
index_type _size{0};
|
|
};
|
|
|
|
namespace renderer {
|
|
|
|
using DrawableList = VSpan<VDrawable *>;
|
|
|
|
enum class DirtyFlagBit : uchar {
|
|
None = 0x00,
|
|
Matrix = 0x01,
|
|
Alpha = 0x02,
|
|
All = (Matrix | Alpha)
|
|
};
|
|
typedef vFlag<DirtyFlagBit> DirtyFlag;
|
|
|
|
class SurfaceCache {
|
|
public:
|
|
SurfaceCache() { mCache.reserve(10); }
|
|
|
|
VBitmap make_surface(
|
|
size_t width, size_t height,
|
|
VBitmap::Format format = VBitmap::Format::ARGB32_Premultiplied)
|
|
{
|
|
if (mCache.empty()) return {width, height, format};
|
|
|
|
auto surface = mCache.back();
|
|
surface.reset(width, height, format);
|
|
|
|
mCache.pop_back();
|
|
return surface;
|
|
}
|
|
|
|
void release_surface(VBitmap &surface) { mCache.push_back(surface); }
|
|
|
|
private:
|
|
std::vector<VBitmap> mCache;
|
|
};
|
|
|
|
class Drawable final : public VDrawable {
|
|
public:
|
|
void sync();
|
|
|
|
public:
|
|
std::unique_ptr<LOTNode> mCNode{nullptr};
|
|
|
|
~Drawable() noexcept
|
|
{
|
|
if (mCNode && mCNode->mGradient.stopPtr)
|
|
free(mCNode->mGradient.stopPtr);
|
|
}
|
|
};
|
|
|
|
struct CApiData {
|
|
CApiData();
|
|
LOTLayerNode mLayer;
|
|
std::vector<LOTMask> mMasks;
|
|
std::vector<LOTLayerNode *> mLayers;
|
|
std::vector<LOTNode *> mCNodeList;
|
|
};
|
|
|
|
class Clipper {
|
|
public:
|
|
explicit Clipper(VSize size) : mSize(size) {}
|
|
void update(const VMatrix &matrix);
|
|
void preprocess(const VRect &clip);
|
|
VRle rle(const VRle &mask);
|
|
|
|
public:
|
|
VSize mSize;
|
|
VPath mPath;
|
|
VRle mMaskedRle;
|
|
VRasterizer mRasterizer;
|
|
bool mRasterRequest{false};
|
|
};
|
|
|
|
class Mask {
|
|
public:
|
|
explicit Mask(model::Mask *data) : mData(data) {}
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag);
|
|
model::Mask::Mode maskMode() const { return mData->mMode; }
|
|
VRle rle();
|
|
void preprocess(const VRect &clip);
|
|
bool inverted() const { return mData->mInv; }
|
|
public:
|
|
model::Mask *mData{nullptr};
|
|
VPath mLocalPath;
|
|
VPath mFinalPath;
|
|
VRasterizer mRasterizer;
|
|
float mCombinedAlpha{0};
|
|
bool mRasterRequest{false};
|
|
};
|
|
|
|
/*
|
|
* Handels mask property of a layer item
|
|
*/
|
|
class LayerMask {
|
|
public:
|
|
explicit LayerMask(model::Layer *layerData);
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag);
|
|
bool isStatic() const { return mStatic; }
|
|
VRle maskRle(const VRect &clipRect);
|
|
void preprocess(const VRect &clip);
|
|
|
|
public:
|
|
std::vector<Mask> mMasks;
|
|
VRle mRle;
|
|
bool mStatic{true};
|
|
bool mDirty{true};
|
|
};
|
|
|
|
class Layer;
|
|
|
|
class Composition {
|
|
public:
|
|
explicit Composition(std::shared_ptr<model::Composition> composition);
|
|
bool update(int frameNo, const VSize &size, bool keepAspectRatio);
|
|
VSize size() const { return mViewSize; }
|
|
void buildRenderTree();
|
|
const LOTLayerNode *renderTree() const;
|
|
bool render(const rlottie::Surface &surface);
|
|
void setValue(const std::string &keypath, LOTVariant &value);
|
|
|
|
private:
|
|
SurfaceCache mSurfaceCache;
|
|
VBitmap mSurface;
|
|
VMatrix mScaleMatrix;
|
|
VSize mViewSize;
|
|
std::shared_ptr<model::Composition> mModel;
|
|
Layer * mRootLayer{nullptr};
|
|
VArenaAlloc mAllocator{2048};
|
|
int mCurFrameNo;
|
|
bool mKeepAspectRatio{true};
|
|
};
|
|
|
|
class Layer {
|
|
public:
|
|
virtual ~Layer() = default;
|
|
Layer &operator=(Layer &&) noexcept = delete;
|
|
Layer(model::Layer *layerData);
|
|
int id() const { return mLayerData->id(); }
|
|
int parentId() const { return mLayerData->parentId(); }
|
|
void setParentLayer(Layer *parent) { mParentLayer = parent; }
|
|
void setComplexContent(bool value) { mComplexContent = value; }
|
|
bool complexContent() const { return mComplexContent; }
|
|
virtual void update(int frameNo, const VMatrix &parentMatrix,
|
|
float parentAlpha);
|
|
VMatrix matrix(int frameNo) const;
|
|
void preprocess(const VRect &clip);
|
|
virtual DrawableList renderList() { return {}; }
|
|
virtual void render(VPainter *painter, const VRle &mask,
|
|
const VRle &matteRle, SurfaceCache &cache);
|
|
bool hasMatte()
|
|
{
|
|
if (mLayerData->mMatteType == model::MatteType::None) return false;
|
|
return true;
|
|
}
|
|
model::MatteType matteType() const { return mLayerData->mMatteType; }
|
|
bool visible() const;
|
|
virtual void buildLayerNode();
|
|
LOTLayerNode & clayer() { return mCApiData->mLayer; }
|
|
std::vector<LOTLayerNode *> &clayers() { return mCApiData->mLayers; }
|
|
std::vector<LOTMask> & cmasks() { return mCApiData->mMasks; }
|
|
std::vector<LOTNode *> & cnodes() { return mCApiData->mCNodeList; }
|
|
const char * name() const { return mLayerData->name(); }
|
|
virtual bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value);
|
|
|
|
protected:
|
|
virtual void preprocessStage(const VRect &clip) = 0;
|
|
virtual void updateContent() = 0;
|
|
inline VMatrix combinedMatrix() const { return mCombinedMatrix; }
|
|
inline int frameNo() const { return mFrameNo; }
|
|
inline float combinedAlpha() const { return mCombinedAlpha; }
|
|
inline bool isStatic() const { return mLayerData->isStatic(); }
|
|
float opacity(int frameNo) const { return mLayerData->opacity(frameNo); }
|
|
inline DirtyFlag flag() const { return mDirtyFlag; }
|
|
bool skipRendering() const
|
|
{
|
|
return (!visible() || vIsZero(combinedAlpha()));
|
|
}
|
|
|
|
protected:
|
|
std::unique_ptr<LayerMask> mLayerMask;
|
|
model::Layer * mLayerData{nullptr};
|
|
Layer * mParentLayer{nullptr};
|
|
VMatrix mCombinedMatrix;
|
|
float mCombinedAlpha{0.0};
|
|
int mFrameNo{-1};
|
|
DirtyFlag mDirtyFlag{DirtyFlagBit::All};
|
|
bool mComplexContent{false};
|
|
std::unique_ptr<CApiData> mCApiData;
|
|
};
|
|
|
|
class CompLayer final : public Layer {
|
|
public:
|
|
explicit CompLayer(model::Layer *layerData, VArenaAlloc *allocator);
|
|
|
|
void render(VPainter *painter, const VRle &mask, const VRle &matteRle,
|
|
SurfaceCache &cache) final;
|
|
void buildLayerNode() final;
|
|
bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value) override;
|
|
|
|
protected:
|
|
void preprocessStage(const VRect &clip) final;
|
|
void updateContent() final;
|
|
|
|
private:
|
|
void renderHelper(VPainter *painter, const VRle &mask, const VRle &matteRle,
|
|
SurfaceCache &cache);
|
|
void renderMatteLayer(VPainter *painter, const VRle &inheritMask,
|
|
const VRle &matteRle, Layer *layer, Layer *src,
|
|
SurfaceCache &cache);
|
|
|
|
private:
|
|
std::vector<Layer *> mLayers;
|
|
std::unique_ptr<Clipper> mClipper;
|
|
};
|
|
|
|
class SolidLayer final : public Layer {
|
|
public:
|
|
explicit SolidLayer(model::Layer *layerData);
|
|
void buildLayerNode() final;
|
|
DrawableList renderList() final;
|
|
|
|
protected:
|
|
void preprocessStage(const VRect &clip) final;
|
|
void updateContent() final;
|
|
|
|
private:
|
|
Drawable mRenderNode;
|
|
VPath mPath;
|
|
VDrawable *mDrawableList{nullptr}; // to work with the Span api
|
|
};
|
|
|
|
class Group;
|
|
|
|
class ShapeLayer final : public Layer {
|
|
public:
|
|
explicit ShapeLayer(model::Layer *layerData, VArenaAlloc *allocator);
|
|
DrawableList renderList() final;
|
|
void buildLayerNode() final;
|
|
bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value) override;
|
|
|
|
protected:
|
|
void preprocessStage(const VRect &clip) final;
|
|
void updateContent() final;
|
|
std::vector<VDrawable *> mDrawableList;
|
|
Group * mRoot{nullptr};
|
|
};
|
|
|
|
class NullLayer final : public Layer {
|
|
public:
|
|
explicit NullLayer(model::Layer *layerData);
|
|
|
|
protected:
|
|
void preprocessStage(const VRect &) final {}
|
|
void updateContent() final;
|
|
};
|
|
|
|
class ImageLayer final : public Layer {
|
|
public:
|
|
explicit ImageLayer(model::Layer *layerData);
|
|
void buildLayerNode() final;
|
|
DrawableList renderList() final;
|
|
|
|
protected:
|
|
void preprocessStage(const VRect &clip) final;
|
|
void updateContent() final;
|
|
|
|
private:
|
|
Drawable mRenderNode;
|
|
VTexture mTexture;
|
|
VPath mPath;
|
|
VDrawable *mDrawableList{nullptr}; // to work with the Span api
|
|
};
|
|
|
|
class Object {
|
|
public:
|
|
enum class Type : uchar { Unknown, Group, Shape, Paint, Trim };
|
|
virtual ~Object() = default;
|
|
Object & operator=(Object &&) noexcept = delete;
|
|
virtual void update(int frameNo, const VMatrix &parentMatrix,
|
|
float parentAlpha, const DirtyFlag &flag) = 0;
|
|
virtual void renderList(std::vector<VDrawable *> &) {}
|
|
virtual bool resolveKeyPath(LOTKeyPath &, uint, LOTVariant &)
|
|
{
|
|
return false;
|
|
}
|
|
virtual Object::Type type() const { return Object::Type::Unknown; }
|
|
};
|
|
|
|
class Shape;
|
|
class Group : public Object {
|
|
public:
|
|
Group() = default;
|
|
explicit Group(model::Group *data, VArenaAlloc *allocator);
|
|
void addChildren(model::Group *data, VArenaAlloc *allocator);
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag) override;
|
|
void applyTrim();
|
|
void processTrimItems(std::vector<Shape *> &list);
|
|
void processPaintItems(std::vector<Shape *> &list);
|
|
void renderList(std::vector<VDrawable *> &list) override;
|
|
Object::Type type() const final { return Object::Type::Group; }
|
|
const VMatrix &matrix() const { return mMatrix; }
|
|
const char * name() const
|
|
{
|
|
static const char *TAG = "__";
|
|
return mModel.hasModel() ? mModel.name() : TAG;
|
|
}
|
|
bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value) override;
|
|
|
|
protected:
|
|
std::vector<Object *> mContents;
|
|
VMatrix mMatrix;
|
|
|
|
private:
|
|
model::Filter<model::Group> mModel;
|
|
};
|
|
|
|
class Shape : public Object {
|
|
public:
|
|
Shape(bool staticPath) : mStaticPath(staticPath) {}
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag) final;
|
|
Object::Type type() const final { return Object::Type::Shape; }
|
|
bool dirty() const { return mDirtyPath; }
|
|
const VPath &localPath() const { return mTemp; }
|
|
void finalPath(VPath &result);
|
|
void updatePath(const VPath &path)
|
|
{
|
|
mTemp = path;
|
|
mDirtyPath = true;
|
|
}
|
|
bool staticPath() const { return mStaticPath; }
|
|
void setParent(Group *parent) { mParent = parent; }
|
|
Group *parent() const { return mParent; }
|
|
|
|
protected:
|
|
virtual void updatePath(VPath &path, int frameNo) = 0;
|
|
virtual bool hasChanged(int prevFrame, int curFrame) = 0;
|
|
|
|
private:
|
|
bool hasChanged(int frameNo)
|
|
{
|
|
int prevFrame = mFrameNo;
|
|
mFrameNo = frameNo;
|
|
if (prevFrame == -1) return true;
|
|
if (mStaticPath || (prevFrame == frameNo)) return false;
|
|
return hasChanged(prevFrame, frameNo);
|
|
}
|
|
Group *mParent{nullptr};
|
|
VPath mLocalPath;
|
|
VPath mTemp;
|
|
int mFrameNo{-1};
|
|
bool mDirtyPath{true};
|
|
bool mStaticPath;
|
|
};
|
|
|
|
class Rect final : public Shape {
|
|
public:
|
|
explicit Rect(model::Rect *data);
|
|
|
|
protected:
|
|
void updatePath(VPath &path, int frameNo) final;
|
|
model::Rect *mData{nullptr};
|
|
|
|
bool hasChanged(int prevFrame, int curFrame) final
|
|
{
|
|
return (mData->mPos.changed(prevFrame, curFrame) ||
|
|
mData->mSize.changed(prevFrame, curFrame) ||
|
|
mData->roundnessChanged(prevFrame, curFrame));
|
|
}
|
|
};
|
|
|
|
class Ellipse final : public Shape {
|
|
public:
|
|
explicit Ellipse(model::Ellipse *data);
|
|
|
|
private:
|
|
void updatePath(VPath &path, int frameNo) final;
|
|
model::Ellipse *mData{nullptr};
|
|
bool hasChanged(int prevFrame, int curFrame) final
|
|
{
|
|
return (mData->mPos.changed(prevFrame, curFrame) ||
|
|
mData->mSize.changed(prevFrame, curFrame));
|
|
}
|
|
};
|
|
|
|
class Path final : public Shape {
|
|
public:
|
|
explicit Path(model::Path *data);
|
|
|
|
private:
|
|
void updatePath(VPath &path, int frameNo) final;
|
|
model::Path *mData{nullptr};
|
|
bool hasChanged(int prevFrame, int curFrame) final
|
|
{
|
|
return mData->mShape.changed(prevFrame, curFrame);
|
|
}
|
|
};
|
|
|
|
class Polystar final : public Shape {
|
|
public:
|
|
explicit Polystar(model::Polystar *data);
|
|
|
|
private:
|
|
void updatePath(VPath &path, int frameNo) final;
|
|
model::Polystar *mData{nullptr};
|
|
|
|
bool hasChanged(int prevFrame, int curFrame) final
|
|
{
|
|
return (mData->mPos.changed(prevFrame, curFrame) ||
|
|
mData->mPointCount.changed(prevFrame, curFrame) ||
|
|
mData->mInnerRadius.changed(prevFrame, curFrame) ||
|
|
mData->mOuterRadius.changed(prevFrame, curFrame) ||
|
|
mData->mInnerRoundness.changed(prevFrame, curFrame) ||
|
|
mData->mOuterRoundness.changed(prevFrame, curFrame) ||
|
|
mData->mRotation.changed(prevFrame, curFrame));
|
|
}
|
|
};
|
|
|
|
class Paint : public Object {
|
|
public:
|
|
Paint(bool staticContent);
|
|
void addPathItems(std::vector<Shape *> &list, size_t startOffset);
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag) override;
|
|
void renderList(std::vector<VDrawable *> &list) final;
|
|
Object::Type type() const final { return Object::Type::Paint; }
|
|
|
|
protected:
|
|
virtual bool updateContent(int frameNo, const VMatrix &matrix,
|
|
float alpha) = 0;
|
|
|
|
private:
|
|
void updateRenderNode();
|
|
|
|
protected:
|
|
std::vector<Shape *> mPathItems;
|
|
Drawable mDrawable;
|
|
VPath mPath;
|
|
DirtyFlag mFlag;
|
|
bool mStaticContent;
|
|
bool mRenderNodeUpdate{true};
|
|
bool mContentToRender{true};
|
|
};
|
|
|
|
class Fill final : public Paint {
|
|
public:
|
|
explicit Fill(model::Fill *data);
|
|
|
|
protected:
|
|
bool updateContent(int frameNo, const VMatrix &matrix, float alpha) final;
|
|
bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value) final;
|
|
|
|
private:
|
|
model::Filter<model::Fill> mModel;
|
|
};
|
|
|
|
class GradientFill final : public Paint {
|
|
public:
|
|
explicit GradientFill(model::GradientFill *data);
|
|
|
|
protected:
|
|
bool updateContent(int frameNo, const VMatrix &matrix, float alpha) final;
|
|
|
|
private:
|
|
model::GradientFill * mData{nullptr};
|
|
std::unique_ptr<VGradient> mGradient;
|
|
};
|
|
|
|
class Stroke : public Paint {
|
|
public:
|
|
explicit Stroke(model::Stroke *data);
|
|
|
|
protected:
|
|
bool updateContent(int frameNo, const VMatrix &matrix, float alpha) final;
|
|
bool resolveKeyPath(LOTKeyPath &keyPath, uint depth,
|
|
LOTVariant &value) final;
|
|
|
|
private:
|
|
model::Filter<model::Stroke> mModel;
|
|
};
|
|
|
|
class GradientStroke final : public Paint {
|
|
public:
|
|
explicit GradientStroke(model::GradientStroke *data);
|
|
|
|
protected:
|
|
bool updateContent(int frameNo, const VMatrix &matrix, float alpha) final;
|
|
|
|
private:
|
|
model::GradientStroke * mData{nullptr};
|
|
std::unique_ptr<VGradient> mGradient;
|
|
};
|
|
|
|
class Trim final : public Object {
|
|
public:
|
|
explicit Trim(model::Trim *data) : mData(data) {}
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag) final;
|
|
Object::Type type() const final { return Object::Type::Trim; }
|
|
void update();
|
|
void addPathItems(std::vector<Shape *> &list, size_t startOffset);
|
|
|
|
private:
|
|
bool pathDirty() const
|
|
{
|
|
for (auto &i : mPathItems) {
|
|
if (i->dirty()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
struct Cache {
|
|
int mFrameNo{-1};
|
|
model::Trim::Segment mSegment{};
|
|
};
|
|
Cache mCache;
|
|
std::vector<Shape *> mPathItems;
|
|
model::Trim * mData{nullptr};
|
|
VPathMesure mPathMesure;
|
|
bool mDirty{true};
|
|
};
|
|
|
|
class Repeater final : public Group {
|
|
public:
|
|
explicit Repeater(model::Repeater *data, VArenaAlloc *allocator);
|
|
void update(int frameNo, const VMatrix &parentMatrix, float parentAlpha,
|
|
const DirtyFlag &flag) final;
|
|
void renderList(std::vector<VDrawable *> &list) final;
|
|
|
|
private:
|
|
model::Repeater *mRepeaterData{nullptr};
|
|
bool mHidden{false};
|
|
int mCopies{0};
|
|
};
|
|
|
|
} // namespace renderer
|
|
|
|
} // namespace internal
|
|
|
|
} // namespace rlottie
|
|
|
|
#endif // LOTTIEITEM_H
|