experimental/skottie_ios: Skottie iOS/Metal App

To use, see instructions in experimental/skottie_ios/README.md .

No-Try: true
Change-Id: I4fb71576c5e38c7776d14561930b8c2598cfb48f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/240284
Commit-Queue: Hal Canary <halcanary@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
diff --git a/experimental/skottie_ios/BUILD.gn b/experimental/skottie_ios/BUILD.gn
new file mode 100644
index 0000000..af1efa6
--- /dev/null
+++ b/experimental/skottie_ios/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright 2019 Google LLC.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("../../gn/ios.gni")
+
+if (is_ios && skia_use_metal) {
+  ios_app_bundle("skottie_ios") {
+    sources = [
+      "SkMetalViewBridge.h",
+      "SkMetalViewBridge.mm",
+      "SkottieMtkView.h",
+      "SkottieMtkView.mm",
+      "main.mm",
+    ]
+    data_sources = [
+      "../../resources/skottie/skottie-3d-rotation-order.json",
+      "../../resources/skottie/skottie-camera-parent-3.json",
+      "../../resources/skottie/skottie-gradient-ramp.json",
+      "../../resources/skottie/skottie-linear-wipe-effect.json",
+      "../../resources/skottie/skottie-text-animator-1.json",
+      "../../resources/skottie/skottie-text-animator-2.json",
+      "../../resources/skottie/skottie-text-animator-3.json",
+      "../../resources/skottie/skottie-text-animator-4.json",
+      "../../resources/skottie/skottie-text-animator-5.json",
+      "../../resources/skottie/skottie-text-animator-8.json",
+      "../../resources/skottie/skottie-transform-effect.json",
+      "../../resources/skottie/skottie_sample_2.json",
+    ]
+    deps = [
+      "../..:skia",
+      "../../modules/skottie",
+    ]
+    cflags_objcc = [
+      "-std=c++14",
+      "-w",
+    ]
+    libs = [
+      "Metal.framework",
+      "MetalKit.framework",
+      "UIKit.framework",
+    ]
+    launchscreen = "../../platform_tools/ios/app/LaunchScreen.storyboard"
+  }
+}
diff --git a/experimental/skottie_ios/README.md b/experimental/skottie_ios/README.md
new file mode 100644
index 0000000..fcf3489
--- /dev/null
+++ b/experimental/skottie_ios/README.md
@@ -0,0 +1,29 @@
+##`skottie_ios`
+
+How to compile:
+
+    cd $SKIA_ROOT_DIRECTORY
+
+    cat >> BUILD.gn <<EOM
+    if (is_ios && skia_use_metal) {
+      group("skottie_ios") {
+        deps = [ "experimental/skottie_ios" ]
+      }
+    }
+    EOM
+
+    mkdir -p out/ios_arm64_mtl
+    cat > out/ios_arm64_mtl/args.gn <<EOM
+    target_os="ios"
+    target_cpu="arm64"
+    skia_use_metal=true
+    skia_use_expat=false
+    skia_enable_pdf=false
+    EOM
+
+    tools/git-sync-deps
+    bin/gn gen out/ios_arm64_mtl
+    ninja -C out/ios_arm64_mtl skottie_ios
+
+Then install the `out/ios_arm64_mtl/skottie_ios.app` bundle.
+
diff --git a/experimental/skottie_ios/SkMetalViewBridge.h b/experimental/skottie_ios/SkMetalViewBridge.h
new file mode 100644
index 0000000..9fb88a4
--- /dev/null
+++ b/experimental/skottie_ios/SkMetalViewBridge.h
@@ -0,0 +1,19 @@
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+#ifndef SkMetalViewBridge_DEFINED
+#define SkMetalViewBridge_DEFINED
+
+#import <MetalKit/MetalKit.h>
+
+class SkSurface;
+class GrContext;
+class GrContextOptions;
+template <typename T> class sk_sp;
+
+sk_sp<SkSurface> SkMtkViewToSurface(MTKView*, GrContext*);
+
+sk_sp<GrContext> SkMetalDeviceToGrContext(id<MTLDevice>, const GrContextOptions&);
+
+void SkMtkViewConfigForSkia(MTKView*);
+
+#endif  // SkMetalViewBridge_DEFINED
diff --git a/experimental/skottie_ios/SkMetalViewBridge.mm b/experimental/skottie_ios/SkMetalViewBridge.mm
new file mode 100644
index 0000000..aabf2ff
--- /dev/null
+++ b/experimental/skottie_ios/SkMetalViewBridge.mm
@@ -0,0 +1,53 @@
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+
+#include "experimental/skottie_ios/SkMetalViewBridge.h"
+
+#include "include/core/SkSurface.h"
+#include "include/gpu/GrBackendSurface.h"
+#include "include/gpu/GrContext.h"
+#include "include/gpu/GrContextOptions.h"
+#include "include/gpu/mtl/GrMtlTypes.h"
+
+#import <Metal/Metal.h>
+#import <MetalKit/MetalKit.h>
+
+sk_sp<SkSurface> SkMtkViewToSurface(MTKView* mtkView, GrContext* grContext) {
+    if (![[mtkView currentDrawable] texture] ||
+        !grContext ||
+        MTLPixelFormatDepth32Float_Stencil8 != [mtkView depthStencilPixelFormat] ||
+        MTLPixelFormatBGRA8Unorm != [mtkView colorPixelFormat]) {
+        return nullptr;
+    }
+    const SkColorType colorType = kBGRA_8888_SkColorType;  // MTLPixelFormatBGRA8Unorm
+    sk_sp<SkColorSpace> colorSpace = nullptr;  // MTLPixelFormatBGRA8Unorm
+    const GrSurfaceOrigin origin = kTopLeft_GrSurfaceOrigin;
+    const SkSurfaceProps surfaceProps(SkSurfaceProps::kLegacyFontHost_InitType);
+    int sampleCount = (int)[mtkView sampleCount];
+    CGSize size = [mtkView drawableSize];
+    int width  = (int)size.width;
+    int height = (int)size.height;
+
+    GrMtlTextureInfo fbInfo;
+    fbInfo.fTexture.retain((__bridge const void*)([[mtkView currentDrawable] texture]));
+    if (sampleCount == 1) {
+        GrBackendRenderTarget backendRT(width, height, 1, fbInfo);
+        return SkSurface::MakeFromBackendRenderTarget(grContext, backendRT, origin,
+                                                      colorType, colorSpace, &surfaceProps);
+    } else {
+        GrBackendTexture backendTexture(width, height, GrMipMapped::kNo, fbInfo);
+        return SkSurface::MakeFromBackendTexture(grContext, backendTexture, origin, sampleCount,
+                                                 colorType, colorSpace, &surfaceProps);
+    }
+}
+
+sk_sp<GrContext> SkMetalDeviceToGrContext(id<MTLDevice> device, const GrContextOptions& opts) {
+    return GrContext::MakeMetal((void*)device,
+                                (void*)[device newCommandQueue], opts);
+}
+
+void SkMtkViewConfigForSkia(MTKView* mtkView) {
+    [mtkView setDepthStencilPixelFormat:MTLPixelFormatDepth32Float_Stencil8];
+    [mtkView setColorPixelFormat:MTLPixelFormatBGRA8Unorm];
+    [mtkView setSampleCount:1];
+}
diff --git a/experimental/skottie_ios/SkottieMtkView.h b/experimental/skottie_ios/SkottieMtkView.h
new file mode 100644
index 0000000..33b7384
--- /dev/null
+++ b/experimental/skottie_ios/SkottieMtkView.h
@@ -0,0 +1,19 @@
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+#ifndef SkottieMtkView_DEFINED
+#define SkottieMtkView_DEFINED
+
+#import <MetalKit/MetalKit.h>
+#import <UIKit/UIKit.h>
+
+class GrContext;
+
+@interface SkottieMtkView : MTKView
+@property (assign) GrContext* grContext;  // non-owning pointer.
+- (void)drawRect:(CGRect)rect;
+- (BOOL)loadAnimation:(NSData*)d;
+- (CGSize)size;
+- (BOOL)togglePaused;
+@end
+
+#endif  // SkottieMtkView_DEFINED
diff --git a/experimental/skottie_ios/SkottieMtkView.mm b/experimental/skottie_ios/SkottieMtkView.mm
new file mode 100644
index 0000000..3c71e52
--- /dev/null
+++ b/experimental/skottie_ios/SkottieMtkView.mm
@@ -0,0 +1,95 @@
+ //Copyright 2019 Google LLC.
+ //Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+
+#include "experimental/skottie_ios/SkottieMtkView.h"
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkSurface.h"
+#include "include/core/SkTime.h"
+
+#include "modules/skottie/include/Skottie.h"
+
+#include "experimental/skottie_ios/SkMetalViewBridge.h"
+
+@implementation SkottieMtkView {
+    sk_sp<skottie::Animation> fAnimation; // owner
+    CGSize fSize;
+    double fStartTime;
+    double fTime;
+    SkMatrix fMatrix;
+    SkRect fAnimRect;
+    bool fPaused;
+}
+
+-(void)dealloc {
+    fAnimation = nullptr;
+    [super dealloc];
+}
+
+- (void)drawRect:(CGRect)rect {
+    [super drawRect:rect];
+    // TODO(halcanary): Use the rect and the InvalidationController to speed up rendering.
+    if (!fAnimation || ![[self currentDrawable] texture] || ![self grContext]) {
+        return;
+    }
+    CGSize size = [self drawableSize];
+    if (size.width != fSize.width || size.height != fSize.height) {
+        float aw = fAnimRect.right(),
+              ah = fAnimRect.bottom();
+        if (aw > 0 && ah > 0) {
+            float scale = std::min(size.width / aw, size.height / ah);
+            fMatrix.setScaleTranslate(scale,
+                                      scale,
+                                      ((float)size.width  - aw * scale) * 0.5f,
+                                      ((float)size.height - ah * scale) * 0.5f);
+        } else {
+            fMatrix = SkMatrix();
+        }
+        fSize = size;
+    }
+    SkPaint whitePaint(SkColors::kWhite);
+    if (!fPaused) {
+        fTime = SkTime::GetNSecs();
+        fAnimation->seekFrameTime(std::fmod(1e-9 * (fTime - fStartTime),
+                                            fAnimation->duration()), nullptr);
+    }
+    sk_sp<SkSurface> surface = SkMtkViewToSurface(self, [self grContext]);
+    if (!surface) {
+        NSLog(@"error: no sksurface");
+        return;
+    }
+    SkCanvas* canvas = surface->getCanvas();
+    canvas->concat(fMatrix);
+    canvas->drawRect(fAnimRect, whitePaint);
+    fAnimation->render(canvas);
+    surface->flush();
+    surface = nullptr;
+    [[self currentDrawable] present];
+}
+
+- (BOOL)loadAnimation:(NSData*) data {
+    skottie::Animation::Builder builder;
+    fAnimation = builder.make((const char*)[data bytes], (size_t)[data length]);
+    fTime = fStartTime = SkTime::GetNSecs();
+    fSize = {0, 0};
+    fAnimRect = fAnimation ? SkRect::MakeSize(fAnimation->size()) : SkRect{0, 0, 0, 0};
+    return fAnimation != nullptr;
+}
+
+- (CGSize)size {
+    if (fAnimation) {
+        const SkSize& s = fAnimation->size();
+        return {(CGFloat)s.width(), (CGFloat)s.height()};
+    }
+    return {0, 0};
+}
+
+- (BOOL)togglePaused {
+    fPaused = !fPaused;
+    if (!fPaused) {
+        fStartTime += (SkTime::GetNSecs() - fTime);
+    }
+    return fPaused;
+}
+@end
diff --git a/experimental/skottie_ios/main.mm b/experimental/skottie_ios/main.mm
new file mode 100644
index 0000000..73ea26d
--- /dev/null
+++ b/experimental/skottie_ios/main.mm
@@ -0,0 +1,141 @@
+// Copyright 2019 Google LLC.
+// Use of this source cofcee is governed by a BSD-style license that can be found in the LICENSE file.
+
+#include "experimental/skottie_ios/SkMetalViewBridge.h"
+#include "experimental/skottie_ios/SkottieMtkView.h"
+
+#include "include/gpu/GrContext.h"
+#include "include/gpu/GrContextOptions.h"
+
+#import <Metal/Metal.h>
+#import <MetalKit/MetalKit.h>
+#import <UIKit/UIKit.h>
+
+static UIStackView* make_skottie_stack(CGFloat width,
+                                       id<MTLDevice> metalDevice,
+                                       GrContext* grContext) {
+    UIStackView* stack = [[UIStackView alloc] init];
+    [stack setAxis:UILayoutConstraintAxisVertical];
+    [stack setDistribution:UIStackViewDistributionEqualSpacing];
+
+    NSBundle* mainBundle = [NSBundle mainBundle];
+    NSArray<NSString*>* paths = [mainBundle pathsForResourcesOfType:@"json"
+                                            inDirectory:nil];
+    constexpr CGFloat kSpacing = 2;
+    CGFloat totalHeight = kSpacing;
+    for (NSUInteger i = 0; i < [paths count]; ++i) {
+        NSString* path = [paths objectAtIndex:i];
+        NSData* content = [NSData dataWithContentsOfFile:path];
+        if (!content) {
+            NSLog(@"'%@' not found", path);
+            continue;
+        }
+        SkottieMtkView* skottieView = [[SkottieMtkView alloc] init];
+        if (![skottieView loadAnimation:content]) {
+            continue;
+        }
+        [skottieView setDevice:metalDevice];
+        [skottieView setGrContext:grContext];
+        SkMtkViewConfigForSkia(skottieView);
+        CGSize animSize = [skottieView size];
+        CGFloat height = animSize.width ? (width * animSize.height / animSize.width) : 0;
+        [skottieView setFrame:{{0, 0}, {width, height}}];
+        [skottieView setPreferredFramesPerSecond:30];
+        [[[skottieView heightAnchor] constraintEqualToConstant:height] setActive:true];
+        [[[skottieView widthAnchor] constraintEqualToConstant:width] setActive:true];
+        [stack addArrangedSubview:skottieView];
+        totalHeight += height + kSpacing;
+    }
+    [stack setFrame:{{0, 0}, {width, totalHeight}}];
+    return stack;
+}
+
+@interface AppViewController : UIViewController
+    @property (strong) id<MTLDevice> metalDevice;
+    @property (strong) UIStackView* stackView;
+@end
+
+@implementation AppViewController {
+    sk_sp<GrContext> fGrContext;
+}
+
+- (void)dealloc {
+    fGrContext = nullptr;
+    [super dealloc];
+}
+
+- (void)loadView {
+    [self setView:[[UIView alloc] init]];
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    if (!fGrContext) {
+        [self setMetalDevice:MTLCreateSystemDefaultDevice()];
+        if(![self metalDevice]) {
+            NSLog(@"Metal is not supported on this device");
+            return;
+        }
+        GrContextOptions grContextOptions;  // set different options here.
+        fGrContext = SkMetalDeviceToGrContext([self metalDevice], grContextOptions);
+    }
+
+    [self setStackView:make_skottie_stack([[UIScreen mainScreen] bounds].size.width,
+                                          [self metalDevice], fGrContext.get())];
+
+    CGFloat statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
+    CGSize mainScreenSize = [[UIScreen mainScreen] bounds].size;
+    CGRect scrollViewBounds = {{0, statusBarHeight},
+                               {mainScreenSize.width, mainScreenSize.height - statusBarHeight}};
+    UIScrollView* scrollView = [[UIScrollView alloc] initWithFrame:scrollViewBounds];
+    [scrollView setContentSize:[[self stackView] frame].size];
+    [scrollView addSubview:[self stackView]];
+    [scrollView setBackgroundColor:[UIColor blackColor]];
+
+    UIView* mainView = [self view];
+    [mainView setBounds:{{0, 0}, mainScreenSize}];
+    [mainView setBackgroundColor:[UIColor whiteColor]];
+    [mainView addSubview:scrollView];
+
+    UITapGestureRecognizer* tapGestureRecognizer = [[UITapGestureRecognizer alloc] init];
+    [tapGestureRecognizer addTarget:self action:@selector(handleTap:)];
+    [mainView addGestureRecognizer:tapGestureRecognizer];
+}
+
+- (void)handleTap:(UIGestureRecognizer*)sender {
+    if (![sender state] == UIGestureRecognizerStateEnded) {
+        return;
+    }
+    NSArray<UIView*>* subviews = [[self stackView] subviews];
+    for (NSUInteger i = 0; i < [subviews count]; ++i) {
+        UIView* subview = [subviews objectAtIndex:i];
+        if (![subview isKindOfClass:[SkottieMtkView class]]) {
+            continue;
+        }
+        SkottieMtkView* skottieView = (SkottieMtkView*)subview;
+        BOOL paused = [skottieView togglePaused];
+        [skottieView setEnableSetNeedsDisplay:paused];
+        [skottieView setPaused:paused];
+    }
+}
+@end
+
+@interface AppDelegate : UIResponder <UIApplicationDelegate>
+@property (strong, nonatomic) UIWindow* window;
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication*)app didFinishLaunchingWithOptions:(NSDictionary*)ops {
+    [self setWindow:[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]];
+    [[self window] setRootViewController:[[AppViewController alloc] init]];
+    [[self window] makeKeyAndVisible];
+    return YES;
+}
+@end
+
+int main(int argc, char* argv[]) {
+    @autoreleasepool {
+        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+    }
+}
diff --git a/infra/bots/compile.isolate b/infra/bots/compile.isolate
index 508a541..85accb8 100644
--- a/infra/bots/compile.isolate
+++ b/infra/bots/compile.isolate
@@ -17,15 +17,7 @@
       '../../dm',
       '../../docs/examples',
       '../../example',
-      '../../experimental/Networking/SkSockets.cpp',
-      '../../experimental/Networking/SkSockets.h',
-      '../../experimental/c-api-example/skia-c-example.c',
-      '../../experimental/ffmpeg',
-      '../../experimental/minimal_ios_mtl_skia_app/BUILD.gn',
-      '../../experimental/svg/model',
-      '../../experimental/tools/coreGraphicsPdf2png.cpp',
-      '../../experimental/wasm-skp-debugger/debugger_bindings.cpp',
-      '../../experimental/xform',
+      '../../experimental',
       '../../fuzz',
       '../../gm',
       '../../gn',