Skip to content

Instantly share code, notes, and snippets.

@clooth
Created October 17, 2013 20:48
Show Gist options
  • Save clooth/7031941 to your computer and use it in GitHub Desktop.
Save clooth/7031941 to your computer and use it in GitHub Desktop.
WebGL Photo resizer example
#
# 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