├── 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 | [![Build Status](https://travis-ci.org/Khan/Prototope.svg?branch=master)](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 | --------------------------------------------------------------------------------