From 690ccce89b7cf67b3c7c33f56ebf459083ca3ecc Mon Sep 17 00:00:00 2001 From: maxime Date: Sat, 4 Jan 2020 01:39:33 +0000 Subject: [PATCH] first commit, working abalone --- Abalone.class.js | 310 + Board.class.js | 205 + Move.class.js | 228 + Piece.class.js | 91 + Player.class.js | 15 + Slot.class.js | 162 + index.html | 139 + kinetic.js | 15057 +++++++++++++++++++++++++++++++++++++++++++++ settings.js | 33 + 9 files changed, 16240 insertions(+) create mode 100644 Abalone.class.js create mode 100644 Board.class.js create mode 100644 Move.class.js create mode 100644 Piece.class.js create mode 100644 Player.class.js create mode 100644 Slot.class.js create mode 100644 index.html create mode 100644 kinetic.js create mode 100644 settings.js 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