├── Prototope
├── finger@2x.png
├── Prototope-Bridging-Header.h
├── Double+Extensions.swift
├── NSView+Extensions.swift
├── Border.swift
├── Radian.swift
├── Shadow.swift
├── InputEvent.swift
├── Dictionary+Extensions.swift
├── Video.swift
├── Info.plist
├── Timing.swift
├── VideoLayer.swift
├── Heartbeat.swift
├── Speech.swift
├── Math.swift
├── ParticlePreset.swift
├── FontProvider.swift
├── Image.swift
├── Sound.swift
├── ParticleEmitter.swift
├── Environment.swift
├── CameraLayer.swift
├── DisplayLink.swift
└── Color.swift
├── Examples
├── Jukebox
│ ├── coin.wav
│ ├── jump.wav
│ ├── laser.wav
│ ├── note.png
│ ├── powerup.wav
│ ├── explosion.wav
│ ├── note_highlighted.png
│ └── main.js
├── TouchUnicorns
│ ├── star.png
│ ├── unicorn.png
│ └── main.js
├── UnicornPower
│ ├── star.png
│ └── unicorn.png
├── Animated Gif
│ ├── yayfez-1.png
│ ├── yayfez-2.png
│ ├── yayfez-3.png
│ ├── yayfez-4.png
│ ├── yayfez-5.png
│ ├── yayfez-6.png
│ ├── yayfez-7.png
│ ├── yayfez-8.png
│ ├── yayfez-9.png
│ ├── yayfez-10.png
│ ├── yayfez-11.png
│ ├── yayfez-12.png
│ ├── yayfez-13.png
│ ├── yayfez-14.png
│ ├── yayfez-15.png
│ ├── yayfez-16.png
│ ├── yayfez-17.png
│ ├── yayfez-18.png
│ ├── yayfez-19.png
│ ├── yayfez-20.png
│ ├── yayfez-21.png
│ ├── yayfez-22.png
│ ├── yayfez-23.png
│ ├── yayfez-24.png
│ ├── yayfez-25.png
│ ├── yayfez-26.png
│ ├── yayfez-27.png
│ ├── yayfez-28.png
│ ├── yayfez-29.png
│ ├── yayfez-30.png
│ ├── yayfez-31.png
│ ├── yayfez-32.png
│ ├── yayfez-33.png
│ ├── yayfez-34.png
│ ├── yayfez-35.png
│ ├── yayfez-36.png
│ ├── yayfez-37.png
│ ├── yayfez-38.png
│ ├── yayfez-39.png
│ ├── yayfez-40.png
│ ├── yayfez-41.png
│ ├── yayfez-42.png
│ ├── yayfez-43.png
│ ├── yayfez-44.png
│ ├── yayfez-45.png
│ ├── yayfez-46.png
│ ├── yayfez-47.png
│ ├── yayfez-48.png
│ ├── yayfez-49.png
│ ├── yayfez-50.png
│ ├── yayfez-51.png
│ ├── yayfez-52.png
│ ├── yayfez-53.png
│ ├── yayfez-54.png
│ ├── yayfez-55.png
│ ├── yayfez-56.png
│ ├── yayfez-57.png
│ ├── yayfez-58.png
│ ├── yayfez-59.png
│ ├── yayfez-60.png
│ ├── yayfez-61.png
│ ├── yayfez-62.png
│ └── AnimatedGif.js
├── Custom Fonts
│ ├── FontAwesome.otf
│ └── main.js
├── TouchUnicornsSimpler
│ ├── star.png
│ ├── unicorn.png
│ └── main.js
├── Two finger ribbons
│ └── toolbar.png
├── TapTapTherapy
│ └── TapTapTherapy.js
├── TestInsets
│ └── main.js
├── CameraLayer
│ └── main.js
├── TestText
│ └── main.js
├── Hello Color
│ └── Hello Color.js
├── TouchAnimators
│ └── main.js
├── Color Puddles
│ └── main.js
├── Behaviors
│ └── main.js
├── Masking
│ └── Masking.js
├── ShapeLayer
│ └── ShapeLayer.js
└── SquishyBall
│ └── main.js
├── PrototopeTestApp
├── Glass.aiff
├── countdown.mp4
├── Images.xcassets
│ ├── paint.imageset
│ │ ├── paint.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── README.mdown
├── AppDelegate.swift
├── Info.plist
├── Base.lproj
│ ├── Main.storyboard
│ └── LaunchScreen.xib
├── JSTest.js
└── ViewController.swift
├── .arcconfig
├── PrototopeTests
├── PrototopeTests-Bridging-Header.h
├── MathTests.swift
├── Info.plist
├── GeometryTests.swift
├── ExceptionHandlingTests.swift
└── ViewTests.swift
├── .travis.yml
├── Protocaster
├── Images.xcassets
│ └── AppIcon.appiconset
│ │ ├── protoro-16.png
│ │ ├── protoro-32.png
│ │ ├── protoro-64.png
│ │ ├── protoro-1024.png
│ │ ├── protoro-128.png
│ │ ├── protoro-256-1.png
│ │ ├── protoro-256.png
│ │ ├── protoro-32-1.png
│ │ ├── protoro-512-1.png
│ │ ├── protoro-512.png
│ │ └── Contents.json
├── Protocaster-Bridging-Header.h
├── LogWindowController.swift
├── Info.plist
├── LogViewController.swift
├── ProtoscopeScanner.swift
└── ViewController.swift
├── Protoscope
├── Images.xcassets
│ ├── PrototopeP.imageset
│ │ ├── PrototopeP.png
│ │ ├── PrototopeP@2x.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── js
│ ├── package.json
│ ├── README.md
│ ├── webpack.config.js
│ └── main.js
├── Protoscope-Bridging-Header.h
├── Style.swift
├── StatusViewController.swift
├── ExceptionViewController.swift
├── Info.plist
├── ExceptionView.swift
├── StatusView.swift
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.xib
├── ProtoscopeServer.swift
├── ConsoleView.swift
├── URLMonitor.swift
├── SessionInteractor.swift
└── SceneViewController.swift
├── script
├── schemes.awk
├── targets.awk
├── xctool.awk
├── xcodebuild.awk
├── bootstrap
├── LICENSE.md
└── README.md
├── Prototope.xcodeproj
└── project.xcworkspace
│ └── contents.xcworkspacedata
├── PrototopeJSBridge
├── BridgeType.swift
├── PrototopeJSBridge.h
├── JSContext+BridgingExtensions.swift
├── VideoBridge.swift
├── Info.plist
├── SpeechBridge.swift
├── TimingBridge.swift
├── VideoLayerBridge.swift
├── ShadowBridge.swift
├── BorderBridge.swift
├── SoundBridge.swift
├── ImageBridge.swift
├── HeartbeatBridge.swift
├── MathBridge.swift
├── TunableBridge.swift
├── CameraLayerBridge.swift
├── ColorBridge.swift
└── ParticleEmitterBridge.swift
├── .arclint
├── Protorope
├── Protorope.swift
└── Message.swift
├── .gitmodules
├── PrototopeJSBridgeTests
├── JSBridgeTestCase.swift
├── ContextTests.swift
├── GeometryBridgeTests.swift
├── Info.plist
├── MathBridgeTests.swift
├── HeartbeatBridgeTests.swift
├── TunableBridgeTests.swift
├── TimingBridgeTests.swift
└── GestureBridgeTests.swift
├── PrototopeOSX
├── PrototopeOSX.h
└── Info.plist
├── .gitignore
├── ProtocasterTests
├── Info.plist
└── ProtocasterTests.swift
├── ProtoscopeTests
├── Info.plist
└── ProtoscopeTests.swift
├── PrototopeOSXTests
├── Info.plist
└── PrototopeOSXTests.swift
├── README.mdown
└── Prototype.swift
/Prototope/finger@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Prototope/finger@2x.png
--------------------------------------------------------------------------------
/Examples/Jukebox/coin.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/coin.wav
--------------------------------------------------------------------------------
/Examples/Jukebox/jump.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/jump.wav
--------------------------------------------------------------------------------
/Examples/Jukebox/laser.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/laser.wav
--------------------------------------------------------------------------------
/Examples/Jukebox/note.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/note.png
--------------------------------------------------------------------------------
/Examples/Jukebox/powerup.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/powerup.wav
--------------------------------------------------------------------------------
/PrototopeTestApp/Glass.aiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/PrototopeTestApp/Glass.aiff
--------------------------------------------------------------------------------
/Examples/Jukebox/explosion.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/explosion.wav
--------------------------------------------------------------------------------
/Examples/TouchUnicorns/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/TouchUnicorns/star.png
--------------------------------------------------------------------------------
/Examples/UnicornPower/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/UnicornPower/star.png
--------------------------------------------------------------------------------
/PrototopeTestApp/countdown.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/PrototopeTestApp/countdown.mp4
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-1.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-2.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-3.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-4.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-5.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-6.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-7.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-8.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-9.png
--------------------------------------------------------------------------------
/Examples/TouchUnicorns/unicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/TouchUnicorns/unicorn.png
--------------------------------------------------------------------------------
/Examples/UnicornPower/unicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/UnicornPower/unicorn.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-10.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-11.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-12.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-13.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-14.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-15.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-16.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-17.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-18.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-19.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-20.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-21.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-22.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-23.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-24.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-25.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-26.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-27.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-28.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-29.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-30.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-31.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-32.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-33.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-34.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-35.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-36.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-37.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-38.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-39.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-40.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-41.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-42.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-43.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-44.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-45.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-46.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-46.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-47.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-47.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-48.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-49.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-49.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-50.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-51.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-51.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-52.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-53.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-53.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-54.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-55.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-55.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-56.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-56.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-57.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-58.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-59.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-59.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-60.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-61.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-61.png
--------------------------------------------------------------------------------
/Examples/Animated Gif/yayfez-62.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Animated Gif/yayfez-62.png
--------------------------------------------------------------------------------
/Examples/Custom Fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Custom Fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/Examples/Jukebox/note_highlighted.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Jukebox/note_highlighted.png
--------------------------------------------------------------------------------
/Examples/TouchUnicornsSimpler/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/TouchUnicornsSimpler/star.png
--------------------------------------------------------------------------------
/Examples/Two finger ribbons/toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/Two finger ribbons/toolbar.png
--------------------------------------------------------------------------------
/Examples/TouchUnicornsSimpler/unicorn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Examples/TouchUnicornsSimpler/unicorn.png
--------------------------------------------------------------------------------
/.arcconfig:
--------------------------------------------------------------------------------
1 | {
2 | "conduit_uri": "https://phabricator.khanacademy.org/",
3 | "lint.engine": "ArcanistConfigurationDrivenLintEngine"
4 | }
5 |
--------------------------------------------------------------------------------
/Examples/Custom Fonts/main.js:
--------------------------------------------------------------------------------
1 | var layer = new TextLayer()
2 | layer.fontName = "FontAwesome"
3 | layer.fontSize = 64
4 | layer.text = "KHANacademy"
5 |
--------------------------------------------------------------------------------
/PrototopeTestApp/Images.xcassets/paint.imageset/paint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/PrototopeTestApp/Images.xcassets/paint.imageset/paint.png
--------------------------------------------------------------------------------
/PrototopeTests/PrototopeTests-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | osx_image: xcode611
3 | before_install: true
4 | install: true
5 | git:
6 | submodules: false
7 | script: script/cibuild Prototope
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-16.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-32.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-64.png
--------------------------------------------------------------------------------
/Protoscope/Images.xcassets/PrototopeP.imageset/PrototopeP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protoscope/Images.xcassets/PrototopeP.imageset/PrototopeP.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-1024.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-128.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-256-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-256-1.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-256.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-32-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-32-1.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-512-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-512-1.png
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protocaster/Images.xcassets/AppIcon.appiconset/protoro-512.png
--------------------------------------------------------------------------------
/Protoscope/Images.xcassets/PrototopeP.imageset/PrototopeP@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khan/Prototope/HEAD/Protoscope/Images.xcassets/PrototopeP.imageset/PrototopeP@2x.png
--------------------------------------------------------------------------------
/script/schemes.awk:
--------------------------------------------------------------------------------
1 | BEGIN {
2 | FS = "\n";
3 | }
4 |
5 | /Schemes:/ {
6 | while (getline && $0 != "") {
7 | sub(/^ +/, "");
8 | print "'" $0 "'";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Prototope.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/script/targets.awk:
--------------------------------------------------------------------------------
1 | BEGIN {
2 | FS = "\n";
3 | }
4 |
5 | /Targets:/ {
6 | while (getline && $0 != "") {
7 | if ($0 ~ /Tests/) continue;
8 |
9 | sub(/^ +/, "");
10 | print "'" $0 "'";
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Examples/TapTapTherapy/TapTapTherapy.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | And example to demonstrate:
4 | – how to count the number of times 1 object has been tapped
5 | – taking into account the frequency of tapping on one object
6 |
7 | */
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Protoscope/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "babel-core": "^5.1.10",
4 | "source-map": "^0.4.2"
5 | },
6 | "devDependencies": {
7 | "webpack": "^1.8.5"
8 | },
9 | "scripts": {
10 | "build": "webpack"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Prototope/Prototope-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Prototope-Bridging-Header.h
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 11/16/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "KFTunableSpec.h"
--------------------------------------------------------------------------------
/Protoscope/Protoscope-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "DTBonjourDataChunk.h"
6 | #import "DTBonjourDataConnection.h"
7 | #import "DTBonjourServer.h"
8 | #import "NSScanner+DTBonjour.h"
--------------------------------------------------------------------------------
/Protocaster/Protocaster-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "DTBonjourDataChunk.h"
6 | #import "DTBonjourDataConnection.h"
7 | #import "DTBonjourServer.h"
8 | #import "NSScanner+DTBonjour.h"
--------------------------------------------------------------------------------
/PrototopeTestApp/README.mdown:
--------------------------------------------------------------------------------
1 | # PrototopeTestApp
2 |
3 | This is a barebones sandbox to sanity-check interactive Prototope features during development. Prototypes can be written either in Swift or in JS.
4 |
5 | This should probably eventually go away (to be replaced completely by Protoscope and OhaiPrototope).
--------------------------------------------------------------------------------
/PrototopeJSBridge/BridgeType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BridgeType.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/1/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import JavaScriptCore
10 |
11 | public protocol BridgeType {
12 | static func addToContext(context: JSContext)
13 | }
14 |
--------------------------------------------------------------------------------
/Protoscope/js/README.md:
--------------------------------------------------------------------------------
1 | # Protoscope JS runtime
2 |
3 | I help with transforming JavaScript before runtime and mapping error stack traces to their original source lines. To rebuild me, run
4 |
5 | ```
6 | $ npm install
7 | $ npm run build
8 | ```
9 |
10 | Or run `./node_modules/.bin/webpack --watch` to automatically rebuild `dist/protoscope-bundle.js` whenever you change the source files.
11 |
--------------------------------------------------------------------------------
/Prototope/Double+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+Extensions.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on Apr-21-2015.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | public extension Double {
10 |
11 | /** If self is not a number (i.e., NaN), returns 0. Otherwise returns self. */
12 | var notNaNValue: Double {
13 | return self.isNaN ? 0 : self
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Protoscope/Style.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Style.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/7/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct Style {
12 | static let cyan = UIColor(red: 77.0/255.0, green: 208.0/255.0, blue: 225.0/255.0, alpha: 1.0)
13 | static let warning = UIColor(red: 233.0/255.0, green: 151.0/255.0, blue:17.0/255.0, alpha: 1.0)
14 | }
15 |
--------------------------------------------------------------------------------
/script/xctool.awk:
--------------------------------------------------------------------------------
1 | # Exit statuses:
2 | #
3 | # 0 - No errors found.
4 | # 1 - Wrong SDK. Retry with SDK `iphonesimulator`.
5 | # 2 - Missing target.
6 |
7 | BEGIN {
8 | status = 0;
9 | }
10 |
11 | {
12 | print;
13 | }
14 |
15 | /Testing with the '(.+)' SDK is not yet supported/ {
16 | status = 1;
17 | }
18 |
19 | /does not contain a target named/ {
20 | status = 2;
21 | }
22 |
23 | END {
24 | exit status;
25 | }
26 |
--------------------------------------------------------------------------------
/Protoscope/js/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require("webpack");
2 |
3 | module.exports = {
4 | context: __dirname,
5 | entry: "./main.js",
6 | output: {
7 | path: __dirname + "/dist",
8 | filename: "protoscope-bundle.js",
9 | library: "Protoscope"
10 | },
11 | module: {
12 | noParse: /\/node_modules\/babel-core\/browser.js$/
13 | },
14 | node: {
15 | fs: "empty"
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/PrototopeTestApp/Images.xcassets/paint.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "2x",
10 | "filename" : "paint.png"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/.arclint:
--------------------------------------------------------------------------------
1 | {
2 | "linters": {
3 | "khan-linter": {
4 | "type": "script-and-regex",
5 | "script-and-regex.script": "ka-lint --always-exit-0 --blacklist=yes --propose-arc-fixes",
6 | "script-and-regex.regex": "\/^((?P[^:]*):(?P\\d+):((?P\\d+):)? (?P((?PE)|(?PW))\\S+) (?P[^\\x00]*)(\\x00(?P[^\\x00]*)\\x00(?P[^\\x00]*)\\x00)?)|(?PSKIPPING.*)$\/m"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Protorope/Protorope.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Protorope.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | // Protorope is the communications medium over which prototypes can be live-updated.
12 |
13 | // The type of the service published by Protorope receivers (i.e. Protoscope).
14 | let ProtoropeReceiverServiceType = "_protorope_receiver._tcp."
15 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "ThirdParty/pop"]
2 | path = ThirdParty/pop
3 | url = https://github.com/jbrennan/pop.git
4 | [submodule "ThirdParty/TunableSpec"]
5 | path = ThirdParty/TunableSpec
6 | url = https://github.com/Khan/TunableSpec.git
7 | [submodule "ThirdParty/DTBonjour"]
8 | path = ThirdParty/DTBonjour
9 | url = https://github.com/Khan/DTBonjour.git
10 | [submodule "ThirdParty/swiftz"]
11 | path = ThirdParty/swiftz
12 | url = https://github.com/Khan/swiftz.git
13 |
--------------------------------------------------------------------------------
/Protoscope/Images.xcassets/PrototopeP.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x",
6 | "filename" : "PrototopeP.png"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x",
11 | "filename" : "PrototopeP@2x.png"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/Examples/Jukebox/main.js:
--------------------------------------------------------------------------------
1 | const sounds = ["coin", "explosion", "jump", "laser", "powerup"]
2 | Layer.root.backgroundColor = new Color({hue: 0.1, brightness: 1.0, saturation: 0.4});
3 |
4 | const button = new Layer({imageName: "note"})
5 | button.position = Layer.root.bounds.center
6 |
7 | button.gestures = [
8 | new TapGesture({
9 | handler: function() {
10 | const soundIndex = Math.floor(Math.random() * sounds.length)
11 | const sound = new Sound({name: sounds[soundIndex]})
12 | sound.play()
13 | }
14 | })
15 | ]
16 |
--------------------------------------------------------------------------------
/PrototopeTestApp/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/3/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
18 | return true
19 | }
20 |
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Prototope/NSView+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSView+Extensions.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-08-17.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | extension NSView {
12 | var frameCenter: Point {
13 | get { return Point(CGPoint(x: NSMidX(frame), y: NSMidY(frame))) }
14 | set {
15 | let center = CGPoint(newValue)
16 | setFrameOrigin(CGPoint(x: center.x - frame.width / 2.0, y: center.y - frame.height / 2.0))
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Prototope/Border.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Border.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 12/1/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | /** A simple specification of a layer border. */
10 | public struct Border {
11 | public var color: Color
12 |
13 | /** Specifies the width of the border in the border-owning layer's coordinate space. */
14 | public var width: Double
15 |
16 | public init(color: Color, width: Double) {
17 | self.color = color
18 | self.width = width
19 | }
20 | }
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/JSBridgeTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSBridgeTestCase.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class JSBridgeTestCase: XCTestCase {
14 | var context: Context!
15 |
16 | override func setUp() {
17 | context = Context()
18 | context.exceptionHandler = { value in XCTFail("Received JS exception: \(value)") }
19 | super.setUp()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Examples/TestInsets/main.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | And example to demonstrate how to use insets on Rects
4 |
5 | */
6 |
7 |
8 | let r1 = new Rect({x:100, y:100, width:250, height:250})
9 |
10 | console.log(r1.minX+","+r1.minY+":"+r1.maxX+","+r1.maxY);
11 |
12 | //r1 = r1.inset({value: 10})
13 | //r1 = r1.inset({horizontal: 20, vertical: 10})
14 | r1 = r1.inset({top:10, right:20, bottom:30, left:40})
15 |
16 | // This will trigger a protonope
17 | //r1 = r1.inset({top:1000, right:20, bottom:30, left:40})
18 |
19 |
20 | console.log(r1.minX+","+r1.minY+":"+r1.maxX+","+r1.maxY);
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/ContextTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContextBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 3/2/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class ContextTests: JSBridgeTestCase {
14 | func testContextExecutesInStrictMode() {
15 | let context = Context()
16 | var hitException = false
17 | context.exceptionHandler = { _ in hitException = true }
18 | context.evaluateScript("x = 4")
19 | XCTAssertTrue(hitException)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Prototope/Radian.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Radian.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on Feb-05-2015.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /** For when we're dealing with radians, this type makes it more explicit. */
12 | public typealias Radian = Double
13 |
14 | extension Radian {
15 |
16 | /** One full revolution in radians. */
17 | static let circle = 2.0 * M_PI
18 |
19 |
20 | /** Initialize a radian with degrees. */
21 | public init(degrees: Double) {
22 | self.init(degrees * M_PI / 180)
23 | }
24 | }
--------------------------------------------------------------------------------
/Prototope/Shadow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Shadow.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 12/2/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | /** A simple specification of a layer shadow. */
10 | public struct Shadow {
11 | public var color: Color
12 | public var alpha: Double
13 | public var offset: Size
14 | public var radius: Double
15 |
16 | public init(color: Color, alpha: Double, offset: Size, radius: Double) {
17 | self.color = color
18 | self.alpha = alpha
19 | self.offset = offset
20 | self.radius = radius
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Protoscope/StatusViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusViewController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/7/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class StatusViewController: UIViewController {
12 | init() {
13 | super.init(nibName: nil, bundle: nil)
14 | }
15 |
16 | required init(coder aDecoder: NSCoder) {
17 | fatalError("init(coder:) has intentionally not been implemented")
18 | }
19 |
20 | override func loadView() {
21 | view = StatusView()
22 | view.autoresizingMask = .FlexibleWidth | .FlexibleHeight
23 | }
24 | }
--------------------------------------------------------------------------------
/PrototopeOSX/PrototopeOSX.h:
--------------------------------------------------------------------------------
1 | //
2 | // PrototopeOSX.h
3 | // PrototopeOSX
4 | //
5 | // Created by Jason Brennan on 2015-07-12.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for PrototopeOSX.
12 | FOUNDATION_EXPORT double PrototopeOSXVersionNumber;
13 |
14 | //! Project version string for PrototopeOSX.
15 | FOUNDATION_EXPORT const unsigned char PrototopeOSXVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 | //#import
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 |
20 | # CocoaPods
21 | #
22 | # We recommend against adding the Pods directory to your .gitignore. However
23 | # you should judge for yourself, the pros and cons are mentioned at:
24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
25 | #
26 | # Pods/
27 |
28 | # JavaScript
29 | node_modules/
30 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/PrototopeJSBridge.h:
--------------------------------------------------------------------------------
1 | //
2 | // PrototopeJSBridge.h
3 | // PrototopeJSBridge
4 | //
5 | // Created by Andy Matuschak on 1/29/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for PrototopeJSBridge.
12 | FOUNDATION_EXPORT double PrototopeJSBridgeVersionNumber;
13 |
14 | //! Project version string for PrototopeJSBridge.
15 | FOUNDATION_EXPORT const unsigned char PrototopeJSBridgeVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/script/xcodebuild.awk:
--------------------------------------------------------------------------------
1 | # Exit statuses:
2 | #
3 | # 0 - No errors found.
4 | # 1 - Build or test failure. Errors will be logged automatically.
5 | # 2 - Untestable target. Retry with the "build" action.
6 |
7 | BEGIN {
8 | status = 0;
9 | }
10 |
11 | {
12 | print;
13 | fflush(stdout);
14 | }
15 |
16 | /is not valid for Testing/ {
17 | exit 2;
18 | }
19 |
20 | /[0-9]+: (error|warning):/ {
21 | errors = errors $0 "\n";
22 | }
23 |
24 | /(TEST|BUILD) FAILED/ {
25 | status = 1;
26 | }
27 |
28 | END {
29 | if (length(errors) > 0) {
30 | print "\n*** All errors:\n" errors;
31 | }
32 |
33 | fflush(stdout);
34 | exit status;
35 | }
36 |
--------------------------------------------------------------------------------
/Prototope/InputEvent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputEvent.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-08-17.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | /////////////////////////////////////////////
10 | // OS X Only
11 | /////////////////////////////////////////////
12 |
13 | import AppKit
14 |
15 | /* Represents input from a person, like pointer (mouse) or keyboard. */
16 | public struct InputEvent {
17 | let event: NSEvent
18 |
19 | public var globalLocation: Point {
20 | let rootView = Environment.currentEnvironment!.rootLayer.view
21 | return Point(rootView.convertPoint(event.locationInWindow, fromView: nil))
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Examples/TouchUnicornsSimpler/main.js:
--------------------------------------------------------------------------------
1 | function makeUnicornLayer() {
2 | const unicornLayer = new Layer({imageName: "unicorn"})
3 | unicornLayer.x = 400
4 | unicornLayer.y = 512
5 | return unicornLayer
6 | }
7 |
8 | function gimmeSparkle() {
9 | return new Layer({imageName: "star"})
10 | }
11 |
12 | Layer.root.backgroundColor = new Color({hex: "FF31A0"})
13 | const unicornLayer = makeUnicornLayer()
14 |
15 | Layer.root.gestures = [new PanGesture({handler: function(phase, centroidSequence) {
16 | const finger = centroidSequence.currentSample.globalLocation
17 | const sparkle = gimmeSparkle()
18 | sparkle.position = finger
19 | unicornLayer.position = finger
20 | unicornLayer.zPosition = 1
21 | }})]
22 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/GeometryBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class GeometryBridgeTests: JSBridgeTestCase {
14 | func testPointBridging() {
15 | XCTAssertEqual(context.evaluateScript("(new Point({x: 5, y: 10})).y").toDouble(), 10)
16 | XCTAssertEqual(context.evaluateScript("Point.zero.x").toDouble(), 0)
17 | XCTAssertEqual(context.evaluateScript("(new Point({x: 2, y: 3})).add(new Point({x: 5})).x").toDouble(), 7)
18 | XCTAssertEqual(context.evaluateScript("tunable({name: 'foo', default: 1.0})").toDouble(), 1)
19 | }
20 | }
--------------------------------------------------------------------------------
/PrototopeJSBridge/JSContext+BridgingExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSContext+BridgingExtensions.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import JavaScriptCore
10 |
11 | extension JSContext {
12 | func setFunctionForKey(key: String, fn: T) {
13 | // Some grossness is needed to persuade Swift to treat closures as objects.
14 | setObject(unsafeBitCast(fn, AnyObject.self), forKeyedSubscript: key)
15 | }
16 | }
17 |
18 | extension JSValue {
19 | func setFunctionForKey(key: String, fn: T) {
20 | // Some grossness is needed to persuade Swift to treat closures as objects.
21 | setObject(unsafeBitCast(fn, AnyObject.self), forKeyedSubscript: key)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/PrototopeTestApp/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "ipad",
5 | "size" : "29x29",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "ipad",
10 | "size" : "29x29",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "ipad",
15 | "size" : "40x40",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "ipad",
20 | "size" : "40x40",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "ipad",
25 | "size" : "76x76",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "ipad",
30 | "size" : "76x76",
31 | "scale" : "2x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/Examples/TouchUnicorns/main.js:
--------------------------------------------------------------------------------
1 | function makeUnicornLayer() {
2 | const unicornLayer = new Layer({imageName: "unicorn"})
3 | unicornLayer.x = 400
4 | unicornLayer.y = 512
5 | return unicornLayer
6 | }
7 |
8 | function gimmeSparkle(x, y) {
9 | const sparkleLayer = new Layer({imageName: "star"})
10 | sparkleLayer.x = x
11 | sparkleLayer.y = y
12 | return sparkleLayer
13 | }
14 |
15 | Layer.root.backgroundColor = new Color({hex: "FF31A0"})
16 | const unicornLayer = makeUnicornLayer()
17 |
18 | Layer.root.gestures = [new PanGesture({handler: function(phase, centroidSequence) {
19 | const finger = centroidSequence.currentSample.globalLocation
20 | gimmeSparkle(finger.x, finger.y)
21 |
22 | unicornLayer.x = finger.x
23 | unicornLayer.y = finger.y
24 | // or: unicornLayer.position = finger
25 | unicornLayer.zPosition = 1
26 | }})]
27 |
--------------------------------------------------------------------------------
/PrototopeTests/MathTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MathTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/16/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Prototope
10 | import XCTest
11 | import Foundation
12 |
13 | class MathTests: XCTestCase {
14 | func testInterpolate() {
15 | XCTAssertEqual(interpolate(from: 3, to: 9, at: 0.25), 4.5)
16 | XCTAssertEqual(interpolate(from: 10, to: 4, at: 0.5), 7)
17 | }
18 |
19 | func testMap() {
20 | XCTAssertEqual(map(4, fromInterval: (2, 8), toInterval: (1, 4)), 2)
21 | }
22 |
23 | func testClip() {
24 | var result: Double = clip(5, min: 3, max: 4)
25 | XCTAssertEqual(result, 4)
26 | result = clip(2, min: 3, max: 4)
27 | XCTAssertEqual(result, 3)
28 | result = clip(3.5, min: 3, max: 4)
29 | XCTAssertEqual(result, 3.5)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Protocaster/LogWindowController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogWindowController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/11/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class LogWindowController: NSWindowController {
12 | private var logViewController: LogViewController {
13 | return window!.contentViewController! as! LogViewController
14 | }
15 |
16 | func appendConsoleMessage(message: String) {
17 | logViewController.appendConsoleMessage(message)
18 | }
19 |
20 | func appendException(exception: String) {
21 | logViewController.appendException(exception)
22 | }
23 |
24 | func appendReloadMessage() {
25 | logViewController.appendReloadMessage()
26 | }
27 |
28 | func appendPrototypeChangedMessage(url: NSURL) {
29 | logViewController.appendPrototypeChangedMessage(url)
30 | }
31 | }
--------------------------------------------------------------------------------
/ProtocasterTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ProtoscopeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Prototope/Dictionary+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dictionary+Extensions.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 11/14/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | /** Creates a dictionary from an array of key-value pairs. */
10 | func dictionaryFromElements(elements: [(Key, Value)]) -> [Key: Value] {
11 | var dictionary = [Key: Value](minimumCapacity: elements.count)
12 | for (key, value) in elements {
13 | dictionary[key] = value
14 | }
15 |
16 | return dictionary
17 | }
18 |
19 | func +(var a: [Key: Value], b: [Key: Value]) -> [Key: Value] {
20 | for (k, v) in b {
21 | a[k] = v
22 | }
23 | return a
24 | }
25 |
26 | func -(var a: [Key: Value], b: [Key: Value]) -> [Key: Value] {
27 | for (k, _) in b {
28 | a.removeValueForKey(k)
29 | }
30 | return a
31 | }
32 |
--------------------------------------------------------------------------------
/PrototopeOSXTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/PrototopeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Protoscope/ExceptionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExceptionViewController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/11/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ExceptionViewController: UIViewController {
12 | var exception: String? {
13 | get { return exceptionView.exception }
14 | set { exceptionView.exception = newValue }
15 | }
16 |
17 | private var exceptionView: ExceptionView { return view as! ExceptionView }
18 |
19 | init() {
20 | super.init(nibName: nil, bundle: nil)
21 | }
22 |
23 | required init(coder aDecoder: NSCoder) {
24 | fatalError("init(coder:) has intentionally not been implemented")
25 | }
26 |
27 | override func loadView() {
28 | view = ExceptionView()
29 | }
30 |
31 | override func prefersStatusBarHidden() -> Bool {
32 | return true
33 | }
34 | }
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/MathBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MathBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 |
10 | import Foundation
11 | import Prototope
12 | import PrototopeJSBridge
13 | import XCTest
14 |
15 | class MathBridgeTests: JSBridgeTestCase {
16 | func testMathBridging() {
17 | XCTAssertEqual(context.evaluateScript("interpolate({from: 5, to: 10, at: 0.4})").toDouble(), interpolate(from: 5, to: 10, at: 0.4))
18 | XCTAssertEqual(context.evaluateScript("map({value: 0.3, fromInterval: [0, 1], toInterval: [0, 10]})").toDouble(), map(0.3, fromInterval: (0, 1), toInterval: (0, 10)))
19 |
20 | let clipResult: Double = clip(5, min: 1, max: 3)
21 | XCTAssertEqual(context.evaluateScript("clip({value: 5, min: 1, max: 3})").toDouble(), clipResult)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/HeartbeatBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeartbeatBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class HeartbeatBridgeTests: JSBridgeTestCase {
14 | func testHeartbeatBridging() {
15 | var expectation = expectationWithDescription("heartbeat")
16 | var heartbeatCount: Int = 0
17 | context.exceptionHandler = { heartbeatValue in
18 | heartbeatCount++
19 | if heartbeatCount >= 5 {
20 | heartbeatValue.invokeMethod("stop", withArguments: [])
21 | expectation.fulfill()
22 | }
23 | }
24 | context.evaluateScript("var h = new Heartbeat({handler: function(heartbeat) { throw heartbeat } })")
25 |
26 | waitForExpectationsWithTimeout(0.5, handler: nil)
27 |
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Examples/CameraLayer/main.js:
--------------------------------------------------------------------------------
1 | const cameraLayer = new CameraLayer()
2 | cameraLayer.width = Layer.root.width * 0.5
3 | cameraLayer.height = Layer.root.height * 0.5
4 | cameraLayer.cameraPosition = CameraPosition.Front
5 | cameraLayer.x = Layer.root.x
6 | cameraLayer.y = Layer.root.y
7 |
8 | const flipButton = new TextLayer()
9 | flipButton.fontName = "Futura"
10 | flipButton.fontSize = 30
11 | flipButton.text = "Flip"
12 | flipButton.x = Layer.root.x
13 | flipButton.y = 100
14 | flipButton.animators.alpha.springBounciness = 0
15 | flipButton.animators.alpha.springSpeed = 20
16 |
17 | Layer.root.touchBeganHandler = function() {
18 | flipButton.animators.alpha.target = 0.5
19 | }
20 | Layer.root.touchEndedHandler = function(touchSequence) {
21 | cameraLayer.cameraPosition = (cameraLayer.cameraPosition === CameraPosition.Front) ? CameraPosition.Back : CameraPosition.Front
22 | flipButton.animators.alpha.target = 1.0
23 | }
24 |
--------------------------------------------------------------------------------
/Prototope/Video.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Video.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-09.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 |
11 | /** Represents a video object. Can be any kind iOS natively supports. */
12 | public struct Video: CustomStringConvertible {
13 |
14 | let name: String
15 | let player: AVPlayer
16 |
17 | /** Initialize the video with a filename. The name must include the file extension. */
18 | public init?(name: String) {
19 | self.name = name
20 |
21 | if let URL = NSBundle.mainBundle().URLForResource(name, withExtension: nil) {
22 | self.player = AVPlayer(URL: URL)
23 | } else {
24 | Environment.currentEnvironment?.exceptionHandler("Video named \(name) not found")
25 | return nil
26 | }
27 | }
28 |
29 |
30 | public var description: String {
31 | return name
32 | }
33 | }
--------------------------------------------------------------------------------
/Prototope/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/VideoBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoBridge.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-10.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol VideoJSExport: JSExport {
14 | init?(args: NSDictionary)
15 | }
16 |
17 | @objc public class VideoBridge: NSObject, VideoJSExport, BridgeType {
18 | var video: Video!
19 |
20 | public class func addToContext(context: JSContext) {
21 | context.setObject(self, forKeyedSubscript: "Video")
22 | }
23 |
24 | required public init?(args: NSDictionary) {
25 | if let videoName = args["name"] as! String? {
26 | video = Video(name: videoName)
27 | super.init()
28 | } else {
29 | super.init()
30 | return nil
31 | }
32 | }
33 |
34 |
35 | public override var description: String {
36 | return video.description
37 | }
38 | }
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/TunableBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TunableBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class TunableBridgeTests: JSBridgeTestCase {
14 | func testTunableBridging() {
15 | XCTAssertEqual(context.evaluateScript("tunable({name: 'foo', default: 1.0})").toDouble(), 1)
16 | XCTAssertEqual(context.evaluateScript("tunable({name: 'bar', default: true})").toBool(), true)
17 | XCTAssertEqual(context.evaluateScript("var output = null; tunable({name: 'baz', default: 50, changeHandler: function (value) { output = value; }}); output").toDouble(), 50)
18 | XCTAssertEqual(context.evaluateScript("var output = null; tunable({name: 'bat', default: true, changeHandler: function (value) { output = value; }}); output").toBool(), true)
19 | }
20 | }
--------------------------------------------------------------------------------
/PrototopeJSBridge/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/script/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export SCRIPT_DIR=$(dirname "$0")
4 |
5 | ##
6 | ## Bootstrap Process
7 | ##
8 |
9 | main ()
10 | {
11 | local submodules=$(git submodule status)
12 | local result=$?
13 |
14 | if [ "$result" -ne "0" ]
15 | then
16 | exit $result
17 | fi
18 |
19 | if [ -n "$submodules" ]
20 | then
21 | echo "*** Updating submodules..."
22 | update_submodules
23 | fi
24 | }
25 |
26 | bootstrap_submodule ()
27 | {
28 | local bootstrap="script/bootstrap"
29 |
30 | if [ -e "$bootstrap" ]
31 | then
32 | echo "*** Bootstrapping $name..."
33 | "$bootstrap" >/dev/null
34 | else
35 | update_submodules
36 | fi
37 | }
38 |
39 | update_submodules ()
40 | {
41 | git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule
42 | }
43 |
44 | export -f bootstrap_submodule
45 | export -f update_submodules
46 |
47 | main
48 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/SpeechBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpeechBridge.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-17.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol SpeechJSExport: JSExport {
14 | }
15 |
16 | @objc public class SpeechBridge: NSObject, SpeechJSExport, BridgeType {
17 |
18 | public class func addToContext(context: JSContext) {
19 | context.setObject(self, forKeyedSubscript: "Speech")
20 | let speechBridge = context.objectForKeyedSubscript("Speech")
21 |
22 | speechBridge.setFunctionForKey("say", fn: sayTrampoline)
23 | }
24 |
25 | }
26 |
27 |
28 | let sayTrampoline: @convention(block) JSValue -> Void = { args in
29 | let text = args.valueForProperty("text")
30 |
31 | if !text.isUndefined {
32 | Speech.say(text.toString())
33 | }
34 |
35 | }
36 |
37 |
38 | let shhhTrampoline: @convention(block) JSValue -> Void = { args in
39 | Speech.shhh()
40 | }
--------------------------------------------------------------------------------
/PrototopeOSX/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSHumanReadableCopyright
24 | Copyright © 2015 Khan Academy. All rights reserved.
25 | NSPrincipalClass
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/TimingBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimingBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class TimingBridgeTests: JSBridgeTestCase {
14 | func testCurrentTimestampBridging() {
15 | let firstTimestamp = context.evaluateScript("Timestamp.currentTimestamp()").toDouble()
16 | let secondTimestamp = context.evaluateScript("Timestamp.currentTimestamp()").toDouble()
17 | XCTAssertGreaterThan(secondTimestamp, firstTimestamp)
18 | }
19 |
20 | func testAfterDurationBridging() {
21 | var output: Double = 0
22 | var expectation = expectationWithDescription("afterDuration")
23 | context.exceptionHandler = { value in
24 | output = value.toDouble()
25 | expectation.fulfill()
26 | }
27 | context.evaluateScript("afterDuration(0.25, function() { throw 3 })")
28 |
29 | waitForExpectationsWithTimeout(0.5, handler: nil)
30 | XCTAssertEqual(output, 3)
31 | }
32 | }
--------------------------------------------------------------------------------
/PrototopeJSBridge/TimingBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimingBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import JavaScriptCore
11 | import Prototope
12 |
13 | public class TimingBridge: BridgeType {
14 | public class func addToContext(context: JSContext) {
15 | let timestampObject = JSValue(newObjectInContext: context)
16 | context.setObject(timestampObject, forKeyedSubscript: "Timestamp")
17 |
18 | let currentTimestampFunction: @convention(block) Void -> Double = { return Prototope.Timestamp.currentTimestamp.nsTimeInterval }
19 | timestampObject.setFunctionForKey("currentTimestamp", fn: currentTimestampFunction)
20 |
21 | let afterDurationFunction: @convention(block) (Double, JSValue) -> Void = { duration, callable in
22 | Prototope.afterDuration(duration) {
23 | callable.callWithArguments([])
24 | return
25 | }
26 | }
27 | context.setFunctionForKey("afterDuration", fn: afterDurationFunction)
28 | }
29 | }
--------------------------------------------------------------------------------
/ProtoscopeTests/ProtoscopeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProtoscopeTests.swift
3 | // ProtoscopeTests
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import XCTest
11 |
12 | class ProtoscopeTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/ProtocasterTests/ProtocasterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProtocasterTests.swift
3 | // ProtocasterTests
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import XCTest
11 |
12 | class ProtocasterTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/PrototopeOSXTests/PrototopeOSXTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrototopeOSXTests.swift
3 | // PrototopeOSXTests
4 | //
5 | // Created by Jason Brennan on 2015-07-12.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import XCTest
11 |
12 | class PrototopeOSXTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | XCTAssert(true, "Pass")
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measureBlock() {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/script/LICENSE.md:
--------------------------------------------------------------------------------
1 | **Copyright (c) 2013 Justin Spahr-Summers**
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/VideoLayerBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoLayerBridge.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-10.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol VideoLayerJSExport: JSExport {
14 | init?(args: NSDictionary)
15 | func play()
16 | func pause()
17 | }
18 |
19 |
20 | @objc public class VideoLayerBridge: LayerBridge, VideoLayerJSExport {
21 | var videoLayer: VideoLayer { return layer as! VideoLayer }
22 |
23 | public override class func addToContext(context: JSContext) {
24 | context.setObject(self, forKeyedSubscript: "VideoLayer")
25 | }
26 |
27 | required public init?(args: NSDictionary) {
28 | let parentLayer = (args["parent"] as! LayerBridge?)?.layer
29 | let video = (args["video"] as! VideoBridge?)?.video
30 |
31 | let videoLayer = VideoLayer(parent: parentLayer, video: video)
32 | super.init(videoLayer)
33 |
34 | }
35 |
36 | public func play() {
37 | videoLayer.play()
38 | }
39 |
40 | public func pause() {
41 | videoLayer.pause()
42 | }
43 | }
--------------------------------------------------------------------------------
/PrototopeTests/GeometryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/16/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Prototope
10 | import XCTest
11 | import Foundation
12 |
13 | class PointTests: XCTestCase {
14 | func testDistanceToPoint() {
15 | XCTAssertEqualWithAccuracy(Point(x: 5, y: 5).distanceToPoint(Point(x: 10, y:10)), sqrt(2) * 5, 0.001)
16 | }
17 |
18 | func testLength() {
19 | XCTAssertEqual(Point(x: 3, y: 4).length, 5)
20 | }
21 | }
22 |
23 | class RectTests: XCTestCase {
24 | func testComputedLocations() {
25 | let testRect = Rect(x: 5, y: 10, width: 20, height: 30)
26 | XCTAssertEqual(testRect.minX, 5)
27 | XCTAssertEqual(testRect.midX, 15)
28 | XCTAssertEqual(testRect.maxX, 25)
29 | XCTAssertEqual(testRect.minY, 10)
30 | XCTAssertEqual(testRect.midY, 25)
31 | XCTAssertEqual(testRect.maxY, 40)
32 |
33 | XCTAssertEqual(testRect.center, Point(x: 15, y: 25))
34 | var updatedRect = testRect
35 | updatedRect.center += Point(x: 15, y: 10)
36 | XCTAssertEqual(updatedRect.center, Point(x: 30, y: 35))
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/ShadowBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShadowBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/2/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol ShadowJSExport: JSExport {
14 | init(args: NSDictionary)
15 | // TODO make mutable properties, but will need to notify layerbridge owner on changes (yuuuck)
16 | }
17 |
18 | @objc public class ShadowBridge: NSObject, ShadowJSExport, BridgeType {
19 | var shadow: Shadow
20 |
21 | public class func addToContext(context: JSContext) {
22 | context.setObject(self, forKeyedSubscript: "Shadow")
23 | }
24 |
25 | required public init(args: NSDictionary) {
26 | shadow = Shadow(
27 | color: (args["color"] as! ColorBridge?)?.color ?? Color.black,
28 | alpha: (args["alpha"] as! Double?) ?? 1.0,
29 | offset: (args["offset"] as! SizeBridge?)?.size ?? Size.zero,
30 | radius: (args["radius"] as! Double?) ?? 3.0
31 | )
32 | super.init()
33 | }
34 |
35 | init(_ shadow: Shadow) {
36 | self.shadow = shadow
37 | super.init()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Protocaster/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1
25 | LSMinimumSystemVersion
26 | $(MACOSX_DEPLOYMENT_TARGET)
27 | NSHumanReadableCopyright
28 | Copyright © 2015 Khan Academy. All rights reserved.
29 | NSMainStoryboardFile
30 | Main
31 | NSPrincipalClass
32 | NSApplication
33 |
34 |
35 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/BorderBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BorderBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/2/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol BorderJSExport: JSExport {
14 | init(args: NSDictionary)
15 | // TODO: make setters work again... will require notifying the owning layer bridge of a change. yuck.
16 | var color: ColorJSExport { get }
17 | var width: Double { get }
18 | }
19 |
20 | @objc public class BorderBridge: NSObject, BorderJSExport, BridgeType {
21 | var border: Border
22 |
23 | public class func addToContext(context: JSContext) {
24 | context.setObject(self, forKeyedSubscript: "Border")
25 | }
26 |
27 | required public init(args: NSDictionary) {
28 | border = Border(
29 | color: (args["color"] as! ColorBridge?)?.color ?? Color.black,
30 | width: (args["width"] as! Double?) ?? 0.0
31 | )
32 | super.init()
33 | }
34 |
35 | init(_ border: Border) {
36 | self.border = border
37 | super.init()
38 | }
39 |
40 | public var color: ColorJSExport {
41 | return ColorBridge(border.color)
42 | }
43 |
44 | public var width: Double {
45 | return border.width
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/SoundBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoundBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol SoundJSExport: JSExport {
14 | init?(args: NSDictionary)
15 | func play()
16 | func stop()
17 | var volume: Double { get set }
18 | }
19 |
20 | @objc public class SoundBridge: NSObject, SoundJSExport, BridgeType {
21 | var sound: Sound!
22 |
23 | public class func addToContext(context: JSContext) {
24 | context.setObject(self, forKeyedSubscript: "Sound")
25 | }
26 |
27 | required public init?(args: NSDictionary) {
28 | if let soundName = args["name"] as! String?, let sound = Sound(name: soundName) {
29 | self.sound = sound
30 | super.init()
31 | } else {
32 | super.init()
33 | return nil
34 | }
35 | }
36 |
37 | public override var description: String {
38 | return sound.description
39 | }
40 |
41 | public func play() {
42 | sound.play()
43 | }
44 |
45 | public func stop() {
46 | sound.stop()
47 | }
48 |
49 | /// From 0.0 to 1.0.
50 | public var volume: Double {
51 | get { return sound.volume }
52 | set { sound.volume = newValue }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Examples/TestText/main.js:
--------------------------------------------------------------------------------
1 | Layer.root.backgroundColor = new Color({hue: 0.5, saturation: 0.9, brightness: 0.4})
2 |
3 | const singleLineLayer = new TextLayer()
4 | singleLineLayer.text = "This is a single centered line"
5 | singleLineLayer.border = new Border({color: Color.red, width: 1})
6 | singleLineLayer.textColor = new Color({white: 0.85})
7 | singleLineLayer.fontName = "Optima"
8 | singleLineLayer.fontSize = 25
9 | singleLineLayer.x = Layer.root.x
10 | singleLineLayer.y = 30
11 |
12 |
13 | const wrappingLayer = new TextLayer()
14 | wrappingLayer.text = "Hello there and welcome to Prototope, where we wrap text for you if you want."
15 | wrappingLayer.textAlignment = TextAlignment.Right
16 | wrappingLayer.textColor = Color.white
17 | wrappingLayer.wraps = true
18 | wrappingLayer.border = new Border({color: Color.red, width: 1})
19 | wrappingLayer.x = Layer.root.x
20 | wrappingLayer.y = 300
21 | wrappingLayer.width = 100
22 |
23 | const alignmentLayer = new TextLayer()
24 | alignmentLayer.text = "align"
25 | alignmentLayer.fontSize = 64
26 | alignmentLayer.border = new Border({color: Color.red, width: 1})
27 |
28 | const h = new Heartbeat({handler: function(heartbeat) {
29 | wrappingLayer.width = 100 + Math.sin(heartbeat.timestamp * 4) * 50
30 | alignmentLayer.alignWithBaselineOf(wrappingLayer)
31 | alignmentLayer.x = wrappingLayer.x + wrappingLayer.width*0.5 + alignmentLayer.width*0.5
32 |
33 | }})
34 |
--------------------------------------------------------------------------------
/Protoscope/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Prototope/Timing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Timing.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/8/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import QuartzCore
10 |
11 | /** Represents an interval between two times. */
12 | public typealias TimeInterval = NSTimeInterval
13 |
14 | /** Represents an instant in time. */
15 | public struct Timestamp: Comparable, Hashable {
16 | public let nsTimeInterval: NSTimeInterval
17 |
18 | public static var currentTimestamp: Timestamp {
19 | return Timestamp(CACurrentMediaTime())
20 | }
21 |
22 | public init(_ nsTimeInterval: NSTimeInterval) {
23 | self.nsTimeInterval = nsTimeInterval
24 | }
25 |
26 | public var hashValue: Int {
27 | return nsTimeInterval.hashValue
28 | }
29 | }
30 |
31 | public func <(a: Timestamp, b: Timestamp) -> Bool {
32 | return a.nsTimeInterval < b.nsTimeInterval
33 | }
34 |
35 | public func ==(a: Timestamp, b: Timestamp) -> Bool {
36 | return a.nsTimeInterval == b.nsTimeInterval
37 | }
38 |
39 | public func -(a: Timestamp, b: Timestamp) -> TimeInterval {
40 | return a.nsTimeInterval - b.nsTimeInterval
41 | }
42 |
43 |
44 | // MARK: -
45 |
46 | /** Performs an action after a duration. */
47 | public func afterDuration(duration: TimeInterval, action: () -> Void) {
48 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(duration * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), action)
49 | }
50 |
--------------------------------------------------------------------------------
/PrototopeTests/ExceptionHandlingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExceptionHandlingTests.swift
3 | // Prototope
4 | //
5 | // Created by Saniul Ahmed on 05/03/2015.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Prototope
10 | import XCTest
11 | import Foundation
12 |
13 | class ExceptionHandlingTests: XCTestCase {
14 |
15 | var exceptionHandled = false
16 |
17 | override func setUp() {
18 | let aView = UIView()
19 |
20 | let env = Environment(rootView: aView, imageProvider: { _ in return nil }, soundProvider: { _ in return nil }, fontProvider: { _ in return nil }, exceptionHandler: { _ in self.exceptionHandled = true })
21 |
22 | Environment.runWithEnvironment(env) {
23 | }
24 |
25 | super.setUp()
26 | }
27 |
28 | override func tearDown() {
29 | super.tearDown()
30 |
31 | self.exceptionHandled = false
32 | }
33 |
34 | func testNonexistentImage() {
35 | let img = Image(name: "nonexistentImage")
36 | XCTAssertTrue(exceptionHandled)
37 | }
38 |
39 | func testNonexistentSound() {
40 | let img = Sound(name: "nonexistentSound")
41 | XCTAssertTrue(exceptionHandled)
42 | }
43 |
44 | func testNonexistentVideo() {
45 | let img = Video(name: "nonexistentVideo")
46 | XCTAssertTrue(exceptionHandled)
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/PrototopeTestApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations~ipad
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationPortraitUpsideDown
37 | UIInterfaceOrientationLandscapeLeft
38 | UIInterfaceOrientationLandscapeRight
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Examples/Hello Color/Hello Color.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Adapted from P.1.0 "Hello, Color" in "Generative Design" by Bohnacker et al.
4 | built on Prototope@7b5d29cf6
5 |
6 | */
7 |
8 | const rect = new Layer()
9 | rect.position = Layer.root.position
10 | rect.width = rect.height = 100
11 | rect.backgroundColor = Color.black
12 |
13 | Layer.root.animators.backgroundColor.springSpeed = rect.animators.backgroundColor.springSpeed = 40
14 | Layer.root.animators.backgroundColor.springBounciness = rect.animators.backgroundColor.springBounciness = 0
15 |
16 | rect.animators.bounds.springSpeed = 15
17 | rect.animators.bounds.springBounciness = 3
18 |
19 | Layer.root.touchBeganHandler = Layer.root.touchMovedHandler = Layer.root.touchEndedHandler = function(sequence) {
20 | const hue = sequence.currentSample.globalLocation.y / Layer.root.height
21 | Layer.root.animators.backgroundColor.target = new Color({hue: hue, saturation: 1.0, brightness: 1.0})
22 | rect.animators.backgroundColor.target = new Color({hue: (1.0 - hue), saturation: 1.0, brightness: 1.0})
23 |
24 | const size = Math.abs(sequence.currentSample.globalLocation.x - Layer.root.x) * 2.0
25 | rect.animators.bounds.target = new Rect({x: 0, y: 0, width: size, height: size})
26 | }
27 |
28 | Layer.root.touchEndedHandler = function() {
29 | Layer.root.animators.backgroundColor.target = Color.white
30 | rect.animators.backgroundColor.target = Color.black
31 | rect.animators.bounds.target = new Rect({x: 0, y: 0, width: 100, height: 100})
32 | }
33 |
--------------------------------------------------------------------------------
/Examples/TouchAnimators/main.js:
--------------------------------------------------------------------------------
1 | function gimmeSquare(x) {
2 | // return a rounded white square at some x value (defaults to 324)
3 |
4 | var square = new Layer()
5 | square.width = 100
6 | square.height = 100
7 | square.backgroundColor = Color.white
8 | square.cornerRadius = 5
9 | square.x = x
10 | square.y = 512
11 | return square
12 | }
13 |
14 | function makeSpinnyLayer() {
15 | // touching this layer will rotate it 90 degrees to the right
16 |
17 | var spinnyLayer = gimmeSquare(324);
18 | spinnyLayer.touchBeganHandler = function() {
19 | Layer.animate({
20 | duration: 0.35,
21 | curve: AnimationCurve.EaseInOut,
22 | animations: function() {
23 | spinnyLayer.rotationDegrees = 90
24 | }
25 | })
26 | }
27 |
28 | spinnyLayer.touchEndedHandler = function() {
29 | Layer.animate({
30 | duration: 0.35,
31 | curve: AnimationCurve.EaseInOut,
32 | animations: function() {
33 | spinnyLayer.rotationDegrees = 0
34 | }
35 | })
36 | }
37 | }
38 |
39 | function makeNeedyLayer() {
40 | var needyLayer = gimmeSquare(444)
41 | needyLayer.touchBeganHandler = function() {
42 | needyLayer.animators.rotationRadians.target = 1.57
43 | needyLayer.animators.rotationRadians.springBounciness = 6.0
44 | }
45 |
46 | // letting go restores the values
47 | needyLayer.touchEndedHandler = function() {
48 | needyLayer.animators.rotationRadians.target = 0
49 | }
50 | }
51 |
52 | Layer.root.backgroundColor = new Color({hex: "535F55"})
53 | makeSpinnyLayer()
54 | makeNeedyLayer()
--------------------------------------------------------------------------------
/PrototopeJSBridge/ImageBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/2/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol ImageJSExport: JSExport {
14 | init?(args: NSDictionary)
15 | }
16 |
17 | @objc public class ImageBridge: NSObject, ImageJSExport, BridgeType {
18 | var image: Image!
19 |
20 | public class func addToContext(context: JSContext) {
21 | context.setObject(self, forKeyedSubscript: "Image")
22 | }
23 |
24 | required public init?(args: NSDictionary) {
25 | if let imageName = args["name"] as! String? {
26 | image = Image(name: imageName)
27 | super.init()
28 | } else if let text = args["text"] as! String? {
29 |
30 | // TODO(jb): Replace this with a FontBridge type.
31 | let fontName = (args["fontName"] as! String?) ?? "Helvetica"
32 | let fontSize = (args["fontSize"] as! Double?) ?? 18
33 | let font = SystemFont(name: fontName, size: CGFloat(fontSize))
34 |
35 | let textColor = (args["textColor"] as! ColorBridge).color ?? Color.black
36 |
37 | image = Image(text: text, font: font!, textColor: textColor)
38 |
39 | super.init()
40 | } else {
41 | super.init()
42 | return nil
43 | }
44 | }
45 |
46 | init(_ image: Image) {
47 | self.image = image
48 | super.init()
49 | }
50 |
51 |
52 | public override var description: String {
53 | return image.description
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/HeartbeatBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HeartbeatBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol HeartbeatJSExport: JSExport {
14 | init?(args: JSValue)
15 | var paused: Bool { get set }
16 | var timestamp: Double { get }
17 | func stop()
18 | }
19 |
20 | @objc public class HeartbeatBridge: NSObject, HeartbeatJSExport, BridgeType {
21 | var heartbeat: Heartbeat!
22 |
23 | public class func addToContext(context: JSContext) {
24 | context.setObject(self, forKeyedSubscript: "Heartbeat")
25 | }
26 |
27 | required public init?(args: JSValue) {
28 | super.init()
29 | let handler = args.objectForKeyedSubscript("handler")
30 | if !handler.isUndefined {
31 | heartbeat = Heartbeat { [weak self] heartbeat in
32 | if let strongSelf = self {
33 | handler.callWithArguments([strongSelf])
34 | }
35 | }
36 | JSContext.currentContext().virtualMachine.addManagedReference(self, withOwner: self)
37 | } else {
38 | return nil
39 | }
40 | }
41 |
42 | public var paused: Bool {
43 | get { return heartbeat.paused }
44 | set { heartbeat.paused = paused }
45 | }
46 |
47 | public var timestamp: Double {
48 | return heartbeat.timestamp.nsTimeInterval
49 | }
50 |
51 | public func stop() {
52 | heartbeat.stop()
53 | JSContext.currentContext().virtualMachine.removeManagedReference(self, withOwner: self)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Prototope/VideoLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoLayer.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-09.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import UIKit
11 | #else
12 | import AppKit
13 | #endif
14 | import AVFoundation
15 |
16 | /** This layer can play a video object. */
17 | public class VideoLayer: Layer {
18 |
19 | /** The layer's current video. */
20 | var video: Video? {
21 | didSet {
22 | if let video = video {
23 | self.playerLayer.player = video.player
24 | }
25 | }
26 | }
27 |
28 |
29 | private var playerLayer: AVPlayerLayer {
30 | return (self.view as! VideoView).layer as! AVPlayerLayer
31 | }
32 |
33 | /** Creates a video layer with the given video. */
34 | public init(parent: Layer? = nil, video: Video?) {
35 | self.video = video
36 |
37 | super.init(parent: parent, name: video?.name, viewClass: VideoView.self)
38 | if let video = video {
39 | self.playerLayer.player = video.player
40 | }
41 | }
42 |
43 |
44 | /** Play the video. */
45 | public func play() {
46 | self.video?.player.play()
47 | }
48 |
49 |
50 | /** Pause the video. */
51 | public func pause() {
52 | self.video?.player.pause()
53 | }
54 |
55 |
56 | /** Underlying video view class. */
57 | private class VideoView: SystemView {
58 | #if os(iOS)
59 | override class func layerClass() -> AnyClass {
60 | return AVPlayerLayer.self
61 | }
62 | #else
63 | override func makeBackingLayer() -> CALayer {
64 | return AVPlayerLayer()
65 | }
66 | #endif
67 | }
68 | }
69 |
70 |
71 |
--------------------------------------------------------------------------------
/Protocaster/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "16x16",
5 | "idiom" : "mac",
6 | "filename" : "protoro-16.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "16x16",
11 | "idiom" : "mac",
12 | "filename" : "protoro-32-1.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "32x32",
17 | "idiom" : "mac",
18 | "filename" : "protoro-32.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "32x32",
23 | "idiom" : "mac",
24 | "filename" : "protoro-64.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "128x128",
29 | "idiom" : "mac",
30 | "filename" : "protoro-128.png",
31 | "scale" : "1x"
32 | },
33 | {
34 | "size" : "128x128",
35 | "idiom" : "mac",
36 | "filename" : "protoro-256-1.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "256x256",
41 | "idiom" : "mac",
42 | "filename" : "protoro-256.png",
43 | "scale" : "1x"
44 | },
45 | {
46 | "size" : "256x256",
47 | "idiom" : "mac",
48 | "filename" : "protoro-512.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "512x512",
53 | "idiom" : "mac",
54 | "filename" : "protoro-512-1.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "512x512",
59 | "idiom" : "mac",
60 | "filename" : "protoro-1024.png",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/Examples/Color Puddles/main.js:
--------------------------------------------------------------------------------
1 | Layer.root.backgroundColor = new Color({hue: 0.5, saturation: 0.8, brightness: 0.3})
2 |
3 | // TODO: you currently can't attach touch handlers to the root layer. oops.
4 | const touchLayer = new Layer()
5 | touchLayer.frame = Layer.root.bounds
6 |
7 | const touchesToLayers = new Map()
8 |
9 | let z = 0
10 | touchLayer.touchBeganHandler = function(touchSequence) {
11 | const touchCircleLayer = new Layer()
12 | touchCircleLayer.position = touchSequence.currentSample.globalLocation
13 | touchCircleLayer.width = touchCircleLayer.height = 125
14 | touchCircleLayer.cornerRadius = touchCircleLayer.width / 2.0
15 | touchCircleLayer.backgroundColor = new Color({hue: Math.random(), saturation: 0.8, brightness: 1.0})
16 | touchesToLayers.set(touchSequence.id, touchCircleLayer)
17 | touchCircleLayer.zPosition = z
18 | touchCircleLayer.userInteractionEnabled = false
19 | z += 1
20 | }
21 |
22 | touchLayer.touchMovedHandler = function(touchSequence) {
23 | touchesToLayers.get(touchSequence.id).position = touchSequence.currentSample.globalLocation
24 | }
25 |
26 | touchLayer.touchEndedHandler = touchLayer.touchCancelledHandler = function(touchSequence) {
27 | const layer = touchesToLayers.get(touchSequence.id)
28 | touchesToLayers.delete(touchSequence.id)
29 |
30 | layer.animators.scale.target = new Point({x: 0, y: 0})
31 | layer.animators.scale.springBounciness = 3
32 | layer.animators.scale.springSpeed = 30
33 | layer.animators.scale.completionHandler = () => { layer.parent = undefined }
34 | }
35 |
36 | const h = new Heartbeat({handler: function() {
37 | for (let layer of touchesToLayers.values()) {
38 | layer.scale *= 1.11
39 | }
40 | }})
41 |
--------------------------------------------------------------------------------
/Examples/Animated Gif/AnimatedGif.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | AnimatedGif
4 |
5 | Simple support for frame-by-frame animations
6 | Generated through a series of images
7 |
8 | When not animating, blink randomly
9 |
10 | */
11 |
12 | Layer.root.backgroundColor = new Color({hex:"150728"})
13 |
14 | const touchCatchingLayer = new Layer()
15 | touchCatchingLayer.frame = Layer.root.bounds
16 |
17 | const starterFps = 60
18 | const numFirstFrame = 1
19 | const numLastFrame = 62
20 | let currentFrame = numFirstFrame
21 | const strFilename = "yayfez-"
22 |
23 | const firstFrameName = strFilename + numFirstFrame
24 | const gifLayer = new Layer({imageName: firstFrameName})
25 | gifLayer.y = Layer.root.height - gifLayer.height/2
26 | gifLayer.x = Layer.root.x
27 |
28 | // start looping on touch
29 | touchCatchingLayer.touchBeganHandler = function(touchSequence) {
30 | touchCatchingLayer.behaviors = [playThroughImages]
31 | }
32 |
33 | // speed and slow animation depending on where the touch is in x
34 | touchCatchingLayer.touchMovedHandler = function(touchSequence) {
35 |
36 | }
37 |
38 | // stop looping when touch has ended
39 | touchCatchingLayer.touchEndedHandler = touchCatchingLayer.touchCancelledHandler = function(touchSequence) {
40 |
41 | }
42 |
43 | const playThroughImages = new ActionBehavior({handler:function(layer) {
44 | const nextFrameName = strFilename + currentFrame
45 | gifLayer.image = new Image({name:nextFrameName})
46 | if (currentFrame + 1 > numLastFrame) {
47 | currentFrame = numFirstFrame
48 | touchCatchingLayer.behaviors = []
49 | }
50 | else {
51 | currentFrame++
52 | }
53 |
54 | }
55 | })
56 |
57 |
58 |
59 | function playThroughImagesWithSpeed(fps) {
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/PrototopeTestApp/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Protoscope/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | org.khanacademy.$(PRODUCT_NAME:rfc1034identifier)$(BUNDLE_ID_SUFFIX)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UIStatusBarHidden
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 | UIInterfaceOrientationPortraitUpsideDown
39 |
40 | UISupportedInterfaceOrientations~ipad
41 |
42 | UIInterfaceOrientationPortrait
43 | UIInterfaceOrientationPortraitUpsideDown
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Protoscope/ExceptionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExceptionView.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/10/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ExceptionView: UIView {
12 |
13 | var exception: String? {
14 | get { return exceptionTextView.text }
15 | set { exceptionTextView.text = newValue }
16 | }
17 |
18 | private let protonopeLabel: UILabel = {
19 | let label = UILabel()
20 | label.font = UIFont(name: "Futura", size: 16)
21 | label.textColor = UIColor.whiteColor()
22 | label.text = "protonope!"
23 | return label
24 | }()
25 |
26 | private let exceptionTextView: UITextView = {
27 | let textView = UITextView()
28 | textView.font = UIFont(name: "Menlo-Regular", size: 14)
29 | textView.textColor = UIColor.whiteColor()
30 | textView.backgroundColor = UIColor.clearColor()
31 | textView.textContainerInset = UIEdgeInsets()
32 | textView.textContainer.lineFragmentPadding = 0
33 | textView.editable = false
34 | return textView
35 | }()
36 |
37 | init() {
38 | super.init(frame: CGRect())
39 | backgroundColor = Style.warning
40 |
41 | addSubview(exceptionTextView)
42 | addSubview(protonopeLabel)
43 | }
44 |
45 | override func layoutSubviews() {
46 | let insetBounds = CGRectInset(bounds, 20, 20)
47 |
48 | protonopeLabel.sizeToFit()
49 | protonopeLabel.frame.origin = insetBounds.origin
50 |
51 | exceptionTextView.frame = insetBounds
52 | exceptionTextView.frame.origin.y = protonopeLabel.frame.maxY + 20
53 | exceptionTextView.frame.size.height -= exceptionTextView.frame.origin.y
54 |
55 | super.layoutSubviews()
56 | }
57 |
58 | required init(coder aDecoder: NSCoder) {
59 | fatalError("init(coder:) has intentionally not been implemented")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Protoscope/StatusView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatusView.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/7/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class StatusView: UIView {
12 | private let prototopeP: UIImageView = {
13 | let icon = UIImage(named: "PrototopeP")
14 | let iconView = UIImageView(image: icon)
15 | return iconView
16 | }()
17 |
18 | private let statusLabel: UILabel = {
19 | let label = UILabel()
20 | label.font = UIFont(name: "Futura", size: 20)
21 | label.textColor = UIColor.whiteColor()
22 | return label
23 | }()
24 |
25 | func breatheLogo() {
26 | // make the logo pulse, as if it were breathing, while it waits to connect to something
27 | UIView.animateKeyframesWithDuration(4.42, delay: 1.67, options: .Repeat | .Autoreverse | .CalculationModeCubicPaced, animations: {
28 | UIView.addKeyframeWithRelativeStartTime(0.15, relativeDuration: 0.15, animations: { self.prototopeP.alpha = 0})
29 | UIView.addKeyframeWithRelativeStartTime(0.85, relativeDuration: 0.15, animations: { self.prototopeP.alpha = 1})
30 | }, completion: nil)
31 | }
32 |
33 | init() {
34 | super.init(frame: CGRect())
35 | backgroundColor = Style.cyan
36 | addSubview(prototopeP)
37 | addSubview(statusLabel)
38 | breatheLogo()
39 |
40 | statusLabel.text = "waiting for protorope…"
41 | }
42 |
43 | override func layoutSubviews() {
44 | prototopeP.sizeToFit()
45 | prototopeP.center.x = bounds.midX
46 | prototopeP.center.y = bounds.midY - 90
47 |
48 | statusLabel.sizeToFit()
49 | statusLabel.center.x = bounds.midX
50 | statusLabel.center.y = bounds.midY + 30
51 | }
52 |
53 | required init(coder aDecoder: NSCoder) {
54 | fatalError("init(coder:) has intentionally not been implemented")
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Protocaster/LogViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LogViewController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/11/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | class LogViewController: NSViewController {
12 |
13 | func appendConsoleMessage(message: String) {
14 | appendMessage(message, attributes: [:])
15 | }
16 |
17 | func appendException(exception: String) {
18 | appendMessage(
19 | exception,
20 | attributes: [
21 | NSForegroundColorAttributeName: NSColor.whiteColor(),
22 | NSBackgroundColorAttributeName: NSColor(red: 233.0/255.0, green: 151.0/255.0, blue:17.0/255.0, alpha: 1.0)
23 | ]
24 | )
25 | }
26 |
27 | func appendReloadMessage() {
28 | appendMessage(
29 | "(prototype reloaded)",
30 | attributes: [
31 | NSForegroundColorAttributeName: NSColor.lightGrayColor(),
32 | ]
33 | )
34 | }
35 |
36 | func appendPrototypeChangedMessage(url: NSURL) {
37 | appendMessage(
38 | "(switching prototype to \(url.filePathURL!.path!))",
39 | attributes: [
40 | NSForegroundColorAttributeName: NSColor.lightGrayColor(),
41 | ]
42 | )
43 | }
44 |
45 | private func appendMessage(message: String, var attributes: [String: AnyObject]) {
46 | attributes[NSFontAttributeName] = LogViewController.font
47 | logTextView.textStorage!.appendAttributedString(NSAttributedString(
48 | string: "\(message)\n",
49 | attributes: attributes
50 | ))
51 |
52 | logTextView.moveToEndOfDocument(nil)
53 | }
54 |
55 | @IBAction func clear(sender: AnyObject) {
56 | logTextView.textStorage!.deleteCharactersInRange(NSMakeRange(0, logTextView.textStorage!.length))
57 | }
58 |
59 | @IBOutlet var logTextView: NSTextView!
60 |
61 | private static var font = NSFont(name: "Menlo", size: 14)!
62 | }
63 |
--------------------------------------------------------------------------------
/Examples/Behaviors/main.js:
--------------------------------------------------------------------------------
1 | function gimmeSquare(x) {
2 | // return a rounded white square at some x value (defaults to 324)
3 |
4 | const square = new Layer()
5 | square.width = 100
6 | square.height = 100
7 | square.backgroundColor = Color.white
8 | square.cornerRadius = 5
9 | square.x = x
10 | square.y = 512
11 | return square
12 | }
13 |
14 | function makeBreathingLayer() {
15 | const spinnyLayer = gimmeSquare(324);
16 | let t = 0
17 | const behavior = new ActionBehavior({handler:function(layer) {
18 | const scale = 1+Math.cos(t)
19 | t = t+0.09
20 | layer.scale = scale
21 | }});
22 | spinnyLayer.behaviors = [behavior];
23 |
24 | return spinnyLayer
25 | }
26 |
27 | Layer.root.backgroundColor = new Color({hex: "FF5F55"})
28 | const breathingLayer = makeBreathingLayer()
29 |
30 | const square = gimmeSquare(75)
31 | square.gestures = [
32 | new PanGesture({
33 | handler: function(phase, sequence) {
34 | if (sequence.previousSample !== undefined) {
35 | const current = sequence.currentSample.globalLocation
36 | const previous = sequence.previousSample.globalLocation
37 | square.position = square.position.add(current.subtract(previous))
38 | }
39 | }
40 | })
41 | ]
42 |
43 | square.behaviors = [
44 | new CollisionBehavior({
45 | with: breathingLayer,
46 | handler: function(kind) {
47 | if (kind == CollisionBehaviorKind.Entering) {
48 | square.animators.backgroundColor.target = Color.yellow
49 | } else if (kind == CollisionBehaviorKind.Leaving) {
50 | square.animators.backgroundColor.target = Color.white
51 | }
52 | }
53 | }),
54 | ]
55 |
--------------------------------------------------------------------------------
/Prototope/Heartbeat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Heartbeat.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 11/19/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import QuartzCore
10 |
11 | /** Allows you to run code once for every frame the display will render. */
12 | public class Heartbeat {
13 | /** The heartbeat's handler won't be called when paused is true. Defaults to false. */
14 | public var paused: Bool {
15 | get { return displayLink.paused }
16 | set { displayLink.paused = newValue }
17 | }
18 |
19 | /** The current timestamp of the heartbeat. Only valid to call from the handler block. */
20 | public var timestamp: Timestamp {
21 | return Timestamp(displayLink.timestamp)
22 | }
23 |
24 | /** The handler will be invoked for every frame to be rendered. It will be passed the
25 | Heartbeat instance initialized by this constructor (which permits you to access its
26 | properties from within the closure). */
27 | public init(handler: Heartbeat -> ()) {
28 | self.handler = handler
29 | #if os(iOS)
30 | displayLink = SystemDisplayLink(target: self, selector: "handleDisplayLink:")
31 | #else
32 | displayLink = SystemDisplayLink(heartbeatCallback: handleDisplayLink)
33 | #endif
34 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
35 | }
36 |
37 | /** Permanently stops the heartbeat. */
38 | public func stop() {
39 | displayLink.invalidate()
40 | }
41 |
42 | // MARK: Private interfaces
43 |
44 | private let handler: Heartbeat -> ()
45 | private var displayLink: SystemDisplayLink!
46 |
47 | @objc private func handleDisplayLink(sender: SystemDisplayLink) {
48 | precondition(displayLink === sender)
49 | handler(self)
50 | }
51 | }
52 |
53 |
54 | #if os(iOS)
55 | import UIKit
56 | typealias SystemDisplayLink = CADisplayLink
57 | #else
58 | import AppKit
59 | typealias SystemDisplayLink = DisplayLink
60 | #endif
61 |
62 |
--------------------------------------------------------------------------------
/Prototope/Speech.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Speech.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-17.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 |
11 | /** Speak text using the built in system text-to-speech system. */
12 | public struct Speech {
13 |
14 | #if os(iOS)
15 | private let synthesizer = AVSpeechSynthesizer()
16 | #else
17 | private let synthesizer = NSSpeechSynthesizer()
18 | #endif
19 |
20 |
21 | private static var speech: Speech {
22 | struct InnerVoice {
23 | static let instance = Speech()
24 | }
25 |
26 | return InnerVoice.instance
27 | }
28 |
29 |
30 | /** Speak the given text with the default system voice. Optionally, specify a speech rate between 0 and 1.
31 | Multiple calls to this queue up, so texts are read one after another until done. */
32 | public static func say(text: String, rate: Float = 0.2) {
33 | let speaker = Speech.speech.synthesizer
34 | speaker.say(text, atRate: rate)
35 | }
36 |
37 |
38 | /** Hush the speech synthesizer at the end of the next word. */
39 | public static func shhh() {
40 | let speaker = Speech.speech.synthesizer
41 |
42 | }
43 | }
44 |
45 |
46 | protocol Synthesizer {
47 | init()
48 | func say(text: String, atRate: Float)
49 | func shhh()
50 | }
51 |
52 |
53 | #if os(iOS)
54 | extension AVSpeechSynthesizer: Synthesizer {
55 | func say(text: String, atRate rate: Float) {
56 | let utterance = AVSpeechUtterance(string: text)
57 | utterance.rate = rate
58 |
59 | self.speakUtterance(utterance)
60 | }
61 |
62 | func shhh() {
63 | self.stopSpeakingAtBoundary(.Word)
64 | }
65 | }
66 |
67 | #else
68 | extension NSSpeechSynthesizer: Synthesizer {
69 | func say(text: String, atRate rate: Float) {
70 | self.rate = rate
71 | self.startSpeakingString(text)
72 | }
73 |
74 | func shhh() {
75 | self.stopSpeakingAtBoundary(.WordBoundary)
76 | }
77 | }
78 | #endif
79 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/MathBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MathBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | public struct MathBridge: BridgeType {
14 | public static func addToContext(context: JSContext) {
15 | let interpolateTrampoline: @convention(block) NSDictionary -> Double = { args in
16 | Prototope.interpolate(
17 | from: (args["from"] as! Double?) ?? 0,
18 | to: (args["to"] as! Double?) ?? 0,
19 | at: (args["at"] as! Double?) ?? 0
20 | )
21 | }
22 | context.setFunctionForKey("interpolate", fn: interpolateTrampoline)
23 |
24 | let mapTrampoline: @convention(block) NSDictionary -> Double = { args in
25 | let fromInterval = (args["fromInterval"] as! [Double]?) ?? [0, 0]
26 | let toInterval = (args["toInterval"] as! [Double]?) ?? [0, 0]
27 | return Prototope.map(
28 | (args["value"] as! Double?) ?? 0,
29 | fromInterval: (fromInterval[0], fromInterval[1]),
30 | toInterval: (toInterval[0], toInterval[1])
31 | )
32 | }
33 | context.setFunctionForKey("map", fn: mapTrampoline)
34 |
35 | let clipTrampoline: @convention(block) NSDictionary -> Double = { args in
36 | Prototope.clip(
37 | (args["value"] as! Double?) ?? 0,
38 | min: (args["min"] as! Double?) ?? 0,
39 | max: (args["max"] as! Double?) ?? 0
40 | )
41 | }
42 | context.setFunctionForKey("clip", fn: clipTrampoline)
43 |
44 | let pixelAwareCeil: @convention(block) NSDictionary -> Double = { args in
45 | Prototope.pixelAwareCeil(
46 | (args["value"] as! Double?) ?? 0
47 | )
48 | }
49 | context.setFunctionForKey("pixelAwareCeil", fn: pixelAwareCeil)
50 |
51 | let pixelAwareFloor: @convention(block) NSDictionary -> Double = { args in
52 | Prototope.pixelAwareFloor(
53 | (args["value"] as! Double?) ?? 0
54 | )
55 | }
56 | context.setFunctionForKey("pixelAwareFloor", fn: pixelAwareFloor)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Protocaster/ProtoscopeScanner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProtoscopeScanner.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class ProtoscopeScanner {
12 | private let browser = NSNetServiceBrowser()
13 | private var browserDelegate: BrowserDelegate!
14 | private(set) var services: [NSNetService] = []
15 |
16 | init(serviceDidAppearHandler: NSNetService -> () = {_ in return}, serviceDidDisappearHandler: NSNetService -> () = {_ in return}) {
17 | browserDelegate = BrowserDelegate(
18 | serviceDidAppearHandler: { [unowned self] service in
19 | self.services.append(service)
20 | serviceDidAppearHandler(service)
21 | },
22 | serviceDidDisappearHandler: { [unowned self] service in
23 | self.services = self.services.filter { $0 !== service }
24 | serviceDidDisappearHandler(service)
25 | }
26 | )
27 | browser.delegate = browserDelegate
28 | browser.searchForServicesOfType(ProtoropeReceiverServiceType, inDomain: "")
29 | }
30 |
31 | func stop() {
32 | browser.delegate = nil
33 | browser.stop()
34 | }
35 |
36 | deinit {
37 | stop()
38 | }
39 |
40 | @objc private class BrowserDelegate: NSObject, NSNetServiceBrowserDelegate {
41 | let serviceDidAppearHandler: NSNetService -> ()
42 | let serviceDidDisappearHandler: NSNetService -> ()
43 |
44 | init(serviceDidAppearHandler: NSNetService -> (), serviceDidDisappearHandler: NSNetService -> ()) {
45 | self.serviceDidAppearHandler = serviceDidAppearHandler
46 | self.serviceDidDisappearHandler = serviceDidDisappearHandler
47 | }
48 |
49 | @objc private func netServiceBrowser(aNetServiceBrowser: NSNetServiceBrowser, didFindService aNetService: NSNetService, moreComing: Bool) {
50 | serviceDidAppearHandler(aNetService)
51 | }
52 |
53 | @objc private func netServiceBrowser(aNetServiceBrowser: NSNetServiceBrowser, didRemoveService aNetService: NSNetService, moreComing: Bool) {
54 | serviceDidDisappearHandler(aNetService)
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/Protoscope/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Protoscope
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Prototope
11 | import PrototopeJSBridge
12 |
13 | @UIApplicationMain
14 | class AppDelegate: UIResponder, UIApplicationDelegate {
15 | var window: UIWindow?
16 |
17 | var rootViewController: RootViewController!
18 | var server: ProtoscopeServer!
19 | var sessionInteractor: SessionInteractor!
20 |
21 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
22 |
23 | application.idleTimerDisabled = true
24 |
25 | rootViewController = RootViewController()
26 | window = UIWindow(frame: UIScreen.mainScreen().bounds)
27 | window!.rootViewController = rootViewController
28 | window!.makeKeyAndVisible()
29 |
30 |
31 | //-----------------------------------------------------------------------
32 | // Enable "phony finger" touch dots to show, useful for screen recordings
33 | //
34 |
35 | // Prototope.Screen.touchDotsEnabled = true
36 |
37 | self.sessionInteractor = SessionInteractor(
38 | exceptionHandler: { [weak self] exception in
39 | self?.rootViewController.displayException(exception)
40 | self?.server.sendMessage(.PrototypeHitException(exception))
41 | return
42 | },
43 | consoleLogHandler: { [weak self] message in
44 | self?.rootViewController.appendConsoleMessage(message)
45 | self?.server.sendMessage(.PrototypeConsoleLog(message))
46 | return
47 | }
48 | )
49 |
50 | server = ProtoscopeServer(messageHandler: { message in
51 | switch message {
52 | case let .ReplacePrototype(prototype):
53 | let sceneDisplayHostView = self.rootViewController.transitionToSceneDisplay()
54 | self.sessionInteractor.displayPrototype(prototype, rootView: sceneDisplayHostView)
55 | default:
56 | println("Unexpected message: \(message))")
57 | }
58 | })
59 |
60 | return true
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Protoscope/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Protoscope/ProtoscopeServer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProtoscopeServer.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import swiftz_core
11 |
12 | class ProtoscopeServer {
13 | private let bonjourServer: DTBonjourServer
14 | private var serverDelegate: ServerDelegate?
15 | private var currentConnection: DTBonjourDataConnection?
16 |
17 | init(messageHandler: Message -> ()) {
18 | bonjourServer = DTBonjourServer(bonjourType: ProtoropeReceiverServiceType)
19 | serverDelegate = ServerDelegate(
20 | connectionHandler: { [weak self] connection in
21 | self?.currentConnection = connection
22 | return
23 | },
24 | messageHandler: messageHandler
25 | )
26 | bonjourServer.delegate = serverDelegate!
27 | bonjourServer.start()
28 | }
29 |
30 | func sendMessage(message: Message) {
31 | currentConnection?.sendObject(Message.toJSON(message).encode()!, error: nil)
32 | }
33 |
34 | func stop() {
35 | bonjourServer.delegate = nil
36 | bonjourServer.stop()
37 | serverDelegate = nil
38 | }
39 |
40 | deinit {
41 | stop()
42 | }
43 |
44 | @objc private class ServerDelegate: NSObject, DTBonjourServerDelegate {
45 | let connectionHandler: DTBonjourDataConnection -> ()
46 | let messageHandler: Message -> ()
47 |
48 | init(connectionHandler: DTBonjourDataConnection -> (), messageHandler: Message -> ()) {
49 | self.connectionHandler = connectionHandler
50 | self.messageHandler = messageHandler
51 | }
52 |
53 | @objc private func bonjourServer(server: DTBonjourServer!, didAcceptConnection connection: DTBonjourDataConnection!) {
54 | connectionHandler(connection)
55 | }
56 |
57 | @objc private func bonjourServer(server: DTBonjourServer!, didReceiveObject object: AnyObject!, onConnection connection: DTBonjourDataConnection!) {
58 | if let data = object as? NSData {
59 | if let message = JSONValue.decode(data) >>- Message.fromJSON {
60 | messageHandler(message)
61 | } else {
62 | println("Received unknown message: \(NSString(data: data, encoding: NSUTF8StringEncoding))")
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Protoscope/ConsoleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConsoleView.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/11/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ConsoleView: UIView {
12 | private let vibrancyEffectView: UIVisualEffectView
13 | private let visualEffectView: UIVisualEffectView
14 | private let textView: UITextView = {
15 | let textView = UITextView(frame: CGRect())
16 | textView.autoresizingMask = .FlexibleWidth | .FlexibleHeight
17 | textView.textColor = UIColor.whiteColor()
18 | textView.backgroundColor = UIColor.clearColor()
19 | textView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
20 | textView.textContainer.lineFragmentPadding = 0
21 | textView.editable = false
22 |
23 | return textView
24 | }()
25 |
26 | func appendConsoleMessage(message: String) {
27 | textView.textStorage.appendAttributedString(
28 | NSAttributedString(string: "\(message)\n", attributes: [NSFontAttributeName: UIFont(name: "Menlo", size: 16)!])
29 | )
30 | scrollToBottomAnimated(true)
31 | }
32 |
33 | func scrollToBottomAnimated(animated: Bool) {
34 | var newContentOffset = textView.contentOffset
35 | newContentOffset.y = max(0, textView.contentSize.height - textView.bounds.size.height)
36 | textView.setContentOffset(newContentOffset, animated: animated)
37 | }
38 |
39 | func reset() {
40 | textView.textStorage.deleteCharactersInRange(NSMakeRange(0, textView.textStorage.length))
41 | }
42 |
43 | init() {
44 | let blurEffect = UIBlurEffect(style: .Dark)
45 | visualEffectView = UIVisualEffectView(effect: blurEffect)
46 | visualEffectView.autoresizingMask = .FlexibleWidth | .FlexibleHeight
47 |
48 | vibrancyEffectView = UIVisualEffectView(effect: UIVibrancyEffect(forBlurEffect: blurEffect))
49 | vibrancyEffectView.autoresizingMask = .FlexibleWidth | .FlexibleHeight
50 | visualEffectView.contentView.addSubview(vibrancyEffectView)
51 |
52 | vibrancyEffectView.contentView.addSubview(textView)
53 |
54 | super.init(frame: CGRect())
55 |
56 | addSubview(visualEffectView)
57 | }
58 |
59 | required init(coder aDecoder: NSCoder) {
60 | fatalError("init(coder:) has intentionally not been implemented")
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Protoscope/URLMonitor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLMonitor.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/7/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class URLMonitor {
12 | let URL: NSURL
13 | private let presenter: Presenter
14 |
15 | var everythingDidChangeHandler: () -> Void {
16 | get { return presenter.everythingDidChangeHandler }
17 | set { presenter.everythingDidChangeHandler = newValue }
18 | }
19 |
20 | init(URL: NSURL) {
21 | self.URL = URL
22 | presenter = Presenter(url: URL)
23 | NSFileCoordinator.addFilePresenter(presenter)
24 | }
25 |
26 | deinit {
27 | stop()
28 | }
29 |
30 | func stop() {
31 | NSFileCoordinator.removeFilePresenter(presenter)
32 | }
33 |
34 | @objc private class Presenter: NSObject, NSFilePresenter {
35 | let url: NSURL
36 | var everythingDidChangeHandler: () -> Void = {}
37 |
38 | init(url: NSURL) {
39 | self.url = url
40 | }
41 |
42 | @objc var presentedItemURL: NSURL? {
43 | return url
44 | }
45 |
46 | @objc var presentedItemOperationQueue: NSOperationQueue {
47 | // TODO: something less ridiculous
48 | return NSOperationQueue.mainQueue()
49 | }
50 |
51 | @objc private func presentedItemDidChange() {
52 | everythingDidChangeHandler()
53 | }
54 |
55 | @objc private func presentedSubitemDidAppearAtURL(url: NSURL) {
56 | println("Subitem appared: \(url)")
57 | everythingDidChangeHandler()
58 | }
59 |
60 | @objc private func presentedSubitemDidChangeAtURL(url: NSURL) {
61 | println("Subitem changed: \(url)")
62 | if !url.lastPathComponent!.hasPrefix(".") {
63 | everythingDidChangeHandler()
64 | }
65 | }
66 |
67 | @objc private func presentedSubitemAtURL(oldURL: NSURL, didMoveToURL newURL: NSURL) {
68 | println("Subitem moved: \(oldURL) -> \(newURL)")
69 | everythingDidChangeHandler()
70 | }
71 |
72 | @objc private func accommodatePresentedSubitemDeletionAtURL(url: NSURL, completionHandler: (NSError!) -> Void) {
73 | println("Subitem deleted: \(url)")
74 | everythingDidChangeHandler()
75 | completionHandler(nil)
76 | }
77 |
78 | @objc private func accommodatePresentedItemDeletionWithCompletionHandler(completionHandler: (NSError!) -> Void) {
79 | println("Item disappeared")
80 | fatalError("Unimplemented")
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Examples/Masking/Masking.js:
--------------------------------------------------------------------------------
1 | const activeColor = new Color({hue: 0.78, saturation: 0.41, brightness: 0.61})
2 |
3 | // We begin with a colored label...
4 | const positiveLabel = makeButtonLabel()
5 |
6 | // Create the highlight circle that will cover it, but start it scaled down.
7 | const overlayCircle = new Layer()
8 | overlayCircle.width = overlayCircle.height = positiveLabel.width * 1.4
9 | overlayCircle.position = Layer.root.position
10 | const fullySizedCircleFrame = overlayCircle.frame
11 | overlayCircle.scale = 0.001
12 | overlayCircle.cornerRadius = overlayCircle.width / 2.0
13 | overlayCircle.backgroundColor = activeColor
14 |
15 | // Then make a negatively-colored label in a container the same size the highlight circle will eventually be.
16 | const negativeLabelContainer = new Layer()
17 | negativeLabelContainer.frame = fullySizedCircleFrame
18 |
19 | const negativeLabel = makeButtonLabel()
20 | negativeLabel.parent = negativeLabelContainer
21 | negativeLabel.x = negativeLabelContainer.bounds.midX
22 | negativeLabel.y = negativeLabelContainer.bounds.midY
23 | negativeLabel.textColor = Color.white
24 |
25 | // And make a circle like the highlight circle to mask the negative label.
26 | const maskingCircle = new Layer()
27 | maskingCircle.frame = negativeLabelContainer.bounds
28 | maskingCircle.cornerRadius = overlayCircle.cornerRadius
29 | maskingCircle.backgroundColor = Color.black
30 | maskingCircle.scale = overlayCircle.scale
31 | negativeLabelContainer.maskLayer = maskingCircle
32 |
33 | overlayCircle.animators.scale.springSpeed = maskingCircle.animators.scale.springSpeed = 0
34 | overlayCircle.animators.scale.springBounciness = maskingCircle.animators.scale.springBounciness = 3
35 |
36 | Layer.root.touchBeganHandler = () => { setExpanded(true) }
37 |
38 | Layer.root.touchEndedHandler = Layer.root.touchCancelledHandler = () => { setExpanded(false) }
39 |
40 | function setExpanded(expanded) {
41 | const newTarget = expanded ? new Point({x: 1.0, y: 1.0}) : new Point({x: 0.001, y: 0.001})
42 | overlayCircle.animators.scale.target = maskingCircle.animators.scale.target = newTarget
43 | }
44 |
45 | function makeButtonLabel() {
46 | const label = new TextLayer()
47 | label.fontName = "AvenirNext-Regular"
48 | label.fontSize = 100
49 | label.text = "Press Me"
50 | label.position = Layer.root.position
51 | label.textColor = activeColor
52 | return label
53 | }
54 |
--------------------------------------------------------------------------------
/Prototope/Math.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Math.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/16/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /** Linearly interpolates between the from: value and the to: value based on the at:
12 | fraction. When at:0, returns the from: value. When at: 1, returns the to: value.
13 |
14 | For example:
15 | interpolate(from: 5, to: 15, at: 0.5) // Returns 10
16 |
17 | Like Processing's lerp() */
18 | public func interpolate(from fromValue: Double, to toValue: Double, at fraction: Double) -> Double {
19 | return fraction * (toValue - fromValue) + fromValue
20 | }
21 |
22 | /** Maps a value from one interval to another.
23 |
24 | For example:
25 | map(value: 0.4, fromInterval: (0, 1), toInterval: (0, 10)) // Returns 4
26 |
27 | Like Processing's map(). */
28 | public func map(value: Double, fromInterval: (Double, Double), toInterval: (Double, Double)) -> Double {
29 | return interpolate(from: toInterval.0, to: toInterval.1, at: (value - fromInterval.0) / (fromInterval.1 - fromInterval.0))
30 | }
31 |
32 | /** Clips a value so that it falls between the specified minimum and maximum. */
33 | public func clip(value: T, min minValue: T, max maxValue: T) -> T {
34 | return max(min(value, maxValue), minValue)
35 | }
36 |
37 | #if os(iOS)
38 | import UIKit
39 | typealias SystemScreen = UIScreen
40 | #else
41 | import AppKit
42 | typealias SystemScreen = NSScreen
43 |
44 | extension NSScreen {
45 | var scale: CGFloat {
46 | return self.backingScaleFactor
47 | }
48 | }
49 | #endif
50 |
51 | extension SystemScreen {
52 |
53 | /** Returns the main screen's scale. */
54 | class var mainScreenScale: Double {
55 | #if os(iOS)
56 | let mainScreen = SystemScreen.mainScreen()
57 | #else
58 | let mainScreen = SystemScreen.mainScreen()!
59 | #endif
60 | return Double(mainScreen.scale)
61 | }
62 | }
63 |
64 | /** `ceil`s the value, snapping to screen's pixel values */
65 | public func pixelAwareCeil(value: Double) -> Double {
66 | let scale = SystemScreen.mainScreenScale
67 | return ceil(value * scale) / scale
68 | }
69 |
70 | /** `floor`s the value, snapping to screen's pixel values */
71 | public func pixelAwareFloor(value: Double) -> Double {
72 | let scale = SystemScreen.mainScreenScale
73 | return floor(value * scale) / scale
74 | }
75 |
--------------------------------------------------------------------------------
/Prototope/ParticlePreset.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParticlePreset.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on Feb-06-2015.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /** These are presets you can use when setting up a particle.
13 |
14 | Use these as starting points for configuring your particles,
15 | or use .IKnowWhatImDoing and configure everything yourself. */
16 | public enum ParticlePreset {
17 |
18 | /** Particles explode in all directions. */
19 | case Explode
20 |
21 | /** Particles fall like rain all the way down. */
22 | case Rain
23 |
24 | /** Particles fly upward and and quickly burn out. */
25 | case Sparkle
26 |
27 | /** Sets nothing on the particle. We trust you to do the right thing. */
28 | case IKnowWhatImDoing
29 |
30 | internal func configureParticle(var particle: Particle) {
31 | switch self {
32 | case Explode:
33 | particle.lifetime = 3
34 | particle.lifetimeRange = 3
35 |
36 | particle.birthRate = 80
37 |
38 | particle.velocity = 100
39 |
40 | particle.emissionRange = M_PI * 2.0
41 |
42 | particle.color = Color(red: 0.5, green: 0.5, blue: 0.5, alpha: 1.0)
43 |
44 | particle.redRange = 1.0
45 | particle.blueRange = 1.0
46 | particle.greenRange = 1.0
47 | particle.alphaRange = 1.0
48 |
49 | case Rain:
50 | particle.lifetime = 4
51 | particle.lifetimeRange = 5
52 | particle.birthRate = 25
53 | particle.velocity = 70
54 | particle.velocityRange = 160
55 | particle.yAcceleration = 1000
56 | particle.emissionRange = Radian.circle
57 |
58 | particle.color = Color(red: 0, green: 0, blue: 1, alpha: 0.3)
59 | particle.redRange = 0
60 | particle.greenRange = 0
61 | particle.blueRange = 1.0
62 | particle.alphaRange = 0.55
63 |
64 | particle.scale = 0.4
65 |
66 | case Sparkle:
67 | particle.lifetime = 0.71
68 | particle.lifetimeRange = 0.5
69 | particle.birthRate = 20
70 |
71 | particle.velocity = 6.5
72 | particle.yAcceleration = -300
73 |
74 | particle.spin = Radian(degrees: -200)
75 | particle.spinRange = Radian(degrees: 490)
76 |
77 | particle.color = Color(red: 1, green: 0.95, blue: 0.27, alpha: 0)
78 | particle.alphaSpeed = 3
79 |
80 | particle.scale = 0.70
81 | particle.scaleRange = 0.30
82 |
83 | case IKnowWhatImDoing:
84 | break
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/Protoscope/js/main.js:
--------------------------------------------------------------------------------
1 | require("babel-core/polyfill");
2 |
3 | var SourceMapConsumer = require("source-map").SourceMapConsumer;
4 | var babel = require("babel-core/browser");
5 |
6 | var Protoscope = {
7 | transform: function(code) {
8 | var result = babel.transform(code, {sourceMaps: true});
9 |
10 | var sourceMapConsumer;
11 | function originalSourcePositionFor(line, column) {
12 | if (!sourceMapConsumer) {
13 | // Lazily parse source map since we only need it in case of
14 | // exception
15 | sourceMapConsumer = new SourceMapConsumer(result.map);
16 | }
17 | var position = sourceMapConsumer.originalPositionFor({
18 | line: line,
19 | column: column
20 | });
21 | return position;
22 | }
23 |
24 | return {
25 | code: result.code,
26 | originalSourcePositionFor: originalSourcePositionFor
27 | };
28 | },
29 | normalizeError: function(rawError, sourceMappers) {
30 | var normalized = Object.create(Object.getPrototypeOf(rawError));
31 | normalized.message = rawError.message;
32 | normalized.line = rawError.line;
33 | normalized.column = rawError.column;
34 |
35 | if (rawError.sourceURL && sourceMappers[rawError.sourceURL] &&
36 | rawError.line && rawError.column) {
37 | var mapper = sourceMappers[rawError.sourceURL];
38 | if (mapper) {
39 | var info = mapper(rawError.line, rawError.column);
40 | normalized.line = info.line;
41 | normalized.column = info.column;
42 | }
43 | }
44 |
45 | if (rawError.stack) {
46 | normalized.stack = rawError.stack.replace(
47 | /@([^@]+):(\d+):(\d+)(?=\n|$)/g,
48 | function(match, file, line, column) {
49 | var mapper = sourceMappers[file];
50 | if (mapper) {
51 | var info = mapper(+line, +column);
52 | return "@" + file + ":" + info.line + ":" + info.column;
53 | } else {
54 | return match;
55 | }
56 | }
57 | );
58 | }
59 |
60 | return normalized;
61 | }
62 | };
63 |
64 | module.exports = Protoscope;
65 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/TunableBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TunableBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/3/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | public struct TunableBridge: BridgeType {
14 | public static func addToContext(context: JSContext) {
15 | let tunableTrampoline: @convention(block) JSValue -> NSNumber = { TunableBridge.tunable($0) }
16 | context.setFunctionForKey("tunable", fn: tunableTrampoline)
17 | }
18 |
19 | private static func tunable(args: JSValue) -> NSNumber {
20 | let nameValue = args.objectForKeyedSubscript("name")
21 | let defaultValueValue = args.objectForKeyedSubscript("default")
22 | let minValue = args.objectForKeyedSubscript("min")
23 | let maxValue = args.objectForKeyedSubscript("max")
24 | let maintainValue = args.objectForKeyedSubscript("changeHandler")
25 |
26 | let name = nameValue.isUndefined ? nil : nameValue.toString()
27 | let defaultValue = defaultValueValue.isUndefined ? nil : defaultValueValue.toNumber()
28 | let min: Double? = minValue.isUndefined ? nil : minValue.toDouble()
29 | let max: Double? = maxValue.isUndefined ? nil : maxValue.toDouble()
30 | let maintain: JSValue? = maintainValue.isUndefined ? nil : maintainValue
31 |
32 | if let name = name {
33 | if let defaultValue = defaultValue {
34 | let someDouble: NSNumber = 1.0
35 | if String.fromCString(defaultValue.objCType) == String.fromCString(someDouble.objCType) {
36 | if let maintainCallable = maintain {
37 | var result: Double = 0
38 | Prototope.tunable(defaultValue.doubleValue, name: name, min: min, max: max, changeHandler: { value in
39 | result = value
40 | maintainCallable.callWithArguments([value])
41 | })
42 | return result
43 | } else {
44 | return Prototope.tunable(defaultValue.doubleValue, name: name, min: min, max: max)
45 | }
46 | } else {
47 | if let maintainCallable = maintain {
48 | var result = false
49 | Prototope.tunable(defaultValue.boolValue, name: name, changeHandler: { value in
50 | result = value
51 | maintainCallable.callWithArguments([value])
52 | })
53 | return result
54 | } else {
55 | return Prototope.tunable(defaultValue.boolValue, name: name)
56 | }
57 | }
58 | }
59 | }
60 | return 0
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/PrototopeTestApp/JSTest.js:
--------------------------------------------------------------------------------
1 | var layer = new Layer({parent: Layer.root});
2 | Layer.animate({
3 | duration: 3.0,
4 | curve: AnimationCurve.EaseOut,
5 | animations: function() {
6 | layer.x = 400;
7 | }
8 | });
9 |
10 | var pinch = new PinchGesture({
11 | handler: function(phase, sequence) {
12 | console.log('pinch:'+sequence.currentSample.scale);
13 | }
14 | });
15 |
16 | var pan = new PanGesture({
17 | handler: function(phase, sequence) {
18 | var loc = sequence.currentSample.globalLocation;
19 | console.log('pan:'+loc.x+','+loc.y);
20 | }
21 | });
22 |
23 | var simul = function(gesture) { return true; };
24 | pinch.shouldRecognizeSimultaneouslyWithGesture = simul;
25 | pan.shouldRecognizeSimultaneouslyWithGesture = simul;
26 |
27 | layer.gestures = [
28 | pinch,
29 | pan
30 | ];
31 | layer.backgroundColor = new Color({red: 0.5, green: 0.7, blue: 0.1, alpha: 0.7});
32 | layer.frame = new Rect({x: 75, y: 80, width: 400, height: 400});
33 | layer.border = new Border({color: Color.black, width: 2});
34 | layer.shadow = new Shadow({alpha: 1.0});
35 | console.log(layer);
36 |
37 | //(new Sound({name: 'Glass'})).play();
38 |
39 | //var video = new Video({name: "countdown.mp4"});
40 | //var videoLayer = new VideoLayer({parent: Layer.root, video: video });
41 | //videoLayer.play();
42 |
43 | var particle = new Particle({imageName: "paint"});
44 | particle.spin = 2;
45 |
46 | var emitter = new ParticleEmitter({particle: particle});
47 | layer.addParticleEmitter(emitter);
48 | layer.removeParticleEmitter(emitter);
49 |
50 | var scrollLayer = new ScrollLayer({parent: layer, name: "yo"});
51 | scrollLayer.updateScrollableSizeToFitSublayers();
52 |
53 |
54 | scrollLayer.moveToRightOfSiblingLayer({siblingLayer: layer, margin: 10.0});
55 |
56 | Speech.say({text: " "}); // silent, but here for illustration.
57 | var p1 = new Point({x: 50, y: 100});
58 | var p2 = new Point({x: 100, y: 60});
59 | var slope = p1.slopeToPoint(p2);
60 | console.log(p1.toString());
61 | console.log(new Rect({x: 1, y: 2, width: 3, height: 4}));
62 |
63 | console.log("Hello, world!");
64 | "Done JSTest.js";
65 |
--------------------------------------------------------------------------------
/Prototope/FontProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FontProvider.swift
3 | // Prototope
4 | //
5 | // Created by Saniul Ahmed on 15/06/2015.
6 | // Copyright © 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class FontProvider {
12 | static private let supportedExtensions = ["ttf", "otf"]
13 |
14 | let resources: [String : NSData]
15 |
16 | var registeredFontsURLs = [NSURL]()
17 |
18 | public init(resources: [String : NSData]) {
19 | self.resources = resources
20 | }
21 |
22 | deinit {
23 | for URL in registeredFontsURLs {
24 | var fontError: Unmanaged?
25 | if CTFontManagerUnregisterFontsForURL(URL, CTFontManagerScope.Process, &fontError) {
26 | print("Successfully unloaded font: '\(URL)'.")
27 | } else if let fontError = fontError?.takeRetainedValue() {
28 | let errorDescription = CFErrorCopyDescription(fontError)
29 | print("Failed to unload font '\(URL)': \(errorDescription)")
30 | } else {
31 | print("Failed to unload font '\(URL)'.")
32 | }
33 | }
34 | }
35 |
36 | func resourceForFontWithName(name: String) -> NSData? {
37 | for fileExtension in FontProvider.supportedExtensions {
38 | if let data = resources[name + ".\(fileExtension)"] {
39 | return data
40 | }
41 | }
42 |
43 | return nil
44 | }
45 |
46 | public func fontForName(name: String, size: Double) -> UIFont? {
47 | if let font = UIFont(name: name, size: CGFloat(size)) {
48 | return font
49 | }
50 |
51 | if let customFontData = resourceForFontWithName(name) {
52 | let URL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first as NSURL!
53 |
54 | let fontFileURL = URL.URLByAppendingPathComponent(name)
55 |
56 | customFontData.writeToURL(fontFileURL, atomically: true)
57 |
58 | var fontError: Unmanaged?
59 | if CTFontManagerRegisterFontsForURL(fontFileURL, CTFontManagerScope.Process, &fontError) {
60 | registeredFontsURLs += [fontFileURL]
61 |
62 | print("Successfully loaded font: '\(name)'.")
63 | if let font = UIFont(name: name, size: CGFloat(size)) {
64 | return font
65 | }
66 | } else if let fontError = fontError?.takeRetainedValue() {
67 | let errorDescription = CFErrorCopyDescription(fontError)
68 | print("Failed to load font '\(name)': \(errorDescription)")
69 | } else {
70 | print("Failed to load font '\(name)'.")
71 | }
72 | }
73 |
74 | return nil
75 | }
76 | }
--------------------------------------------------------------------------------
/Prototope/Image.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Image.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/16/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import UIKit
11 | public typealias SystemImage = UIImage
12 | #else
13 | import AppKit
14 |
15 | public typealias SystemImage = NSImage
16 | extension SystemImage {
17 | var CGImage: CGImageRef {
18 | var rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
19 | return self.CGImageForProposedRect(&rect, context: nil, hints: nil)!.takeUnretainedValue()
20 | }
21 | }
22 | #endif
23 |
24 |
25 | /** A simple abstraction for a bitmap image. */
26 | public struct Image: CustomStringConvertible {
27 |
28 |
29 | /** The size of the image, in points. */
30 | public var size: Size {
31 | return Size(systemImage.size)
32 | }
33 |
34 | public var name: String!
35 |
36 | var systemImage: SystemImage
37 |
38 | /** Loads a named image from the assets built into the app. */
39 | public init?(name: String) {
40 | if let image = Environment.currentEnvironment!.imageProvider(name) {
41 | systemImage = image
42 | self.name = name
43 | } else {
44 | Environment.currentEnvironment?.exceptionHandler("Image named \(name) not found")
45 | return nil
46 | }
47 | }
48 |
49 | /** Constructs an Image from a UIImage. */
50 | init(_ image: SystemImage) {
51 | systemImage = image
52 | }
53 |
54 |
55 | public var description: String {
56 | return self.name
57 | }
58 | }
59 |
60 |
61 | extension Image {
62 |
63 | /** Creates an image by rendering the given text into an image. */
64 | public init(text: String, font: SystemFont = SystemFont.boldSystemFontOfSize(SystemFont.systemFontSize()), textColor: Color = Color.black) {
65 |
66 | self.init(Image.imageFromText(text, font: font, textColor: textColor))
67 | }
68 |
69 | static func imageFromText(text: String, font: SystemFont = SystemFont.boldSystemFontOfSize(SystemFont.systemFontSize()), textColor: Color = Color.black) -> SystemImage {
70 | let attributes = [NSFontAttributeName: font, NSForegroundColorAttributeName: textColor.systemColor]
71 | let size = (text as NSString).sizeWithAttributes(attributes)
72 |
73 | let isOpaque = false
74 | let automaticScale: CGFloat = 0.0
75 | UIGraphicsBeginImageContextWithOptions(size, isOpaque, automaticScale)
76 | (text as NSString).drawAtPoint(CGPoint(), withAttributes: attributes)
77 |
78 | let image = UIGraphicsGetImageFromCurrentImageContext()
79 | UIGraphicsEndImageContext()
80 |
81 | return image
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/Prototope/Sound.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sound.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 11/19/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import Foundation
11 |
12 | /** Provides a simple way to play sound files. Supports .aif, .aiff, .wav, and .caf files. */
13 | public struct Sound: CustomStringConvertible {
14 |
15 | private let player: AVAudioPlayer
16 | private let name: String!
17 |
18 | /** Creates a sound from a filename. No need to include the file extension: Prototope will
19 | try all the valid extensions. */
20 | public init?(name: String) {
21 | if let data = Environment.currentEnvironment!.soundProvider(name) {
22 | player = try! AVAudioPlayer(data: data)
23 | player.prepareToPlay()
24 | self.name = name
25 | } else {
26 | Environment.currentEnvironment?.exceptionHandler("Sound named \(name) not found")
27 | return nil
28 | }
29 | }
30 |
31 | public var description: String {
32 | return self.name
33 | }
34 |
35 | /// From 0.0 to 1.0
36 | public var volume: Double {
37 | get { return Double(player.volume) }
38 | set { player.volume = Float(newValue) }
39 | }
40 |
41 | public func play() {
42 | player.currentTime = 0
43 | if player.delegate == nil {
44 | let delegate = AVAudioPlayerDelegate()
45 | player.delegate = delegate
46 | playingAVAudioPlayerDelegates.insert(delegate)
47 | }
48 | playingAVAudioPlayers.insert(player)
49 | player.play()
50 | }
51 |
52 | public func stop() {
53 | player.stop()
54 | if let delegate = (player.delegate as? Sound.AVAudioPlayerDelegate) {
55 | playingAVAudioPlayerDelegates.remove(delegate)
56 | player.delegate = nil
57 | }
58 | playingAVAudioPlayers.remove(player)
59 | }
60 |
61 | public static let supportedExtensions = ["caf", "aif", "aiff", "wav"]
62 |
63 | // Fancy scheme to keep playing AVAudioPlayers from deallocating while they're playing.
64 | @objc private class AVAudioPlayerDelegate: NSObject, AVFoundation.AVAudioPlayerDelegate {
65 | @objc func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
66 | player.delegate = nil
67 | playingAVAudioPlayers.remove(player)
68 | playingAVAudioPlayerDelegates.remove(self)
69 | }
70 |
71 | @objc func audioPlayerDecodeErrorDidOccur(player: AVAudioPlayer, error: NSError?) {
72 | player.delegate = nil
73 | playingAVAudioPlayers.remove(player)
74 | playingAVAudioPlayerDelegates.remove(self)
75 | }
76 | }
77 | }
78 |
79 | private var playingAVAudioPlayers = Set()
80 | private var playingAVAudioPlayerDelegates = Set()
--------------------------------------------------------------------------------
/PrototopeTestApp/Base.lproj/LaunchScreen.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/README.mdown:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/Khan/Prototope)
2 |
3 | # Prototope
4 |
5 | Prototope is a lightweight, high-performance prototyping framework. Its goals are:
6 | * making simple things very easy
7 | * making complex things possible
8 | * enabling rapid iteration
9 | * high performance execution
10 | * concepts easily mapped onto production implementation
11 |
12 | Interfaces to the API are presently available in Swift and JavaScript. The current implementation only runs on iOS, but the interface should be portable.
13 |
14 | You can use Protocaster (a Mac app) to broadcast live-reloading JavaScript prototypes to Protoscope (an iOS app). More documentation about this is forthcoming.
15 |
16 | Documentation is available [here](http://khan.github.io/Prototope/).
17 |
18 | ## Bootstrapping with prototope
19 |
20 | **You'll need Xcode 6.3 to use Prototope!**
21 |
22 | You can clone the [OhaiPrototope project](https://github.com/khan/ohaiprototope). If you do, you'll need to run
23 | ```
24 | $ git submodule update --init --recursive
25 | ```
26 | from within the repo in order to pull down the prototope and pop submodules. The project, however, is ready to go. Edit `MainScene.swift` and start making dreams come true!
27 |
28 | ## Including prototope in your existing project
29 |
30 | If you plan to include prototope as a submodule from within your project, you'll likely have to do the following from within your project
31 |
32 | ### getting it
33 | ```
34 | $ git submodule add https://github.com/khan/prototope
35 | $ git submodule update --init --recursive
36 | ```
37 |
38 | the first adds prototope as a git submodule to your project (and clones it outright), but you need the second command in order to pull in prototope's dependencies (namely pop).
39 |
40 | ### adding it to xcode
41 |
42 | This part is somewhat more involved.
43 |
44 | 1. under *Embedded Libraries*, add Prototope.framework
45 | 2. under *Build Settings -> Other Linker Flags*, add `-Objc -lc++`
46 | 3. under *Build Settings -> Header Search Paths*, add
47 | * `$(SRCROOT)/prototope/Prototope/`
48 | * `$(SRCROOT)/prototope/ThirdParty/` (set it to be recursive)
49 | 4. under Build Settings -> Library Search Paths, add `$(SRCROOT)/prototope/ThirdParty` (set it to be recursive)
50 |
51 | ### making sure things work
52 |
53 | You should be able to test that you've imported everything if you can type `import Prototope` in your ViewController.swift file and if the project *builds*. XCode may complain that it can't find the bridging header in the gutter, but it's a lie. It can, and if the project builds, you're in good shape.
54 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/CameraLayerBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraLayerBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/15/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol CameraLayerJSExport: JSExport {
14 | var cameraPosition: JSValue { get set }
15 | }
16 |
17 | @objc public class CameraLayerBridge: LayerBridge, CameraLayerJSExport {
18 | var cameraLayer: CameraLayer { return layer as! CameraLayer }
19 |
20 | public override class func addToContext(context: JSContext) {
21 | context.setObject(self, forKeyedSubscript: "CameraLayer")
22 | }
23 |
24 | public required init?(args: NSDictionary) {
25 | let parentLayer = (args["parent"] as! LayerBridge?)?.layer
26 | let cameraLayer = CameraLayer(parent: parentLayer, name: (args["name"] as! String?))
27 | super.init(cameraLayer)
28 | }
29 |
30 | public var cameraPosition: JSValue {
31 | get { return CameraPositionBridge.encodeCameraPosition(cameraLayer.cameraPosition, inContext: JSContext.currentContext()) }
32 | set { cameraLayer.cameraPosition = CameraPositionBridge.decodeCameraPosition(newValue) }
33 | }
34 |
35 | }
36 |
37 | public class CameraPositionBridge: NSObject, BridgeType {
38 | enum RawCameraPosition: Int {
39 | case Front = 0
40 | case Back = 1
41 | }
42 |
43 | public class func addToContext(context: JSContext) {
44 | let alignmentObject = JSValue(newObjectInContext: context)
45 | alignmentObject.setObject(RawCameraPosition.Front.rawValue, forKeyedSubscript: "Front")
46 | alignmentObject.setObject(RawCameraPosition.Back.rawValue, forKeyedSubscript: "Back")
47 | context.setObject(alignmentObject, forKeyedSubscript: "CameraPosition")
48 | }
49 |
50 | public class func encodeCameraPosition(cameraPosition: Prototope.CameraLayer.CameraPosition, inContext context: JSContext) -> JSValue {
51 | var rawCameraPosition: RawCameraPosition
52 | switch cameraPosition {
53 | case .Front: rawCameraPosition = .Front
54 | case .Back: rawCameraPosition = .Back
55 | }
56 | return JSValue(int32: Int32(rawCameraPosition.rawValue), inContext: context)
57 | }
58 |
59 | public class func decodeCameraPosition(bridgedCameraPosition: JSValue) -> Prototope.CameraLayer.CameraPosition! {
60 | if let rawCameraPosition = RawCameraPosition(rawValue: Int(bridgedCameraPosition.toInt32())) {
61 | switch rawCameraPosition {
62 | case .Front: return .Front
63 | case .Back: return .Back
64 | }
65 | } else {
66 | Environment.currentEnvironment!.exceptionHandler("Unknown camera position: \(bridgedCameraPosition)")
67 | return nil
68 | }
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/Prototope/ParticleEmitter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParticleEmitter.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on Feb-05-2015.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /** A particle emitter shows one or more kinds of Particles, and can show them in different formations. */
12 | public class ParticleEmitter {
13 | let particles: [Particle]
14 |
15 | let emitterLayer = CAEmitterLayer()
16 |
17 | /** Creates a particle emitter with an array of particles. */
18 | public init(particles: [Particle]) {
19 | self.particles = particles
20 | self.emitterLayer.emitterCells = self.particles.map {
21 | (particle: Particle) -> CAEmitterCell in
22 | return particle.emitterCell
23 | }
24 | }
25 |
26 |
27 | /** Creates a particle emitter with one kind of particle. */
28 | public convenience init(particle: Particle) {
29 | self.init(particles: [particle])
30 | }
31 |
32 |
33 | /** How often new baby particles are born. */
34 | public var birthRate: Double {
35 | get { return Double(self.emitterLayer.birthRate) }
36 | set { self.emitterLayer.birthRate = Float(newValue) }
37 | }
38 |
39 | /** The render mode of the emitter. */
40 | public var renderMode: String {
41 | get { return self.emitterLayer.renderMode }
42 | set { self.emitterLayer.renderMode = newValue }
43 | }
44 |
45 |
46 | /** The shape of the emitter. c.f., CAEmitterLayer for valid strings. */
47 | public var shape: String {
48 | get { return self.emitterLayer.emitterShape }
49 | set { self.emitterLayer.emitterShape = newValue }
50 | }
51 |
52 | /** The mode of the emission shape. c.f. CAEmitterLayer for valid strings.
53 | TODO make a real enum for this, lazy bum. */
54 | public var shapeMode: String {
55 | get { return self.emitterLayer.emitterMode }
56 | set { self.emitterLayer.emitterMode = newValue }
57 | }
58 |
59 |
60 | /** The render mode of the emitter. */
61 | public var size: Size {
62 | get { return Size(self.emitterLayer.emitterSize) }
63 | set { self.emitterLayer.emitterSize = CGSize(newValue) }
64 | }
65 |
66 |
67 | /** The render mode of the emitter. */
68 | public var position: Point {
69 | get { return Point(self.emitterLayer.emitterPosition) }
70 | set { self.emitterLayer.emitterPosition = CGPoint(newValue) }
71 | }
72 |
73 |
74 | /** The x position of the emitter. This is a shortcut for `position`. */
75 | public var x: Double {
76 | get { return self.position.x }
77 | set { self.position = Point(x: newValue, y: self.y) }
78 | }
79 |
80 |
81 | /** The y position of the emitter. This is a shortcut for `position`. */
82 | public var y: Double {
83 | get { return self.position.y }
84 | set { self.position = Point(x: self.x, y: newValue) }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Protoscope/SessionInteractor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SessionInteractor.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/8/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import PrototopeJSBridge
12 | import swiftz_core
13 |
14 | class SessionInteractor {
15 | private var context: PrototopeJSBridge.Context?
16 | private let exceptionHandler: String -> ()
17 | private let consoleLogHandler: String -> ()
18 |
19 | init(exceptionHandler: String -> (), consoleLogHandler: String -> ()) {
20 | self.exceptionHandler = exceptionHandler
21 | self.consoleLogHandler = consoleLogHandler
22 | }
23 |
24 | func displayPrototype(prototype: Prototype, rootView: UIView) {
25 | let fontProvider = FontProvider(resources: prototype.resources)
26 |
27 | Prototope.Layer.root?.removeAllSublayers()
28 | Prototope.Environment.currentEnvironment = Environment(
29 | rootView: rootView,
30 | imageProvider: { name in
31 | let scale = UIScreen.mainScreen().scale
32 | let filenameWithScale = name.stringByAppendingString("@\(Int(scale))x").stringByAppendingPathExtension("png")!
33 | let filename = name.stringByAppendingPathExtension("png")!
34 |
35 | let loadImage: (String, CGFloat) -> UIImage? = { filename, scale in
36 | // What does the brainfuck operator do?
37 | return prototype.resources[filename] >>- {
38 | let image = UIImage(data: $0, scale: scale)
39 | return image
40 | }
41 | }
42 |
43 | return loadImage(filenameWithScale, scale) ?? loadImage(filename, 1)
44 | },
45 | soundProvider: { name in
46 | for fileExtension in Sound.supportedExtensions {
47 | if let name = name.stringByAppendingPathExtension(fileExtension) {
48 | if let data = prototype.resources[name] {
49 | return data
50 | }
51 | }
52 | }
53 | return nil
54 | },
55 | fontProvider: fontProvider.fontForName,
56 | exceptionHandler: { [weak self] exception in
57 | self?.exceptionHandler(exception)
58 | return
59 | }
60 | )
61 |
62 | let script = NSString(data: prototype.mainScript, encoding: NSUTF8StringEncoding)!
63 | context = createContext()
64 | context?.evaluateScript(script as String)
65 | }
66 |
67 | private func createContext() -> PrototopeJSBridge.Context {
68 | let context = PrototopeJSBridge.Context()
69 | context.exceptionHandler = { [weak self] value in
70 | let lineNumber = value.objectForKeyedSubscript("line")
71 | let stack = value.objectForKeyedSubscript("stack")
72 | let exception = ("Line \(lineNumber): \(value)\n\n\(stack)")
73 | self?.exceptionHandler(exception)
74 | }
75 | context.consoleLogHandler = { [weak self] message in
76 | self?.consoleLogHandler(message)
77 | return
78 | }
79 | return context
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Examples/ShapeLayer/ShapeLayer.js:
--------------------------------------------------------------------------------
1 | // You can draw by tapping:
2 |
3 | const hintLabel = new TextLayer()
4 | hintLabel.text = "Tap to draw!"
5 | hintLabel.fontSize = 40
6 | hintLabel.x = Layer.root.x
7 | hintLabel.originY = 30
8 |
9 | const drawing = new ShapeLayer()
10 | drawing.strokeWidth = 2
11 | drawing.fillColor = new Color({hue: 0.1, saturation: 0.3, brightness: 1.0})
12 | drawing.lineCapStyle = LineCapStyle.Round
13 | drawing.closed = true
14 | drawing.lineJoinStyle = LineJoinStyle.Round
15 |
16 | const currentPointDot = new ShapeLayer.Circle({center: Point.zero, radius: 10})
17 | currentPointDot.alpha = 0
18 |
19 | Layer.root.touchBeganHandler = function(sequence) {
20 | currentPointDot.alpha = 1
21 | currentPointDot.position = sequence.currentSample.globalLocation
22 | drawing.addPoint(sequence.currentSample.globalLocation)
23 | }
24 |
25 | Layer.root.touchMovedHandler = function(sequence) {
26 | const segments = drawing.segments
27 | const lastSegment = segments.pop()
28 | lastSegment.point = sequence.currentSample.globalLocation
29 | segments.push(lastSegment)
30 | drawing.segments = segments
31 |
32 | currentPointDot.position = sequence.currentSample.globalLocation
33 | }
34 |
35 | Layer.root.touchEndedHandler = Layer.root.touchCancelledHandler = function(sequence) {
36 | currentPointDot.alpha = 0
37 | }
38 |
39 | // Demo of various shapes.
40 |
41 | const circle = new ShapeLayer.Circle({
42 | center: new Point({x: 75, y: Layer.root.frameMaxY - 75}),
43 | radius: 50
44 | })
45 | circle.fillColor = new Color({hue: 0.3, saturation: 0.6, brightness: 1.0})
46 | circle.strokeColor = undefined
47 |
48 | const oval = new ShapeLayer.Oval({
49 | rectangle: new Rect({x: circle.frameMaxX + 25, y: circle.originY, width: 50, height: circle.height})
50 | })
51 | oval.fillColor = undefined
52 | oval.strokeColor = new Color({hue: 0.6, saturation: 0.6, brightness: 1.0})
53 | oval.strokeWidth = 4
54 |
55 | const polygon = new ShapeLayer.Polygon({center: Point.zero, radius: 50, numberOfSides: 5})
56 | polygon.strokeWidth = 4
57 | polygon.fillColor = new Color({hue: 0.8, saturation: 0.6, brightness: 1.0})
58 | polygon.strokeColor = undefined
59 | polygon.lineCapStyle = LineCapStyle.Round
60 | polygon.lineJoinStyle = LineJoinStyle.Round
61 | polygon.y = oval.y
62 | polygon.originX = oval.frameMaxX + 25
63 |
64 | const pizza = new ShapeLayer()
65 | pizza.fillColor = Color.orange
66 | pizza.strokeColor = undefined
67 | pizza.segments = [
68 | new Segment({
69 | point: new Point({x: 10, y: 10}),
70 | handleIn: new Point({x: -10, y: 10}),
71 | handleOut: new Point({x: 10, y: -10})
72 | }),
73 | new Segment({
74 | point: new Point({x: 100, y: 30}),
75 | handleIn: new Point({x: -10, y: -10}),
76 | handleOut: new Point({x: -10, y: 10})
77 | }),
78 | new Segment(new Point({x: 30, y: 100})),
79 | ]
80 | pizza.originX = polygon.frameMaxX + 25
81 | pizza.y = polygon.y
82 |
--------------------------------------------------------------------------------
/PrototopeTestApp/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/3/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Prototope
11 | import PrototopeJSBridge
12 |
13 | class ViewController: UIViewController {
14 |
15 | var context: Context!
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 |
20 | Environment.currentEnvironment = Environment.defaultEnvironmentWithRootView(view)
21 |
22 | // You might write a prototype in Swift...
23 | //runSwiftPrototype()
24 |
25 | // ... or in JavaScript. (uncomment one; comment out the other)
26 | runJSPrototope()
27 | }
28 |
29 | func runSwiftPrototype() {
30 | for i in 0..<5 {
31 | let layer = makeRedLayer("Layer \(i)", y: Double(i) * 250)
32 | }
33 | @IBOutlet weak var connectionSettingsCheckbox: NSButton!
34 | @IBOutlet weak var connectionCheckbox: NSButton!
35 | }
36 |
37 | func makeRedLayer(name: String, y: Double) -> Layer {
38 | let redLayer = Layer(parent: Layer.root, name: name)
39 | redLayer.image = Image(name: "paint")
40 | tunable(50, name: "x") { value in redLayer.frame.origin = Point(x: value, y: y) }
41 | redLayer.backgroundColor = Color.red
42 | redLayer.cornerRadius = 10
43 | redLayer.border = Border(color: Color.black, width: 4)
44 |
45 | redLayer.gestures.append(PanGesture(handler: { phase, centroidSequence in
46 | if phase == .Began {
47 | redLayer.animators.position.stop()
48 | } else if let previousSample = centroidSequence.previousSample {
49 | redLayer.position += (centroidSequence.currentSample.globalLocation - previousSample.globalLocation)
50 | }
51 | if phase == .Ended {
52 | redLayer.animators.position.target = Point(x: 100, y: 100)
53 | redLayer.animators.position.velocity = centroidSequence.currentVelocityInLayer(Layer.root)
54 | }
55 | }))
56 | redLayer.gestures.append(TapGesture(handler: { location in
57 | if tunable(true, name: "shrinks when tapped") {
58 | Sound(name: "Glass")?.play()
59 | redLayer.animators.frame.target = Rect(x: 30, y: 30, width: 50, height: 50)
60 | redLayer.animators.frame.completionHandler = { println("Converged") }
61 | }
62 | }))
63 | return redLayer
64 | }
65 |
66 | func runJSPrototope() {
67 | // Run the "JSTest.js" prototype in the bundle.
68 |
69 | context = Context()
70 | context.exceptionHandler = { value in
71 | let lineNumber = value.objectForKeyedSubscript("line")
72 | println("Exception on line \(lineNumber): \(value)")
73 | }
74 | context.consoleLogHandler = { message in
75 | println(message)
76 | }
77 |
78 | let script = NSString(contentsOfURL: NSBundle.mainBundle().URLForResource("JSTest", withExtension: "js")!, encoding: NSUTF8StringEncoding, error: nil)!
79 | context.evaluateScript(script as String)
80 | }
81 |
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/Prototope/Environment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Environment.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/8/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | // TODO(jb): This belongs with Font
13 | #if os(iOS)
14 | import UIKit
15 | public typealias SystemFont = UIFont
16 | #else
17 | import AppKit
18 | public typealias SystemFont = NSFont
19 | #endif
20 |
21 | /** Establishes an environment in which Prototope can execute. */
22 | public struct Environment {
23 |
24 | public let rootLayer: Layer
25 | public let imageProvider: String -> SystemImage?
26 | public let soundProvider: String -> NSData?
27 | public let fontProvider: (name: String, size: Double) -> SystemFont?
28 | public let exceptionHandler: String -> Void
29 | let behaviorDriver: BehaviorDriver
30 |
31 | public static var currentEnvironment: Environment?
32 |
33 | public init(rootView: SystemView, imageProvider: String -> SystemImage?, soundProvider: String -> NSData?, fontProvider: (String, Double) -> SystemFont?, exceptionHandler: String -> Void) {
34 |
35 | self.rootLayer = Layer(hostingView: rootView, name: "Root")
36 |
37 | #if os(iOS)
38 | // TODO: move defaultSpec into Environment.
39 | let gesture = defaultSpec.twoFingerTripleTapGestureRecognizer()
40 | rootView.addGestureRecognizer(gesture)
41 | gesture.cancelsTouchesInView = false
42 | gesture.delaysTouchesEnded = false
43 |
44 | #endif
45 | self.behaviorDriver = BehaviorDriver()
46 |
47 | self.imageProvider = imageProvider
48 | self.soundProvider = soundProvider
49 | self.fontProvider = fontProvider
50 | self.exceptionHandler = exceptionHandler
51 | }
52 |
53 | public static func runWithEnvironment(environment: Environment, action: () -> Void) {
54 | // Eventually this will push and pop... but we're a long way from that because we still get events from the system now (e.g. timers, gestures). Before we can really push and pop, callbacks to clients will have to restore the environment according with those events. So for now, the expectation is that everything's dead when you change the environment.
55 | currentEnvironment = environment
56 | action()
57 | }
58 |
59 | public static func defaultEnvironmentWithRootView(rootView: SystemView) -> Environment {
60 | return Environment(
61 | rootView: rootView,
62 | imageProvider: { SystemImage(named: $0) },
63 | soundProvider: { name in
64 | for fileExtension in Sound.supportedExtensions {
65 | if let URL = NSBundle.mainBundle().URLForResource(name, withExtension: fileExtension) {
66 | return try? NSData(contentsOfURL: URL, options: [])
67 | }
68 | }
69 | return nil
70 | },
71 | fontProvider: { name, size in
72 | return SystemFont(name: name, size: CGFloat(size))
73 | },
74 | exceptionHandler: { exception in
75 | fatalError("⚠️ Prototope exception: \(exception)")
76 | }
77 | )
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Examples/SquishyBall/main.js:
--------------------------------------------------------------------------------
1 | function randomPrettyColor() {
2 | // 5º increments of hue
3 | const hue = Math.random()*(72+1) * 5.0/360.0;
4 | // 1/8 increments of brightness
5 | const brightness = Math.max(0.5, Math.random()*(8+1) * 1.0/8.0);
6 | // 1/8 increments of saturation
7 | const saturation = Math.max(0.5, Math.random()*(8+1) * 1.0/8.0);
8 |
9 | return new Color({hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0})
10 | }
11 |
12 | Layer.root.backgroundColor = randomPrettyColor();
13 |
14 | let delta = new Point();
15 | let mouse = new Point();
16 |
17 | let translation = new Point();
18 |
19 | const drag = 0.2;
20 | const radius = 50;
21 |
22 | const center = new Point({x: Layer.root.width*0.5,
23 | y: Layer.root.height*0.5})
24 |
25 | mouse = center
26 | translation = center
27 |
28 | const shadow = new ShapeLayer.Polygon({ center: center, radius:radius, numberOfSides: 15});
29 | shadow.fillColor = new Color({red: 0, green: 0, blue: 0, alpha: 0.2})
30 | shadow.strokeColor = Color.clear;
31 | shadow.scale = 0.85
32 |
33 | const ball = new ShapeLayer.Polygon({ center: center, radius:radius, numberOfSides: 15});
34 | ball.fillColor = randomPrettyColor();
35 |
36 | let shadowOffset = new Point()
37 |
38 | function updateTargetWithSequence(seq) {
39 | mouse = seq.currentSample.globalLocation
40 | const shadowOffsetX = 5 * radius * (mouse.x - center.x) / Layer.root.width;
41 | const shadowOffsetY = 5 * radius * (mouse.y - center.y) / Layer.root.height;
42 | shadowOffset = new Point({x:shadowOffsetX, y:shadowOffsetY})
43 | }
44 |
45 | Layer.root.touchMovedHandler = function(seq) {
46 | updateTargetWithSequence(seq)
47 | }
48 |
49 | Layer.root.touchBeganHandler = function(seq) {
50 | updateTargetWithSequence(seq)
51 | }
52 |
53 | const origins = ball.segments.map((segment) =>
54 | new Point({x: segment.point.x, y: segment.point.y}))
55 |
56 | //ball.backgroundColor = Color.black
57 |
58 | ball.behaviors = [new ActionBehavior({
59 |
60 | handler: function() {
61 |
62 | delta = mouse.subtract(translation)
63 |
64 | const newSegments = []
65 | for (let i = 0; i AnyClass {
86 | return AVCaptureVideoPreviewLayer.self
87 | }
88 | #else
89 | override func makeBackingLayer() -> CALayer {
90 | return AVCaptureVideoPreviewLayer()
91 | }
92 | #endif
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Protorope/Message.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Message.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/9/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import swiftz_core
11 |
12 | enum Message {
13 | /// A message from host to player which replaces the player's prototype with the argument.
14 | case ReplacePrototype(Prototype)
15 |
16 | /// A message from player to host indicating an exception was hit while a prototype was being played.
17 | case PrototypeHitException(String)
18 |
19 | /// A message from player to host for a log message hit while the prototype was being played.
20 | case PrototypeConsoleLog(String)
21 | }
22 |
23 | extension Message: JSON {
24 | static func fromJSON(jsonValue: JSONValue) -> Message? {
25 | switch jsonValue {
26 | case let .JSONObject(dictionary):
27 | return dictionary["type"]
28 | >>- MessageTypeEncoding.fromJSON
29 | >>- { typeEncoding in
30 | dictionary["payload"] >>- self.decodeMessageType(typeEncoding)
31 | }
32 | default:
33 | return nil
34 | }
35 | }
36 |
37 | static func toJSON(message: Message) -> JSONValue {
38 | return .JSONObject([
39 | "type": MessageTypeEncoding.toJSON(message.typeEncoding),
40 | "payload": encodeMessagePayload(message)
41 | ])
42 | }
43 |
44 | private var typeEncoding: MessageTypeEncoding {
45 | switch self {
46 | case .ReplacePrototype(_): return .ReplacePrototype
47 | case .PrototypeHitException(_): return .PrototypeHitException
48 | case .PrototypeConsoleLog(_): return .PrototypeConsoleLog
49 | }
50 | }
51 |
52 | private static func decodeMessageType(type: MessageTypeEncoding)(payload: JSONValue) -> Message? {
53 | switch type {
54 | case .ReplacePrototype:
55 | return Prototype.fromJSON(payload) >>- { .ReplacePrototype($0) }
56 | case .PrototypeHitException:
57 | return JString.fromJSON(payload) >>- { .PrototypeHitException($0) }
58 | case .PrototypeConsoleLog:
59 | return JString.fromJSON(payload) >>- { .PrototypeConsoleLog($0) }
60 | }
61 | }
62 |
63 | private static func encodeMessagePayload(message: Message) -> JSONValue {
64 | switch message {
65 | case let .ReplacePrototype(prototype):
66 | return Prototype.toJSON(prototype)
67 | case let .PrototypeHitException(exception):
68 | return JString.toJSON(exception)
69 | case let .PrototypeConsoleLog(message):
70 | return JString.toJSON(message)
71 | }
72 | }
73 |
74 | enum MessageTypeEncoding: String, JSON {
75 | case ReplacePrototype = "ReplacePrototype"
76 | case PrototypeHitException = "PrototypeHitException"
77 | case PrototypeConsoleLog = "PrototypeConsoleLog"
78 |
79 | static func fromJSON(jsonValue: JSONValue) -> MessageTypeEncoding? {
80 | return JString.fromJSON(jsonValue) >>- { MessageTypeEncoding(rawValue: $0) }
81 | }
82 |
83 | static func toJSON(jsonValue: MessageTypeEncoding) -> JSONValue {
84 | return JString.toJSON(jsonValue.rawValue)
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Prototope/DisplayLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DisplayLink.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-08-10.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | /******************************************
10 |
11 | OS X Only, folks
12 | (also it's currently pretty broken, sorry!)
13 |
14 | */
15 | import AppKit
16 |
17 |
18 | typealias HeartbeatDisplayLinkCallback = (sender: SystemDisplayLink) -> Void
19 |
20 | /** Crappy wrapper around CVDisplayLink to act pretty close to a CADisplayLink. Only OS X kids get this. */
21 | class DisplayLink: NSObject {
22 |
23 | private let displayLink:CVDisplayLink? = {
24 | var linkRef:Unmanaged?
25 | CVDisplayLinkCreateWithActiveCGDisplays(&linkRef)
26 |
27 | return linkRef?.takeUnretainedValue()
28 | }()
29 |
30 |
31 | /** Starts or stops the display link. */
32 | var paused: Bool {
33 | get { return CVDisplayLinkIsRunning(self.displayLink) > 0 }
34 | set {
35 | if newValue {
36 | CVDisplayLinkStop(self.displayLink)
37 | } else {
38 | CVDisplayLinkStart(self.displayLink)
39 | }
40 | }
41 | }
42 |
43 | var timestamp: NSTimeInterval {
44 | var outTime: CVTimeStamp = CVTimeStamp()
45 | CVDisplayLinkGetCurrentTime(self.displayLink, &outTime)
46 |
47 | // TODO(jb): I don't know if hostTime is what I want
48 | return NSTimeInterval(outTime.hostTime)
49 | }
50 |
51 | /** Initialize with a given callback. */
52 | init(heartbeatCallback: HeartbeatDisplayLinkCallback) {
53 |
54 | super.init()
55 |
56 | let callback = {(
57 | _:CVDisplayLink!,
58 | _:UnsafePointer,
59 | _:UnsafePointer,
60 | _:CVOptionFlags,
61 | _:UnsafeMutablePointer,
62 | _:UnsafeMutablePointer)->Void in
63 |
64 | heartbeatCallback(sender: self)
65 | }
66 | self.dynamicType.DisplayLinkSetOutputCallback(self.displayLink!, callback: callback)
67 | }
68 |
69 | /** Starts the display link, but ignores the parameters. They only exist to keep a compatible API. */
70 | func addToRunLoop(runLoop: NSRunLoop, forMode: String) {
71 | self.paused = false
72 | }
73 |
74 |
75 | /** Stops the display link. */
76 | func invalidate() {
77 | self.paused = true
78 | }
79 |
80 | }
81 |
82 |
83 | // Junk related to wrapping the CVDisplayLink callback function.
84 | extension DisplayLink {
85 | private typealias DisplayLinkCallback = @objc_block ( CVDisplayLink!, UnsafePointer, UnsafePointer, CVOptionFlags, UnsafeMutablePointer, UnsafeMutablePointer)->Void
86 |
87 | private class func DisplayLinkSetOutputCallback(displayLink:CVDisplayLink, callback:DisplayLinkCallback) {
88 | let block:DisplayLinkCallback = callback
89 | let myImp = imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self))
90 | let callback = unsafeBitCast(myImp, CVDisplayLinkOutputCallback.self)
91 |
92 | CVDisplayLinkSetOutputCallback(displayLink, callback, UnsafeMutablePointer())
93 | }
94 | }
--------------------------------------------------------------------------------
/PrototopeTests/ViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/7/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Prototope
10 | import XCTest
11 | import Foundation
12 |
13 | class ViewTests: XCTestCase {
14 | func testSublayers() {
15 | let parent1 = Layer(parent: nil)
16 | let child = Layer(parent: parent1)
17 | XCTAssertEqual(parent1.sublayers, [child])
18 | XCTAssertEqual(child.parent!, parent1)
19 |
20 | let parent2 = Layer(parent: nil)
21 | child.parent = parent2
22 | XCTAssertEqual(parent1.sublayers, [])
23 | XCTAssertEqual(parent2.sublayers, [child])
24 | }
25 |
26 | func testAncestorNamed() {
27 | let superparent = Layer(parent: nil, name: "A")
28 | let parent = Layer(parent: superparent, name: "B")
29 | let child = Layer(parent: parent, name: "C")
30 |
31 | XCTAssertEqual(child.ancestorNamed("A")!, superparent)
32 | XCTAssertNil(child.ancestorNamed("D"))
33 |
34 | let alternativeParent = Layer(parent: superparent, name: "A")
35 | child.parent = alternativeParent
36 | XCTAssertEqual(child.ancestorNamed("A")!, alternativeParent)
37 | }
38 |
39 | func testSublayerAtFront() {
40 | let parent = Layer(parent: nil)
41 | let child1 = Layer(parent: parent)
42 | let child2 = Layer(parent: parent)
43 |
44 | XCTAssertEqual(parent.sublayerAtFront!, child2)
45 | XCTAssertNil(child2.sublayerAtFront)
46 | }
47 |
48 | func testSublayerNamed() {
49 | let parent = Layer(parent: nil)
50 | let child1 = Layer(parent: parent, name: "A")
51 | let child2 = Layer(parent: parent, name: "B")
52 | XCTAssertEqual(parent.sublayerNamed("A")!, child1)
53 | XCTAssertEqual(parent.sublayerNamed("B")!, child2)
54 | }
55 |
56 | func testDescendentNamed() {
57 | let superparent = Layer(parent: nil, name: "A")
58 | let redHerring = Layer(parent: superparent, name: "Nope")
59 | let parent = Layer(parent: superparent, name: "B")
60 | let child = Layer(parent: parent, name: "C")
61 |
62 | XCTAssertEqual(superparent.descendentNamed("C")!, child)
63 | XCTAssertNil(superparent.descendentNamed("What?"))
64 |
65 | XCTAssertEqual(superparent.descendentAtPath(["B", "C"])!, child)
66 | XCTAssertNil(superparent.descendentAtPath(["C"]))
67 | }
68 |
69 | func testRemoveAllSublayers() {
70 | let parent = Layer(parent: nil)
71 | let child1 = Layer(parent: parent)
72 | let child2 = Layer(parent: parent)
73 |
74 | parent.removeAllSublayers()
75 | XCTAssertEqual(parent.sublayers, [])
76 | XCTAssertNil(child1.parent)
77 | XCTAssertNil(child2.parent)
78 | }
79 |
80 | func testContainsGlobalPoint() {
81 | let parent = Layer(parent: nil)
82 | parent.frame = Rect(x: 0, y: 0, width: 200, height: 200)
83 | let child1 = Layer(parent: parent)
84 | child1.frame = Rect(x: 30, y: 30, width: 50, height: 50)
85 |
86 | XCTAssertTrue(child1.containsGlobalPoint(Point(x: 30, y: 30)))
87 | XCTAssertFalse(child1.containsGlobalPoint(Point(x: 29, y: 30)))
88 | XCTAssertFalse(child1.containsGlobalPoint(Point(x: 80, y: 30)))
89 | XCTAssertTrue(child1.containsGlobalPoint(Point(x: 79, y: 30)))
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/script/README.md:
--------------------------------------------------------------------------------
1 | # objc-build-scripts
2 |
3 | This project is a collection of scripts created with two goals:
4 |
5 | 1. To standardize how Objective-C projects are bootstrapped after cloning
6 | 1. To easily build Objective-C projects on continuous integration servers
7 |
8 | ## Scripts
9 |
10 | Right now, there are two important scripts: [`bootstrap`](#bootstrap) and
11 | [`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and
12 | eliminate pesky system configuration issues (like setting up a working Ruby
13 | environment).
14 |
15 | The structure of the scripts on disk is meant to follow that of a typical Ruby
16 | project:
17 |
18 | ```
19 | script/
20 | bootstrap
21 | cibuild
22 | ```
23 |
24 | ### bootstrap
25 |
26 | This script is responsible for bootstrapping (initializing) your project after
27 | it's been checked out. Here, you should install or clone any dependencies that
28 | are required for a working build and development environment.
29 |
30 | By default, the script will verify that [xctool][] is installed, then initialize
31 | and update submodules recursively. If any submodules contain `script/bootstrap`,
32 | that will be run as well.
33 |
34 | To check that other tools are installed, you can set the `REQUIRED_TOOLS`
35 | environment variable before running `script/bootstrap`, or edit it within the
36 | script directly. Note that no installation is performed automatically, though
37 | this can always be added within your specific project.
38 |
39 | ### cibuild
40 |
41 | This script is responsible for building the project, as you would want it built
42 | for continuous integration. This is preferable to putting the logic on the CI
43 | server itself, since it ensures that any changes are versioned along with the
44 | source.
45 |
46 | By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode
47 | workspace or project in the working directory, then build all targets/schemes
48 | (as found by `xcodebuild -list`) using [xctool][].
49 |
50 | You can also specify the schemes to build by passing them into the script:
51 |
52 | ```sh
53 | script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS
54 | ```
55 |
56 | As with the `bootstrap` script, there are several environment variables that can
57 | be used to customize behavior. They can be set on the command line before
58 | invoking the script, or the defaults changed within the script directly.
59 |
60 | ## Getting Started
61 |
62 | To add the scripts to your project, read the contents of this repository into
63 | a `script` folder:
64 |
65 | ```
66 | $ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
67 | $ git fetch objc-build-scripts
68 | $ git read-tree --prefix=script/ -u objc-build-scripts/master
69 | ```
70 |
71 | Then commit the changes, to incorporate the scripts into your own repository's
72 | history. You can also freely tweak the scripts for your specific project's
73 | needs.
74 |
75 | To merge in upstream changes later:
76 |
77 | ```
78 | $ git fetch -p objc-build-scripts
79 | $ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
80 | ```
81 |
82 | [xctool]: https://github.com/facebook/xctool
83 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/ColorBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorBridge.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/1/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol ColorJSExport: JSExport {
14 | init?(args: NSDictionary)
15 | }
16 |
17 | @objc public class ColorBridge: NSObject, ColorJSExport, BridgeType {
18 |
19 | public class func addToContext(context: JSContext) {
20 | context.setObject(self, forKeyedSubscript: "Color")
21 | let colorBridge = context.objectForKeyedSubscript("Color")
22 | colorBridge.setObject(ColorBridge(Color.black), forKeyedSubscript: "black")
23 | colorBridge.setObject(ColorBridge(Color.darkGray), forKeyedSubscript: "darkGray")
24 | colorBridge.setObject(ColorBridge(Color.lightGray), forKeyedSubscript: "lightGray")
25 | colorBridge.setObject(ColorBridge(Color.white), forKeyedSubscript: "white")
26 | colorBridge.setObject(ColorBridge(Color.gray), forKeyedSubscript: "gray")
27 | colorBridge.setObject(ColorBridge(Color.red), forKeyedSubscript: "red")
28 | colorBridge.setObject(ColorBridge(Color.green), forKeyedSubscript: "green")
29 | colorBridge.setObject(ColorBridge(Color.blue), forKeyedSubscript: "blue")
30 | colorBridge.setObject(ColorBridge(Color.cyan), forKeyedSubscript: "cyan")
31 | colorBridge.setObject(ColorBridge(Color.yellow), forKeyedSubscript: "yellow")
32 | colorBridge.setObject(ColorBridge(Color.magenta), forKeyedSubscript: "magenta")
33 | colorBridge.setObject(ColorBridge(Color.orange), forKeyedSubscript: "orange")
34 | colorBridge.setObject(ColorBridge(Color.purple), forKeyedSubscript: "purple")
35 | colorBridge.setObject(ColorBridge(Color.brown), forKeyedSubscript: "brown")
36 | colorBridge.setObject(ColorBridge(Color.clear), forKeyedSubscript: "clear")
37 | }
38 |
39 | let color: Color!
40 |
41 | required public init?(args: NSDictionary) {
42 | let alpha = (args["alpha"] as! Double?) ?? 1
43 | if let hue = args["hue"] as! Double? {
44 | if let saturation = args["saturation"] as! Double? {
45 | if let brightness = args["brightness"] as! Double? {
46 | color = Color(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha)
47 | } else {
48 | color = nil
49 | super.init()
50 | return nil
51 | }
52 | } else {
53 | color = nil
54 | super.init()
55 | return nil
56 | }
57 | } else if let white = args["white"] as! Double? {
58 | color = Color(white: white, alpha: alpha)
59 | } else if let hexString = args["hex"] as! String? {
60 | let scanner = NSScanner(string: hexString)
61 | var hex: UInt32 = 0
62 | if scanner.scanHexInt(&hex) {
63 | color = Color(hex: hex, alpha: alpha)
64 | } else {
65 | color = nil
66 | super.init()
67 | return nil
68 | }
69 | } else {
70 | color = Color(
71 | red: (args["red"] as! Double?) ?? 0,
72 | green: (args["green"] as! Double?) ?? 0,
73 | blue: (args["blue"] as! Double?) ?? 0,
74 | alpha: (args["alpha"] as! Double?) ?? 1
75 | )
76 | }
77 | super.init()
78 | }
79 |
80 | init(_ color: Color) {
81 | self.color = color
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Protoscope/SceneViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneViewController.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/7/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SceneViewController: UIViewController {
12 | // TODO Factor all this UI stuff out in to a UIView class
13 | var sceneView: UIView = SceneViewController.createSceneView()
14 |
15 | private var consoleView: ConsoleView = {
16 | let consoleView = ConsoleView()
17 | consoleView.autoresizingMask = .FlexibleBottomMargin | .FlexibleWidth
18 | return consoleView
19 | }()
20 | private var consoleViewTransitionCount = 0
21 | private var consoleViewVisible = false
22 |
23 | func resetSceneView() {
24 | sceneView.removeFromSuperview()
25 |
26 | sceneView = SceneViewController.createSceneView()
27 | sceneView.frame = view.bounds
28 | view.addSubview(sceneView)
29 |
30 | consoleView.reset()
31 | setConsoleViewVisible(false, animated: true)
32 | }
33 |
34 | func appendConsoleMessage(message: String) {
35 | consoleView.appendConsoleMessage(message)
36 | setConsoleViewVisible(true, animated: true)
37 | }
38 |
39 | private func setConsoleViewVisible(visible: Bool, animated: Bool) {
40 | if visible == consoleViewVisible { return }
41 |
42 | consoleView.frame = view.bounds
43 | consoleView.frame.size.height = 50
44 | consoleView.frame.origin.y = visible ? -consoleView.frame.size.height : 0
45 |
46 | let finalOrigin = visible ? 0 : -consoleView.frame.size.height
47 | if visible && consoleView.superview == nil {
48 | view.insertSubview(consoleView, aboveSubview: sceneView)
49 | }
50 | let cleanup: () -> Void = {
51 | if self.consoleViewTransitionCount == 0 && !self.consoleViewVisible {
52 | self.consoleView.removeFromSuperview()
53 | }
54 | }
55 |
56 | if animated {
57 | UIView.animateWithDuration(0.4, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .AllowUserInteraction, animations: { () -> Void in
58 | self.consoleView.frame.origin.y = finalOrigin
59 | self.consoleViewTransitionCount++
60 | }, completion: { _ in
61 | self.consoleViewTransitionCount--
62 | cleanup()
63 | })
64 | } else {
65 | consoleView.frame.origin.y = finalOrigin
66 | cleanup()
67 | }
68 |
69 | consoleViewVisible = visible
70 | if visible {
71 | consoleView.scrollToBottomAnimated(false)
72 | }
73 | }
74 |
75 | init() {
76 | super.init(nibName: nil, bundle: nil)
77 | }
78 |
79 | required init(coder aDecoder: NSCoder) {
80 | fatalError("init(coder:) has intentionally not been implemented")
81 | }
82 |
83 | override func loadView() {
84 | super.loadView()
85 | sceneView.frame = view.bounds
86 | consoleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "dismissConsole"))
87 | view.addSubview(sceneView)
88 | }
89 |
90 | @objc private func dismissConsole() {
91 | setConsoleViewVisible(false, animated: true)
92 | }
93 |
94 | private class func createSceneView() -> UIView {
95 | let view = UIView()
96 | view.backgroundColor = UIColor.whiteColor()
97 | view.autoresizingMask = .FlexibleWidth | .FlexibleHeight
98 | return view
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/PrototopeJSBridge/ParticleEmitterBridge.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParticleEmitterBridge.swift
3 | // Prototope
4 | //
5 | // Created by Jason Brennan on 2015-02-11.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Prototope
11 | import JavaScriptCore
12 |
13 | @objc public protocol ParticleEmitterJSExport: JSExport {
14 | init?(args: NSDictionary)
15 |
16 |
17 | var birthRate: Double { get set }
18 | var renderMode: String { get set }
19 | var shape: String { get set }
20 | var shapeMode: String { get set }
21 | var size: SizeJSExport { get set }
22 | var position: PointJSExport { get set }
23 | var x: Double { get set }
24 | var y: Double { get set }
25 |
26 | var emitterBridge: ParticleEmitterBridge { get }
27 | }
28 |
29 | @objc public class ParticleEmitterBridge: NSObject, ParticleEmitterJSExport, BridgeType {
30 | var emitter: ParticleEmitter!
31 |
32 | public class func addToContext(context: JSContext) {
33 | context.setObject(self, forKeyedSubscript: "ParticleEmitter")
34 | }
35 |
36 | required public init?(args: NSDictionary) {
37 | if let particleBridge = args["particle"] as! ParticleBridge? {
38 | self.emitter = ParticleEmitter(particle: particleBridge.particle)
39 | super.init()
40 | } else {
41 | super.init()
42 | return nil
43 | }
44 | }
45 |
46 |
47 | // MARK: Properties
48 |
49 | /** How often new baby particles are born. */
50 | public var birthRate: Double {
51 | get { return self.emitter.birthRate }
52 | set { self.emitter.birthRate = newValue }
53 | }
54 |
55 |
56 | /** The render mode of the emitter. */
57 | public var renderMode: String {
58 | get { return self.emitter.renderMode }
59 | set { self.emitter.renderMode = newValue }
60 | }
61 |
62 |
63 | /** The shape of the emitter. c.f., CAemitter for valid strings. */
64 | public var shape: String {
65 | get { return self.emitter.shape }
66 | set { self.emitter.shape = newValue }
67 | }
68 |
69 | /** The mode of the emission shape. c.f. CAEmitterLayer for valid strings.
70 | TODO make a real enum for this, lazy bum. */
71 | public var shapeMode: String {
72 | get { return self.emitter.shapeMode }
73 | set { self.emitter.shapeMode = newValue }
74 | }
75 |
76 |
77 | /** The render mode of the emitter. */
78 | public var size: SizeJSExport {
79 | get { return SizeBridge(self.emitter.size) }
80 | set { self.emitter.size = (newValue as! SizeBridge).size }
81 | }
82 |
83 |
84 | /** The render mode of the emitter. */
85 | public var position: PointJSExport {
86 | get { return PointBridge(self.emitter.position) }
87 | set { self.emitter.position = (newValue as! PointBridge).point }
88 | }
89 |
90 |
91 | /** The x position of the emitter. This is a shortcut for `position`. */
92 | public var x: Double {
93 | get { return self.position.x }
94 | set { self.position = PointBridge(Point(x: newValue, y: self.y)) }
95 | }
96 |
97 |
98 | /** The y position of the emitter. This is a shortcut for `position`. */
99 | public var y: Double {
100 | get { return self.position.y }
101 | set { self.position = PointBridge(Point(x: self.x, y: newValue)) }
102 | }
103 |
104 |
105 | public var emitterBridge: ParticleEmitterBridge {
106 | return self
107 | }
108 | }
--------------------------------------------------------------------------------
/Prototope/Color.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 10/7/14.
6 | // Copyright (c) 2014 Khan Academy. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import UIKit
11 | typealias SystemColor = UIColor
12 | #else
13 | import AppKit
14 | typealias SystemColor = NSColor
15 | #endif
16 |
17 |
18 | /** A simple representation of color. */
19 | public struct Color {
20 | let systemColor: SystemColor
21 |
22 | /** The underlying CGColor of this colour. */
23 | var CGColor: CGColorRef {
24 | return self.systemColor.CGColor
25 | }
26 |
27 | /** Constructs a color from RGB and alpha values. Arguments range from 0.0 to 1.0. */
28 | public init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) {
29 | systemColor = SystemColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(alpha))
30 | }
31 |
32 | /** Constructs a grayscale color. Arguments range from 0.0 to 1.0. */
33 | public init(white: Double, alpha: Double = 1.0) {
34 | systemColor = SystemColor(white: CGFloat(white), alpha: CGFloat(alpha))
35 | }
36 |
37 | /** Constructs a color from HSB and alpha values. Arguments range from 0.0 to 1.0. */
38 | public init(hue: Double, saturation: Double, brightness: Double, alpha: Double = 1.0) {
39 | systemColor = SystemColor(hue: CGFloat(hue), saturation: CGFloat(saturation), brightness: CGFloat(brightness), alpha: CGFloat(alpha))
40 | }
41 |
42 | /** Construct a color from a hex value and with alpha from 0.0 - 1.0.
43 | i.e. Color(hex: 0x336699, alpha: 0.2)
44 | */
45 | public init(hex: UInt32, alpha: Double) {
46 | let r = CGFloat((hex >> 16) & 0xff) / 255.0
47 | let g = CGFloat((hex >> 8) & 0xff) / 255.0
48 | let b = CGFloat(hex & 0xff) / 255.0
49 |
50 | systemColor = SystemColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(alpha))
51 | }
52 |
53 | /** Construct an opaque color from a hex value
54 | i.e. Color(hex: 0x336699)
55 | */
56 | public init(hex: UInt32) {
57 | self.init(hex: hex, alpha: 1.0)
58 | }
59 |
60 | /** Constructs a Color from a UIColor. */
61 | init(_ systemColor: SystemColor) {
62 | self.systemColor = systemColor
63 | }
64 |
65 | public static var black: Color { return Color(SystemColor.blackColor()) }
66 | public static var darkGray: Color { return Color(SystemColor.darkGrayColor()) }
67 | public static var lightGray: Color { return Color(SystemColor.lightGrayColor()) }
68 | public static var white: Color { return Color(SystemColor.whiteColor()) }
69 | public static var gray: Color { return Color(SystemColor.grayColor()) }
70 | public static var red: Color { return Color(SystemColor.redColor()) }
71 | public static var green: Color { return Color(SystemColor.greenColor()) }
72 | public static var blue: Color { return Color(SystemColor.blueColor()) }
73 | public static var cyan: Color { return Color(SystemColor.cyanColor()) }
74 | public static var yellow: Color { return Color(SystemColor.yellowColor()) }
75 | public static var magenta: Color { return Color(SystemColor.magentaColor()) }
76 | public static var orange: Color { return Color(SystemColor.orangeColor()) }
77 | public static var purple: Color { return Color(SystemColor.purpleColor()) }
78 | public static var brown: Color { return Color(SystemColor.brownColor()) }
79 | public static var clear: Color { return Color(SystemColor.clearColor()) }
80 | }
81 |
--------------------------------------------------------------------------------
/PrototopeJSBridgeTests/GestureBridgeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GestureBridgeTests.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/4/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import PrototopeJSBridge
11 | import XCTest
12 |
13 | class GestureBridgeTests: JSBridgeTestCase {
14 | func testTouchSampleBridging() {
15 | let touchSampleValue = context.evaluateScript("new TouchSample({globalLocation: new Point({x: 5, y: 10}), timestamp: 20.0})")
16 | let globalLocationBridge = touchSampleValue.valueForProperty("globalLocation").toObject() as! PointBridge
17 | XCTAssertEqual(globalLocationBridge.x, 5)
18 | XCTAssertEqual(globalLocationBridge.y, 10)
19 | XCTAssertEqual(touchSampleValue.valueForProperty("timestamp").toDouble(), 20)
20 | }
21 |
22 | func testTouchSequenceBridging() {
23 | let touchSequenceValue = context.evaluateScript("var a = new TouchSample({globalLocation: new Point({x: 5, y: 10}), timestamp: 20.0}); var b = new TouchSample({globalLocation: new Point({x: 10, y: 20}), timestamp: 21.0}); new TouchSequence({samples: [a, b], id: 42})")
24 | let samples = touchSequenceValue.valueForProperty("samples").toArray() as! [TouchSampleBridge]
25 | XCTAssertEqual(samples[0].globalLocation.x, 5)
26 | XCTAssertEqual(samples[1].globalLocation.x, 10)
27 | XCTAssertEqual(touchSequenceValue.valueForProperty("firstSample").valueForProperty("timestamp").toDouble(), 20)
28 | XCTAssertEqual(touchSequenceValue.valueForProperty("previousSample").valueForProperty("timestamp").toDouble(), 20)
29 | XCTAssertEqual(touchSequenceValue.valueForProperty("currentSample").valueForProperty("timestamp").toDouble(), 21)
30 | XCTAssertEqual(touchSequenceValue.valueForProperty("id").toDouble(), 42)
31 |
32 | let touchSampleValue = context.evaluateScript("new TouchSample({globalLocation: new Point({x: 20, y: 30}), timestamp: 22.0})")
33 | let appendedSequenceValue = touchSequenceValue.invokeMethod("sampleSequenceByAppendingSample", withArguments: [touchSampleValue])
34 | let appendedSamples = appendedSequenceValue.valueForProperty("samples").toArray() as! [TouchSampleBridge]
35 | XCTAssertEqual(appendedSamples.count, 3)
36 | }
37 |
38 | func testSampleSequenceBridging() {
39 | let touchSequenceValue = context.evaluateScript("var a = new TouchSample({globalLocation: new Point({x: 5, y: 10}), timestamp: 20.0}); var b = new TouchSample({globalLocation: new Point({x: 10, y: 20}), timestamp: 21.0}); new SampleSequence({samples: [a, b], id: 42})")
40 | let samples = touchSequenceValue.valueForProperty("samples").toArray() as! [TouchSampleBridge]
41 | XCTAssertEqual(samples[0].globalLocation.x, 5)
42 | XCTAssertEqual(samples[1].globalLocation.x, 10)
43 | XCTAssertEqual(touchSequenceValue.valueForProperty("firstSample").valueForProperty("timestamp").toDouble(), 20)
44 | XCTAssertEqual(touchSequenceValue.valueForProperty("previousSample").valueForProperty("timestamp").toDouble(), 20)
45 | XCTAssertEqual(touchSequenceValue.valueForProperty("currentSample").valueForProperty("timestamp").toDouble(), 21)
46 | XCTAssertEqual(touchSequenceValue.valueForProperty("id").toDouble(), 42)
47 |
48 | let touchSampleValue = context.evaluateScript("new TouchSample({globalLocation: new Point({x: 20, y: 30}), timestamp: 22.0})")
49 | let appendedSequenceValue = touchSequenceValue.invokeMethod("sampleSequenceByAppendingSample", withArguments: [touchSampleValue])
50 | let appendedSamples = appendedSequenceValue.valueForProperty("samples").toArray() as! [TouchSampleBridge]
51 | XCTAssertEqual(appendedSamples.count, 3)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Prototype.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Prototype.swift
3 | // Prototope
4 | //
5 | // Created by Andy Matuschak on 2/9/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import swiftz_core
11 |
12 | struct Prototype {
13 | var mainScript: NSData
14 | var resources: [String: NSData]
15 | }
16 |
17 | extension Prototype {
18 | init?(url: NSURL) {
19 | // TODO: return a Result, kill printlns
20 | if !url.fileURL { return nil }
21 |
22 | var error: NSError? = nil
23 | let path = url.filePathURL!.path!
24 |
25 | var isDirectory: ObjCBool = ObjCBool(false)
26 | let exists = NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectory)
27 | if !exists {
28 | println("File does not exist: \(path)")
29 | return nil
30 | }
31 |
32 | var mainScriptPath: String
33 | self.resources = [:]
34 | if isDirectory.boolValue {
35 | var error: NSError? = nil
36 | let contents = NSFileManager.defaultManager().contentsOfDirectoryAtPath(path, error: &error) as! [String]?
37 | if contents == nil {
38 | println("Couldn't read directory \(path): \(error)")
39 | return nil
40 | }
41 |
42 | let javaScriptFiles = contents!.filter { $0.pathExtension == "js" }
43 | switch javaScriptFiles.count {
44 | case 0:
45 | println("No JavaScript files found in \(path)")
46 | return nil
47 | case 1:
48 | mainScriptPath = path.stringByAppendingPathComponent(javaScriptFiles.first!)
49 | default:
50 | println("Multiple JavaScript files found in \(path): \(javaScriptFiles)")
51 | return nil
52 | }
53 |
54 | var resourceExtensions = Set(["png", "caf", "aif", "aiff", "wav", "otf", "ttf"])
55 | for resources in contents!.filter({ resourceExtensions.contains($0.pathExtension) }) {
56 | let resourcePath = path.stringByAppendingPathComponent(resources)
57 | if let resourceData = NSData(contentsOfFile: resourcePath, options: nil, error: &error) {
58 | self.resources[resources] = resourceData
59 | }
60 | }
61 |
62 | } else {
63 | mainScriptPath = path
64 | }
65 |
66 | if let mainScriptData = NSData(contentsOfFile: mainScriptPath, options: nil, error: &error) {
67 | self.mainScript = mainScriptData
68 | } else {
69 | println("Failed to read main script: \(mainScriptPath): \(error)")
70 | return nil
71 | }
72 | }
73 | }
74 |
75 | extension Prototype: JSON {
76 | private static func create(mainScript: NSData)(resources: [String: NSData]) -> Prototype { return Prototype(mainScript: mainScript, resources: resources) }
77 |
78 | static func fromJSON(jsonValue: JSONValue) -> Prototype? {
79 | switch jsonValue {
80 | case let .JSONObject(dictionary):
81 | return create
82 | <^> (dictionary["mainScript"] >>- NSDataJSONCoder.fromJSON)
83 | <*> (dictionary["resources"] >>- JDictionaryFrom.fromJSON)
84 | default:
85 | return nil
86 | }
87 | }
88 |
89 | static func toJSON(prototype: Prototype) -> JSONValue {
90 | return .JSONObject([
91 | "mainScript": NSDataJSONCoder.toJSON(prototype.mainScript),
92 | "resources": JDictionaryTo.toJSON(prototype.resources)
93 | ])
94 | }
95 | }
96 |
97 | struct NSDataJSONCoder: JSON {
98 | static func fromJSON(jsonValue: JSONValue) -> NSData? {
99 | return JString.fromJSON(jsonValue) >>- { NSData(base64EncodedString: $0, options: nil) }
100 | }
101 |
102 | static func toJSON(data: NSData) -> JSONValue {
103 | return JString.toJSON(data.base64EncodedStringWithOptions(nil))
104 | }
105 | }
--------------------------------------------------------------------------------
/Protocaster/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Protocaster
4 | //
5 | // Created by Andy Matuschak on 2/6/15.
6 | // Copyright (c) 2015 Khan Academy. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import swiftz_core
11 |
12 | let LastSelectedDeviceNameKey = "LastSelectedDeviceNameKey"
13 | let LastSelectedPathURLKey = "LastSelectedPathURLKey"
14 |
15 | class ViewController: NSViewController {
16 |
17 | var selectedPathDidChange: (NSURL? -> ())?
18 | var selectedDeviceDidChange: (NSNetService? -> ())?
19 |
20 | var selectedDeviceSession: NSNetService?
21 |
22 | var lastSelectedDeviceName: NSString? {
23 | get {
24 | return NSUserDefaults.standardUserDefaults().stringForKey(LastSelectedDeviceNameKey)
25 | }
26 |
27 | set {
28 | NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: LastSelectedDeviceNameKey)
29 | }
30 | }
31 |
32 | var lastSelectedPathURL: NSURL? {
33 | get {
34 | return NSUserDefaults.standardUserDefaults().URLForKey(LastSelectedPathURLKey)
35 | }
36 |
37 | set {
38 | if let URL = newValue {
39 | NSUserDefaults.standardUserDefaults().setURL(URL, forKey: LastSelectedPathURLKey)
40 | } else {
41 | NSUserDefaults.standardUserDefaults().setNilValueForKey(LastSelectedPathURLKey)
42 | }
43 | }
44 | }
45 |
46 | @IBOutlet weak var pathControl: NSPathControl!
47 | @IBOutlet var deviceListController: NSArrayController!
48 | @IBOutlet weak var deviceChooserButton: NSPopUpButton!
49 | @IBOutlet weak var deviceSettingsCheckbox: NSButton!
50 |
51 | override func awakeFromNib() {
52 | pathControl.URL = lastSelectedPathURL
53 | }
54 |
55 | @IBAction func pathControlDidChange(sender: NSPathControl) {
56 | selectedPathDidChange?(sender.URL)
57 | if let URL = sender.URL {
58 | lastSelectedPathURL = URL
59 | }
60 | }
61 |
62 | @IBAction func deviceSelectionDidChange(sender: NSPopUpButton) {
63 | let service = sender.selectedItem?.representedObject as! NSNetService?
64 | toggleCheckboxForService(service)
65 | selectedDeviceDidChange?(service)
66 | }
67 |
68 | func addService(service: NSNetService) {
69 | deviceListController.addObject(service)
70 | if service.name == lastSelectedDeviceName {
71 | selectedDeviceDidChange?(service)
72 | deviceChooserButton.selectItemWithTitle(service.name)
73 | }
74 | toggleCheckboxForService(service)
75 | }
76 |
77 |
78 | func toggleCheckboxForService(service: NSNetService?) {
79 | let currentlySelectedDeviceName = self.deviceChooserButton.selectedItem?.title
80 | let serviceName = service?.name
81 | if currentlySelectedDeviceName != serviceName {
82 | return // we don't care!
83 | }
84 |
85 | if serviceName == lastSelectedDeviceName {
86 | deviceSettingsCheckbox.state = NSOnState
87 | } else {
88 | deviceSettingsCheckbox.state = NSOffState
89 | }
90 | }
91 |
92 | func removeService(service: NSNetService) {
93 | deviceListController.removeObject(service)
94 | }
95 |
96 | @IBAction func checkboxDidChange(sender: NSButton) {
97 | let currentlySelectedDeviceName = self.deviceChooserButton.selectedItem?.title
98 |
99 | if sender.state == NSOnState {
100 | self.lastSelectedDeviceName = currentlySelectedDeviceName
101 | } else if self.lastSelectedDeviceName == currentlySelectedDeviceName {
102 | // We've unchecked saving the current device, so forget its name
103 | self.lastSelectedDeviceName = nil
104 | }
105 | }
106 | }
107 |
108 |
--------------------------------------------------------------------------------