class Caman.Renderer
Handles all of the various rendering methods in Caman. Most of the image modification happens here. A new Renderer object is created for every render operation.
class Caman.Renderer
The number of blocks to split the image into during the render process to simulate concurrency. This also helps the browser manage the (possibly) long running render jobs.
@Blocks = if Caman.NodeJS then require('os').cpus().length else 4
constructor: (@c) ->
@renderQueue = []
@modPixelData = null
add: (job) ->
return unless job?
@renderQueue.push job
Grabs the next operation from the render queue and passes it to Renderer for execution
processNext: =>
If the queue is empty, fire the finished callback
if @renderQueue.length is 0
Event.trigger @, "renderFinished"
@finishedFn.call(@c) if @finishedFn?
return @
@currentJob = @renderQueue.shift()
switch @currentJob.type
when Filter.Type.LayerDequeue
layer = @c.canvasQueue.shift()
@c.executeLayer layer
@processNext()
when Filter.Type.LayerFinished
@c.applyCurrentLayer()
@c.popContext()
@processNext()
when Filter.Type.LoadOverlay
@loadOverlay @currentJob.layer, @currentJob.src
when Filter.Type.Plugin
@executePlugin()
else
@executeFilter()
execute: (callback) ->
@finishedFn = callback
@modPixelData = Util.dataArray(@c.pixelData.length)
@processNext()
eachBlock: (fn) ->
Prepare all the required render data
@blocksDone = 0
n = @c.pixelData.length
blockPixelLength = Math.floor (n / 4) / Renderer.Blocks
blockN = blockPixelLength * 4
lastBlockN = blockN + ((n / 4) % Renderer.Blocks) * 4
for i in [0...Renderer.Blocks]
start = i * blockN
end = start + (if i is Renderer.Blocks - 1 then lastBlockN else blockN)
if Caman.NodeJS
f = Fiber => fn.call(@, i, start, end)
bnum = f.run()
@blockFinished(bnum)
else
setTimeout do (i, start, end) =>
=> fn.call(@, i, start, end)
, 0
The core of the image rendering, this function executes the provided filter.
NOTE: this does not write the updated pixel data to the canvas. That happens when all filters are finished rendering in order to be as fast as possible.
executeFilter: ->
Event.trigger @c, "processStart", @currentJob
if @currentJob.type is Filter.Type.Single
@eachBlock @renderBlock
else
@eachBlock @renderKernel
Executes a standalone plugin
executePlugin: ->
Log.debug "Executing plugin #{@currentJob.plugin}"
Plugin.execute @c, @currentJob.plugin, @currentJob.args
Log.debug "Plugin #{@currentJob.plugin} finished!"
@processNext()
Renders a single block of the canvas with the current filter function
renderBlock: (bnum, start, end) ->
Log.debug "Block ##{bnum} - Filter: #{@currentJob.name}, Start: #{start}, End: #{end}"
Event.trigger @c, "blockStarted",
blockNum: bnum
totalBlocks: Renderer.Blocks
startPixel: start
endPixel: end
pixel = new Pixel()
pixel.setContext @c
for i in [start...end] by 4
pixel.loc = i
pixel.r = @c.pixelData[i]
pixel.g = @c.pixelData[i+1]
pixel.b = @c.pixelData[i+2]
pixel.a = @c.pixelData[i+3]
@currentJob.processFn pixel
@c.pixelData[i] = Util.clampRGB pixel.r
@c.pixelData[i+1] = Util.clampRGB pixel.g
@c.pixelData[i+2] = Util.clampRGB pixel.b
@c.pixelData[i+3] = Util.clampRGB pixel.a
if Caman.NodeJS
Fiber.yield(bnum)
else
@blockFinished bnum
Applies an image kernel to the canvas
renderKernel: (bnum, start, end) ->
name = @currentJob.name
bias = @currentJob.bias
divisor = @currentJob.divisor
n = @c.pixelData.length
adjust = @currentJob.adjust
adjustSize = Math.sqrt adjust.length
kernel = []
Log.debug "Rendering kernel - Filter: #{@currentJob.name}"
start = Math.max start, @c.dimensions.width * 4 * ((adjustSize - 1) / 2)
end = Math.min end, n - (@c.dimensions.width * 4 * ((adjustSize - 1) / 2))
builder = (adjustSize - 1) / 2
pixel = new Pixel()
pixel.setContext(@c)
for i in [start...end] by 4
pixel.loc = i
builderIndex = 0
for j in [-builder..builder]
for k in [builder..-builder]
p = pixel.getPixelRelative j, k
kernel[builderIndex * 3] = p.r
kernel[builderIndex * 3 + 1] = p.g
kernel[builderIndex * 3 + 2] = p.b
builderIndex++
res = @processKernel adjust, kernel, divisor, bias
@modPixelData[i] = Util.clampRGB(res.r)
@modPixelData[i+1] = Util.clampRGB(res.g)
@modPixelData[i+2] = Util.clampRGB(res.b)
@modPixelData[i+3] = @c.pixelData[i+3]
if Caman.NodeJS
Fiber.yield(bnum)
else
@blockFinished bnum
Called when a single block is finished rendering. Once all blocks are done, we signal that this filter is finished rendering and continue to the next step.
blockFinished: (bnum) ->
Log.debug "Block ##{bnum} finished! Filter: #{@currentJob.name}" if bnum >= 0
@blocksDone++
Event.trigger @c, "blockFinished",
blockNum: bnum
blocksFinished: @blocksDone
totalBlocks: Renderer.Blocks
if @blocksDone is Renderer.Blocks
if @currentJob.type is Filter.Type.Kernel
for i in [0...@c.pixelData.length]
@c.pixelData[i] = @modPixelData[i]
Log.debug "Filter #{@currentJob.name} finished!" if bnum >=0
Event.trigger @c, "processComplete", @currentJob
@processNext()
The "filter function" for kernel adjustments.
processKernel: (adjust, kernel, divisor, bias) ->
val = r: 0, g: 0, b: 0
for i in [0...adjust.length]
val.r += adjust[i] * kernel[i * 3]
val.g += adjust[i] * kernel[i * 3 + 1]
val.b += adjust[i] * kernel[i * 3 + 2]
val.r = (val.r / divisor) + bias
val.g = (val.g / divisor) + bias
val.b = (val.b / divisor) + bias
val
Loads an image onto the current canvas
loadOverlay: (layer, src) ->
img = document.createElement 'img'
img.onload = =>
layer.context.drawImage img, 0, 0, @c.dimensions.width, @c.dimensions.height
layer.imageData = layer.context.getImageData 0, 0, @c.dimensions.width, @c.dimensions.height
layer.pixelData = layer.imageData.data
@c.pixelData = layer.pixelData
@processNext()
proxyUrl = IO.remoteCheck src
img.src = if proxyUrl? then proxyUrl else src
Renderer = Caman.Renderer