Home: Selected PROJECTS

Adult Trike Kiddie Cab

And now for something completely different...

Adult tricycle white with wooden kid bench

The idea for this emerged from the repeated dilemma of needing to transport two kids, which the trail-a-bike simply can't accommodate. I started looking at cargo bikes, or "bakfietsen" which I had seen all over Amsterdam, but found the cost prohibitive. Trikes on the other hand, were roughly the same price as two-wheelers. I found what I was after on Craig's list, gave the bike some TLC, built a custom bench out of some pine support slats I rescued from a bed being thrown out, et voilà:

What I started with: Adult Tricycle with Electric Motor Yellow

Old motor components removed: Adult Tricycle Yellow

Primed and painted: Adult Tricycle White

Designing the bench: Tricycle kid bench sketch Tricycle kid bench sketch Tricycle kid bench sketch Tricycle kid bench sketch Tricycle kid bench sketch

Building the bench: Adult tricycle wooden kid bench
I've been toying with the idea of motorizing it (even the most fit of us can only go so far carrying so much weight), hence the locking storage under the seat.
Adult tricycle wooden kid bench Adult tricycle wooden kid bench
<3 airline seat buckles. Why aren't these used in cars?

Final: Adult tricycle white with wooden kid bench

Classroom Cubbies


It was only a passing request from my daughter's first grade teacher - whether I had any ideas for improving the book storage above the coat cubbies, which was admittedly an eyesore. Upon some examination of the alcove, I told him he should rethink the entire situation, and via my own pushy compulsion, somehow volunteered to rectify the situation. What I peeled away during demolition was an unused legacy cubby system of hooks mounted on a long shelf, which sat unused behind the existing milk crates housing the kids' stuff. The milk crates rested on top of a couple of unused shelves fashioned out of the doors that once rendered the alcove a large closet. Above all of this nonsense was a shelf, also made of the doors, housing a few hundred pounds of hidden books (yes, above where the kids retrieved their coats), covered with a few pieces of 1/2" plywood, held vertically in place by small bolted slats that he would turn to remove the wood in order to get access.

Some remnants of what I ripped out: milk crate cubbies alcove with shelf

What I started with - 7 chipboard bookcases being sold by a downsizing lawfirm. I like reclaimed materials, not just for the ecological aspect, but because I hate staining and varnishing: bookcases

Sketches: bookcase cubby sketch bookcase cubby sketch

Phases: rolling bookcase shelves with hooks partially done bookcase cubbies

Final: transforming bookcase cubby system front transforming bookcase cubby system back

SPARK Mobile App Contest

Q1 2016 - Q4 2018

Responsibilities: HTML CSS/SASS JS/jQuery PHP/MySQL AWS

Traditional wordpress site, I built a custom theme based off llorix. SPARK for Autism needed an accompanying website for the duration of the contest to handle contest registration, announcements, and community app voting. I worked on this during my time as an internal dev at the Simons Foundation.

Notable tasks: I opted for a kanban approach to development given that feature development needed to occur around events and announcements that comprised the contest. For registration I connected a CF7 form template to a Googlesheet, such that as users registered, their data would be entered into the spreadsheet, which was shared with stakeholders so they would always be up-to-date without me spending time on plebian tasks like pulling reports. Students were asked to register themselves and their teammates by entering their emails and setting a team password. Teammates would receive an invite email, as all participants had to register in order to be eligible for prize money. To set up voting I started with a general star-voting plugin, added some authentication which hit the SPARK api to determine voting status and access privileges, and modified the JS and PHP according to aesthetic/functional specifications.

Spectrum News

Q1 2016 - Q4 2018

Responsibilities: HTML CSS/SASS JS/jQuery PHP/MySQL AWS

Traditional wordpress site, with custom theme built by Madwell. I inherited the task of maintenance, bugfixes, new feature implementation during my time as an internal dev at the Simons Foundation.

Notable tasks: Migrating the site off WP Engine and onto AWS. Getting a team new to Agile to adopt a loosely SCRUM process. Shaving about .1 seconds from the pageload DB query duration with a 30% reduction in DB calls by converting some massive reqs to ajax calls when needed and replacing wp methods with cached vars where applicable.

Simons Foundation

Q1 2016 - Q4 2018

Responsibilities: HTML CSS/SASS JS/jQuery PHP/MySQL AWS

I inherited the task of maintenance, bugfixes, and new feature implementation during my time as an internal dev at the Simons Foundation. The bugs and tech debt that the initial vendor delivered with the project was a painful reminder of why waterfall development should be avoided at all costs.

Notable tasks: Getting a team new to Agile to adopt a SCRUM process running on 2-week sprints. Diving deeper in MySQL procedures to handle migrations of the alien information schema handed off to us. Migrating an Unfuddle ticket project to our Jira account in a jungle of chunked spreadsheets, field-remapping, and consolidation scripts to preserve ticket comment history. Migrating ACF field schema out of the PHP into which it had been distilled to prevent alteration by non-developer WP admins, back into the DB. Running a strategy-mapping workshop with the team to identify strategic solutions to obstacles (I am forever grateful to Rob Purdie from whom I learned this method). Writing a plugin that works with the arxiv.org api to provide a tool for helping content editors to sync arxiv's publications authored by foundation scientists with our wordpress publications archive. Restructuring hundreds of daisy-chained SASS files into a more sane and manageable schema.

html5 banners, various clients

2016

html/css/js, animation with Greensock libraries.

replay

replay

replay

Auth fail on image stream when generating PDF’s with PHP

The error:
PHP Warning: failed to open stream: HTTP request failed! HTTP/1.1 401 Unauthorized

…and you either get blanks where there should be images in your PDF, or you get no PDF at all. I encountered this problem while using wp-post-to-pdf-enhanced and Kalin's PDF Creation Station for Wordpress.

The methods throwing the warning:
getimagesize()
imagecreatefromjpeg()

The cause:
This appears to happen under 2 conditions: Apache authentication is required on the directory, AND the path to the image file contains directories named with numbers.

The fix:
You need to change just one of these conditions for it to work. Either rename the image directories to NOT begin with numbers, OR remove the apache authentication.

THINK Academy - IBM

Think Academy is a desktop and mobile MOOC application written in HTML/CSS/JS, using IBM Connections' social API and Brightcove video. Monthly topic deployments to the global IBM community make it highly agile project Read more here.

Role: HTML/CSS/JS, PHP/MySQL, Grunt, Brightcove video, Dotsub closed captions, Accessibility standards




THINK Exchange - IBM

think-exchange.com is IBM's members-only community, events program and content portal for C-level executives. It began with THINK Marketing for CMO's and has expanded to include Finance and Technology.

Responsive site using PHP/MySQL, HTML/CSS/JS, Wordpress, Buddypress, Media element.

Brightcove videoplayer accessibility plugin

Q2 2014
Javascript/Actionscript solution to Brightcove player accessibility problems

DOWNLOAD SRC

When I wrote this, Brightcove did have an "Accessible" template - which was not even remotely accessible. Accessible means I should be able to navigate the web page/application with my eyes closed, using only the keyboard, and access the same content in as smooth and efficient an experience as a visually-equipped user.

This solution isn't perfect - I still received complaints from users about tab-accessibility inconsistency and video control button labels not changing on toggle, which I couldn't control without access to the video src. Ultimately, we decided to move off of Brightcove as a video solution, but if you're stuck with it, this will help significantly.

Tabbing into and out of Flash objects embedded on an HTML page is a common problem -- if you can't tab into an embedded application, it doesn't exist to any user using a screenreader. The solution I stitched together uses a Class from Adobe to setup the tab ordering of clickable elements within the swf, in combination with some javascript by Richard England to handle focus into and out of the swf.

1. Create the swf that will become your custom Brightcove plugin. This is an empty .swf which, upon being added to the stage, will explore the heirarchy of the swf into which is has been imported, find the video controls, will pass those to the SWFFocus class. Below isthe Document class of your .fla.

package {
	import com.adobe.swffocus.SWFFocus;

	import flash.accessibility.AccessibilityProperties;
	import flash.accessibility.Accessibility;
	import flash.display.Sprite;
	import flash.events.Event;
//	import com.richardengland.utils.Tabbing;
	import flash.display.DisplayObjectContainer;
	import flash.display.DisplayObject;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;


	public class SWFFocusFix extends Sprite {
		public function SWFFocusFix() {
			if (stage) {
				init();
			} else {
				addEventListener(Event.ADDED_TO_STAGE, init);
			}
		}

		private function init(event:Event = null):void {
			trace("SWFFocusFix.init");

			var player:Object = parent;
			var counter:Number = 0;

			var buttonContainer:Object;

			while (player.parent) {
				for (var i:uint = 0; i < player.numChildren; i++) {
					var child:DisplayObject = player.getChildAt(i);
					var childClass = getQualifiedClassName(child);

					if (childClass.indexOf("BEMLContainer") != -1) {
						//then this is where we want the tabbing focus to be
						buttonContainer = child;
					}

				}

				player = player.parent;
				counter++;

				if (buttonContainer) {
					break;
				}
			}

			traceDisplayList(DisplayObjectContainer(player));

			if (player) {
				trace("Found player.stage! | " + player.stage);
				if (player.stage) {
					if (hasEventListener(Event.ADDED_TO_STAGE)) {
						removeEventListener(Event.ADDED_TO_STAGE, init);
					}
					/*
					if (buttonContainer) {
					var tabbing = new Tabbing(DisplayObjectContainer(buttonContainer));
					}
					*/
					if (buttonContainer) {
						
							SWFFocus.init(buttonContainer.stage);
					}
				}
			} else {
				if (stage) {
					trace("Using local stage.");
					SWFFocus.init(stage);
				}
			}
		}

		private function traceDisplayList(container:DisplayObjectContainer, indentString:String = ""):void {
			var child:DisplayObject;
			for (var i:uint=0; i < container.numChildren; i++) {
				child = container.getChildAt(i);
				if (child.accessibilityProperties) {
					trace(child.accessibilityProperties.name, child.accessibilityProperties.description );
				}
				trace(indentString, child, child.name);


				if (container.getChildAt(i) is DisplayObjectContainer) {
					traceDisplayList(DisplayObjectContainer(child), indentString + "    ");
				}
				
				
				var childClass = getQualifiedClassName(child);
				//adjustment for the closedCaptions BG
				//if (child.name == "instance137" && childClass.indexOf("GlowingButton") != -1) {
					if (childClass.indexOf("GlowingButton") != -1){
						if (getQualifiedClassName(child.parent.parent.parent).indexOf("CaptionControls") != -1){
							trace("******* removed child " + child.name);
							//child.parent.removeChild(child);
							child.alpha = 0;
							
						}
				}

			}
		}

	}
}


2. In Brightcove VideoCloud, select the player you are embedding, click "Edit" and check the option "Enable Javascript/Actionscript API's":



Upload the swf to your host, and include a reference in your Brightcove BEML template.


<SWFLoader depth="2" width="10" height="10" source="https://www.yourdomain.com/swf/SWFFocusFix.swf"/>



According to their documentation, it seems the XML node to import the plugin can be placed anywhere. I placed it within the video controls HBox and this is where it worked in my project, see complete BEML src:




<Runtime>
  <Labels>
    <label key="controls play">PLAY button labeled via BEML</label>
    <label key="controls pause">PAUSE button labeled via BEML</label>
  </Labels>
  <Theme name="Deluxe" style="Light"></Theme>
  <Layout boxType="vbox">
    <ChromelessVideoPlayer id="videoPlayer" useOverlayMenu="false">
      <ChromelessControls boxType="vbox" visible="{!videoPlayer.menu.open}" backgroundImage="https://www.yourdomain.com/images/bc_chromelessBG.png">
        <Spacer width="1"/>
        <HBox hAlign="center" height="10">
          <Playhead id="playhead" height="10" mediaController="{videoPlayer}" useTimeToolTip="true" enabled="{videoPlayer.video}"/>
        </HBox>
        <HBox hAlign="center" height="50">
          <HBox id="defaultView" gutter="0" padding="0" vAlign="middle" maxWidth="820">
            <ToggleButton id="playButton" showBack="false" tabIndex="0" width="65" iconName="play" toggledIconName="pause" accessibleName="acc play pause name" tooltip="controls play tooltip" toggledTooltip="controls pause tooltip" click="{videoPlayer.play()}" toggledClick="{videoPlayer.pause()}" toggled="{videoPlayer.playing}" enabled="{videoPlayer.video}"/>
            <Canvas height="50">
              <HBox gutter="4" vAlign="middle" padding="10">
                <Canvas width="40">
                  <Label id="durationLabel" y="-1" style="font-weight:bold;" tabIndex="1" text="{format(videoPlayer.mediaDuration, SecondsTimecodeFormatter)}" vAlign="middle" hAlign="left" alpha=".75"/>
                </Canvas>
              </HBox>
            </Canvas>
            <CaptionControls hoverMenu="true" id="captionsButton" showBack="false" tabIndex="2" width="50" height="50" accessibleName="Closed captions button. Use spacebar to toggle " iconName="cc-on" toggledIconName="cc-off" tooltip="acc cc showcaptions" toggledTooltip="no CC" click="{videoPlayer.enableCaptions(true)}" toggledClick="{videoPlayer.enableCaptions(false)}" toggled="{videoPlayer.captionsEnabled}" includeInLayout="{videoPlayer.captionsAvailable}"/>
            <VolumeControl id="volumeButton" showBack="false" tabIndex="3" width="50" height="50" accessibleName="acc volume name" mediaController="{videoPlayer}" useOverlayLayer="false" iconName="volume" mutedIconName="muted" tooltip="mute tooltip" mutedTooltip="unmute tooltip" useZeroWidth="true" horizontalPadding="10" popupGutter="3" popupHeight="100" sliderHeight="20"/>
            <ToggleButton id="fullscreenButton" showBack="false" tabIndex="4" width="50" height="50" accessibleName="acc fullscreen name" iconName="maximize" toggledIconName="minimize" tooltip="controls maximize tooltip" toggledTooltip="controls minimize tooltip" click="{videoPlayer.goFullScreen(true)}" toggledClick="{videoPlayer.goFullScreen(false)}" toggled="{videoPlayer.fullscreen}"/>
            <SWFLoader depth="2" width="10" height="10" source="https://www.yourdomain.com/swf/SWFFocusFix.swf"/>
          </HBox>
        </HBox>
      </ChromelessControls>
    </ChromelessVideoPlayer>
  </Layout>
</Runtime>





3. Add a crossdomain.xml policy file to the directory where you are hosting the swf.


<cross-domain-policy>
  <allow-access-from domain="*.yourdomain.com"/>
<allow-access-from domain="admin.brightcove.com" />
<allow-access-from domain="sadmin.brightcove.com" />
</cross-domain-policy>



4. Add a reference to the Javascript to your HTML page. Use <script> or ajax, whatever you please, as long as it is loaded by the time the user needs to tab into and out of the Brightcove player. stealFocus(){} is the function you must modify in order to determine where focus should go when the user has tabbed out of your flash application.




/**
* Cross browser tabbing solution/engine for Flash projects
* 
* Released under MIT license:
* http://www.opensource.org/licenses/mit-license.php
* 
* @author Richard England 2011
* @see http://www.richardengland.co.uk
* @version 0.1
*/


var lastAction = ""; //store last "click" selection (useful for toggle states for example - e.g. play/pause - remembers focus)
var index = 0;
var flashObj; // the flash player object
var firstInput; // the first input object
var shifted = false;

function playerReady( obj ){
	flashObj = obj;
	//get the flash object's "top" CSS value
	var topV = $("#flashplayer").offset().top;
	//add a form to the document and places it at same top offset as flash player object
	$("body").prepend('<form name="form1" id="form1" style="position:absolute; top:'+topV+'px; left:-1500px;"></form>');

	//flashObj returning no name for some reason?
	t = thisMovie("flashplayer");
	t.refreshTabbingList();
	
}

doOnce = false;
function stealFocus( shiftKeyDown ){
	//INSERT YOUR JS FUNCTIONS, or bind to window event:
	
	//are we tabbing backwards or forwards?
	if (shiftKeyDown){
		$(window).trigger("VIDEO_SHIFT_TAB");
	}else{
		$(window).trigger("VIDEO_TAB");
	}
	
	if(!doOnce){
		//alert("steal the focus");
		doOnce = true;
	}
	
}

function addFormField( name, accessName, accessDesc, tabIndex, theTop) {
	//trace("tabbing: addFormField");

	var id = document.getElementById("id").value;
	var accesskey = "";
	if(accessDesc == "") accessDesc = accessName;
	if(index == 0) accesskey = " accesskey='q' "; //for first item only - keyboard shortcut
	var field = $("#form1").append("<div class='temp' id='row" + id + "'><label for='txt" + id + "'>" + accessDesc + " <input tabindex='"+tabIndex+"' type='button' size='20' name='" + name + "' id='"+ name + "' value='"+accessName+"' alt='"+accessName+"' title='"+accessName+"' onfocus='sendFocus( this ); ' onclick='doAction( this );' " + accesskey + "></label></div>");
	
	//set the position so scroll will work properly with tabbing
	$("#row" + id).css( 
              { 
			  "position"	: "absolute",
              "top"			: theTop + "px" ,
              "display"		:"block" 
              }
	)
	id = (id - 1) + 2;

	document.getElementById("id").value = id;
	
}

function addTextField( name, accessName, accessDesc, tabIndex, theTop ) {
	//trace("tabbing: addTextField");

	var id = document.getElementById("id").value;
	if(accessDesc == "") accessDesc = accessName;
	$("#form1").append("<div class='temp' id='row" + id + "' style='position:absolute; top:" + theTop + "px; left:0px;'><label for='txt" + id + "'>" + accessDesc + " <input tabindex='"+tabIndex+"' type='text' size='20' name='" + name + "' id='"+ name + "' value='' alt='"+accessName+"' title='"+accessName+"' onfocus='sendFocus( this ); ' onclick='doAction( this );' onkeyup='doTextUpdate(this)' style='position:absolute;' ></div>");

	id = (id - 1) + 2;

	document.getElementById("id").value = id;
	
}

function removeAllFormFields() {
	//trace("tabbing: removeAllFormFields");
	$(".temp").remove();
	
	document.getElementById('form1').innerHTML = '<input type="hidden" id="id" value="1">';
}

function removeFormField(id) {

	$(id).remove();

}


//receives a new tabbing list object/array from Flash and generates the form by looping through the list
function newTabbingList( obj ){
	//trace("tabbing: newTabbingList");
//alert("newTabbingList");
	removeAllFormFields();

	var i = 0;
	var highestTabIndex = 0;
	var isLastActionStillAvailable = false;

	var lowestTabIndex = 10000;
	while(i < obj.length ) {
		if(obj[i].tabIndex < lowestTabIndex) {
			firstInput = obj[i].name;
			lowestTabIndex = obj[i].tabIndex;
		}
		if(lastAction == obj[i].name)  isLastActionStillAvailable = true;
		if(obj[i].isText){
			addTextField(obj[i].name, obj[i].accessibilityName, obj[i].accessibilityDesc, obj[i].tabIndex, obj[i].theY);
		} else {
			addFormField(obj[i].name, obj[i].accessibilityName, obj[i].accessibilityDesc, obj[i].tabIndex, obj[i].theY);
		}
		if(obj[i].tabIndex > highestTabIndex) highestTabIndex = obj[i].tabIndex;
		i++
	}
	
	
	if(lastAction != "" && isLastActionStillAvailable){
		
		$("#" + lastAction).focus();
	} else {
		
		$("#" + firstInput).focus();
	}

	addFormField("blank", "blank",  "blank", highestTabIndex + 1, 0);
	
	
	index = i;
}

function doPlayFocus(){
	//trace("tabbing: doPlayFocus");
	t = thisMovie("flashplayer");
	t.setFocusTo("playButton", false);
}

//sends text input back to text input in Flash object
function doTextUpdate( inputFocus ){
	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	t.sendTextUpdate (id , {txt: inputFocus.value});
	lastAction = id;
	
	//alert("doTextUpdate " + inputFocus.value);
}


function doAction( inputFocus ){
	//trace("tabbing: doAction");	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	t.sendTabAction (id , false);
	lastAction = id;
	
	//alert("doAction " + inputFocus.id);
	//$("#" + lastAction).focus();
}

function thisMovie(movieName) {
	 if (navigator.appName.indexOf("Microsoft") != -1) {
		 return window[movieName];
	 } else {
		 return document.getElementById(movieName);
	 }
 }
 
function sendFocus( inputFocus ) {
	//trace("tabbing: sendFocus");	
	id = inputFocus.id;
	t = thisMovie("flashplayer");
	if(id == "blank") {
		//fix for IE focus
		$("#" + firstInput).focus();
	} else {
		t.setFocusTo(id, false);
	}
	
	
}