Created
October 17, 2013 20:48
-
-
Save clooth/7031941 to your computer and use it in GitHub Desktop.
WebGL Photo resizer example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Resizyr - WebGL Image Resizer | |
# | |
# Extensions | |
# This allows us to do for example the following: | |
# 5.times () -> console.log "foo" | |
# | |
Number::times = (fn) -> | |
do fn for [1..@valueOf()] | |
return | |
# Split array into chunks with given size (maximum size) | |
# Usage for splitting into chunks of 5 values: | |
# chunked_array = other_array.chunkify 5 | |
# | |
Array::chunkify = (chunkSize) -> | |
finalArray = [] | |
i = 0 | |
j = this.length | |
while i < j | |
tempArray = this.slice(i, i + chunkSize) | |
finalArray.push tempArray | |
i += chunkSize | |
finalArray | |
# Main namespace | |
window.Resizyr = | |
# Simple logger helpers | |
log: (msg) -> | |
if window.console and window.console.log | |
window.console.log msg | |
error: (msg) -> | |
console = window.console | |
if console | |
if console.error | |
console.error msg | |
else if console.log | |
console.log msg | |
# Disable all logging | |
disableLogging: () -> | |
window.Resizyr.log = () -> false | |
window.Resizyr.log = () -> false | |
# | |
# Resizyr - ImageLoader | |
# Takes care of the handling of File objects and image loading | |
window.Resizyr.Resizyr = class Resizyr | |
# The array that's filled when the files are loaded | |
imageFileList: [] | |
# The amount of image files we're going to be processing | |
imageFileCount: 0 | |
# The element where our dropzone will be at | |
dropZoneElement: null | |
# Our resizer workers doing all the big lifting | |
workers: [] | |
# The amount of workers that have been created | |
workerCount: 0 | |
# The amount of workers that have finished | |
workersFinished: 0 | |
# The target size for our resize | |
targetSize: null | |
# Creates a new ImageLoader instance | |
# @param fileList The FileList object from | |
# @param completionHandler The func for when we're done with setup | |
constructor: (dropZoneElementID, @targetSize, @workerCount = 3) -> | |
# Set up dropzone | |
this.initDropZone(dropZoneElementID) | |
initDropZone: (dropZoneElementID) -> | |
console.log "Initializing drop zone" | |
# Reference DOM | |
@dropZoneElement = document.getElementById dropZoneElementID | |
# Check that the element exists | |
if @dropZoneElement.length == 0 | |
throw new Error("DropZone element not found.", dropZoneElementID) | |
# Add state classes on hover | |
@dropZoneElement.ondragover = (e) -> | |
@className = 'dropzone_hover' | |
false | |
# Remove state classes on drag end | |
@dropZoneElement.ondragend = (e) -> | |
@className = "" | |
false | |
# Handle dropped files | |
@dropZoneElement.ondrop = (e) => | |
e.preventDefault() | |
console.log "Dropped files" | |
files = e.dataTransfer.files | |
if !files | |
alert "Something went wrong." | |
this.handleDroppedFiles files | |
this.initWorkers() | |
initWorkers: () -> | |
console.log "Initializing workers" | |
# Split file list among workers | |
chunkedFileList = @imageFileList.chunkify @workerCount | |
chunkIndex = 0 | |
while chunkIndex < chunkedFileList.length | |
files = chunkedFileList[chunkIndex] | |
worker = new Worker(files, @targetSize, @) | |
worker.start() | |
@workers.push(worker) | |
chunkIndex++ | |
# Handle the incoming filelist from the dropzone | |
handleDroppedFiles: (fileList) -> | |
console.log "Handling dropped files" | |
# Iterate through fileList and push all File objects into the | |
# prototype variable of the loader | |
# Filter out everything that's not a supported image | |
@imageFileList = (file for file in fileList).filter (f) -> | |
f.type in ["image/png", "image/jpeg", "image/gif"] | |
# Save the amount of files we have so we can refer to it later | |
@imageFileCount = @imageFileList.length | |
# Handle processed images | |
handleResizedPhoto: (imageObject) -> | |
setTimeout () => | |
imagesEl = document.getElementById 'images' | |
imagesEl.appendChild(imageObject) | |
console.log "Handling resized photo" | |
# | |
# Resizyr - Worker | |
# Takes care of the actual image resizing and rendering | |
# | |
window.Resizyr.Worker = class Worker | |
# The File objects this worker was given | |
files = [] | |
# The size we want these photos to fit into | |
targetSize: {width: 80, height: 80} | |
# What class do we tell about our progress? | |
# Required implemented methods | |
delegate: null | |
# Canvas element | |
canvas: null | |
# WebGL context | |
context: null | |
# WebGL Texture | |
texture: null | |
# WebGL Program | |
program: null | |
# Boot up a new worker | |
constructor: (@files, @targetSize, @delegate = null) -> | |
# Set up everything | |
this.createCanvas() | |
this.createWebGLContext() | |
vertexShader = this.loadShaderFromElement "2d-vertex-shader" | |
fragmentShader = this.loadShaderFromElement "2d-fragment-shader" | |
@program = this.createProgram [vertexShader, fragmentShader] | |
@context.useProgram @program | |
this.createTexture() | |
# Start processing images | |
start: () -> | |
this.loadNextPhoto() | |
# Create a new WebGL canvas | |
createCanvas: () -> | |
@canvas = document.createElement 'canvas' | |
@canvas.width = @targetSize.width | |
@canvas.height = @targetSize.height | |
# Get the WebGL context | |
createWebGLContext: () -> | |
# Check for WebGL support | |
if !window.WebGLRenderingContext | |
throw new Error("WebGL not supported in this browser.") | |
# Get proper context | |
@context = @canvas.getContext("webgl") || @canvas.getContext("experimental-webgl") | |
# Load shader info from DOM element and compile it | |
loadShaderFromElement: (elementID) -> | |
# Grab element | |
element = document.getElementById elementID | |
if !element | |
throw Error("Shader element not found", elementID) | |
shaderSource = element.text; | |
shaderType = null | |
if element.type == "x-shader/x-vertex" | |
shaderType = @context.VERTEX_SHADER | |
else if element.type = "x-shader/x-fragment" | |
shaderType = @context.FRAGMENT_SHADER | |
else | |
throw new Error("Unknown shader type", element.type) | |
# Compile shader | |
shader = @context.createShader shaderType | |
@context.shaderSource shader, shaderSource | |
@context.compileShader shader | |
# Check if everything went okay | |
if [email protected](shader, @context.COMPILE_STATUS) | |
@context.deleteShader shader | |
throw new Error("Error compiling shader '"+ shader + "':"+ @context.getShaderInfoLog(shader)) | |
shader | |
# Create webgl program program | |
createProgram: (shaders) -> | |
# Create new program | |
program = @context.createProgram() | |
# Attach given shaders | |
for shader in shaders | |
@context.attachShader program, shader | |
@context.linkProgram program | |
# Check status | |
if [email protected](program, @context.LINK_STATUS) | |
@context.deleteProgram program | |
throw new Error("Error linking program:"+ @context.getProgramInfoLog(program)) | |
program | |
# Create re-usable texture object | |
createTexture: () -> | |
# Create 2D texture object and bind it to the webgl context | |
@texture = @context.createTexture() | |
@context.bindTexture @context.TEXTURE_2D, @texture | |
# Set texture parameters | |
# This allows us to render any sized image | |
@context.texParameteri @context.TEXTURE_2D, @context.TEXTURE_WRAP_S, @context.CLAMP_TO_EDGE | |
@context.texParameteri @context.TEXTURE_2D, @context.TEXTURE_WRAP_T, @context.CLAMP_TO_EDGE | |
@context.texParameteri @context.TEXTURE_2D, @context.TEXTURE_MIN_FILTER, @context.NEAREST | |
@context.texParameteri @context.TEXTURE_2D, @context.TEXTURE_MAG_FILTER, @context.NEAREST | |
# Handle file object loading | |
# NOTE: This removes the first file object on each cycle | |
loadNextPhoto: () -> | |
# Get topmost file object in the array | |
file = @files.shift() | |
# Initialize FileReader API | |
reader = new FileReader() | |
reader.onload = (event) => | |
# Create image element | |
image = new Image() | |
image.onload = () => | |
# Scale and render photo | |
this.renderScaledPhoto image | |
setTimeout () => | |
this.loadNextPhoto() | |
, 50 | |
image.src = event.target.result | |
# Read file | |
reader.readAsDataURL file | |
# Render scaled photo | |
renderScaledPhoto: (image) -> | |
# Look up where the vertex data needs to go | |
positionLocation = @context.getAttribLocation @program, "a_position" | |
texCoordLocation = @context.getAttribLocation @program, "a_texCoord" | |
# Provide texture coordinates for the rectangle | |
texCoordBuffer = @context.createBuffer() | |
@context.bindBuffer @context.ARRAY_BUFFER, texCoordBuffer | |
@context.bufferData @context.ARRAY_BUFFER, new Float32Array([ | |
0.0, 0.0, | |
1.0, 0.0, | |
0.0, 1.0, | |
0.0, 1.0, | |
1.0, 0.0, | |
1.0, 1.0 | |
]), @context.STATIC_DRAW | |
@context.enableVertexAttribArray texCoordLocation | |
@context.vertexAttribPointer texCoordLocation, 2, @context.FLOAT, false, 0, 0 | |
# Clear the color buffer with opaque black | |
# This gives us that nice "video frame" look | |
@context.clearColor 0.0, 0.0, 0.0, 1.0 | |
@context.clear @context.COLOR_BUFFER_BIT | |
# Upload the image into the texture | |
@context.texImage2D @context.TEXTURE_2D, 0, @context.RGB, @context.RGB, @context.UNSIGNED_BYTE, image | |
# Look up and uniform location | |
resolutionLocation = @context.getUniformLocation @program, "u_resolution" | |
@context.uniform2f resolutionLocation, @targetSize.width, @targetSize.height | |
# Create a buffer for the position of the rectangle corners | |
buffer = @context.createBuffer() | |
@context.bindBuffer @context.ARRAY_BUFFER, buffer | |
@context.enableVertexAttribArray positionLocation | |
@context.vertexAttribPointer positionLocation, 2, @context.FLOAT, false, 0, 0 | |
# Calculate correct position to render the image into | |
imageWidth = image.width | |
imageHeight = image.height | |
# Calculate the size factor in comparison to canvas size | |
widthFactor = imageWidth / @targetSize.width | |
heightFactor = imageHeight / @targetSize.height | |
# Find out the maximum size factor, but keep it at minimum 1 | |
# so we don't scale images up | |
sizeFactor = Math.max Math.max(widthFactor, heightFactor), 1 | |
# Our new image size can now be calculated easily | |
newWidth = imageWidth / sizeFactor | |
newHeight = imageHeight / sizeFactor | |
# Make sure our image is always centered within the final image | |
leftOffset = (@targetSize.width - newWidth) / 2 | |
topOffset = (@targetSize.height - newHeight) / 2 | |
# Create final render rectangle for the image | |
this.setBufferDataRectangle leftOffset, topOffset, newWidth, newHeight | |
# Render the photo | |
@context.drawArrays @context.TRIANGLES, 0, 6 | |
# Grab the final video from our webgl context and | |
# pass it to our delegate who's listening | |
imageData = null | |
imageNode = null | |
imageData = @canvas.toDataURL() | |
imageNode = document.createElement "img" | |
imageNode.src = imageData | |
@delegate.handleResizedPhoto imageNode | |
# Set the rendering rectangle correctly | |
setBufferDataRectangle: (x, y, width, height) -> | |
x1 = x | |
x2 = x + width | |
y1 = y | |
y2 = y + height | |
@context.bufferData @context.ARRAY_BUFFER, new Float32Array([ | |
x1, y1, | |
x2, y1, | |
x1, y2, | |
x1, y2, | |
x2, y1, | |
x2, y2 | |
]), @context.STATIC_DRAW |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment