mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Lottie updates
This commit is contained in:
parent
8df43bb775
commit
b5b772a41b
@ -19,7 +19,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path);
|
||||
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data;
|
||||
|
||||
- (void)setFrame:(NSInteger)index;
|
||||
- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering;
|
||||
- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -23,30 +23,25 @@ public:
|
||||
|
||||
public:
|
||||
CoreGraphicsCanvasImpl(int width, int height);
|
||||
CoreGraphicsCanvasImpl(CGContextRef context, int width, int height);
|
||||
virtual ~CoreGraphicsCanvasImpl();
|
||||
|
||||
std::shared_ptr<Canvas> makeLayer(int width, int height) override;
|
||||
|
||||
virtual void saveState() override;
|
||||
virtual void restoreState() override;
|
||||
|
||||
virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override;
|
||||
virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) override;
|
||||
|
||||
virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, lottie::Color const &color) override;
|
||||
virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
|
||||
virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override;
|
||||
virtual void setBlendMode(BlendMode blendMode) override;
|
||||
virtual void clip(CGRect const &rect) override;
|
||||
virtual void concatenate(lottie::Transform2D const &transform) override;
|
||||
|
||||
virtual std::shared_ptr<Image> makeImage();
|
||||
virtual void draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) override;
|
||||
|
||||
virtual void pushLayer(CGRect const &rect) override;
|
||||
virtual bool pushLayer(CGRect const &rect, float alpha, std::optional<MaskMode> maskMode) override;
|
||||
virtual void popLayer() override;
|
||||
|
||||
std::vector<uint8_t> &backingData();
|
||||
@ -56,6 +51,8 @@ private:
|
||||
std::shared_ptr<Layer> ¤tLayer();
|
||||
|
||||
private:
|
||||
int _width = 0;
|
||||
int _height = 0;
|
||||
CGContextRef _topContext = nil;
|
||||
std::vector<std::shared_ptr<Layer>> _layerStack;
|
||||
};
|
||||
|
@ -64,9 +64,23 @@ bool addEnumeratedPath(CGContextRef context, CanvasPathEnumerator const &enumera
|
||||
|
||||
class CoreGraphicsCanvasImpl::Layer {
|
||||
public:
|
||||
explicit Layer(int width, int height) {
|
||||
struct Composition {
|
||||
CGRect rect;
|
||||
float alpha;
|
||||
Transform2D transform;
|
||||
std::optional<Canvas::MaskMode> maskMode;
|
||||
|
||||
Composition(CGRect rect_, float alpha_, Transform2D transform_, std::optional<Canvas::MaskMode> maskMode_) :
|
||||
rect(rect_), alpha(alpha_), transform(transform_), maskMode(maskMode_) {
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
explicit Layer(int width, int height, std::optional<Composition> composition) {
|
||||
_width = width;
|
||||
_height = height;
|
||||
_composition = composition;
|
||||
|
||||
_bytesPerRow = alignUp(width * 4, 16);
|
||||
_backingData.resize(_bytesPerRow * _height);
|
||||
memset(_backingData.data(), 0, _backingData.size());
|
||||
@ -77,7 +91,6 @@ public:
|
||||
CFRelease(colorSpace);
|
||||
|
||||
CGContextClearRect(_context, CGRectMake(0.0, 0.0, _width, _height));
|
||||
|
||||
}
|
||||
|
||||
~Layer() {
|
||||
@ -88,12 +101,29 @@ public:
|
||||
return _context;
|
||||
}
|
||||
|
||||
std::optional<Composition> composition() const {
|
||||
return _composition;
|
||||
}
|
||||
|
||||
std::shared_ptr<CoreGraphicsCanvasImpl::Image> makeImage() {
|
||||
::CGImageRef nativeImage = CGBitmapContextCreateImage(_context);
|
||||
if (nativeImage) {
|
||||
auto image = std::make_shared<CoreGraphicsCanvasImpl::Image>(nativeImage);
|
||||
CFRelease(nativeImage);
|
||||
return image;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
CGContextRef _context = nil;
|
||||
int _width = 0;
|
||||
int _height = 0;
|
||||
int _bytesPerRow = 0;
|
||||
std::vector<uint8_t> _backingData;
|
||||
|
||||
std::optional<Composition> _composition;
|
||||
};
|
||||
|
||||
CoreGraphicsCanvasImpl::Image::Image(::CGImageRef image) {
|
||||
@ -108,24 +138,13 @@ CoreGraphicsCanvasImpl::Image::~Image() {
|
||||
return _image;
|
||||
}
|
||||
|
||||
CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(int width, int height) {
|
||||
_layerStack.push_back(std::make_shared<Layer>(width, height));
|
||||
_topContext = CGContextRetain(currentLayer()->context());
|
||||
}
|
||||
|
||||
CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(CGContextRef context, int width, int height) {
|
||||
_layerStack.push_back(std::make_shared<Layer>(width, height));
|
||||
_topContext = CGContextRetain(context);
|
||||
CoreGraphicsCanvasImpl::CoreGraphicsCanvasImpl(int width, int height) :
|
||||
_width(width),
|
||||
_height(height) {
|
||||
_layerStack.push_back(std::make_shared<Layer>(width, height, std::nullopt));
|
||||
}
|
||||
|
||||
CoreGraphicsCanvasImpl::~CoreGraphicsCanvasImpl() {
|
||||
if (_topContext) {
|
||||
CFRelease(_topContext);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Canvas> CoreGraphicsCanvasImpl::makeLayer(int width, int height) {
|
||||
return std::make_shared<CoreGraphicsCanvasImpl>(_topContext, width, height);
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::saveState() {
|
||||
@ -142,7 +161,7 @@ void CoreGraphicsCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath,
|
||||
}
|
||||
|
||||
CGFloat components[4] = { color.r, color.g, color.b, color.a };
|
||||
CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components);
|
||||
CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components);
|
||||
CGContextSetFillColorWithColor(currentLayer()->context(), nativeColor);
|
||||
CFRelease(nativeColor);
|
||||
|
||||
@ -194,7 +213,7 @@ void CoreGraphicsCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size());
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size());
|
||||
if (nativeGradient) {
|
||||
CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
||||
CFRelease(nativeGradient);
|
||||
@ -204,7 +223,7 @@ void CoreGraphicsCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &
|
||||
CGContextRestoreGState(currentLayer()->context());
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) {
|
||||
void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) {
|
||||
CGContextSaveGState(currentLayer()->context());
|
||||
|
||||
if (!addEnumeratedPath(currentLayer()->context(), enumeratePath)) {
|
||||
@ -240,9 +259,9 @@ void CoreGraphicsCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size());
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size());
|
||||
if (nativeGradient) {
|
||||
CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
||||
CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(center.x, center.y), 0.0, CGPointMake(center.x, center.y), radius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
||||
CFRelease(nativeGradient);
|
||||
}
|
||||
|
||||
@ -256,7 +275,7 @@ void CoreGraphicsCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePat
|
||||
}
|
||||
|
||||
CGFloat components[4] = { color.r, color.g, color.b, color.a };
|
||||
CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components);
|
||||
CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(currentLayer()->context()), components);
|
||||
CGContextSetStrokeColorWithColor(currentLayer()->context(), nativeColor);
|
||||
CFRelease(nativeColor);
|
||||
|
||||
@ -385,7 +404,7 @@ void CoreGraphicsCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size());
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size());
|
||||
if (nativeGradient) {
|
||||
CGContextDrawLinearGradient(currentLayer()->context(), nativeGradient, CGPointMake(start.x, start.y), CGPointMake(end.x, end.y), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
||||
CFRelease(nativeGradient);
|
||||
@ -470,7 +489,7 @@ void CoreGraphicsCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(_topContext), components.data(), locations.data(), locations.size());
|
||||
CGGradientRef nativeGradient = CGGradientCreateWithColorComponents(CGBitmapContextGetColorSpace(currentLayer()->context()), components.data(), locations.data(), locations.size());
|
||||
if (nativeGradient) {
|
||||
CGContextDrawRadialGradient(currentLayer()->context(), nativeGradient, CGPointMake(startCenter.x, startCenter.y), startRadius, CGPointMake(endCenter.x, endCenter.y), endRadius, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
|
||||
CFRelease(nativeGradient);
|
||||
@ -480,32 +499,8 @@ void CoreGraphicsCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const
|
||||
CGContextRestoreGState(currentLayer()->context());
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) {
|
||||
CGFloat components[4] = { fillColor.r, fillColor.g, fillColor.b, fillColor.a };
|
||||
CGColorRef nativeColor = CGColorCreate(CGBitmapContextGetColorSpace(_topContext), components);
|
||||
CGContextSetFillColorWithColor(currentLayer()->context(), nativeColor);
|
||||
CFRelease(nativeColor);
|
||||
|
||||
CGContextFillRect(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height));
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::setBlendMode(BlendMode blendMode) {
|
||||
::CGBlendMode nativeMode = kCGBlendModeNormal;
|
||||
switch (blendMode) {
|
||||
case BlendMode::Normal: {
|
||||
nativeMode = kCGBlendModeNormal;
|
||||
break;
|
||||
}
|
||||
case BlendMode::DestinationIn: {
|
||||
nativeMode = kCGBlendModeDestinationIn;
|
||||
break;
|
||||
}
|
||||
case BlendMode::DestinationOut: {
|
||||
nativeMode = kCGBlendModeDestinationOut;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CGContextSetBlendMode(currentLayer()->context(), nativeMode);
|
||||
void CoreGraphicsCanvasImpl::clip(CGRect const &rect) {
|
||||
CGContextClipToRect(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height));
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::concatenate(lottie::Transform2D const &transform) {
|
||||
@ -513,29 +508,70 @@ void CoreGraphicsCanvasImpl::concatenate(lottie::Transform2D const &transform) {
|
||||
}
|
||||
|
||||
std::shared_ptr<CoreGraphicsCanvasImpl::Image> CoreGraphicsCanvasImpl::makeImage() {
|
||||
::CGImageRef nativeImage = CGBitmapContextCreateImage(currentLayer()->context());
|
||||
if (nativeImage) {
|
||||
auto image = std::make_shared<CoreGraphicsCanvasImpl::Image>(nativeImage);
|
||||
CFRelease(nativeImage);
|
||||
return image;
|
||||
return currentLayer()->makeImage();
|
||||
}
|
||||
|
||||
bool CoreGraphicsCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional<Canvas::MaskMode> maskMode) {
|
||||
auto currentTransform = fromNativeTransform(CATransform3DMakeAffineTransform(CGContextGetCTM(currentLayer()->context())));
|
||||
|
||||
CGRect globalRect(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
if (rect == CGRect::veryLarge()) {
|
||||
globalRect = CGRect(0.0f, 0.0f, (float)_width, (float)_height);
|
||||
} else {
|
||||
return nil;
|
||||
CGRect transformedRect = rect.applyingTransform(currentTransform);
|
||||
|
||||
CGRect integralTransformedRect(
|
||||
std::floor(transformedRect.x),
|
||||
std::floor(transformedRect.y),
|
||||
std::ceil(transformedRect.width + transformedRect.x - floor(transformedRect.x)),
|
||||
std::ceil(transformedRect.height + transformedRect.y - floor(transformedRect.y))
|
||||
);
|
||||
globalRect = integralTransformedRect.intersection(CGRect(0.0, 0.0, (CGFloat)_width, (CGFloat)_height));
|
||||
}
|
||||
if (globalRect.width <= 0.0f || globalRect.height <= 0.0f) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) {
|
||||
CGContextSetAlpha(currentLayer()->context(), alpha);
|
||||
CoreGraphicsCanvasImpl *impl = (CoreGraphicsCanvasImpl *)other.get();
|
||||
auto image = impl->makeImage();
|
||||
CGContextDrawImage(currentLayer()->context(), CGRectMake(rect.x, rect.y, rect.width, rect.height), ((CoreGraphicsCanvasImpl::Image *)image.get())->nativeImage());
|
||||
CGContextSetAlpha(currentLayer()->context(), 1.0);
|
||||
}
|
||||
_layerStack.push_back(std::make_shared<Layer>(globalRect.width, globalRect.height, Layer::Composition(globalRect, alpha, currentTransform, maskMode)));
|
||||
concatenate(Transform2D::identity().translated(Vector2D(-globalRect.x, -globalRect.y)));
|
||||
concatenate(currentTransform);
|
||||
|
||||
void CoreGraphicsCanvasImpl::pushLayer(CGRect const &rect) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void CoreGraphicsCanvasImpl::popLayer() {
|
||||
auto layer = _layerStack[_layerStack.size() - 1];
|
||||
_layerStack.pop_back();
|
||||
|
||||
if (const auto composition = layer->composition()) {
|
||||
saveState();
|
||||
concatenate(composition->transform.inverted());
|
||||
|
||||
CGContextSetAlpha(currentLayer()->context(), composition->alpha);
|
||||
|
||||
if (composition->maskMode) {
|
||||
switch (composition->maskMode.value()) {
|
||||
case Canvas::MaskMode::Normal: {
|
||||
CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationIn);
|
||||
break;
|
||||
}
|
||||
case Canvas::MaskMode::Inverse: {
|
||||
CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeDestinationOut);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto image = layer->makeImage();
|
||||
CGContextDrawImage(currentLayer()->context(), CGRectMake(composition->rect.x, composition->rect.y, composition->rect.width, composition->rect.height), ((CoreGraphicsCanvasImpl::Image *)image.get())->nativeImage());
|
||||
CGContextSetAlpha(currentLayer()->context(), 1.0);
|
||||
CGContextSetBlendMode(currentLayer()->context(), kCGBlendModeNormal);
|
||||
|
||||
restoreState();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<CoreGraphicsCanvasImpl::Layer> &CoreGraphicsCanvasImpl::currentLayer() {
|
||||
|
@ -17,6 +17,8 @@
|
||||
#include "include/effects/SkDashPathEffect.h"
|
||||
#include "include/effects/SkGradientShader.h"
|
||||
|
||||
#include <cfloat>
|
||||
|
||||
namespace lottie {
|
||||
|
||||
namespace {
|
||||
@ -89,10 +91,6 @@ SkiaCanvasImpl::~SkiaCanvasImpl() {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Canvas> SkiaCanvasImpl::makeLayer(int width, int height) {
|
||||
return std::make_shared<SkiaCanvasImpl>(width, height);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::saveState() {
|
||||
_canvas->save();
|
||||
}
|
||||
@ -105,7 +103,6 @@ void SkiaCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie:
|
||||
SkPaint paint;
|
||||
paint.setColor(skColor(color));
|
||||
paint.setAntiAlias(true);
|
||||
paint.setBlendMode(_blendMode);
|
||||
|
||||
SkPath nativePath;
|
||||
skPath(enumeratePath, nativePath);
|
||||
@ -117,7 +114,6 @@ void SkiaCanvasImpl::fillPath(CanvasPathEnumerator const &enumeratePath, lottie:
|
||||
void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) {
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setBlendMode(_blendMode);
|
||||
paint.setDither(false);
|
||||
paint.setStyle(SkPaint::Style::kFill_Style);
|
||||
|
||||
@ -136,7 +132,7 @@ void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumerat
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror));
|
||||
paint.setShader(SkGradientShader::MakeLinear(linearPoints, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp));
|
||||
|
||||
SkPath nativePath;
|
||||
skPath(enumeratePath, nativePath);
|
||||
@ -145,11 +141,9 @@ void SkiaCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumerat
|
||||
_canvas->drawPath(nativePath, paint);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) {
|
||||
void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) {
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setBlendMode(_blendMode);
|
||||
paint.setDither(false);
|
||||
paint.setStyle(SkPaint::Style::kFill_Style);
|
||||
|
||||
std::vector<SkColor> colors;
|
||||
@ -162,7 +156,7 @@ void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumerat
|
||||
locations.push_back(location);
|
||||
}
|
||||
|
||||
paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(startCenter.x, startCenter.y), endRadius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kMirror));
|
||||
paint.setShader(SkGradientShader::MakeRadial(SkPoint::Make(center.x, center.y), radius, colors.data(), locations.data(), (int)colors.size(), SkTileMode::kClamp));
|
||||
|
||||
SkPath nativePath;
|
||||
skPath(enumeratePath, nativePath);
|
||||
@ -172,9 +166,11 @@ void SkiaCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumerat
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, lottie::Color const &color) {
|
||||
if (lineWidth <= FLT_EPSILON) {
|
||||
return;
|
||||
}
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setBlendMode(_blendMode);
|
||||
paint.setColor(skColor(color));
|
||||
paint.setStyle(SkPaint::Style::kStroke_Style);
|
||||
|
||||
@ -223,6 +219,9 @@ void SkiaCanvasImpl::strokePath(CanvasPathEnumerator const &enumeratePath, float
|
||||
for (auto value : dashPattern) {
|
||||
intervals.push_back(value);
|
||||
}
|
||||
if (intervals.size() == 1) {
|
||||
intervals.push_back(intervals[0]);
|
||||
}
|
||||
paint.setPathEffect(SkDashPathEffect::Make(intervals.data(), (int)intervals.size(), dashPhase));
|
||||
}
|
||||
|
||||
@ -240,34 +239,8 @@ void SkiaCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumer
|
||||
assert(false);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) {
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setColor(skColor(fillColor));
|
||||
paint.setBlendMode(_blendMode);
|
||||
|
||||
_canvas->drawRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), paint);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::setBlendMode(BlendMode blendMode) {
|
||||
switch (blendMode) {
|
||||
case BlendMode::Normal: {
|
||||
_blendMode = SkBlendMode::kSrcOver;
|
||||
break;
|
||||
}
|
||||
case BlendMode::DestinationIn: {
|
||||
_blendMode = SkBlendMode::kDstIn;
|
||||
break;
|
||||
}
|
||||
case BlendMode::DestinationOut: {
|
||||
_blendMode = SkBlendMode::kDstOut;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
_blendMode = SkBlendMode::kSrcOver;
|
||||
break;
|
||||
}
|
||||
}
|
||||
void SkiaCanvasImpl::clip(CGRect const &rect) {
|
||||
_canvas->clipRect(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), true);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::concatenate(lottie::Transform2D const &transform) {
|
||||
@ -281,13 +254,32 @@ void SkiaCanvasImpl::concatenate(lottie::Transform2D const &transform) {
|
||||
_canvas->concat(matrix);
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) {
|
||||
SkiaCanvasImpl *impl = (SkiaCanvasImpl *)other.get();
|
||||
auto image = impl->surface()->makeImageSnapshot();
|
||||
bool SkiaCanvasImpl::pushLayer(CGRect const &rect, float alpha, std::optional<MaskMode> maskMode) {
|
||||
SkPaint paint;
|
||||
paint.setBlendMode(_blendMode);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setAlphaf(alpha);
|
||||
_canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint);
|
||||
if (maskMode) {
|
||||
switch (maskMode.value()) {
|
||||
case Canvas::MaskMode::Normal: {
|
||||
paint.setBlendMode(SkBlendMode::kDstIn);
|
||||
break;
|
||||
}
|
||||
case Canvas::MaskMode::Inverse: {
|
||||
paint.setBlendMode(SkBlendMode::kDstOut);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_canvas->saveLayer(SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), &paint);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::popLayer() {
|
||||
_canvas->restore();
|
||||
}
|
||||
|
||||
void SkiaCanvasImpl::flush() {
|
||||
|
@ -14,24 +14,21 @@ public:
|
||||
SkiaCanvasImpl(int width, int height, int bytesPerRow, void *pixelData);
|
||||
virtual ~SkiaCanvasImpl();
|
||||
|
||||
virtual std::shared_ptr<Canvas> makeLayer(int width, int height) override;
|
||||
|
||||
virtual void saveState() override;
|
||||
virtual void restoreState() override;
|
||||
|
||||
virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override;
|
||||
virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) override;
|
||||
virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, lottie::Color const &color) override;
|
||||
virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override;
|
||||
|
||||
virtual void setBlendMode(BlendMode blendMode) override;
|
||||
|
||||
virtual void clip(CGRect const &rect) override;
|
||||
virtual void concatenate(lottie::Transform2D const &transform) override;
|
||||
|
||||
virtual void draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) override;
|
||||
virtual bool pushLayer(CGRect const &rect, float alpha, std::optional<MaskMode> maskMode) override;
|
||||
virtual void popLayer() override;
|
||||
|
||||
void flush();
|
||||
sk_sp<SkSurface> surface() const;
|
||||
@ -41,7 +38,6 @@ private:
|
||||
bool _ownsPixelData = false;
|
||||
sk_sp<SkSurface> _surface;
|
||||
SkCanvas *_canvas = nullptr;
|
||||
SkBlendMode _blendMode = SkBlendMode::kSrcOver;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -57,98 +57,62 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) {
|
||||
_renderer->setFrame((int)index);
|
||||
}
|
||||
|
||||
- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering {
|
||||
- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering canUseMoreMemory:(bool)canUseMoreMemory skipImageGeneration:(bool)skipImageGeneration {
|
||||
std::shared_ptr<lottie::RenderTreeNode> renderNode = _renderer->renderNode();
|
||||
if (!renderNode) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
lottie::CanvasRenderer::Configuration configuration;
|
||||
configuration.canUseMoreMemory = canUseMoreMemory;
|
||||
//configuration.canUseMoreMemory = true;
|
||||
//configuration.disableGroupTransparency = true;
|
||||
|
||||
if (useReferenceRendering) {
|
||||
auto context = std::make_shared<lottie::CoreGraphicsCanvasImpl>((int)size.width, (int)size.height);
|
||||
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height));
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration);
|
||||
|
||||
auto image = context->makeImage();
|
||||
|
||||
return [[UIImage alloc] initWithCGImage:std::static_pointer_cast<lottie::CoreGraphicsCanvasImpl::Image>(image)->nativeImage()];
|
||||
} else {
|
||||
if ((int64_t)"" > 0) {
|
||||
/*auto surface = SkSurfaces::Raster(SkImageInfo::MakeN32Premul((int)size.width, (int)size.height));
|
||||
|
||||
int bytesPerRow = ((int)size.width) * 4;
|
||||
void *pixelData = malloc(bytesPerRow * (int)size.height);
|
||||
|
||||
sk_sp<SkSurface> surface2 = SkSurfaces::WrapPixels(
|
||||
SkImageInfo::MakeN32Premul((int)size.width, (int)size.height),
|
||||
pixelData,
|
||||
bytesPerRow,
|
||||
nullptr
|
||||
);
|
||||
|
||||
SkCanvas *canvas = surface->getCanvas();
|
||||
|
||||
SkPaint paint;
|
||||
paint.setAntiAlias(true);
|
||||
SkPath path;
|
||||
path.moveTo(124, 108);
|
||||
path.lineTo(172, 24);
|
||||
path.addCircle(50, 50, 30);
|
||||
path.moveTo(36, 148);
|
||||
path.quadTo(66, 188, 120, 136);
|
||||
canvas->drawPath(path, paint);
|
||||
paint.setStyle(SkPaint::kStroke_Style);
|
||||
paint.setColor(SK_ColorBLUE);
|
||||
paint.setStrokeWidth(3);
|
||||
canvas->drawPath(path, paint);
|
||||
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
|
||||
|
||||
CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext);
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored];
|
||||
CGImageRelease(bitmapImage);
|
||||
|
||||
CGContextRelease(targetContext);
|
||||
|
||||
free(pixelData);
|
||||
|
||||
return image;*/
|
||||
|
||||
int bytesPerRow = ((int)size.width) * 4;
|
||||
void *pixelData = malloc(bytesPerRow * (int)size.height);
|
||||
auto context = std::make_shared<lottie::SkiaCanvasImpl>((int)size.width, (int)size.height, bytesPerRow, pixelData);
|
||||
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height));
|
||||
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration);
|
||||
context->flush();
|
||||
|
||||
vImage_Buffer src;
|
||||
src.data = (void *)pixelData;
|
||||
src.width = (int)size.width;
|
||||
src.height = (int)size.height;
|
||||
src.rowBytes = bytesPerRow;
|
||||
if (skipImageGeneration) {
|
||||
free(pixelData);
|
||||
} else {
|
||||
vImage_Buffer src;
|
||||
src.data = (void *)pixelData;
|
||||
src.width = (int)size.width;
|
||||
src.height = (int)size.height;
|
||||
src.rowBytes = bytesPerRow;
|
||||
|
||||
uint8_t permuteMap[4] = {2, 1, 0, 3};
|
||||
vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, kvImageDoNotTile);
|
||||
uint8_t permuteMap[4] = {2, 1, 0, 3};
|
||||
vImagePermuteChannels_ARGB8888(&src, &src, permuteMap, kvImageDoNotTile);
|
||||
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host;
|
||||
|
||||
CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
CGContextRef targetContext = CGBitmapContextCreate(pixelData, (int)size.width, (int)size.height, 8, bytesPerRow, colorSpace, bitmapInfo);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
|
||||
CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext);
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored];
|
||||
CGImageRelease(bitmapImage);
|
||||
CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext);
|
||||
UIImage *image = [[UIImage alloc] initWithCGImage:bitmapImage scale:1.0f orientation:UIImageOrientationDownMirrored];
|
||||
CGImageRelease(bitmapImage);
|
||||
|
||||
CGContextRelease(targetContext);
|
||||
CGContextRelease(targetContext);
|
||||
|
||||
free(pixelData);
|
||||
free(pixelData);
|
||||
|
||||
return image;
|
||||
return image;
|
||||
}
|
||||
} else if ((int64_t)"" < 0) {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@ -158,7 +122,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) {
|
||||
int bytesPerRow = ((int)size.width) * 4;
|
||||
auto context = std::make_shared<lottie::ThorVGCanvasImpl>((int)size.width, (int)size.height, bytesPerRow);
|
||||
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height));
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration);
|
||||
|
||||
context->flush();
|
||||
|
||||
@ -177,7 +141,7 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) {
|
||||
return image;
|
||||
} else {
|
||||
auto context = std::make_shared<lottie::NullCanvasImpl>((int)size.width, (int)size.height);
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height));
|
||||
_canvasRenderer->render(_renderer, context, lottie::Vector2D(size.width, size.height), configuration);
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
@ -63,10 +63,6 @@ _transform(lottie::Transform2D::identity()) {
|
||||
ThorVGCanvasImpl::~ThorVGCanvasImpl() {
|
||||
}
|
||||
|
||||
std::shared_ptr<Canvas> ThorVGCanvasImpl::makeLayer(int width, int height) {
|
||||
return std::make_shared<ThorVGCanvasImpl>(width, height, width * 4);
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::saveState() {
|
||||
_stateStack.push_back(_transform);
|
||||
}
|
||||
@ -120,14 +116,14 @@ void ThorVGCanvasImpl::linearGradientFillPath(CanvasPathEnumerator const &enumer
|
||||
_canvas->push(std::move(shape));
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) {
|
||||
void ThorVGCanvasImpl::radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, Gradient const &gradient, Vector2D const ¢er, float radius) {
|
||||
auto shape = tvg::Shape::gen();
|
||||
tvgPath(enumeratePath, shape.get());
|
||||
|
||||
shape->transform(tvgTransform(_transform));
|
||||
|
||||
auto fill = tvg::RadialGradient::gen();
|
||||
fill->radial(startCenter.x, startCenter.y, endRadius);
|
||||
fill->radial(center.x, center.y, radius);
|
||||
|
||||
std::vector<tvg::Fill::ColorStop> colors;
|
||||
for (size_t i = 0; i < gradient.colors().size(); i++) {
|
||||
@ -214,55 +210,8 @@ void ThorVGCanvasImpl::linearGradientStrokePath(CanvasPathEnumerator const &enum
|
||||
void ThorVGCanvasImpl::radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) {
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::fill(lottie::CGRect const &rect, lottie::Color const &fillColor) {
|
||||
auto shape = tvg::Shape::gen();
|
||||
shape->appendRect(rect.x, rect.y, rect.width, rect.height, 0.0f, 0.0f);
|
||||
|
||||
shape->transform(tvgTransform(_transform));
|
||||
|
||||
shape->fill((int)(fillColor.r * 255.0), (int)(fillColor.g * 255.0), (int)(fillColor.b * 255.0), (int)(fillColor.a * 255.0));
|
||||
|
||||
_canvas->push(std::move(shape));
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::setBlendMode(BlendMode blendMode) {
|
||||
/*switch (blendMode) {
|
||||
case CGBlendMode::Normal: {
|
||||
_blendMode = SkBlendMode::kSrcOver;
|
||||
break;
|
||||
}
|
||||
case CGBlendMode::DestinationIn: {
|
||||
_blendMode = SkBlendMode::kDstIn;
|
||||
break;
|
||||
}
|
||||
case CGBlendMode::DestinationOut: {
|
||||
_blendMode = SkBlendMode::kDstOut;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
_blendMode = SkBlendMode::kSrcOver;
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::concatenate(lottie::Transform2D const &transform) {
|
||||
_transform = transform * _transform;
|
||||
/*_canvas->concat(SkM44(
|
||||
transform.m11, transform.m21, transform.m31, transform.m41,
|
||||
transform.m12, transform.m22, transform.m32, transform.m42,
|
||||
transform.m13, transform.m23, transform.m33, transform.m43,
|
||||
transform.m14, transform.m24, transform.m34, transform.m44
|
||||
));*/
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) {
|
||||
/*ThorVGCanvasImpl *impl = (ThorVGCanvasImpl *)other.get();
|
||||
auto image = impl->surface()->makeImageSnapshot();
|
||||
SkPaint paint;
|
||||
paint.setBlendMode(_blendMode);
|
||||
paint.setAlphaf(_alpha);
|
||||
_canvas->drawImageRect(image.get(), SkRect::MakeXYWH(rect.x, rect.y, rect.width, rect.height), SkSamplingOptions(SkFilterMode::kLinear), &paint);*/
|
||||
}
|
||||
|
||||
void ThorVGCanvasImpl::flush() {
|
||||
|
@ -14,25 +14,18 @@ public:
|
||||
ThorVGCanvasImpl(int width, int height, int bytesPerRow);
|
||||
virtual ~ThorVGCanvasImpl();
|
||||
|
||||
virtual std::shared_ptr<Canvas> makeLayer(int width, int height) override;
|
||||
|
||||
virtual void saveState() override;
|
||||
virtual void restoreState() override;
|
||||
|
||||
virtual void fillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Color const &color) override;
|
||||
virtual void linearGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
virtual void radialGradientFillPath(CanvasPathEnumerator const &enumeratePath, lottie::FillRule fillRule, lottie::Gradient const &gradient, Vector2D const ¢er, float radius) override;
|
||||
virtual void strokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, lottie::Color const &color) override;
|
||||
virtual void linearGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &start, lottie::Vector2D const &end) override;
|
||||
virtual void radialGradientStrokePath(CanvasPathEnumerator const &enumeratePath, float lineWidth, lottie::LineJoin lineJoin, lottie::LineCap lineCap, float dashPhase, std::vector<float> const &dashPattern, Gradient const &gradient, lottie::Vector2D const &startCenter, float startRadius, lottie::Vector2D const &endCenter, float endRadius) override;
|
||||
virtual void fill(lottie::CGRect const &rect, lottie::Color const &fillColor) override;
|
||||
|
||||
virtual void setBlendMode(BlendMode blendMode) override;
|
||||
|
||||
virtual void concatenate(lottie::Transform2D const &transform) override;
|
||||
|
||||
virtual void draw(std::shared_ptr<Canvas> const &other, float alpha, lottie::CGRect const &rect) override;
|
||||
|
||||
uint32_t *backingData() {
|
||||
return _backingData;
|
||||
}
|
||||
|
@ -10,17 +10,26 @@ import SoftwareLottieRenderer
|
||||
import LottieSwift
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? {
|
||||
func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage, allowedDifference: Double) -> (UIImage?, UIImage) {
|
||||
let lhsBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!)
|
||||
let rhsBuffer = try! vImage_Buffer(cgImage: rhs.cgImage!)
|
||||
let deltaBuffer = try! vImage_Buffer(cgImage: lhs.cgImage!)
|
||||
defer {
|
||||
lhsBuffer.free()
|
||||
rhsBuffer.free()
|
||||
deltaBuffer.free()
|
||||
}
|
||||
|
||||
let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * 0.01))
|
||||
memset(deltaBuffer.data, 0, Int(deltaBuffer.height) * deltaBuffer.rowBytes)
|
||||
|
||||
let maxDifferenceCount = Int((Double(Int(lhs.size.width) * Int(lhs.size.height)) * allowedDifference))
|
||||
|
||||
var foundDifferenceCount = 0
|
||||
|
||||
outer: for y in 0 ..< Int(lhs.size.height) {
|
||||
let lhsRowPixels = lhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes)
|
||||
let rhsRowPixels = rhsBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes)
|
||||
let deltaRowPixels = deltaBuffer.data.assumingMemoryBound(to: UInt8.self).advanced(by: y * lhsBuffer.rowBytes)
|
||||
|
||||
for x in 0 ..< Int(lhs.size.width) {
|
||||
let lhs0 = lhsRowPixels.advanced(by: x * 4 + 0).pointee
|
||||
@ -36,32 +45,30 @@ func areImagesEqual(_ lhs: UIImage, _ rhs: UIImage) -> UIImage? {
|
||||
let maxDiff = 25
|
||||
if abs(Int(lhs0) - Int(rhs0)) > maxDiff || abs(Int(lhs1) - Int(rhs1)) > maxDiff || abs(Int(lhs2) - Int(rhs2)) > maxDiff || abs(Int(lhs3) - Int(rhs3)) > maxDiff {
|
||||
|
||||
/*if false {
|
||||
lhsRowPixels.advanced(by: x * 4 + 0).pointee = 255
|
||||
lhsRowPixels.advanced(by: x * 4 + 1).pointee = 0
|
||||
lhsRowPixels.advanced(by: x * 4 + 2).pointee = 0
|
||||
lhsRowPixels.advanced(by: x * 4 + 3).pointee = 255
|
||||
}*/
|
||||
deltaRowPixels.advanced(by: x * 4 + 0).pointee = 255
|
||||
deltaRowPixels.advanced(by: x * 4 + 1).pointee = 0
|
||||
deltaRowPixels.advanced(by: x * 4 + 2).pointee = 0
|
||||
deltaRowPixels.advanced(by: x * 4 + 3).pointee = 255
|
||||
|
||||
foundDifferenceCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lhsBuffer.free()
|
||||
rhsBuffer.free()
|
||||
let colorSpace = Unmanaged<CGColorSpace>.passRetained(lhs.cgImage!.colorSpace!)
|
||||
let deltaImage = try! deltaBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile)
|
||||
|
||||
if foundDifferenceCount > maxDifferenceCount {
|
||||
let colorSpace = Unmanaged<CGColorSpace>.passRetained(lhs.cgImage!.colorSpace!)
|
||||
let diffImage = try! lhsBuffer.createCGImage(format: vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: colorSpace, bitmapInfo: lhs.cgImage!.bitmapInfo, version: 0, decode: nil, renderingIntent: .defaultIntent), flags: .doNotTile)
|
||||
return UIImage(cgImage: diffImage)
|
||||
|
||||
return (UIImage(cgImage: diffImage), UIImage(cgImage: deltaImage))
|
||||
} else {
|
||||
return nil
|
||||
return (nil, UIImage(cgImage: deltaImage))
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, alwaysDraw: Bool, useNonReferenceRendering: Bool, updateImage: @escaping (UIImage?, UIImage?) -> Void) async -> Bool {
|
||||
func processDrawAnimation(baseCachePath: String, path: String, name: String, size: CGSize, allowedDifference: Double, alwaysDraw: Bool, useNonReferenceRendering: Bool, updateImage: @escaping (UIImage?, UIImage?, UIImage?) -> Void) async -> Bool {
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||
print("Could not load \(path)")
|
||||
return false
|
||||
@ -85,16 +92,18 @@ func processDrawAnimation(baseCachePath: String, path: String, name: String, siz
|
||||
let referenceImage = decompressImageFrame(data: referenceImageData)
|
||||
|
||||
renderer.setFrame(frameIndex)
|
||||
let image = renderer.render(for: size, useReferenceRendering: !useNonReferenceRendering)!
|
||||
let image = renderer.render(for: size, useReferenceRendering: !useNonReferenceRendering, canUseMoreMemory: false, skipImageGeneration: false)!
|
||||
|
||||
if !useNonReferenceRendering, let diffImage = areImagesEqual(image, referenceImage) {
|
||||
updateImage(diffImage, referenceImage)
|
||||
let (diffImage, deltaImage) = areImagesEqual(image, referenceImage, allowedDifference: allowedDifference)
|
||||
|
||||
if !useNonReferenceRendering, let diffImage {
|
||||
updateImage(diffImage, referenceImage, deltaImage)
|
||||
|
||||
print("Mismatch in frame \(frameIndex)")
|
||||
return false
|
||||
} else {
|
||||
if alwaysDraw {
|
||||
updateImage(image, referenceImage)
|
||||
updateImage(image, referenceImage, diffImage)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -279,6 +288,43 @@ func decompressImageFrame(data: Data) -> UIImage {
|
||||
return decodeImageQOI(data)!
|
||||
}
|
||||
|
||||
final class ReferenceLottieAnimationItem {
|
||||
private let referenceAnimation: Animation
|
||||
private let referenceLayer: MainThreadAnimationLayer
|
||||
let frameCount: Int
|
||||
|
||||
init?(path: String) {
|
||||
guard let referenceAnimation = Animation.filepath(path) else {
|
||||
return nil
|
||||
}
|
||||
self.referenceAnimation = referenceAnimation
|
||||
|
||||
self.referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider())
|
||||
self.referenceLayer.position = referenceAnimation.bounds.center
|
||||
self.referenceLayer.isOpaque = false
|
||||
self.referenceLayer.backgroundColor = nil
|
||||
|
||||
self.frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame)
|
||||
}
|
||||
|
||||
func setFrame(index: Int) {
|
||||
self.referenceLayer.currentFrame = self.referenceAnimation.startFrame + CGFloat(index)
|
||||
self.referenceLayer.displayUpdate()
|
||||
}
|
||||
|
||||
func makeImage(width: Int, height: Int) -> UIImage? {
|
||||
let size = CGSize(width: CGFloat(width), height: CGFloat(width))
|
||||
|
||||
let referenceContext = ImageContext(width: width, height: height)
|
||||
referenceContext.context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
referenceContext.context.scaleBy(x: size.width / CGFloat(self.referenceAnimation.width), y: size.height / CGFloat(self.referenceAnimation.height))
|
||||
|
||||
referenceLayer.render(in: referenceContext.context)
|
||||
|
||||
return referenceContext.makeImage()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, name: String) -> String {
|
||||
let targetFolderPath = cacheReferenceFolderPath(baseCachePath: baseCachePath, width: width, name: name)
|
||||
@ -286,34 +332,19 @@ func cacheReferenceAnimation(baseCachePath: String, width: Int, path: String, na
|
||||
return targetFolderPath
|
||||
}
|
||||
|
||||
guard let referenceAnimation = Animation.filepath(path) else {
|
||||
preconditionFailure("Could not parse reference animation at \(path)")
|
||||
guard let referenceItem = ReferenceLottieAnimationItem(path: path) else {
|
||||
preconditionFailure("Could not load reference animation at \(path)")
|
||||
}
|
||||
let referenceLayer = MainThreadAnimationLayer(animation: referenceAnimation, imageProvider: BlankImageProvider(), textProvider: DefaultTextProvider(), fontProvider: DefaultFontProvider())
|
||||
|
||||
let cacheFolderPath = NSTemporaryDirectory() + "\(UInt64.random(in: 0 ... UInt64.max))"
|
||||
let _ = try? FileManager.default.createDirectory(atPath: cacheFolderPath, withIntermediateDirectories: true)
|
||||
|
||||
let frameCount = Int(referenceAnimation.endFrame - referenceAnimation.startFrame)
|
||||
for i in 0 ..< min(100000, referenceItem.frameCount) {
|
||||
let frameIndex = i % referenceItem.frameCount
|
||||
|
||||
let size = CGSize(width: CGFloat(width), height: CGFloat(width))
|
||||
referenceItem.setFrame(index: frameIndex)
|
||||
|
||||
for i in 0 ..< min(100000, frameCount) {
|
||||
let frameIndex = i % frameCount
|
||||
|
||||
referenceLayer.currentFrame = CGFloat(frameIndex)
|
||||
referenceLayer.displayUpdate()
|
||||
referenceLayer.position = referenceAnimation.bounds.center
|
||||
|
||||
referenceLayer.isOpaque = false
|
||||
referenceLayer.backgroundColor = nil
|
||||
let referenceContext = ImageContext(width: width, height: width)
|
||||
referenceContext.context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
referenceContext.context.scaleBy(x: size.width / CGFloat(referenceAnimation.width), y: size.height / CGFloat(referenceAnimation.height))
|
||||
|
||||
referenceLayer.render(in: referenceContext.context)
|
||||
|
||||
let referenceImage = referenceContext.makeImage()
|
||||
let referenceImage = referenceItem.makeImage(width: width, height: width)!
|
||||
try! compressImageFrame(image: referenceImage).write(to: URL(fileURLWithPath: cacheFolderPath + "/frame\(i)"))
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ private final class ReferenceCompareTest {
|
||||
private let view: UIView
|
||||
private let imageView = UIImageView()
|
||||
private let referenceImageView = UIImageView()
|
||||
private let deltaImageView = UIImageView()
|
||||
|
||||
init(view: UIView, testNonReference: Bool) {
|
||||
lottieSwift_getPathNativeBoundingBox = { path in
|
||||
@ -37,6 +38,12 @@ private final class ReferenceCompareTest {
|
||||
self.referenceImageView.backgroundColor = self.view.backgroundColor
|
||||
self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0)
|
||||
|
||||
self.view.addSubview(self.deltaImageView)
|
||||
self.deltaImageView.layer.magnificationFilter = .nearest
|
||||
self.deltaImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0 + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0))
|
||||
self.deltaImageView.backgroundColor = self.view.backgroundColor
|
||||
self.deltaImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0)
|
||||
|
||||
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
||||
|
||||
Task.detached {
|
||||
@ -67,6 +74,9 @@ private final class ReferenceCompareTest {
|
||||
"1391391008142393350.json": 1024
|
||||
]
|
||||
|
||||
let allowedDifferences: [String: Double] = [
|
||||
"1258816259754165.json": 0.04
|
||||
]
|
||||
let defaultSize = 128
|
||||
|
||||
let baseCachePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).path + "/frame-cache"
|
||||
@ -78,7 +88,7 @@ private final class ReferenceCompareTest {
|
||||
}
|
||||
|
||||
var continueFromName: String?
|
||||
//continueFromName = "5089561049196134821.json"
|
||||
//continueFromName = "1258816259754165.json"
|
||||
|
||||
let _ = await processAnimationFolderAsync(basePath: bundlePath, path: "", stopOnFailure: !testNonReference, process: { path, name, alwaysDraw in
|
||||
if let continueFromNameValue = continueFromName {
|
||||
@ -91,10 +101,11 @@ private final class ReferenceCompareTest {
|
||||
|
||||
let size = sizeMapping[name] ?? defaultSize
|
||||
|
||||
let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), alwaysDraw: alwaysDraw, useNonReferenceRendering: testNonReference, updateImage: { image, referenceImage in
|
||||
let result = await processDrawAnimation(baseCachePath: baseCachePath, path: path, name: name, size: CGSize(width: size, height: size), allowedDifference: allowedDifferences[name] ?? 0.01, alwaysDraw: alwaysDraw, useNonReferenceRendering: testNonReference, updateImage: { image, referenceImage, differenceImage in
|
||||
DispatchQueue.main.async {
|
||||
self.imageView.image = image
|
||||
self.referenceImageView.image = referenceImage
|
||||
self.deltaImageView.image = differenceImage
|
||||
}
|
||||
})
|
||||
return result
|
||||
@ -103,6 +114,139 @@ private final class ReferenceCompareTest {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private final class ManualReferenceCompareTest {
|
||||
private final class Item {
|
||||
let renderer: SoftwareLottieRenderer
|
||||
let referenceRenderer: ReferenceLottieAnimationItem
|
||||
|
||||
init(renderer: SoftwareLottieRenderer, referenceRenderer: ReferenceLottieAnimationItem) {
|
||||
self.renderer = renderer
|
||||
self.referenceRenderer = referenceRenderer
|
||||
}
|
||||
}
|
||||
|
||||
private let view: UIView
|
||||
private let imageView = UIImageView()
|
||||
private let referenceImageView = UIImageView()
|
||||
private let labelView = UILabel()
|
||||
|
||||
private let renderSize: CGSize
|
||||
private let testNonReference: Bool
|
||||
|
||||
private let fileList: [(filePath: String, fileName: String)]
|
||||
private var currentFileIndex: Int = 0
|
||||
private var currentItem: Item?
|
||||
|
||||
private var frameDisplayLink: SharedDisplayLinkDriver.Link?
|
||||
|
||||
init(view: UIView) {
|
||||
self.testNonReference = true
|
||||
|
||||
self.currentFileIndex = 0
|
||||
|
||||
lottieSwift_getPathNativeBoundingBox = { path in
|
||||
return getPathNativeBoundingBox(path)
|
||||
}
|
||||
|
||||
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
||||
self.fileList = buildAnimationFolderItems(basePath: bundlePath, path: "")
|
||||
|
||||
self.renderSize = CGSize(width: 128.0, height: 128.0)
|
||||
|
||||
self.view = view
|
||||
self.view.backgroundColor = .white
|
||||
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
let topInset: CGFloat = 50.0
|
||||
|
||||
self.view.addSubview(self.imageView)
|
||||
self.imageView.layer.magnificationFilter = .nearest
|
||||
self.imageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset), size: CGSize(width: 256.0, height: 256.0))
|
||||
self.imageView.backgroundColor = self.view.backgroundColor
|
||||
self.imageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0)
|
||||
|
||||
self.view.addSubview(self.referenceImageView)
|
||||
self.referenceImageView.layer.magnificationFilter = .nearest
|
||||
self.referenceImageView.frame = CGRect(origin: CGPoint(x: 10.0, y: topInset + 256.0 + 1.0), size: CGSize(width: 256.0, height: 256.0))
|
||||
self.referenceImageView.backgroundColor = self.view.backgroundColor
|
||||
self.referenceImageView.transform = CGAffineTransform.init(scaleX: 1.0, y: -1.0)
|
||||
|
||||
self.view.addSubview(self.labelView)
|
||||
|
||||
self.updateCurrentAnimation()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if recognizer.location(in: self.view).x <= self.view.bounds.width * 0.5 {
|
||||
if self.currentFileIndex != 0 {
|
||||
self.currentFileIndex = self.currentFileIndex - 1
|
||||
}
|
||||
} else {
|
||||
self.currentFileIndex = (self.currentFileIndex + 1) % self.fileList.count
|
||||
}
|
||||
self.updateCurrentAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCurrentAnimation() {
|
||||
self.imageView.image = nil
|
||||
self.referenceImageView.image = nil
|
||||
self.currentItem = nil
|
||||
|
||||
self.labelView.text = "\(self.currentFileIndex + 1) / \(self.fileList.count)"
|
||||
self.labelView.sizeToFit()
|
||||
self.labelView.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.height - 10.0 - self.labelView.bounds.height)
|
||||
|
||||
self.frameDisplayLink?.invalidate()
|
||||
self.frameDisplayLink = nil
|
||||
|
||||
let (filePath, _) = self.fileList[self.currentFileIndex]
|
||||
|
||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
|
||||
print("Could not load \(filePath)")
|
||||
return
|
||||
}
|
||||
guard let renderer = SoftwareLottieRenderer(data: data) else {
|
||||
print("Could not load animation at \(filePath)")
|
||||
return
|
||||
}
|
||||
guard let referenceRenderer = ReferenceLottieAnimationItem(path: filePath) else {
|
||||
print("Could not load reference animation at \(filePath)")
|
||||
return
|
||||
}
|
||||
|
||||
let currentItem = Item(renderer: renderer, referenceRenderer: referenceRenderer)
|
||||
self.currentItem = currentItem
|
||||
|
||||
var animationTime = 0.0
|
||||
let secondsPerFrame = 1.0 / Double(renderer.framesPerSecond)
|
||||
|
||||
let frameDisplayLink = SharedDisplayLinkDriver.shared.add { [weak self] deltaTime in
|
||||
guard let self, let currentItem = self.currentItem else {
|
||||
return
|
||||
}
|
||||
|
||||
var frameIndex = Int(animationTime / secondsPerFrame)
|
||||
frameIndex = frameIndex % currentItem.renderer.frameCount
|
||||
|
||||
currentItem.renderer.setFrame(frameIndex)
|
||||
let image = currentItem.renderer.render(for: self.renderSize, useReferenceRendering: !self.testNonReference, canUseMoreMemory: false, skipImageGeneration: false)!
|
||||
self.imageView.image = image
|
||||
|
||||
currentItem.referenceRenderer.setFrame(index: frameIndex)
|
||||
let referenceImage = currentItem.referenceRenderer.makeImage(width: Int(self.renderSize.width), height: Int(self.renderSize.height))!
|
||||
self.referenceImageView.image = referenceImage
|
||||
|
||||
animationTime += deltaTime
|
||||
}
|
||||
self.frameDisplayLink = frameDisplayLink
|
||||
frameDisplayLink.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
public final class ViewController: UIViewController {
|
||||
private var link: SharedDisplayLinkDriver.Link?
|
||||
private var test: AnyObject?
|
||||
@ -115,14 +259,18 @@ public final class ViewController: UIViewController {
|
||||
let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")!
|
||||
let filePath = bundlePath + "/fire.json"
|
||||
|
||||
let performanceFrameSize = 512
|
||||
let performanceFrameSize = 128
|
||||
|
||||
self.view.layer.addSublayer(MetalEngine.shared.rootLayer)
|
||||
|
||||
if "".isEmpty {
|
||||
if !"".isEmpty {
|
||||
if #available(iOS 13.0, *) {
|
||||
self.test = ReferenceCompareTest(view: self.view, testNonReference: false)
|
||||
}
|
||||
} else if !"".isEmpty {
|
||||
if #available(iOS 13.0, *) {
|
||||
self.test = ManualReferenceCompareTest(view: self.view)
|
||||
}
|
||||
} else if !"".isEmpty {
|
||||
/*let cachedAnimation = cacheLottieMetalAnimation(path: filePath)!
|
||||
let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)!
|
||||
@ -161,7 +309,7 @@ public final class ViewController: UIViewController {
|
||||
var frameIndex = 0
|
||||
while true {
|
||||
animationRenderer.setFrame(frameIndex)
|
||||
let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false)
|
||||
let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false, canUseMoreMemory: true, skipImageGeneration: true)
|
||||
frameIndex = (frameIndex + 1) % animationRenderer.frameCount
|
||||
numUpdates += 1
|
||||
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 97ed00c853de5078fd2a7f68d0da0b8872ac95b8
|
||||
Subproject commit 4aa40846d24842e1168375c3db79d85e913ca1e9
|
Loading…
x
Reference in New Issue
Block a user