Skip to content
Browse files

First commit.

  • Loading branch information...
0 parents commit a79a2f44cf942ae67cca31ca2281020c80c99c63 @phoboslab committed Sep 25, 2012
Showing with 16,187 additions and 0 deletions.
  1. +68 −0 App/index.js
  2. +72 −0 Classes/Ejecta/EJApp.h
  3. +333 −0 Classes/Ejecta/EJApp.m
  4. +15 −0 Classes/Ejecta/EJAudio/EJAudioSource.h
  5. +14 −0 Classes/Ejecta/EJAudio/EJAudioSourceAVAudio.h
  6. +68 −0 Classes/Ejecta/EJAudio/EJAudioSourceAVAudio.m
  7. +17 −0 Classes/Ejecta/EJAudio/EJAudioSourceOpenAL.h
  8. +177 −0 Classes/Ejecta/EJAudio/EJAudioSourceOpenAL.m
  9. +24 −0 Classes/Ejecta/EJAudio/EJBindingAudio.h
  10. +166 −0 Classes/Ejecta/EJAudio/EJBindingAudio.m
  11. +16 −0 Classes/Ejecta/EJAudio/EJOpenALManager.h
  12. +36 −0 Classes/Ejecta/EJAudio/EJOpenALManager.m
  13. +119 −0 Classes/Ejecta/EJBindingBase.h
  14. +113 −0 Classes/Ejecta/EJBindingBase.m
  15. +8 −0 Classes/Ejecta/EJBindingEjectaCore.h
  16. +96 −0 Classes/Ejecta/EJBindingEjectaCore.m
  17. +24 −0 Classes/Ejecta/EJBindingEventedBase.h
  18. +109 −0 Classes/Ejecta/EJBindingEventedBase.m
  19. +17 −0 Classes/Ejecta/EJCanvas/EAGLView.h
  20. +41 −0 Classes/Ejecta/EJCanvas/EAGLView.m
  21. +24 −0 Classes/Ejecta/EJCanvas/EJBindingCanvas.h
  22. +598 −0 Classes/Ejecta/EJCanvas/EJBindingCanvas.m
  23. +14 −0 Classes/Ejecta/EJCanvas/EJBindingImage.h
  24. +93 −0 Classes/Ejecta/EJCanvas/EJBindingImage.m
  25. +13 −0 Classes/Ejecta/EJCanvas/EJBindingImageData.h
  26. +66 −0 Classes/Ejecta/EJCanvas/EJBindingImageData.m
  27. +123 −0 Classes/Ejecta/EJCanvas/EJCanvasContext.h
  28. +338 −0 Classes/Ejecta/EJCanvas/EJCanvasContext.m
  29. +25 −0 Classes/Ejecta/EJCanvas/EJCanvasContextScreen.h
  30. +126 −0 Classes/Ejecta/EJCanvas/EJCanvasContextScreen.m
  31. +9 −0 Classes/Ejecta/EJCanvas/EJCanvasContextTexture.h
  32. +25 −0 Classes/Ejecta/EJCanvas/EJCanvasContextTexture.m
  33. +73 −0 Classes/Ejecta/EJCanvas/EJCanvasTypes.h
  34. +8 −0 Classes/Ejecta/EJCanvas/EJDrawable.h
  35. +16 −0 Classes/Ejecta/EJCanvas/EJImageData.h
  36. +25 −0 Classes/Ejecta/EJCanvas/EJImageData.m
  37. +35 −0 Classes/Ejecta/EJCanvas/EJPath.h
  38. +493 −0 Classes/Ejecta/EJCanvas/EJPath.mm
  39. +30 −0 Classes/Ejecta/EJCanvas/EJTexture.h
  40. +202 −0 Classes/Ejecta/EJCanvas/EJTexture.m
  41. +12 −0 Classes/Ejecta/EJConvert.h
  42. +103 −0 Classes/Ejecta/EJConvert.m
  43. +18 −0 Classes/Ejecta/EJTimer.h
  44. +39 −0 Classes/Ejecta/EJTimer.m
  45. +5 −0 Classes/Ejecta/EJUtils/EJBindingAccelerometer.h
  46. +27 −0 Classes/Ejecta/EJUtils/EJBindingAccelerometer.m
  47. +11 −0 Classes/Ejecta/EJUtils/EJBindingAdBanner.h
  48. +84 −0 Classes/Ejecta/EJUtils/EJBindingAdBanner.m
  49. +8 −0 Classes/Ejecta/EJUtils/EJBindingGameCenter.h
  50. +122 −0 Classes/Ejecta/EJUtils/EJBindingGameCenter.m
  51. +8 −0 Classes/Ejecta/EJUtils/EJBindingLocalStorage.h
  52. +42 −0 Classes/Ejecta/EJUtils/EJBindingLocalStorage.m
  53. +12 −0 Classes/Ejecta/EJUtils/EJBindingTouchInput.h
  54. +50 −0 Classes/Ejecta/EJUtils/EJBindingTouchInput.m
  55. +11 −0 Classes/EjectaAppDelegate.h
  56. +60 −0 Classes/EjectaAppDelegate.m
  57. BIN Default-568h@2x.png
  58. BIN Default-Landscape.png
  59. BIN Default-Portrait.png
  60. BIN Default.png
  61. BIN Default@2x.png
  62. +52 −0 Ejecta-Info.plist
  63. +598 −0 Ejecta.xcodeproj/project.pbxproj
  64. +7 −0 Ejecta.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  65. BIN Ejecta.xcodeproj/project.xcworkspace/xcuserdata/dominic.xcuserdatad/UserInterfaceState.xcuserstate
  66. +10 −0 Ejecta.xcodeproj/project.xcworkspace/xcuserdata/dominic.xcuserdatad/WorkspaceSettings.xcsettings
  67. +473 −0 Ejecta.xcodeproj/xcuserdata/dominic.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist
  68. +88 −0 Ejecta.xcodeproj/xcuserdata/dominic.xcuserdatad/xcschemes/Ejecta.xcscheme
  69. +22 −0 Ejecta.xcodeproj/xcuserdata/dominic.xcuserdatad/xcschemes/xcschememanagement.plist
  70. +8 −0 Ejecta_Prefix.pch
  71. BIN Icon-72.png
  72. BIN Icon-72@2x.png
  73. BIN Icon.png
  74. BIN Icon@2x.png
  75. +138 −0 JavaScriptCore/JSBase.h
  76. +132 −0 JavaScriptCore/JSContextRef.h
  77. +694 −0 JavaScriptCore/JSObjectRef.h
  78. +145 −0 JavaScriptCore/JSStringRef.h
  79. +60 −0 JavaScriptCore/JSStringRefCF.h
  80. +301 −0 JavaScriptCore/JSValueRef.h
  81. +36 −0 JavaScriptCore/JavaScript.h
  82. +32 −0 JavaScriptCore/JavaScriptCore.h
  83. +923 −0 JavaScriptCore/WebKitAvailability.h
  84. +170 −0 MainWindow.xib
  85. +8 −0 README.md
  86. BIN Resources/line16px.png
  87. BIN Resources/line4px.png
  88. +243 −0 ejecta.js
  89. BIN libJavaScriptCore.a
  90. +5,925 −0 lodepng/lodepng.c
  91. +1,633 −0 lodepng/lodepng.h
  92. +8 −0 main.m
68 App/index.js
@@ -0,0 +1,68 @@
+var w = window.innerWidth;
+var h = window.innerHeight;
+var w2 = w/2;
+var h2 = h/2;
+
+var canvas = document.getElementById('canvas');
+canvas.width = w
+canvas.height = h;
+
+var ctx = canvas.getContext('2d');
+
+
+var curves = [];
+for( var i = 0; i < 200; i++ ) {
+ curves.push({
+ current: Math.random() * 1000,
+ inc: Math.random() * 0.005 + 0.002,
+ color: '#'+(Math.random()*0xFFFFFF<<0).toString(16) // Random color
+ });
+}
+
+var p = [0,0, 0,0, 0,0, 0,0];
+var animate = function() {
+ // Clear the screen - note that .globalAlpha is still honored,
+ // so this will only "darken" the sceen a bit
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.fillRect(0,0,w,h);
+
+ // Use the additive blend mode to draw the bezier curves
+ ctx.globalCompositeOperation = 'lighter';
+
+ // Calculate curve positions and draw
+ for( var i = 0; i < maxCurves; i++ ) {
+ var curve = curves[i];
+ curve.current += curve.inc;
+ for( var j = 0; j < p.length; j+=2 ) {
+ var a = Math.sin( curve.current * (j+3) * 373 * 0.0001 );
+ var b = Math.sin( curve.current * (j+5) * 927 * 0.0002 );
+ var c = Math.sin( curve.current * (j+5) * 573 * 0.0001 );
+ p[j] = (a * a * b + c * a + b) * w * c + w2;
+ p[j+1] = (a * b * b + c - a * b *c) * h2 + h2;
+ }
+
+ ctx.beginPath();
+ ctx.moveTo( p[0], p[1] );
+ ctx.bezierCurveTo( p[2], p[3], p[4], p[5], p[6], p[7] );
+ ctx.strokeStyle = curve.color;
+ ctx.stroke();
+ }
+};
+
+
+// The vertical touch position controls the number of curves;
+// horizontal controls the line width
+var maxCurves = 70;
+document.addEventListener( 'touchmove', function( ev ) {
+ ctx.lineWidth = (ev.touches[0].pageX/w) * 20;
+ maxCurves = Math.floor((ev.touches[0].pageY/h) * curves.length);
+}, false );
+
+
+
+ctx.fillStyle = '#000000';
+ctx.fillRect( 0, 0, w, h );
+
+ctx.globalAlpha = 0.05;
+ctx.lineWidth = 2;
+setInterval( animate, 16 );
72 Classes/Ejecta/EJApp.h
@@ -0,0 +1,72 @@
+#import <Foundation/Foundation.h>
+#import <QuartzCore/QuartzCore.h>
+#import "JavaScriptCore/JavaScriptCore.h"
+#import "EJConvert.h"
+
+#define EJECTA_VERSION @"1.0"
+#define EJECTA_APP_FOLDER @"App/"
+
+#define EJECTA_BOOT_JS @"../ejecta.js"
+#define EJECTA_MAIN_JS @"index.js"
+
+@protocol TouchDelegate
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
+@end
+
+
+@class EJCanvasContext;
+@class EJCanvasContextScreen;
+
+@interface EJApp : UIViewController {
+ BOOL paused;
+ JSGlobalContextRef jsGlobalContext;
+ UIWindow * window;
+ NSMutableDictionary * jsClasses;
+ UIImageView * loadingScreen;
+ NSObject<TouchDelegate> * touchDelegate;
+
+ int uniqueId;
+ NSMutableDictionary * timers;
+ NSTimeInterval currentTime;
+
+ CADisplayLink * displayLink;
+
+ NSOperationQueue * opQueue;
+ EJCanvasContext * currentRenderingContext;
+ EJCanvasContextScreen * screenRenderingContext;
+
+ float internalScaling;
+}
+
+- (id)initWithWindow:(UIWindow *)window;
+
+- (void)run:(CADisplayLink *)sender;
+- (void)pause;
+- (void)resume;
+- (JSValueRef)createTimer:(JSContextRef)ctx argc:(size_t)argc argv:(const JSValueRef [])argv repeat:(BOOL)repeat;
+- (JSValueRef)deleteTimer:(JSContextRef)ctx argc:(size_t)argc argv:(const JSValueRef [])argv;
+
+- (JSClassRef)getJSClassForClass:(id)classId;
+- (void)hideLoadingScreen;
+- (void)loadScriptAtPath:(NSString *)path;
+- (JSValueRef)invokeCallback:(JSObjectRef)callback thisObject:(JSObjectRef)thisObject argc:(size_t)argc argv:(const JSValueRef [])argv;
+- (void)logException:(JSValueRef)exception ctx:(JSContextRef)ctxp;
+
+
++ (EJApp *)instance;
++ (NSString *)pathForResource:(NSString *)resourcePath;
++ (BOOL)landscapeMode;
++ (BOOL)statusBarHidden;
+
+@property (nonatomic, readonly) JSGlobalContextRef jsGlobalContext;
+@property (nonatomic, readonly) UIWindow * window;
+@property (nonatomic, retain) NSObject<TouchDelegate> * touchDelegate;
+
+@property (nonatomic, readonly) NSOperationQueue * opQueue;
+@property (nonatomic, assign) EJCanvasContext * currentRenderingContext;
+@property (nonatomic, assign) EJCanvasContextScreen * screenRenderingContext;
+@property (nonatomic) float internalScaling;
+
+@end
333 Classes/Ejecta/EJApp.m
@@ -0,0 +1,333 @@
+#import <objc/runtime.h>
+
+#import "EJApp.h"
+#import "EJBindingBase.h"
+#import "EJCanvas/EJCanvasContext.h"
+#import "EJCanvas/EJCanvasContextScreen.h"
+#import "EJTimer.h"
+
+
+// ---------------------------------------------------------------------------------
+// JavaScript callback functions to retrieve and create instances of a native class
+
+JSValueRef ej_global_undefined;
+JSClassRef ej_constructorClass;
+JSValueRef ej_getNativeClass(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) {
+ CFStringRef className = JSStringCopyCFString( kCFAllocatorDefault, propertyNameJS );
+
+ JSObjectRef obj = NULL;
+ NSString * fullClassName = [NSString stringWithFormat:@"EJBinding%@", className];
+ id class = NSClassFromString(fullClassName);
+ if( class ) {
+ obj = JSObjectMake( ctx, ej_constructorClass, (void *)class );
+ }
+
+ CFRelease(className);
+ return obj ? obj : ej_global_undefined;
+}
+
+JSObjectRef ej_callAsConstructor(JSContextRef ctx, JSObjectRef constructor, size_t argc, const JSValueRef argv[], JSValueRef* exception) {
+ id class = (id)JSObjectGetPrivate( constructor );
+
+ JSClassRef jsClass = [[EJApp instance] getJSClassForClass:class];
+ JSObjectRef obj = JSObjectMake( ctx, jsClass, NULL );
+
+ id instance = [(EJBindingBase *)[class alloc] initWithContext:ctx object:obj argc:argc argv:argv];
+ JSObjectSetPrivate( obj, (void *)instance );
+
+ return obj;
+}
+
+
+
+
+// ---------------------------------------------------------------------------------
+// Ejecta Main Class implementation - this creates the JavaScript Context and loads
+// the initial JavaScript source files
+
+@implementation EJApp
+@synthesize jsGlobalContext;
+@synthesize window;
+@synthesize touchDelegate;
+
+@synthesize opQueue;
+@synthesize currentRenderingContext;
+@synthesize screenRenderingContext;
+@synthesize internalScaling;
+
+static EJApp * ejectaInstance = NULL;
+
++ (EJApp *)instance {
+ return ejectaInstance;
+}
+
+- (id)initWithWindow:(UIWindow *)windowp {
+ if( self = [super init] ) {
+
+ ejectaInstance = self;
+ window = windowp;
+ [window setRootViewController:self];
+ [UIApplication sharedApplication].idleTimerDisabled = YES;
+
+
+ // Show the loading screen - commented out for now.
+ // This causes some visual quirks on different devices, as the launch screen may be a
+ // different one than we loade here - let's rather show a black screen for 200ms...
+ //NSString * loadingScreenName = [EJApp landscapeMode] ? @"Default-Landscape.png" : @"Default-Portrait.png";
+ //loadingScreen = [[UIImageView alloc] initWithImage:[UIImage imageNamed:loadingScreenName]];
+ //loadingScreen.frame = self.view.bounds;
+ //[self.view addSubview:loadingScreen];
+
+ paused = false;
+ internalScaling = 1;
+
+
+ opQueue = [[NSOperationQueue alloc] init];
+ timers = [[NSMutableDictionary alloc] init];
+
+ displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(run:)] retain];
+ [displayLink setFrameInterval:1];
+ [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+
+
+ // Create the global JS context and attach the 'Ejecta' object
+ jsClasses = [[NSMutableDictionary alloc] init];
+
+ JSClassDefinition constructorClassDef = kJSClassDefinitionEmpty;
+ constructorClassDef.callAsConstructor = ej_callAsConstructor;
+ ej_constructorClass = JSClassCreate(&constructorClassDef);
+
+ JSClassDefinition globalClassDef = kJSClassDefinitionEmpty;
+ globalClassDef.getProperty = ej_getNativeClass;
+ JSClassRef globalClass = JSClassCreate(&globalClassDef);
+
+
+ jsGlobalContext = JSGlobalContextCreate(NULL);
+ ej_global_undefined = JSValueMakeUndefined(jsGlobalContext);
+ JSValueProtect(jsGlobalContext, ej_global_undefined);
+ JSObjectRef globalObject = JSContextGetGlobalObject(jsGlobalContext);
+
+ JSObjectRef iosObject = JSObjectMake( jsGlobalContext, globalClass, NULL );
+ JSObjectSetProperty(
+ jsGlobalContext, globalObject,
+ JSStringCreateWithUTF8CString("Ejecta"), iosObject,
+ kJSPropertyAttributeDontDelete | kJSPropertyAttributeReadOnly, NULL
+ );
+
+ // Load the initial JavaScript source files
+ [self loadScriptAtPath:EJECTA_BOOT_JS];
+ [self loadScriptAtPath:EJECTA_MAIN_JS];
+ }
+ return self;
+}
+
+
+- (void)dealloc {
+ JSGlobalContextRelease(jsGlobalContext);
+ [touchDelegate release];
+ [jsClasses release];
+ [opQueue release];
+
+ [displayLink release];
+ [timers release];
+ [super dealloc];
+}
+
+
+-(NSUInteger)supportedInterfaceOrientations {
+ return [EJApp landscapeMode] ? UIInterfaceOrientationMaskLandscape : UIInterfaceOrientationMaskPortrait;
+}
+
+
+- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation {
+ // Deprecated in iOS6 - supportedInterfaceOrientations is the new way to do this
+ BOOL landscape = [EJApp landscapeMode];
+ return (
+ (landscape && UIInterfaceOrientationIsLandscape(orientation)) ||
+ (!landscape && UIInterfaceOrientationIsPortrait(orientation))
+ );
+}
+
+
+// ---------------------------------------------------------------------------------
+// The run loop
+
+- (void)run:(CADisplayLink *)sender {
+ if( paused ) { return; }
+
+ // Check all timers
+ currentTime = [NSDate timeIntervalSinceReferenceDate];
+ for( NSNumber *timerId in [timers allKeys]) {
+
+ EJTimer * timer = [timers objectForKey:timerId];
+ [timer check:currentTime];
+
+ if( !timer.active ) {
+ [timers removeObjectForKey:timerId];
+ }
+ }
+
+ // Redraw the canvas
+ [screenRenderingContext present];
+}
+
+
+- (void)pause {
+ [displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+ paused = true;
+}
+
+
+- (void)resume {
+ [screenRenderingContext resetGLContext];
+ [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
+ paused = false;
+}
+
+
+- (void)hideLoadingScreen {
+ //[loadingScreen removeFromSuperview];
+ //[loadingScreen release];
+ //loadingScreen = nil;
+}
+
+
+// ---------------------------------------------------------------------------------
+// Script loading and execution
+
+- (void)loadScriptAtPath:(NSString *)path {
+ NSString * script = [NSString stringWithContentsOfFile:[EJApp pathForResource:path] encoding:NSUTF8StringEncoding error:NULL];
+
+ if( !script ) {
+ NSLog(@"Error: Can't Find Script %@", path );
+ return;
+ }
+
+ NSLog(@"Loading Script: %@", path );
+ JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)script);
+ JSStringRef pathJS = JSStringCreateWithCFString((CFStringRef)path);
+
+ JSValueRef exception = NULL;
+ JSEvaluateScript( jsGlobalContext, scriptJS, NULL, pathJS, 0, &exception );
+ [self logException:exception ctx:jsGlobalContext];
+
+ JSStringRelease( scriptJS );
+}
+
+- (JSValueRef)invokeCallback:(JSObjectRef)callback thisObject:(JSObjectRef)thisObject argc:(size_t)argc argv:(const JSValueRef [])argv {
+ JSValueRef exception = NULL;
+ JSValueRef result = JSObjectCallAsFunction( jsGlobalContext, callback, thisObject, argc, argv, &exception );
+ [self logException:exception ctx:jsGlobalContext];
+ return result;
+}
+
+- (JSClassRef)getJSClassForClass:(id)classId {
+ JSClassRef jsClass = [[jsClasses objectForKey:classId] pointerValue];
+
+ // Not already loaded? Ask the objc class for the JSClassRef!
+ if( !jsClass ) {
+ jsClass = [classId getJSClass];
+ [jsClasses setObject:[NSValue valueWithPointer:jsClass] forKey:classId];
+ }
+ return jsClass;
+}
+
+- (void)logException:(JSValueRef)exception ctx:(JSContextRef)ctxp {
+ if( !exception ) return;
+
+ JSStringRef jsLinePropertyName = JSStringCreateWithUTF8CString("line");
+ JSStringRef jsFilePropertyName = JSStringCreateWithUTF8CString("sourceURL");
+
+ JSObjectRef exObject = JSValueToObject( ctxp, exception, NULL );
+ JSValueRef line = JSObjectGetProperty( ctxp, exObject, jsLinePropertyName, NULL );
+ JSValueRef file = JSObjectGetProperty( ctxp, exObject, jsFilePropertyName, NULL );
+
+ NSLog(
+ @"%@ at line %@ in %@",
+ JSValueToNSString( ctxp, exception ),
+ JSValueToNSString( ctxp, line ),
+ JSValueToNSString( ctxp, file )
+ );
+
+ JSStringRelease( jsLinePropertyName );
+ JSStringRelease( jsFilePropertyName );
+}
+
+
+
+// ---------------------------------------------------------------------------------
+// Touch handlers
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ [touchDelegate touchesBegan:touches withEvent:event];
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+ [touchDelegate touchesEnded:touches withEvent:event];
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+ [touchDelegate touchesMoved:touches withEvent:event];
+}
+
+
+
+// ---------------------------------------------------------------------------------
+// Timers
+
+- (JSValueRef)createTimer:(JSContextRef)ctxp argc:(size_t)argc argv:(const JSValueRef [])argv repeat:(BOOL)repeat {
+ if( argc != 2 || !JSValueIsObject(ctxp, argv[0]) || !JSValueIsNumber(jsGlobalContext, argv[1]) ) {
+ return NULL;
+ }
+
+ uniqueId++;
+ JSObjectRef func = JSValueToObject(ctxp, argv[0], NULL);
+ float interval = JSValueToNumberFast(ctxp, argv[1])/1000;
+
+ // Make sure short intervals (< 34ms) run each frame
+ if( interval < 0.034 ) {
+ interval = 0;
+ }
+
+ EJTimer * timer = [[EJTimer alloc] initWithCurrentTime:currentTime interval:interval callback:func repeat:repeat];
+ [timers setObject:timer forKey:[NSNumber numberWithInt:uniqueId]];
+ [timer release];
+
+ return JSValueMakeNumber( ctxp, uniqueId );
+}
+
+- (JSValueRef)deleteTimer:(JSContextRef)ctxp argc:(size_t)argc argv:(const JSValueRef [])argv {
+ if( argc != 1 || !JSValueIsNumber(ctxp, argv[0]) ) return NULL;
+
+ NSNumber * timerId = [NSNumber numberWithInt:(int)JSValueToNumberFast(ctxp, argv[0])];
+ [timers removeObjectForKey:timerId];
+
+ return NULL;
+}
+
+- (void)setCurrentRenderingContext:(EJCanvasContext *)renderingContext {
+ if( renderingContext != currentRenderingContext ) {
+ [currentRenderingContext flushBuffers];
+ [renderingContext prepare];
+ currentRenderingContext = renderingContext;
+ }
+}
+
+
+// ---------------------------------------------------------------------------------
+// Utilities
+
++ (NSString *)pathForResource:(NSString *)path {
+ return [NSString stringWithFormat:@"%@/" EJECTA_APP_FOLDER "%@", [[NSBundle mainBundle] resourcePath], path];
+}
+
++ (BOOL)landscapeMode {
+ return [[[[NSBundle mainBundle] infoDictionary]
+ objectForKey:@"UIInterfaceOrientation"] hasPrefix:@"UIInterfaceOrientationLandscape"];
+}
+
++ (BOOL)statusBarHidden {
+ return [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"UIStatusBarHidden"] boolValue];
+}
+
+@end
15 Classes/Ejecta/EJAudio/EJAudioSource.h
@@ -0,0 +1,15 @@
+#import <UIKit/UIKit.h>
+
+
+@protocol EJAudioSource
+
+- (id)initWithPath:(NSString *)path;
+- (void)load;
+- (void)play;
+- (void)pause;
+- (void)setLooping:(BOOL)loop;
+- (void)setVolume:(float)volume;
+- (float)getCurrentTime;
+- (void)setCurrentTime:(float)time;
+
+@end
14 Classes/Ejecta/EJAudio/EJAudioSourceAVAudio.h
@@ -0,0 +1,14 @@
+#import <Foundation/Foundation.h>
+#import <AVFoundation/AVFoundation.h>
+
+#import "EJAudioSource.h"
+
+@interface EJAudioSourceAVAudio : NSObject <EJAudioSource> {
+ NSString * path;
+ AVAudioPlayer * player;
+ NSObject<AVAudioPlayerDelegate> * delegate;
+}
+
+@property (nonatomic, assign) NSObject<AVAudioPlayerDelegate> * delegate;
+
+@end
68 Classes/Ejecta/EJAudio/EJAudioSourceAVAudio.m
@@ -0,0 +1,68 @@
+#import "EJAudioSourceAVAudio.h"
+
+
+@implementation EJAudioSourceAVAudio
+
+@synthesize delegate;
+
+- (id)initWithPath:(NSString *)pathp {
+ if( self = [super init] ) {
+ path = [pathp retain];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [path release];
+ [player release];
+
+ [super dealloc];
+}
+
+- (void)setDelegate:(NSObject<AVAudioPlayerDelegate> *)delegatep {
+ delegate = delegatep;
+ if( player ) {
+ player.delegate = delegate;
+ }
+}
+
+- (void)load {
+ if( player || !path ) return; // already loaded or no path set?
+
+ player = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:nil];
+ if( delegate ) {
+ player.delegate = delegate;
+ }
+}
+
+- (void)play {
+ [self load];
+ [player play];
+}
+
+- (void)pause {
+ [player pause];
+}
+
+- (void)setLooping:(BOOL)loop {
+ player.numberOfLoops = loop ? -1 : 0;
+}
+
+- (void)setVolume:(float)volume {
+ player.volume = volume;
+}
+
+- (float)getCurrentTime {
+ return player.currentTime;
+}
+
+- (void)setCurrentTime:(float)time {
+ if( time == 0 ) {
+ [player stop];
+ }
+ else {
+ player.currentTime = time;
+ }
+}
+
+@end
17 Classes/Ejecta/EJAudio/EJAudioSourceOpenAL.h
@@ -0,0 +1,17 @@
+#import <Foundation/Foundation.h>
+
+#import <OpenAL/al.h>
+#import <OpenAL/alc.h>
+#import <AudioToolbox/AudioToolbox.h>
+#import <AudioToolbox/ExtendedAudioFile.h>
+
+#import "EJAudioSource.h"
+
+@interface EJAudioSourceOpenAL : NSObject <EJAudioSource> {
+ NSString * path;
+ NSUInteger bufferId, sourceId;
+}
+
+- (void*)getAudioDataWithURL:(NSURL *)inFileURL size:(ALsizei *)outDataSize format:(ALenum *)outDataFormat rate:(ALsizei *)outSampleRate;
+
+@end
177 Classes/Ejecta/EJAudio/EJAudioSourceOpenAL.m
@@ -0,0 +1,177 @@
+#import "EJAudioSourceOpenAL.h"
+
+@implementation EJAudioSourceOpenAL
+
+
+- (id)initWithPath:(NSString *)pathp {
+ if( self = [super init] ) {
+ path = [pathp retain];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ if( path ) {
+ [path release];
+ }
+
+ if( sourceId ) {
+ alDeleteSources(1, &sourceId);
+ }
+ if( bufferId ) {
+ alDeleteBuffers(1, &bufferId);
+ }
+
+ [super dealloc];
+}
+
+- (void)load {
+ if( sourceId || !path ) return; // already loaded or no path set?
+
+ ALenum format;
+ ALsizei size;
+ ALsizei freq;
+
+ NSURL * url = [NSURL fileURLWithPath:path];
+ void * data = [self getAudioDataWithURL:url size:&size format:&format rate:&freq];
+
+ if( !data ) {
+ return;
+ }
+
+ alGenBuffers( 1, &bufferId );
+ alBufferData( bufferId, format, data, size, freq );
+
+
+ alGenSources(1, &sourceId);
+ alSourcei(sourceId, AL_BUFFER, bufferId);
+ alSourcef(sourceId, AL_PITCH, 1.0f);
+ alSourcef(sourceId, AL_GAIN, 1.0f);
+
+ free(data);
+ return;
+}
+
+
+- (void*)getAudioDataWithURL:(NSURL *)inFileURL
+ size:(ALsizei *)outDataSize
+ format:(ALenum *)outDataFormat
+ rate:(ALsizei *)outSampleRate
+{
+
+ OSStatus err = noErr;
+ SInt64 theFileLengthInFrames = 0;
+ AudioStreamBasicDescription theFileFormat;
+ UInt32 thePropertySize = sizeof(theFileFormat);
+ ExtAudioFileRef extRef = NULL;
+ void* theData = NULL;
+ AudioStreamBasicDescription theOutputFormat;
+
+ // Open a file with ExtAudioFileOpen()
+ err = ExtAudioFileOpenURL((CFURLRef)inFileURL, &extRef);
+ if(err) {
+ NSLog(@"OpenALSource: ExtAudioFileOpenURL FAILED, Error = %ld", err);
+ goto Exit;
+ }
+
+ // Get the audio data format
+ err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
+ if(err) {
+ NSLog(@"OpenALSource: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld", err);
+ goto Exit;
+ }
+ if (theFileFormat.mChannelsPerFrame > 2) {
+ NSLog(@"OpenALSource: Unsupported Format, channel count is greater than stereo");
+ goto Exit;
+ }
+
+ // Set the client format to 16 bit signed integer (native-endian) data
+ // Maintain the channel count and sample rate of the original source format
+ theOutputFormat.mSampleRate = theFileFormat.mSampleRate;
+ theOutputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;
+
+ theOutputFormat.mFormatID = kAudioFormatLinearPCM;
+ theOutputFormat.mBytesPerPacket = 2 * theOutputFormat.mChannelsPerFrame;
+ theOutputFormat.mFramesPerPacket = 1;
+ theOutputFormat.mBytesPerFrame = 2 * theOutputFormat.mChannelsPerFrame;
+ theOutputFormat.mBitsPerChannel = 16;
+ theOutputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
+
+ // Set the desired client (output) data format
+ err = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(theOutputFormat), &theOutputFormat);
+ if( err ) {
+ NSLog(@"OpenALSource: ExtAudioFileSetProperty(kExtAudioFileProperty_ClientDataFormat) FAILED, Error = %ld", err);
+ goto Exit;
+ }
+
+ // Get the total frame count
+ thePropertySize = sizeof(theFileLengthInFrames);
+ err = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
+ if( err ) {
+ NSLog(@"OpenALSource: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld", err);
+ goto Exit;
+ }
+
+ // Read all the data into memory
+ UInt32 dataSize = theFileLengthInFrames * theOutputFormat.mBytesPerFrame;;
+ theData = malloc(dataSize);
+ if( theData ) {
+ AudioBufferList theDataBuffer;
+ theDataBuffer.mNumberBuffers = 1;
+ theDataBuffer.mBuffers[0].mDataByteSize = dataSize;
+ theDataBuffer.mBuffers[0].mNumberChannels = theOutputFormat.mChannelsPerFrame;
+ theDataBuffer.mBuffers[0].mData = theData;
+
+ // Read the data into an AudioBufferList
+ err = ExtAudioFileRead(extRef, (UInt32*)&theFileLengthInFrames, &theDataBuffer);
+ if( err == noErr ) {
+ // success
+ *outDataSize = (ALsizei)dataSize;
+ *outDataFormat = (theOutputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
+ *outSampleRate = (ALsizei)theOutputFormat.mSampleRate;
+ }
+ else {
+ // failure
+ free (theData);
+ theData = NULL; // make sure to return NULL
+ NSLog(@"OpenALSource: ExtAudioFileRead FAILED, Error = %ld", err);
+ goto Exit;
+ }
+ }
+
+Exit:
+ // Dispose the ExtAudioFileRef, it is no longer needed
+ if (extRef) {
+ ExtAudioFileDispose(extRef);
+ }
+ return theData;
+}
+
+- (void)play {
+ [self load];
+ alSourcePlay( sourceId );
+}
+
+- (void)pause {
+ alSourceStop( sourceId );
+}
+
+- (void)setLooping:(BOOL)loop {
+ alSourcei( sourceId, AL_LOOPING, loop ? AL_TRUE : AL_FALSE );
+}
+
+- (void)setVolume:(float)volume {
+ alSourcef( sourceId, AL_GAIN, volume );
+}
+
+- (float)getCurrentTime {
+ float time;
+ alGetSourcef( sourceId, AL_SEC_OFFSET, &time );
+ return time;
+}
+
+- (void)setCurrentTime:(float)time {
+ alSourcef( sourceId, AL_SEC_OFFSET, time );
+}
+
+@end
24 Classes/Ejecta/EJAudio/EJBindingAudio.h
@@ -0,0 +1,24 @@
+#import <Foundation/Foundation.h>
+#import "EJBindingEventedBase.h"
+
+#import "EJOpenALManager.h"
+#import "EJAudioSourceOpenAL.h"
+#import "EJAudioSourceAVAudio.h"
+
+// Max file size of audio effects using OpenAL; beyond that, the AVAudioPlayer is used
+#define EJ_AUDIO_OPENAL_MAX_SIZE 512 * 1024 // 512kb
+
+@interface EJBindingAudio : EJBindingEventedBase <AVAudioPlayerDelegate> {
+ NSString * path;
+ NSString * preload;
+ NSObject<EJAudioSource> * source;
+
+ BOOL loop, ended;
+ float volume;
+}
+
+- (void)load;
+- (void)releaseSource;
+- (void)setSourcePath:(NSString *)pathp;
+
+@end
166 Classes/Ejecta/EJAudio/EJBindingAudio.m
@@ -0,0 +1,166 @@
+#import "EJBindingAudio.h"
+
+
+@implementation EJBindingAudio
+
+- (id)initWithContext:(JSContextRef)ctx object:(JSObjectRef)obj argc:(size_t)argc argv:(const JSValueRef [])argv {
+ if( self = [super initWithContext:ctx object:obj argc:argc argv:argv] ) {
+ volume = 1;
+ preload = [@"none" retain];
+
+ if( argc > 0 ) {
+ [self setSourcePath:JSValueToNSString(ctx, argv[0])];
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self releaseSource];
+ [preload release];
+ [super dealloc];
+}
+
+- (void)releaseSource {
+ // If the retainCount is 2, only this instance and the .sources dictionary
+ // still retain the source - so remove it from the dict and delete it completely
+ if( source && [source retainCount] == 2 ) {
+ [[EJOpenALManager instance].sources removeObjectForKey:path];
+ }
+ [source release];
+ [path release];
+}
+
+- (void)setSourcePath:(NSString *)pathp {
+ // Is this source already loaded? Check in the manager's sources dictionary
+ NSObject<EJAudioSource> * loadedSource = [[EJOpenALManager instance].sources objectForKey:pathp];
+
+ if( loadedSource && loadedSource != source ) {
+ [self releaseSource];
+
+ path = [pathp retain];
+ source = [loadedSource retain];
+ }
+ else if( !loadedSource ) {
+ [self releaseSource];
+
+ path = [pathp retain];
+ }
+}
+
+- (void)load {
+ if( source || !path ) { return; }
+
+ // Decide whether to load the sound as OpenAL or AVAudioPlayer source
+ NSString * fullPath = [EJApp pathForResource:path];
+ unsigned long long size = [[[NSFileManager defaultManager]
+ attributesOfItemAtPath:fullPath error:nil] fileSize];
+
+ if( size <= EJ_AUDIO_OPENAL_MAX_SIZE ) {
+ NSLog(@"Loading Sound(OpenAL): %@", path);
+ source = [[EJAudioSourceOpenAL alloc] initWithPath:fullPath];
+ }
+ else {
+ NSLog(@"Loading Sound(AVAudio): %@", path);
+ source = [[EJAudioSourceAVAudio alloc] initWithPath:fullPath];
+ ((EJAudioSourceAVAudio *)source).delegate = self;
+ }
+ [source load];
+
+ [[EJOpenALManager instance].sources setObject:source forKey:path];
+ [self triggerEvent:@"canplaythrough" argc:0 argv:NULL];
+}
+
+- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
+ ended = true;
+ [self triggerEvent:@"ended" argc:0 argv:NULL];
+}
+
+
+EJ_BIND_FUNCTION(play, ctx, argc, argv) {
+ [self load];
+ [source play];
+ ended = false;
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(pause, ctx, argc, argv) {
+ [source pause];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(load, ctx, argc, argv) {
+ [self load];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(canPlayType, ctx, argc, argv) {
+ if( argc != 1 ) return NULL;
+
+ NSString * mime = JSValueToNSString(ctx, argv[0]);
+ if(
+ [mime hasPrefix:@"audio/x-caf"] ||
+ [mime hasPrefix:@"audio/mpeg"] ||
+ [mime hasPrefix:@"audio/mp4"]
+ ) {
+ return NSStringToJSValue(ctx, @"probably");
+ }
+ return NULL;
+}
+
+EJ_BIND_GET(loop, ctx) {
+ return JSValueMakeBoolean( ctx, loop );
+}
+
+EJ_BIND_SET(loop, ctx, value) {
+ loop = JSValueToBoolean(ctx, value);
+ [source setLooping:loop];
+}
+
+EJ_BIND_GET(volume, ctx) {
+ return JSValueMakeNumber( ctx, volume );
+}
+
+EJ_BIND_SET(volume, ctx, value) {
+ volume = JSValueToNumberFast(ctx, value);
+ [source setVolume:MIN(1,MAX(volume,0))];
+}
+
+EJ_BIND_GET(currentTime, ctx) {
+ return JSValueMakeNumber( ctx, [source getCurrentTime] );
+}
+
+EJ_BIND_SET(currentTime, ctx, value) {
+ float time = JSValueToNumberFast(ctx, value);
+ [source setCurrentTime:time];
+}
+
+EJ_BIND_GET(src, ctx) {
+ return path ? NSStringToJSValue(ctx, path) : NULL;
+}
+
+EJ_BIND_SET(src, ctx, value) {
+ [self setSourcePath:JSValueToNSString(ctx, value)];
+}
+
+EJ_BIND_GET(preload, ctx) {
+ return NSStringToJSValue(ctx, preload);
+}
+
+EJ_BIND_SET(preload, ctx, value) {
+ [preload release];
+
+ preload = [JSValueToNSString(ctx, value) retain];
+ if( [preload isEqualToString:@"auto"] ) {
+ [self load];
+ }
+}
+
+EJ_BIND_GET(ended, ctx) {
+ return JSValueMakeBoolean(ctx, ended);
+}
+
+EJ_BIND_EVENT(canplaythrough);
+EJ_BIND_EVENT(ended);
+
+@end
16 Classes/Ejecta/EJAudio/EJOpenALManager.h
@@ -0,0 +1,16 @@
+#import <Foundation/Foundation.h>
+
+#import <OpenAL/al.h>
+#import <OpenAL/alc.h>
+
+@interface EJOpenALManager : NSObject {
+ ALCcontext * context;
+ ALCdevice * device;
+ NSMutableDictionary *sources;
+}
+
++ (EJOpenALManager *)instance;
+
+@property (readonly, nonatomic) NSMutableDictionary * sources;
+
+@end
36 Classes/Ejecta/EJAudio/EJOpenALManager.m
@@ -0,0 +1,36 @@
+#import "EJOpenALManager.h"
+
+
+@implementation EJOpenALManager
+
+@synthesize sources;
+
+
+static EJOpenALManager * openALManagerInstance = NULL;
+
++ (EJOpenALManager *)instance {
+ if( openALManagerInstance == NULL ) {
+ openALManagerInstance = [[self alloc] init];
+ }
+ return openALManagerInstance;
+}
+
+-(id)init {
+ if( self = [super init] ) {
+ sources = [[NSMutableDictionary alloc] init];
+ device = alcOpenDevice(NULL);
+ if( device ) {
+ context = alcCreateContext( device, NULL );
+ alcMakeContextCurrent( context );
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ alcDestroyContext( context );
+ alcCloseDevice( device );
+ [super dealloc];
+}
+
+@end
119 Classes/Ejecta/EJBindingBase.h
@@ -0,0 +1,119 @@
+#import <Foundation/Foundation.h>
+#import "EJApp.h"
+
+id objc_msgSend(id theReceiver, SEL theSelector, ...);
+
+extern JSValueRef ej_global_undefined;
+
+// (Not sure if clever hack or really stupid...)
+
+// All classes derived from this JS_BaseClass will return a JSClassRef through the
+// 'getJSClass' class method. The properties and functions that are exposed to
+// JavaScript are defined through the 'staticFunctions' and 'staticValues' in this
+// JSClassRef.
+
+// Since these functions don't have extra data (e.g. a void*), we have to define a
+// C callback function for each js function, for each js getter and for each js setter.
+
+// Furthermore, a class method is added to the objc class to return the function pointer
+// to the particular C callback function - this way we can later inflect the objc class
+// and gather all function pointers.
+
+
+// The class method that returns a pointer to the static C callback function
+#define __EJ_GET_CALLBACK_CLASS_METHOD(NAME) \
+ + (JSObjectCallAsFunctionCallback)_callback_for##NAME {\
+ return (JSObjectCallAsFunctionCallback)&NAME;\
+ }
+
+
+// ------------------------------------------------------------------------------------
+// Function - use with EJ_BIND_FUNCTION( functionName, ctx, argc, argv ) { ... }
+
+#define EJ_BIND_FUNCTION(NAME, CTX_NAME, ARGC_NAME, ARGV_NAME) \
+ \
+ /* The C callback function for the exposed method and class method that returns it */ \
+ static JSValueRef _func_##NAME( \
+ JSContextRef ctx, \
+ JSObjectRef function, \
+ JSObjectRef object, \
+ size_t argc, \
+ const JSValueRef argv[], \
+ JSValueRef* exception \
+ ) { \
+ id instance = (id)JSObjectGetPrivate(object); \
+ JSValueRef ret = (JSValueRef)objc_msgSend(instance, @selector(_func_##NAME:argc:argv:), ctx, argc, argv); \
+ return ret ? ret : ej_global_undefined; \
+ } \
+ __EJ_GET_CALLBACK_CLASS_METHOD(_func_##NAME)\
+ \
+ /* The actual implementation for this method */ \
+ - (JSValueRef)_func_##NAME:(JSContextRef)CTX_NAME argc:(size_t)ARGC_NAME argv:(const JSValueRef [])ARGV_NAME
+
+
+// ------------------------------------------------------------------------------------
+// Getter - use with EJ_BIND_GET( propertyName, ctx ) { ... }
+
+#define EJ_BIND_GET(NAME, CTX_NAME) \
+ \
+ /* The C callback function for the exposed getter and class method that returns it */ \
+ static JSValueRef _get_##NAME( \
+ JSContextRef ctx, \
+ JSObjectRef object, \
+ JSStringRef propertyName, \
+ JSValueRef* exception \
+ ) { \
+ id instance = (id)JSObjectGetPrivate(object); \
+ return (JSValueRef)objc_msgSend(instance, @selector(_get_##NAME:), ctx); \
+ } \
+ __EJ_GET_CALLBACK_CLASS_METHOD(_get_##NAME)\
+ \
+ /* The actual implementation for this getter */ \
+ - (JSValueRef)_get_##NAME:(JSContextRef)CTX_NAME
+
+
+// ------------------------------------------------------------------------------------
+// Setter - use with EJ_BIND_SET( propertyName, ctx, value ) { ... }
+
+#define EJ_BIND_SET(NAME, CTX_NAME, VALUE_NAME) \
+ \
+ /* The C callback function for the exposed setter and class method that returns it */ \
+ static bool _set_##NAME( \
+ JSContextRef ctx, \
+ JSObjectRef object, \
+ JSStringRef propertyName, \
+ JSValueRef value, \
+ JSValueRef* exception \
+ ) { \
+ id instance = (id)JSObjectGetPrivate(object); \
+ objc_msgSend(instance, @selector(_set_##NAME:value:), ctx, value); \
+ return true; \
+ } \
+ __EJ_GET_CALLBACK_CLASS_METHOD(_set_##NAME) \
+ \
+ /* The actual implementation for this setter */ \
+ - (void)_set_##NAME:(JSContextRef)CTX_NAME value:(JSValueRef)VALUE_NAME
+
+
+
+// ------------------------------------------------------------------------------------
+// Shorthand to define a function that logs a "not implemented" warning
+
+#define EJ_BIND_FUNCTION_NOT_IMPLEMENTED( NAME ) \
+ EJ_BIND_FUNCTION( NAME, ctx, argc, argv ) { \
+ static bool didShowWarning; \
+ if( !didShowWarning ) { \
+ NSLog(@"Warning: method %s() is not yet implemented!", #NAME); \
+ didShowWarning = true; \
+ } \
+ return NULL; \
+ }
+
+@interface EJBindingBase : NSObject {
+ JSObjectRef jsObject;
+}
+
+- (id)initWithContext:(JSContextRef)ctxp object:(JSObjectRef)obj argc:(size_t)argc argv:(const JSValueRef [])argv;
++ (JSClassRef)getJSClass;
+
+@end
113 Classes/Ejecta/EJBindingBase.m
@@ -0,0 +1,113 @@
+#import "EJBindingBase.h"
+#import <objc/runtime.h>
+
+
+void _ej_class_finalize(JSObjectRef object) {
+ id instance = (id)JSObjectGetPrivate(object);
+ [instance release];
+}
+
+NSData * NSDataFromString( NSString *str ) {
+ int len = [str length] + 1;
+ NSMutableData * d = [NSMutableData dataWithLength:len];
+ strlcpy([d mutableBytes], [str UTF8String], len);
+ return d;
+}
+
+
+@implementation EJBindingBase
+
+- (id)initWithContext:(JSContextRef)ctxp object:(JSObjectRef)obj argc:(size_t)argc argv:(const JSValueRef [])argv {
+ if( self = [super init] ) {
+ jsObject = obj;
+ }
+ return self;
+}
+
++ (JSClassRef)getJSClass {
+ // Gather all class methods that return C callbacks for this class or it's parents
+ NSMutableArray * methods = [[NSMutableArray alloc] init];
+ NSMutableArray * properties = [[NSMutableArray alloc] init];
+
+ // Traverse this class and all its super classes
+ id base = [EJBindingBase class];
+ for( id sc = [self class]; sc != base && [sc isSubclassOfClass:base]; sc = [sc superclass] ) {
+
+ // Traverse all class methods for this class; i.e. all classes that are defined with the
+ // EJ_BIND_FUNCTION, EJ_BIND_GET or EJ_BIND_SET macros
+ u_int count;
+ Method * methodList = class_copyMethodList(sc, &count);
+ for (int i = 0; i < count ; i++) {
+ SEL selector = method_getName(methodList[i]);
+ NSString * name = NSStringFromSelector(selector);
+
+ if( [name hasPrefix:@"_func_"] ) {
+ NSString * shortName = [[[name componentsSeparatedByString:@":"] objectAtIndex:0]
+ substringFromIndex:sizeof("_func_")-1];
+ [methods addObject:shortName];
+ }
+ else if( [name hasPrefix:@"_get_"] ) {
+ // We only look for getters - a property that has a setter, but no getter will be ignored
+ NSString * shortName = [[[name componentsSeparatedByString:@":"] objectAtIndex:0]
+ substringFromIndex:sizeof("_get_")-1];
+ [properties addObject:shortName];
+ }
+ }
+ free(methodList);
+ }
+
+
+ // Set up the JSStaticValue struct array
+ JSStaticValue * values = malloc( sizeof(JSStaticValue) * (properties.count+1) );
+ memset( values, 0, sizeof(JSStaticValue) * (properties.count+1) );
+ for( int i = 0; i < properties.count; i++ ) {
+ NSString * name = [properties objectAtIndex:i];
+ NSData * nameData = NSDataFromString( name );
+
+ values[i].name = [nameData bytes];
+ values[i].attributes = kJSPropertyAttributeDontDelete;
+
+ SEL get = NSSelectorFromString([NSString stringWithFormat:@"_callback_for_get_%@", name]);
+ values[i].getProperty = (JSObjectGetPropertyCallback)[self performSelector:get];
+
+ SEL set = NSSelectorFromString([NSString stringWithFormat:@"_callback_for_set_%@", name]);
+
+ // Property has a setter? Otherwise mark as read only
+ if( [self respondsToSelector:set] ) {
+ values[i].setProperty = (JSObjectSetPropertyCallback)[self performSelector:set];
+ }
+ else {
+ values[i].attributes |= kJSPropertyAttributeReadOnly;
+ }
+ }
+
+ // Set up the JSStaticFunction struct array
+ JSStaticFunction * functions = malloc( sizeof(JSStaticFunction) * (methods.count+1) );
+ memset( functions, 0, sizeof(JSStaticFunction) * (methods.count+1) );
+ for( int i = 0; i < methods.count; i++ ) {
+ NSString * name = [methods objectAtIndex:i];
+ NSData * nameData = NSDataFromString( name );
+
+ functions[i].name = [nameData bytes];
+ functions[i].attributes = kJSPropertyAttributeDontDelete;
+
+ SEL call = NSSelectorFromString([NSString stringWithFormat:@"_callback_for_func_%@", name]);
+ functions[i].callAsFunction = (JSObjectCallAsFunctionCallback)[self performSelector:call];
+ }
+
+ JSClassDefinition classDef = kJSClassDefinitionEmpty;
+ classDef.finalize = _ej_class_finalize;
+ classDef.staticValues = values;
+ classDef.staticFunctions = functions;
+ JSClassRef class = JSClassCreate(&classDef);
+
+ free( values );
+ free( functions );
+
+ [properties release];
+ [methods release];
+
+ return class;
+}
+
+@end
8 Classes/Ejecta/EJBindingEjectaCore.h
@@ -0,0 +1,8 @@
+#import <Foundation/Foundation.h>
+#import "EJBindingBase.h"
+
+@interface EJBindingEjectaCore : EJBindingBase {
+ NSString * urlToOpen;
+}
+
+@end
96 Classes/Ejecta/EJBindingEjectaCore.m
@@ -0,0 +1,96 @@
+#import "EJBindingEjectaCore.h"
+
+@implementation EJBindingEjectaCore
+
+EJ_BIND_FUNCTION(log, ctx, argc, argv ) {
+ if( argc < 1 ) return NULL;
+
+ NSLog( @"JS: %@", JSValueToNSString(ctx, argv[0]) );
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(require, ctx, argc, argv ) {
+ if( argc < 1 ) { return NULL; }
+
+ [[EJApp instance] loadScriptAtPath:JSValueToNSString(ctx, argv[0])];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(openURL, ctx, argc, argv ) {
+ if( argc < 1 ) { return NULL; }
+
+ NSString * url = JSValueToNSString( ctx, argv[0] );
+ if( argc == 2 ) {
+ [urlToOpen release];
+ urlToOpen = [url retain];
+
+ NSString * confirm = JSValueToNSString( ctx, argv[1] );
+ UIAlertView * alert = [[UIAlertView alloc] initWithTitle:@"Open Browser?" message:confirm delegate:self cancelButtonTitle:@"OK" otherButtonTitles:@"Cancel", nil];
+ [alert show];
+ [alert release];
+ }
+ else {
+ [[UIApplication sharedApplication] openURL:[NSURL URLWithString: url]];
+ }
+ return NULL;
+}
+
+- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)index {
+ if( index == 0 ) {
+ [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlToOpen]];
+ }
+ [urlToOpen release];
+ urlToOpen = nil;
+}
+
+
+EJ_BIND_FUNCTION(setTimeout, ctx, argc, argv ) {
+ return [[EJApp instance] createTimer:ctx argc:argc argv:argv repeat:NO];
+}
+
+EJ_BIND_FUNCTION(setInterval, ctx, argc, argv ) {
+ return [[EJApp instance] createTimer:ctx argc:argc argv:argv repeat:YES];
+}
+
+EJ_BIND_FUNCTION(clearTimeout, ctx, argc, argv ) {
+ return [[EJApp instance] deleteTimer:ctx argc:argc argv:argv];
+}
+
+EJ_BIND_FUNCTION(clearInterval, ctx, argc, argv ) {
+ return [[EJApp instance] deleteTimer:ctx argc:argc argv:argv];
+}
+
+
+
+EJ_BIND_GET(devicePixelRatio, ctx ) {
+ return JSValueMakeNumber( ctx, [UIScreen mainScreen].scale );
+}
+
+EJ_BIND_GET(screenWidth, ctx ) {
+ return JSValueMakeNumber( ctx, [EJApp instance].view.bounds.size.width );
+}
+
+EJ_BIND_GET(screenHeight, ctx ) {
+ return JSValueMakeNumber( ctx, [EJApp instance].view.bounds.size.height );
+}
+
+EJ_BIND_GET(landscapeMode, ctx ) {
+ return JSValueMakeBoolean( ctx, [EJApp landscapeMode] );
+}
+
+EJ_BIND_GET(userAgent, ctx ) {
+ // FIXME?! iPhone3/4/5 and iPod all have the same user agent string ('iPhone')
+ // Only iPad is different
+
+ return NSStringToJSValue(ctx,
+ [[[UIDevice currentDevice] model] hasPrefix:@"iPad"]
+ ? @"iPad"
+ : @"iPhone"
+ );
+}
+
+EJ_BIND_GET(appVersion, ctx ) {
+ return NSStringToJSValue( ctx, EJECTA_VERSION );
+}
+
+@end
24 Classes/Ejecta/EJBindingEventedBase.h
@@ -0,0 +1,24 @@
+#import "EJBindingBase.h"
+
+
+// ------------------------------------------------------------------------------------
+// Events; shorthand for EJ_BIND_GET/SET - use with EJ_BIND_EVENT( eventname );
+
+#define EJ_BIND_EVENT(NAME) \
+ EJ_BIND_GET(on##NAME, ctx ) { \
+ return [self getCallbackWith:( @ #NAME) ctx:ctx]; \
+ } \
+ EJ_BIND_SET(on##NAME, ctx, callback) {\
+ [self setCallbackWith:( @ #NAME) ctx:ctx callback:callback]; \
+ }
+
+@interface EJBindingEventedBase : EJBindingBase {
+ NSMutableDictionary * eventListeners; // for addEventListener
+ NSMutableDictionary * onCallbacks; // for on* setters
+}
+
+- (JSObjectRef)getCallbackWith:(NSString *)name ctx:(JSContextRef)ctx;
+- (void)setCallbackWith:(NSString *)name ctx:(JSContextRef)ctx callback:(JSValueRef)callback;
+- (void)triggerEvent:(NSString *)name argc:(int)argc argv:(JSValueRef[])argv;
+
+@end
109 Classes/Ejecta/EJBindingEventedBase.m
@@ -0,0 +1,109 @@
+#import "EJBindingEventedBase.h"
+
+@implementation EJBindingEventedBase
+
+- (id)initWithContext:(JSContextRef)ctxp object:(JSObjectRef)obj argc:(size_t)argc argv:(const JSValueRef [])argv {
+ if( self = [super initWithContext:ctxp object:obj argc:argc argv:argv] ) {
+ eventListeners = [[NSMutableDictionary alloc] init];
+ onCallbacks = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ JSContextRef ctx = [EJApp instance].jsGlobalContext;
+
+ // Unprotect all event callbacks
+ for( NSString * name in eventListeners ) {
+ NSArray * listeners = [eventListeners objectForKey:name];
+ for( NSValue * callbackValue in listeners ) {
+ JSValueUnprotect(ctx, [callbackValue pointerValue]);
+ }
+ }
+ [eventListeners release];
+
+ // Unprotect all event callbacks
+ for( NSString * name in onCallbacks ) {
+ NSValue * listener = [onCallbacks objectForKey:name];
+ JSValueUnprotect(ctx, [(NSValue *)listener pointerValue]);
+ }
+ [onCallbacks release];
+
+ [super dealloc];
+}
+
+- (JSObjectRef)getCallbackWith:(NSString *)name ctx:(JSContextRef)ctx {
+ NSValue * listener = [onCallbacks objectForKey:name];
+ return listener ? [listener pointerValue] : NULL;
+}
+
+- (void)setCallbackWith:(NSString *)name ctx:(JSContextRef)ctx callback:(JSValueRef)callbackValue {
+ // remove old event listener?
+ JSObjectRef oldCallback = [self getCallbackWith:name ctx:ctx];
+ if( oldCallback ) {
+ JSValueUnprotect(ctx, oldCallback);
+ [onCallbacks removeObjectForKey:name];
+ }
+
+ JSObjectRef callback = JSValueToObject(ctx, callbackValue, NULL);
+ if( JSObjectIsFunction(ctx, callback) ) {
+ JSValueProtect(ctx, callback);
+ [onCallbacks setObject:[NSValue valueWithPointer:callback] forKey:name];
+ return;
+ }
+}
+
+EJ_BIND_FUNCTION(addEventListener, ctx, argc, argv) {
+ if( argc < 2 ) { return NULL; }
+
+ NSString * name = JSValueToNSString( ctx, argv[0] );
+ JSObjectRef callback = JSValueToObject(ctx, argv[1], NULL);
+ JSValueProtect(ctx, callback);
+ NSValue * callbackValue = [NSValue valueWithPointer:callback];
+
+ NSMutableArray * listeners = NULL;
+ if( (listeners = [eventListeners objectForKey:name]) ) {
+ [listeners addObject:callbackValue];
+ }
+ else {
+ [eventListeners setObject:[NSMutableArray arrayWithObject:callbackValue] forKey:name];
+ }
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(removeEventListener, ctx, argc, argv) {
+ if( argc < 2 ) { return NULL; }
+
+ NSString * name = JSValueToNSString( ctx, argv[0] );
+
+ NSMutableArray * listeners = NULL;
+ if( (listeners = [eventListeners objectForKey:name]) ) {
+ JSObjectRef callback = JSValueToObject(ctx, argv[1], NULL);
+ for( int i = 0; i < listeners.count; i++ ) {
+ if( JSValueIsStrictEqual(ctx, callback, [[listeners objectAtIndex:i] pointerValue]) ) {
+ [listeners removeObjectAtIndex:i];
+ return NULL;
+ }
+ }
+ }
+ return NULL;
+}
+
+- (void)triggerEvent:(NSString *)name argc:(int)argc argv:(JSValueRef[])argv {
+ EJApp * ejecta = [EJApp instance];
+
+ NSArray * listeners = [eventListeners objectForKey:name];
+ if( listeners ) {
+ for( NSValue * callbackValue in listeners ) {
+ [ejecta invokeCallback:[callbackValue pointerValue] thisObject:jsObject argc:argc argv:argv];
+ }
+ }
+
+ NSValue * callbackValue = [onCallbacks objectForKey:name];
+ if( callbackValue ) {
+ [ejecta invokeCallback:[callbackValue pointerValue] thisObject:jsObject argc:argc argv:argv];
+ }
+}
+
+
+@end
17 Classes/Ejecta/EJCanvas/EAGLView.h
@@ -0,0 +1,17 @@
+#import <UIKit/UIKit.h>
+
+#import <OpenGLES/ES1/gl.h>
+#import <OpenGLES/ES1/glext.h>
+#import <OpenGLES/ES2/gl.h>
+#import <OpenGLES/ES2/glext.h>
+
+@interface EAGLView : UIView {
+ EAGLContext * context;
+}
+
+- (id)initWithFrame:(CGRect)frame contentScale:(float)contentScale;
+- (void)resetContext;
+
+@property (nonatomic, retain) EAGLContext *context;
+
+@end
41 Classes/Ejecta/EJCanvas/EAGLView.m
@@ -0,0 +1,41 @@
+#import <QuartzCore/QuartzCore.h>
+#import "EAGLView.h"
+
+@implementation EAGLView
+
+@synthesize context;
+
++ (Class)layerClass {
+ return [CAEAGLLayer class];
+}
+
+- (id)initWithFrame:(CGRect)frame contentScale:(float)contentScale {
+ if( self = [super initWithFrame:frame] ) {
+ [self setMultipleTouchEnabled:YES];
+ CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;
+
+ self.contentScaleFactor = contentScale;
+ eaglLayer.contentsScale = contentScale;
+
+ eaglLayer.opaque = TRUE;
+ eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:TRUE], kEAGLDrawablePropertyRetainedBacking,
+ kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat,
+ nil];
+
+ context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];
+ [EAGLContext setCurrentContext:context];
+ }
+ return self;
+}
+
+- (void)resetContext {
+ [EAGLContext setCurrentContext:context];
+}
+
+- (void)dealloc {
+ [context release];
+ [super dealloc];
+}
+
+@end
24 Classes/Ejecta/EJCanvas/EJBindingCanvas.h
@@ -0,0 +1,24 @@
+#import <Foundation/Foundation.h>
+#import "EJBindingBase.h"
+#import "EJCanvasContextTexture.h"
+#import "EJCanvasContextScreen.h"
+#import "EJTexture.h"
+#import "EJDrawable.h"
+
+@interface EJBindingCanvas : EJBindingBase <EJDrawable> {
+ EJCanvasContext * renderingContext;
+ EJApp * ejectaInstance;
+ short width, height;
+
+ BOOL isScreenCanvas;
+ BOOL useRetinaResolution;
+ EJScalingMode scalingMode;
+
+ JSValueRef jsValueSourceOver, jsValueDarker, jsValueLighter;
+ JSValueRef jsValueLineCapButt, jsValueLineCapSquare, jsValueLineCapRound;
+ JSValueRef jsValueLineJoinMiter, jsValueLineJoinBevel, jsValueLineJoinRound;
+}
+
+@property (readonly, nonatomic) EJTexture * texture;
+
+@end
598 Classes/Ejecta/EJCanvas/EJBindingCanvas.m
@@ -0,0 +1,598 @@
+#import "EJBindingCanvas.h"
+#import "EJBindingImageData.h"
+
+
+@implementation EJBindingCanvas
+
+static int firstCanvasInstance = YES;
+
+- (id)initWithContext:(JSContextRef)ctx object:(JSObjectRef)obj argc:(size_t)argc argv:(const JSValueRef [])argv {
+ if( self = [super initWithContext:ctx object:obj argc:argc argv:argv] ) {
+
+ ejectaInstance = [EJApp instance]; // Keep a local copy - may be faster?
+ scalingMode = kEJScalingModeFitWidth;
+ useRetinaResolution = true;
+
+ jsValueSourceOver = NSStringToJSValueProtect( ctx, @"source-over" );
+ jsValueDarker = NSStringToJSValueProtect( ctx, @"darker" );
+ jsValueLighter = NSStringToJSValueProtect( ctx, @"lighter" );
+
+ jsValueLineCapButt = NSStringToJSValue( ctx, @"butt" );
+ jsValueLineCapRound = NSStringToJSValue( ctx, @"round" );
+ jsValueLineCapSquare = NSStringToJSValue( ctx, @"square" );
+
+ jsValueLineJoinMiter = NSStringToJSValue( ctx, @"miter" );
+ jsValueLineJoinRound = NSStringToJSValue( ctx, @"round" );
+ jsValueLineJoinBevel = NSStringToJSValue( ctx, @"bevel" );
+
+ // If this is the first canvas instance we created, make it the screen canvas
+ if( firstCanvasInstance ) {
+ isScreenCanvas = YES;
+ firstCanvasInstance = NO;
+ }
+
+ if( argc == 2 ) {
+ width = JSValueToNumberFast(ctx, argv[0]);
+ height = JSValueToNumberFast(ctx, argv[1]);
+ }
+ else {
+ CGSize screen = [EJApp instance].view.bounds.size;
+ width = screen.width;
+ height = screen.height;
+ }
+ }
+ return self;
+}
+
+- (void)dealloc {
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueSourceOver );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueDarker );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLighter );
+
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineCapButt );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineCapRound );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineCapSquare );
+
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineJoinMiter );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineJoinRound );
+ JSValueUnprotect( ejectaInstance.jsGlobalContext, jsValueLineJoinBevel );
+
+ [renderingContext release];
+ [super dealloc];
+}
+
+- (EJTexture *)texture {
+ if( [renderingContext isKindOfClass:[EJCanvasContextTexture class]] ) {
+ return ((EJCanvasContextTexture *)renderingContext).texture;
+ }
+ else {
+ return nil;
+ }
+}
+
+
+EJ_BIND_GET(fillStyle, ctx ) {
+ return ColorRGBAToJSValue(ctx, renderingContext.state->fillColor);
+}
+
+EJ_BIND_SET(fillStyle, ctx, value) {
+ renderingContext.state->fillColor = JSValueToColorRGBA(ctx, value);
+}
+
+EJ_BIND_GET(strokeStyle, ctx ) {
+ return ColorRGBAToJSValue(ctx, renderingContext.state->strokeColor);
+}
+
+EJ_BIND_SET(strokeStyle, ctx, value) {
+ renderingContext.state->strokeColor = JSValueToColorRGBA(ctx, value);
+}
+
+EJ_BIND_GET(globalAlpha, ctx ) {
+ return JSValueMakeNumber(ctx, renderingContext.state->globalAlpha );
+}
+
+EJ_BIND_SET(globalAlpha, ctx, value) {
+ renderingContext.state->globalAlpha = MIN(1,MAX(JSValueToNumberFast(ctx, value),0));
+}
+
+EJ_BIND_GET(globalCompositeOperation, ctx) {
+ JSStringRef src = JSStringCreateWithUTF8CString( renderingContext.state->globalCompositeOperation->name );
+ JSValueRef ret = JSValueMakeString(ctx, src);
+ JSStringRelease(src);
+ return ret;
+}
+
+EJ_BIND_SET(globalCompositeOperation, ctx, value) {
+ if( JSValueIsStrictEqual(ctx, value, jsValueSourceOver) ) {
+ renderingContext.globalCompositeOperation = &kEJCompositeOperationSourceOver;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueLighter) ) {
+ renderingContext.globalCompositeOperation = &kEJCompositeOperationLighter;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueDarker) ) {
+ renderingContext.globalCompositeOperation = &kEJCompositeOperationDarker;
+ }
+}
+
+EJ_BIND_GET(lineWidth, ctx) {
+ return JSValueMakeNumber(ctx, renderingContext.state->lineWidth);
+}
+
+EJ_BIND_SET(lineWidth, ctx, value) {
+ renderingContext.state->lineWidth = JSValueToNumberFast(ctx, value);
+}
+
+EJ_BIND_GET(miterLimit, ctx) {
+ return JSValueMakeNumber(ctx, renderingContext.state->miterLimit);
+}
+
+EJ_BIND_SET(miterLimit, ctx, value) {
+ renderingContext.state->miterLimit = JSValueToNumberFast(ctx, value);
+}
+
+EJ_BIND_GET(lineCap, ctx) {
+ switch( renderingContext.state->lineCap ) {
+ case kEJLineCapButt: return jsValueLineCapButt;
+ case kEJLineCapSquare: return jsValueLineCapSquare;
+ case kEJLineCapRound: return jsValueLineCapRound;
+ }
+ return NULL;
+}
+
+EJ_BIND_SET(lineCap, ctx, value) {
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineCapButt) ) {
+ renderingContext.state->lineCap = kEJLineCapButt;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineCapRound) ) {
+ renderingContext.state->lineCap = kEJLineCapRound;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineCapSquare) ) {
+ renderingContext.state->lineCap = kEJLineCapSquare;
+ }
+}
+
+EJ_BIND_GET(lineJoin, ctx) {
+ switch( renderingContext.state->lineJoin ) {
+ case kEJLineJoinMiter: return jsValueLineJoinMiter;
+ case kEJLineJoinBevel: return jsValueLineJoinBevel;
+ case kEJLineJoinRound: return jsValueLineJoinRound;
+ }
+ return NULL;
+}
+
+EJ_BIND_SET(lineJoin, ctx, value) {
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineJoinMiter) ) {
+ renderingContext.state->lineJoin = kEJLineJoinMiter;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineJoinBevel) ) {
+ renderingContext.state->lineJoin = kEJLineJoinBevel;
+ }
+ if( JSValueIsStrictEqual(ctx, value, jsValueLineJoinRound) ) {
+ renderingContext.state->lineJoin = kEJLineJoinRound;
+ }
+}
+
+EJ_BIND_GET(width, ctx) {
+ return JSValueMakeNumber(ctx, width);
+}
+
+EJ_BIND_SET(width, ctx, value) {
+ if( renderingContext ) {
+ NSLog(@"Warning: rendering context already created; can't change width");
+ return;
+ }
+ width = JSValueToNumberFast(ctx, value);
+}
+
+EJ_BIND_GET(height, ctx) {
+ return JSValueMakeNumber(ctx, height);
+}
+
+EJ_BIND_SET(height, ctx, value) {
+ if( renderingContext ) {
+ NSLog(@"Warning: rendering context already created; can't change height");
+ return;
+ }
+ height = JSValueToNumberFast(ctx, value);
+}
+
+EJ_BIND_GET(offsetLeft, ctx) {
+ return JSValueMakeNumber(ctx, 0);
+}
+
+EJ_BIND_GET(offsetTop, ctx) {
+ return JSValueMakeNumber(ctx, 0);
+}
+
+EJ_BIND_SET(retinaResolutionEnabled, ctx, value) {
+ useRetinaResolution = JSValueToBoolean(ctx, value);
+}
+
+EJ_BIND_GET(retinaResolutionEnabled, ctx) {
+ return JSValueMakeBoolean(ctx, useRetinaResolution);
+}
+
+EJ_BIND_SET(imageSmoothingEnabled, ctx, value) {
+ [EJTexture setSmoothScaling:JSValueToBoolean(ctx, value)];
+}
+
+EJ_BIND_GET(imageSmoothingEnabled, ctx) {
+ return JSValueMakeBoolean(ctx, [EJTexture smoothScaling]);
+}
+
+EJ_BIND_SET(scalingMode, ctx, value) {
+ NSString * mode = JSValueToNSString(ctx, value);
+ if( [mode isEqualToString:@"fit-width"] ) {
+ scalingMode = kEJScalingModeFitWidth;
+ }
+ else if( [mode isEqualToString:@"fit-height"] ) {
+ scalingMode = kEJScalingModeFitHeight;
+ }
+ else if( [mode isEqualToString:@"none"] ) {
+ scalingMode = kEJScalingModeNone;
+ }
+}
+
+EJ_BIND_GET(scalingMode, ctx) {
+ JSStringRef scalingModeName;
+ if( scalingMode == kEJScalingModeFitWidth ) {
+ scalingModeName = JSStringCreateWithUTF8CString( "fit-width" );
+ }
+ else if( scalingMode == kEJScalingModeFitHeight ) {
+ scalingModeName = JSStringCreateWithUTF8CString( "fit-height" );
+ }
+ else {
+ scalingModeName = JSStringCreateWithUTF8CString( "none" );
+ }
+ JSValueRef ret = JSValueMakeString(ctx, scalingModeName);
+ JSStringRelease(scalingModeName);
+ return ret;
+}
+
+EJ_BIND_FUNCTION(getContext, ctx, argc, argv) {
+ if( argc < 1 || ![JSValueToNSString(ctx, argv[0]) isEqualToString:@"2d"] ) {
+ return NULL;
+ };
+
+ if( renderingContext ) { return jsObject; }
+ ejectaInstance.currentRenderingContext = nil;
+
+ if( isScreenCanvas ) {
+ EJCanvasContextScreen * sc = [[EJCanvasContextScreen alloc] initWithWidth:width height:height];
+ sc.useRetinaResolution = useRetinaResolution;
+ sc.scalingMode = scalingMode;
+
+ ejectaInstance.screenRenderingContext = sc;
+ renderingContext = sc;
+ }
+ else {
+ renderingContext = [[EJCanvasContextTexture alloc] initWithWidth:width height:height];
+ }
+
+ [renderingContext create];
+ ejectaInstance.currentRenderingContext = renderingContext;
+
+ // Context and canvas are one and the same object, so getContext just
+ // returns itself
+ return jsObject;
+}
+
+EJ_BIND_FUNCTION(save, ctx, argc, argv) {
+ [renderingContext save];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(restore, ctx, argc, argv) {
+ [renderingContext restore];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(rotate, ctx, argc, argv) {
+ if( argc < 1 ) { return NULL; }
+ [renderingContext rotate:JSValueToNumberFast(ctx, argv[0])];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(translate, ctx, argc, argv) {
+ if( argc < 2 ) { return NULL; }
+ [renderingContext translateX:JSValueToNumberFast(ctx, argv[0]) y:JSValueToNumberFast(ctx, argv[1])];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(scale, ctx, argc, argv) {
+ if( argc < 2 ) { return NULL; }
+ [renderingContext scaleX:JSValueToNumberFast(ctx, argv[0]) y:JSValueToNumberFast(ctx, argv[1])];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(transform, ctx, argc, argv) {
+ if( argc < 6 ) { return NULL; }
+
+ float
+ m11 = JSValueToNumberFast(ctx, argv[0]),
+ m12 = JSValueToNumberFast(ctx, argv[1]),
+ m21 = JSValueToNumberFast(ctx, argv[2]),
+ m22 = JSValueToNumberFast(ctx, argv[3]),
+ dx = JSValueToNumberFast(ctx, argv[4]),
+ dy = JSValueToNumberFast(ctx, argv[5]);
+ [renderingContext transformM11:m11 m12:m12 m21:m21 m22:m22 dx:dx dy:dy];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(setTransform, ctx, argc, argv) {
+ if( argc < 6 ) { return NULL; }
+
+ float
+ m11 = JSValueToNumberFast(ctx, argv[0]),
+ m12 = JSValueToNumberFast(ctx, argv[1]),
+ m21 = JSValueToNumberFast(ctx, argv[2]),
+ m22 = JSValueToNumberFast(ctx, argv[3]),
+ dx = JSValueToNumberFast(ctx, argv[4]),
+ dy = JSValueToNumberFast(ctx, argv[5]);
+ [renderingContext setTransformM11:m11 m12:m12 m21:m21 m22:m22 dx:dx dy:dy];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(drawImage, ctx, argc, argv) {
+ if( argc < 3 || !JSValueIsObject(ctx, argv[0]) ) return NULL;
+
+ NSObject<EJDrawable> * drawable = (NSObject<EJDrawable> *)JSObjectGetPrivate((JSObjectRef)argv[0]);
+ EJTexture * image = drawable.texture;
+
+ short sx = 0, sy = 0, sw = 0, sh = 0;
+ float dx = 0, dy = 0, dw = sw, dh = sh;
+
+ if( argc == 3 ) {
+ // drawImage(image, dx, dy)
+ dx = JSValueToNumberFast(ctx, argv[1]);
+ dy = JSValueToNumberFast(ctx, argv[2]);
+ dw = sw = image.width;
+ dh = sh = image.height;
+ }
+ else if( argc == 5 ) {
+ // drawImage(image, dx, dy, dw, dh)
+ dx = JSValueToNumberFast(ctx, argv[1]);
+ dy = JSValueToNumberFast(ctx, argv[2]);
+ dw = JSValueToNumberFast(ctx, argv[3]);
+ dh = JSValueToNumberFast(ctx, argv[4]);
+ sw = image.width;
+ sh = image.height;
+ }
+ else if( argc >= 9 ) {
+ // drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
+ sx = JSValueToNumberFast(ctx, argv[1]);
+ sy = JSValueToNumberFast(ctx, argv[2]);
+ sw = JSValueToNumberFast(ctx, argv[3]);
+ sh = JSValueToNumberFast(ctx, argv[4]);
+
+ dx = JSValueToNumberFast(ctx, argv[5]);
+ dy = JSValueToNumberFast(ctx, argv[6]);
+ dw = JSValueToNumberFast(ctx, argv[7]);
+ dh = JSValueToNumberFast(ctx, argv[8]);
+ }
+ else {
+ return NULL;
+ }
+
+ ejectaInstance.currentRenderingContext = renderingContext;
+ [renderingContext drawImage:image sx:sx sy:sy sw:sw sh:sh dx:dx dy:dy dw:dw dh:dh];
+
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(fillRect, ctx, argc, argv) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ dx = JSValueToNumberFast(ctx, argv[0]),
+ dy = JSValueToNumberFast(ctx, argv[1]),
+ w = JSValueToNumberFast(ctx, argv[2]),
+ h = JSValueToNumberFast(ctx, argv[3]);
+
+ ejectaInstance.currentRenderingContext = renderingContext;
+ [renderingContext fillRectX:dx y:dy w:w h:h];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(strokeRect, ctx, argc, argv) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ dx = JSValueToNumberFast(ctx, argv[0]),
+ dy = JSValueToNumberFast(ctx, argv[1]),
+ w = JSValueToNumberFast(ctx, argv[2]),
+ h = JSValueToNumberFast(ctx, argv[3]);
+
+ ejectaInstance.currentRenderingContext = renderingContext;
+ [renderingContext strokeRectX:dx y:dy w:w h:h];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(clearRect, ctx, argc, argv) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ dx = JSValueToNumberFast(ctx, argv[0]),
+ dy = JSValueToNumberFast(ctx, argv[1]),
+ w = JSValueToNumberFast(ctx, argv[2]),
+ h = JSValueToNumberFast(ctx, argv[3]);
+
+ ejectaInstance.currentRenderingContext = renderingContext;
+ [renderingContext clearRectX:dx y:dy w:w h:h];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION(getImageData, ctx, argc, argv) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ sx = JSValueToNumberFast(ctx, argv[0]),
+ sy = JSValueToNumberFast(ctx, argv[1]),
+ sw = JSValueToNumberFast(ctx, argv[2]),
+ sh = JSValueToNumberFast(ctx, argv[3]);
+
+ // Get the image data
+ ejectaInstance.currentRenderingContext = renderingContext;
+ EJImageData * imageData = [renderingContext getImageDataSx:sx sy:sy sw:sw sh:sh];
+
+ // Create the JS object
+ JSClassRef imageDataClass = [[EJApp instance] getJSClassForClass:[EJBindingImageData class]];
+ JSObjectRef obj = JSObjectMake( ctx, imageDataClass, NULL );
+
+ // Create the native instance
+ EJBindingImageData * jsImageData = [[EJBindingImageData alloc] initWithContext:ctx object:obj imageData:imageData];
+
+ // Attach the native instance to the js object
+ JSObjectSetPrivate( obj, (void *)jsImageData );
+ return obj;
+}
+
+EJ_BIND_FUNCTION(createImageData, ctx, argc, argv) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ sw = JSValueToNumberFast(ctx, argv[0]),
+ sh = JSValueToNumberFast(ctx, argv[1]);
+
+ GLubyte * pixels = malloc( sw * sh * 4 * sizeof(GLubyte));
+ EJImageData * imageData = [[[EJImageData alloc] initWithWidth:sw height:sh pixels:pixels] autorelease];
+
+ // Create the JS object
+ JSClassRef imageDataClass = [[EJApp instance] getJSClassForClass:[EJBindingImageData class]];
+ JSObjectRef obj = JSObjectMake( ctx, imageDataClass, NULL );
+
+ // Create the native instance
+ EJBindingImageData * jsImageData = [[EJBindingImageData alloc] initWithContext:ctx object:obj imageData:imageData];
+
+ // Attach the native instance to the js object
+ JSObjectSetPrivate( obj, (void *)jsImageData );
+ return obj;
+}
+
+EJ_BIND_FUNCTION(putImageData, ctx, argc, argv) {
+ if( argc < 3 ) { return NULL; }
+
+ EJBindingImageData * jsImageData = (EJBindingImageData *)JSObjectGetPrivate((JSObjectRef)argv[0]);
+ float
+ dx = JSValueToNumberFast(ctx, argv[1]),
+ dy = JSValueToNumberFast(ctx, argv[2]);
+
+ ejectaInstance.currentRenderingContext = renderingContext;
+ [renderingContext putImageData:jsImageData.imageData dx:dx dy:dy];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( beginPath, ctx, argc, argv ) {
+ [renderingContext beginPath];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( closePath, ctx, argc, argv ) {
+ [renderingContext closePath];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( fill, ctx, argc, argv ) {
+ [renderingContext fill];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( stroke, ctx, argc, argv ) {
+ [renderingContext stroke];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( moveTo, ctx, argc, argv ) {
+ if( argc < 2 ) { return NULL; }
+
+ float
+ x = JSValueToNumberFast(ctx, argv[0]),
+ y = JSValueToNumberFast(ctx, argv[1]);
+ [renderingContext moveToX:x y:y];
+
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( lineTo, ctx, argc, argv ) {
+ if( argc < 2 ) { return NULL; }
+
+ float
+ x = JSValueToNumberFast(ctx, argv[0]),
+ y = JSValueToNumberFast(ctx, argv[1]);
+ [renderingContext lineToX:x y:y];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( rect, ctx, argc, argv ) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ x = JSValueToNumberFast(ctx, argv[0]),
+ y = JSValueToNumberFast(ctx, argv[1]),
+ w = JSValueToNumberFast(ctx, argv[2]),
+ h = JSValueToNumberFast(ctx, argv[3]);
+ [renderingContext rectX:x y:y w:w h:h];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( bezierCurveTo, ctx, argc, argv ) {
+ if( argc < 6 ) { return NULL; }
+
+ float
+ cpx1 = JSValueToNumberFast(ctx, argv[0]),
+ cpy1 = JSValueToNumberFast(ctx, argv[1]),
+ cpx2 = JSValueToNumberFast(ctx, argv[2]),
+ cpy2 = JSValueToNumberFast(ctx, argv[3]),
+ x = JSValueToNumberFast(ctx, argv[4]),
+ y = JSValueToNumberFast(ctx, argv[5]);
+ [renderingContext bezierCurveToCpx1:cpx1 cpy1:cpy1 cpx2:cpx2 cpy2:cpy2 x:x y:y];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( quadraticCurveTo, ctx, argc, argv ) {
+ if( argc < 4 ) { return NULL; }
+
+ float
+ cpx = JSValueToNumberFast(ctx, argv[0]),
+ cpy = JSValueToNumberFast(ctx, argv[1]),
+ x = JSValueToNumberFast(ctx, argv[2]),
+ y = JSValueToNumberFast(ctx, argv[3]);
+ [renderingContext quadraticCurveToCpx:cpx cpy:cpy x:x y:y];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( arcTo, ctx, argc, argv ) {
+ if( argc < 5 ) { return NULL; }
+
+ float
+ x1 = JSValueToNumberFast(ctx, argv[0]),
+ y1 = JSValueToNumberFast(ctx, argv[1]),
+ x2 = JSValueToNumberFast(ctx, argv[2]),
+ y2 = JSValueToNumberFast(ctx, argv[3]),
+ radius = JSValueToNumberFast(ctx, argv[4]);
+ [renderingContext arcToX1:x1 y1:y1 x2:x2 y2:y2 radius:radius];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION( arc, ctx, argc, argv ) {
+ if( argc < 5 ) { return NULL; }
+
+ float
+ x = JSValueToNumberFast(ctx, argv[0]),
+ y = JSValueToNumberFast(ctx, argv[1]),
+ radius = JSValueToNumberFast(ctx, argv[2]),
+ startAngle = JSValueToNumberFast(ctx, argv[3]),
+ endAngle = JSValueToNumberFast(ctx, argv[4]);
+ BOOL antiClockwise = argc < 6 ? false : JSValueToBoolean(ctx, argv[5]);
+ [renderingContext arcX:x y:y radius:radius startAngle:startAngle endAngle:endAngle antiClockwise:antiClockwise];
+ return NULL;
+}
+
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( fillText );
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( createRadialGradient );
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( createLinearGradient );
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( createPattern );
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( clip );
+EJ_BIND_FUNCTION_NOT_IMPLEMENTED( isPointInPath );
+
+@end
14 Classes/Ejecta/EJCanvas/EJBindingImage.h
@@ -0,0 +1,14 @@
+#import "EJBindingEventedBase.h"
+#import "EJTexture.h"
+#import "EJDrawable.h"
+
+@interface EJBindingImage : EJBindingEventedBase <EJDrawable> {
+ EJTexture * texture;
+ NSString * path;
+ EAGLContext * oldContext;
+ BOOL loading;
+}
+
+@property (readonly, nonatomic) EJTexture * texture;
+
+@end
93 Classes/Ejecta/EJCanvas/EJBindingImage.m
@@ -0,0 +1,93 @@
+#import "EJBindingImage.h"
+#import "EJApp.h"
+
+@implementation EJBindingImage
+@synthesize texture;
+
+- (void)beginLoad {
+ // This will begin loading the texture in a background thread and will call the
+ // JavaScript onload callback when done
+ loading = YES;
+ oldContext = [EAGLContext currentContext];
+
+ NSInvocationOperation* loadOp = [[NSInvocationOperation alloc] initWithTarget:self
+ selector:@selector(load:) object:oldContext];
+ [loadOp setThreadPriority:0.0];
+ [[EJApp instance].opQueue addOperation:loadOp];
+ [loadOp release];
+}
+
+- (void)load:(EAGLContext *)context {
+ NSAutoreleasePool *autoreleasepool = [[NSAutoreleasePool alloc] init];
+
+ NSLog(@"Loading Image: %@", path );
+ EJTexture * tempTex = [[[EJTexture alloc] initWithPath:[EJApp pathForResource:path] context:context] autorelease];
+ [self performSelectorOnMainThread:@selector(endLoad:) withObject:tempTex waitUntilDone:NO];
+
+ [autoreleasepool release];
+}
+
+- (void)endLoad:(EJTexture *)tex {
+ [EAGLContext setCurrentContext:oldContext];
+ loading = NO;
+ texture = [tex retain];
+ if( tex.textureId ) {
+ [self triggerEvent:@"load" argc:0 argv:NULL];
+ }
+ else {
+ [self triggerEvent:@"error" argc:0 argv:NULL];
+ }
+}
+
+- (void)dealloc {
+ [texture release];
+ [path release];
+ [super dealloc];
+}
+
+EJ_BIND_GET(src, ctx ) {
+ JSStringRef src = JSStringCreateWithUTF8CString( [path UTF8String] );
+ JSValueRef ret = JSValueMakeString(ctx, src);
+ JSStringRelease(src);
+ return ret;
+}
+
+EJ_BIND_SET(src, ctx, value) {
+ // If the texture is still loading, do nothing to avoid confusion
+ // This will break some edge cases; FIXME
+ if( loading ) { return; }
+
+ NSString * newPath = JSValueToNSString( ctx, value );
+
+ // Same as the old path? Nothing to do here
+ if( [path isEqualToString:newPath] ) { return; }
+
+
+ // Release the old path and texture?
+ if( path ) {
+ [path release];
+ path = nil;
+
+ [texture release];
+ texture = nil;
+ }
+
+ if( [newPath length] ) {
+ path = [newPath retain];
+ [self beginLoad];
+ }
+}
+
+EJ_BIND_GET(width, ctx ) {
+ return JSValueMakeNumber( ctx, texture ? texture.width : 0);
+}
+
+EJ_BIND_GET(height, ctx ) {
+ return JSValueMakeNumber( ctx, texture ? texture.height : 0 );
+}
+
+EJ_BIND_EVENT(load);
+EJ_BIND_EVENT(error);
+
+
+@end
13 Classes/Ejecta/EJCanvas/EJBindingImageData.h
@@ -0,0 +1,13 @@
+#import "