diff --git "a/assets/player/gl/KNWebGLObjects.js" "b/assets/player/gl/KNWebGLObjects.js"
--- "a/assets/player/gl/KNWebGLObjects.js"
+++ "b/assets/player/gl/KNWebGLObjects.js"
@@ -1,3 +1,4274 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b221426d70ab0ed9375a36a008768a4cba30c8fd37b43c2bef77d63c176bcc23
-size 168355
+/*
+ * KNWebGLObjects.js
+ * Keynote HTML Player
+ *
+ * Created by Tungwei Cheng
+ * Copyright (c) 2016-2018 Apple Inc. All rights reserved.
+ */
+
+var kShaderUniformGravity = "Gravity";
+var kShaderUniformMaskTexture = "MaskTexture";
+var kShaderUniformNoiseAmount = "NoiseAmount";
+var kShaderUniformNoiseMax = "NoiseMax";
+var kShaderUniformNoiseSeed = "NoiseSeed";
+var kShaderUniformParticleBurstTiming = "ParticleBurstTiming";
+var kShaderUniformPreviousParticleBurstTiming = "PreviousParticleBurstTiming";
+var kShaderUniformPreviousPercent = "PreviousPercent";
+var kShaderUniformShouldSparkle = "ShouldSparkle";
+var kShaderUniformSparklePeriod = "SparklePeriod";
+var kShaderUniformSparkleStartTime = "SparkleStartTime";
+var kShaderUniformStartScale = "StartScale";
+
+var kShimmerUniformParticleScalePercent = "ParticleScalePercent";
+var kShimmerUniformRotationMatrix = "RotationMatrix";
+
+var KNSparkleMaxParticleLife = 0.667;
+
+var KNWebGLRenderer = Class.create({
+    initialize: function(params) {
+        var canvas = this.canvas = params.canvas;
+        this.canvasId = params.canvasId;
+        this.textureAssets = params.textureAssets;
+        this.durationMax = params.overallEndTime * 1000;
+        this.glPrograms = [];
+
+        // to be used in request animation frame
+        this.elapsed = 0;
+
+        // attempt to create webgl context
+        var gl = this.gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
+
+        // if webgl is not supported then set noGL to true
+        if (!gl) {
+            this.noGL = true;
+            return;
+        }
+
+        // indicate if the animation has started for this renderer
+        this.animationStarted = false;
+
+        gl.viewportWidth = canvas.width;
+        gl.viewportHeight = canvas.height;
+
+        // create default project matrix
+        this.initMVPMatrix();
+    },
+
+    initMVPMatrix: function() {
+        var gl = this.gl;
+        var w = gl.viewportWidth;
+        var h = gl.viewportHeight;
+        var fovradians = 20 * (Math.PI / 180);
+        var backupDistance = h / (2 * Math.tan(fovradians / 2));
+        var frontclipping = backupDistance - (w * 1.5);
+        var backclipping = backupDistance + (w * 15.0);
+
+        // create default ortho and proj matrices
+        this.slideProjectionMatrix = WebGraphics.makePerspectiveMatrix4(20, w / h, Math.max(1, frontclipping), backclipping);
+
+        var translate = WebGraphics.translateMatrix4(WebGraphics.createMatrix4(), -w / 2, -h / 2, -backupDistance);
+
+        this.slideProjectionMatrix = WebGraphics.multiplyMatrix4(this.slideProjectionMatrix, translate);
+        this.slideOrthoMatrix = WebGraphics.makeOrthoMatrix4(0, w, 0, h, -1, 1);
+    },
+
+    setupTexture: function(effect) {
+        var textures = [];
+        this.textureInfoFromEffect(effect.kpfLayer, {"pointX": 0, "pointY": 0}, textures);
+
+        for (var i = 0, length = textures.length; i < length; i++) {
+            var textureId = textures[i].textureId;
+            var image = this.textureAssets[textureId];
+
+            textures[i].texture = KNWebGLUtil.createTexture(this.gl, image);
+        }
+
+        return textures;
+    },
+
+    textureInfoFromEffect: function(kpfLayer, offset, textures) {
+        var textureInfo = {};
+
+        textureInfo.offset = {
+            "pointX": offset.pointX + kpfLayer.bounds.offset.pointX,
+            "pointY": offset.pointY + kpfLayer.bounds.offset.pointY
+        };
+
+        if (kpfLayer.textureId) {
+            textureInfo.textureId = kpfLayer.textureId;
+            textureInfo.width = kpfLayer.bounds.width;
+            textureInfo.height = kpfLayer.bounds.height;
+            textureInfo.initialState = kpfLayer.initialState;
+            textureInfo.animations = kpfLayer.animations;
+            textureInfo.hasHighlightedBulletAnimation = kpfLayer.hasHighlightedBulletAnimation;
+
+            textureInfo.textureRect = {
+                origin: {
+                    x: textureInfo.offset.pointX,
+                    y: textureInfo.offset.pointY
+                },
+                size: {
+                    width: textureInfo.width,
+                    height: textureInfo.height
+                }
+            };
+
+            textures.push(textureInfo);
+        } else {
+            for (var i = 0, length = kpfLayer.layers.length; i < length; i++) {
+                this.textureInfoFromEffect(kpfLayer.layers[i], textureInfo.offset, textures);
+            }
+        }
+    },
+
+    draw: function(effect) {
+        var params = {
+            effect: effect,
+            textures: this.setupTexture(effect)
+        };
+
+        var effectType = effect.type;
+        var program;
+
+        if (effectType === "transition") {
+            switch (effect.name) {
+                case "apple:wipe-iris":
+                    program = new KNWebGLTransitionIris(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.BUKTwist":
+                    program = new KNWebGLTransitionTwist(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNColorPlanes":
+                    program = new KNWebGLTransitionColorPlanes(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.BUKFlop":
+                    program = new KNWebGLTransitionFlop(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNConfetti":
+                    program = new KNWebGLTransitionConfetti(this, params);
+                    break;
+
+                default:
+                    // fallback to dissolve
+                    program = new KNWebGLDissolve(this, params);
+                    break;
+            }
+        } else if (effectType === "buildIn" || effectType === "buildOut") {
+            switch (effect.name) {
+                case "apple:wipe-iris":
+                    program = new KNWebGLBuildIris(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.BUKAnvil":
+                    program = new KNWebGLBuildAnvil(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNFlame":
+                    program = new KNWebGLBuildFlame(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KNFireworks":
+                    program = new KNWebGLBuildFireworks(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNConfetti":
+                    program = new KNWebGLBuildConfetti(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNDiffuse":
+                    program = new KNWebGLBuildDiffuse(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNShimmer":
+                    program = new KNWebGLBuildShimmer(this, params);
+                    break;
+
+                case "com.apple.iWork.Keynote.KLNSparkle":
+                    program = new KNWebGLBuildSparkle(this, params);
+                    break;
+
+                default:
+                    // fallback to dissolve
+                    program = new KNWebGLDissolve(this, params);
+                    break;
+            }
+        } else if (effectType === "smartBuild") {
+            switch (effect.name) {
+                case "apple:gallery-dissolve":
+                    program = new KNWebGLContents(this, params);
+                    break;
+
+                default:
+                    // fallback to dissolve
+                    program = new KNWebGLDissolve(this, params);
+                    break;
+            }
+        }
+
+        // remove existing gl program for the same object when new program is rendered such as build in by highlighted paragraph
+        this.removeProgram(effect.objectID);
+
+        // push new gl program into the array
+        this.glPrograms.push(program);
+    },
+
+    animate: function() {
+        // compute time difference
+        var time = new Date();
+        var difference = 0;
+        if (this.time) {
+            var mseconds = time.getTime();
+            difference = mseconds - this.time;
+            this.time = mseconds;
+        } else {
+            difference = 0;
+            this.time = time.getTime();
+        }
+        this.elapsed += difference;
+
+        var glPrograms = this.glPrograms;
+        var length = glPrograms.length;
+
+        if (this.elapsed <= this.durationMax) {
+            // set up the frame for the next drawing operation, only if there is time left in the animation
+            this.animationRequest = window.requestAnimFrame(this.animate.bind(this));
+        } else {
+            // set gl program to isCompleted when there is no overall event time left
+            for (var i = 0; i < length; i++) {
+                var program = glPrograms[i];
+                program.isCompleted = true;
+            }
+        }
+
+        // clear the buffers before animation frame
+        var gl = this.gl;
+        gl.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+        for (var i = 0; i < length; i++) {
+            var program = glPrograms[i];
+            program.drawFrame(difference, this.elapsed, program.duration);
+        }
+    },
+
+    removeProgram: function(objectID) {
+        var glPrograms = this.glPrograms;
+        var glProgramLength = glPrograms.length;
+
+        // remove gl program for the same objectID from the array
+        while (glProgramLength--) {
+            var glProgram = glPrograms[glProgramLength];
+
+            if (glProgram.effect.objectID === objectID) {
+                glPrograms.splice(glProgramLength, 1);
+            }
+        }
+    },
+
+    resize: function(viewport) {
+        var gl = this.gl;
+        var viewportWidth = viewport.width;
+        var viewportHeight = viewport.height;
+
+        if (gl.viewportWidth !== viewportWidth || gl.viewportHeight !== viewportHeight) {
+            gl.viewport(0, 0, viewportWidth, viewportHeight);
+            gl.viewportWidth = viewportWidth;
+            gl.viewportHeight = viewportHeight;
+        }
+    }
+});
+
+var KNWebGLProgram = Class.create({
+    initialize: function(renderer, programData) {
+        // reference to the renderer
+        this.renderer = renderer;
+
+        // reference to gl context
+        this.gl = renderer.gl;
+
+        // specify textures
+        this.textures = programData.textures;
+
+        // reference to the effect object
+        var effect = this.effect = programData.effect;
+
+        // specify the effect type
+        var type = this.type = effect.type;
+
+        // specific the direction from the effect
+        this.direction = effect.attributes ? effect.attributes.direction : null;
+
+        // specify the duration from the effect
+        this.duration = effect.duration * 1000;
+
+        // boolean to indicate if the effect is a build out
+        this.buildOut = type === "buildOut";
+
+        // boolean to indicate if the effect is a build in
+        this.buildIn = type === "buildIn";
+
+        // create a shader program container object
+        this.program = {};
+
+        // indicate if the effect is completed
+        this.isCompleted = false;
+
+        // setup program data
+        if (programData.programNames) {
+            this.setupProgram(programData);
+        }
+    },
+
+    setupProgram: function(programData) {
+        var gl = this.gl;
+
+        for (var i = 0, length = programData.programNames.length; i < length; i++) {
+            var programName = programData.programNames[i];
+
+            this.program[programName] = KNWebGLUtil.setupProgram(gl, programName);
+        }
+
+        // enable blend function
+        gl.enable(gl.BLEND);
+        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+    }
+});
+
+var KNWebGLContents = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "contents",
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        // initialize percent finish based on effect type
+        this.percentfinished = 0;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var textureRect = this.textures[0].textureRect;
+        var vertexRect = CGRectMake(0, 0, textureRect.size.width, textureRect.size.height);
+        var meshSize = CGSizeMake(2, 2);
+
+        // init contents shader and data buffer
+        var contentsShader = this.contentsShader = new TSDGLShader(gl);
+        contentsShader.initWithContentsShader();
+
+        // contents shader set methods
+        contentsShader.setMat4WithTransform3D(renderer.slideProjectionMatrix, kTSDGLShaderUniformMVPMatrix);
+
+        // outgoing Texture
+        contentsShader.setGLint(0, kTSDGLShaderUniformTexture2);
+
+        // incoming Texture
+        contentsShader.setGLint(1, kTSDGLShaderUniformTexture);
+
+        // init contents data buffer
+        var contentsDataBuffer = this.contentsDataBuffer = new TSDGLDataBuffer(gl);
+        contentsDataBuffer.initWithVertexRect(vertexRect, TSDRectUnit, meshSize, false, false);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var percentfinished = this.percentfinished;
+
+        percentfinished += difference / duration;
+
+        if (percentfinished >= 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        // draw contents using glsl mix
+        this.p_drawContents(percentfinished);
+    },
+
+    p_drawContents: function(percent) {
+        var gl = this.gl;
+        var textures = this.textures;
+        var incomingTexture = textures[0].texture;
+        var outgoingTexture = textures[1].texture;
+
+        // calculate the mix factor in ease in and ease out fashion
+        var mixFactor = TSUSineMap(percent);
+
+        if (percent >= 1) {
+            mixFactor = 1.0;
+        }
+
+        gl.activeTexture(gl.TEXTURE1);
+        gl.bindTexture(gl.TEXTURE_2D, incomingTexture);
+
+        gl.activeTexture(gl.TEXTURE0);
+        gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+
+        this.contentsShader.setGLFloat(mixFactor, "mixFactor");
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+        this.contentsDataBuffer.drawWithShader(this.contentsShader, true);
+    }
+});
+
+var KNWebGLDrawable = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "WebDrawable",
+            programNames:["defaultTextureAndOpacity"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        this.Opacity = 1.0;
+
+        // setup web drawable requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTextureAndOpacity"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textureInfo = this.textures[0];
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // create WebGLBuffer object for texture coordinates
+        var textureCoordinateBuffer = this.textureCoordinateBuffer = gl.createBuffer();
+        var textureCoordinates = this.textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinateBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+
+        // create WebGLBuffer object for position coordinates
+        var positionBuffer = this.positionBuffer = gl.createBuffer();
+        var boxPosition = this.boxPosition = [
+            0.0, 0.0, 0.0,
+            0.0, textureInfo.height, 0.0,
+            textureInfo.width, 0.0, 0.0,
+            textureInfo.width, textureInfo.height, 0.0
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.STATIC_DRAW);
+
+        // move the MVPMatrix to appropriate offset
+        this.MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX, gl.viewportHeight - textureInfo.offset.pointY - textureInfo.height, 0);
+    },
+
+    drawFrame: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTextureAndOpacity"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textures = this.textures;
+        var texture = textures[0].texture;
+
+        gl.useProgram(program.shaderProgram);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinateBuffer);
+        // assigns the WebGLBuffer object currently bound to the gl.ARRAY_BUFFER target to a vertex attribute index
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+        // assigns the WebGLBuffer object currently bound to the gl.ARRAY_BUFFER target to a vertex attribute index
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        // set MVPMatrix
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+
+        // set Opacity
+        gl.uniform1f(uniforms["Opacity"], this.Opacity);
+
+        // set sampler2D Texture in fragment shader to have the value 0, so it matches the texture unit gl.TEXTURE0
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+
+        // bind the texture to texture unit gl.TEXTURE0
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+    }
+});
+
+var KNWebGLFramebufferDrawable = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var gl = renderer.gl;
+        var frameRect = this.frameRect = params.frameRect;
+        var texture = this.texture = this.createFramebufferTexture(gl, frameRect);
+
+        this.buffer = this.createFramebuffer(gl, texture);
+
+        var textureInfo = {
+            width: frameRect.size.width,
+            height: frameRect.size.height,
+            offset: {pointX: 0, pointY: 0},
+            texture: texture
+        };
+
+        this.programData = {
+            name: "FramebufferDrawable",
+            programNames:["defaultTexture"],
+            effect: params.effect,
+            textures: [textureInfo]
+        };
+
+        $super(renderer, this.programData);
+
+        this.drawableFrame = params.drawableFrame;
+
+        // setup web drawable requirements
+        this.animationWillBeginWithContext();
+    },
+
+    createFramebufferTexture: function(gl, rect) {
+        var texture = gl.createTexture();
+
+        // bind texture
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+
+        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
+        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+
+        // setup texture parameters
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+
+        // specify the texture size for memory allocation
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, rect.size.width, rect.size.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+
+        // unbind texture
+        gl.bindTexture(gl.TEXTURE_2D, null);
+
+        return texture;
+    },
+
+    createFramebuffer: function(gl, texture) {
+        var buffer = gl.createFramebuffer();
+
+        //bind framebuffer to texture
+        gl.bindFramebuffer(gl.FRAMEBUFFER, buffer);
+        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
+
+        return buffer;
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTexture"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textureInfo = this.textures[0];
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // create WebGLBuffer object for texture coordinates
+        var textureCoordinateBuffer = this.textureCoordinateBuffer = gl.createBuffer();
+        var textureCoordinates = this.textureCoordinates = [
+            0.0, 1.0,
+            0.0, 0.0,
+            1.0, 1.0,
+            1.0, 0.0,
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinateBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+
+        // create WebGLBuffer object for position coordinates
+        var positionBuffer = this.positionBuffer = gl.createBuffer();
+        var boxPosition = this.boxPosition = [
+            0.0, 0.0, 0.0,
+            0.0, textureInfo.height, 0.0,
+            textureInfo.width, 0.0, 0.0,
+            textureInfo.width, textureInfo.height, 0.0
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.STATIC_DRAW);
+
+        // move the MVPMatrix to appropriate offset
+        this.MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX, gl.viewportHeight - textureInfo.offset.pointY - textureInfo.height, 0);
+    },
+
+    drawFrame: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTexture"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textures = this.textures;
+        var texture = textures[0].texture;
+
+        gl.useProgram(program.shaderProgram);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinateBuffer);
+        // assigns the WebGLBuffer object currently bound to the gl.ARRAY_BUFFER target to a vertex attribute index
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+        // assigns the WebGLBuffer object currently bound to the gl.ARRAY_BUFFER target to a vertex attribute index
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        // set MVPMatrix
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+
+        // set sampler2D Texture in fragment shader to have the value 0, so it matches the texture unit gl.TEXTURE0
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+
+        // bind the texture to texture unit gl.TEXTURE0
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+    }
+});
+
+var KNWebGLDissolve = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "dissolve",
+            programNames:["defaultTextureAndOpacity"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        // initialize percent finish based on effect type
+        this.percentfinished = 0;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTextureAndOpacity"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textureInfo = this.textures[0];
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // create WebGLBuffer object for texture coordinates
+        var textureCoordinateBuffer = this.textureCoordinateBuffer = gl.createBuffer();
+        var textureCoordinates = this.textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinateBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+
+        // create WebGLBuffer object for position coordinates
+        var positionBuffer = this.positionBuffer = gl.createBuffer();
+        var boxPosition = this.boxPosition = [
+            0.0, 0.0, 0.0,
+            0.0, textureInfo.height, 0.0,
+            textureInfo.width, 0.0, 0.0,
+            textureInfo.width, textureInfo.height, 0.0
+        ];
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+        // send vertex data to this bound buffer
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.STATIC_DRAW);
+
+        this.MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX,  gl.viewportHeight - (textureInfo.offset.pointY + textureInfo.height), 0);
+
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var percentfinished = this.percentfinished;
+
+        percentfinished += difference / duration;
+        percentfinished > 1 ? percentfinished = 1 : 0;
+
+        var percentAlpha = TSUSineMap(percentfinished);
+        if (percentfinished === 1) {
+            percentAlpha = 1.0;
+        }
+
+        if (this.buildOut) {
+            percentAlpha = 1 - percentAlpha;
+        }
+
+        this.percentfinished = percentfinished;
+        this.percentAlpha = percentAlpha;
+        this.draw();
+    },
+
+    draw: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["defaultTextureAndOpacity"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textures = this.textures;
+        var texture = textures[0].texture;
+        var outgoingTexture;
+
+        if (textures.length > 1) {
+            outgoingTexture = textures[1].texture;
+        }
+
+        // use this program
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinateBuffer);
+        // send vertex data to this bound buffer
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        // bind WebGLBuffer object to gl.ARRAY_BUFFER target
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+        // send vertex data to this bound buffer
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+        // call enableVertexAttribArray, otherwise it won't draw
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        // set MVPMatrix
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+
+        // set sampler2D Texture in fragment shader to have the value 0, so it matches the texture unit gl.TEXTURE0
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+
+        // bind the texture to texture unit gl.TEXTURE0
+        if (outgoingTexture) {
+            gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+            gl.uniform1f(uniforms["Opacity"], 1.0);
+            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+        }
+
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+        gl.uniform1f(uniforms["Opacity"], this.percentAlpha);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+    }
+});
+
+var KNWebGLTransitionIris = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "apple:wipe-iris",
+            programNames: ["iris"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        // determine the type and direction
+        var direction = this.direction;
+        var directionOut = direction === KNDirection.kKNDirectionOut;
+        var buildOut = this.buildOut;
+
+        if ((buildOut && directionOut) || (!buildOut && !directionOut)) {
+            this.mix = 0.0;
+            this.percentfinished = 1.0;
+        } else {
+            this.mix = 1.0;
+            this.percentfinished = 0.0;
+        }
+
+        this.percentAlpha = 0.0;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["iris"];
+        var attribs = program.attribs;
+        var uniforms = program.uniforms;
+        var textureInfo = this.textures[0];
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // initial scale uniform
+        this.scale = textureInfo.width/textureInfo.height;
+
+        // create buffers
+        var textureCoordinatesBuffer = this.textureCoordinatesBuffer = gl.createBuffer();
+        var textureCoordinates = this.textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinatesBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+
+        var positionBuffer = this.positionBuffer = gl.createBuffer();
+        var boxPosition = this.boxPosition = [
+            0.0, 0.0, 0.0,
+            0.0, textureInfo.height, 0.0,
+            textureInfo.width, 0.0, 0.0,
+            textureInfo.width, textureInfo.height, 0.0
+        ];
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.STATIC_DRAW);
+
+        this.MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX,  gl.viewportHeight - (textureInfo.offset.pointY + textureInfo.height), 0);
+
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        // determine the type and direction
+        var buildOut = this.buildOut;
+        var directionOut = this.direction === KNDirection.kKNDirectionOut;
+        var percentfinished = this.percentfinished;
+
+        if ((buildOut && directionOut) || (!buildOut && !directionOut)) {
+            percentfinished -= difference / duration;
+            percentfinished < 0 ? percentfinished = 0 : 0;
+        } else {
+            percentfinished += difference / duration;
+            percentfinished > 1 ? percentfinished = 1 : 0;
+        }
+
+        var percentAlpha = TSUSineMap(percentfinished);
+        if (percentfinished === 1) {
+            percentAlpha = 1.0;
+        }
+
+        if (buildOut) {
+            percentAlpha = 1 - percentAlpha;
+        }
+
+        this.percentAlpha = percentAlpha;
+        this.percentfinished = percentfinished;
+        this.draw();
+    },
+
+    draw: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["iris"];
+        var attribs = program.attribs;
+        var uniforms = program.uniforms;
+        var textures = this.textures;
+        var texture = textures[0].texture;
+        var textureInfo = textures[0];
+
+        var outgoingTexture;
+        var scale = this.scale;
+
+        if (textures.length > 1) {
+            outgoingTexture = textures[1].texture;
+        }
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // setup attributes
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinatesBuffer);
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        // setup uniforms and textures
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+
+        // set Opacity
+        gl.uniform1f(uniforms["Opacity"], 1);
+
+        // bg texture
+        if (outgoingTexture) {
+            gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+            gl.uniform1f(uniforms["PercentForAlpha"], 0.0);
+            gl.uniform1f(uniforms["Scale"], scale);
+            gl.uniform1f(uniforms["Mix"], 0.0);
+            gl.drawArrays(gl.TRIANGLE_STRIP, 0 , 4);
+        }
+
+        //fg texture
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+        gl.uniform1f(uniforms["PercentForAlpha"], this.percentAlpha);
+        gl.uniform1f(uniforms["Scale"], scale);
+        gl.uniform1f(uniforms["Mix"], this.mix);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0 , 4);
+    }
+});
+
+var KNWebGLBuildIris = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "apple:wipe-iris",
+            programNames: ["iris"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        // determine the type and direction
+        var direction = this.direction;
+        var directionOut = direction === KNDirection.kKNDirectionOut;
+        var buildOut = this.buildOut;
+
+        if ((buildOut && directionOut) || (!buildOut && !directionOut)) {
+            this.mix = 0.0;
+            this.percentfinished = 1.0;
+        } else {
+            this.mix = 1.0;
+            this.percentfinished = 0.0;
+        }
+
+        this.percentAlpha = 0.0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["iris"];
+        var attribs = program.attribs;
+        var uniforms = program.uniforms;
+
+        gl.useProgram(program.shaderProgram);
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // setup attributes
+        var textureCoordinatesBuffer = gl.createBuffer();
+        var textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinatesBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        this.irisSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var width = textureInfo.width;
+            var height = textureInfo.height;
+
+            // initial scale uniform
+            var scale = textureInfo.width/textureInfo.height;
+
+            var positionBuffer = gl.createBuffer();
+            var boxPosition = [
+                0.0, 0.0, 0.0,
+                0.0, textureInfo.height, 0.0,
+                textureInfo.width, 0.0, 0.0,
+                textureInfo.width, textureInfo.height, 0.0
+            ];
+
+            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.STATIC_DRAW);
+
+            var MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX,  gl.viewportHeight - (textureInfo.offset.pointY + textureInfo.height), 0);
+
+            this.irisSystems[i] = {
+                textureCoordinatesBuffer: textureCoordinatesBuffer,
+                positionBuffer: positionBuffer,
+                MVPMatrix: MVPMatrix,
+                scale: scale
+            };
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+
+        // determine the type and direction
+        var buildOut = this.buildOut;
+        var directionOut = this.direction === KNDirection.kKNDirectionOut;
+
+        var percentfinished = this.percentfinished;
+
+        if ((buildOut && directionOut) || (!buildOut && !directionOut)) {
+            percentfinished -= difference / duration;
+
+            if (percentfinished <= 0) {
+                percentfinished = 0;
+                this.isCompleted = true;
+            }
+        } else {
+            percentfinished += difference / duration;
+
+            if (percentfinished >= 1) {
+                percentfinished = 1;
+                this.isCompleted = true;
+            }
+        }
+
+        var percentAlpha = TSUSineMap(percentfinished);
+
+        if (percentfinished === 1) {
+            percentAlpha = 1.0;
+        }
+
+        if (buildOut) {
+            percentAlpha = 1 - percentAlpha;
+        }
+
+        this.percentAlpha = percentAlpha;
+        this.percentfinished = percentfinished;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        if (buildOut) {
+                            opacity = opacityFrom + diff * (1 - this.percentfinished);
+                        } else {
+                            opacity = opacityFrom + diff * this.percentfinished;
+                        }
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (!buildOut) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;;
+                        this.drawableObjects[i].drawFrame();
+                    }
+                    continue;
+                }
+
+                var program = this.program["iris"];
+                var attribs = program.attribs;
+                var uniforms = program.uniforms;
+
+                var irisSystem = this.irisSystems[i];
+                var scale = irisSystem.scale;
+
+                gl.useProgram(program.shaderProgram);
+
+                var textureCoordinatesBuffer = irisSystem.textureCoordinatesBuffer;
+                gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordinatesBuffer);
+                gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+                gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+                var positionBuffer = irisSystem.positionBuffer;
+                gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+                gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+                gl.enableVertexAttribArray(attribs["Position"]);
+
+                var MVPMatrix = irisSystem.MVPMatrix;
+                gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, MVPMatrix);
+                gl.activeTexture(gl.TEXTURE0);
+                gl.uniform1i(uniforms["Texture"], 0);
+
+                // set Opacity
+                gl.uniform1f(uniforms["Opacity"], this.parentOpacity * textureInfo.initialState.opacity);
+
+                //fg texture
+                gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+                gl.uniform1f(uniforms["PercentForAlpha"], this.percentAlpha);
+                gl.uniform1f(uniforms["Scale"], scale);
+                gl.uniform1f(uniforms["Mix"], this.mix);
+                gl.drawArrays(gl.TRIANGLE_STRIP, 0 , 4);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLTransitionTwist = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.BUKTwist",
+            programNames:["twist"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+        this.direction = this.effect.attributes.direction;
+        this.percentfinished = 0.0;
+
+        var mNumPoints = this.mNumPoints = 24;
+        var dx = gl.viewportWidth / (mNumPoints - 1);
+        var dy = gl.viewportHeight / (mNumPoints - 1);
+        var fractionOfUnitLength = 1 / (mNumPoints - 1);
+        var x, y;
+        var TexCoords = this.TexCoords = [];
+        var PositionCoords = this.PositionCoords = [];
+        var NormalCoords = this.NormalCoords = [];
+        for (y = 0; y < mNumPoints; y++) {
+            for (x = 0; x < mNumPoints; x++) {
+                var index = y * mNumPoints + x;
+                PositionCoords[index * 3] = x * dx;
+                PositionCoords[index * 3 + 1] = y * dy;
+                PositionCoords[index * 3 + 2] = 0;
+                TexCoords.push(x * fractionOfUnitLength);
+                TexCoords.push(y * fractionOfUnitLength);
+                NormalCoords.push(0);
+                NormalCoords.push(0);
+                NormalCoords.push(-1);
+            }
+        }
+
+        var index = 0;
+        var elementArray = this.elementArray = [];
+        for (y = 0; y < mNumPoints - 1; y++) {
+            for (x = 0; x < mNumPoints; x++) {
+                elementArray[index++] = (y) * (mNumPoints) + x;
+                elementArray[index++] = (y + 1) * (mNumPoints) + x;
+            }
+        }
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["twist"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+
+        gl.enable(gl.CULL_FACE);
+
+        this.buffers = {};
+        this.buffers["TexCoord"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers["TexCoord"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.TexCoords), gl.STATIC_DRAW);
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        this.buffers["Position"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers["Position"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.PositionCoords), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        this.buffers["Normal"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers["Normal"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.NormalCoords), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Normal"], 3, gl.FLOAT, false, 0, 0);
+        gl.enableVertexAttribArray(attribs["Normal"]);
+
+        this.MVPMatrix = renderer.slideProjectionMatrix;
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+
+        this.AffineTransform = new Matrix3();
+        this.AffineTransform.affineScale(1.0, -1.0);
+        this.AffineTransform.affineTranslate(0.0, 1.0);
+
+        this.AffineIdentity = new Matrix3();
+
+        this.elementIndicesBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementIndicesBuffer);
+        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.elementArray), gl.STATIC_DRAW);
+
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var gl = this.gl;
+        var program = this.program["twist"];
+        var attribs = program.attribs;
+        var percentfinished = this.percentfinished;
+
+        percentfinished += difference / duration;
+        percentfinished > 1 ? percentfinished = 1 : 0;
+        this.specularcolor = TSUSineMap(percentfinished * 2) * 0.5;
+
+        var y, x;
+        var height = gl.viewportHeight / 2.0;
+        var mNumPoints = this.mNumPoints;
+        var TexCoords = this.TexCoords;
+        var PositionCoords = this.PositionCoords;
+        var NormalCoords = this.NormalCoords;
+
+        for (y = 0; y < mNumPoints; y++) {
+            for (x = 0; x < mNumPoints; x++) {
+                var index = y * mNumPoints + x;
+                var start = {};
+                start.x = TexCoords[index * 2];
+                start.y = TexCoords[index * 2 + 1];
+                var angle = -Math.PI * TwistFX(this.direction === KNDirection.kKNDirectionLeftToRight ? start.x : (1 - start.x), percentfinished);
+                var result = {};
+                result.y = (height - (height * (1 - start.y * 2) * Math.cos(angle)));
+                result.z = (height * (1 - start.y * 2) * Math.sin(angle));
+                PositionCoords[index * 3 + 1] = result.y;
+                PositionCoords[index * 3 + 2] = result.z;
+            }
+        }
+
+        for (y = 0; y < mNumPoints; y++) {
+            for (x = 0; x < mNumPoints; x++) {
+                var finalNormal = new vector3();
+                var index = y * mNumPoints + x;
+                for (var q = 0; q < 4; q++) {
+                    var q1x = 0, q1y = 0, q2x = 0, q2y = 0;
+                    switch (q) {
+                    case 0:
+                        q1x = 1;
+                        q2y = 1;
+                        break;
+                    case 1:
+                        q1y = 1;
+                        q2x = -1;
+                        break;
+                    case 2:
+                        q1x = -1;
+                        q2y = -1;
+                        break;
+                    case 3:
+                        q1y = -1;
+                        q2x = 1;
+                    default:
+                        break;
+                    }
+                    if ((x + q1x) < 0 || (x + q2x) < 0 || (y + q1y) < 0 || (y + q2y) < 0
+                        || x + q1x >= mNumPoints || x + q2x >= mNumPoints || y + q1y >= mNumPoints || y + q2y >= mNumPoints) {
+                        continue;
+                    }
+                    var thisV = new vector3([PositionCoords[index * 3], PositionCoords[index * 3 + 1], PositionCoords[index * 3 + 2] ]);
+                    var nextV = new vector3([PositionCoords[((y + q1y) * mNumPoints + (x + q1x)) * 3], PositionCoords[((y + q1y) * mNumPoints + (x + q1x)) * 3 + 1], PositionCoords[((y + q1y) * mNumPoints + (x + q1x)) * 3 + 2] ]);
+                    var prevV = new vector3([PositionCoords[(((y + q2y) * mNumPoints) + (x + q2x)) * 3], PositionCoords[(((y + q2y) * mNumPoints) + (x + q2x)) * 3 + 1], PositionCoords[(((y + q2y) * mNumPoints) + (x + q2x)) * 3 + 2] ]);
+                    nextV.subtract(thisV);
+                    prevV.subtract(thisV);
+                    nextV.cross(prevV); // cross gives you the normal
+
+                    finalNormal.add(nextV);
+                }
+                finalNormal.normalize();
+                finalNormal.scale(-1.0);
+                finalNormal = finalNormal.getArray();
+                NormalCoords[index * 3] = finalNormal[0];
+                NormalCoords[index * 3 + 1] = finalNormal[1];
+                NormalCoords[index * 3 + 2] = finalNormal[2];
+            }
+        }
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers["Position"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(PositionCoords), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers["Normal"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(NormalCoords), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Normal"], 3, gl.FLOAT, false, 0, 0);
+
+        this.percentfinished = percentfinished;
+        this.draw();
+    },
+
+    draw: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["twist"];
+        var uniforms = program.uniforms;
+        var textures = this.textures;
+        var texture = textures[0].texture;
+        var outgoingTexture = textures[1].texture;
+        var mNumPoints = this.mNumPoints;
+        var specularcolor = this.specularcolor;
+        var AffineTransform = this.AffineTransform.getColumnMajorFloat32Array();
+        var AffineIdentity = this.AffineIdentity.getColumnMajorFloat32Array();
+        var elementIndicesBuffer = this.elementIndicesBuffer;
+
+        if (!specularcolor) {
+            specularcolor = 0;
+        }
+        gl.uniform1f(uniforms["SpecularColor"], specularcolor);
+        if (this.percentfinished < 0.5) {
+            gl.cullFace(gl.BACK);
+            gl.bindTexture(gl.TEXTURE_2D, texture);
+            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
+
+            gl.uniformMatrix3fv(uniforms["TextureMatrix"], false, AffineTransform);
+            gl.uniform1f(uniforms["FlipNormals"], 1.0);
+            // draw
+            for (y = 0; y < mNumPoints - 1; y++) {
+                gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints * 2, gl.UNSIGNED_SHORT, y * mNumPoints * 2 * (2));
+            }
+            // ANIMATE OVERLAY
+            gl.cullFace(gl.FRONT);
+            gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+            gl.uniformMatrix3fv(uniforms["TextureMatrix"], false, AffineIdentity);
+            gl.uniform1f(uniforms["FlipNormals"], -1.0);
+            for (y = 0; y < mNumPoints - 1; y++) {
+                gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints * 2, gl.UNSIGNED_SHORT, y * mNumPoints * 2 * (2));
+            }
+        } else {
+            gl.cullFace(gl.FRONT);
+            gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+            gl.uniformMatrix3fv(uniforms["TextureMatrix"], false, AffineIdentity);
+            gl.uniform1f(uniforms["FlipNormals"], -1.0);
+            for (y = 0; y < mNumPoints - 1; y++) {
+                gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints * 2, gl.UNSIGNED_SHORT, y * mNumPoints * 2 * (2));
+            }
+
+            gl.cullFace(gl.BACK);
+            gl.bindTexture(gl.TEXTURE_2D, texture);
+            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
+
+            gl.uniformMatrix3fv(uniforms["TextureMatrix"], false, AffineTransform);
+            gl.uniform1f(uniforms["SpecularColor"], specularcolor);
+            gl.uniform1f(uniforms["FlipNormals"], 1.0);
+            // draw
+            for (y = 0; y < mNumPoints - 1; y++) {
+                gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints * 2, gl.UNSIGNED_SHORT, y * mNumPoints * 2 * (2));
+            }
+        }
+    }
+});
+
+var KNWebGLTransitionColorPlanes = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNColorPlanes",
+            programNames:["colorPlanes"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var direction = this.effect.attributes.direction;
+        if (direction !== KNDirection.kKNDirectionLeftToRight && direction !== KNDirection.kKNDirectionRightToLeft && direction !== KNDirection.kKNDirectionTopToBottom && direction !== KNDirection.kKNDirectionBottomToTop) {
+            // default direction to left to right if not specified
+            direction = KNDirection.kKNDirectionLeftToRight
+        }
+        this.direction = direction;
+
+        this.mNumColors = 3;
+        this.percentfinished = 0.0;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["colorPlanes"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textureInfo = this.textures[0];
+
+        gl.disable(gl.CULL_FACE);
+        gl.blendFunc(gl.ONE, gl.ONE);
+
+        var buffers = this.buffers = {};
+        buffers["TexCoord"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["TexCoord"]);
+
+        var TexCoords = this.TexCoords = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(TexCoords), gl.STATIC_DRAW);
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0,0);
+        gl.enableVertexAttribArray(attribs["TexCoord"]);
+
+        buffers["Position"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["Position"]);
+
+        var PositionCoords = this.PositionCoords = [
+            0.0, 0.0, 0.0,
+            0.0, textureInfo.height, 0.0,
+            textureInfo.width, 0.0, 0.0,
+            textureInfo.width, textureInfo.height, 0.0
+        ];
+
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(PositionCoords), gl.STATIC_DRAW);
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0,0);
+        gl.enableVertexAttribArray(attribs["Position"]);
+
+        this.MVPMatrix = renderer.slideProjectionMatrix;
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, this.MVPMatrix);
+
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(uniforms["Texture"], 0);
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["colorPlanes"];
+        var uniforms = program.uniforms;
+        var attribs = program.attribs;
+        var textures = this.textures;
+        var textureInfo = textures[0];
+        var outgoingTextureInfo = textures[1];
+
+        this.percentfinished += difference / duration;
+        this.percentfinished > 1 ? this.percentfinished = 1 : 0;
+        var percent = this.percentfinished;
+        var direction = this.direction;
+
+        var planeSeparation = 0.25;
+        var cameraPullBack = 1.0;
+
+        var clockwise = (direction == KNDirection.kKNDirectionRightToLeft || direction == KNDirection.kKNDirectionBottomToTop);
+        var yAxis = (direction == KNDirection.kKNDirectionLeftToRight || direction == KNDirection.kKNDirectionRightToLeft);
+
+        var percentInvSq = 1-(1-percent)*(1-percent);
+
+        var cameraAmount = yAxis ? textureInfo.width : textureInfo.height;
+
+        var uCurve = TSUSineMap(percent * 2.0);
+        var planeOffset = uCurve * cameraAmount * planeSeparation;
+
+        var zOffset = Math.sin(-percentInvSq*2.*Math.PI);
+        zOffset *= percentInvSq * cameraAmount * cameraPullBack;
+
+        if (percent < 0.5) {
+            gl.bindTexture(gl.TEXTURE_2D, outgoingTextureInfo.texture);
+            gl.uniform2fv(uniforms["FlipTexCoords"], new Float32Array([0,0]));
+        } else {
+            gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+            if (direction == KNDirection.kKNDirectionTopToBottom || direction == KNDirection.kKNDirectionBottomToTop) {
+                gl.uniform2fv(uniforms["FlipTexCoords"], new Float32Array([0,1]));
+            } else {
+                gl.uniform2fv(uniforms["FlipTexCoords"], new Float32Array([1,0]));
+            }
+        }
+
+        for (var iHue = 0, mNumColors = this.mNumColors; iHue < mNumColors; iHue++) {
+            var thisHue = iHue/mNumColors;
+
+            // setup color mask
+            var color = WebGraphics.colorWithHSBA(thisHue, 1, 1, 1/mNumColors);
+            gl.uniform4fv(uniforms["ColorMask"], new Float32Array([color.red, color.green, color.blue, color.alpha]));
+
+            var angle = (Math.PI/180.0) * (180.0 * (TSUSineMap(percent)));
+            var mvpMatrix = WebGraphics.translateMatrix4(this.MVPMatrix, textureInfo.width/2, textureInfo.height/2, zOffset);
+            mvpMatrix =  WebGraphics.rotateMatrix4AboutXYZ(mvpMatrix, angle, (clockwise ? -1 : 1) * (yAxis ? 0 : 1), (clockwise ? -1 : 1) * (yAxis ? 1 : 0), 0);
+            mvpMatrix = WebGraphics.translateMatrix4(mvpMatrix, -textureInfo.width/2, -textureInfo.height/2, planeOffset*(iHue-1));
+
+            gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, mvpMatrix);
+            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+        }
+    }
+});
+
+var KNWebGLTransitionFlop = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.BUKFlop",
+            programNames:["flop", "defaultTexture"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var direction = this.effect.attributes.direction;
+        if (direction !== KNDirection.kKNDirectionLeftToRight && direction !== KNDirection.kKNDirectionRightToLeft && direction !== KNDirection.kKNDirectionTopToBottom && direction !== KNDirection.kKNDirectionBottomToTop) {
+            // default direction to left to right if not specified
+            direction = KNDirection.kKNDirectionLeftToRight
+        }
+        this.direction = direction;
+
+        this.percentfinished = 0.0;
+        var elementArray = this.elementArray = [];
+
+        var gl = this.gl;
+        var texWidth = gl.viewportWidth;
+        var texHeight = gl.viewportHeight;
+        var width = texWidth
+        var height = texHeight;
+
+        if (direction === KNDirection.kKNDirectionTopToBottom || direction === KNDirection.kKNDirectionBottomToTop) {
+            height *= 0.5;
+        } else {
+            width *= 0.5;
+        }
+
+        var mNumPoints = this.mNumPoints = 8;
+        var index = 0;
+
+        for (y = 0; y < mNumPoints - 1; y++) {
+            for (x = 0; x < mNumPoints; x++) {
+                elementArray[index++] = (y + 0) * (mNumPoints) + x;
+                elementArray[index++] = (y + 1) * (mNumPoints) + x;
+            }
+        }
+
+        var dx = width / (mNumPoints - 1);
+        var dy = height / (mNumPoints - 1);
+        var yOffset = (direction == KNDirection.kKNDirectionTopToBottom) ? height : yOffset = 0;
+        var xOffset = (direction == KNDirection.kKNDirectionRightToLeft) ? width : xOffset = 0;
+
+        var attributeBufferData = this.attributeBufferData = {
+            Position: [],
+            TexCoords: [],
+            Normal: [],
+            ShadowPosition: [],
+            ShadowTexCoord: [],
+            PreviousPosition: [],
+            PreviousTexCoords: [],
+            PreviousNormal: []
+        };
+
+        for (var y = 0; y < mNumPoints; y++) {
+            for (var x = 0; x < mNumPoints; x++) {
+                index = y * mNumPoints + x;
+                KNWebGLUtil.setPoint3DAtIndexForAttribute(WebGraphics.makePoint3D(x * dx + xOffset, y * dy, 0), index, attributeBufferData["Position"]);
+                KNWebGLUtil.setPoint2DAtIndexForAttribute(WebGraphics.makePoint((x * dx + xOffset) / texWidth, (y * dy + yOffset) / texHeight), index, attributeBufferData["TexCoords"]);
+                KNWebGLUtil.setPoint3DAtIndexForAttribute(WebGraphics.makePoint3D(0, 0, 1), index, attributeBufferData["Normal"]);
+            }
+        }
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["flop"];
+        var attribs = program.attribs;
+        var uniforms = program.uniforms;
+        var basicProgram = this.program["defaultTexture"];
+        var MVPMatrix = this.MVPMatrix = renderer.slideProjectionMatrix;
+        var width = gl.viewportWidth;
+        var height = gl.viewportHeight;
+        var direction = this.direction;
+
+        if (direction === KNDirection.kKNDirectionTopToBottom || direction === KNDirection.kKNDirectionBottomToTop) {
+            height *= 0.5;
+        } else {
+            width *= 0.5;
+        }
+
+        var textureCoordinates = [
+            0.0, 0.0,
+            0.0, 0.5,
+            1.0, 0.0,
+            1.0, 0.5,
+        ];
+
+        var boxPosition = [
+            0.0, 0.0, 0.0,
+            0.0, height, 0.0,
+            width,0.0, 0.0,
+            width, height, 0.0,
+        ];
+
+        var textureCoordinates2 = [
+            0.0, 0.5,
+            0.0, 1.0,
+            1.0, 0.5,
+            1.0, 1.0,
+        ];
+
+        var boxPosition2 = [
+            0.0, height, 0.0,
+            0.0, height*2, 0.0,
+            width, height, 0.0,
+            width, height*2, 0.0,
+        ];
+
+        // use this program and enable vertex attrib array
+        KNWebGLUtil.enableAttribs(gl, program);
+
+        var attributeBufferData = this.attributeBufferData;
+        var buffers = this.buffers = {};
+        var Coordinates = this.Coordinates = {};
+
+        buffers["TexCoord"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["TexCoord"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["TexCoords"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0,0);
+
+        buffers["Position"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["Position"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["Position"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Position"], 3, gl.FLOAT, false, 0,0);
+
+        buffers["Normal"] = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["Normal"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["Normal"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(attribs["Normal"], 3, gl.FLOAT, false, 0,0);
+
+        gl.uniformMatrix4fv(uniforms["MVPMatrix"], false, MVPMatrix);
+
+        var AffineTransform = this.AffineTransform = new Matrix3();
+        if (direction === KNDirection.kKNDirectionTopToBottom) {
+            AffineTransform.affineScale(1.0, -1.0);
+            AffineTransform.affineTranslate(0.0,1.0);
+        } else if (direction == KNDirection.kKNDirectionBottomToTop) {
+            AffineTransform.affineScale(1.0, -1.0);
+            AffineTransform.affineTranslate(0.0,1.0);
+            textureCoordinates = [
+               0.0, 0.5,
+               0.0, 1.0,
+               1.0, 0.5,
+               1.0, 1.0,
+            ];
+
+            textureCoordinates2 = [
+                0.0, 0.0,
+                0.0, 0.5,
+                1.0, 0.0,
+                1.0, 0.5,
+            ];
+
+            boxPosition = [
+                0.0, height, 0.0,
+                0.0, height*2, 0.0,
+                width,height, 0.0,
+                width, height*2, 0.0,
+            ];
+
+            boxPosition2 = [
+                0, 0, 0.0,
+                0, height, 0.0,
+                width, 0, 0.0,
+                width, height, 0.0,
+            ];
+        } else if (direction == KNDirection.kKNDirectionRightToLeft) {
+            AffineTransform.affineScale(-1.0, 1.0);
+            AffineTransform.affineTranslate(1.0, 0.0);
+            textureCoordinates = [
+                0.0, 0.0,
+                0.0, 1.0,
+                0.5, 0.0,
+                0.5, 1.0,
+            ];
+            textureCoordinates2 = [
+                0.5, 0.0,
+                0.5, 1.0,
+                1.0, 0.0,
+                1.0, 1.0,
+            ];
+            boxPosition2 = [
+                width, 0, 0.0,
+                width, height, 0.0,
+                width*2, 0, 0.0,
+                width*2, height, 0.0,
+            ];
+        } else if (direction === KNDirection.kKNDirectionLeftToRight) {
+            AffineTransform.affineScale(-1.0, 1.0);
+            AffineTransform.affineTranslate(1.0, 0.0);
+            boxPosition = [
+                width, 0, 0.0,
+                width, height, 0.0,
+                width*2,0, 0.0,
+                width*2, height, 0.0,
+            ];
+            textureCoordinates = [
+                0.5, 0.0,
+                0.5, 1.0,
+                1.0, 0.0,
+                1.0, 1.0,
+            ];
+            textureCoordinates2 = [
+                0.0, 0.0,
+                0.0, 1.0,
+                0.5, 0.0,
+                0.5, 1.0,
+            ];
+            boxPosition2 = [
+                0, 0, 0.0,
+                0, height, 0.0,
+                width, 0, 0.0,
+                width, height, 0.0,
+            ];
+        }
+
+        this.AffineIdentity = new Matrix3();
+        this.elementIndicesBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementIndicesBuffer);
+        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this.elementArray), gl.STATIC_DRAW);
+
+        //setup second program
+        Coordinates["DefaultTexture"] = textureCoordinates;
+        Coordinates["DefaultTexture2"] = textureCoordinates2;
+        Coordinates["DefaultPosition"] = boxPosition;
+        Coordinates["DefaultPosition2"] = boxPosition2;
+
+        // use this program and enable vertex attrib array
+        KNWebGLUtil.enableAttribs(gl, basicProgram);
+
+        //setup VBO and FTB
+        buffers["TextureCoordinates"] = gl.createBuffer();
+        buffers["PositionCoordinates"] = gl.createBuffer();
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["TextureCoordinates"]);
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["PositionCoordinates"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(basicProgram.attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(boxPosition), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(basicProgram.attribs["Position"], 3, gl.FLOAT, false, 0, 0);
+
+        gl.uniform1i(basicProgram.uniforms["Texture"], 0);
+        gl.uniformMatrix4fv(basicProgram.uniforms["MVPMatrix"], false, MVPMatrix);
+
+        // switch back to main program with animation
+        gl.useProgram(program.shaderProgram);
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(program.uniforms["Texture"], 0);
+
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        this.percentfinished += difference / duration;
+        this.percentfinished > 1 ? this.percentfinished = 1 : 0;
+
+        this.updateFlopWithPercent();
+        this.draw();
+    },
+
+    updateFlopWithPercent: function() {
+        var gl = this.gl;
+        var direction = this.direction;
+        var texWidth = gl.viewportWidth;
+        var texHeight = gl.viewportHeight;
+
+        var thetaA = this.percentfinished * Math.PI;
+        var thetaB = this.percentfinished * this.percentfinished * this.percentfinished * Math.PI;
+
+        var height = texHeight / 2.0;
+        var width = texWidth / 2.0;
+        var location = 0.0;
+        var mNumPoints = this.mNumPoints;
+        var attributeBufferData = this.attributeBufferData;
+
+        for (var y = 0; y < mNumPoints; y++) {
+            for(var x = 0; x < mNumPoints; x++) {
+                var index = y * mNumPoints + x;
+                var start = KNWebGLUtil.getPoint2DForArrayAtIndex(attributeBufferData["TexCoords"], index);
+
+                start.x *= texWidth;
+                start.y *= texHeight;
+
+                if (direction === KNDirection.kKNDirectionBottomToTop) {
+                    location = start.y / height;
+                } else if (direction === KNDirection.kKNDirectionTopToBottom) {
+                    location = (height*2 - start.y) / height;
+                } else if (direction === KNDirection.kKNDirectionLeftToRight) {
+                    location = start.x / width;
+                } else {
+                    location = (width*2 - start.x) / width;
+                }
+
+                var angle = location*thetaA + (1-location) * thetaB;
+                if (direction === KNDirection.kKNDirectionLeftToRight || direction === KNDirection.kKNDirectionTopToBottom) {
+                    angle *= -1;
+                }
+
+                var sinAngle = Math.sin(angle);
+                var cosAngle = Math.cos(angle);
+                var startPosition = KNWebGLUtil.getPoint3DForArrayAtIndex(attributeBufferData["Position"], index);
+                var startNormal = KNWebGLUtil.getPoint3DForArrayAtIndex(attributeBufferData["Normal"], index);
+
+                if (direction === KNDirection.kKNDirectionTopToBottom || direction === KNDirection.kKNDirectionBottomToTop) {
+                    var thisPosition = WebGraphics.makePoint3D(startPosition.x, height - (height - start.y) * cosAngle, (height - start.y) * sinAngle);
+                    KNWebGLUtil.setPoint3DAtIndexForAttribute(thisPosition, index, attributeBufferData["Position"]);
+
+                    var thisNormal = WebGraphics.makePoint3D(startNormal.x, -sinAngle, cosAngle);
+                    KNWebGLUtil.setPoint3DAtIndexForAttribute(thisNormal, index, attributeBufferData["Normal"]);
+                } else {
+                    var thisPosition = WebGraphics.makePoint3D(width - (width - start.x) * cosAngle, startPosition.y, -(width - start.x) * sinAngle);
+                    KNWebGLUtil.setPoint3DAtIndexForAttribute(thisPosition, index, attributeBufferData["Position"]);
+
+                    var thisNormal = WebGraphics.makePoint3D(-sinAngle, startNormal.y, cosAngle);
+                    KNWebGLUtil.setPoint3DAtIndexForAttribute(thisNormal, index, attributeBufferData["Normal"]);
+                }
+            }
+        }
+    },
+
+    draw: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["flop"];
+        var basicProgram = this.program["defaultTexture"];
+        var textures = this.textures;
+        var outgoingTexture = textures[1].texture;
+        var incomingTexture = textures[0].texture;
+
+        gl.useProgram(basicProgram.shaderProgram);
+        gl.disable(gl.CULL_FACE);
+        gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+
+        var mNumPoints = this.mNumPoints;
+        var buffers = this.buffers;
+        var Coordinates = this.Coordinates;
+        var attributeBufferData = this.attributeBufferData;
+
+        KNWebGLUtil.bindDynamicBufferWithData(gl, basicProgram.attribs["Position"], buffers["PositionCoordinates"], Coordinates["DefaultPosition"], 3);
+        KNWebGLUtil.bindDynamicBufferWithData(gl, basicProgram.attribs["TexCoord"], buffers["TextureCoordinates"], Coordinates["DefaultTexture"], 2);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+        gl.useProgram(basicProgram.shaderProgram);
+        gl.disable(gl.CULL_FACE);
+        gl.bindTexture(gl.TEXTURE_2D, incomingTexture);
+
+        KNWebGLUtil.bindDynamicBufferWithData(gl, basicProgram.attribs["Position"], buffers["PositionCoordinates"], Coordinates["DefaultPosition2"], 3);
+        KNWebGLUtil.bindDynamicBufferWithData(gl, basicProgram.attribs["TexCoord"], buffers["TextureCoordinates"], Coordinates["DefaultTexture2"], 2);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+
+        gl.enable(gl.CULL_FACE);
+
+        //ANIMATE OVERLAY
+        gl.useProgram(program.shaderProgram);
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["Position"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["Position"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(program.attribs["Position"], 3, gl.FLOAT, false, 0,0);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["Normal"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["Normal"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(program.attribs["Normal"], 3, gl.FLOAT, false, 0,0);
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, buffers["TexCoord"]);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(attributeBufferData["TexCoords"]), gl.DYNAMIC_DRAW);
+        gl.vertexAttribPointer(program.attribs["TexCoord"], 2, gl.FLOAT, false, 0,0);
+
+        gl.cullFace(gl.BACK);
+        gl.bindTexture(gl.TEXTURE_2D, incomingTexture);
+
+        gl.uniformMatrix3fv(program.uniforms["TextureMatrix"], false, this.AffineTransform.getColumnMajorFloat32Array());
+        gl.uniform1f(program.uniforms["FlipNormals"], -1.0);
+
+        for (var y = 0; y< mNumPoints-1; y++) {
+            gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints*2, gl.UNSIGNED_SHORT, y*mNumPoints*2*(2));
+        }
+
+        gl.bindTexture(gl.TEXTURE_2D, outgoingTexture);
+        gl.cullFace(gl.FRONT);
+
+        gl.uniformMatrix3fv(program.uniforms["TextureMatrix"], false, this.AffineIdentity.getColumnMajorFloat32Array());
+        gl.uniform1f(program.uniforms["FlipNormals"], 1.0);
+        for (var y = 0; y < mNumPoints-1; y++) {
+            gl.drawElements(gl.TRIANGLE_STRIP, mNumPoints*2, gl.UNSIGNED_SHORT, y*mNumPoints*2*(2));
+        }
+    }
+
+});
+
+var KNWebGLBuildAnvil = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "com.apple.iWork.Keynote.BUKAnvil",
+            programNames: ["anvilsmoke", "anvilspeck"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+
+        // bind required textures from base64 image source
+        this.smokeTexture = KNWebGLUtil.bindTextureWithImage(gl, smokeImage);
+        this.speckTexture = KNWebGLUtil.bindTextureWithImage(gl, speckImage);
+
+        // initialize percent finish
+        this.percentfinished = 0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+            this.drawableObjects.push(drawableObject);
+        }
+
+        this.objectY = 1;
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+
+        this.smokeSystems = [];
+        this.speckSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var width = textureInfo.width;
+            var height = textureInfo.height;
+            var viewportWidth = gl.viewportWidth;
+            var viewportHeight = gl.viewportHeight;
+
+            var numParticles = 300;
+
+            var smokeSystem = new KNWebGLBuildAnvilSmokeSystem(
+                renderer,
+                this.program["anvilsmoke"],
+                {"width": width, "height": height},
+                {"width": viewportWidth, "height": viewportHeight},
+                this.duration,
+                {"width": numParticles, "height": 1},
+                {"width": kParticleSize, "height": kParticleSize},
+                this.smokeTexture);
+
+            numParticles = 40;
+            var speckSystem = new KNWebGLBuildAnvilSpeckSystem(
+                renderer,
+                this.program["anvilspeck"],
+                {"width": width, "height": height},
+                {"width": viewportWidth, "height": viewportHeight},
+                this.duration,
+                {"width": numParticles, "height": 1},
+                {"width": kParticleSize, "height": kParticleSize},
+                this.speckTexture);
+
+            this.smokeSystems.push(smokeSystem);
+            this.speckSystems.push(speckSystem);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+
+        this.percentfinished += difference / duration;
+
+        if (this.percentfinished >= 1) {
+            this.percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * this.percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    // if completed, just draw its texture object for better performance
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;;
+                    this.drawableObjects[i].drawFrame();
+                    continue;
+                }
+
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var offsetX = textureInfo.offset.pointX;
+                var offsetY = textureInfo.offset.pointY;
+                var viewportWidth = gl.viewportWidth;
+                var viewportHeight = gl.viewportHeight;
+
+                duration /= 1000;
+
+                var kObjectSmashDuration = Math.min(0.20, duration * 0.4);
+                var kCameraShakeDuration = Math.min(0.25, duration * 0.5);
+
+                var cameraShakePoints = this.cameraShakePointsWithRandomGenerator();
+                var cameraShakePercent = (this.percentfinished * duration - kObjectSmashDuration) / kCameraShakeDuration;
+
+                var shakePoint = WebGraphics.makePoint(0, 0);
+                if (0 < cameraShakePercent && cameraShakePercent < 1) {
+                    var minIndex = Math.floor(cameraShakePercent * kNumCameraShakePoints);
+                    var maxIndex = Math.ceil(WebGraphics.clamp(cameraShakePercent * kNumCameraShakePoints, 0, cameraShakePoints.length - 1));
+                    var minPoint = cameraShakePoints[minIndex];
+                    var maxPoint = cameraShakePoints[maxIndex];
+                    var cameraLerp = cameraShakePercent * kNumCameraShakePoints - minIndex;
+                        shakePoint = WebGraphics.makePoint(
+                        WebGraphics.mix(minPoint.x, maxPoint.x, cameraLerp),
+                        WebGraphics.mix(minPoint.y, maxPoint.y, cameraLerp));
+                }
+
+                var objectSmashPercent = WebGraphics.clamp((this.percentfinished * duration) / kObjectSmashDuration, 0, 1);
+                var smokepercent = WebGraphics.clamp(((this.percentfinished * duration) - kObjectSmashDuration) / (duration - kObjectSmashDuration), 0, 1);
+
+                var percent = this.percentfinished;
+
+                // calculations for the camera shake
+                this.objectY = offsetY + height;
+                this.objectY *= (1.0 - objectSmashPercent * objectSmashPercent);
+
+                // draw the texture
+                this.drawableObjects[i].MVPMatrix = WebGraphics.translateMatrix4(
+                    renderer.slideOrthoMatrix,
+                    offsetX + (shakePoint.x * viewportWidth),
+                    viewportHeight - offsetY - height + this.objectY + (shakePoint.y * viewportHeight),
+                    0);
+
+                this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                this.drawableObjects[i].drawFrame();
+
+                // draw smoke
+                var MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, offsetX, viewportHeight - (offsetY + (height + 16)) * (1 - (smokepercent * smokepercent * 0.02)), 0);
+                var smokeSystem = this.smokeSystems[i];
+                smokeSystem.setMVPMatrix(MVPMatrix);
+                smokeSystem.drawFrame(smokepercent, 1 - (smokepercent * smokepercent));
+
+                // draw specks
+                if (smokepercent < 0.50) {
+                    MVPMatrix = WebGraphics.translateMatrix4(renderer.slideOrthoMatrix, offsetX, viewportHeight - (offsetY + height + 16), 0);
+                    var speckSystem = this.speckSystems[i];
+                    speckSystem.setMVPMatrix(MVPMatrix);
+                    speckSystem.drawFrame(smokepercent, WebGraphics.clamp(1 - WebGraphics.sineMap(smokepercent) * 2, 0, 1));
+                }
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    },
+
+    cameraShakePointsWithRandomGenerator: function() {
+        var cameraShakePoints = [];
+        var globalScale = 0.025;
+
+        for (var i = 0; i < kNumCameraShakePoints; i++) {
+            var scale = 1 - (i / kNumCameraShakePoints);
+            scale *= scale;
+
+            var thisPoint = WebGraphics.makePoint(
+                WebGraphics.randomBetween(-1, 1) * globalScale * scale * 0.4, Math.pow(-1, i) * globalScale * scale);
+
+            cameraShakePoints[i] = thisPoint;
+        }
+        return cameraShakePoints;
+    }
+});
+
+var KNWebGLBuildFlame = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNFlame",
+            programNames: ["flame"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+
+        // bind required textures from base64 image source
+        this.flameTexture = KNWebGLUtil.bindTextureWithImage(gl, flameImage);
+
+        // initialize percent finish
+        this.percentfinished = 0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        // create framebuffer drawable object array for drawing flame
+        this.framebufferDrawableObjects = [];
+
+        this.slideSize = {"width": gl.viewportWidth, "height": gl.viewportHeight};
+
+        var effect = this.effect;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+            this.drawableObjects.push(drawableObject);
+
+            var drawableFrame = {
+                "size": {
+                    "width": texture.width,
+                    "height": texture.height
+                },
+                "origin": {
+                    "x": texture.offset.pointX,
+                    "y": texture.offset.pointY
+                }
+            };
+
+            var frameRect = this.frameOfEffectWithFrame(drawableFrame);
+
+            var framebufferParams = {
+                effect: effect,
+                textures: [],
+                drawableFrame: drawableFrame,
+                frameRect: frameRect
+            };
+
+            var framebufferDrawable = new KNWebGLFramebufferDrawable(renderer, framebufferParams);
+
+            // push the framebufferDrawable to the array
+            this.framebufferDrawableObjects.push(framebufferDrawable);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    frameOfEffectWithFrame: function(drawableFrame) {
+        var objSize = drawableFrame.size;
+        var slideSize = this.slideSize;
+
+        // the larger the object, the less we have to inflate its size
+        var widthAdjust = (1.2 - Math.min(1.0, Math.sqrt(objSize.width / slideSize.width))) + 1.0;
+        var heightAdjust = (1.25 - Math.min(1.0, Math.sqrt(objSize.height / slideSize.height))) + 1.0;
+        var viewSize = {
+            "width": Math.round(objSize.width * widthAdjust),
+            "height": Math.round(objSize.height * heightAdjust)
+        };
+
+        if (objSize.width / objSize.height < 1.0) {
+            // for really skinny objects, make sure GL View is more squarish
+            viewSize.width = Math.max(viewSize.width, (objSize.width + objSize.height));
+        }
+
+        var rect = {
+            "size": viewSize,
+            "origin": {
+                "x": drawableFrame.origin.x + (objSize.width - viewSize.width) / 2,
+                "y": drawableFrame.origin.y + (objSize.height - viewSize.height) / 2
+            }
+        };
+
+        // Now move the FBO up a bit so only 25% of extra space is on the bottom
+        rect.origin.y -= (rect.size.height - drawableFrame.size.height) * 0.25;
+
+        var gl = this.gl;
+        var slideRect = {
+            "origin": {
+                "x": 0,
+                "y": 0
+            },
+            "size": {
+                "width": gl.viewportWidth,
+                "height": gl.viewportHeight
+            }
+        };
+
+        var mFrameRect = CGRectIntersection(rect, slideRect);
+        mFrameRect = CGRectIntegral(mFrameRect);
+
+        return mFrameRect;
+    },
+
+    p_orthoTransformWithScale: function(scale, offset, mFrameRect) {
+        var size = {
+            "width": mFrameRect.size.width * scale,
+            "height": mFrameRect.size.height * scale
+        };
+
+        var ortho = WebGraphics.makeOrthoMatrix4(0, size.width, 0, size.height, -1, 1);
+        var result = WebGraphics.translateMatrix4(ortho, offset.x, -offset.y, 0);
+
+        return result;
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var duration = this.duration / 1000;
+
+        this.flameSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var width = textureInfo.width;
+            var height = textureInfo.height;
+            var viewportWidth = gl.viewportWidth;
+            var viewportHeight = gl.viewportHeight;
+            var framebufferDrawable = this.framebufferDrawableObjects[i];
+            var mFrameRect = framebufferDrawable.frameRect
+            var mDrawableFrame = framebufferDrawable.drawableFrame;
+
+            var orthoOffset = {
+                "x": textureInfo.offset.pointX - mFrameRect.origin.x,
+                "y": textureInfo.offset.pointY + height - (mFrameRect.origin.y + mFrameRect.size.height)
+            };
+
+            var bottomPadding = mDrawableFrame.origin.y - mFrameRect.origin.y;
+            var topPadding = mFrameRect.origin.y + mFrameRect.size.height - (mDrawableFrame.origin.y + mDrawableFrame.size.height);
+            orthoOffset.y += (topPadding - bottomPadding);
+
+            framebufferDrawable.MVPMatrix = this.p_orthoTransformWithScale(1.0, orthoOffset, mFrameRect);
+
+            var ratio = width / height;
+            var numParticles = Math.round(ratio * 150);
+            numParticles *= (duration + Math.max(0, 1.0 - duration / 2));
+
+            // We updated actualSize, so need to update the max speed in the shader
+            var flameSystem = new KNWebGLBuildFlameSystem(
+                renderer,
+                this.program["flame"],
+                {"width": width, "height": height},
+                {"width": viewportWidth, "height": viewportHeight},
+                Math.max(2, this.duration),
+                numParticles,
+                this.flameTexture
+            );
+
+            flameSystem.p_setupParticleDataWithTexture(textureInfo);
+
+            this.flameSystems.push(flameSystem);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["flame"];
+        var uniforms = program.uniforms;
+        var buildOut = this.buildOut;
+        var percentfinished = this.percentfinished;
+
+        percentfinished += difference / duration;
+
+        if (percentfinished >= 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * this.percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (!buildOut) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;;
+                        this.drawableObjects[i].drawFrame();
+                    }
+                    continue;
+                }
+
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var offsetX = textureInfo.offset.pointX;
+                var offsetY = textureInfo.offset.pointY;
+                var viewportWidth = gl.viewportWidth;
+                var viewportHeight = gl.viewportHeight;
+
+                duration /= 1000;
+
+                var percent = percentfinished;
+
+                if (buildOut) {
+                    percent = 1.0 - percent;
+                }
+
+                var minCutoff = buildOut ? 0.25 : 0.5;
+                var cutoff = Math.min(minCutoff, 1.0 / duration);
+
+                if (percent > cutoff) {
+                    var newPercent = (percent - cutoff) / (1 - cutoff);
+                    var alpha = TSUSineMap(Math.min(1.0, 2 * newPercent));
+                    alpha *= this.parentOpacity * textureInfo.initialState.opacity;
+
+                    var drawable = this.drawableObjects[i];
+                    drawable.Opacity = alpha;
+                    drawable.drawFrame();
+                }
+
+                var framebufferDrawable = this.framebufferDrawableObjects[i];
+                var mDrawableFrame = framebufferDrawable.drawableFrame;
+                var mFrameRect = framebufferDrawable.frameRect;
+
+                var orthoOffset = {
+                    "x": textureInfo.offset.pointX - mFrameRect.origin.x,
+                    "y": textureInfo.offset.pointY + height - (mFrameRect.origin.y + mFrameRect.size.height)
+                };
+
+                var bottomPadding = mDrawableFrame.origin.y - mFrameRect.origin.y;
+                var topPadding = mFrameRect.origin.y + mFrameRect.size.height - (mDrawableFrame.origin.y + mDrawableFrame.size.height);
+                orthoOffset.y += (topPadding - bottomPadding);
+
+                // this is slightly different implementation because we do not scale up and down in web
+                var MVPMatrix = this.p_orthoTransformWithScale(1, orthoOffset, mFrameRect);
+
+                // change viewport to match the frame buffer size
+                gl.viewport(0, 0, mFrameRect.size.width, mFrameRect.size.height);
+
+                //bind framebuffer
+                gl.bindFramebuffer(gl.FRAMEBUFFER, framebufferDrawable.buffer);
+
+                //now render the scene
+                gl.clear(gl.COLOR_BUFFER_BIT);
+                gl.enable(gl.BLEND);
+                gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
+
+                var flameOpacity = (percentfinished == 0.0 || percentfinished == 1.0 ? 0.0 : 1.0);
+
+                // bind framebuffer texture
+                gl.bindTexture(gl.TEXTURE_2D, framebufferDrawable.texture);
+
+                var flameSystem = this.flameSystems[i];
+                flameSystem.setMVPMatrix(MVPMatrix);
+                gl.uniform1f(uniforms["SpeedMax"], flameSystem._speedMax);
+                flameSystem.drawFrame(percentfinished, flameOpacity);
+
+                // unbind the framebuffer
+                gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+
+                // unbind the texture
+                gl.bindTexture(gl.TEXTURE_2D, null);
+
+                // change viewport back to original size
+                gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+
+                // send result to framebuffer
+                gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                framebufferDrawable.MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, mFrameRect.origin.x, gl.viewportHeight - (mFrameRect.origin.y + mFrameRect.size.height), 0);
+                framebufferDrawable.drawFrame();
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLTransitionConfetti = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNConfetti",
+            programNames: ["confetti", "defaultTexture"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        this.useGravity = this.direction === KNDirection.kKNDirectionGravity ? true : false;
+        this.percentfinished = 0.0;
+
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function(){
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var textures = this.textures;
+        var textureInfo = textures[0];
+        var width = textureInfo.width;
+        var height = textureInfo.height;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        var numParticles = 10000;
+
+        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
+        // create a confetti system
+        this.confettiSystem = new KNWebGLBuildConfettiSystem(
+            renderer,
+            this.program["confetti"],
+            {"width": width, "height": height},
+            {"width": viewportWidth, "height": viewportHeight},
+            this.duration,
+            numParticles,
+            textures[1].texture);
+
+        this.confettiSystem.setMVPMatrix(renderer.slideProjectionMatrix);
+
+        // use default texture shader program for incoming slide
+        var program = this.program["defaultTexture"];
+
+        // enable attribs before binding and set the program to use.
+        KNWebGLUtil.enableAttribs(gl, program);
+
+        var textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        var boxPosition = [
+            0.0, 0.0, -1.0,
+            0.0, viewportHeight, -1.0,
+            viewportWidth, 0.0, -1.0,
+            viewportWidth, viewportHeight, -1.0,
+        ];
+
+        // setup VBO and FTB
+        this.textureCoordinatesBuffer = gl.createBuffer();
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinatesBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+        gl.vertexAttribPointer(program.attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+
+        this.positionBuffer = gl.createBuffer();
+        KNWebGLUtil.bindDynamicBufferWithData(gl, program.attribs["Position"], this.positionBuffer, boxPosition, 3);
+
+        gl.uniformMatrix4fv(program.uniforms["MVPMatrix"], false, renderer.slideOrthoMatrix);
+
+        gl.activeTexture(gl.TEXTURE0);
+        gl.uniform1i(program.uniforms["Texture"], 0);
+
+        this.drawFrame(0, 0, 4);
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        var percentfinished = this.percentfinished;
+        percentfinished += difference / duration;
+
+        if (percentfinished > 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        var percent = this.percentfinished = percentfinished;
+        var revPercent = 1 - percent;
+        var myPercent = 1 - revPercent*revPercent*revPercent;
+        myPercent = myPercent*(1-percent*percent) + (1-revPercent*revPercent)*(percent*percent) + percent;
+
+        myPercent *= 0.5;
+        myPercent*= myPercent;
+
+        var scale= 0.75 + (1 - Math.pow(revPercent,4)) * 0.25;
+
+        var quadShaderMVPMatrix = WebGraphics.translateMatrix4(this.renderer.slideProjectionMatrix, viewportWidth / 2, viewportHeight / 2, 0);
+        quadShaderMVPMatrix = WebGraphics.scaleMatrix4(quadShaderMVPMatrix, scale, scale, 1);
+        quadShaderMVPMatrix = WebGraphics.translateMatrix4(quadShaderMVPMatrix, -viewportWidth / 2, -viewportHeight / 2, 0);
+
+        // draw the incoming slide
+        var program = this.program["defaultTexture"];
+        gl.useProgram(program.shaderProgram);
+        gl.uniformMatrix4fv(program.uniforms["MVPMatrix"], false, quadShaderMVPMatrix);
+        this.draw();
+
+        //draw the confetti system frame
+        var finalPercent = 1 - percent;
+        finalPercent = WebGraphics.clamp(finalPercent, 0, 1);
+        myPercent = WebGraphics.clamp(myPercent, 0, 1);
+
+        if (this.useGravity) {
+            var ratio = 1;
+            var MVPMatrix = this.renderer.slideProjectionMatrix;
+
+            MVPMatrix = WebGraphics.translateMatrix4(MVPMatrix, 0, -viewportHeight * 2 * percent * percent * (1.0 - ratio * 0.5), 0);
+            this.confettiSystem.setMVPMatrix(MVPMatrix);
+        }
+
+        this.confettiSystem.drawFrame(myPercent, finalPercent);
+    },
+
+    draw: function() {
+        var gl = this.gl;
+        var program = this.program["defaultTexture"];
+        var attribs = program.attribs;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        gl.useProgram(program.shaderProgram);
+
+        var textureCoordinates = [
+            0.0, 0.0,
+            0.0, 1.0,
+            1.0, 0.0,
+            1.0, 1.0,
+        ];
+
+        var boxPosition = [
+            0.0, 0.0, -1.0,
+            0.0, viewportHeight, -1.0,
+            viewportWidth, 0.0, -1.0,
+            viewportWidth, viewportHeight, -1.0,
+        ];
+
+        gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordinatesBuffer);
+        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
+        gl.vertexAttribPointer(attribs["TexCoord"], 2, gl.FLOAT, false, 0, 0);
+
+        KNWebGLUtil.bindDynamicBufferWithData(gl, attribs["Position"], this.positionBuffer, boxPosition, 3);
+
+        // bind incoming texture
+        gl.bindTexture(gl.TEXTURE_2D, this.textures[0].texture);
+        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
+    }
+});
+
+var KNWebGLBuildConfetti = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNConfetti",
+            programNames: ["confetti"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        this.useGravity = this.direction === KNDirection.kKNDirectionGravity ? true : false;
+        this.percentfinished = 0.0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        this.confettiSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var width = textureInfo.width;
+            var height = textureInfo.height;
+            var ratio = (height / viewportHeight * width / viewportWidth);
+            ratio = Math.sqrt(Math.sqrt(ratio));
+
+            var numParticles = Math.round(ratio * 10000);
+
+            // create a confetti system
+            var confettiSystem = new KNWebGLBuildConfettiSystem(
+                renderer,
+                this.program["confetti"],
+                {"width": width, "height": height},
+                {"width": viewportWidth, "height": viewportHeight},
+                this.duration,
+                numParticles,
+                textureInfo.texture);
+
+            // set ratio so we don't need to recalculate during draw frame
+            confettiSystem.ratio = ratio;
+
+            this.confettiSystems.push(confettiSystem);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        // determine the type and direction
+        var buildIn = this.buildIn;
+        var buildOut = this.buildOut;
+
+        var percentfinished = this.percentfinished;
+        percentfinished += difference / duration;
+
+        if (percentfinished > 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (buildIn) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;;
+                        this.drawableObjects[i].drawFrame();
+                    }
+                    continue;
+                }
+
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var percent = buildIn ? 1 - percentfinished : percentfinished;
+
+                var revPercent = 1 - percent;
+                var myPercent = 1 - revPercent * revPercent * revPercent;
+                myPercent = myPercent * (1 - percent * percent) + (1 - revPercent * revPercent) * (percent * percent) + percent;
+                myPercent *= 0.5;
+
+                if (buildIn) {
+                   myPercent *= myPercent;
+                }
+
+                //draw the confetti system frame
+                var confettiSystem = this.confettiSystems[i];
+                var MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, textureInfo.offset.pointX,  viewportHeight - (textureInfo.offset.pointY + height), 0);
+
+                var finalPercent = 1 - percent;
+                finalPercent = WebGraphics.clamp(finalPercent, 0, 1);
+                myPercent = WebGraphics.clamp(myPercent, 0, 1);
+
+                if (this.useGravity) {
+                    var ratio = confettiSystem.ratio;
+                    MVPMatrix = WebGraphics.translateMatrix4(MVPMatrix, 0, -viewportHeight * 2 * percent * percent * (1.0 - ratio * 0.5), 0);
+                }
+
+                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
+                confettiSystem.setMVPMatrix(MVPMatrix);
+                confettiSystem.drawFrame(myPercent, finalPercent);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLBuildDiffuse = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNDiffuse",
+            programNames: ["diffuse"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        this.percentfinished = 0.0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        this.diffuseSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var width = textureInfo.width;
+            var height = textureInfo.height;
+            var ratio = (height / viewportHeight * width / viewportWidth);
+            ratio = Math.sqrt(Math.sqrt(ratio));
+
+            var numParticles = Math.round(ratio * 4000);
+
+            // create a confetti system
+            var diffuseSystem = new KNWebGLBuildDiffuseSystem(
+                renderer,
+                this.program["diffuse"],
+                {"width": width, "height": height},
+                {"width": viewportWidth, "height": viewportHeight},
+                this.duration,
+                numParticles,
+                textureInfo.texture,
+                this.direction === KNDirection.kKNDirectionRightToLeft);
+
+            this.diffuseSystems.push(diffuseSystem);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        var percentfinished = this.percentfinished;
+        percentfinished += difference / duration;
+
+        if (percentfinished > 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var offsetX = textureInfo.offset.pointX;
+                var offsetY = textureInfo.offset.pointY;
+
+                //draw the diffuse system frame
+                var diffuseSystem = this.diffuseSystems[i];
+                var MVPMatrix = WebGraphics.translateMatrix4(renderer.slideProjectionMatrix, offsetX,  viewportHeight - (offsetY + height), 0);
+
+                diffuseSystem.setMVPMatrix(MVPMatrix);
+                diffuseSystem.drawFrame(this.percentfinished, 1.0);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLBuildFireworks = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KNFireworks",
+            programNames: ["fireworks"],
+            effect: params.effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+
+        // animation parameter group
+        this.animParameterGroup = new KNAnimParameterGroup("Fireworks");
+
+        // bind required textures from base64 image source
+        this.fireworksTexture = KNWebGLUtil.bindTextureWithImage(gl, fireworksImage);
+        this.fireworksCenterBurstTexture = KNWebGLUtil.bindTextureWithImage(gl, fireworksCenterBurstImage);
+
+        // initialize percent finish
+        this.percentfinished = 0;
+        this.prevpercentfinished = 0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        // frame rect for all firework systems
+        this.frameRect = this.frameOfEffectWithFrame();
+
+        this.slideSize = {"width": gl.viewportWidth, "height": gl.viewportHeight};
+
+        var effect = this.effect;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+
+            // push drawable object to drawableObjects array
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    frameOfEffectWithFrame: function() {
+        var gl = this.gl;
+        var slideRect = {
+            "origin": {
+                "x": 0,
+                "y": 0
+            },
+            "size": {
+                "width": gl.viewportWidth,
+                "height": gl.viewportHeight
+            }
+        };
+
+        return slideRect;
+    },
+
+    p_orthoTransformWithScale: function(scale, offset, mFrameRect) {
+        var size = {
+            "width": mFrameRect.size.width * scale,
+            "height": mFrameRect.size.height * scale
+        };
+
+        var ortho = WebGraphics.makeOrthoMatrix4(0, size.width, 0, size.height, -1, 1);
+        var result = WebGraphics.translateMatrix4(ortho, offset.x, -offset.y, 0);
+
+        return result;
+    },
+
+    p_setupFBOWithSize: function(size) {
+        this.framebuffer = new TSDGLFrameBuffer(this.gl, size, 2);
+    },
+
+    p_fireworksSystemsForTR: function(textureInfo) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+        var duration = this.duration / 1000;
+        var parameterGroup = this.animParameterGroup;
+
+        var numFireworks = duration * parameterGroup.doubleForKey("FireworksCount");
+        // At least 2 fireworks!
+        numFireworks = Math.max(2, numFireworks);
+
+        var systems = [];
+
+        var startOnLeftIndex = 0;
+        var startOnRightIndex = 1;
+        var startImmediatelyIndex = parseInt(WebGraphics.randomBetween(0, numFireworks - 1));
+
+        for (var i = 0; i < numFireworks; i++) {
+            var numParticles = parameterGroup.doubleForKey("ParticleCount");
+            var minSlideSide = Math.min(viewportWidth, viewportHeight);
+            var fireworkSpan = minSlideSide * WebGraphics.doubleBetween(parameterGroup.doubleForKey("FireworkSizeMin"), parameterGroup.doubleForKey("FireworkSizeMax"));
+
+            var particleSystem = new KNWebGLBuildFireworksSystem(
+                renderer,
+                this.program["fireworks"],
+                {"width": textureInfo.width, "height": textureInfo.height},
+                {"width": viewportWidth, "height": viewportHeight},
+                this.duration,
+                {"width": numParticles, "height": 1},
+                {"width": 1, "height": 1},
+                this.fireworksTexture
+            );
+
+            var randomSize = WebGraphics.makeSize(parameterGroup.doubleForKey("ParticleSizeMin"), parameterGroup.doubleForKey("ParticleSizeMax"));
+            randomSize.width = randomSize.width * minSlideSide / 100;
+            randomSize.height = randomSize.height * minSlideSide / 100;
+
+            particleSystem.randomParticleSizeMinMax = randomSize;
+            particleSystem.maxDistance = fireworkSpan;
+            particleSystem.colorRandomness = parameterGroup.doubleForKey("ParticleColorRandomness");
+            particleSystem.lifeSpanMinDuration = parameterGroup.doubleForKey("ParticleLifeSpanMinDuration");
+            particleSystem.randomParticleSpeedMinMax = WebGraphics.makePoint(parameterGroup.doubleForKey("FireworkSpeedMin"), parameterGroup.doubleForKey("FireworkSpeedMax"));
+
+            if (i % 2 === 0) {
+                // 1/2 of particles start in left half
+                particleSystem.fireworkStartingPositionX = WebGraphics.randomBetween(0, 0.5);
+            } else if (i % 2 === 1) {
+                // 1/2 of particles start in right half
+                particleSystem.fireworkStartingPositionX = WebGraphics.randomBetween(0.5, 1);
+            }
+
+            if (i === startOnLeftIndex) {
+                // Make sure at least one burst is all the way on the left side
+                particleSystem.fireworkStartingPositionX = 0;
+            }
+
+            if (i === startOnRightIndex) {
+                // Make sure at least one burst is all the way on the right side
+                particleSystem.fireworkStartingPositionX = 1;
+            }
+
+            // Lifespan/duration of firework
+            var randomDuration = WebGraphics.randomBetween(parameterGroup.doubleForKey("FireworkDurationMin"), parameterGroup.doubleForKey("FireworkDurationMax"));
+
+            randomDuration /= duration;
+
+            var startTime = WebGraphics.randomBetween(0, 1.0 - randomDuration);
+
+            if (i === startImmediatelyIndex) {
+                // Make sure ONE of the fireworks starts right away!
+                startTime = 0;
+            }
+
+            startTime = Math.max(startTime, 0.001);
+
+            particleSystem.lifeSpan = {
+                "start": startTime,
+                "duration": randomDuration
+            };
+
+            particleSystem.setupWithTexture(textureInfo);
+
+            systems.push(particleSystem);
+        }
+
+        return systems;
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var parameterGroup = this.animParameterGroup;
+
+        var centerBurstVertexRect = CGRectMake(0, 0, 512, 512);
+        var vertexRect = CGRectMake(0, 0, this.slideSize.width, this.slideSize.height);
+        var textureRect = CGRectMake(0, 0, 1, 1);
+        var meshSize = CGSizeMake(2, 2);
+        var mFrameRect = this.frameRect;
+
+        this.fireworksSystems = [];
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = this.textures[i];
+
+            var orthoOffset = {
+                "x": texture.offset.pointX - mFrameRect.origin.x,
+                "y": texture.offset.pointY + texture.height - (mFrameRect.origin.y + mFrameRect.size.height)
+            };
+
+            var baseOrthoTransform = WebGraphics.makeOrthoMatrix4(0, mFrameRect.size.width, 0, mFrameRect.size.height, -1, 1);
+            var baseTransform = WebGraphics.translateMatrix4(baseOrthoTransform, orthoOffset.x, -orthoOffset.y, 0);
+
+            // init object shader and data buffer
+            var objectShader = new TSDGLShader(gl);
+            objectShader.initWithDefaultTextureAndOpacityShader();
+
+            // object shader set methods
+            objectShader.setMat4WithTransform3D(baseTransform, kTSDGLShaderUniformMVPMatrix);
+            objectShader.setGLint(0, kTSDGLShaderUniformTexture);
+
+            // init object data buffer
+            var objectTextureRect = texture.textureRect;
+            var objectVertexRect = CGRectMake(0, 0, objectTextureRect.size.width, objectTextureRect.size.height);
+            var objectDataBuffer = new TSDGLDataBuffer(gl);
+
+            objectDataBuffer.initWithVertexRect(objectVertexRect, TSDRectUnit, meshSize, false, false);
+
+            // Set up shaders for particle systems
+            var fireworksMVP = renderer.slideProjectionMatrix;
+            fireworksMVP = WebGraphics.translateMatrix4(fireworksMVP, orthoOffset.x, -orthoOffset.y, 0);
+
+            var fireworksSystems = this.p_fireworksSystemsForTR(texture);
+
+            // set up FBO
+            this.p_setupFBOWithSize(mFrameRect.size);
+
+            var fboShader = this.fboShader = new TSDGLShader(gl);
+            fboShader.initWithShaderFileNames("fireworkstrails", "fireworkstrails");
+
+            fboShader.setMat4WithTransform3D(baseOrthoTransform, kTSDGLShaderUniformMVPMatrix);
+            fboShader.setGLint(0, kTSDGLShaderUniformTexture);
+
+            var fboDataBuffer = this.fboDataBuffer = new TSDGLDataBuffer(gl);
+            fboDataBuffer.initWithVertexRect(CGRectMake(0, 0, mFrameRect.size.width, mFrameRect.size.height), TSDRectUnit, meshSize, false, false);
+
+            var centerBurstShader = this.centerBurstShader = new TSDGLShader(gl);
+            centerBurstShader.initWithDefaultTextureAndOpacityShader();
+
+            centerBurstShader.setGLFloat(1.0, kTSDGLShaderUniformOpacity);
+
+            var centerBurstDataBuffer = this.centerBurstDataBuffer = new TSDGLDataBuffer(gl);
+            centerBurstDataBuffer.initWithVertexRect(centerBurstVertexRect, TSDRectUnit, meshSize, false, false);
+
+            var _bloomEffect = this._bloomEffect = new TSDGLBloomEffect(gl);
+            _bloomEffect.initWithEffectSize(mFrameRect.size, parameterGroup.doubleForKey("BloomBlurScale"));
+
+            var fireworksSystem = {
+                "_baseOrthoTransform": baseOrthoTransform,
+                "_baseTransform": baseTransform,
+                "objectShader": objectShader,
+                "objectDataBuffer": objectDataBuffer,
+                "fireworksMVP": fireworksMVP,
+                "systems": fireworksSystems
+            };
+
+            this.fireworksSystems.push(fireworksSystem);
+
+            gl.clearColor(0.0, 0.0, 0.0, 0.0);
+
+            gl.enable(gl.BLEND);
+            gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+            gl.disable(gl.DEPTH_TEST);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["fireworks"];
+        var uniforms = program.uniforms;
+        var buildOut = this.buildOut;
+        var percentfinished = this.percentfinished;
+        var parameterGroup = this.animParameterGroup;
+        var noiseAmount = parameterGroup.doubleForKey("ParticleTrailsDitherAmount");
+        var noiseMax = parameterGroup.doubleForKey("ParticleTrailsDitherMax");
+        var bloomAmount = parameterGroup.doubleForKey("BloomPower");
+
+        percentfinished += difference / duration;
+
+        if (percentfinished >= 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * this.percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (!buildOut) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                        this.drawableObjects[i].drawFrame();
+                    }
+                    continue;
+                }
+
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var offsetX = textureInfo.offset.pointX;
+                var offsetY = textureInfo.offset.pointY;
+                var viewportWidth = gl.viewportWidth;
+                var viewportHeight = gl.viewportHeight;
+
+                duration /= 1000;
+
+                var percent = percentfinished;
+
+                var currentGLFramebuffer = TSDGLFrameBuffer.currentGLFramebuffer(gl);
+
+                var fireworksSystem = this.fireworksSystems[i];
+                var objectShader = fireworksSystem.objectShader;
+                var objectDataBuffer = fireworksSystem.objectDataBuffer;
+
+                // Draw the actual object
+                this.p_drawObject(percent, textureInfo, objectShader, objectDataBuffer);
+
+                // Draw particles into FBO to save trails
+                var framebuffer = this.framebuffer;
+                var fboShader = this.fboShader;
+                var fboDataBuffer = this.fboDataBuffer;
+
+                var previousFBOTexture = framebuffer.currentGLTexture();
+                framebuffer.setCurrentTextureToNext();
+                framebuffer.bindFramebuffer();
+
+                // clear current framebuffer texture
+                gl.clear(gl.COLOR_BUFFER_BIT);
+
+                // change viewport to match the frame buffer size
+                gl.viewport(0, 0, framebuffer.size.width, framebuffer.size.height);
+
+                // First, draw existing trails, but faded out a bit
+                // bind previous framebuffer texture so we can take the content and draw into current one
+                gl.bindTexture(gl.TEXTURE_2D, previousFBOTexture);
+
+                var minDuration = parameterGroup.doubleForKey("FireworkDurationMin") / duration;
+                minDuration = Math.min(minDuration / 2.0, 1.0);
+
+                var trailsFadePercent = WebGraphics.clamp((percentfinished - minDuration) / (1.0 - minDuration), 0, 1);
+                var trailsFadeOut = 1.0 - WebGraphics.mix(parameterGroup.doubleForKey("TrailsFadeOutMin"), parameterGroup.doubleForKey("TrailsFadeOutMax"), Math.pow(trailsFadePercent, 2));
+
+                fboShader.setGLFloat(trailsFadeOut, kTSDGLShaderUniformOpacity);
+                fboShader.setGLFloat(noiseAmount, kShaderUniformNoiseAmount);
+                fboShader.setGLFloat(noiseMax, kShaderUniformNoiseMax);
+
+                var noiseSeed = WebGraphics.makePoint(WebGraphics.randomBetween(0, 1), WebGraphics.randomBetween(0, 1));
+                fboShader.setPoint2D(noiseSeed, kShaderUniformNoiseSeed);
+
+                fboDataBuffer.drawWithShader(this.fboShader, true);
+
+                // Draw center burst
+
+                gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                // need to use fireworks program before drawing particle system
+                gl.useProgram(program.shaderProgram);
+
+                var gravity = parameterGroup.doubleForKey("Gravity");
+                gravity *= Math.min(viewportWidth, viewportHeight) * 0.001;
+                gravity *= duration; // acceleration is per second!
+                gl.uniform1f(uniforms["Gravity"], gravity);
+
+                var minSlideSide = Math.min(viewportWidth, viewportHeight);
+                var startScale = minSlideSide * parameterGroup.doubleForKey("ParticleSizeStart") / 100;
+                gl.uniform1f(uniforms["StartScale"], startScale);
+
+                gl.uniform1f(uniforms["SparklePeriod"], parameterGroup.doubleForKey("SparklePeriod"));
+
+                // draw particle system with percent
+                this.drawParticleSystemsWithPercent(percentfinished, false, 1.0, fireworksSystem);
+
+                // change viewport back to original
+                gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+
+                // done drawing particle into FBO so unbind the framebuffer
+                framebuffer.unbindFramebufferAndBindGLFramebuffer(currentGLFramebuffer);
+
+                // Draw particle trails
+
+                var maxDuration = parameterGroup.doubleForKey("FireworkDurationMax");
+                maxDuration = Math.min(maxDuration, 0.999);
+                var particleOpacityPercent = WebGraphics.clamp((percentfinished - maxDuration) / (1.0 - maxDuration), 0, 1);
+                var particleSystemOpacity = 1.0 - parameterGroup.doubleForAnimationCurve("ParticleTransparency", particleOpacityPercent);
+
+                // apply bloom effect
+                this._bloomEffect.bindFramebuffer();
+
+                gl.clear(gl.COLOR_BUFFER_BIT);
+
+                // draw to bloom effect's _colorFramebuffer
+                fboShader.setGLFloat(particleSystemOpacity, kTSDGLShaderUniformOpacity);
+                fboShader.setGLFloat(0, kShaderUniformNoiseAmount);
+
+                gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
+                // bind to current framebuffer texture
+                gl.bindTexture(gl.TEXTURE_2D, framebuffer.currentGLTexture());
+
+                // draw trails FBO to bloom effect FBO
+                fboDataBuffer.drawWithShader(fboShader, true);
+
+                // draw new sparkles into bloom effect FBO
+                gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                // need to use the program before drawing fireworks particle system
+                gl.useProgram(program.shaderProgram);
+
+                this.drawParticleSystemsWithPercent(percentfinished, true, particleSystemOpacity, fireworksSystem);
+
+                // unbind bloom effect framebuffer and bind to default drawing buffer
+                this._bloomEffect.unbindFramebufferAndBindGLFramebuffer(currentGLFramebuffer);
+
+                // additive blend mode
+                gl.blendFunc(gl.ONE, gl.ONE);
+
+                this._bloomEffect.drawBloomEffectWithMVPMatrix(fireworksSystem._baseOrthoTransform, bloomAmount, currentGLFramebuffer);
+
+                gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+
+        this.prevpercentfinished = this.percentfinished;
+    },
+
+    p_drawObject: function(percent, textureInfo, objectShader, objectDataBuffer) {
+        var gl = this.gl;
+        var parameterGroup = this.animParameterGroup;
+
+        var beginTime = parameterGroup.doubleForKey("TextOpacityBeginTime");
+        var endTime = parameterGroup.doubleForKey("TextOpacityEndTime");
+
+        percent = WebGraphics.clamp((percent - beginTime) / (endTime - beginTime), 0, 1);
+
+        var opacity = this.parentOpacity * textureInfo.initialState.opacity;
+        opacity *= parameterGroup.doubleForAnimationCurve("TextOpacityTiming", percent);
+
+        objectShader.setGLFloat(opacity, kTSDGLShaderUniformOpacity);
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+
+        objectDataBuffer.drawWithShader(objectShader, true);
+    },
+
+    drawParticleSystemsWithPercent: function(percent, shouldDrawSparkles, particleSystemOpacity, fireworksSystem) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var program = this.program["fireworks"];
+        var uniforms = program.uniforms;
+        var parameterGroup = this.animParameterGroup;
+        var systems = fireworksSystem.systems;
+        var baseTransform = fireworksSystem._baseTransform;
+        var MVPMatrix = fireworksSystem.fireworksMVP;
+
+        // need to use fireworks program before drawing particle system
+        gl.useProgram(program.shaderProgram);
+
+        gl.uniform1f(uniforms["ShouldSparkle"], shouldDrawSparkles ? 1 : 0);
+
+        for (var i = 0, length = systems.length; i < length; i++) {
+            var particleSystem = systems[i];
+            var lifeSpan = particleSystem.lifeSpan;
+            var systemPercent = (percent - lifeSpan.start) / lifeSpan.duration;
+
+            if (systemPercent <= 0 || systemPercent >= 1) {
+                continue;
+            }
+
+            var systemPercent = WebGraphics.clamp(systemPercent, 0, 1);
+            var prevSystemPercent = (this.prevpercentfinished - lifeSpan.start) / lifeSpan.duration;
+            prevSystemPercent = WebGraphics.clamp(prevSystemPercent, systemPercent / 2, 1);
+
+            var opacity = particleSystemOpacity;
+            if (shouldDrawSparkles) {
+                opacity = 1.0 - parameterGroup.doubleForAnimationCurve("ParticleTransparency", systemPercent);
+            }
+
+            // Also send in previous particle burst timing so we can blur in direction of burst velocity and avoid strobing
+            var prevParticleBurstTiming = parameterGroup.doubleForAnimationCurve("ParticleBurstTiming", prevSystemPercent);
+            var particleBurstTiming = parameterGroup.doubleForAnimationCurve("ParticleBurstTiming", systemPercent);
+
+            gl.uniform1f(uniforms["ParticleBurstTiming"], particleBurstTiming);
+
+            gl.uniform1f(uniforms["PreviousParticleBurstTiming"], prevParticleBurstTiming);
+
+            gl.uniform1f(uniforms["PreviousPercent"], prevSystemPercent);
+
+            if (!shouldDrawSparkles) {
+                // Draw big center burst once at very first frame of Firework... the FBO fading will handle persisting it for a bit
+
+                if (!particleSystem.didDrawCenterBurst) {
+                    gl.bindTexture(gl.TEXTURE_2D, this.fireworksCenterBurstTexture);
+
+                    // Scale is percent of slide size
+                    var scale = gl.viewportHeight / 512;
+                    scale *= WebGraphics.randomBetween(parameterGroup.doubleForKey("CenterBurstScaleMin"), parameterGroup.doubleForKey("CenterBurstScaleMax"));
+
+                    var center = particleSystem._startingPoint;
+
+                    var t = WebGraphics.translateMatrix4(baseTransform, center.x, center.y, 0);
+                    var centerAdjust = WebGraphics.makePoint(-(512 / 2.0 * scale), -(512 / 2.0 * scale));
+
+                    t = WebGraphics.translateMatrix4(t, centerAdjust.x, centerAdjust.y, 0);
+                    t = WebGraphics.scaleMatrix4(t, scale, scale, 1);
+
+                    this.centerBurstShader.setGLFloat(parameterGroup.doubleForKey("CenterBurstOpacity"), kTSDGLShaderUniformOpacity);
+                    this.centerBurstShader.setMat4WithTransform3D(t, kTSDGLShaderUniformMVPMatrix);
+
+                    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+
+                    this.centerBurstDataBuffer.drawWithShader(this.centerBurstShader, true);
+
+                    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                    particleSystem.didDrawCenterBurst = true;
+                }
+            }
+
+            // need to use fireworks program before drawing particle system
+            gl.useProgram(program.shaderProgram);
+
+            particleSystem.setMVPMatrix(MVPMatrix);
+
+            particleSystem.drawFrame(systemPercent, opacity);
+        }
+    }
+});
+
+var KNWebGLBuildShimmer = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNShimmer",
+            programNames: ["shimmerObject", "shimmerParticle"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+
+        this.percentfinished = 0.0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        this.slideOrigin = {"x": 0, "y": 0};
+        this.slideSize = {"width": gl.viewportWidth, "height": gl.viewportHeight};
+        this.slideRect = {
+            "origin": this.slideOrigin,
+            "size": this.slideSize
+        };
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableFrame = texture.textureRect;
+
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var frameRect = this.frameOfEffectWithFrame(drawableFrame);
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+
+            drawableObject.frameRect = frameRect;
+
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    frameOfEffectWithFrame: function(drawableFrame) {
+        var gl = this.gl;
+
+        var minPt = {
+            "x": CGRectGetMinX(drawableFrame),
+            "y": CGRectGetMinY(drawableFrame)
+        };
+
+        var maxPt = {
+            "x": CGRectGetMaxX(drawableFrame),
+            "y": CGRectGetMaxY(drawableFrame)
+        };
+
+        var extraPadding = Math.max(drawableFrame.size.width, drawableFrame.size.height);
+        extraPadding = Math.max(extraPadding, this.slideSize.height / 3.0);
+
+        minPt.y -= extraPadding;
+        maxPt.y += extraPadding;
+
+        minPt.x -= extraPadding;
+        maxPt.x += extraPadding;
+
+        var frameRect = TSDRectWithPoints(minPt, maxPt);
+        frameRect = CGRectIntersection(frameRect, this.slideRect);
+        frameRect = CGRectIntegral(frameRect);
+
+        return frameRect;
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+
+        // initialize a shimmer effect object for each texture rectangle
+        this.shimmerEffects = [];
+
+        var program = this.program;
+        var slideRect = this.slideRect;
+        var duration = this.duration;
+        var direction = this.direction;
+        var type = this.type;
+        var parentOpacity = this.parentOpacity;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = this.textures[i];
+            var tr = this.textures[i].textureRect;
+            var frameRect = this.drawableObjects[i].frameRect;
+
+            var orthoOffset = {
+                "x": texture.offset.pointX - frameRect.origin.x,
+                "y": texture.offset.pointY + texture.height - (frameRect.origin.y + frameRect.size.height)
+            };
+
+            var baseOrthoTransform = WebGraphics.makeOrthoMatrix4(0, frameRect.size.width, 0, frameRect.size.height, -1, 1);
+            var baseTransform = WebGraphics.translateMatrix4(baseOrthoTransform, orthoOffset.x, -orthoOffset.y, 0);
+
+            var shimmerEffect = new KNWebGLBuildShimmerEffect(
+                renderer,
+                program,
+                slideRect,
+                texture,
+                frameRect,
+                baseTransform,
+                duration,
+                direction,
+                type,
+                parentOpacity
+            );
+
+            this.shimmerEffects.push(shimmerEffect);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        var percentfinished = this.percentfinished;
+        percentfinished += difference / duration;
+
+        if (percentfinished > 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (this.buildIn) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                        this.drawableObjects[i].drawFrame();
+                    }
+
+                    continue;
+                }
+
+                var width = textureInfo.width;
+                var height = textureInfo.height;
+                var offsetX = textureInfo.offset.pointX;
+                var offsetY = textureInfo.offset.pointY;
+
+                //draw shimmer effect
+                var shimmerEffect = this.shimmerEffects[i];
+                shimmerEffect.renderEffectAtPercent(this.percentfinished);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLBuildShimmerEffect = Class.create({
+    initialize: function(renderer, program, slideRect, texture, destinationRect, translate, duration, direction, buildType, parentOpacity) {
+        this.renderer = renderer;
+        this.gl = renderer.gl;
+        this.program = program;
+        this._slideRect = slideRect;
+        this._texture = texture;
+        this._destinationRect = destinationRect;
+        this._translate = translate;
+        this._duration = duration;
+        this._direction = direction;
+        this._buildType = buildType;
+
+        this._baseTransform = new Float32Array(16);
+
+        this._isSetup = false;
+
+        this.parentOpacity = parentOpacity;
+
+        // bind shimmer texture
+        this.shimmerTexture = KNWebGLUtil.bindTextureWithImage(this.gl, shimmerImage);
+
+        this.setupEffectIfNecessary();
+    },
+
+    setupEffectIfNecessary: function() {
+        if (this._isSetup) {
+            return;
+        }
+
+        var gl = this.gl;
+
+        var texture = this._texture;
+        var meshSize = CGSizeMake(2, 2);
+        var mFrameRect = {
+            "origin": {
+                "x": 0,
+                "y": 0
+            },
+            "size": {
+                "width": gl.viewportWidth,
+                "height": gl.viewportHeight
+            }
+        };
+
+        var orthoOffset = {
+            "x": texture.offset.pointX - mFrameRect.origin.x,
+            "y": texture.offset.pointY + texture.height - (mFrameRect.origin.y + mFrameRect.size.height)
+        };
+
+        var baseOrthoTransform = WebGraphics.makeOrthoMatrix4(0, mFrameRect.size.width, 0, mFrameRect.size.height, -1, 1);
+        var baseTransform = this.baseTransform = WebGraphics.translateMatrix4(baseOrthoTransform, orthoOffset.x, -orthoOffset.y, 0);
+
+        this._objectSystem = this.objectSystemForTR(this._texture, this._slideRect, this._duration);
+        this._objectSystem.setMVPMatrix(this.baseTransform);
+
+        // Set up particle particle system
+        if (this._objectSystem.shouldDraw) {
+            // Only set up the particles if we will actually draw this particle system!
+            this._particleSystem = this.particleSystemForTR(this._texture, this._slideRect, this._duration);
+            this._particleSystem.setMVPMatrix(this.baseTransform);
+        }
+
+        this._isSetup = true;
+    },
+
+    p_numberOfParticlesForTR: function(tr, slideRect, duration) {
+        var destRect = this._destinationRect;
+        var slideSize = slideRect.size;
+        var slideRatio = (destRect.size.width / slideSize.width * destRect.size.height / slideSize.height);
+        var texRatio = (tr.size.width / destRect.size.width * tr.size.height / destRect.size.height);
+
+        // create as many particles as possible without hitting our vertex limit
+        var numParticles = parseInt(Math.min((slideRatio * texRatio * 2000), 3276));
+
+        return numParticles;
+    },
+
+    objectSystemForTR: function(texture, slideRect, duration) {
+        var tr = texture.textureRect;
+        var numParticles = this.p_numberOfParticlesForTR(tr, slideRect, duration);
+
+        var particleSystem = new KNWebGLBuildShimmerObjectSystem(
+            this.renderer,
+            this.program["shimmerObject"],
+            {"width": tr.size.width, "height": tr.size.height},
+            {"width": slideRect.size.width, "height": slideRect.size.height},
+            duration,
+            numParticles,
+            texture.texture,
+            this._direction
+        );
+
+        return particleSystem;
+    },
+
+    particleSystemForTR: function(texture, slideRect, duration) {
+        var tr = texture.textureRect;
+        // Extra sparkles at end
+        var extraParticles = this.p_numberOfParticlesForTR(tr, slideRect, duration);
+
+        extraParticles = Math.max(2, extraParticles / 40);
+
+        // Add in sparkles to match object's particles
+        var objectSystemParticleCount = this._objectSystem.particleCount;
+        var numParticles = objectSystemParticleCount;
+
+        numParticles += extraParticles;
+
+        numParticles = Math.min(numParticles, 3276);
+
+        var particleSystem = new KNWebGLBuildShimmerParticleSystem(
+            this.renderer,
+            this.program["shimmerParticle"],
+            {"width": tr.size.width, "height": tr.size.height},
+            {"width": slideRect.size.width, "height": slideRect.size.height},
+            duration,
+            CGSizeMake(numParticles, 1),
+            this._objectSystem.particleSize,
+            this._objectSystem,
+            this.shimmerTexture,
+            this._direction
+        );
+
+        return particleSystem;
+    },
+
+    p_drawObject: function(percent, textureInfo, objectShader, objectDataBuffer) {
+        var gl = this.gl;
+        var opacity = this.parentOpacity * textureInfo.initialState.opacity;
+
+        opacity = opacity * TSUSineMap(percent);
+
+        objectShader.setGLFloat(opacity, kTSDGLShaderUniformOpacity);
+
+        gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        objectDataBuffer.drawWithShader(objectShader, true);
+    },
+
+    renderEffectAtPercent: function(percent) {
+        var gl = this.gl;
+        var texture = this._texture;
+
+        if (this._buildType === "buildOut") {
+            percent = 1.0 - percent;
+        }
+
+        var accelPercent = (1 - percent) * (1 - percent);
+        var isClockwise = this._buildType === "buildIn";
+
+        var rotation = (TSUReverseSquare(percent) * this._duration/1000 + percent) * Math.PI/2;
+
+        if (!isClockwise) {
+            rotation *= -1.0;
+        }
+
+        // Draw main object as pieces
+        var objectOpacitySpan = WebGraphics.makePoint(0.2, 0.4);
+        var objectOpacity = (percent - objectOpacitySpan.x) / objectOpacitySpan.y;
+
+        objectOpacity = WebGraphics.clamp(objectOpacity, 0.0, 1.0);
+        objectOpacity = TSUSineMap(objectOpacity);
+
+        var opacity = this.parentOpacity * texture.initialState.opacity;
+        objectOpacity *= opacity;
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // need to use the program before drawing the particle system
+        gl.useProgram(this.program["shimmerObject"].shaderProgram);
+
+        // set MVP Matrix for object system
+        this._objectSystem.setMVPMatrix(this.baseTransform);
+        this._objectSystem.drawGLSLWithPercent(accelPercent, objectOpacity, rotation, isClockwise, texture.texture);
+
+        // Draw shimmers
+
+        // need to use the program before drawing the particle system
+        gl.useProgram(this.program["shimmerParticle"].shaderProgram);
+
+        this._particleSystem.setMVPMatrix(this.baseTransform);
+        this._particleSystem.drawGLSLWithPercent(accelPercent, opacity * 0.5, rotation, isClockwise, this.shimmerTexture);
+    }
+});
+
+var KNWebGLBuildSparkle = Class.create(KNWebGLProgram, {
+    initialize: function($super, renderer, params) {
+        var effect = params.effect;
+
+        this.programData = {
+            name: "com.apple.iWork.Keynote.KLNSparkle",
+            programNames: ["sparkle"],
+            effect: effect,
+            textures: params.textures
+        };
+
+        $super(renderer, this.programData);
+
+        var gl = this.gl;
+
+        this.percentfinished = 0.0;
+
+        // create drawable object for drawing static texture
+        this.drawableObjects = [];
+
+        this.slideOrigin = {"x": 0, "y": 0};
+        this.slideSize = {"width": gl.viewportWidth, "height": gl.viewportHeight};
+        this.slideRect = {
+            "origin": this.slideOrigin,
+            "size": this.slideSize
+        };
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = params.textures[i];
+            var drawableFrame = texture.textureRect;
+
+            var drawableParams = {
+                effect: effect,
+                textures: [texture]
+            };
+
+            var frameRect = this.frameOfEffectWithFrame(drawableFrame);
+            var drawableObject = new KNWebGLDrawable(renderer, drawableParams);
+
+            drawableObject.frameRect = frameRect;
+
+            this.drawableObjects.push(drawableObject);
+        }
+
+        // set parent opacity from CA baseLayer
+        this.parentOpacity = effect.baseLayer.initialState.opacity;
+
+        // setup requirements
+        this.animationWillBeginWithContext();
+    },
+
+    frameOfEffectWithFrame: function(drawableFrame) {
+        var minPt = WebGraphics.makePoint(CGRectGetMinX(drawableFrame), CGRectGetMinY(drawableFrame));
+        var maxPt = WebGraphics.makePoint(CGRectGetMaxX(drawableFrame), CGRectGetMaxY(drawableFrame));
+
+        var extraPadding = Math.max(drawableFrame.size.width, drawableFrame.size.height);
+        // Make sure the width is large enough to deal with floating point precision errors in proj matrix
+        // (Otherwise very small text will look blurry)
+        extraPadding = Math.max(extraPadding, 128);
+
+        minPt.y = Math.max(CGRectGetMinY(this.slideRect), minPt.y - extraPadding);
+        maxPt.y = Math.min(CGRectGetMaxY(this.slideRect), maxPt.y + extraPadding);
+
+        minPt.x = Math.max(CGRectGetMinX(this.slideRect), minPt.x - extraPadding);
+        maxPt.x = Math.min(CGRectGetMaxX(this.slideRect), maxPt.x + extraPadding);
+
+        var frameRect = TSDRectWithPoints(minPt, maxPt);
+        frameRect = CGRectIntegral(frameRect);
+
+        return frameRect;
+    },
+
+    animationWillBeginWithContext: function() {
+        var renderer = this.renderer;
+
+        // initialize a shimmer effect object for each texture rectangle
+        this.sparkleEffects = [];
+
+        var program = this.program;
+        var slideRect = this.slideRect;
+        var duration = this.duration;
+        var direction = this.direction;
+        var type = this.type;
+        var parentOpacity = this.parentOpacity;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var texture = this.textures[i];
+            var direction = this.direction;
+            var tr = this.textures[i].textureRect;
+            var frameRect = this.drawableObjects[i].frameRect;
+
+            var orthoOffset = {
+                "x": texture.offset.pointX - frameRect.origin.x,
+                "y": texture.offset.pointY + texture.height - (frameRect.origin.y + frameRect.size.height)
+            };
+
+            var baseOrthoTransform = WebGraphics.makeOrthoMatrix4(0, frameRect.size.width, 0, frameRect.size.height, -1, 1);
+            var baseTransform = WebGraphics.translateMatrix4(baseOrthoTransform, orthoOffset.x, -orthoOffset.y, 0);
+
+            var sparkleEffect = new KNWebGLBuildSparkleEffect(
+                renderer,
+                program,
+                slideRect,
+                texture,
+                frameRect,
+                baseTransform,
+                duration,
+                direction,
+                type,
+                parentOpacity
+            );
+
+            this.sparkleEffects.push(sparkleEffect);
+        }
+    },
+
+    drawFrame: function(difference, elapsed, duration) {
+        var renderer = this.renderer;
+        var gl = this.gl;
+        var viewportWidth = gl.viewportWidth;
+        var viewportHeight = gl.viewportHeight;
+
+        // determine the type and direction
+        var buildIn = this.buildIn;
+        var buildOut = this.buildOut;
+
+        var percentfinished = this.percentfinished;
+        percentfinished += difference / duration;
+
+        if (percentfinished > 1) {
+            percentfinished = 1;
+            this.isCompleted = true;
+        }
+
+        this.percentfinished = percentfinished;
+
+        for (var i = 0, length = this.textures.length; i < length; i++) {
+            var textureInfo = this.textures[i];
+            var initialState = textureInfo.initialState;
+            var animations = textureInfo.animations;
+
+            if (textureInfo.hasHighlightedBulletAnimation) {
+                if (!initialState.hidden) {
+                    var opacity;
+                    if (animations.length > 0 && animations[0].property === "opacity") {
+                        var opacityFrom = animations[0].from.scalar;
+                        var opacityTo = animations[0].to.scalar;
+                        var diff = opacityTo - opacityFrom;
+                        opacity = opacityFrom + diff * percentfinished;
+                    } else {
+                        opacity = textureInfo.initialState.opacity;
+                    }
+
+                    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            } else if (textureInfo.animations.length > 0) {
+                if (this.isCompleted) {
+                    if (buildIn) {
+                        // if completed, just draw its texture object for better performance
+                        this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;;
+                        this.drawableObjects[i].drawFrame();
+                    }
+                    continue;
+                }
+
+                //draw shimmer effect
+                var sparkleEffect = this.sparkleEffects[i];
+                sparkleEffect.renderEffectAtPercent(this.percentfinished);
+            } else {
+                if (!textureInfo.initialState.hidden) {
+                    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+                    this.drawableObjects[i].Opacity = this.parentOpacity * textureInfo.initialState.opacity;
+                    this.drawableObjects[i].drawFrame();
+                }
+            }
+        }
+    }
+});
+
+var KNWebGLBuildSparkleEffect = Class.create({
+    initialize: function(renderer, program, slideRect, texture, destinationRect, translate, duration, direction, buildType, parentOpacity) {
+        this.renderer = renderer;
+        this.gl = renderer.gl;
+        this.program = program;
+        this._slideRect = slideRect;
+        this._texture = texture;
+        this._destinationRect = destinationRect;
+        this._translate = translate;
+        this._duration = duration;
+        this._direction = direction;
+        this._buildType = buildType;
+
+        this._baseTransform = new Float32Array(16);
+
+        this._isSetup = false;
+
+        this.parentOpacity = parentOpacity;
+
+        // bind shimmer texture
+        this.sparkleTexture = KNWebGLUtil.bindTextureWithImage(this.gl, sparkleImage);
+
+        this.setupEffectIfNecessary();
+    },
+
+    setupEffectIfNecessary: function() {
+        if (this._isSetup) {
+            return;
+        }
+
+        var gl = this.gl;
+
+        var texture = this._texture;
+        var meshSize = CGSizeMake(2, 2);
+        var mFrameRect = {
+            "origin": {
+                "x": 0,
+                "y": 0
+            },
+            "size": {
+                "width": gl.viewportWidth,
+                "height": gl.viewportHeight
+            }
+        };
+
+        var orthoOffset = {
+            "x": texture.offset.pointX - mFrameRect.origin.x,
+            "y": texture.offset.pointY + texture.height - (mFrameRect.origin.y + mFrameRect.size.height)
+        };
+
+        var baseOrthoTransform = WebGraphics.makeOrthoMatrix4(0, mFrameRect.size.width, 0, mFrameRect.size.height, -1, 1);
+        var baseTransform = this.baseTransform = WebGraphics.translateMatrix4(baseOrthoTransform, orthoOffset.x, -orthoOffset.y, 0);
+
+        // init object shader and data buffer
+        var objectShader = this._objectShader = new TSDGLShader(gl);
+        objectShader.initWithDefaultTextureAndOpacityShader();
+
+        // object shader set methods
+        objectShader.setMat4WithTransform3D(baseTransform, kTSDGLShaderUniformMVPMatrix);
+        objectShader.setGLint(0, kTSDGLShaderUniformTexture);
+
+        // new data buffer attributes
+        var objectPositionAttribute = new TSDGLDataBufferAttribute(kTSDGLShaderAttributePosition, GL_STREAM_DRAW, GL_FLOAT, false, 2);
+        var objectTexCoordAttribute = new TSDGLDataBufferAttribute(kTSDGLShaderAttributeTexCoord, GL_STREAM_DRAW, GL_FLOAT, false, 2);
+
+        // init object data buffer
+        var objectDataBuffer = this._objectDataBuffer = new TSDGLDataBuffer(gl);
+
+        objectDataBuffer.newDataBufferWithVertexAttributes([objectPositionAttribute, objectTexCoordAttribute] , meshSize, true);
+
+        // Set up sparkle particle system
+        this.sparkleSystem = this.sparkleSystemForTR(this._texture, this._slideRect, this._duration);
+        this.sparkleSystem.setMVPMatrix(this.baseTransform);
+        this.sparkleSystem.setColor(new Float32Array([1, 1, 1, 1]));
+
+        this._isSetup = true;
+    },
+
+    p_numberOfParticlesForTR: function(tr, slideRect, duration) {
+        var destRect = this._destinationRect;
+        var slideSize = slideRect.size;
+        var slideRatio = (destRect.size.width / slideSize.width * destRect.size.height / slideSize.height);
+        var texRatio = (tr.size.width / destRect.size.width * tr.size.height / destRect.size.height);
+
+        // create as many particles as possible without hitting our vertex limit
+        var numParticles = parseInt(Math.min((slideRatio * texRatio * 2000), 3276));
+
+        return numParticles;
+    },
+
+    sparkleSystemForTR: function(texture, slideRect, duration) {
+        var tr = texture.textureRect;
+        var slideSize = this._slideRect.size;
+        var boundingRect = this._destinationRect;
+
+        var slideRatio = Math.min(boundingRect.size.width, slideSize.width) / slideSize.width * Math.min(boundingRect.size.height, slideSize.height) / slideSize.height;
+
+        var numParticles = parseInt(((2 - Math.sqrt(slideRatio)) / 2) * 1500 * this._duration / 1000);
+
+        var sparkleSystem = new KNWebGLBuildSparkleSystem(
+            this.renderer,
+            this.program["sparkle"],
+            {"width": tr.size.width, "height": tr.size.height},
+            {"width": slideRect.size.width, "height": slideRect.size.height},
+            duration,
+            CGSizeMake(numParticles, 1),
+            {"width": 128, "height": 128},
+            this.sparkleTexture,
+            this._direction
+        );
+
+        return sparkleSystem;
+    },
+
+    p_drawObject: function(percent, textureInfo, objectShader, objectDataBuffer) {
+        var gl = this.gl;
+        var opacity = this.parentOpacity * textureInfo.initialState.opacity;
+
+        opacity = opacity * TSUSineMap(percent);
+
+        objectShader.setGLFloat(opacity, kTSDGLShaderUniformOpacity);
+
+        gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        objectDataBuffer.drawWithShader(objectShader, true);
+    },
+
+    renderEffectAtPercent: function(percent) {
+        var gl = this.gl;
+        var texture = this._texture;
+        var direction = this._direction;
+        var tr = texture.textureRect;
+
+        var isReverse = (direction == KNDirection.kKNDirectionRightToLeft || direction == KNDirection.kKNDirectionTopToBottom);
+        var isHorizontal = (direction == KNDirection.kKNDirectionRightToLeft || direction == KNDirection.kKNDirectionLeftToRight);
+
+        var mvpMatrix = this._translate;
+        var alpha = this.parentOpacity * texture.initialState.opacity;
+
+        // CONSTANTS
+        var duration = this._duration / 1000;
+        var blurWidth = 0.2 / duration;
+        var width = tr.size.width;
+        var height = tr.size.height;
+        // =========
+
+        // FIRST, we draw the original image fading out
+        var particleTiming = KNSparkleMaxParticleLife / Math.max(0.75, duration);
+        var opaqueWidth = percent / (1. - particleTiming);
+        var xStart = 0, yStart = 0, xEnd = 0, yEnd = 0, xTexStart = 0, yTexStart = 0, xTexEnd = 0, yTexEnd = 0;
+
+        if (this._buildType == "buildOut") {
+            opaqueWidth -= blurWidth;
+            xStart = (isHorizontal) ? ((isReverse) ? 0 : width) : 0;
+            yStart = (isHorizontal) ? 0 : ((isReverse) ? 0 : height);
+            xEnd = (isHorizontal) ? ((isReverse) ? width - (width * WebGraphics.clamp(opaqueWidth, 0, 1)) : width * WebGraphics.clamp(opaqueWidth, 0, 1)) : width;
+            yEnd = (isHorizontal) ? height : ((isReverse) ? height - (height * WebGraphics.clamp(opaqueWidth, 0, 1)) : (height * WebGraphics.clamp(opaqueWidth, 0, 1)));
+            xTexStart = (isHorizontal) ? ((isReverse) ? 0 : 1) : 0;
+            yTexStart = (isHorizontal) ? 0 : ((isReverse) ? 0 : 1);
+            xTexEnd = (isHorizontal) ? ((isReverse) ? 1 - (1 * WebGraphics.clamp(opaqueWidth, 0, 1)) : (1 * WebGraphics.clamp(opaqueWidth, 0, 1))) : 1;
+            yTexEnd = (isHorizontal) ? 1 : ((isReverse) ? 1 - (1 * WebGraphics.clamp(opaqueWidth, 0, 1)) : (1 * WebGraphics.clamp(opaqueWidth, 0, 1)));
+        } else {
+            opaqueWidth -= blurWidth;
+            xStart = (isHorizontal) ? ((isReverse) ? width : 0) : 0;
+            yStart = (isHorizontal) ? 0 : ((isReverse) ? height : 0);
+            xEnd = (isHorizontal) ? ((isReverse) ? width - (width * WebGraphics.clamp(opaqueWidth, 0, 1)) : width * WebGraphics.clamp(opaqueWidth, 0, 1)) : width;
+            yEnd = (isHorizontal) ? height : ((isReverse) ? height - (height * WebGraphics.clamp(opaqueWidth, 0, 1)) : height * WebGraphics.clamp(opaqueWidth, 0, 1));
+            xTexStart = (isHorizontal) ? ((isReverse) ? 1 : 0) : 0;
+            yTexStart = (isHorizontal) ? 0 : ((isReverse) ? 1 : 0);
+            xTexEnd = (isHorizontal) ? ((isReverse) ? 1 - (1 * WebGraphics.clamp(opaqueWidth, 0, 1)) : 1 * WebGraphics.clamp(opaqueWidth, 0, 1)) : 1;
+            yTexEnd = (isHorizontal) ? 1 : ((isReverse) ? 1 - (1 * WebGraphics.clamp(opaqueWidth, 0, 1)) : 1 * WebGraphics.clamp(opaqueWidth, 0, 1));
+        }
+
+        gl.bindTexture(gl.TEXTURE_2D, texture.texture);
+
+        this._objectShader.setGLFloat(alpha, kTSDGLShaderUniformOpacity);
+
+        // update data buffer position and text coord
+        var objectDataBuffer = this._objectDataBuffer;
+        var objectPositionAttribute = objectDataBuffer.vertexAttributeNamed(kTSDGLShaderAttributePosition);
+        var objectTexCoordAttribute = objectDataBuffer.vertexAttributeNamed(kTSDGLShaderAttributeTexCoord);
+
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xStart, yStart), objectPositionAttribute, 0);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xEnd, yStart), objectPositionAttribute, 1);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xStart, yEnd), objectPositionAttribute, 2);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xEnd, yEnd), objectPositionAttribute, 3);
+
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xTexStart, yTexStart), objectTexCoordAttribute, 0);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xTexEnd, yTexStart), objectTexCoordAttribute, 1);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xTexStart, yTexEnd), objectTexCoordAttribute, 2);
+        objectDataBuffer.setGLPoint2D(WebGraphics.makePoint(xTexEnd, yTexEnd), objectTexCoordAttribute, 3);
+
+        objectDataBuffer.drawWithShader(this._objectShader, true);
+
+        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        // need to use the program before drawing the particle system
+        gl.useProgram(this.program["sparkle"].shaderProgram);
+
+        //this.sparkleSystem.setMVPMatrix(this.baseTransform);
+        this.sparkleSystem.drawFrame(percent, 1.0);
+    }
+});