roar/assets/js/matrix.js

498 lines
12 KiB
JavaScript

function Matrix (w,h,f){
this.x = 0
this.y = 0
this.w = w
this.h = h
this.f = f
this.focus_x = 0
this.focus_y = 0
this.initialize()
}
Matrix.prototype.initialize = function(f){
var w = this.w || 1, h = this.h || 1, f = f || this.f
var aa = new Array (h)
for (var y = 0; y < h; y++) {
aa[y] = new Array (w)
for (var x = 0; x < w; x++) {
aa[y][x] = f(x,y)
}
}
this.aa = aa
}
Matrix.prototype.rebuild = function (){
this.demolish()
this.initialize()
this.append()
this.bind()
this.generate && this.generate()
this.focus_clamp()
check_if_lost_focus()
}
Matrix.prototype.clone = function () {
var base = this
var clone = new Matrix(this.w, this.h, function(x,y){
return base.getCell(x,y).clone()
})
clone.f = this.f
return clone
}
Matrix.prototype.assign = function (mat) {
var base = this
this.demolish()
this.w = mat.w
this.h = mat.h
// this.f = function(){}
this.initialize(function(x,y){
var el = mat.getCell(x,y).clone()
el.build()
return el
})
this.append()
this.bind()
check_if_lost_focus()
return this
}
Matrix.prototype.bind = function () {}
Matrix.prototype.demolish = function (){
this.forEach(function(lex){
lex.demolish()
})
while (this.wrapper && this.wrapper.firstChild) {
this.wrapper.removeChild(this.wrapper.firstChild);
}
this.aa.forEach(function(row){
row.length = 0
})
this.aa.length = 0
}
Matrix.prototype.forEach = function(f){
this.aa.forEach(function(row, y){
row.forEach(function(lex, x){
f(lex, x, y)
})
})
}
Matrix.prototype.focus_clamp = function(){
this.focus_x = clamp(this.focus_x, 0, this.w - 1)
this.focus_y = clamp(this.focus_y, 0, this.h - 1)
}
Matrix.prototype.focus_add = function(x, y){
this.focus(this.focus_x + x, this.focus_y + y)
}
Matrix.prototype.focus = function(x, y){
if (x === undefined) x = this.focus_x
if (y === undefined) y = this.focus_y
x = mod(x, this.w)
y = mod(y, this.h)
this.focus_x = x
this.focus_y = y
//focused_input = this
this.aa[y][x].focus()
}
Matrix.prototype.focusLex = function(y,x){
if (x < 0) {
y -= 1
}
if (x > this.aa[0].length) {
y += 1
}
this.aa[mod(y,this.h)][mod(x,this.w)].focus()
}
Matrix.prototype.clear = function(){
this.forEach(function(lex,x,y){ lex.clear() })
}
Matrix.prototype.erase = function(){
this.forEach(function(lex,x,y){ lex.erase() })
}
Matrix.prototype.fill = function(lex){
this.fg = lex.fg
this.bg = lex.bg
this.char = lex.char
this.opacity = lex.opacity
this.forEach(function(el,x,y){
el.assign(lex)
el.build()
})
}
Matrix.prototype.build = function(){
this.forEach(function(lex,x,y){
lex.build()
})
}
Matrix.prototype.append = function(wrapper){
wrapper = this.wrapper = wrapper || this.wrapper
if (! this.wrapper) return
this.aa.forEach(function(row, y){
var div = document.createElement("div")
row.forEach(function(lex, x) {
div.appendChild(lex.span)
})
wrapper.appendChild( div )
})
}
Matrix.prototype.region = function(w,h,x,y) {
w = w || 1
h = h || 1
x = x || 0
y = y || 0
var parent = this
var mat = new Matrix(w, h, function(x,y){
return parent.aa[y][x]
})
mat.f = this.f
return mat
}
Matrix.prototype.setCell = function(lex,x,y){
this.aa[y] && this.aa[y][x] && this.aa[y][x].assign(lex)
}
Matrix.prototype.getCell = function(x,y){
if (this.aa[y] && this.aa[y][x]) return this.aa[y][x]
else return null
}
Matrix.prototype.get = function(x,y){
y = floor(mod(y || 0, this.h))
x = floor(mod(x || 0, this.w))
if (this.aa[y] && this.aa[y][x]) return this.aa[y][x]
else return null
}
Matrix.prototype.resize = function(w,h){
w = w || canvas.w
h = h || canvas.h
var div, row, lex
var f = this.f, old_h = this.aa.length, old_w = this.aa[0].length
var wrapper = this.wrapper
w = max(w, 1)
h = max(h, 1)
if (h < old_h) {
for (var y = old_h; y > h; y--) {
row = this.aa.pop()
div = row[0].span.parentNode
row.forEach(function(lex, x){
lex.demolish()
})
div.parentNode.removeChild(div)
}
}
else if (h > old_h) {
for (var y = old_h; y < h; y++) {
div = document.createElement("div")
wrapper.appendChild( div )
this.aa[y] = new Array (w)
for (var x = 0; x < w; x++) {
lex = this.aa[y][x] = f(x,y)
div.appendChild(lex.span)
}
}
}
if (w < old_w) {
this.aa.forEach(function(row, y){
while (row.length > w) {
lex = row.pop()
lex.demolish()
}
})
}
else if (w > old_w) {
this.aa.forEach(function(row, y){
div = row[0].span.parentNode
for (var x = row.length; x < w; x++) {
lex = row[x] = f(x,y)
div.appendChild(lex.span)
}
})
}
this.w = w
this.h = h
this.bind && this.bind()
this.focus_clamp()
if (this.wrapper && this.wrapper.parentNode != document.body) {
this.resize_wrapper()
}
}
Matrix.prototype.resize_wrapper = function(){
var cell = canvas.aa[0][0].span
var cw = cell.offsetWidth
var ch = cell.offsetHeight
// if (canvas.grid) { ch++ }
var width = cw * this.aa[0].length
var height = ch * this.aa.length
if (canvas.grid) { width++; height++ }
this.wrapper.parentNode.style.height = ""
this.wrapper.style.width =
this.wrapper.parentNode.style.width = (width) + "px"
this.wrapper.style.top = ""
}
Matrix.prototype.ascii = function () {
var lines = this.aa.map(function(row, y){
var last, line = ""
row.forEach(function(lex, x) {
line += lex.ascii()
})
return line // .replace(/\s+$/,"")
})
var txt = lines.join("\n")
return txt
}
Matrix.prototype.mirc = function (opts) {
var cutoff = false
var lines = this.aa.map(function(row, y){
var last, line = ""
row.forEach(function(lex, x) {
var bg_ = -1, fg_ = 15
if (lex.eqColor(last)) {
line += lex.sanitize()
}
else {
[bg_, fg_, line_] = lex.mirc(bg_, fg_)
line += line_; last = lex;
}
})
if (opts && opts.cutoff && line.length > opts.cutoff) {
cutoff = true
}
return line
})
var txt = lines.filter(function(line){ return line.length > 0 }).join('\n')
if (cutoff) {
txt = new String(txt)
txt.cutoff = true
}
return txt
}
var undo = (function(){
var max_states = 200;
// undotimetotal = 0;
var stack = {undo: [], redo: []};
var current_undo = null;
var dom = {undo: undo_el, redo: redo_el};
dom.undo.is_visible = dom.redo.is_visible = false
var LexState = function(lex){
this.fg = lex.fg;
this.bg = lex.bg;
this.char = lex.char;
this.opacity = lex.opacity;
};
var update_dom_visibility = function(type){
var el = dom[type]
if (el.is_visible){
if (stack[type].length === 0) {
el.classList.add('hidden')
el.is_visible = false
}
} else if (stack[type].length > 0){
el.classList.remove('hidden')
el.is_visible = true
}
}
var update_dom = function(){
update_dom_visibility('undo')
update_dom_visibility('redo')
}
// state is an undo or redo state that might contain these props
// { lexs: {'0,0': LexState, ...}, // for sparse lex changes (eg brush, fill)
// focus: {x:, y: },
// size: {w:, h: },
// rects: [{x:, y:, w:, h:, lexs: [LexState, ...]}, ...]
// }
var new_state = function(){
var state = {lexs:{}};
save_focus(canvas.focus_x, canvas.focus_y, state)
return state
}
var new_redo = function(){
return new_state()
}
var new_undo = function(){
current_undo = new_state()
stack.redo = []
stack.undo.push(current_undo)
if (stack.undo.length > max_states) stack.undo.shift();
update_dom()
return current_undo
}
var save_focus = function(x, y, state){
state = state || current_undo
state.focus = {x:x, y:y}
}
var save_size = function(w, h, state){
state = state || current_undo
state.size = {w:w, h:h};
}
// the reason for stringifying the x y coords is so that each
// coordinate is saved only once in an undo state.
// otherwise there would be problems with, eg, a brush stroke
// that passed over the same grid cell twice.
var save_lex = function(x, y, lex, state){
// var start = Date.now()
state = state || current_undo
var lexs = state.lexs;
var xy = x + "," + y;
if (xy in lexs) return;
lexs[xy] = new LexState(lex)
// undotimetotal += Date.now() - start
}
var save_focused_lex = function(state){
state = state || current_undo
var x = canvas.focus_x
var y = canvas.focus_y
save_lex(x, y, canvas.aa[y][x], state)
}
var save_rect = function(xpos, ypos, w, h, state){
if (w === 0 || h === 0) return;
state = state || current_undo;
state.rects = state.rects || []
var aa = canvas.aa;
var rect = {x: xpos, y: ypos, w: w, h: h, lexs: []}
var lexs = rect.lexs
var xlen = xpos + w
var ylen = ypos + h
for (var y = ypos; y < ylen; y++){
var aay = aa[y]
for (var x = xpos; x < xlen; x++){
lexs.push(new LexState(aay[x]))
}
}
state.rects.push(rect)
}
var save_resize = function(w, h, old_w, old_h, state){
state = state || current_undo
save_size(old_w, old_h, state)
if (old_w > w){
// .---XX
// | XX
// |___XX
save_rect(w, 0, old_w - w, old_h, state)
if (old_h > h){
// .----.
// | |
// XXXX_|
save_rect(0, h, w, old_h - h, state)
}
} else if (old_h > h){
// .----.
// | |
// XXXXXX
save_rect(0, h, old_w, old_h - h, state)
}
}
var restore_state = function(state){
// all redo states will have a cached undo state on them
// an undo state might have a cached redo state
// if it doesn't have one, generate one
var make_redo = ! ('redo' in state || 'undo' in state);
var aa = canvas.aa
var lex, lexs;
if (make_redo){
state.redo = new_redo()
// copy saved rects that intersect with current canvas size
// important to do this before resizing canvas
if ('rects' in state){
for (var ri=0, rect; rect=state.rects[ri]; ri++){
if (rect.x >= canvas.w ||
rect.y >= canvas.h) continue;
var w = Math.min(rect.w, canvas.w - rect.x)
var h = Math.min(rect.h, canvas.h - rect.y)
save_rect(rect.x, rect.y, w, h, state.redo)
}
}
if ('size' in state){
save_resize(state.size.w, state.size.h, canvas.w, canvas.h, state.redo)
}
}
if ('size' in state){
canvas.resize(state.size.w, state.size.h, true);
}
if ('rects' in state){
for (var ri=0, rect; rect=state.rects[ri]; ri++){
lexs = rect.lexs
for (var li=0; lex=lexs[li]; li++){
var x = (li % rect.w) + rect.x
var y = ((li / rect.w)|0) + rect.y
aa[y][x].assign(lex)
}
}
}
lexs = state.lexs
for (var key in lexs){
var xy = key.split(',');
lex = aa[xy[1]][xy[0]]
if (make_redo)
save_lex(xy[0], xy[1], lex, state.redo)
lex.assign(lexs[key])
}
if ('focus' in state){
canvas.focus_x = state.focus.x
canvas.focus_y = state.focus.y
if (current_canvas === canvas){
canvas.focus()
}
}
}
var undo = function(){
var state = stack.undo.pop();
if (!state) return;
restore_state(state)
// now take the applied undo state and store it on the redo state
// and push the redo state to the redo stack
state.redo.undo = state
stack.redo.push(state.redo)
delete state.redo
update_dom()
}
var redo = function(){
var state = stack.redo.pop();
if (!state) return;
restore_state(state)
state.undo.redo = state
stack.undo.push(state.undo)
delete state.undo
update_dom()
}
return {
stack: stack,
new: new_undo,
// new_redo: new_redo,
save_focus: save_focus,
save_size: save_size,
save_lex: save_lex,
save_focused_lex: save_focused_lex,
save_rect: save_rect,
save_resize: save_resize,
undo: undo,
redo: redo
}
})()