commit 690ccce89b7cf67b3c7c33f56ebf459083ca3ecc Author: maxime Date: Sat Jan 4 01:39:33 2020 +0000 first commit, working abalone diff --git a/Abalone.class.js b/Abalone.class.js new file mode 100644 index 0000000..58d71df --- /dev/null +++ b/Abalone.class.js @@ -0,0 +1,310 @@ +class Abalone { + constructor(layers) { + this.id = 0; + this.secret = "lol"; + this.players = []; + this.turn = 0; + + // State : -1 => Not Started, 0 => Ended, 1 => Running + this.state = -1; + this.moves = []; + this.board = false; + this.layers = layers; + this.colors = settings.colors.background; + this.pieces = []; + this.texts = {}; + + this.draw(); + + this.board = new Board(this.layers.board, this.layers.pieces); + } + + start() { + var p0_name = prompt('1st player name?'); + if (p0_name.length <= 0) { + p0_name = 'Player 0'; + } + var p1_name = prompt('2nd player name?'); + if (p1_name.length <= 0) { + p1_name = 'Player 1'; + } + var slot = false; + + this.add_player(new Player(0, p0_name)); + this.add_player(new Player(1, p1_name)); + + var starting_position = this.starting_position( + document.getElementById('startpos').value); + for (let player_id in starting_position) { + for (let coord_id in starting_position[player_id]) { + let coord = starting_position[player_id][coord_id]; + slot = this.board.get_slot(coord); + let piece = new Piece( + this.pieces.length, + this.players[player_id], + slot.x, slot.y, slot.r - 2, slot.rot, slot.o, + coord, + this + ); + this.pieces.push(piece); + } + } + + this.board.draw_pieces(this.pieces); + + this.state = 1; + this.play(); + } + + next_turn() { + this.turn++; + this.texts.turn.text(this.turn); + this.layers.background.draw(); + this.texts.turn.draw(); + this.play(); + } + + play() { + if (this.moves.length > 0) { + console.log(this.moves[this.moves.length - 1]); + } + if (this.turn % 2 == 0) { + this.moves.push( new Move(this.players[0], this)); + alert('Player 1, it is your turn'); + } + else { + this.moves.push( new Move(this.players[1], this)); + alert('Player 2, it is your turn'); + } + } + + draw() { + var rect = new Kinetic.Rect({ + x : 0, + y : 0, + width : 800, + height : 500, + stroke : this.colors.stroke, + fill : this.colors.bg + }); + + var text_cont = new Kinetic.Group({ + x : 800-160, + y : 10, + width : 150, + height : 200, + }); + + + var text_rect = new Kinetic.Rect({ + x : 0, + y : 0, + width : 150, + height : 200, + stroke : '#010101', + fill : '#F0F0F0' + }); + + text_cont.add(text_rect); + + var title_text = new Kinetic.Text({ + x : 5, + y : 5, + width : 150, + height : 30, + text : 'Abalone', + fill : 'red', + fontSize : 25 + }); + var turn_text = new Kinetic.Text({ + x : 5, + y : 35, + width : 150, + height : 30, + text : 'Turn : ', + fill : 'red', + fontSize : 25 + }); + var turn_value = new Kinetic.Text({ + x : 95, + y : 35, + width : 50, + height : 30, + text : this.turn, + fill : 'red', + fontSize : 25, + align : 'right' + }); + this.texts.turn = turn_value; + + var p0_text = new Kinetic.Text({ + x : 5, + y : 65, + width : 150, + height : 30, + text : '', + fill : 'white', + stroke : 'black', + fontSize : 25 + }); + this.texts.p0_name = p0_text; + var p0_value = new Kinetic.Text({ + x : 95, + y : 65, + width : 50, + height : 30, + text : '0', + fill : 'white', + stroke : '#101010', + fontSize : 25, + align : 'right' + }); + this.texts.p0_score = p0_value; + + var p1_text = new Kinetic.Text({ + x : 5, + y : 95, + width : 150, + height : 30, + text : '', + fill : 'black', + stroke : '#101010', + fontSize : 25 + }); + this.texts.p1_name = p1_text; + var p1_value = new Kinetic.Text({ + x : 95, + y : 95, + width : 50, + height : 30, + text : '0', + fill : 'black', + stroke : 'black', + fontSize : 25, + align : 'right' + }); + this.texts.p1_score = p1_value; + + text_cont.add(title_text); + text_cont.add(turn_text); + text_cont.add(turn_value); + text_cont.add(p0_text, p0_value, p1_text, p1_value); + + + this.layers.background.add(rect) + this.layers.background.add(text_cont) + return rect + } + + get_positions() { + console.log(this.players[0]); + return [ + this.players[0].get_positions(), + this.players[1].get_positions() + ] + } + + starting_position(name=false) { + switch (name) { + case 'margbelg': + return [ [ + {'x': 'A', 'y': '1'}, + {'x': 'A', 'y': '2'}, + {'x': 'B', 'y': '1'}, + {'x': 'B', 'y': '2'}, + {'x': 'B', 'y': '3'}, + {'x': 'C', 'y': '2'}, + {'x': 'C', 'y': '3'}, + {'x': 'G', 'y': '7'}, + {'x': 'G', 'y': '8'}, + {'x': 'H', 'y': '7'}, + {'x': 'H', 'y': '8'}, + {'x': 'H', 'y': '9'}, + {'x': 'I', 'y': '8'}, + {'x': 'I', 'y': '9'}, + ], [ + {'x': 'A', 'y': '4'}, + {'x': 'A', 'y': '5'}, + {'x': 'B', 'y': '4'}, + {'x': 'B', 'y': '5'}, + {'x': 'B', 'y': '6'}, + {'x': 'C', 'y': '5'}, + {'x': 'C', 'y': '6'}, + {'x': 'G', 'y': '4'}, + {'x': 'G', 'y': '5'}, + {'x': 'H', 'y': '4'}, + {'x': 'H', 'y': '5'}, + {'x': 'H', 'y': '6'}, + {'x': 'I', 'y': '5'}, + {'x': 'I', 'y': '6'}, + ] ]; + } + return [ [ + {'x': 'A', 'y': '1'}, + {'x': 'A', 'y': '2'}, + {'x': 'A', 'y': '3'}, + {'x': 'A', 'y': '4'}, + {'x': 'A', 'y': '5'}, + {'x': 'B', 'y': '1'}, + {'x': 'B', 'y': '2'}, + {'x': 'B', 'y': '3'}, + {'x': 'B', 'y': '4'}, + {'x': 'B', 'y': '5'}, + {'x': 'B', 'y': '6'}, + {'x': 'C', 'y': '2'}, + {'x': 'C', 'y': '3'}, + {'x': 'C', 'y': '4'}, + {'x': 'C', 'y': '5'}, + {'x': 'C', 'y': '6'}, + ], [ + {'x': 'I', 'y': '5'}, + {'x': 'I', 'y': '6'}, + {'x': 'I', 'y': '7'}, + {'x': 'I', 'y': '8'}, + {'x': 'I', 'y': '9'}, + {'x': 'H', 'y': '4'}, + {'x': 'H', 'y': '5'}, + {'x': 'H', 'y': '6'}, + {'x': 'H', 'y': '7'}, + {'x': 'H', 'y': '8'}, + {'x': 'H', 'y': '9'}, + {'x': 'G', 'y': '4'}, + {'x': 'G', 'y': '5'}, + {'x': 'G', 'y': '6'}, + {'x': 'G', 'y': '7'}, + {'x': 'G', 'y': '8'}, + ] ]; + } + + cur_move() { + return this.moves[this.moves.length-1]; + } + + isplayerturn(player_id) { + return player_id == (this.turn % 2); + } + + add_player(player) { + this.players.push(player); + this.texts['p' + player.id +'_name'].text(player.name); + this.layers.background.draw(); + } + + add_point(piece) { + if (piece.player.id < 1) { + this.players[1].score++; + this.texts.p1_score.text(this.players[1].score); + if (this.players[1].score === 6) { + alert(this.players[1].name + ' WINS!'); + } + } + else { + this.players[0].score++; + this.texts.p0_score.text(this.players[0].score); + if (this.players[0].score === 6) { + alert(this.players[0].name + ' WINS!'); + } + } + this.layers.background.draw(); + } +} diff --git a/Board.class.js b/Board.class.js new file mode 100644 index 0000000..cfc43e1 --- /dev/null +++ b/Board.class.js @@ -0,0 +1,205 @@ +class Board { + constructor(layer, layer_pieces) { + this.grid = { + A:{1:[],2:[],3:[],4:[],5:[]}, //Plateau de jeu + B:{1:[],2:[],3:[],4:[],5:[],6:[]}, + C:{1:[],2:[],3:[],4:[],5:[],6:[],7:[]}, + D:{1:[],2:[],3:[],4:[],5:[],6:[],7:[],8:[]}, + E:{1:[],2:[],3:[],4:[],5:[],6:[],7:[],8:[],9:[]}, + F:{2:[],3:[],4:[],5:[],6:[],7:[],8:[],9:[]}, + G:{3:[],4:[],5:[],6:[],7:[],8:[],9:[]}, + H:{4:[],5:[],6:[],7:[],8:[],9:[]}, + I:{5:[],6:[],7:[],8:[],9:[]} + }; + + this.colors = settings.colors.board; + this.colors_hole = settings.colors.hole; + this.colors_p0 = settings.colors.piece_0; + this.colors_p1 = settings.colors.piece_1; + + this.layer = layer; + this.layer_pieces = layer_pieces; + + this.draw(); + this.draw_slots(); + } + + draw() { + console.log('Board draw'); + var hexagon = new Kinetic.RegularPolygon({ + sides : 6, + radius : settings.radius, + stroke : this.colors.stroke, + strokeWidth : 5, + fill : this.colors.bg, + x : settings.x, + y : settings.y, + }) + hexagon.rotate(Math.PI/6) + + this.layer.add(hexagon) + return hexagon + } + + draw_slots() { + var grid = this.grid; + var colors = this.colors_hole + var x = settings.x; + var y = settings.y; + var radius = settings.radius; + var r = settings.r; + var hor = ["A","B","C","D","E","F","G","H","I"]; + var dia = ["1","2","3","4","5","6","7","8","9"]; + var h = 4; + var d = 4; + var rad = -1; + var off = -1; + var offx = -1; + var radx = -1; + var slot = false; + + // center circle + slot = new Slot(x, y, r, 0, 0, + { x: hor[h], y: dia[d] }); + + grid[hor[h]][dia[d]].push(slot); + // we turn around the center, each "i" + // is a sixth of PI/2 + for (var i = 0; i < 6; i++) { + rad = i * Math.PI / 3; + + // center coordinates + h = 4 + d = 4 + + for (var j = 0; j < 4; j++) { + off = (j + 1) * (2 * r + (radius / 20)); + switch (i) { + case 1: case 2: + h--; + break; + case 4: case 5: + h++; + break; + } + switch (i) { + case 0: case 1: + d--; + break; + case 3: case 4: + d++; + break; + } + + slot = new Slot(x, y, r, rad, off, + { x: hor[h], y: dia[d] }); + + grid[hor[h]][dia[d]].push(slot); + + var h_ = h + var d_ = d + for (var k = 0; k < j; k++) { + + offx = Math.sqrt(Math.pow(j, 2) + j + 1); + radx = Math.atan(Math.sqrt(3) / (j * 2 + 1)); + if (k >= (j / 2)) { + radx = Math.PI/3 - radx; + } + else if (k == 1 && j == 3) { + offx = 2 * Math.sqrt(3); + radx = Math.atan(Math.sqrt(3) / 3); + } + + radx += (i * Math.PI / 3); + + switch (i) { + case 2: + case 3: + h_++; + break; + case 0: + case 5: + h_--; + break; + } + switch (i) { + case 1: case 2: + d_++; + break; + case 4: case 5: + d_--; + break; + } + + slot = new Slot(x, y, r, radx, + offx * off / (j + 1), + { x: hor[h_], y: dia[d_] }); + + grid[hor[h_]][dia[d_]].push(slot); + } + } + } + + for (let g in grid) { + for (let row in grid[g]) { + this.layer.add(grid[g][row][0].draw()); + } + } + } + + draw_pieces(pieces) { + let grid = this.grid; + let coord = false; + let slot = false; + for (let i in pieces) { + coord = pieces[i].coord; + grid[coord.x][coord.y][0].element.setZIndex(1); + grid[coord.x][coord.y][0].element.draw(); + grid[coord.x][coord.y].push(pieces[i]); + grid[coord.x][coord.y][1].draw(); + this.layer_pieces.add(grid[coord.x][coord.y][1].element); + grid[coord.x][coord.y][1].element.setZIndex(10); + grid[coord.x][coord.y][1].element.draw(); + + } + } + + get_coord(coord) { + if (this.grid.hasOwnProperty(coord.x) == + false) { + console.log('Undefined line :' + coord.x); + return false; + } + if (this.grid[coord.x].hasOwnProperty(coord.y) == + false) { + console.log('Undefined column of line ' + coord.x + ' :' + coord.y); + return false; + } + if (!(this.grid[coord.x][coord.y] instanceof Array)) { + console.log(this.grid[coord.x][coord.y]); + console.log('is not array'); + return false; + } + + return this.grid[coord.x][coord.y]; + } + pop_coord(coord) { + if (this.get_coord(coord) !== false) { + return this.get_coord(coord).pop(); + } + return false; + } + push_coord(coord, slot) { + if (this.get_coord(coord) !== false) { + return this.get_coord(coord).push(slot); + } + return false; + } + + get_slot(coord) { + if (this.get_coord(coord) !== false) { + return this.get_coord(coord)[0]; + } + return false; + } +} diff --git a/Move.class.js b/Move.class.js new file mode 100644 index 0000000..86e5f85 --- /dev/null +++ b/Move.class.js @@ -0,0 +1,228 @@ +class Move { + constructor(player, abalone) { + this.id = -1; + this.player = player; + this.abalone = abalone; + this.type = -1; // 1 marble / 2 column / 3 line + this.done = false; + this.repr = "" + this.pieces = [] + this.direction = "" + } + + add_piece(piece) { + if (piece.player !== this.player) { + // Piece is not owned by current player + return false; + } + else if (this.pieces.length >= 3) { + // Maximum pieces by move reached + return false; + } + else if (this.pieces.indexOf(piece) != -1) { + // Piece is already selected + return false; + } + else if (this.pieces.length == 0) { + // Move set is empty + this.pieces.push(piece); + return true; + } + else if ( + Slot.are_aligned(this.pieces.concat([piece]))) { + this.pieces.push(piece); + return true; + } + return false; + } + + del_piece(piece) { + let i = this.pieces.indexOf(piece); + if (i == -1) { + return false; + } + this.pieces.splice(i, 1); + return true; + } + + set_direction(dir) { + if (['nw','ne','e','se','sw','w'].indexOf(dir) == -1) { + return false; + } + this.direction = dir; + return true; + } + + set_type() { + this.sort_pieces(); + if (this.pieces.length < 1) { + this.type = -1; + return; + } + else if (this.pieces.length == 1) { + this.type = 1; + return; + } + let coord = this.pieces[1].get_direction_coord(this.direction); + if (coord == false) { + this.type = -1; + return; + } + else if (coord.x == this.pieces[0].coord.x && + coord.y == this.pieces[0].coord.y) { + this.type = 2; + return; + } + else { + this.type = 3; + return; + } + } + + is_possible() { + this.set_type(); + if (this.type == -1) { + alert('Select at least one piece'); + return false; + } + else if (this.pieces.length > 3) { + alert('Too much marbles are selected'); + } + + for (let piece of this.pieces) { + let coord = piece.get_direction_coord(this.direction); + if (typeof(coord) == 'undefined') { + return false; + } + let next_slot = this.abalone.board.get_coord(coord); + if (typeof(next_slot) != 'undefined' && next_slot.length > 1) { + // A piece is located in the next slot + if (this.pieces.indexOf(next_slot[1])) { + // The next slot is already in the move + continue; + } + else if (next_slot[1].player == this.player && + piece == this.pieces[0]) { + // There is another piece of the current player + // It means that we can't move + return false; + } + else { + // This is a sumito case + if (this.sumito()) { + this.sort_pieces(); + return this.move_pieces(); + } + } + } + else { + // The slot is free, so we move all the pieces in this direction + return this.move_pieces(); + } + } + + } + + move_pieces() { + for (let piece of this.pieces) { + this.abalone.board.pop_coord(piece.coord); + let slot = this.abalone.board.get_slot(piece.coord); + let new_coord = piece.get_direction_coord(this.direction); + if (this.abalone.board.get_slot(new_coord) != false) { + piece.new_coord = new_coord; + } + else { + piece.new_coord = false; + piece.out = true; + } + } + + for (let piece of this.pieces) { + if (piece.out == true || piece.new_coord == false) { + this.abalone.add_point(piece); + piece.set_out(); + continue; + } + let next_slot = this.abalone.board.get_slot(piece.new_coord); + if (next_slot == false || typeof(next_slot) == 'undefined') { + break; + } + + this.abalone.board.push_coord(piece.new_coord, piece); + piece.coord = piece.new_coord; + piece.move(next_slot.element.position(), + next_slot.element.rotation(), + next_slot.element.offsetX()); + if (piece.selected) { + piece.set_selected(); + } + } + + this.abalone.layers.pieces.draw(); + return true; + } + + sumito() { + if (this.type != 2) { + return false; + } + + let new_coord = this.pieces[0].get_direction_coord(this.direction); + + let next_slot = this.abalone.board.get_coord(new_coord); + if (next_slot.length <= 1) { + // Free slot! - should not happen in this function + return true; + } + let enemy_piece = next_slot[1]; + new_coord = enemy_piece.get_direction_coord(this.direction); + next_slot = this.abalone.board.get_coord(new_coord); + if (next_slot == false || next_slot.length <= 1) { + // It's outside! || it's free + this.pieces.unshift(enemy_piece); + return true; + } + else if (this.pieces.length <= 2) { + // We don't have enough pieces to face 2 enemy marbles + return false; + } + else { + this.pieces.unshift(enemy_piece); + } + enemy_piece = next_slot[1]; + new_coord = enemy_piece.get_direction_coord(this.direction); + next_slot = this.abalone.board.get_coord(new_coord); + if (next_slot == false || next_slot.length <= 1) { + this.pieces.unshift(enemy_piece); + return true; + } + return false; + } + + sort_pieces() { + let sort_function = false; + switch (this.direction[0]) { + case 'e': + sort_function = function (a, b) { + return parseInt(b.coord.y) - parseInt(a.coord.y); + } + break; + case 'w': + sort_function = function (a, b) { + return parseInt(a.coord.y) - parseInt(b.coord.y); + } + break; + case 'n': + sort_function = function (a, b) { + return a.coord.x.charCodeAt() - b.coord.x.charCodeAt(); + } + break; + case 's': + sort_function = function (a, b) { + return b.coord.x.charCodeAt() - a.coord.x.charCodeAt(); + } + break; + } + this.pieces.sort(sort_function); + } +} diff --git a/Piece.class.js b/Piece.class.js new file mode 100644 index 0000000..0612a1f --- /dev/null +++ b/Piece.class.js @@ -0,0 +1,91 @@ +class Piece extends Slot { + constructor(id, player, x, y, r, rot, o, coord, abalone) { + super(x, y, r, rot, o, coord) + this.id = id; + this.player = player; + this.player.pieces.push(this); + this.abalone = abalone; + this.zIndex = 10; + this.out = false; + this.colors = settings.colors['piece_'+player.id]; + this.strokeWidth = 5; + this.selected = false; + this.hovered = false; + this.new_coord = false; + + } + + click() { + super.click(); + + if (typeof(this.abalone) == 'undefined' || + this.abalone.state !== 1) { + alert('The game is not running!'); + } + if (this.selected) { + // unselect + if (this.abalone.cur_move().del_piece(this)) { + this.set_selected(); + return true; + } + } + else if (this.abalone.cur_move().add_piece(this)) { + console.log(this.abalone.cur_move()); + this.set_selected(); + return true; + } + return false; + } + + mouseenter() { + if (typeof(this.abalone) == 'undefined' || + this.abalone.state !== 1) { + alert('The game is not running!'); + return; + } + + if (!this.abalone.isplayerturn(this.player.id)) { + return; + } + this.hovered = true; + this.element.stroke(this.colors.selected); + this.element.draw(); + } + + mouseleave(element) { + if (typeof(this.abalone) == 'undefined' || + this.abalone.state !== 1) { + alert('The game is not running!'); + return; + } + + if (this.selected || !this.hovered) { + return; + } + + this.hovered = false; + element.stroke(this.colors.stroke); + element.draw(); + } + + set_coord(coord) { + this.coord = coord; + } + + set_selected() { + if (this.selected != false) { + this.selected = false; + this.element.stroke(this.colors.stroke); + this.element.draw(); + return; + } + + this.selected = true; + this.element.stroke(this.colors.selected); + this.element.draw(); + } + + set_out() { + this.element.hide(); + } +} diff --git a/Player.class.js b/Player.class.js new file mode 100644 index 0000000..6effb6a --- /dev/null +++ b/Player.class.js @@ -0,0 +1,15 @@ +class Player { + constructor(id, name) { + this.id = id; + this.name = name; + this.pieces = []; + this.colors = settings.colors['piece_' + id]; + this.score = 0; + } + + get_positions() { + return this.pieces.map(function (piece) { + return piece.coord; + }); + } +} diff --git a/Slot.class.js b/Slot.class.js new file mode 100644 index 0000000..aaba329 --- /dev/null +++ b/Slot.class.js @@ -0,0 +1,162 @@ +class Slot { + constructor(x, y, r, rot, o, coord) { + this.colors = settings.colors.slot; + this.x = x; + this.y = y; + this.r = r; + this.rot = rot; + this.o = o; + this.coord = coord; + this.zIndex = 1; + this.player = false; + this.element = false; + this.piece = false; + } + + draw() { + let _this = this; + let circle = new Kinetic.Circle({ + layer : true, + stroke : this.colors.stroke, + strokeWidth : this.strokeWidth, + fill : this.colors.bg, + x : this.x, + y : this.y, + radius : this.r, + rotation : this.rot, + offset :{ + x : this.o, + y : 0 + } + }).on('click', function (e) { + _this.click(); + }).on('mouseenter', function (e) { + _this.mouseenter(); + }).on('mouseleave', function (e) { + _this.mouseleave(this); + }).on('mouseover', function (e) { + _this.over(); + }, function(e) { + }); + this.element = circle; + return this.element; + } + + click() { + } + mouseenter() { + } + mouseleave() { + } + over() { + //console.log('Slot coord : ' + JSON.stringify(this.coord)); + } + + move(pos, rotation, offx) { + this.element.position(pos); + this.element.rotation(rotation); + this.element.offsetX(offx); + this.element.draw(); + } + + is_sibling(slot) { + let x0_val = this.coord.x.charCodeAt(); + let x1_val = slot.coord.x.charCodeAt(); + let y0_val = parseInt(this.coord.y); + let y1_val = parseInt(slot.coord.y); + + let e_val = 'E'.charCodeAt(); + + console.log(x0_val + ':' + y0_val); + console.log(x1_val + ':' + y1_val); + if (x0_val === x1_val) { + return Math.abs(y0_val - y1_val) === 1; + } + else if (Math.abs(x0_val - x1_val) === 1) { + return (y0_val - y1_val) <= 0; + } + return false; + } + + count_siblings(slots) { + let res = 0; + for (let slot of slots) { + if (this.is_sibling(slot)) { + res++; + } + } + console.log(res); + return res; + } + + get_direction_coord(direction) { + let res = {'x': this.coord.x, 'y': this.coord.y}; + let e_val = 'E'.charCodeAt(); + let x_val = res.x.charCodeAt(); + if (direction == 'e') { + res.y = (parseInt(res.y) + 1).toString(); + } + else if (direction == 'w') { + res.y = (parseInt(res.y) - 1).toString(); + } + else if (direction.charAt(0) == 'n') { + res.x = String.fromCharCode(x_val - 1); + if (direction.charAt(1) == 'w') { + res.y = (parseInt(res.y) - 1).toString(); + } + } + else if (direction.charAt(0) == 's') { + res.x = String.fromCharCode(x_val + 1); + if (direction.charAt(1) == 'e') { + res.y = (parseInt(res.y) + 1).toString(); + } + } + else { + res = false; + } + + return res; + } + + static are_aligned(slots, dir = false) { + if (slots.length == 1) { + return true; + } + let slot = slots.pop(); + let x0 = slot.coord.x.charCodeAt(); + let y0 = parseInt(slot.coord.y); + let next_slot = false; + if (dir === false) { + for (dir of ['ne', 'nw', 'w', 'sw', 'se', 'e']) { + console.log('trying dir : ' + dir); + next_slot = this.find_by_coord(slots, + slot.get_direction_coord(dir)); + if (next_slot !== false) { + break; + } + } + } + else { + next_slot = this.find_by_coord(slots, + slot.get_direction_coord(dir)); + } + + if (next_slot) { + slots.slice(slots.indexOf(next_slot), 1); + return this.are_aligned(slots, dir); + } + return false; + } + + static find_by_coord(slots, coord) { + let x0 = coord.x.charCodeAt(); + let y0 = parseInt(coord.y); + for (let slot of slots) { + if (slot.coord.x.charCodeAt() == x0 + && parseInt(slot.coord.y) == y0) { + return slot; + } + } + return false; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..9c73376 --- /dev/null +++ b/index.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+ Direction : +
+ + +
+ + +
+ + +
+
+ Debug : + + +
+ +
+ + diff --git a/kinetic.js b/kinetic.js new file mode 100644 index 0000000..d22d94d --- /dev/null +++ b/kinetic.js @@ -0,0 +1,15057 @@ + +/* + * KineticJS JavaScript Framework v5.1.1 + * http://www.kineticjs.com/ + * Copyright 2013, Eric Rowell + * Licensed under the MIT or GPL Version 2 licenses. + * Date: 2014-10-03 + * + * Copyright (C) 2011 - 2013 by Eric Rowell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/** + * @namespace Kinetic + */ +/*jshint -W079, -W020*/ +var Kinetic = {}; +(function(root) { + var PI_OVER_180 = Math.PI / 180; + + Kinetic = { + // public + version: '5.1.1', + + // private + stages: [], + idCounter: 0, + ids: {}, + names: {}, + shapes: {}, + listenClickTap: false, + inDblClickWindow: false, + + // configurations + enableTrace: false, + traceArrMax: 100, + dblClickWindow: 400, + /** + * Global pixel ratio configuration. KineticJS automatically detect pixel ratio of current device. + * But you may override such property, if you want to use your value. + * @property pixelRatio + * @default undefined + * @memberof Kinetic + * @example + * Kinetic.pixelRatio = 1; + */ + pixelRatio: undefined, + /** + * Drag distance property. If you start to drag a node you may want to wait until pointer is moved to some distance from start point, + * only then start dragging. + * @property dragDistance + * @default 0 + * @memberof Kinetic + * @example + * Kinetic.dragDistance = 10; + */ + dragDistance : 0, + /** + * Use degree values for angle properties. You may set this property to false if you want to use radiant values. + * @property angleDeg + * @default true + * @memberof Kinetic + * @example + * node.rotation(45); // 45 degrees + * Kinetic.angleDeg = false; + * node.rotation(Math.PI / 2); // PI/2 radian + */ + angleDeg: true, + /** + * Show different warnings about errors or wrong API usage + * @property showWarnings + * @default true + * @memberof Kinetic + * @example + * Kinetic.showWarnings = false; + */ + showWarnings : true, + + + + /** + * @namespace Filters + * @memberof Kinetic + */ + Filters: {}, + + /** + * Node constructor. Nodes are entities that can be transformed, layered, + * and have bound events. The stage, layers, groups, and shapes all extend Node. + * @constructor + * @memberof Kinetic + * @abstract + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + */ + Node: function(config) { + this._init(config); + }, + + /** + * Shape constructor. Shapes are primitive objects such as rectangles, + * circles, text, lines, etc. + * @constructor + * @memberof Kinetic + * @augments Kinetic.Node + * @param {Object} config + * @param {String} [config.fill] fill color + * @param {Integer} [config.fillRed] set fill red component + * @param {Integer} [config.fillGreen] set fill green component + * @param {Integer} [config.fillBlue] set fill blue component + * @param {Integer} [config.fillAlpha] set fill alpha component + * @param {Image} [config.fillPatternImage] fill pattern image + * @param {Number} [config.fillPatternX] + * @param {Number} [config.fillPatternY] + * @param {Object} [config.fillPatternOffset] object with x and y component + * @param {Number} [config.fillPatternOffsetX] + * @param {Number} [config.fillPatternOffsetY] + * @param {Object} [config.fillPatternScale] object with x and y component + * @param {Number} [config.fillPatternScaleX] + * @param {Number} [config.fillPatternScaleY] + * @param {Number} [config.fillPatternRotation] + * @param {String} [config.fillPatternRepeat] can be "repeat", "repeat-x", "repeat-y", or "no-repeat". The default is "no-repeat" + * @param {Object} [config.fillLinearGradientStartPoint] object with x and y component + * @param {Number} [config.fillLinearGradientStartPointX] + * @param {Number} [config.fillLinearGradientStartPointY] + * @param {Object} [config.fillLinearGradientEndPoint] object with x and y component + * @param {Number} [config.fillLinearGradientEndPointX] + * @param {Number} [config.fillLinearGradientEndPointY] + * @param {Array} [config.fillLinearGradientColorStops] array of color stops + * @param {Object} [config.fillRadialGradientStartPoint] object with x and y component + * @param {Number} [config.fillRadialGradientStartPointX] + * @param {Number} [config.fillRadialGradientStartPointY] + * @param {Object} [config.fillRadialGradientEndPoint] object with x and y component + * @param {Number} [config.fillRadialGradientEndPointX] + * @param {Number} [config.fillRadialGradientEndPointY] + * @param {Number} [config.fillRadialGradientStartRadius] + * @param {Number} [config.fillRadialGradientEndRadius] + * @param {Array} [config.fillRadialGradientColorStops] array of color stops + * @param {Boolean} [config.fillEnabled] flag which enables or disables the fill. The default value is true + * @param {String} [config.fillPriority] can be color, linear-gradient, radial-graident, or pattern. The default value is color. The fillPriority property makes it really easy to toggle between different fill types. For example, if you want to toggle between a fill color style and a fill pattern style, simply set the fill property and the fillPattern properties, and then use setFillPriority('color') to render the shape with a color fill, or use setFillPriority('pattern') to render the shape with the pattern fill configuration + * @param {String} [config.stroke] stroke color + * @param {Integer} [config.strokeRed] set stroke red component + * @param {Integer} [config.strokeGreen] set stroke green component + * @param {Integer} [config.strokeBlue] set stroke blue component + * @param {Integer} [config.strokeAlpha] set stroke alpha component + * @param {Number} [config.strokeWidth] stroke width + * @param {Boolean} [config.strokeScaleEnabled] flag which enables or disables stroke scale. The default is true + * @param {Boolean} [config.strokeEnabled] flag which enables or disables the stroke. The default value is true + * @param {String} [config.lineJoin] can be miter, round, or bevel. The default + * is miter + * @param {String} [config.lineCap] can be butt, round, or sqare. The default + * is butt + * @param {String} [config.shadowColor] + * @param {Integer} [config.shadowRed] set shadow color red component + * @param {Integer} [config.shadowGreen] set shadow color green component + * @param {Integer} [config.shadowBlue] set shadow color blue component + * @param {Integer} [config.shadowAlpha] set shadow color alpha component + * @param {Number} [config.shadowBlur] + * @param {Object} [config.shadowOffset] object with x and y component + * @param {Number} [config.shadowOffsetX] + * @param {Number} [config.shadowOffsetY] + * @param {Number} [config.shadowOpacity] shadow opacity. Can be any real number + * between 0 and 1 + * @param {Boolean} [config.shadowEnabled] flag which enables or disables the shadow. The default value is true + * @param {Array} [config.dash] + * @param {Boolean} [config.dashEnabled] flag which enables or disables the dashArray. The default value is true + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var customShape = new Kinetic.Shape({ + * x: 5, + * y: 10, + * fill: 'red', + * // a Kinetic.Canvas renderer is passed into the drawFunc function + * drawFunc: function(context) { + * context.beginPath(); + * context.moveTo(200, 50); + * context.lineTo(420, 80); + * context.quadraticCurveTo(300, 100, 260, 170); + * context.closePath(); + * context.fillStrokeShape(this); + * } + *}); + */ + Shape: function(config) { + this.__init(config); + }, + + /** + * Container constructor.  Containers are used to contain nodes or other containers + * @constructor + * @memberof Kinetic + * @augments Kinetic.Node + * @abstract + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + + */ + Container: function(config) { + this.__init(config); + }, + + /** + * Stage constructor. A stage is used to contain multiple layers + * @constructor + * @memberof Kinetic + * @augments Kinetic.Container + * @param {Object} config + * @param {String|Element} config.container Container id or DOM element + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * @example + * var stage = new Kinetic.Stage({ + * width: 500, + * height: 800, + * container: 'containerId' + * }); + */ + Stage: function(config) { + this.___init(config); + }, + + /** + * BaseLayer constructor. + * @constructor + * @memberof Kinetic + * @augments Kinetic.Container + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want + * to clear the canvas before each layer draw. The default value is true. + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + + * @example + * var layer = new Kinetic.Layer(); + */ + BaseLayer: function(config) { + this.___init(config); + }, + + /** + * Layer constructor. Layers are tied to their own canvas element and are used + * to contain groups or shapes. + * @constructor + * @memberof Kinetic + * @augments Kinetic.BaseLayer + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want + * to clear the canvas before each layer draw. The default value is true. + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + + * @example + * var layer = new Kinetic.Layer(); + */ + Layer: function(config) { + this.____init(config); + }, + + /** + * FastLayer constructor. Layers are tied to their own canvas element and are used + * to contain shapes only. If you don't need node nesting, mouse and touch interactions, + * or event pub/sub, you should use FastLayer instead of Layer to create your layers. + * It renders about 2x faster than normal layers. + * @constructor + * @memberof Kinetic + * @augments Kinetic.BaseLayer + * @param {Object} config + * @param {Boolean} [config.clearBeforeDraw] set this property to false if you don't want + * to clear the canvas before each layer draw. The default value is true. + * @param {Boolean} [config.visible] + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + + * @example + * var layer = new Kinetic.FastLayer(); + */ + FastLayer: function(config) { + this.____init(config); + }, + + /** + * Group constructor. Groups are used to contain shapes or other groups. + * @constructor + * @memberof Kinetic + * @augments Kinetic.Container + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.visible] + * @param {Boolean} [config.listening] whether or not the node is listening for events + * @param {String} [config.id] unique id + * @param {String} [config.name] non-unique name + * @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1 + * @param {Object} [config.scale] set scale + * @param {Number} [config.scaleX] set scale x + * @param {Number} [config.scaleY] set scale y + * @param {Number} [config.rotation] rotation in degrees + * @param {Object} [config.offset] offset from center point and rotation point + * @param {Number} [config.offsetX] set offset x + * @param {Number} [config.offsetY] set offset y + * @param {Boolean} [config.draggable] makes the node draggable. When stages are draggable, you can drag and drop + * the entire stage by dragging any portion of the stage + * @param {Number} [config.dragDistance] + * @param {Function} [config.dragBoundFunc] + * * @param {Object} [config.clip] set clip + * @param {Number} [config.clipX] set clip x + * @param {Number} [config.clipY] set clip y + * @param {Number} [config.clipWidth] set clip width + * @param {Number} [config.clipHeight] set clip height + + * @example + * var group = new Kinetic.Group(); + */ + Group: function(config) { + this.___init(config); + }, + + /** + * returns whether or not drag and drop is currently active + * @method + * @memberof Kinetic + */ + isDragging: function() { + var dd = Kinetic.DD; + + // if DD is not included with the build, then + // drag and drop is not even possible + if (dd) { + return dd.isDragging; + } else { + return false; + } + }, + /** + * returns whether or not a drag and drop operation is ready, but may + * not necessarily have started + * @method + * @memberof Kinetic + */ + isDragReady: function() { + var dd = Kinetic.DD; + + // if DD is not included with the build, then + // drag and drop is not even possible + if (dd) { + return !!dd.node; + } else { + return false; + } + }, + _addId: function(node, id) { + if(id !== undefined) { + this.ids[id] = node; + } + }, + _removeId: function(id) { + if(id !== undefined) { + delete this.ids[id]; + } + }, + _addName: function(node, name) { + if(name !== undefined) { + var names = name.split(/\W+/g); + for(var n = 0; n < names.length; n++) { + if (names[n]) { + if(this.names[names[n]] === undefined) { + this.names[names[n]] = []; + } + this.names[names[n]].push(node); + } + } + } + }, + _removeName: function(name, _id) { + if(name !== undefined) { + var nodes = this.names[name]; + if(nodes !== undefined) { + for(var n = 0; n < nodes.length; n++) { + var no = nodes[n]; + if(no._id === _id) { + nodes.splice(n, 1); + } + } + if(nodes.length === 0) { + delete this.names[name]; + } + } + } + }, + getAngle: function(angle) { + return this.angleDeg ? angle * PI_OVER_180 : angle; + }, + _parseUA: function(userAgent) { + var ua = userAgent.toLowerCase(), + // jQuery UA regex + match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || + /(webkit)[ \/]([\w.]+)/.exec( ua ) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || + /(msie) ([\w.]+)/.exec( ua ) || + ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || + [], + + // adding mobile flag as well + mobile = !!(userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i)), + ieMobile = !!(userAgent.match(/IEMobile/i)); + + return { + browser: match[ 1 ] || '', + version: match[ 2 ] || '0', + + // adding mobile flab + mobile: mobile, + ieMobile: ieMobile // If this is true (i.e., WP8), then Kinetic touch events are executed instead of equivalent Kinetic mouse events + }; + }, + // user agent + UA: undefined + }; + + Kinetic.UA = Kinetic._parseUA((root.navigator && root.navigator.userAgent) || ''); + +})(this); + +// Uses Node, AMD or browser globals to create a module. + +// If you want something that will work in other stricter CommonJS environments, +// or if you need to create a circular dependency, see commonJsStrict.js + +// Defines a module "returnExports" that depends another module called "b". +// Note that the name of the module is implied by the file name. It is best +// if the file name and the exported global have matching names. + +// If the 'b' module also uses this type of boilerplate, then +// in the browser, it will create a global .b that is used below. + +// If you do not want to support the browser global path, then you +// can remove the `root` use and the passing `this` as the first arg to +// the top function. + +// if the module has no dependencies, the above pattern can be simplified to +( function(root, factory) { + if( typeof exports === 'object') { + var KineticJS = factory(); + // runtime-check for browserify + if(global.window === global) { + Kinetic.document = global.document; + Kinetic.window = global; + } else { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + var Canvas = require('canvas'); + var jsdom = require('jsdom').jsdom; + + Kinetic.document = jsdom(''); + Kinetic.window = Kinetic.document.createWindow(); + Kinetic.window.Image = Canvas.Image; + Kinetic._nodeCanvas = Canvas; + } + + Kinetic.root = root; + module.exports = KineticJS; + return; + } + else if( typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } + Kinetic.document = document; + Kinetic.window = window; + Kinetic.root = root; + +}(this, function() { + + // Just return a value to define the module export. + // This example returns an object, but the module + // can return a function as the exported value. + return Kinetic; +})); +;(function() { + /** + * Collection constructor. Collection extends + * Array. This class is used in conjunction with {@link Kinetic.Container#get} + * @constructor + * @memberof Kinetic + */ + Kinetic.Collection = function() { + var args = [].slice.call(arguments), length = args.length, i = 0; + + this.length = length; + for(; i < length; i++) { + this[i] = args[i]; + } + return this; + }; + Kinetic.Collection.prototype = []; + /** + * iterate through node array and run a function for each node. + * The node and index is passed into the function + * @method + * @memberof Kinetic.Collection.prototype + * @param {Function} func + * @example + * // get all nodes with name foo inside layer, and set x to 10 for each + * layer.get('.foo').each(function(shape, n) { + * shape.setX(10); + * }); + */ + Kinetic.Collection.prototype.each = function(func) { + for(var n = 0; n < this.length; n++) { + func(this[n], n); + } + }; + /** + * convert collection into an array + * @method + * @memberof Kinetic.Collection.prototype + */ + Kinetic.Collection.prototype.toArray = function() { + var arr = [], + len = this.length, + n; + + for(n = 0; n < len; n++) { + arr.push(this[n]); + } + return arr; + }; + /** + * convert array into a collection + * @method + * @memberof Kinetic.Collection + * @param {Array} arr + */ + Kinetic.Collection.toCollection = function(arr) { + var collection = new Kinetic.Collection(), + len = arr.length, + n; + + for(n = 0; n < len; n++) { + collection.push(arr[n]); + } + return collection; + }; + + // map one method by it's name + Kinetic.Collection._mapMethod = function(methodName) { + Kinetic.Collection.prototype[methodName] = function() { + var len = this.length, + i; + + var args = [].slice.call(arguments); + for(i = 0; i < len; i++) { + this[i][methodName].apply(this[i], args); + } + + return this; + }; + }; + + Kinetic.Collection.mapMethods = function(constructor) { + var prot = constructor.prototype; + for(var methodName in prot) { + Kinetic.Collection._mapMethod(methodName); + } + }; + + /* + * Last updated November 2011 + * By Simon Sarris + * www.simonsarris.com + * sarris@acm.org + * + * Free to use and distribute at will + * So long as you are nice to people, etc + */ + + /* + * The usage of this class was inspired by some of the work done by a forked + * project, KineticJS-Ext by Wappworks, which is based on Simon's Transform + * class. Modified by Eric Rowell + */ + + /** + * Transform constructor + * @constructor + * @param {Array} [m] Optional six-element matrix + * @memberof Kinetic + */ + Kinetic.Transform = function(m) { + this.m = (m && m.slice()) || [1, 0, 0, 1, 0, 0]; + }; + + Kinetic.Transform.prototype = { + /** + * Copy Kinetic.Transform object + * @method + * @memberof Kinetic.Transform.prototype + * @returns {Kinetic.Transform} + */ + copy: function() { + return new Kinetic.Transform(this.m); + }, + /** + * Transform point + * @method + * @memberof Kinetic.Transform.prototype + * @param {Object} point 2D point(x, y) + * @returns {Object} 2D point(x, y) + */ + point: function(point) { + var m = this.m; + return { + x: m[0] * point.x + m[2] * point.y + m[4], + y: m[1] * point.x + m[3] * point.y + m[5] + }; + }, + /** + * Apply translation + * @method + * @memberof Kinetic.Transform.prototype + * @param {Number} x + * @param {Number} y + * @returns {Kinetic.Transform} + */ + translate: function(x, y) { + this.m[4] += this.m[0] * x + this.m[2] * y; + this.m[5] += this.m[1] * x + this.m[3] * y; + return this; + }, + /** + * Apply scale + * @method + * @memberof Kinetic.Transform.prototype + * @param {Number} sx + * @param {Number} sy + * @returns {Kinetic.Transform} + */ + scale: function(sx, sy) { + this.m[0] *= sx; + this.m[1] *= sx; + this.m[2] *= sy; + this.m[3] *= sy; + return this; + }, + /** + * Apply rotation + * @method + * @memberof Kinetic.Transform.prototype + * @param {Number} rad Angle in radians + * @returns {Kinetic.Transform} + */ + rotate: function(rad) { + var c = Math.cos(rad); + var s = Math.sin(rad); + var m11 = this.m[0] * c + this.m[2] * s; + var m12 = this.m[1] * c + this.m[3] * s; + var m21 = this.m[0] * -s + this.m[2] * c; + var m22 = this.m[1] * -s + this.m[3] * c; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + }, + /** + * Returns the translation + * @method + * @memberof Kinetic.Transform.prototype + * @returns {Object} 2D point(x, y) + */ + getTranslation: function() { + return { + x: this.m[4], + y: this.m[5] + }; + }, + /** + * Apply skew + * @method + * @memberof Kinetic.Transform.prototype + * @param {Number} sx + * @param {Number} sy + * @returns {Kinetic.Transform} + */ + skew: function(sx, sy) { + var m11 = this.m[0] + this.m[2] * sy; + var m12 = this.m[1] + this.m[3] * sy; + var m21 = this.m[2] + this.m[0] * sx; + var m22 = this.m[3] + this.m[1] * sx; + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + return this; + }, + /** + * Transform multiplication + * @method + * @memberof Kinetic.Transform.prototype + * @param {Kinetic.Transform} matrix + * @returns {Kinetic.Transform} + */ + multiply: function(matrix) { + var m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1]; + var m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1]; + + var m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3]; + var m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3]; + + var dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4]; + var dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5]; + + this.m[0] = m11; + this.m[1] = m12; + this.m[2] = m21; + this.m[3] = m22; + this.m[4] = dx; + this.m[5] = dy; + return this; + }, + /** + * Invert the matrix + * @method + * @memberof Kinetic.Transform.prototype + * @returns {Kinetic.Transform} + */ + invert: function() { + var d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]); + var m0 = this.m[3] * d; + var m1 = -this.m[1] * d; + var m2 = -this.m[2] * d; + var m3 = this.m[0] * d; + var m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]); + var m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]); + this.m[0] = m0; + this.m[1] = m1; + this.m[2] = m2; + this.m[3] = m3; + this.m[4] = m4; + this.m[5] = m5; + return this; + }, + /** + * return matrix + * @method + * @memberof Kinetic.Transform.prototype + */ + getMatrix: function() { + return this.m; + }, + /** + * set to absolute position via translation + * @method + * @memberof Kinetic.Transform.prototype + * @returns {Kinetic.Transform} + * @author ericdrowell + */ + setAbsolutePosition: function(x, y) { + var m0 = this.m[0], + m1 = this.m[1], + m2 = this.m[2], + m3 = this.m[3], + m4 = this.m[4], + m5 = this.m[5], + yt = ((m0 * (y - m5)) - (m1 * (x - m4))) / ((m0 * m3) - (m1 * m2)), + xt = (x - m4 - (m2 * yt)) / m0; + + return this.translate(xt, yt); + } + }; + + // CONSTANTS + var CONTEXT_2D = '2d', + OBJECT_ARRAY = '[object Array]', + OBJECT_NUMBER = '[object Number]', + OBJECT_STRING = '[object String]', + PI_OVER_DEG180 = Math.PI / 180, + DEG180_OVER_PI = 180 / Math.PI, + HASH = '#', + EMPTY_STRING = '', + ZERO = '0', + KINETIC_WARNING = 'Kinetic warning: ', + KINETIC_ERROR = 'Kinetic error: ', + RGB_PAREN = 'rgb(', + COLORS = { + aqua: [0,255,255], + lime: [0,255,0], + silver: [192,192,192], + black: [0,0,0], + maroon: [128,0,0], + teal: [0,128,128], + blue: [0,0,255], + navy: [0,0,128], + white: [255,255,255], + fuchsia: [255,0,255], + olive:[128,128,0], + yellow: [255,255,0], + orange: [255,165,0], + gray: [128,128,128], + purple: [128,0,128], + green: [0,128,0], + red: [255,0,0], + pink: [255,192,203], + cyan: [0,255,255], + transparent: [255,255,255,0] + }, + + RGB_REGEX = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/; + + /** + * @namespace Util + * @memberof Kinetic + */ + Kinetic.Util = { + /* + * cherry-picked utilities from underscore.js + */ + _isElement: function(obj) { + return !!(obj && obj.nodeType == 1); + }, + _isFunction: function(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }, + _isObject: function(obj) { + return (!!obj && obj.constructor == Object); + }, + _isArray: function(obj) { + return Object.prototype.toString.call(obj) == OBJECT_ARRAY; + }, + _isNumber: function(obj) { + return Object.prototype.toString.call(obj) == OBJECT_NUMBER; + }, + _isString: function(obj) { + return Object.prototype.toString.call(obj) == OBJECT_STRING; + }, + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _throttle: function(func, wait, opts) { + var context, args, result; + var timeout = null; + var previous = 0; + var options = opts || {}; + var later = function() { + previous = options.leading === false ? 0 : new Date().getTime(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = new Date().getTime(); + if (!previous && options.leading === false) { + previous = now; + } + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + /* + * other utils + */ + _hasMethods: function(obj) { + var names = [], + key; + + for(key in obj) { + if(this._isFunction(obj[key])) { + names.push(key); + } + } + return names.length > 0; + }, + createCanvasElement: function() { + var canvas = Kinetic.document.createElement('canvas'); + // on some environments canvas.style is readonly + try { + canvas.style = canvas.style || {}; + } catch (e) { + } + return canvas; + }, + isBrowser: function() { + return (typeof exports !== 'object'); + }, + _isInDocument: function(el) { + while(el = el.parentNode) { + if(el == Kinetic.document) { + return true; + } + } + return false; + }, + _simplifyArray: function(arr) { + var retArr = [], + len = arr.length, + util = Kinetic.Util, + n, val; + + for (n=0; n> 16) & 255, + g: (bigint >> 8) & 255, + b: bigint & 255 + }; + }, + /** + * return random hex color + * @method + * @memberof Kinetic.Util.prototype + */ + getRandomColor: function() { + var randColor = (Math.random() * 0xFFFFFF << 0).toString(16); + while (randColor.length < 6) { + randColor = ZERO + randColor; + } + return HASH + randColor; + }, + /** + * return value with default fallback + * @method + * @memberof Kinetic.Util.prototype + */ + get: function(val, def) { + if (val === undefined) { + return def; + } + else { + return val; + } + }, + /** + * get RGB components of a color + * @method + * @memberof Kinetic.Util.prototype + * @param {String} color + * @example + * // each of the following examples return {r:0, g:0, b:255} + * var rgb = Kinetic.Util.getRGB('blue'); + * var rgb = Kinetic.Util.getRGB('#0000ff'); + * var rgb = Kinetic.Util.getRGB('rgb(0,0,255)'); + */ + getRGB: function(color) { + var rgb; + // color string + if (color in COLORS) { + rgb = COLORS[color]; + return { + r: rgb[0], + g: rgb[1], + b: rgb[2] + }; + } + // hex + else if (color[0] === HASH) { + return this._hexToRgb(color.substring(1)); + } + // rgb string + else if (color.substr(0, 4) === RGB_PAREN) { + rgb = RGB_REGEX.exec(color.replace(/ /g,'')); + return { + r: parseInt(rgb[1], 10), + g: parseInt(rgb[2], 10), + b: parseInt(rgb[3], 10) + }; + } + // default + else { + return { + r: 0, + g: 0, + b: 0 + }; + } + }, + // o1 takes precedence over o2 + _merge: function(o1, o2) { + var retObj = this._clone(o2); + for(var key in o1) { + if(this._isObject(o1[key])) { + retObj[key] = this._merge(o1[key], retObj[key]); + } + else { + retObj[key] = o1[key]; + } + } + return retObj; + }, + cloneObject: function(obj) { + var retObj = {}; + for(var key in obj) { + if(this._isObject(obj[key])) { + retObj[key] = this.cloneObject(obj[key]); + } + else if (this._isArray(obj[key])) { + retObj[key] = this.cloneArray(obj[key]); + } else { + retObj[key] = obj[key]; + } + } + return retObj; + }, + cloneArray: function(arr) { + return arr.slice(0); + }, + _degToRad: function(deg) { + return deg * PI_OVER_DEG180; + }, + _radToDeg: function(rad) { + return rad * DEG180_OVER_PI; + }, + _capitalize: function(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + }, + error: function(str) { + throw new Error(KINETIC_ERROR + str); + }, + warn: function(str) { + /* + * IE9 on Windows7 64bit will throw a JS error + * if we don't use window.console in the conditional + */ + if(Kinetic.root.console && console.warn && Kinetic.showWarnings) { + console.warn(KINETIC_WARNING + str); + } + }, + extend: function(c1, c2) { + for(var key in c2.prototype) { + if(!( key in c1.prototype)) { + c1.prototype[key] = c2.prototype[key]; + } + } + }, + /** + * adds methods to a constructor prototype + * @method + * @memberof Kinetic.Util.prototype + * @param {Function} constructor + * @param {Object} methods + */ + addMethods: function(constructor, methods) { + var key; + + for (key in methods) { + constructor.prototype[key] = methods[key]; + } + }, + _getControlPoints: function(x0, y0, x1, y1, x2, y2, t) { + var d01 = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)), + d12 = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)), + fa = t * d01 / (d01 + d12), + fb = t * d12 / (d01 + d12), + p1x = x1 - fa * (x2 - x0), + p1y = y1 - fa * (y2 - y0), + p2x = x1 + fb * (x2 - x0), + p2y = y1 + fb * (y2 - y0); + + return [p1x ,p1y, p2x, p2y]; + }, + _expandPoints: function(p, tension) { + var len = p.length, + allPoints = [], + n, cp; + + for (n=2; n= Kinetic.traceArrMax) { + traceArr.shift(); + } + }, + /** + * reset canvas context transform + * @method + * @memberof Kinetic.Context.prototype + */ + reset: function() { + var pixelRatio = this.getCanvas().getPixelRatio(); + this.setTransform(1 * pixelRatio, 0, 0, 1 * pixelRatio, 0, 0); + }, + /** + * get canvas + * @method + * @memberof Kinetic.Context.prototype + * @returns {Kinetic.Canvas} + */ + getCanvas: function() { + return this.canvas; + }, + /** + * clear canvas + * @method + * @memberof Kinetic.Context.prototype + * @param {Object} [bounds] + * @param {Number} [bounds.x] + * @param {Number} [bounds.y] + * @param {Number} [bounds.width] + * @param {Number} [bounds.height] + */ + clear: function(bounds) { + var canvas = this.getCanvas(); + + if (bounds) { + this.clearRect(bounds.x || 0, bounds.y || 0, bounds.width || 0, bounds.height || 0); + } + else { + this.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + } + }, + _applyLineCap: function(shape) { + var lineCap = shape.getLineCap(); + if(lineCap) { + this.setAttr('lineCap', lineCap); + } + }, + _applyOpacity: function(shape) { + var absOpacity = shape.getAbsoluteOpacity(); + if(absOpacity !== 1) { + this.setAttr('globalAlpha', absOpacity); + } + }, + _applyLineJoin: function(shape) { + var lineJoin = shape.getLineJoin(); + if(lineJoin) { + this.setAttr('lineJoin', lineJoin); + } + }, + setAttr: function(attr, val) { + this._context[attr] = val; + }, + + // context pass through methods + arc: function() { + var a = arguments; + this._context.arc(a[0], a[1], a[2], a[3], a[4], a[5]); + }, + beginPath: function() { + this._context.beginPath(); + }, + bezierCurveTo: function() { + var a = arguments; + this._context.bezierCurveTo(a[0], a[1], a[2], a[3], a[4], a[5]); + }, + clearRect: function() { + var a = arguments; + this._context.clearRect(a[0], a[1], a[2], a[3]); + }, + clip: function() { + this._context.clip(); + }, + closePath: function() { + this._context.closePath(); + }, + createImageData: function() { + var a = arguments; + if(a.length === 2) { + return this._context.createImageData(a[0], a[1]); + } + else if(a.length === 1) { + return this._context.createImageData(a[0]); + } + }, + createLinearGradient: function() { + var a = arguments; + return this._context.createLinearGradient(a[0], a[1], a[2], a[3]); + }, + createPattern: function() { + var a = arguments; + return this._context.createPattern(a[0], a[1]); + }, + createRadialGradient: function() { + var a = arguments; + return this._context.createRadialGradient(a[0], a[1], a[2], a[3], a[4], a[5]); + }, + drawImage: function() { + var a = arguments, + _context = this._context; + + if(a.length === 3) { + _context.drawImage(a[0], a[1], a[2]); + } + else if(a.length === 5) { + _context.drawImage(a[0], a[1], a[2], a[3], a[4]); + } + else if(a.length === 9) { + _context.drawImage(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]); + } + }, + fill: function() { + this._context.fill(); + }, + fillText: function() { + var a = arguments; + this._context.fillText(a[0], a[1], a[2]); + }, + getImageData: function() { + var a = arguments; + return this._context.getImageData(a[0], a[1], a[2], a[3]); + }, + lineTo: function() { + var a = arguments; + this._context.lineTo(a[0], a[1]); + }, + moveTo: function() { + var a = arguments; + this._context.moveTo(a[0], a[1]); + }, + rect: function() { + var a = arguments; + this._context.rect(a[0], a[1], a[2], a[3]); + }, + putImageData: function() { + var a = arguments; + this._context.putImageData(a[0], a[1], a[2]); + }, + quadraticCurveTo: function() { + var a = arguments; + this._context.quadraticCurveTo(a[0], a[1], a[2], a[3]); + }, + restore: function() { + this._context.restore(); + }, + rotate: function() { + var a = arguments; + this._context.rotate(a[0]); + }, + save: function() { + this._context.save(); + }, + scale: function() { + var a = arguments; + this._context.scale(a[0], a[1]); + }, + setLineDash: function() { + var a = arguments, + _context = this._context; + + // works for Chrome and IE11 + if(this._context.setLineDash) { + _context.setLineDash(a[0]); + } + // verified that this works in firefox + else if('mozDash' in _context) { + _context.mozDash = a[0]; + } + // does not currently work for Safari + else if('webkitLineDash' in _context) { + _context.webkitLineDash = a[0]; + } + + // no support for IE9 and IE10 + }, + setTransform: function() { + var a = arguments; + this._context.setTransform(a[0], a[1], a[2], a[3], a[4], a[5]); + }, + stroke: function() { + this._context.stroke(); + }, + strokeText: function() { + var a = arguments; + this._context.strokeText(a[0], a[1], a[2]); + }, + transform: function() { + var a = arguments; + this._context.transform(a[0], a[1], a[2], a[3], a[4], a[5]); + }, + translate: function() { + var a = arguments; + this._context.translate(a[0], a[1]); + }, + _enableTrace: function() { + var that = this, + len = CONTEXT_METHODS.length, + _simplifyArray = Kinetic.Util._simplifyArray, + origSetter = this.setAttr, + n, args; + + // to prevent creating scope function at each loop + var func = function(methodName) { + var origMethod = that[methodName], + ret; + + that[methodName] = function() { + args = _simplifyArray(Array.prototype.slice.call(arguments, 0)); + ret = origMethod.apply(that, arguments); + + that._trace({ + method: methodName, + args: args + }); + + return ret; + }; + }; + // methods + for (n=0; n 255) { + return 255; + } else if (val < 0) { + return 0; + } else { + return Math.round(val); + } + }, + alphaComponent: function(val) { + if (val > 1) { + return 1; + } + // chrome does not honor alpha values of 0 + else if (val < 0.0001) { + return 0.0001; + } + else { + return val; + } + } + }; +})();;(function() { + // CONSTANTS + var ABSOLUTE_OPACITY = 'absoluteOpacity', + ABSOLUTE_TRANSFORM = 'absoluteTransform', + CHANGE = 'Change', + CHILDREN = 'children', + DOT = '.', + EMPTY_STRING = '', + GET = 'get', + ID = 'id', + KINETIC = 'kinetic', + LISTENING = 'listening', + MOUSEENTER = 'mouseenter', + MOUSELEAVE = 'mouseleave', + NAME = 'name', + SET = 'set', + SHAPE = 'Shape', + SPACE = ' ', + STAGE = 'stage', + TRANSFORM = 'transform', + UPPER_STAGE = 'Stage', + VISIBLE = 'visible', + CLONE_BLACK_LIST = ['id'], + + TRANSFORM_CHANGE_STR = [ + 'xChange.kinetic', + 'yChange.kinetic', + 'scaleXChange.kinetic', + 'scaleYChange.kinetic', + 'skewXChange.kinetic', + 'skewYChange.kinetic', + 'rotationChange.kinetic', + 'offsetXChange.kinetic', + 'offsetYChange.kinetic', + 'transformsEnabledChange.kinetic' + ].join(SPACE); + + + Kinetic.Util.addMethods(Kinetic.Node, { + _init: function(config) { + var that = this; + this._id = Kinetic.idCounter++; + this.eventListeners = {}; + this.attrs = {}; + this._cache = {}; + this._filterUpToDate = false; + this.setAttrs(config); + + // event bindings for cache handling + this.on(TRANSFORM_CHANGE_STR, function() { + this._clearCache(TRANSFORM); + that._clearSelfAndDescendantCache(ABSOLUTE_TRANSFORM); + }); + this.on('visibleChange.kinetic', function() { + that._clearSelfAndDescendantCache(VISIBLE); + }); + this.on('listeningChange.kinetic', function() { + that._clearSelfAndDescendantCache(LISTENING); + }); + this.on('opacityChange.kinetic', function() { + that._clearSelfAndDescendantCache(ABSOLUTE_OPACITY); + }); + }, + _clearCache: function(attr){ + if (attr) { + delete this._cache[attr]; + } + else { + this._cache = {}; + } + }, + _getCache: function(attr, privateGetter){ + var cache = this._cache[attr]; + + // if not cached, we need to set it using the private getter method. + if (cache === undefined) { + this._cache[attr] = privateGetter.call(this); + } + + return this._cache[attr]; + }, + /* + * when the logic for a cached result depends on ancestor propagation, use this + * method to clear self and children cache + */ + _clearSelfAndDescendantCache: function(attr) { + this._clearCache(attr); + + if (this.children) { + this.getChildren().each(function(node) { + node._clearSelfAndDescendantCache(attr); + }); + } + }, + /** + * clear cached canvas + * @method + * @memberof Kinetic.Node.prototype + * @returns {Kinetic.Node} + * @example + * node.clearCache(); + */ + clearCache: function() { + delete this._cache.canvas; + this._filterUpToDate = false; + return this; + }, + /** + * cache node to improve drawing performance, apply filters, or create more accurate + * hit regions + * @method + * @memberof Kinetic.Node.prototype + * @param {Object} config + * @param {Number} [config.x] + * @param {Number} [config.y] + * @param {Number} [config.width] + * @param {Number} [config.height] + * @param {Boolean} [config.drawBorder] when set to true, a red border will be drawn around the cached + * region for debugging purposes + * @returns {Kinetic.Node} + * @example + * // cache a shape with the x,y position of the bounding box at the center and + * // the width and height of the bounding box equal to the width and height of + * // the shape obtained from shape.width() and shape.height() + * image.cache(); + * + * // cache a node and define the bounding box position and size + * node.cache({ + * x: -30, + * y: -30, + * width: 100, + * height: 200 + * }); + * + * // cache a node and draw a red border around the bounding box + * // for debugging purposes + * node.cache({ + * x: -30, + * y: -30, + * width: 100, + * height: 200, + * drawBorder: true + * }); + */ + cache: function(config) { + var conf = config || {}, + x = conf.x || 0, + y = conf.y || 0, + width = conf.width || this.width(), + height = conf.height || this.height(), + drawBorder = conf.drawBorder || false; + + if (width === 0 || height === 0) { + Kinetic.Util.warn('Width or height of caching configuration equals 0. Cache is ignored.'); + return; + } + var cachedSceneCanvas = new Kinetic.SceneCanvas({ + pixelRatio: 1, + width: width, + height: height + }), + cachedFilterCanvas = new Kinetic.SceneCanvas({ + pixelRatio: 1, + width: width, + height: height + }), + cachedHitCanvas = new Kinetic.HitCanvas({ + width: width, + height: height + }), + sceneContext = cachedSceneCanvas.getContext(), + hitContext = cachedHitCanvas.getContext(); + + cachedHitCanvas.isCache = true; + + this.clearCache(); + + sceneContext.save(); + hitContext.save(); + + // this will draw a red border around the cached box for + // debugging purposes + if (drawBorder) { + sceneContext.save(); + sceneContext.beginPath(); + sceneContext.rect(0, 0, width, height); + sceneContext.closePath(); + sceneContext.setAttr('strokeStyle', 'red'); + sceneContext.setAttr('lineWidth', 5); + sceneContext.stroke(); + sceneContext.restore(); + } + + sceneContext.translate(x * -1, y * -1); + hitContext.translate(x * -1, y * -1); + + // don't need to translate canvas if shape is not added to layer + if (this.nodeType === 'Shape') { + sceneContext.translate(this.x() * -1, this.y() * -1); + hitContext.translate(this.x() * -1, this.y() * -1); + } + + this.drawScene(cachedSceneCanvas, this); + this.drawHit(cachedHitCanvas, this); + + sceneContext.restore(); + hitContext.restore(); + + this._cache.canvas = { + scene: cachedSceneCanvas, + filter: cachedFilterCanvas, + hit: cachedHitCanvas + }; + + return this; + }, + _drawCachedSceneCanvas: function(context) { + context.save(); + this.getLayer()._applyTransform(this, context); + context._applyOpacity(this); + context.drawImage(this._getCachedSceneCanvas()._canvas, 0, 0); + context.restore(); + }, + _getCachedSceneCanvas: function() { + var filters = this.filters(), + cachedCanvas = this._cache.canvas, + sceneCanvas = cachedCanvas.scene, + filterCanvas = cachedCanvas.filter, + filterContext = filterCanvas.getContext(), + len, imageData, n, filter; + + if (filters) { + if (!this._filterUpToDate) { + try { + len = filters.length; + filterContext.clear(); + // copy cached canvas onto filter context + filterContext.drawImage(sceneCanvas._canvas, 0, 0); + imageData = filterContext.getImageData(0, 0, filterCanvas.getWidth(), filterCanvas.getHeight()); + + // apply filters to filter context + for (n=0; n