Swift on Android
Eric Wing blurrrsdk.com
try! Swift Tokyo 2017 Need to be quick… Android dev is a large topic for the uninitiated
• (Obligatory) Why Swift on Android?
• A native language that is cross-platform (like C & C++)
• I’ll assume everybody here already here already sees the value
• Note: I put extra things in my slides that, I won’t have time to talk about, but are to help you in the future when you refer back to the talk. Yes, the real Apple Swift My Background: Worn lots of hats
• Prove to you I actually know what I’m talking about
• Not just an Android dev
• Also a seasoned Mac/iOS dev (and other platforms too)
• I will act as your bridge between worlds
• I look at this holistically from the vantage point of computer history & fundamentals Globalstar: Satellites & Rockets
• Global communication system based on constellation of satellites and ground stations
• Launch satellites into space with rockets!
• (Not relevant for this talk, but I’m told it sounds cool) From Cross-platform to Native Cocoa
• Cross-platform, Scientific Visualization
• End of the Unix Wars => Microsoft Windows domination
• Mac OS X: A Unix with a user-friendly UI
• Meld cross-platform OpenGL sci-viz with native Cocoa UI for best experience Open Source Projects
• Got involved in some open source projects
• Usually related to improving Apple platform support LuaCocoa
• Wrote world’s first full-featured bridge between Lua & Cocoa
• Obj-C runtime + libffi + Mac OS X 10.5 BridgeSupport
• Complete API coverage including C APIs
• Dual mode: Obj-C garbage collection & traditional
• PowerPC/Intel, 32-bit/64-bit Universal Binaries Beginning iPhone Games Development Commercial Game Engines Corona SDK Platino (Lua) (JavaScript)
• Primary platforms: iOS & Android
• Also: Mac & Windows First “proper” Swift/Android app
https://youtu.be/7fVC0245Io8 https://youtu.be/w6FY_qSi8yY Ouroboros: Eternal cycle of life & death What’s old is new again
• Nothing in this talk is “new”
• Simply re-applying old concepts in different ways
• Compilers / Native code
• C / Application Binary Interface (ABI)
• Unix / Linux / Android
• Recognizing this may make this topic easier to understand C interoperability
• C is the most portable language
• Every platform has a C compiler (even the web with Emscripten)
• The C ABI is stable and everything is built on it
• Most languages have a way to talk to C
• Swift provides one of the easiest & most direct ways
• Decades of pre-existing software
• A lot still useful today Language vs. Libraries
• Helpful to not confuse the two
Swift “standard” libraries
• Swift C library • AppKit / UIKit • Darwin • libdispatch • swiftCore • Core Graphics • GlibC • Foundation • Core Audio • Bionic
Minimal Many Language vs. Libraries
• Using Swift on Android (or other platforms) doesn’t mean using Cocoa
Native • Swift C • libdispatch • UIKit iOS • swiftCore library • Foundation • Core Audio App • Darwin
• Swift C Native • Android SDK • swiftCore library Android App • OpenSL ES • Bionic
Cross-platform • SDL • swiftCore Game • OpenAL App Development vs. Server Development (i.e. Extra things that Swift on Linux devs don’t do)
• Binaries must run on end-users native machines
• All dependencies must be bundled with app or provided by the OS
• Should not require installers or users to pre-install additional things
• And never root access!
• Apps have resources that must be bundled
• e.g. images, sounds, data sets
• Ideal: Third-party libraries can also be in binary form (requires stable ABI) We need to build real user-facing (Android) apps
(Mostly) Okay Wrong Wrong Android NDK (Native Development Kit)
• All Android apps must be written using the Android SDK which is in Java
• Android NDK (Native Development Kit) allows you to create dynamic libraries using native code
• All GUI APIs (and almost everything else) are exclusive to the Android SDK, i.e. Java only
• Your Android app must use Java’s loadLibrary to load the dynamic libraries, and then communicate using JNI Swift & the Android NDK
• NDK intended for C & C++, but Swift works too
• Swift itself is written in C++ (which has consequences we’ll get too)
• Your Swift programs are compiled to native code
• So your programs live on the NDK side
• Thus you must use the Android NDK The Android NDK “Really Does Suck”
• John Carmack - “Half-baked”, “Really does suck”
• Second class citizen on Android
• IDE and build systems not well integrated
• Almost no Android libraries are provided in the NDK
• Lots of things are broken, slow to get fixed, if ever
• Word on the street (few years ago): Only 2 full-time Google engineers + a few part time
• Consistent with number of Google employees on NDK mailing list
• No slight intended on those 2 engineers. Valiant effort. Google treats them as the black sheep.
• Google: Among the richest, powerful companies in the world with #1 dominance in mobile, and this is the best effort Google chooses to put in Bionic (Android’s C standard library)
• Android does not use glibc (unlike “real” Linux)
• Wrote their own called “Bionic”
• Doesn’t care about POSIX compliance
• Doesn’t even care about ANSI compliance
• 8 years into Android, still terrible Lua 5.2 (2013) builds with Turbo C 1.0 (1990) for MS- DOS without modification https://youtu.be/-jvLY5pUwic Lua 5.2 (2013) builds with Turbo C 1.0 (1990) for MS- DOS without modification https://youtu.be/-jvLY5pUwic
Android NDK (r11c) fails to compile Lua Insights into Bionic
https://mail-index.netbsd.org/tech-userlevel/2012/07/25/msg006571.html Terrible performance bug for strlcpy
• Wrote a test program to run Test262 suite on JavaScriptCore
• Runs through 11,000+ files
• Used strlcpy
• Mac, iOS so fast that I didn’t think about it
• Android took: 9000 ms
• Switch to strncpy + manual NULL term: 14 ms
• Maybe I should be impressed they provide this function at all? Android SDK/Java isn’t that much better
• Get a list of files (‘ls’) in a directory of an APK is well known to have a serious performance bug
• Get list 11,000+ files: I killed the process after 3 hours of waiting Android file system and the .apk • Files that ship with your app are inside the “.apk” (think .zip)
• Can’t use standard C file family (fopen, fread)
• Needs a “God” object from the Java Android Activity or Context class
• AAsset* AAssetManager_open(AAssetManager *mgr, const char *filename, int mode);
• Existing cross-platform (ANSI) C/C++ libraries won’t work without modification
• Places outside the .apk can use C file family
• “Internal Storage”, but may not have much storage space
• “External Storage”, but may not exist or have correct access permissions Dynamic Library System wonky too
• System.loadLibrary doesn’t automatically load dependencies
• Load must be manually done & in the correct order
• Will silently ignore if a library by the same name is already loaded
• Never use soname versioning and never use symlinks
• (Android further tightened the soname rules in 6.0 and 7.0)
• (Lots of other gotchas I’m omitting for time) Fight Club
• What is the First Rule of Library Programming?
• You do not use C++
• What is the Second Rule of Library Programming?
• ? Swift & C++ (and how it affects you)
• Swift is implemented in C++ and uses the C++ standard library
• Swift has runtime dependencies that use C++ and the C++ standard library
• Swift’s standard libraries are implemented in C++ and use the C++ standard library
• Thus C++ issues are propagated forward to you (inherited) Android NDK and C++
• Android NDK provides 5 different C++ standard libraries you have to choose from
• libstdc++, gabi++, stlport, gnustl, libc++
• All are incompatible with each other
• NDK API level doesn't help either
• C++ standard library does not guarantee a stable ABI so every NDK update potentially breaks
• If you dynamically link
• You must bundle with your app because Android does not ship one with the OS
• In contrast Apple, ships system wide and tries to keep the ABI stable/backwards compatible Android NDK and C++
• In the real world…
• People build binaries of libraries and share them
• People don’t upgrade NDKs at the same time and versions get mixed
• People use multiple libraries, all built under different NDK versions
• So the final application must include a copy all these different C++ library versions
• But Android doesn’t name versions differently
• So files overwrite each other
• And bad things can happen
• In contrast, Microsoft Visual Studio at least has the sense to put the version number in the file name (e.g. msvcp140.dll) Android NDK and C++
• So we should statically link, right? Android NDK and C++
• So we should statically link, right? Android NDK and C++
• So we should statically link, right?
• Android documentation warnings: Android NDK and C++
• So Lose, Lose
• Thanks (for nothing) Google
• In practice, I personally found static linking to work better Fight Club
• What is the Second Rule of Library Programming?
• You do not use C++
• Unfortunately, Swift uses a ton of it, so we must pay
• Static linking possible, but altering the Swift build system is sooooo hard :( Let’s build Swift for Android: Dependency hell
• Android comes with almost no libraries for the NDK, so we must build all Swift dependencies
• Remember: we build user-facing apps
• All dependencies must be bundled with app (or statically linked) libICU (used by swiftCore): Let’s talk about the “(Mostly)”
(Mostly) Okay libICU: “DLL hell” problem
• Android manufacturers or Android itself may use libICU internally
• If it is used, when we load our library, the attempt silently fails and we call into the internal one
• Bad things can happen if the versions don’t match
• (libICU likes to break the API between versions)
• Static linking can avoid this (but build system hell)
• ICU has switches to modify symbol names differently and rename libraries
• watch out for soname (name and versioning) Good news! You can now write Swift apps
• Once you make it past this, you should be able to build:
• The Swift compiler
• swiftCore
• Swift C library
• This is (more than) enough to write full-fledged, complete Swift apps Foundation dependencies • libxml (C)
• libcurl (C)
• Nice library and very stable ABI
• But depends on OpenSSL which is a headache
• Extremely difficult build system (no Android aarch64/x64 yet)
• In constant need of security updates (Google scans binaries)
• C++ standard library (Bug SR-23 as an example of ABI hell)
• libdispatch How to use Swift in a real Android app
• Instead, we must build a proper Android/Java app, and start from Java
• Follow standard NDK techniques Wrong Wrong Android/Java: Load dynamic libraries in starting Activity public class MyLaunchActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// Note: Order matters! System.loadLibrary("c++_shared"); // (if we didn't compile statically)
// Notice that I renamed the ICU libraries & symbols System.loadLibrary(“icudataswift"); System.loadLibrary("icuucswift"); System.loadLibrary("icui18nswift");
System.loadLibrary("swiftSwiftOnoneSupport"); // used by swiftCore System.loadLibrary("swiftCore");
System.loadLibrary("swiftGlibc"); // Should be named Bionic
// System.loadLibrary("dispatch"); // System.loadLibrary(“Foundation"); // (may have additional dependencies)
// This is our Swift code compiled into a dynamic library System.loadLibrary("MySwiftMainProgram"); } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Android/Java: Call into Native (C) code
package com.blurrrsdk.app; public class MyLaunchActivity extends Activity {
// Tells Java/JNI we promise to implement this function in native code // Activity is unused in this example, but useful as ‘God’ object public native void MyMainEntry(Activity activity);
@Override protected void onStart() { super.onStart(); // Call into native code MyMainEntry(this); } } Now in C: Call into Swift
#include
// Promise C we've implemented this function. extern void MyMain(void);
JNIEXPORT void JNICALL Java_com_blurrrsdk_MyLaunchActivity_MyMainEntry( JNIEnv* jni_env, jobject thiz, jobject activity) { // Call into Swift MyMain(); } Finally in Swift
@_cdecl("MyMain") public func MyMain() { // Now the fun can begin! //
let four = 2 + 2 // TODO/FIXME: print goes to /dev/null on Android print("2 + 2 = \(four)”)
// Remember to not block the event loop } Other details
• Astute viewers may see a shortcut on how to call directly from Java to Swift
• Long-form done for clarity
• The underscore in @_cdecl means it is not finalized. When it is, remove the _
• Do not use @_silgen_name. It was not intended for this purpose and may break
• AndroidManifest.xml (declare start Activity)
• Package everything properly into .apk
• Java parts + Native parts
• Don’t forget all dependencies Swift Differences on Android
• No Objective-C
• No Xcode
• Foundation & libdispatch
• large binary size
• (libICU mostly at fault, ~30MB per arch) Caveats
• Requires Android 5.1+ (thanks to an Android 5.0 deadlock regression bug with stdio)
• Must use Linux to develop (for now)
• armv7 only (for now)
• no main on Android (don’t bother with main.swift)
• print() goes to /dev/null
• adb shell setprop log.redirect-stdio true does not work for NDK side
• Use __android_log_print
• TODO: Put this in Swift implementation directly
• static/global variables Static & Global variables not reinitialized in NDK (actual exit, not background/resume) var g_isInit = false; public func MyEntry() { if(!g_isInit) { DoImportantInitStuff(); Run on g_isInit = true; first launch } else { Now set to true AlreadyInitStuff(); } } Static & Global variables not reinitialized in NDK (actual exit, not background/resume) var g_isInit = false; May not re-initialize on second launch public func MyEntry() { Still set to true on second launch if(!g_isInit) { DoImportantInitStuff(); Does not get run g_isInit = true; on second launch } else { AlreadyInitStuff(); } Run instead of } DoImportantInitStuff() Static & Global variables not reinitialized in NDK (actual exit, not background/resume) var g_isInit = false; public func MyInit() { Try to design a way to force g_isInit = false; re-initialize variables at start } public func MyEntry() { if(!g_isInit) { DoImportantInitStuff(); g_isInit = true; } else { AlreadyInitStuff(); } } Build Systems (Ugh…) Build Systems (Ugh…)
• Languages are standardized, but every platform build system is radically different & changing
• Lots of BS, but important to get right
• Bleeds into user experience if done wrong
• Poor handling leads to installers, admin access, dependency hell, hard coded paths, user hoop jumping, long build times, complexity
• People tend to dismiss or ignore this problem, use band-aids
• (I may be doing additional disservice by speeding past this topic too) Build System Options
• Swift Package Manager
• Doesn’t really understand cross-platform app development
• Complex C dependencies?
• Do-it-yourself
• hard to scale cross-platform Build System Options
• Used to build LLVM, Clang, & Swift
• Officially being added to Android
• Much better at cross-platform requirements
• But no Swift
• So I started implementing support…
• https://github.com/ewmailing/CMake CMake for Swift example
• It can build executables
• It can build dynamic libraries, which we need for Android
• It can mix with C code and link to C libraries
• It supports bridging headers
• It supports Xcode and Makefiles
• The major thing still to do is linking to other Swift modules cmake_minimum_required(VERSION 3.4) project(MySwiftProject C Swift) if(ANDROID) # Must build library on Android add_library( SwiftApp CCode.c SwiftCode.swift) else() add_executable(SwiftApp CCode.c SwiftCode.swift main.swift) endif() set_property(TARGET SwiftApp PROPERTY SWIFT_BRIDGING_HEADER "MyBridgingHeader.h") Let’s talk about Libraries for writing Android apps
1. Native Android-only development in Swift
2. Cross-platform development (non-native GUI)
3. Cross-platform native GUI Native Android in Swift
• Painful: Must jump back and forth between Java
• Sub-classing Java classes not possible through JNI
• Requires byte code hacks or automatic code generation
• Again: Nothing new here
• Write higher level libraries to encapsulate/hide/reuse/minimize all the JNI code you normally have to write
• Or develop code generation tools Cross-platform (C) libraries • Already solved, especially by the game industry
• Already cross-platform far beyond just Mac/iOS/Android
• SDL (Simple DirectMedia Layer)
• OpenGL, OpenAL
• FreeType
• libpng, libjpeg
• cURL
• Chipmunk Physics FlappyBlurrr Swift
• Flappy Bird contains a lot of subtle details most people overlook
• 60 fps, small footprint
• Real Earth gravity (physics)
• But customized collisions & movement contrary to physics engine
• (e.g. face-plant, no bouncing off pipes)
• Graphics, sound, physics, user input, GUI
• Works on Android, Mac, iOS, Linux, Raspberry Pi, and pretty much everywhere else where Swift is available
What about (non-native) GUI?
• Many choices, many trade-offs
• My current favorite: Nuklear
• Immediate Mode GUI
• Pure C
• No dependences (header-only)
• Renderer agnostic What about (non-native) GUI?
• Many choices, many trade-offs
• My current favorite: Nuklear
• Immediate Mode GUI
• Pure C
• No dependences (header-only)
• Renderer agnostic Nuklear screenshots Nuklear on Android
https://youtu.be/3-MiceegZlM Nuklear on Android
https://youtu.be/3-MiceegZlM “Sparkle Sparkle” SIMD powered Particle Designer “Sparkle Sparkle” SIMD powered Particle Designer What about cross-platform native GUI?
• Still an “unsolved” problem
• Few solutions, all have major downsides
• Original APIs vs. Cocoa Huge casualty list just in Cocoa…
• OpenStep
• Yellow Box for Windows
• GNUStep
• Cocotron
• Apportable
• Marmalade Juice
• Microsoft Windows Bridge for iOS And truly native list is generally short
• Native: • Not (cross-platform) native: • wxWidgets (C++) • Qt (C++) • IUP (C) • GTK (C) • libui (C) • Tk (Tcl) • Appcelerator Titanium • Electron (JavaScript) (JavaScript) • Swing, AWT (Java) • React Native (JavaScript)
*And remember that only C APIs are callable by Swift IUP (Portable User Interface) A Hidden Gem
• Origins: Academic research project/paper from mid-1990’s
• PUC-Rio (Pontifical Catholic University of Rio de Janeiro, Brazil)
• Same university as Lua language
• Used by certain circles
• Oil industry, Scientific Visualization IUP features • True Native widgets
• Simple & small (library instead of framework)
• Unlike most others, GUI-only (not kitchen sink)
• Foundation + IUP = perfect fit (no redundancy/bloat)
• Pure C (for ultimate portability)
• Designed with language bindings in mind
• LED: Textual layout description language
• Can also be used for per-platform customized layout IUP + Swift?
• Very well thought out API
• Can deal with wide platform variations
• Not trapped in classically Object-Oriented mindset
• Recognized not all native platforms are OO
• Not all languages that want to bind are OO
• Centered around key-value system (e.g. think NSUserDefaults)
• IupSetAttribute(button, "TITLE", “Push Me"); Squint hard and see…
• Basically, this is Protocol-Oriented-Design
• (as best as can be done with weak types & limitations of C)
• Building a Swift layer around this presents fascinating possibilities
• Leverage Swift’s abilities to realize the full potential of POD
• Already lots of elegant solutions in this space
• try! Swift Tokyo 2016: “Swift Eye for the Stringly Typed API” by Andyy Hope IUP drawbacks
• Windows, GTK2, GTK3, Motif backends but…
• No Cocoa backend Of all the problems we could have… this is the best one
• Wait, I’ve been doing Cocoa for over a decade…
• And I’m speaking to a room filled with Cocoa & Cocoa Touch experts
• We can fix this! Linux
Windows Mac (IUP Cocoa, in progress) But what about mobile? We can fix this too!
• IUP’s design/API is well thought out / flexible
• Example: Every other API made the mistake of using a “Window” API
• IUP used “Dialog”, which has fewer pre-conceptions
• Map “Dialog” to
• iOS: NavigationController & ViewControllers
• Android: Activity
• (For more details, see my YouTube presentation: “IUP for iOS & Android”) Demo Program: Create Dialog & Button (recursive) var g_buttonCounter = 0; func BlurrrMain() -> Int32 { IupOpen(nil, nil) IupSetFunction("ENTRY_POINT", IupEntryPoint); IupMainLoop() return 0; } func IupEntryPoint(_ ih:OpaquePointer?) -> Int32 { g_buttonCounter = 0 return ShowNewDialogCallback(nil) } func ShowNewDialogCallback(_ ih:OpaquePointer?) -> Int32 { let button = IupButton(nil, nil) IupSetStrAttribute(button, "TITLE", "Iup Button \(g_buttonCounter)") IupSetCallback(button, "ACTION", ShowNewDialogCallback) let dialog = IupDialog(button) IupSetAttribute(dialog, "SIZE", "QUARTERxQUARTER") IupSetStrAttribute(dialog, "TITLE", "Iup Dialog \(g_buttonCounter)") IupShow(dialog) g_buttonCounter += 1 return IUP_DEFAULT } Ubuntu Linux (amd64) Ubuntu Linux (amd64) Raspberry Pi (Raspbian) Raspberry Pi (Raspbian) Mac Mac iOS iOS Android Android Surprise! Swift on Windows
*Extremely alpha Surprise! Swift on Windows
*Extremely alpha Re-cap of what we just saw
• Native GUI
• Cross-Platform
• Desktop & Mobile
• Single codebase
• In Swift IUP development links • IUP for iOS & Android intro video (with Mac backend cameo):
• https://www.youtube.com/watch?v=UvrEfOg3Nyk
• My repos
• https://github.com/ewmailing/IupCocoa
• https://github.com/ewmailing/IupCocoaTouch
• https://github.com/ewmailing/IupAndroid
• https://github.com/ewmailing/IupEmscripten
• Official IUP website
• http://webserver2.tecgraf.puc-rio.br/iup/ Blurrr SDK (My current endeavor)
• All the things I did in this presentation were done with Blurrr
• No magic: Everything comes from the core ideas I explained
• You can do it too!
• But lots of tedious work & esoteric knowledge
• Want you to focus on just your project code Blurrr SDK (My current endeavor)
• Blurrr deals with a lot of the annoying platform specific issues like build systems and event loop setup
• Provides a bunch of pre-built cross-platform libraries to get you going
• Download and go (no installers, no admin, depends only on native platform build tools)
• “Native” development in all aspects (not a second-class citizen)
• Designed for modularity, customizability, and extensibility Blurrr SDK Workflow Blurrr SDK Workflow
Android Studio Visual Studio Makefiles Xcode Blurrr SDK Workflow
Visual Android Studio Makefiles Xcode Studio Blurrr SDK
• Starting with the usual suspects, like SDL, cURL, etc.
• Best for games & multimedia apps at the immediate moment
• Looking to IUP next to support general app development Opening up the beta today https://blurrrsdk.com
• Free beta download in celebration of try! Swift
• (Thank you very much for inviting me to speak)
• Again, there’s no magic
• You can do it yourself, but I hope you’ll like Blurrr
• I really could use your support
• Additional Note: I will be at the Hackathon Saturday doing a game dev workshop. Come by and say hi! Links
• Blurrr SDK • BlurrrSDK.com, @BlurrrSDK • Eric Wing (@ewingfighter) • Website: playcontrol.net • YouTube: youtube.com/user/ewmailing • Now playing: “Why we loved Sierra Games” Carlos M. Icaza June 5, 1966 - May 17, 2016
Adobe Macromedia
Corona SDK Platino
@codinginswift
@CodingInSwift Meetup Silicon Valley, August 2014 @CodingInSwift Meetup Silicon Valley, August 2014 Carlos
me Chris Lattner
So…
• Something in his memory
• Also try to inspire you: showing what’s already possible with cross-platform Swift today
• Made with Blurrr
• (Carlos was kicking my butt to launch Blurrr already)
In Memory of Carlos Icaza 1966 - 2016
(He loved the mathematics of splines.) Designed & Programmed by Eric Wing playcontrol.net blurrrsdk.com
Background Art by Judy Rosenwaig www.behance.net/judyrosenwaig
"Late Snows of Winter" (music) by Jeffrey Roberts (jmr) (remix of "The Magic Meadow" by Mark Seibert) ocremix.org/remix/OCR02393 Made with Blurrr SDK blurrrsdk.com Thank you
Eric Wing blurrrsdk.com
(Office hours immediately following)