Created
December 14, 2011 06:06
-
-
Save benvanik/1475466 to your computer and use it in GitHub Desktop.
Simple Image Pyramid Demo
This file contains 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
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Image Pyramid Demo</title> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | |
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,user-scalable=no"> | |
<script src="pyramiddemo.js"></script> | |
<style> | |
body { | |
margin: 0; | |
} | |
#toolbar { | |
position: absolute; | |
left: 0; | |
top: 0; | |
right: 0; | |
height: 20px; | |
z-index: 1; | |
} | |
#displayCanvas { | |
position: absolute; | |
left: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
-o-user-select: none; | |
user-select: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="toolbar"> | |
<input type="range" min="1" max="1000" value="100" step="1" onchange="bias(parseFloat(this.value) / 100)"/> | |
</div> | |
<canvas id="displayCanvas"></canvas> | |
<script> | |
initDemo(); | |
</script> | |
</body> | |
</html> |
This file contains 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
// A pyramid represents a single source of tiles. Some information is cached to make the rest | |
// of the code cleaner, such as the level sizes. Each pyramid can provide tiles - one could | |
// have different implementations of this that return imagery/videos/etc. | |
var ImagePyramid = function(width, height, tileSize, tileOverlap, border) { | |
this.id = 'p' + (ImagePyramid.nextId_++); | |
this.width = width; | |
this.height = height; | |
this.tileSize = tileSize || 256; | |
this.tileOverlap = tileOverlap || 0; | |
this.border = border || 0; | |
// Min and max level ranges - currently the first level that fits on a single tile and the max level | |
// If rendering from a collection minLevel would be the minLevel of that collection | |
var levelCount = Math.ceil(Math.log(Math.max(width, height)) / Math.LN2) + 1; | |
this.minLevel = Math.floor(Math.log(tileSize) / Math.LN2); | |
this.maxLevel = Math.max(0, levelCount - 1); | |
this.levels = new Array(levelCount); | |
for (var n = 0; n < levelCount; n++) { | |
var pow2 = Math.pow(2, levelCount - n - 1); | |
var pixelWidth = Math.max(1, Math.ceil(width / pow2)); | |
var pixelHeight = Math.max(1, Math.ceil(height / pow2)); | |
this.levels[n] = { | |
// Size, in pixels, of the level imagery | |
pixelWidth: pixelWidth, | |
pixelHeight: pixelHeight, | |
// Size, in tiles, of the level imagery | |
tileWidth: Math.ceil(pixelWidth / tileSize), | |
tileHeight: Math.ceil(pixelHeight / tileSize) | |
}; | |
} | |
}; | |
ImagePyramid.nextId_ = 0; | |
// Computes a tiles dimensions taking into account tile overlap and border | |
ImagePyramid.prototype.computeTileDimensions = function(level, x, y) { | |
var levelInfo = this.levels[level]; | |
var tileWidth = this.tileSize; | |
if (x == 0) { | |
tileWidth += this.border; | |
} else { | |
tileWidth += this.tileOverlap; | |
} | |
if (x == levelInfo.tileWidth - 1) { | |
tileWidth += this.border; | |
} else { | |
tileWidth += this.tileOverlap; | |
} | |
var tileHeight = this.tileSize; | |
if (y == 0) { | |
tileHeight += this.border; | |
} else { | |
tileHeight += this.tileOverlap; | |
} | |
if (y == levelInfo.tileHeight - 1) { | |
tileHeight += this.border; | |
} else { | |
tileHeight += this.tileOverlap; | |
} | |
return { | |
width: tileWidth, | |
height: tileHeight | |
}; | |
}; | |
ImagePyramid.levelColors_ = [ | |
'rgb(255,0,0)', | |
'rgb(255,255,0)', | |
'rgb(255,0,255)', | |
'rgb(0,255,0)', | |
'rgb(0,255,255)', | |
'rgb(0,0,255)' | |
]; | |
// Gets a tile with alternating color per level with the LOD stamped on it | |
ImagePyramid.prototype.getTile = function(level, x, y) { | |
var tileDimensions = this.computeTileDimensions(level, x, y); | |
var canvas = document.createElement('canvas'); | |
canvas.width = tileDimensions.width; | |
canvas.height = tileDimensions.height; | |
var ctx = canvas.getContext('2d'); | |
ctx.fillStyle = ImagePyramid.levelColors_[level % ImagePyramid.levelColors_.length]; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
ctx.fillStyle = 'rgb(255,255,255)'; | |
ctx.fillText(level + '@' + x + ',' + y, this.tileOverlap, this.tileOverlap + 8); | |
return canvas; | |
}; | |
// Simple LERP for temporal blending | |
var OpacityTween = function() { | |
this.value = 0; | |
this.target = 1; | |
this.startTimestamp = 0; | |
}; | |
OpacityTween.duration = 0.250; | |
OpacityTween.prototype.update = function(timestamp) { | |
if (!this.startTimestamp) { | |
this.startTimestamp = timestamp; | |
return true; | |
} | |
var t = (timestamp - this.startTimestamp) / 1000; | |
t = Math.min(OpacityTween.duration, t) / OpacityTween.duration; | |
this.value = Math.max(0, Math.min(1, t)); | |
return Math.abs(this.target - this.value) > 0.01; | |
}; | |
// A single tile entry in the tile cache | |
var Tile = function(content) { | |
this.width = content.width; | |
this.height = content.height; | |
this.content = content; | |
this.opacity = new OpacityTween(); | |
}; | |
// A very simple tile cache that discards all tiles not used in the last frame | |
// Also manages requesting new tiles each frame by picking the highest priority one | |
var TileCache = function() { | |
this.tiles = {}; | |
this.usedTiles = {}; | |
this.animatingTiles = []; | |
this.requests = []; | |
}; | |
TileCache.maxPerFrame = 2; | |
TileCache.prototype.beginFrame = function(timestamp) { | |
// Update all opacity tweens, removing them from the animating list if they have completed | |
for (var n = 0; n < this.animatingTiles.length; n++) { | |
var tile = this.animatingTiles[n]; | |
if (!tile.opacity.update(timestamp)) { | |
tile.opacity.value = 1; | |
this.animatingTiles.splice(n, 1); | |
n--; | |
} | |
} | |
return this.animatingTiles.length > 0; | |
}; | |
TileCache.prototype.endFrame = function() { | |
// Swap the maps - effectively drops all unused tiles | |
this.tiles = this.usedTiles; | |
this.usedTiles = {}; | |
// Sort and pick the highest priority tile to fetch | |
var anyRequested = false; | |
if (this.requests.length) { | |
this.requests.sort(function(a, b) { | |
return a.priority - b.priority; | |
}); | |
var request = this.requests[0]; | |
var tile = new Tile(request.pyramid.getTile( | |
request.level, request.x, request.y)); | |
this.tiles[request.key] = tile; | |
this.animatingTiles.push(tile); | |
anyRequested = true; | |
} | |
this.requests.length = 0; | |
return anyRequested; | |
}; | |
// Attempt to retrieve a tile from the cache; if not present, queue it | |
TileCache.prototype.getTile = function(pyramid, level, x, y, priority) { | |
var key = pyramid.id + '-' + level + '@' + x + ',' + y; | |
var tile = this.tiles[key]; | |
if (tile) { | |
this.usedTiles[key] = tile; | |
return tile; | |
} | |
this.requests.push({ | |
pyramid: pyramid, | |
level: level, | |
x: x, | |
y: y, | |
key: key, | |
priority: priority | |
}); | |
return null; | |
}; | |
var CanvasRenderer = function(canvas) { | |
this.canvas = canvas; | |
this.ctx = canvas.getContext('2d'); | |
}; | |
CanvasRenderer.prototype.clear = function() { | |
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
}; | |
// Draw a single quad with the given content and opacity at the given bounds with the given source texture coordinates | |
CanvasRenderer.prototype.drawQuad = function(bounds, content, opacity, texCoords) { | |
this.ctx.globalAlpha = opacity; | |
this.ctx.drawImage( | |
content, | |
texCoords.x1, texCoords.y1, | |
texCoords.x2 - texCoords.x1, texCoords.y2 - texCoords.y1, | |
bounds.x, bounds.y, | |
bounds.w, bounds.h); | |
}; | |
// Magical tile layer stack for emitting vertical slices of the pyramid | |
var TileStack = function(pyramid, renderer) { | |
this.pyramid = pyramid; | |
this.renderer = renderer; | |
this.layers = new Array(pyramid.levels.length); | |
for (var n = 0; n < this.layers.length; n++) { | |
this.layers[n] = { | |
x: 0, | |
y: 0, | |
tile: null, | |
// Blending opacity (tile opacity and spatial blending) | |
opacity: 0, | |
// The size of a tile on the screen | |
scaledTileSize: 1 | |
}; | |
} | |
// The current index into the layer stack | |
this.index = -1; | |
// The first level of detail that has a solid tile | |
this.firstLevel = 0; | |
}; | |
// Pushes a new tile onto the stack | |
TileStack.prototype.push = function(level, x, y, tile, blendWeight, scaledTileSize) { | |
this.index = level; | |
var layer = this.layers[level]; | |
layer.x = x; | |
layer.y = y; | |
layer.tile = tile; | |
layer.opacity = blendWeight * (tile ? tile.opacity.value : 0); | |
layer.scaledTileSize = scaledTileSize; | |
if (layer.opacity == 1) { | |
this.firstLevel = this.index; | |
} | |
}; | |
// Pops the last tile from the stack | |
TileStack.prototype.pop = function() { | |
if (this.firstLevel == this.index) { | |
this.firstLevel--; | |
} | |
this.index--; | |
}; | |
// Emits a vertical slice of the pyramid | |
TileStack.prototype.emit = function(screenBounds) { | |
var tileSize = this.pyramid.tileSize; | |
var tileOverlap = this.pyramid.tileOverlap; | |
var border = this.pyramid.border; | |
var texCoords = { x1: 0, y1: 0, x2: 0, y2: 0 }; | |
// Compute the slice region in image space ([0,0]-[1,1]) | |
var topLevel = this.pyramid.levels[this.index]; | |
var topLayer = this.layers[this.index]; | |
var sx = topLayer.x * tileSize / topLevel.pixelWidth; | |
var sy = topLayer.y * tileSize / topLevel.pixelHeight; | |
var sw = topLayer.x < topLevel.tileWidth - 1 ? tileSize : topLevel.pixelWidth - (topLayer.x * tileSize); | |
sw /= topLevel.pixelWidth; | |
var sh = topLayer.y < topLevel.tileHeight - 1 ? tileSize : topLevel.pixelHeight - (topLayer.y * tileSize); | |
sh /= topLevel.pixelHeight; | |
// Compute the slice's position on the screen | |
var bounds = { | |
x: screenBounds.x + topLayer.x * topLayer.scaledTileSize, | |
y: screenBounds.y + topLayer.y * topLayer.scaledTileSize, | |
w: Math.min(topLayer.scaledTileSize, screenBounds.w - topLayer.x * topLayer.scaledTileSize), | |
h: Math.min(topLayer.scaledTileSize, screenBounds.h - topLayer.y * topLayer.scaledTileSize) | |
}; | |
// Run from the first opaque layer to the current layer | |
for (var n = this.firstLevel; n <= this.index; n++) { | |
var levelInfo = this.pyramid.levels[n]; | |
var layer = this.layers[n]; | |
if (!layer.tile) { | |
continue; | |
} | |
// Slice region in level space [0,0]-[level width, level height] | |
var lx = sx * levelInfo.pixelWidth; | |
var ly = sy * levelInfo.pixelHeight; | |
var lw = sw * levelInfo.pixelWidth; | |
var lh = sh * levelInfo.pixelHeight; | |
// Texture coordinates in the tile | |
texCoords.x1 = lx - (layer.x * tileSize); | |
if (layer.x) { | |
texCoords.x1 += tileOverlap; | |
} else { | |
texCoords.x1 += border; | |
} | |
texCoords.y1 = ly - (layer.y * tileSize); | |
if (layer.y) { | |
texCoords.y1 += tileOverlap; | |
} else { | |
texCoords.y1 += border; | |
} | |
texCoords.x2 = texCoords.x1 + lw; | |
texCoords.y2 = texCoords.y1 + lh; | |
// Draw the quad! | |
this.renderer.drawQuad(bounds, layer.tile.content, layer.opacity, texCoords); | |
} | |
}; | |
// An individual item in the scene that references a pyramid | |
var Item = function(pyramid) { | |
this.pyramid = pyramid; | |
// http://blogs.msdn.com/b/lutzg/archive/2009/10/05/exact-map-rendering.aspx | |
this.blurFactor = 1; | |
// Position in scene space | |
this.bounds = { | |
x: 0, | |
y: 0, | |
w: pyramid.width, | |
h: pyramid.height | |
}; | |
}; | |
// Setup the item for rendering | |
Item.prototype.prepare = function(tileCache, renderer) { | |
this.tileCache = tileCache; | |
this.renderer = renderer; | |
this.tileStack = new TileStack(this.pyramid, renderer); | |
// The viewport screen center x,y and scale | |
this.viewportCenterX; | |
this.viewportCenterY; | |
this.viewportScale; | |
// The desired level of detail | |
this.desiredLevel; | |
// The next level of detail (may == desiredLevel) for spatial blending | |
this.nextLevel; | |
// [0-1] blend weight between desiredLevel and nextLevel | |
this.blendFactor; | |
// The visible tile regions in each level of detail | |
this.visibleRegions = new Array(this.pyramid.levels.length); | |
for (var n = 0; n < this.visibleRegions.length; n++) { | |
this.visibleRegions[n] = { | |
l: 0, | |
t: 0, | |
r: 0, | |
b: 0 | |
}; | |
} | |
}; | |
// Draw an item to the screen | |
Item.prototype.draw = function(screenSize, viewport, viewportBounds) { | |
// Get the scene and screen bounds of the item | |
var sceneBounds = this.bounds; | |
var screenBounds = { | |
x: (sceneBounds.x * viewport.scale) - viewport.x, | |
y: (sceneBounds.y * viewport.scale) - viewport.y, | |
w: sceneBounds.w * viewport.scale, | |
h: sceneBounds.h * viewport.scale | |
}; | |
// See if the item is off the screen | |
var offScreen = | |
screenBounds.x > screenSize.w || | |
screenBounds.x + screenBounds.w < 0 | |
screenBounds.y > screenSize.h || | |
screenBounds.y + screenBounds.h < 0; | |
if (offScreen) { | |
return false; | |
} | |
this.viewportCenterX = screenSize.w / 2; | |
this.viewportCenterY = screenSize.h / 2; | |
this.viewportScale = viewport.scale; | |
// Compute the level of detail and associated values | |
var minLevel = this.pyramid.minLevel; | |
var maxLevel = this.pyramid.maxLevel; | |
var contentArea = this.pyramid.width * this.pyramid.height; | |
var screenArea = screenBounds.w * screenBounds.h; | |
screenArea /= (this.blurFactor * this.blurFactor); | |
var lod = maxLevel - Math.log(contentArea / screenArea) / Math.log(4); | |
this.desiredLevel = Math.max(minLevel, Math.min(maxLevel, Math.floor(lod))); | |
this.nextLevel = Math.max(minLevel, Math.min(maxLevel, Math.ceil(lod))); | |
this.blendFactor = this.nextLevel > this.desiredLevel ? lod - this.desiredLevel : 1; | |
// Visible region in image space [0,0]-[1,1] | |
var clipL = Math.max(0, Math.min(1, (viewportBounds.x - sceneBounds.x) / sceneBounds.w)); | |
var clipT = Math.max(0, Math.min(1, (viewportBounds.y - sceneBounds.y) / sceneBounds.h)); | |
var clipR = Math.max(clipL, Math.min(1, (viewportBounds.x + viewportBounds.w - sceneBounds.x) / sceneBounds.w)); | |
var clipB = Math.max(clipT, Math.min(1, (viewportBounds.y + viewportBounds.h - sceneBounds.y) / sceneBounds.h)); | |
// Compute the visible region, in tiles, for each level of detail | |
var tileSize = this.pyramid.tileSize; | |
for (var n = minLevel; n <= maxLevel; n++) { | |
var levelInfo = this.pyramid.levels[n]; | |
var visibleRegion = this.visibleRegions[n]; | |
visibleRegion.l = Math.max(0, Math.min(levelInfo.tileWidth - 1, | |
Math.floor(clipL * levelInfo.pixelWidth / tileSize))); | |
visibleRegion.t = Math.max(0, Math.min(levelInfo.tileHeight - 1, | |
Math.floor(clipT * levelInfo.pixelHeight / tileSize))); | |
visibleRegion.r = Math.max(0, Math.min(levelInfo.tileWidth - 1, | |
Math.floor(clipR * levelInfo.pixelWidth / tileSize))); | |
visibleRegion.b = Math.max(0, Math.min(levelInfo.tileHeight - 1, | |
Math.floor(clipB * levelInfo.pixelHeight / tileSize))); | |
} | |
// Recurse down the pyramid | |
this.recurse(screenBounds, minLevel, 0, 0); | |
}; | |
// Process a single tile in the pyramid and either continue walking down or draw it | |
Item.prototype.recurse = function(screenBounds, level, x, y) { | |
// Compute the tile priority for foveating - basically level + distance from center | |
// A lower priority value is a tile that should be fetched earlier | |
var levelInfo = this.pyramid.levels[level]; | |
var scaledTileSize = Math.min(Math.max(levelInfo.pixelWidth, levelInfo.pixelHeight), this.pyramid.tileSize) * | |
this.viewportScale * Math.pow(2, this.pyramid.maxLevel - level); | |
var tileX = screenBounds.x + x * scaledTileSize + scaledTileSize / 2; | |
var tileY = screenBounds.y + y * scaledTileSize + scaledTileSize / 2; | |
var distanceToCenter = Math.floor(Math.sqrt( | |
(tileX - this.viewportCenterX) * (tileX - this.viewportCenterX) + | |
(tileY - this.viewportCenterY) * (tileY - this.viewportCenterY))); | |
var priority = level * 4294967296 + Math.min(distanceToCenter, 4294967296); | |
// Get the tile from the cache (or request it) | |
var tile = this.tileCache.getTile(this.pyramid, level, x, y, priority); | |
// Push the tile (if found) onto the tile stack | |
var blendWeight = (level == this.nextLevel) ? this.blendFactor : 0.99; | |
this.tileStack.push(level, x, y, tile, blendWeight, scaledTileSize); | |
// If the tile is present, fully temporally blended, and we aren't at the bottom of the pyramid, keep walking | |
if (tile && tile.opacity.value == 1 && level < this.nextLevel) { | |
var nextLevelInfo = this.pyramid.levels[level + 1]; | |
if (nextLevelInfo.tileWidth == 1 && nextLevelInfo.tileHeight == 1) { | |
// Easy case - single tile level | |
this.recurse(screenBounds, level + 1, x, y); | |
} else { | |
// Recurse down into the 4 child tiles, but only if they are within the visible region | |
// This is what culls the pyramid as we descend | |
var visibleRegion = this.visibleRegions[level + 1]; | |
var cx = x * 2; | |
var cy = y * 2; | |
if (cx >= visibleRegion.l && cy >= visibleRegion.t && | |
cx <= visibleRegion.r && cy <= visibleRegion.b) { | |
this.recurse(screenBounds, level + 1, cx, cy); | |
} | |
if (cx + 1 >= visibleRegion.l && cy >= visibleRegion.t && | |
cx + 1 <= visibleRegion.r && cy <= visibleRegion.b) { | |
this.recurse(screenBounds, level + 1, cx + 1, cy); | |
} | |
if (cx >= visibleRegion.l && cy + 1 >= visibleRegion.t && | |
cx <= visibleRegion.r && cy + 1 <= visibleRegion.b) { | |
this.recurse(screenBounds, level + 1, cx, cy + 1); | |
} | |
if (cx + 1 >= visibleRegion.l && cy + 1 >= visibleRegion.t && | |
cx + 1 <= visibleRegion.r && cy + 1 <= visibleRegion.b) { | |
this.recurse(screenBounds, level + 1, cx + 1, cy + 1); | |
} | |
} | |
} else { | |
// No coverage in the pyramid? Draw the current stack! | |
this.tileStack.emit(screenBounds); | |
} | |
this.tileStack.pop(); | |
}; | |
var DemoApp = function() { | |
var self = this; | |
this.tileCache = new TileCache(); | |
this.canvas = document.getElementById('displayCanvas'); | |
this.renderer = new CanvasRenderer(this.canvas); | |
this.items = []; | |
this.viewport = { | |
x: 0, | |
y: 0, | |
scale: 1 | |
}; | |
this.dirty = true; | |
this.requestId = null; | |
this.boundUpdate = function(timestamp) { | |
self.requestId = null; | |
self.update(timestamp || Date.now()); | |
}; | |
var mouseDown = false; | |
var lastMouseX = 0; | |
var lastMouseY = 0; | |
var startingScale = 1; | |
function addMultiEventListeners(target, names, callback) { | |
for (var n = 0; n < names.length; n++) { | |
target.addEventListener(names[n], callback, false); | |
} | |
} | |
addMultiEventListeners(this.canvas, ['mousedown', 'touchstart'], function(e) { | |
mouseDown = true; | |
lastMouseX = e.pageX; | |
lastMouseY = e.pageY; | |
startingScale = self.viewport.scale; | |
self.canvas.style.cursor = 'pointer'; | |
e.preventDefault(); | |
}); | |
addMultiEventListeners(this.canvas, ['mouseup', 'mouseout', 'touchend', 'touchcancel'], function(e) { | |
mouseDown = false; | |
self.canvas.style.cursor = ''; | |
e.preventDefault(); | |
}); | |
addMultiEventListeners(this.canvas, ['mousemove', 'touchmove'], function(e) { | |
if (mouseDown) { | |
var dx = e.pageX - lastMouseX; | |
var dy = e.pageY - lastMouseY; | |
lastMouseX = e.pageX; | |
lastMouseY = e.pageY; | |
if (e.touches && e.touches.length > 1) { | |
var newScale = e.scale * startingScale; | |
self.zoomAroundPoint(e.pageX, e.pageY, newScale); | |
} else { | |
self.viewport.x -= dx; | |
self.viewport.y -= dy; | |
} | |
self.requestAnimationFrame(true); | |
} | |
e.preventDefault(); | |
}); | |
addMultiEventListeners(this.canvas, ['mousewheel', 'DOMMouseScroll'], function(e) { | |
var z = 0; | |
if (e.wheelDelta !== undefined) { | |
z = e.wheelDelta / 120; | |
} else if (e.detail !== undefined) { | |
z = -e.detail / 3; | |
} | |
var newScale = self.viewport.scale; | |
if (z > 0) { | |
newScale *= 1.5; | |
} else { | |
newScale /= 1.5; | |
} | |
self.zoomAroundPoint(e.pageX, e.pageY, newScale); | |
e.preventDefault(); | |
}); | |
function resize() { | |
self.canvas.width = window.innerWidth; | |
self.canvas.height = window.innerHeight; | |
self.requestAnimationFrame(true); | |
}; | |
resize(); | |
window.addEventListener('resize', resize, false); | |
window.addEventListener('orientationchange', resize, false); | |
}; | |
DemoApp.prototype.addItem = function(item) { | |
item.prepare(this.tileCache, this.renderer); | |
this.items.push(item); | |
}; | |
DemoApp.prototype.update = function(timestamp) { | |
var updating = this.dirty; | |
this.dirty = false; | |
this.renderer.clear(); | |
updating = this.tileCache.beginFrame(timestamp) || updating; | |
var screenSize = { | |
w: this.canvas.width, | |
h: this.canvas.height | |
}; | |
var viewportBounds = { | |
x: this.viewport.x / this.viewport.scale, | |
y: this.viewport.y / this.viewport.scale, | |
w: screenSize.w / this.viewport.scale, | |
h: screenSize.h / this.viewport.scale | |
}; | |
for (var n = 0; n < this.items.length; n++) { | |
var item = this.items[n]; | |
updating = item.draw(screenSize, this.viewport, viewportBounds) || updating; | |
} | |
updating = this.tileCache.endFrame() || updating; | |
if (updating) { | |
this.requestAnimationFrame(); | |
} | |
}; | |
DemoApp.prototype.requestAnimationFrame = function(dirty) { | |
if (dirty) { | |
this.dirty = true; | |
} | |
if (this.requestId !== null) { | |
return; | |
} | |
if (window.webkitRequestAnimationFrame) { | |
this.requestId = window.webkitRequestAnimationFrame(this.boundUpdate, this.canvas); | |
} else if (window.mozRequestAnimationFrame) { | |
this.requestId = window.mozRequestAnimationFrame(this.boundUpdate); | |
} else { | |
this.requestId = window.setTimeout(this.boundUpdate, 16); | |
} | |
}; | |
DemoApp.prototype.zoomAroundPoint = function(x, y, scale) { | |
var ox = (this.viewport.x + x) / this.viewport.scale; | |
var oy = (this.viewport.y + y) / this.viewport.scale; | |
var nx = (this.viewport.x + x) / scale; | |
var ny = (this.viewport.y + y) / scale; | |
this.viewport.x -= (nx - ox) * scale; | |
this.viewport.y -= (ny - oy) * scale; | |
this.viewport.scale = scale; | |
this.requestAnimationFrame(true); | |
}; | |
var demo; | |
function initDemo() { | |
demo = new DemoApp(); | |
//var pyramid = new ImagePyramid(128 * 1024 * 1024, 128 * 1024 * 1024, 254, 1, 0); | |
//var pyramid = new ImagePyramid(16 * 1024, 16 * 1024, 510, 1, 0); | |
var pyramid = new ImagePyramid(2147483648, 2147483648, 256, 0, 0); | |
//var pyramid = new ImagePyramid(16 * 1024, 16 * 1024, 256, 0, 0); | |
var item = new Item(pyramid); | |
demo.addItem(item); | |
demo.viewport.scale = demo.canvas.width < demo.canvas.height ? | |
demo.canvas.width / item.bounds.w : | |
demo.canvas.height / item.bounds.h; | |
demo.requestAnimationFrame(true); | |
} | |
function bias(value) { | |
for (var n = 0; n < demo.items.length; n++) { | |
demo.items[n].blurFactor = value; | |
} | |
demo.requestAnimationFrame(true); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment