Not Motion Graphics.

My work extends beyond the screen into physical design and prototyping. Through collaborations with Wieden + Kennedy’s Creative Engineering team and ongoing personal projects, I explore 3D design, digital fabrication, and electronics. This collection includes both professional and independent work focused on translating ideas into tangible objects.

Minecraft Toys

Client: McDonalds

Learn more

Scripting Examples for Animation

Bracelet Generator

The visuals for this campaign were based on friendship bracelets. I wrote a Python script in Cinema 4D that procedurally generates a bracelet from a predefined library of bead geometries. By changing the name of a layer and re-running the script, the bracelet automatically rebuilds and stays linked to the animation, allowing for rapid iteration and versioning.

Inverse Kinematic Rig for After Effects

// Bracelet Generator
import c4d
import copy

# Helper function to get the correct object index from the character
def get_object_index_from_char(char):
    # For standard characters 'A'-'Z', 'a'-'z', and '0'-'9'
    if 'A' <= char <= 'Z':  # A to Z
        return ord(char) - ord('A')  # Index 0-25 for A-Z
    elif 'a' <= char <= 'z':  # a to z
        return ord(char.upper()) - ord('A')  # Also map lowercase to A-Z range
    elif '0' <= char <= '9':  # 0 to 9
        return ord(char) - ord('0') + 26  # Index 26-35 for 0-9
    # Handle special characters '+', '?', '<', '>', '$', '*', '_'
    special_chars = {'+': 36, '?': 37, '<': 38, '>': 39, '$': 40, '*': 41, '_': 42, '=': 43}
    return special_chars.get(char)

def check_conditions(cloner, linked_object):
    # Conditions that need to be met for the script to set the clone count correctly
    warnings = []

    # Check if the cloner is in Object Mode (ID_MG_MOTIONGENERATOR_MODE == 0)
    if cloner[c4d.ID_MG_MOTIONGENERATOR_MODE] != 0:
        warnings.append("Cloner is not in Object Mode.")

    # Check if the linked object is a spline
    if linked_object and linked_object.GetType() not in [c4d.Ospline, c4d.Osplinecircle, c4d.Osplinerectangle]:  # Add more spline types as needed
        warnings.append("The linked object is not a spline.")

    # Check if the spline mode is set to 0 (MG_SPLINE_MODE == 0)
    if cloner[c4d.MG_SPLINE_MODE] != 0:
        warnings.append("Cloner distribution is not set to 'count' (MG_SPLINE_MODE is not 0).")

    return warnings

def populate_cloner(cloner):
    try:
        doc = c4d.documents.GetActiveDocument()  # Get the active document

        cloner_name = cloner.GetName()  # Get the name of the cloner object

        # Validate the cloner name
        if not cloner_name:
            c4d.gui.MessageDialog("Error: Cloner name is empty.")
            return

        # Check for linked object (MG_OBJECT_LINK)
        linked_object = cloner.GetParameter(c4d.MG_OBJECT_LINK, c4d.DESCFLAGS_GET_0)
        if linked_object:
            print(f"Cloner is cloning over object: {linked_object.GetName()}")
        else:
            c4d.gui.MessageDialog("Cloner does not have a linked object.")
            return

        # Search for the selection object by name
        selection_obj = doc.SearchObject("character_refs")  # Replace with the actual name of the selection object
        if not selection_obj:
            c4d.gui.MessageDialog("Error: Selection object not found.")
            return

        # Access the In-/Exclusion list (InExcludeData) from the selection object
        in_exclude_list = selection_obj.GetParameter(c4d.SELECTIONOBJECT_LIST, c4d.DESCFLAGS_GET_0)
        
        if not in_exclude_list or in_exclude_list.GetObjectCount() == 0:
            c4d.gui.MessageDialog("Error: The selection object list is empty.")
            return

        # Clear existing children in the cloner
        children = cloner.GetChildren()
        for child in children:
            doc.AddUndo(c4d.UNDOTYPE_DELETE, child)
            child.Remove()

        # Track the last inserted instance for InsertAfter
        last_instance = None

        # Count the number of clones
        clone_count = 0

        # Collect warnings for unassigned characters
        unassigned_warnings = []

        # Iterate through each character in the cloner's name
        for i, char in enumerate(cloner_name):
            # Create a new instance object for each character, including underscores
            instance = c4d.BaseObject(c4d.Oinstance)

            # Special naming for $, *, and _
            if char == "$":
                instance.SetName("heart_instance")
            elif char == "*":
                instance.SetName("star_instance")
            elif char == "_":
                instance.SetName("Filler_bead_instance")  # Renaming for underscore instances
            else:
                instance.SetName(f"{char}_instance")  # Default naming with character followed by _instance

            # Get the corresponding object index from the character
            object_index = get_object_index_from_char(char)

            if object_index is not None and object_index < in_exclude_list.GetObjectCount():
                ref_object = in_exclude_list.ObjectFromIndex(doc, object_index)
                if ref_object:
                    print(f"Assigning reference object {ref_object.GetName()} to instance '{char}'.")
                    instance[c4d.INSTANCEOBJECT_LINK] = ref_object  # Link the instance to the reference object
                else:
                    unassigned_warnings.append(f"Warning: No reference object found for character '{char}'. Instance left empty.")
                    instance[c4d.INSTANCEOBJECT_LINK] = None  # Leave the instance unassigned
            else:
                # Character does not have a corresponding object in the list, leave the instance empty and warn
                unassigned_warnings.append(f"Warning: Character '{char}' is unassigned, instance object left empty.")
                instance[c4d.INSTANCEOBJECT_LINK] = None  # Leave the instance unassigned

            # Add the instance object as a child of the cloner
            if last_instance is None:
                instance.InsertUnder(cloner)
            else:
                instance.InsertAfter(last_instance)

            # Track the current instance as the last inserted one
            last_instance = instance

            # Increase clone count
            clone_count += 1

        # Update the clone count using the correct parameter
        cloner.SetParameter(c4d.MG_SPLINE_COUNT, len(cloner_name), c4d.DESCFLAGS_SET_0)

        # Print the length of the name and the clone count
        print(f"Cloner name length: {len(cloner_name)}")
        print(f"Number of clones created: {clone_count}")

        # After the script finishes running, check if any warnings need to be issued
        warnings = check_conditions(cloner, linked_object)
        if warnings:
            warning_message = "\n".join(warnings)
            c4d.gui.MessageDialog(f"Warning: Cloner count couldn't be set due to the following reasons:\n{warning_message}")

        # Display all collected unassigned warnings at once
        if unassigned_warnings:
            warning_message = "\n".join(unassigned_warnings)
            c4d.gui.MessageDialog(f"Unassigned Character Warnings:\n{warning_message}")

        # Update Cinema 4D document
        c4d.EventAdd()

    except Exception as e:
        c4d.gui.MessageDialog(f"Error: {e}")

# Main function to run the script on the selected object
def main():
    doc = c4d.documents.GetActiveDocument()  # Get the active document
    selected_object = doc.GetActiveObject()  # Get the currently selected object

    # Check if the selected object is a cloner
    if selected_object and selected_object.GetType() == c4d.Omgcloner:
        populate_cloner(selected_object)
    else:
        c4d.gui.MessageDialog("Error: No cloner object selected. Please select a cloner object and run the script.")

# Run the main function
if __name__ == "__main__":
    main()

//noodleIK Rig

function solveAngle(a, b, c) { //solves for C
	var x = (a * a + b * b - c * c) / (2 * a*b);
	return radiansToDegrees(Math.acos(clamp(x,-1,1)));
}

function findSlope(P1,P2){
	m = P2-P1;
	return radiansToDegrees(Math.atan2(m[1],m[0]));
}

function midPoint(P1,P2,i){
i = i/100;
P1 = P1*(1-i);
P2 = P2*(i);
return P1+P2;
}



//variables
point_A = thisLayer.toWorld(anchorPoint);
point_B = []
point_C = thisComp.layer("noodleRig | CNTRL").toWorld(anchorPoint);

bendDirection = -thisComp.layer("noodleRig | CNTRL")("Effects")("Noodle Rig")("Bend Direction");

length_b = length(point_A,point_C); 

limbLength = thisComp.layer("noodleRig | CNTRL")("Effects")("Noodle Rig")("Limb Length");
limbRatio = thisComp.layer("noodleRig | CNTRL")("Effects")("Noodle Rig")("Limb Ratio");
limbShrink = thisComp.layer("noodleRig | CNTRL").effect("Noodle Rig")("Shrink");

openStrength = thisComp.layer("noodleRig | CNTRL").effect("Noodle Rig")("Tangent Strength")/100;
closeStrength = thisComp.layer("noodleRig | CNTRL").effect("Noodle Rig")("Closed Tangent Strength")/100;


openTans = thisComp.layer("noodleRig | CNTRL").effect("Noodle Rig")("Tangent Ratio");
closeTans = thisComp.layer("noodleRig | CNTRL").effect("Noodle Rig")("Closed Tangent Ratio");


tanInOpen = linear(openTans,-100,100,0,100);
tanOutOpen = linear(openTans,-100,100,100,0);

tanInClose = linear(closeTans,-100,100,0,100);
tanOutClose = linear(closeTans,-100,100,100,0);
tanRot = [0];

//-------------------------------------------------

  
if(length_b >= limbLength){
	limbLength = length_b;
	} else {
		  limbLength = linear(Math.abs(bendDirection),0,1,length_b,limbLength);
	}


if (bendDirection < 0){
	iAdj = -1;
	} else {
		iAdj = 1;
		}

limbLength_adj = linear(length_b, 0,limbLength,limbLength*limbShrink/100,limbLength);

length_a  =  limbLength_adj*limbRatio/100;
length_c  =  limbLength_adj-limbLength_adj*limbRatio/100;

angle_A=-clamp(solveAngle(length_b,length_a,length_c),-180,180)*iAdj;
angle_B=clamp(solveAngle(length_a,length_c,length_b),-180,180);


slope_AC = findSlope(point_A,point_C);

angle_A = degreesToRadians(slope_AC-(angle_A));
point_B=[length_a*Math.cos(angle_A),length_a*Math.sin(angle_A)];
midPoint = midPoint(point_A,point_C,limbRatio);
slope_B = findSlope(point_B,midPoint-point_A)+90*iAdj;

if (Math.round(angle_B)>=180){
	tanRot = slope_AC;
} else{
	tanRot = slope_B;
}

pathPoint = [point_A,point_B,point_C]

tanRot = degreesToRadians(tanRot);

tanStrength = linear(angle_B, 0,180,closeStrength,openStrength);

tanInStrength = linear(angle_B, 0,180,tanInClose,tanInOpen);
tanInLength = length_a*tanInStrength/100*tanStrength;
tanInPos=[tanInLength*Math.cos(tanRot),tanInLength*Math.sin(tanRot)];
tanIn =  [tanInPos[0],tanInPos[1]];

tanOutStrength = linear(angle_B, 0,180,tanOutClose,tanOutOpen);
tanOutLength = length_c*tanOutStrength/100*tanStrength;
tanOutPos=[tanOutLength*Math.cos(tanRot),tanOutLength*Math.sin(tanRot)];
tanOut =  [tanOutPos[0],tanOutPos[1]];

createPath(points = [[0,0],pathPoint[1],pathPoint[2]-pathPoint[0]], inTangents = [[0,0],-tanIn,[0,0]], outTangents = [[0,0],tanOut,[0,0]], is_closed = false)

Unhappy with existing IK solutions, I set out to build my own inverse kinematics rig in JavaScript for After Effects. The rig supports standard IK behavior, while also introducing the ability to morph between two states based on the angle of the chain. What sets it apart is its vector-based approach, which drives After Effects’ internal shape paths directly, leveraging their tangents to create flexible, noodle- or rubber-hose-style motion. The system was later adapted to Cinema 4D for use in 3D animation, rebuilt as a node-based rig using the same underlying logic.

Creative Engineering & Prototyping

While working at Wieden+Kennedy, I was fortunate to collaborate closely with the Creative Engineering department. Through that partnership, I developed my skills in designing 3D objects intended to integrate with the electronic projects they were building. Drawing on my background in 3D design and 3D printing, I created custom parts to meet their needs, as well as designing and prototyping my own technology-driven objects.

Monocular Adapter for iPhone

This is a custom phone case I designed and 3D printed to hold a small monocular as an external lens for my phone. The case uses a series of embedded magnets to guide and rotate the monocular into position over each of the iPhone’s three camera lenses, allowing it to snap quickly and securely into place.

Moon Phase Clock

Powered by a pi pico, this ongoing project creates a physical representation of the lunar phase using a rotating sphere to accurately present the current phase of the moon..

Bus Timer

This is an ongoing project with one purpose. When’s the next bus?

The device has been developed both for Raspberry Pi Pico and Arduino and uses Google’s GTFS API to retrieve live transit data. The Pico displays upcoming bus information on a small screen, with physical buttons that allows me to scroll between arrival times and view the next available buses.

I often use my downtime to explore technologies adjacent to animation. This series highlights a few tests I created while experimenting with augmented reality.

AR Studies

Oscar the Grouch Filter

This filter first separates a person from their environment, recolors them green, and places them inside a virtual trash can, with a tap-triggered interaction to open and close the lid.

Fry Helmet Filter

Another test takes the Fry Helmet Minecraft toy I designed and composites it onto the user’s head in selfie mode.

Hamburglar Box Filter

The final experiment identifies a source image (a bundle box), uses it to orient the scene, and triggers an animation of the Hamburglar running around the box.