agentscript
Version:
AgentScript Model in Model/View architecture
218 lines (202 loc) • 6.83 kB
JavaScript
import * as util from './utils.js'
import Shapes from './Shapes.js'
// Sprites are shapes rendered within a sprite-sheet canvas.
class SpriteSheet {
// Initialize a one row by cols initial sheet.
// spriteSize rounded up if a float.
constructor(spriteSize = 64, cols = 16, usePowerOf2 = false) {
spriteSize = Math.ceil(spriteSize) // in integer pixels
Object.assign(this, { spriteSize, cols, usePowerOf2 })
this.rows = 1
this.nextCol = 0
this.nextRow = 0
this.spritesIndex = {} // index to sprites by name
this.sprites = []
this.shapes = new Shapes()
if (usePowerOf2) this.checkPowerOf2()
this.ctx = util.createCtx(this.width, this.height) // offscreen
// THREE texture optional, texture.needsUpdate = true on sheet change
this.texture = null
}
// Get a sprite from cache. Create if not there.
// Currently an alias for newSprite() but may change.
getSprite(shapeName, color, strokeColor) {
return this.newSprite(shapeName, color, strokeColor)
}
// Return a random sprite from the cache.
oneOf() {
return util.oneOf(this.sprites)
}
// Draw the sprite on ctx with given x,y,theta.
// Uses world/patchSize euclidean coords & angle
// Does not use transformed ctx, just canvas pixels.
draw(ctx, sprite, x, y, theta, world, patchSize, noRotate = false) {
const [x0, y0] = world.patchXYtoPixelXY(x, y, patchSize)
const theta0 = -theta
this.drawCanvas(ctx, sprite, x0, y0, theta0, noRotate)
}
// Draw the sprite on ctx with given x,y,theta.
// Uses canvas pixel coords
drawCanvas(ctx, sprite, x, y, theta = 0, noRotate = false) {
const { x: x0, y: y0, size } = sprite
const halfSize = size / 2
if (noRotate) theta = 0
if (theta === 0) {
ctx.drawImage(
this.ctx.canvas,
x0,
y0,
size,
size,
x - halfSize,
y - halfSize,
size,
size
)
} else {
ctx.save()
ctx.translate(x, y)
ctx.rotate(theta)
ctx.drawImage(
this.ctx.canvas,
x0,
y0,
size,
size,
-halfSize,
-halfSize,
size,
size
)
ctx.restore()
}
}
// A sprite identifies a slot in the sheet.
// If sprite in cache, return it. Otherwise create and return it.
newSprite(shapeName, color, strokeColor = null) {
// Create a normalized name. Use shapes (includes size)
const name = this.shapes.imageName(
shapeName,
this.spriteSize,
color,
strokeColor
)
// If sprite of ths name already exists, return it.
if (this.spritesIndex[name]) return this.spritesIndex[name]
// The sprite image
const img = this.shapes.shapeToImage(
shapeName,
this.spriteSize,
color,
strokeColor
)
this.checkSheetSize() // Resize ctx if nextRow === rows
// pixel coords
const [x, y, size] = [this.nextX, this.nextY, this.spriteSize]
this.ctx.drawImage(img, x, y, size, size)
const { nextRow: row, nextCol: col } = this
const sprite = {
name,
id: this.sprites.length,
x,
y,
row,
col,
size,
sheet: this,
}
sprite.uvs = this.getUVs(sprite)
this.incrementRowCol()
// Add sprite to cache and return it.
this.spritesIndex[name] = sprite
this.sprites.push(sprite)
if (this.texture) this.texture.needsUpdate = true
return sprite
}
// getters for derived values.
// width & height in pixels
get width() {
return this.spriteSize * this.cols
}
get height() {
return this.spriteSize * this.rows
}
// next col, row in pixels
get nextX() {
return this.spriteSize * this.nextCol
}
get nextY() {
return this.spriteSize * this.nextRow
}
checkPowerOf2() {
const { width, height } = this
if (!(util.isPowerOf2(width) && util.isPowerOf2(height))) {
throw Error(`SpriteSheet non power of 2: ${width}x${height}`)
}
}
// Adjust for new sheet size if necessary:
checkSheetSize() {
if (this.nextRow === this.rows) {
// this.nextCol should be 0
this.rows = this.usePowerOf2 ? this.rows * 2 : this.rows + 1
// Resizes ctx preserving it's current image
util.resizeCtx(this.ctx, this.width, this.height)
// Recalculate existing sprite uvs.
util.forLoop(this.sprites, sprite => {
sprite.uvs = this.getUVs(sprite)
})
}
}
// Advance nextCol/Row. Done after checkSheetSize enlarged ctx if needed.
incrementRowCol() {
this.nextCol += 1
if (this.nextCol < this.cols) return
this.nextCol = 0
this.nextRow += 1
}
// Return standard agentscript quad:
// 3 2
// -----
// | /|
// | / |
// |/ |
// -----
// 0 1
// I.e. botLeft, botRight, topRight, topLeft
getUVs(sprite) {
// note v's are measured from the bottom.
const { row, col } = sprite
const { rows, cols } = this
const u0 = col / cols
const v0 = (rows - (row + 1)) / rows
const u1 = (col + 1) / cols
const v1 = (rows - row) / rows
// return [[x0, y1], [x1, y1], [x1, y0], [x0, y0]]
return [u0, v0, u1, v0, u1, v1, u0, v1]
}
}
export default SpriteSheet
// Return sprite image coords for drawing: x,y,size,size
// getSpriteCoords(sprite) {
// const { x, y, size } = sprite
// return { x, y, size }
// }
// Given parameters for a sprite, create a name
// spriteName(shapeName, color, strokeColor = null) {
// const path = this.shapes.getPath(shapeName)
// if (!path) throw Error(`spriteName: ${shapeName} not in Shapes`)
//
// if (path.name === 'imagePath') return `${shapeName}_image`
//
// if (!color) {
// throw Error(`spriteName: No color for shape ${shapeName}`)
// }
// // OK to give strokeColor when not needed.
// if (!this.shapes.needsStrokeColor(shapeName)) strokeColor = null
//
// return `${shapeName}_${color}${strokeColor ? `_${strokeColor}` : ''}`
// }
// id = number of sprites
// get id() {
// return Object.keys(this.sprites).length
// }