diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5be756 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +![Mapello](images/LAIG3_T01_G03_1.png) + +

+ +A fun *Reversi* derivative board game for 2 players +

+

Made with WebGL and Prolog

+ +---- + +## Description + +The goal of this project was to develop a human-like environment to play a browser version of a board game. + +The board game elected was [Mapello](docs/MAPELLO_EN.pdf) and the tech behind this project consists of: +* a [backend](backend/) game engine powered by the logic programming language **Prolog**. + +* a [frontend](frontend/) environment created with the WebGL technology, entirely written in **JavaScript**. + +---- + +## Demo + +![Demo](images/mapello.gif) + +---- + +## Run + +To run this project, first ensure you have `Docker` and `docker-compose` installed. + +A [Sicstus](https://sicstus.sics.se/download4.html) license for the Prolog development system is also needed. Download the Linux version, apply your license and copy the `sicstus` folder installation (with this name) inside the `backend` folder of this project. + +After that, run: + +* `docker-compose up -d` to build up the project containers and start them. + * When both of the containers have been created, go to `http://0.0.0.0:8080` in your browser and start playing. +* `docker-compose down` to stop the containers. + +---- + +## Mapello Rules + +In the Mapello game, for two players, **the pieces used are black and white reversible discs**, with the color facing up identifying a player's piece. + +![Init](images/mapello-init.png) + +The board, with a **10x10 square configuration,** is delimited by walls, and the playable area corresponds to the center of the board, 8x8. **Two pieces of each player are placed in the four central cells**, in a diagonal pattern. + +On the board, there are also other pieces of each of the following types: +* **walls**, extra and immovable, in any position of the playable area; +* **bonuses**, placed in the playable area and rewarding, with 3 points, the player who plays on them; +* **jokers**, acting as the current player's pieces, which exist only on the edges, outside the playable area. + +**The players play alternately**, with the black player starting. **On each turn, the player places a piece on an empty space, or bonus, adjacent to an opponent's piece.** + +![Example](images/mapello-example.png) + +**The opponent's pieces that are continuously between the new piece and another piece of the current player, or a joker, either diagonally, vertically, or horizontally, are turned upside down to the current player's color.** + +**A valid move must cause at least one of the opponent's pieces to be flipped.** If the player cannot make a valid move, he passes the turn to his opponent. + +**The game ends when neither player has a valid move.** When it ends, the points are counted by adding the number of each player's pieces present on the board to their accumulated bonuses, and the player with the most points wins. + +---- + +## Game Instructions + +### Start Menu + +![Start Menu](images/start-menu.png) + +> Choosing Players and Board: + +1. To choose the players' types, select *Player* or *Computer* from the *Type* dropdown menus. +2. To choose the difficulty level, in case the player is a *Computer*, select *Random* or *Greedy* from the *Level* dropdown menu. +3. Select the type of the generated board in the *Board* dropdown menu - *Random* or *Default*. +4. Press **Play** to start the game + +### Interface + +![Interface](images/interface.png) + +> Cameras, Themes, Lights, Undo and Replay: + +In the interface you can select the active camera, theme and lights. In addition, there are Undo and Replay buttons for the respective functions. + +### How to play + +![How to play](images/LAIG3_T01_G03_2.png) + +> Piece selection and user interaction throughout the game: + +* The scoreboard indicates which player must play and each player's points (bonus). +* The player has 60 seconds to play, and if not, the game is over. +* To play, the player whose color is indicated on the marker board can choose any piece on the auxiliary board by clicking on it. +* When choosing a piece, the valid positions are highlighted. +* To play the piece, simply click on one of the highlighted positions. +* The turn passes to the next player and the points will be updated if any bonuses are captured. +* At any time during the game, the player can Undo the play, going back to the previous move, or Replay to see all the moves made so far. +* When there are no more valid moves for any player, the game ends and the totals can be seen on the scoreboard, as well as the winner. + +---- + +## Screenshots + +| ![](images/LAIG3_T01_G03_2.png) | ![](images/LAIG3_T01_G03_3.png) | +|----|----| +| ![](images/LAIG3_T01_G03_4.png) | ![](images/LAIG3_T01_G03_5.png) | + +---- + +## Authors + +| Name | E-Mail | +| ---------------- | ---------------------| +| Diana Freitas | up201806230@fe.up.pt | +| Eduardo Brito | up201806271@fe.up.pt | \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d5f6032 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:latest + +COPY . /home/ + +WORKDIR /home/ + +ENV PATH "$PATH:/home/sicstus/bin" + +CMD ["sicstus", "-l", "server.pl", "--goal","server."] \ No newline at end of file diff --git a/backend/boards.pl b/backend/boards.pl new file mode 100644 index 0000000..120c6ce --- /dev/null +++ b/backend/boards.pl @@ -0,0 +1,71 @@ +% Initial Boards + +% initial(+Key, -Board) - Creates the initial Game State identified by Key +% Create default initial board +initial(default, [ + [wall, wall, wall, wall, joker, wall, wall, wall, wall, wall], + [wall, wall, empty, bonus, empty, empty, bonus, empty, wall, wall], + [wall, empty, empty, empty, empty, empty, wall, empty, empty, wall], + [joker, bonus, wall, empty, empty, empty, empty, empty, bonus, joker], + [joker, empty, empty, empty, black, white, empty, empty, empty, wall], + [wall, empty, empty, empty, white, black, empty, empty, bonus, wall], + [wall, bonus, empty, empty, empty, empty, empty, wall, empty, joker], + [joker, empty, empty, wall, empty, empty, empty, empty, empty, wall], + [wall, wall, empty, bonus, empty, empty, empty, bonus, wall, wall], + [wall, wall, wall, joker, wall, wall, wall, joker, wall, wall] + ]). + +% Create random board +initial(random, Board) :- + empty(Empty), + random_jokers(Empty, B1, 8), + random_walls(B1, B2, 8), + random_bonus(B2, Board, 8). + +% random_jokers(+Board, -NewBoard, +N) - Places N random jokers in Board, returning a NewBoard +random_jokers(Board, Board, 0). + +random_jokers(Board, NewBoard, N) :- + N > 0, + findall(Row-Col,(between(0,9,Row), between(0,9,Col), get_matrix_value(Board, Row, Col, wall)), Walls), + random_member(Row-Col, Walls), + set_matrix_value(Board, Row, Col, joker, B1), + N1 is N - 1, + random_jokers(B1, NewBoard, N1). + +% random_walls(+Board, -NewBoard, +N) - Places N random walls in Board, returning a NewBoard +random_walls(Board, Board, 0). + +random_walls(Board, NewBoard, N) :- + N > 0, + findall(Row-Col,(between(1,8,Row), between(1,8,Col), get_matrix_value(Board, Row, Col, empty)), Empties), + random_member(Row-Col, Empties), + set_matrix_value(Board, Row, Col, wall, B1), + N1 is N - 1, + random_walls(B1, NewBoard, N1). + +% random_bonus(+Board, -NewBoard, +N) - Places N random bonus in Board, returning a NewBoard +random_bonus(Board, Board, 0). + +random_bonus(Board, NewBoard, N) :- + N > 0, + findall(Row-Col,(between(1,8,Row), between(1,8,Col), get_matrix_value(Board, Row, Col, empty)), Empties), + random_member(Row-Col, Empties), + set_matrix_value(Board, Row, Col, bonus, B1), + N1 is N - 1, + random_bonus(B1, NewBoard, N1). + + +% empty(-Board) - Returns an empty Board +empty([ +[wall, wall, wall, wall, wall, wall, wall, wall, wall, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, empty, empty, empty, black, white, empty, empty, empty, wall], +[wall, empty, empty, empty, white, black, empty, empty, empty, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, empty, empty, empty, empty, empty, empty, empty, empty, wall], +[wall, wall, wall, wall, wall, wall, wall, wall, wall, wall] +]). diff --git a/backend/game.pl b/backend/game.pl new file mode 100644 index 0000000..3e38cbc --- /dev/null +++ b/backend/game.pl @@ -0,0 +1,239 @@ +% Game + +% valid_moves(+GameState, +Player, -ListOfMoves) - Get the ordered list of possible moves +valid_moves(GameState, Player, ListOfMoves):- + setof([Val, Row, Col, WouldTurn], + get_move(GameState, Player, Val, Row, Col, WouldTurn), + ListOfMoves), !. + +valid_moves(_, _, []). + + +get_move(GameState, Player, Val, Row, Col, WouldTurn):- + between(1,8,Row), between(1,8,Col), + valid_move(GameState, Player, Row, Col, S-WouldTurn), + get_bonus_at(GameState, Row, Col, Bonus), + Val is S + Bonus. + + +valid_move(GameState, Player, Row, Col, WouldTurn):- + % cell is empty or with bonus + (empty_cell(GameState, Row, Col); bonus_cell(GameState, Row, Col)), + % gets the player's piece and the opponent's piece + player(Player, _, PlayerPiece, OpponentPiece), + % get all the opponent cells that would be turned + would_turn(GameState, Row, Col, PlayerPiece, OpponentPiece, WouldTurnList), + length(WouldTurnList, N), N > 0, + WouldTurn = N-WouldTurnList. + + +% Make an already verified move and get new game state +move(GameState, Player, Row, Col, WouldTurn, BlackPoints, WhitePoints, NewBP, NewWP, NewGameState):- + player(Player, _, Piece, _), + % place the piece + set_matrix_value(GameState, Row, Col, Piece, NGS1), + % turn opponent's pieces + turn_pieces(NGS1, WouldTurn, Piece, NewGameState), + % finally update the points + update_points(GameState, Row, Col, Player, BlackPoints, WhitePoints, NewBP, NewWP). + +move(_, _, _, _, _, _, _, _, _):- write('\n ERROR: Invalid move!\n'), fail. + + +% get_total_points(+GameState-BlackPoints-Wh , TotalBp, TotalWp) - Counts the Total Black or White Points +get_total_points(GameState, BlackPoints, WhitePoints, TotalBp, TotalWp):- + count_pieces(GameState, black, Bp), + count_pieces(GameState, white, Wp), + TotalBp is BlackPoints + Bp, + TotalWp is WhitePoints + Wp. + + +% update_points(+GameState, +Row, +Col, +Player, +BlackPoints, +WhitePoints, -NewBP, -NewWP) - Updates both players' points +update_points(GameState, Row, Col, Player, BlackPoints, WhitePoints, NewBP, NewWP) :- + bonus_cell(GameState, Row, Col), !, + update_points(Player, BlackPoints, WhitePoints, NewBP, NewWP). + +update_points(_, _, _, _, BlackPoints, WhitePoints, BlackPoints, WhitePoints). + +update_points(1, BlackPoints, WhitePoints, NewBP, WhitePoints) :- + NewBP is BlackPoints + 3. + +update_points(_, BlackPoints, WhitePoints, BlackPoints, NewWP) :- + NewWP is WhitePoints + 3. + + +/* would_turn(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, -WouldTurn) - +get all the cells that would be turned if the player put his piece in the position [Row,Col] */ +would_turn(GameState, Row, Col, PlayerPiece, OpponentPiece, WouldTurn):- + would_turn_right(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnR), + would_turn_left(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnL), + would_turn_top(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnT), + would_turn_bottom(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnB), + would_turn_top_right(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnTR), + would_turn_top_left(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnTL), + would_turn_bottom_right(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnBR), + would_turn_bottom_left(GameState, Row, Col, PlayerPiece, OpponentPiece, [], WouldTurnBL), + append([WouldTurnR, WouldTurnL, WouldTurnT, WouldTurnB, WouldTurnTR, WouldTurnTL, WouldTurnBR, WouldTurnBL], WouldTurn). + + +/* would_turn_right(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the right of [Row,Col] */ +would_turn_right(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check right for an opponent + check_right(GameState, Row, Col, [OpponentPiece]), + Right is Col + 1, Right =< 9, + append(Acc, [[Row, Right]], NewAcc), + % check right pieces + would_turn_right(GameState, Row, Right, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_right(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check right for a joker or player piece + check_right(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_right(GameState, Row, Col, _, _, _, []):- + % right is a wall, bonus or empty + check_right(GameState, Row, Col, [empty, wall, bonus]). + + +/* would_turn_left(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the left of [Row,Col] */ +would_turn_left(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check left for an opponent + check_left(GameState, Row, Col, [OpponentPiece]), + Left is Col - 1, Left >= 0, + append(Acc, [[Row, Left]], NewAcc), + % check left pieces + would_turn_left(GameState, Row, Left, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_left(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check left for a joker or player piece + check_left(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_left(GameState, Row, Col, _, _, _,[]):- + % left is a wall, bonus or empty + check_left(GameState, Row, Col, [empty, wall, bonus]). + + +/* would_turn_top(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the top of [Row,Col] */ +would_turn_top(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check top for an opponent + check_top(GameState, Row, Col, [OpponentPiece]), + Top is Row - 1, Top >= 0, + append(Acc, [[Top, Col]], NewAcc), + % check top pieces + would_turn_top(GameState, Top, Col, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_top(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check top for a joker or player piece + check_top(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_top(GameState, Row, Col, _, _, _, []):- + % top is a wall, bonus or empty + check_top(GameState, Row, Col, [empty, wall, bonus]). + +/* would_turn_bottom(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the bottom of [Row,Col] */ +would_turn_bottom(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check bottom for an opponent + check_bottom(GameState, Row, Col, [OpponentPiece]), + Bottom is Row + 1, Bottom =< 9, + append(Acc, [[Bottom, Col]], NewAcc), + % check bottom pieces + would_turn_bottom(GameState, Bottom, Col, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_bottom(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check bottom for a joker or player piece + check_bottom(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_bottom(GameState, Row, Col, _, _, _, []):- + % bottom is a wall, bonus or empty + check_bottom(GameState, Row, Col, [empty, wall, bonus]). + + +/* would_turn_top_right(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the top_right of [Row,Col] */ +would_turn_top_right(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check top_right for an opponent + check_top_right(GameState, Row, Col, [OpponentPiece]), + Top is Row - 1, Top >= 0, + Right is Col + 1, Right =< 9, + append(Acc, [[Top, Right]], NewAcc), + % check top_right pieces + would_turn_top_right(GameState, Top, Right, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_top_right(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check top_right for a joker or player piece + check_top_right(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_top_right(GameState, Row, Col, _, _, _, []):- + % top_right is a wall, bonus or empty + check_top_right(GameState, Row, Col, [empty, wall, bonus]). + + +/* would_turn_top_left(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the top_left of [Row,Col] */ +would_turn_top_left(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check top_left for an opponent + check_top_left(GameState, Row, Col, [OpponentPiece]), + Top is Row - 1, Top >= 0, + Left is Col - 1, Left >= 0, + append(Acc, [[Top, Left]], NewAcc), + % check top_left pieces + would_turn_top_left(GameState, Top, Left, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_top_left(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check top_left for a joker or player piece + check_top_left(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_top_left(GameState, Row, Col, _, _, _, []):- + % top_left is a wall, bonus or empty + check_top_left(GameState, Row, Col, [empty, wall, bonus]). + + +/* would_turn_bottom_right(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the bottom_right of [Row,Col] */ +would_turn_bottom_right(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check bottom_right for an opponent + check_bottom_right(GameState, Row, Col, [OpponentPiece]), + Bottom is Row + 1, Bottom =< 9, + Right is Col + 1, Right =< 9, + append(Acc, [[Bottom, Right]], NewAcc), + % check bottom_right pieces + would_turn_bottom_right(GameState, Bottom, Right, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_bottom_right(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check bottom_right for a joker or player piece + check_bottom_right(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_bottom_right(GameState, Row, Col, _, _, _, []):- + % bottom_right is a wall, bonus or empty + check_bottom_right(GameState, Row, Col, [empty, bonus, wall]). + + +/* would_turn_bottom_left(+GameState, +Row, +Col, +PlayerPiece, +OpponentPiece, +Acc, -WouldTurn) - +get all the cells that would be turned on the bottom_left of [Row,Col] */ +would_turn_bottom_left(GameState, Row, Col, PlayerPiece, OpponentPiece, Acc, WouldTurn):- + % check bottom_left for an opponent + check_bottom_left(GameState, Row, Col, [OpponentPiece]), + Bottom is Row + 1, Bottom =< 9, + Left is Col - 1, Left >= 0, + append(Acc, [[Bottom, Left]], NewAcc), + % check bottom_left pieces + would_turn_bottom_left(GameState, Bottom, Left, PlayerPiece, OpponentPiece, NewAcc, WouldTurn). + +would_turn_bottom_left(GameState, Row, Col, PlayerPiece, _, Acc, Acc):- + % check bottom_left for a joker or player piece + check_bottom_left(GameState, Row, Col, [joker, PlayerPiece]). + +would_turn_bottom_left(GameState, Row, Col, _, _, _, []):- + % bottom left is a wall, bonus or empty + check_bottom_left(GameState, Row, Col, [empty, wall, bonus]). + + +/* turn_pieces(+GameState, +WouldTurn, +Piece, -NewGameState) - +turn all pieces in WouldTurn by giving them their new value (the current Player Piece) and returning a new board */ +turn_pieces(GameState, [], _, GameState). +turn_pieces(GameState, [[Row,Col]|WouldTurn], Piece, NewGameState):- + set_matrix_value(GameState, Row, Col, Piece, NGS1), + turn_pieces(NGS1, WouldTurn, Piece, NewGameState). \ No newline at end of file diff --git a/backend/mapello.pl b/backend/mapello.pl new file mode 100644 index 0000000..e50584a --- /dev/null +++ b/backend/mapello.pl @@ -0,0 +1,7 @@ +:- use_module(library(random)). +:- use_module(library(between)). +:- use_module(library(lists)). + +:- consult('utils.pl'). +:- consult('boards.pl'). +:- consult('game.pl'). diff --git a/backend/server.pl b/backend/server.pl new file mode 100644 index 0000000..9282f38 --- /dev/null +++ b/backend/server.pl @@ -0,0 +1,103 @@ +:-use_module(library(sockets)). +:-use_module(library(lists)). +:-use_module(library(codesio)). +:-use_module(library(json)). +:-use_module(library('json/examples/json_codes')). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% To run, enter 'server.' on sicstus command line after consulting this file. + +port(8080). + +% Server Entry Point +server :- + port(Port), + write('Opened Server'),nl,nl, + socket_server_open(Port, Socket), + server_loop(Socket), + socket_server_close(Socket), + write('Closed Server'),nl. + +% Server Loop +% Uncomment writes for more information on incomming connections +server_loop(Socket) :- + repeat, + socket_server_accept(Socket, _Client, Stream, [type(text)]), + % write('Accepted connection'), nl, + % Parse Request + catch(( + read_request(Stream, Request), + read_header(Stream) + ),_Exception,( + % write('Error parsing request.'),nl, + close_stream(Stream), + fail + )), + + % Generate Response + handle_request(Request, MyReply, Status), + % format('Request: ~q~n',[Request]), + % format('Reply: ~q~n', [MyReply]), + + % Output Response + format(Stream, 'HTTP/1.0 ~p~n', [Status]), + format(Stream, 'Access-Control-Allow-Origin: *~n', []), + format(Stream, 'Content-Type: text/plain~n~n', []), + format(Stream, '~p', [MyReply]), + + % write('Finnished Connection'),nl,nl, + close_stream(Stream), + (Request = quit), !. + +close_stream(Stream) :- flush_output(Stream), close(Stream). + +% Handles parsed HTTP requests +% Returns 200 OK on successful aplication of parse_input on request +% Returns 400 Bad Request on syntax error (received from parser) or on failure of parse_input +handle_request(Request, MyReply, '200 OK') :- catch(parse_input(Request, MyReply),error(_,_),fail), !. +handle_request(syntax_error, 'Syntax Error', '400 Bad Request') :- !. +handle_request(_, 'Bad Request', '400 Bad Request'). + +% Reads first Line of HTTP Header and parses request +% Returns term parsed from Request-URI +% Returns syntax_error in case of failure in parsing +read_request(Stream, Request) :- + read_line(Stream, LineCodes), + print_header_line(LineCodes), + + % Parse Request + atom_codes('GET /',Get), + append(Get,RL,LineCodes), + read_request_aux(RL,RL2), + + catch(read_from_codes(RL2, Request), error(syntax_error(_),_), fail), !. +read_request(_,syntax_error). + +read_request_aux([32|_],[46]) :- !. +read_request_aux([C|Cs],[C|RCs]) :- read_request_aux(Cs, RCs). + + +% Reads and Ignores the rest of the lines of the HTTP Header +read_header(Stream) :- + repeat, + read_line(Stream, Line), + print_header_line(Line), + (Line = []; Line = end_of_file),!. + +check_end_of_header([]) :- !, fail. +check_end_of_header(end_of_file) :- !,fail. +check_end_of_header(_). + +% Function to Output Request Lines (uncomment the line bellow to see more information on received HTTP Requests) +% print_header_line(LineCodes) :- catch((atom_codes(Line,LineCodes),write(Line),nl),_,fail), !. +print_header_line(_). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +:- consult('mapello.pl'). + +parse_input(Request, Res) :- + Pred-Args = Request, + T =.. [Pred | Args], T, + json_to_atom(Args, Res, [compact(true)]). diff --git a/backend/sicstus/README.md b/backend/sicstus/README.md new file mode 100644 index 0000000..f39ba5f --- /dev/null +++ b/backend/sicstus/README.md @@ -0,0 +1,13 @@ +Replace this folder with your linux installation of Sicstus Prolog. + +Final Structure: + +* backend + * sicstus + * bin + * include + * lib + * boards.pl + * Dockerfile + * game.pl + * ... \ No newline at end of file diff --git a/backend/utils.pl b/backend/utils.pl new file mode 100644 index 0000000..5aaf386 --- /dev/null +++ b/backend/utils.pl @@ -0,0 +1,107 @@ +% Utils + +% get_list_value(+List, +Pos, -Value) - Get the Value of the element at index Pos of the List +get_list_value(L, Pos, Value) :- nth0(Pos,L,Value). + +% get_matrix_value(+Matrix, +Row, +Col, -Value) - Get the Value of the element at cell [Row, Col] of the Matrix +get_matrix_value(M, Row, Col, Value) :- + nth0(Row, M, L), + get_list_value(L, Col, Value). + + +% set_list_value(+List, +Pos, +Value, -NewList) - Set the Value of the element at index Pos of the List +set_list_value([_|T], 0, Value, [Value|T]). + +set_list_value([H|T], Pos, Value, [H|R]) :- + Pos > 0, + Pos1 is Pos - 1, + set_list_value(T, Pos1, Value, R). + +% set_matrix_value(+Matrix, +Row, +Col, +Value, -NewMatrix) - Set the Value of the element at cell [Row, Col] of the Matrix +set_matrix_value([H|T], 0, Col, Value, [R|T]) :- + set_list_value(H, Col, Value, R). + +set_matrix_value([H|T], Row, Col, Value, [H|R]) :- + Row > 0, + Row1 is Row - 1, + set_matrix_value(T, Row1, Col, Value, R). + + +% check_right(+Matrix, +Row, +Col, +Pieces) - True if the cell on the right of Matrix[Row, Col] is in Pieces +check_right(Matrix, Row, Col, Pieces):- + Right is Col + 1, Right =< 9, + get_matrix_value(Matrix, Row, Right, Value), + member(Value, Pieces). + +% check_left(+Matrix, +Row, +Col, +Pieces) - True if the cell on the left of Matrix[Row, Col] is in Pieces +check_left(Matrix, Row, Col, Pieces):- + Left is Col - 1, Left >= 0, + get_matrix_value(Matrix, Row, Left, Value), + member(Value, Pieces). + +% check_top(+Matrix, +Row, +Col, +Pieces) - True if the cell on top of Matrix[Row, Col] is in Pieces +check_top(Matrix, Row, Col, Pieces):- + Top is Row - 1, Top >= 0, + get_matrix_value(Matrix, Top, Col, Value), + member(Value, Pieces). + +% check_bottom(+Matrix, +Row, +Col, +Pieces) - True if the cell below Matrix[Row, Col] is in Pieces +check_bottom(Matrix, Row, Col, Pieces):- + Bottom is Row + 1, Bottom =< 9, + get_matrix_value(Matrix, Bottom, Col, Value), + member(Value, Pieces). + +% check_top_left(+Matrix, +Row, +Col, +Pieces) - True if the top left cell of Matrix[Row, Col] is in Pieces +check_top_left(Matrix, Row, Col, Pieces):- + Top is Row - 1, Top >= 0, + Left is Col - 1, Left >= 0, + get_matrix_value(Matrix, Top, Left, Value), + member(Value, Pieces). + +% check_top_right(+Matrix, +Row, +Col, +Pieces) - True if the top right cell of Matrix[Row, Col] is in Pieces +check_top_right(Matrix, Row, Col, Pieces):- + Top is Row - 1, Top >= 0, + Right is Col + 1, Right =< 9, + get_matrix_value(Matrix, Top, Right, Value), + member(Value, Pieces). + +% check_bottom_left(+Matrix, +Row, +Col, +Pieces) - True if the bottom left cell of Matrix[Row, Col] is in Pieces +check_bottom_left(Matrix, Row, Col, Pieces):- + Bottom is Row + 1, Bottom =< 9, + Left is Col - 1, Left >= 0, + get_matrix_value(Matrix, Bottom, Left, Value), + member(Value, Pieces). + +% check_bottom_right(+Matrix, +Row, +Col, +Pieces) - True if the bottom right cell of Matrix[Row, Col] is in Pieces +check_bottom_right(Matrix, Row, Col, Pieces):- + Bottom is Row + 1, Bottom =< 9, + Right is Col + 1, Right =< 9, + get_matrix_value(Matrix, Bottom, Right, Value), + member(Value, Pieces). + + +% empty_cell(+Matrix, +Row, +Col) - Check if a cell [Row, Col] is empty +empty_cell(Matrix, Row, Col) :- + get_matrix_value(Matrix, Row, Col, empty). + +% bonus_cell(+Matrix, +Row, +Col) - Check if a cell [Row, Col] has a bonus +bonus_cell(Matrix, Row, Col):- + get_matrix_value(Matrix, Row, Col, bonus). + +% get_bonus_at(+Matrix, +Row, +Col, -Bonus) - Get the bonus in the cell [Row, Col] +get_bonus_at(Matrix, Row, Col, 3):- + bonus_cell(Matrix, Row, Col), !. + +get_bonus_at(_, _, _, 0). + +% count_pieces(+GameState, +Piece, -N) - Count all occurrences of the Piece in the current Board +count_pieces(GameState, Piece, N) :- + findall(Row-Col, + (between(1,8,Row), between(1,8,Col), + get_matrix_value(GameState, Row, Col, Piece)), + ListOfPieces), + length(ListOfPieces, N). + +% player(+PlayerId, -Name, -Piece, -OpponentPiece) +player(1, 'BLACK', black, white). +player(-1, 'WHITE', white, black). \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3ff7f61 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,20 @@ +version: "3" +services: + web-gl: + ports: + - "8080:80" + volumes: + - "./frontend/:/usr/share/nginx/html/" + image: "nginx:latest" + networks: + - "pnet" + prolog: + build: "./backend/" + image: "prolog" + ports: + - "8081:8080" + networks: + - "pnet" +networks: + pnet: + driver: "bridge" \ No newline at end of file diff --git a/docs/LAIG3_T1_G03.pdf b/docs/LAIG3_T1_G03.pdf new file mode 100644 index 0000000..28bec14 Binary files /dev/null and b/docs/LAIG3_T1_G03.pdf differ diff --git a/docs/MAPELLO_EN.pdf b/docs/MAPELLO_EN.pdf new file mode 100644 index 0000000..eef24bb Binary files /dev/null and b/docs/MAPELLO_EN.pdf differ diff --git a/frontend/MyInterface.js b/frontend/MyInterface.js new file mode 100644 index 0000000..47bc725 --- /dev/null +++ b/frontend/MyInterface.js @@ -0,0 +1,127 @@ +/** +* MyInterface class, creating a GUI interface. +*/ +class MyInterface extends CGFinterface { + /** + * @constructor + */ + constructor() { + super(); + } + + /** + * Initializes the interface. + * @param {CGFapplication} application + */ + init(application) { + super.init(application); + // init GUI. For more information on the methods, check: + // http://workshop.chromeexperiments.com/examples/gui + + this.gui = new dat.GUI(); + + dat.GUI.prototype.removeFolder = function (name) { + var folder = this.__folders[name]; + if (!folder) { + return; + } + folder.close(); + this.__ul.removeChild(folder.domElement.parentNode); + delete this.__folders[name]; + this.onResize(); + } + + this.cameraFolder = this.gui.addFolder("Camera"); + this.cameraFolder.open(); + this.lightsFolder = this.gui.addFolder("Lights"); + this.lightsFolder.open(); + this.themesFolder = this.gui.addFolder("Themes"); + this.themesFolder.open(); + + this.initKeys(); + + return true; + } + + /** + * Initializes the Dropdown Menu for the scene's cameras + * @param scene CGFscene object + * @param attribute relates to the scene's 'selected camera' + */ + initCameras(scene, attribute) { + this.cameraFolder.add(scene, attribute, Object.keys(scene.cameras)) + .name(attribute) + .onChange( + () => { + scene.animator.addMove( + new CameraAnimation(scene, scene.camera, scene.cameras[scene[attribute]])) + }); + } + + /** + * Initializes the Checkboxes for the scene's lights + * @param lights scene's array of CGFlight objects + * @param attribute relates to the 'enabled' property of the lights + */ + initLights(lights, attribute) { + this.gui.removeFolder("Lights"); + this.lightsFolder = this.gui.addFolder("Lights"); + + this.lightsFolder.open(); + + for (const light of lights) { + if (light.name != undefined){ + this.lightsFolder.add(light, attribute) + .name(light.name) + .onChange(() => light.update()); + } + } + } + + /** + * Initializes the Dropdown Menu for the game themes + * @param scene CGFscene object + * @param attribute relates to the scene's 'selected theme' + * @param themes list of game themes + */ + initThemes(scene, attribute, themes) { + this.themesFolder.add(scene, attribute, themes) + .name("Theme") + .onChange( + () => { + scene.setCurrentTheme(scene[attribute], true); + }); + } + + /** + * Initializes the Undo and Replay buttons + * @param scene CGFscene object + */ + initButtons(scene){ + var obj = { Undo:function(){ scene.undo() }, Replay:function(){ scene.replay() }}; + + this.gui.add(obj,'Undo'); + this.gui.add(obj,'Replay'); + } + + /** + * initKeys + */ + initKeys() { + this.scene.gui = this; + this.processKeyboard = function () { }; + this.activeKeys = {}; + } + + processKeyDown(event) { + this.activeKeys[event.code] = true; + }; + + processKeyUp(event) { + this.activeKeys[event.code] = false; + }; + + isKeyPressed(keyCode) { + return this.activeKeys[keyCode] || false; + } +} \ No newline at end of file diff --git a/frontend/MyNode.js b/frontend/MyNode.js new file mode 100644 index 0000000..f9e42be --- /dev/null +++ b/frontend/MyNode.js @@ -0,0 +1,30 @@ +/** + * MyNode + * @constructor + * @param scene - Reference to MyScene object + * @param id + * @param transformations + * @param materialID + * @param textureID + * @param afs + * @param aft + * @param descendants + * @param primitives + * @param sprites + */ +class MyNode extends CGFobject { + constructor(scene, id, transformations, materialID, textureID, animationID, afs, aft, descendants, primitives, sprites) { + super(scene); + this.id = id; + this.transformations = transformations; + this.materialID = materialID; + this.textureID = textureID; + this.animationID = animationID; + this.afs = afs; + this.aft = aft; + this.descendants = descendants; + this.primitives = primitives; + this.sprites = sprites; + } + +} diff --git a/frontend/MySceneGraph.js b/frontend/MySceneGraph.js new file mode 100644 index 0000000..827d712 --- /dev/null +++ b/frontend/MySceneGraph.js @@ -0,0 +1,1755 @@ +// Order of the groups in the XML document. +var INITIALS_INDEX = 0; +var VIEWS_INDEX = 1; +var ILLUMINATION_INDEX = 2; +var LIGHTS_INDEX = 3; +var TEXTURES_INDEX = 4; +var SPRITESHEETS_INDEX = 5; +var MATERIALS_INDEX = 6; +var ANIMATIONS_INDEX = 7; +var NODES_INDEX = 8; + +/** + * MySceneGraph class, representing the scene graph. + */ +class MySceneGraph { + /** + * Constructor for MySceneGraph class. + * Initializes necessary variables and starts the XML file reading process. + * @param {string} filename - File that defines the 3D scene + * @param {XMLScene} scene + */ + constructor(filename, scene) { + this.loadedOk = null; + + // WRONG // Establish bidirectional references between scene and graph. + this.scene = scene; + + this.nodes = []; + + this.idRoot = null; // The id of the root element. + + this.axisCoords = []; + this.axisCoords['x'] = [1, 0, 0]; + this.axisCoords['y'] = [0, 1, 0]; + this.axisCoords['z'] = [0, 0, 1]; + + // File reading + this.reader = new CGFXMLreader(); + + /* + * Read the contents of the xml file, and refer to this class for loading and error handlers. + * After the file is read, the reader calls onXMLReady on this object. + * If any error occurs, the reader calls onXMLError on this object, with an error message + */ + this.reader.open('scenes/' + filename, this); + } + + /** + * Callback to be executed after successful reading + */ + onXMLReady() { + this.log("XML Loading finished."); + var rootElement = this.reader.xmlDoc.documentElement; + + // Here should go the calls for different functions to parse the various blocks + var error = this.parseXMLFile(rootElement); + + if (error != null) { + this.onXMLError(error); + return; + } + + this.loadedOk = true; + + // As the graph loaded ok, signal the scene so that any additional initialization depending on the graph can take place + this.scene.onGraphLoaded(); + } + + /** + * Callback to be executed on any read error, showing an error on the console. + * @param {string} message + */ + onXMLError(message) { + console.error("XML Loading Error: " + message); + this.loadedOk = false; + } + + /** + * Callback to be executed on any minor error, showing a warning on the console. + * @param {string} message + */ + onXMLMinorError(message) { + console.warn("Warning: " + message); + } + + /** + * Check for errors in an array of parsed variables + * @param {array} dataArr + */ + hasXMLErrors(dataArr) { + for (var data of dataArr) { + if (typeof data === 'string') + return true; + } + return false; + } + + /** + * To be executed on an array with minor error messages, showing an error for each of the errors and a final warning. + * @param {array} errorArr + * @param {array} nodeID + */ + onXMLNodeMultipleErrors(errorArr, nodeID) { + for (var i = 0; i < errorArr.length; i++) { + if (typeof errorArr[i] === 'string') { + if (i != errorArr.length - 1) + this.onXMLError(errorArr[i] + " on node " + nodeID); + else + this.onXMLMinorError(errorArr[i] + " on node " + nodeID); + } + } + } + + /** + * Callback to be executed on any message. + * @param {string} message + */ + log(message) { + console.log(" " + message); + } + + /** + * Check if a file exists + * @param {path} the path of the file + */ + fileExists(path) { + var req = new XMLHttpRequest(); + req.open('HEAD', path, false); + req.send(); + + return (req.status == "404") ? false : true; + } + + /** + * Parses the XML file, processing each block. + * @param {XML root element} rootElement + */ + parseXMLFile(rootElement) { + if (rootElement.nodeName != "lsf") + return "root tag missing"; + + var nodes = rootElement.children; + + // Reads the names of the nodes to an auxiliary buffer. + var nodeNames = []; + + for (var i = 0; i < nodes.length; i++) { + nodeNames.push(nodes[i].nodeName); + } + + var error; + + // Processes each node, verifying errors. + + // + var index; + if ((index = nodeNames.indexOf("parsererror")) != -1) { // Parsing Error + var errorNode = nodes[index].children; + return errorNode[1].innerHTML; + } + + // + if ((index = nodeNames.indexOf("initials")) == -1) + return "tag missing"; + else { + if (index != INITIALS_INDEX) + this.onXMLMinorError("tag out of order " + index); + + //Parse initials block + if ((error = this.parseInitials(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("views")) == -1) + return "tag missing"; + else { + if (index != VIEWS_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse views block + if ((error = this.parseViews(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("illumination")) == -1) + return "tag missing"; + else { + if (index != ILLUMINATION_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse illumination block + if ((error = this.parseIllumination(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("lights")) == -1) + return "tag missing"; + else { + if (index != LIGHTS_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse lights block + if ((error = this.parseLights(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("spritesheets")) == -1) + return "tag missing"; + else { + if (index != SPRITESHEETS_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse spritesheets block + if ((error = this.parseSpritesheets(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("textures")) == -1) + return "tag missing"; + else { + if (index != TEXTURES_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse textures block + if ((error = this.parseTextures(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("materials")) == -1) + return "tag missing"; + else { + if (index != MATERIALS_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse materials block + if ((error = this.parseMaterials(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("animations")) == -1) + this.onXMLMinorError("tag missing"); + else { + if (index != ANIMATIONS_INDEX) + this.onXMLMinorError("tag out of order"); + + //Parse animations block + if ((error = this.parseAnimations(nodes[index])) != null) + return error; + } + + var nodesIdx; + if((index = nodeNames.indexOf("animations")) == -1) + nodesIdx = NODES_INDEX-1; + else + nodesIdx = NODES_INDEX; + + // + if ((index = nodeNames.indexOf("nodes")) == -1) + return "tag missing"; + else { + if (index != nodesIdx) + this.onXMLMinorError("tag out of order"); + + //Parse nodes block + if ((error = this.parseNodes(nodes[index])) != null) + return error; + } + + } + + /** + * Parses the block. + * @param {initials block element} initialsNode + */ + parseInitials(initialsNode) { + var children = initialsNode.children; + var nodeNames = []; + + for (var i = 0; i < children.length; i++) + nodeNames.push(children[i].nodeName); + + var rootIndex = nodeNames.indexOf("root"); + var referenceIndex = nodeNames.indexOf("reference"); + + // Get root of the scene. + if (rootIndex == -1) + return "No root id defined for scene."; + + var rootNode = children[rootIndex]; + var id = this.reader.getString(rootNode, 'id'); + if (id == null) + return "No root id defined for scene."; + + this.idRoot = id; + + // Get axis length + if (referenceIndex == -1) + this.onXMLMinorError("no axis_length defined for scene; assuming 'length = 1'"); + + var refNode = children[referenceIndex]; + var axis_length = this.reader.getFloat(refNode, 'length'); + if (axis_length == null) + this.onXMLMinorError("no axis_length defined for scene; assuming 'length = 1'"); + + this.referenceLength = axis_length || 1; + + + return null; + } + + /** + * Parses the block. + * @param {view block element} viewsNode + */ + parseViews(viewsNode) { + + var children = viewsNode.children; + this.views = []; + var defaultCamera = this.reader.getString(viewsNode, "default"); + + if (defaultCamera == null) { + this.onXMLMinorError("No default camera was set"); + this.scene.selectedCamera = "defaultCamera"; + } + else { + this.scene.selectedCamera = defaultCamera; + } + + var cameraIDs = []; + + for (var i = 0; i < children.length; i++) { + + var nodeName = children[i].nodeName; + var global = []; + var attributeNames = []; + + if (nodeName != "perspective" && nodeName != "ortho") { + this.onXMLMinorError("unknown camera tag <" + nodeName + ">"); + continue; + } + else { + attributeNames = ["near", "far", "angle", "left", "right", "bottom", "top"]; + global.push(nodeName); // perspective or ortho + } + + // Get id of the current view. + var viewID = this.reader.getString(children[i], 'id'); + if (viewID == null) + return "no ID defined for view"; + + // Checks for repeated IDs. + if (this.views[viewID] != null) + return "ID must be unique for each view (conflict: ID = " + viewID + ")"; + + for (var j = 0; j < attributeNames.length; j++) { + if ((attributeNames[j] == "angle" && nodeName == "ortho") || (j > 2 && nodeName == "perspective")) continue; + + var aux = this.reader.getFloat(children[i], attributeNames[j]); + if (aux == null) { + this.onXMLMinorError("no " + attributeNames[j] + " attribute defined for view " + viewID); + aux = 1.0; + } + if (typeof aux === 'string') + return aux; + + global.push(aux); + } + + // Specifications for the current view. + var grandChildren = children[i].children; + attributeNames = ["from", "to", "up"]; + + var nodeNames = []; + for (var j = 0; j < grandChildren.length; j++) { + nodeNames.push(grandChildren[j].nodeName); + } + + nodeNames.forEach(val => { + if (!attributeNames.includes(val)) { + this.onXMLMinorError("unknown property " + val + " for view ID = " + viewID); + } + }); + + for (var j = 0; j < attributeNames.length; j++) { + var attributeIndex = nodeNames.indexOf(attributeNames[j]); + + if (attributeIndex != -1) { + var aux = this.parseCoordinates3D(grandChildren[attributeIndex], attributeNames[j] + " view position for ID" + viewID); + + if (typeof aux === 'string') + return aux; + + global.push(aux); + } + else if (attributeNames[j] != "up") { + this.onXMLMinorError(attributeNames[j] + " is not defined for view ID = " + viewID); + global.push([10, 10, 10]); // default vector + } + else if (attributeNames[j] == "up" && nodeName == "ortho") + global.push([0, 1, 0]); // default up vector + } + + this.views[viewID] = global; + cameraIDs.push(viewID); + } + + if (cameraIDs.length == 0) { + this.onXMLMinorError("No Perspective or Ortho cameras were set"); + this.views[this.scene.selectedCamera] = ["perspective", 0.1, 500, 40, [0, 15, 15], [0, 0, 0]]; + } + else if (this.views[this.scene.selectedCamera] == null) { + this.onXMLMinorError("Default Camera ID does not match existing cameras. Setting it to the first of the list."); + this.scene.selectedCamera = cameraIDs[0]; + } + + + return null; + } + + /** + * Parses the node. + * @param {illumination block element} illuminationsNode + */ + parseIllumination(illuminationsNode) { + + var children = illuminationsNode.children; + + this.ambient = []; + this.background = []; + + var nodeNames = []; + + for (var i = 0; i < children.length; i++) + nodeNames.push(children[i].nodeName); + + var ambientIndex = nodeNames.indexOf("ambient"); + if (ambientIndex == -1) { + this.onXMLMinorError("No ambient illumination defined for scene."); + this.ambient = [0.3, 0.3, 0.4, 0.8]; + } + else { + + var color = this.parseColor(children[ambientIndex], "ambient"); + if (!Array.isArray(color)) + return color; + else + this.ambient = color; + } + + var backgroundIndex = nodeNames.indexOf("background"); + if (backgroundIndex == -1) { + this.onXMLMinorError("No background illumination defined for scene."); + this.background = [0.53, 0.95, 0.8, 1]; + } + else { + + color = this.parseColor(children[backgroundIndex], "background"); + if (!Array.isArray(color)) + return color; + else + this.background = color; + } + + + + return null; + } + + /** + * Parses the node. + * @param {lights block element} lightsNode + */ + parseLights(lightsNode) { + var children = lightsNode.children; + + this.lights = []; + var numLights = 0; + + var grandChildren = []; + var nodeNames = []; + var error; + + // Any number of lights. + for (var i = 0; i < children.length; i++) { + + // Storing light information + var global = []; + var attributeNames = []; + var attributeTypes = []; + error = false; + + //Check type of light + if (children[i].nodeName != "light") { + this.onXMLMinorError("unknown tag <" + children[i].nodeName + ">"); + continue; + } + else { + attributeNames.push(...["enable", "position", "ambient", "diffuse", "specular"]); + attributeTypes.push(...["boolean", "position", "color", "color", "color"]); + } + + // Get id of the current light. + var lightId = this.reader.getString(children[i], 'id'); + if (lightId == null) + return "no ID defined for light"; + + // Checks for repeated IDs. + if (this.lights[lightId] != null) + return "ID must be unique for each light (conflict: ID = " + lightId + ")"; + + // Specifications for the current light. + grandChildren = children[i].children; + + nodeNames = []; + for (var j = 0; j < grandChildren.length; j++) { + nodeNames.push(grandChildren[j].nodeName); + } + + for (var j = 0; j < attributeNames.length; j++) { + var attributeIndex = nodeNames.indexOf(attributeNames[j]); + + if (attributeIndex != -1) { + if (attributeTypes[j] == "boolean") + var aux = this.parseBoolean(grandChildren[attributeIndex], "value", "enabled attribute for light of ID" + lightId); + else if (attributeTypes[j] == "position") + var aux = this.parseCoordinates4D(grandChildren[attributeIndex], "light position for ID" + lightId); + else + var aux = this.parseColor(grandChildren[attributeIndex], attributeNames[j] + " illumination for ID" + lightId); + + if (typeof aux === 'string') { + this.onXMLError(aux); + error = true; + } + + global.push(aux); + } + else { + this.onXMLError("light " + attributeNames[j] + " block is undefined for ID " + lightId); + error = true; + break; + } + } + + if (error) { + this.onXMLError("light with ID " + lightId + " has missing attributes or values"); + continue; + } + + this.lights[lightId] = global; + numLights++; + } + + if (numLights == 0) + return "at least one light must be defined"; + else if (numLights > 8) + this.onXMLMinorError("too many lights defined; WebGL imposes a limit of 8 lights"); + + + return null; + } + + /** + * Parses the block. + * @param {spritesheets block element} spritesheetsNode + */ + parseSpritesheets(spritesheetsNode) { + var children = spritesheetsNode.children; + + this.spritesheets = []; + + // Any number of spritesheets. + for (var i = 0; i < children.length; i++) { + + var global = []; + + if (children[i].nodeName != "spritesheet") { + this.onXMLMinorError("unknown spritesheet tag <" + children[i].nodeName + ">"); + continue; + } + + // Get id of the current spritesheet. + var spritesheetID = this.reader.getString(children[i], 'id'); + if (spritesheetID == null) + return "no ID defined for spritesheet"; + + // Checks for repeated IDs. + if (this.spritesheets[spritesheetID] != null) + return "ID must be unique for each texture (conflict: ID = " + spritesheetID + ")"; + + // Get path of the current spritesheet. + var path = this.reader.getString(children[i], 'path'); + + if (!this.fileExists(path)) { + this.onXMLError("invalid path for spritesheet with ID " + this.spritesheetID); + continue; + } + else if (path == null || path == "") { + this.onXMLMinorError("failed to load spritesheet with ID " + this.spritesheetID); + continue; + } + + global.push(path); + + + // Get number of columns(M) of the current spritesheet. + var sizeM = this.parseInteger(children[i], 'sizeM', ' spritesheet with ID ' + this.spritesheetID); + + if (typeof sizeM === 'string') { + this.onXMLMinorError(sizeM); + continue; + } + + if (sizeM < 0) { + this.onXMLMinorError('spritesheet sizeM should be positive in spritesheet with ID ' + this.spritesheetID); + continue; + } + + global.push(sizeM); + + // Get number of rows(N) of the current spritesheet. + var sizeN = this.parseInteger(children[i], 'sizeN', ' spritesheet with ID ' + this.spritesheetID); + + if (typeof sizeN === 'string') { + this.onXMLMinorError(sizeM); + continue; + } + + if (sizeN < 0) { + this.onXMLMinorError('spritesheet sizeM should be positive in spritesheet with ID ' + this.spritesheetID); + continue; + } + global.push(sizeN); + + this.spritesheets[spritesheetID] = global; + + } + + + return null; + } + + /** + * Parses the block. + * @param {textures block element} texturesNode + */ + parseTextures(texturesNode) { + var children = texturesNode.children; + + this.textures = []; + + // Any number of textures. + for (var i = 0; i < children.length; i++) { + + if (children[i].nodeName != "texture") { + this.onXMLMinorError("unknown texture tag <" + children[i].nodeName + ">"); + continue; + } + + // Get id of the current texture. + var textureID = this.reader.getString(children[i], 'id'); + if (textureID == null) + return "no ID defined for texture"; + + // Checks for repeated IDs. + if (this.textures[textureID] != null) + return "ID must be unique for each texture (conflict: ID = " + textureID + ")"; + + // Get path of the current texture. + var texturePath = this.reader.getString(children[i], 'path'); + + if (!this.fileExists(texturePath)) { + this.onXMLError("invalid path for texture with ID " + this.textureID); + continue; + } + else if (texturePath == null || texturePath == "") { + this.onXMLMinorError("failed to load texture with ID " + this.textureID); + continue; + } + + this.textures[textureID] = texturePath; + + } + + + return null; + } + + /** + * Parses the node. + * @param {materials block element} materialsNode + */ + parseMaterials(materialsNode) { + var children = materialsNode.children; + var attrError; + + this.materials = []; + + var grandChildren = []; + var nodeNames = []; + + // Any number of materials. + for (var i = 0; i < children.length; i++) { + // Attribute error flag + attrError = false; + + // Storing material information + var global = []; + var attributeNames = []; + var attributeTypes = []; + + if (children[i].nodeName != "material") { + this.onXMLMinorError("unknown tag <" + children[i].nodeName + ">"); + continue; + } + else { + attributeNames.push(...["shininess", "ambient", "diffuse", "specular", "emissive"]); + attributeTypes.push(...["positive float", "color", "color", "color", "color"]); + } + + // Get id of the current material. + var materialID = this.reader.getString(children[i], 'id'); + if (materialID == null) + return "no ID defined for material"; + + // Checks for repeated IDs. + if (this.materials[materialID] != null) + return "ID must be unique for each material (conflict: ID = " + materialID + ")"; + + // Specifications for the current material. + grandChildren = children[i].children; + + nodeNames = []; + for (var j = 0; j < grandChildren.length; j++) { + nodeNames.push(grandChildren[j].nodeName); + } + + for (var j = 0; j < attributeNames.length; j++) { + var attributeIndex = nodeNames.indexOf(attributeNames[j]); + + if (attributeIndex != -1) { + if (attributeTypes[j] == "positive float") { + var aux = this.parseFloat(grandChildren[attributeIndex], "value", attributeNames[j] + " attribute for material of ID " + materialID); + + if (typeof aux !== 'string' && aux <= 0) { + this.onXMLError("unable to parse non positive value of the " + attributeNames[j] + " attribute for material of ID " + materialID); + this.onXMLMinorError("unable to parse material with ID " + materialID); + attrError = true; + break; + } + } + else + var aux = this.parseColor(grandChildren[attributeIndex], attributeNames[j] + " attribute for material of ID " + materialID); + + if (typeof aux === 'string') { + this.onXMLError(aux); + this.onXMLMinorError("unable to parse material with ID " + materialID); + attrError = true; + break; + } + + global.push(aux); + } + else { + this.onXMLError("material " + attributeNames[i] + " block undefined for ID " + materialID); + attrError = true; + } + } + + if (!attrError) + this.materials[materialID] = global; + + } + + if (Object.keys(this.materials).length === 0) { + this.onXMLMinorError("at least one material must be defined. Default material will be used."); + this.defaultMaterial(); + } + + + return null; + } + + + /** + * Parses the node. + * @param {animations block element} animationsNode + */ + parseAnimations(animationsNode) { + var children = animationsNode.children; + this.animations = []; + + // Any number of animations. + for (var i = 0; i < children.length; i++) { + + if (children[i].nodeName != "animation") { + this.onXMLMinorError("unknown tag <" + children[i].nodeName + ">"); + continue; + } + + // Get id of the current animation. + var animationID = this.reader.getString(children[i], 'id'); + if (animationID == null) + return "no ID defined for animation"; + + // Checks for repeated IDs. + if (this.animations[animationID] != null) + return "ID must be unique for each animation (conflict: ID = " + animationID + ")"; + + // Keyframes for the current animation. + var grandChildren = children[i].children; + + var keyFrames = []; + for (var j = 0; j < grandChildren.length; j++) { + + var instant = this.parseFloat(grandChildren[j], 'instant', ' keyframe ' + j); + + if(instant <= 0) instant = 0.01; + + if (typeof instant === 'string' || (j > 0 && instant * 1000 <= keyFrames[j - 1]['instant'])) { + this.onXMLMinorError("unable to parse keyframe " + j + " instant time"); + instant = (keyFrames[j - 1]['instant'])/1000 + 0.2; + } + + var transformations = grandChildren[j].children; + + if (transformations.length < 5) + this.onXMLMinorError("transformations for the keyframe " + j + " are missing"); + + var transformationMatrix = this.parseKeyframeTransformations(transformations); + + if (typeof transformationMatrix === 'string') { + this.onXMLMinorError(transformationMatrix + " on keyframe " + j); + transformationMatrix = { translation: [0, 0, 0], rotationX: 0, rotationY: 0, rotationZ: 0, scale: [1, 1, 1] }; + } + + keyFrames.push(new Keyframe(this.scene, instant * 1000, transformationMatrix)); + } + + if (keyFrames.length > 0) + this.animations[animationID] = new KeyframeAnimation(this.scene, keyFrames); + else { + this.onXMLMinorError('No keyframes defined for animation ' + animationID); + } + } + + + return null; + } + + + /** + * Parses the block. + * @param {nodes block element} nodesNode + */ + parseNodes(nodesNode) { + var children = nodesNode.children; + + this.nodes = []; + this.spriteAnims = []; + + var grandChildren = []; + var grandgrandChildren = []; + var nodeNames = []; + var descendants = []; + var primitives = []; + var sprites = []; + var transformationMatrix, animationNode, materialNode, textureNode; + + // Any number of nodes. + for (var i = 0; i < children.length; i++) { + + if (children[i].nodeName != "node") { + this.onXMLMinorError("unknown tag <" + children[i].nodeName + ">"); + continue; + } + + // Get id of the current node. + var nodeID = this.reader.getString(children[i], 'id'); + if (nodeID == null) + return "no ID defined for nodeID"; + + // Checks for repeated IDs. + if (this.nodes[nodeID] != null) + return "ID must be unique for each node (conflict: ID = " + nodeID + ")"; + + grandChildren = children[i].children; + + nodeNames = []; + for (var j = 0; j < grandChildren.length; j++) { + nodeNames.push(grandChildren[j].nodeName); + } + + var transformationsIndex = nodeNames.indexOf("transformations"); + var materialIndex = nodeNames.indexOf("material"); + var textureIndex = nodeNames.indexOf("texture"); + var descendantsIndex = nodeNames.indexOf("descendants"); + var animationIndex = nodeNames.indexOf("animationref"); + + // Transformations + if (transformationsIndex == -1) { + this.onXMLMinorError("missing transformations block on node " + nodeID); + transformationMatrix = mat4.create(); + } + else { + grandgrandChildren = grandChildren[transformationsIndex].children; + + transformationMatrix = this.parseTransformations(grandgrandChildren); + + if (typeof transformationMatrix === 'string') + this.onXMLMinorError(transformationMatrix + "on node " + nodeID); + } + + // Animation + if (animationIndex == -1) { + animationNode = { 'id': null }; + } + else { + animationNode = this.reader.getString(grandChildren[animationIndex], 'id'); + + if (animationNode == null || this.animations == undefined || this.animations[animationNode] == null) { + this.onXMLMinorError("no ID defined for animationref on nodeID " + nodeID); + animationNode = { 'id': null }; + } + else { + animationNode = { 'id': animationNode }; + } + } + + // Material + if (materialIndex == -1) { + this.onXMLMinorError("missing material block on node " + nodeID); + materialNode = { "id": this.defaultMaterial() }; + } + else { + materialNode = this.parseNodeMaterial(grandChildren[materialIndex]); + + if (typeof materialNode === 'string') { + this.onXMLMinorError(materialNode + "on node " + nodeID); + } + + if (this.materials[materialNode.id] == null && materialNode.id != "null") { + this.onXMLMinorError("missing material on node " + nodeID + ". Default was used"); + materialNode = { "id": this.defaultMaterial() }; + } + + if (nodeID == this.idRoot && materialNode.id == "null") { + this.onXMLMinorError("Root's material shouldn't be null. Default material will be used."); + materialNode.id = this.defaultMaterial(); + } + } + + // Texture + if (textureIndex == -1) { + this.onXMLMinorError("missing texture block on node " + nodeID); + textureNode = { "id": "clear", "afs": 1, "aft": 1 }; + } + else { + + textureNode = this.parseNodeTexture(grandChildren[textureIndex]); + + if (typeof textureNode === 'string') + this.onXMLMinorError(textureNode + "on node " + nodeID); + + if (this.textures[textureNode.id] == null && textureNode.id != "null" && textureNode.id != "clear") { + this.onXMLMinorError("missing texture on node " + nodeID + ". No texture was used"); + textureNode = { "id": "clear", "afs": 1, "aft": 1 }; + } + + if (nodeID == this.idRoot && textureNode.id == "null") { + this.onXMLMinorError("Root's texture shouldn't be null. 'clear' will be used instead."); + textureNode.id = "clear"; + } + } + + // Descendants + descendants = []; + primitives = []; + sprites = []; + + if (descendantsIndex == -1) + this.onXMLMinorError("missing descendants block on node " + nodeID); + else { + grandgrandChildren = grandChildren[descendantsIndex].children; + + if (grandgrandChildren.length == 0) + this.onXMLMinorError("no descendants on node " + nodeID); + + for (var j = 0; j < grandgrandChildren.length; j++) { + + switch (grandgrandChildren[j].nodeName) { + case 'noderef': + var descendantId = this.reader.getString(grandgrandChildren[j], 'id'); + if (descendantId == null) + return "no ID defined for noderef of node " + nodeID; + + descendants.push(descendantId); + break; + + case 'leaf': + // Parse Sprites + if (this.reader.getString(grandgrandChildren[j], 'type') == 'spritetext' || + this.reader.getString(grandgrandChildren[j], 'type') == 'spriteanim') { + + var sprite = this.parseSpriteLeaf(grandgrandChildren[j]); + + if (Array.isArray(sprite)) { + this.onXMLNodeMultipleErrors(sprite, nodeID); + continue; + } + + sprites.push(sprite); + } + + // Parse other primitives + else { + + var primitive = this.parseLeaf(grandgrandChildren[j]); + + if (Array.isArray(primitive)) { + this.onXMLNodeMultipleErrors(primitive, nodeID); + continue; + } + + primitives.push(primitive); + + } + + break; + + default: + return "unknown tag <" + grandgrandChildren[j].nodeName + ">"; + } + } + } + + var node = new MyNode(this.scene, nodeID, transformationMatrix, materialNode.id, textureNode.id, animationNode.id, textureNode.afs, textureNode.aft, descendants, primitives, sprites); + this.nodes[nodeID] = node; + + } + } + + /** + * Parse the transformations from a node + * @param {block element} transformationsNode + */ + parseTransformations(transformationsNode) { + + var transformationMatrix = mat4.create(); + + // Any number of transformations. + for (var i = 0; i < transformationsNode.length; i++) { + + switch (transformationsNode[i].nodeName) { + case "translation": + var coords = this.parseCoordinates3D(transformationsNode[i], "translate transformation "); + + // Error check + if (typeof coords === 'string') + return coords; + + transformationMatrix = mat4.translate(transformationMatrix, transformationMatrix, coords); + break; + + case "rotation": + var angle = Utils.degToRad(this.reader.getFloat(transformationsNode[i], 'angle')); + var axis = this.reader.getString(transformationsNode[i], 'axis'); + + // Error check + if (!(angle != null && !isNaN(angle))) + return "unable to parse rotation angle of the rotation transformation "; + + switch (axis) { + case 'x': + transformationMatrix = mat4.rotateX(transformationMatrix, transformationMatrix, angle); + break; + case 'y': + transformationMatrix = mat4.rotateY(transformationMatrix, transformationMatrix, angle); + break; + case 'z': + transformationMatrix = mat4.rotateZ(transformationMatrix, transformationMatrix, angle); + break; + default: + return "unable to parse rotation axis of the rotation transformation "; + } + break; + + case "scale": + var coords = this.parseScaleCoordinates3D(transformationsNode[i], "scale transformation "); + + // Error check + if (typeof coords === 'string') + return coords; + + transformationMatrix = mat4.scale(transformationMatrix, transformationMatrix, coords); + break; + + default: + return "unknown transformation tag <" + transformationsNode[i].nodeName + ">"; + } + } + + return transformationMatrix; + } + + /** + * Creates a defaultMaterial + */ + defaultMaterial() { + if (this.materials['onErrorDefaultMaterial'] != null) + return 'onErrorDefaultMaterial'; + + var shininess = 10.0; + var ambient = [0, 1, 0, 1]; + var diffuse = [0, 1, 0, 1]; + var specular = [0, 1, 0, 1]; + var emissive = [0, 0, 0, 1]; + + this.materials['onErrorDefaultMaterial'] = [shininess, ambient, diffuse, specular, emissive]; + + return 'onErrorDefaultMaterial'; + } + + /** + * Parse the material from a node + * @param {block element} materialsNode + */ + parseNodeMaterial(materialsNode) { + var materialID = this.reader.getString(materialsNode, 'id'); + if (materialID == null) + return "no ID defined for material "; + + return { "id": materialID }; + } + + /** + * Parse the texture from a node + * @param {block element} textureNode + */ + parseNodeTexture(textureNode) { + var textureID = this.reader.getString(textureNode, 'id'); + if (textureID == null) + return "no ID defined for texture "; + + var textureChildren = textureNode.children; + var amps = []; + + if (textureChildren.length == 0) { + this.onXMLMinorError("No amplification tag was set on texture tag " + textureID); + amps = [1, 1]; + } + else { + + var element = textureChildren[0]; + + if (element.nodeName != "amplification") { + this.onXMLMinorError("unknown tag <" + element.nodeName + "> on texture tag " + textureID); + amps = [1, 1]; + } + else { + for (const iterator of ["afs", "aft"]) { + var aux = this.parseFloat(element, iterator, "texture " + textureID + " amplification"); + if (typeof aux === "string") { + this.onXMLMinorError(aux); + amps.push(1); + } + else { + amps.push(aux); + } + } + + } + } + + + return { "id": textureID, "afs": amps[0], "aft": amps[1] }; + } + + /** + * Parse a primitive + * @param {block element} leafNode + */ + parseLeaf(leafNode) { + + var type = this.reader.getString(leafNode, 'type'); + var primitive; + var primitiveData = []; + + switch (type) { + case 'triangle': + var x1 = this.parseFloat(leafNode, 'x1', type); + var y1 = this.parseFloat(leafNode, 'y1', type); + var x2 = this.parseFloat(leafNode, 'x2', type); + var y2 = this.parseFloat(leafNode, 'y2', type); + var x3 = this.parseFloat(leafNode, 'x3', type); + var y3 = this.parseFloat(leafNode, 'y3', type); + primitiveData.push(x1, y1, x2, y2, x3, y3); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Triangle "); + return primitiveData; + } + + primitive = new MyTriangle(this.scene, x1, y1, x2, y2, x3, y3); + break; + + case 'rectangle': + var x1 = this.parseFloat(leafNode, 'x1', type); + var y1 = this.parseFloat(leafNode, 'y1', type); + var x2 = this.parseFloat(leafNode, 'x2', type); + var y2 = this.parseFloat(leafNode, 'y2', type); + primitiveData.push(x1, y1, x2, y2); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Rectangle "); + return primitiveData; + } + + primitive = new MyRectangle(this.scene, x1, y1, x2, y2); + break; + + case 'cylinder': + var height = this.parseFloat(leafNode, 'height', type); + var topRadius = this.parseFloat(leafNode, 'topRadius', type); + var bottomRadius = this.parseFloat(leafNode, 'bottomRadius', type); + var stacks = this.parseInteger(leafNode, 'stacks', type); + var slices = this.parseInteger(leafNode, 'slices', type); + primitiveData.push(height, topRadius, bottomRadius, stacks, slices); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Cylinder "); + return primitiveData; + } + + primitive = new MyCylinder(this.scene, bottomRadius, topRadius, height, slices, stacks); + break; + + case 'sphere': + var radius = this.parseFloat(leafNode, 'radius', type); + var stacks = this.parseInteger(leafNode, 'stacks', type); + var slices = this.parseInteger(leafNode, 'slices', type); + primitiveData.push(radius, stacks, slices); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Sphere "); + return primitiveData; + } + + primitive = new MySphere(this.scene, radius, slices, stacks); + break; + + case 'torus': + var inner = this.parseFloat(leafNode, 'inner', type); + var outer = this.parseFloat(leafNode, 'outer', type); + var slices = this.parseInteger(leafNode, 'slices', type); + var loops = this.parseInteger(leafNode, 'loops', type); + primitiveData.push(inner, outer, slices, loops); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Torus "); + return primitiveData; + } + + primitive = new MyTorus(this.scene, inner, outer, slices, loops); + break; + + case 'plane': + var npartsU = this.parseInteger(leafNode, 'npartsU', type); + var npartsV = this.parseInteger(leafNode, 'npartsV', type); + primitiveData.push(npartsU, npartsV); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Plane "); + return primitiveData; + } + + primitive = new MyPlane(this.scene, npartsU, npartsV); + break; + + case 'patch': + var npointsU = this.parseInteger(leafNode, 'npointsU', type); + var npointsV = this.parseInteger(leafNode, 'npointsV', type); + var npartsU = this.parseInteger(leafNode, 'npartsU', type); + var npartsV = this.parseInteger(leafNode, 'npartsV', type); + primitiveData.push(npointsU, npointsV, npartsU, npartsV); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Patch "); + return primitiveData; + } + + + // Parse Control Points + + var controlPointsNodes = leafNode.children; + var controlPoints = []; + + // Check number of control points + if (controlPointsNodes.length != npointsU * npointsV) { + primitiveData.push("Wrong number of Control Points in Patch "); + return primitiveData; + } + + for (var i = 0; i < npointsU * npointsV; i++) { + if (controlPointsNodes[i].nodeName != "controlpoint") { + this.onXMLMinorError("unknown tag <" + controlPointsNodes[i].nodeName + ">"); + primitiveData.push("Unknown tag in patch "); + return primitiveData; + } + var coords = this.parseCoordinates3D(controlPointsNodes[i], 'coordinates of Patch control point '); + + if (typeof coords === 'string') { + primitiveData.push("Error parsing coordinates of Patch "); + return primitiveData; + } + + coords.push(1);// w = 1 + + controlPoints.push(coords); + } + + primitive = new MyPatch(this.scene, npointsU, npointsV, npartsU, npartsV, controlPoints); + break; + + case 'defbarrel': + var base = this.parseFloat(leafNode, 'base', type); + var middle = this.parseFloat(leafNode, 'middle', type); + var height = this.parseFloat(leafNode, 'height', type); + var slices = this.parseInteger(leafNode, 'slices', type); + var stacks = this.parseInteger(leafNode, 'stacks', type); + var angle = Utils.degToRad(30); + + primitiveData.push(base, middle, height, slices, stacks); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Barrel "); + return primitiveData; + } + + primitive = new MyBarrel(this.scene, base, middle, height, angle, slices, stacks); + break; + + case 'cube': + primitive = new MyCube(this.scene); + break; + + case 'water': + var x1 = this.parseFloat(leafNode, 'x1', type); + var y1 = this.parseFloat(leafNode, 'y1', type); + var x2 = this.parseFloat(leafNode, 'x2', type); + var y2 = this.parseFloat(leafNode, 'y2', type); + primitiveData.push(x1, y1, x2, y2); + + if (this.hasXMLErrors(primitiveData)) { + primitiveData.push("Failed drawing Water "); + return primitiveData; + } + + primitive = new MyWater(this.scene, x1, y1, x2, y2); + break; + + case null: + primitiveData.push("No type defined for a primitive"); + return primitiveData; + default: + primitiveData.push("Invalid type defined for a primitive"); + return primitiveData; + } + + return primitive; + } + + /** + * Parse a sprite primitive + * @param {block element} spriteNode + */ + parseSpriteLeaf(spriteNode) { + var type = this.reader.getString(spriteNode, 'type'); + + var sprite; + var spriteData = []; + + switch (type) { + + case 'spritetext': + var text = this.reader.getString(spriteNode, 'text'); + sprite = new MySpriteText(this.scene, text); + break; + + case 'spriteanim': + var spriteID = this.reader.getString(spriteNode, 'ssid'); + if (spriteID == null || this.spritesheets[spriteID] == null) { + spriteData.push("No valid ID defined for SpriteSheet "); + return spriteData; + } + + var duration = this.parseFloat(spriteNode, 'duration', type); + var startCell = this.parseInteger(spriteNode, 'startCell', type); + var endCell = this.parseInteger(spriteNode, 'endCell', type); + spriteData.push(duration, startCell, endCell); + + if (this.hasXMLErrors(spriteData)) { + spriteData.push("Failed reading SpriteAnim "); + return spriteData; + } + + duration *= 1000; + sprite = new MySpriteAnimation(this.scene, spriteID, startCell, endCell, duration); + this.spriteAnims.push(sprite); + break; + + default: + spriteData.push("Invalid type defined for a sprite"); + return spriteData; + } + + return sprite; + } + + + /** + * Parse the animations keyframe transformations + * @param {block element} transformationsNode + */ + parseKeyframeTransformations(transformationsNode) { + + var transformations = {}; + var check = 0; + + // Any number of transformations. + for (var i = 0; i < transformationsNode.length; i++) { + + switch (transformationsNode[i].nodeName) { + case "translation": + var coords = this.parseCoordinates3D(transformationsNode[i], "translate transformation "); + + // Error check + if (typeof coords === 'string') + return coords; + + transformations.translation = coords; + check++; + break; + + case "rotation": + var angle = Utils.degToRad(this.reader.getFloat(transformationsNode[i], 'angle')); + var axis = this.reader.getString(transformationsNode[i], 'axis'); + + // Error check + if (!(angle != null && !isNaN(angle))) + return "unable to parse rotation angle of the rotation transformation "; + + switch (axis) { + case 'x': + transformations.rotationX = angle; + check++; + break; + case 'y': + transformations.rotationY = angle; + check++; + break; + case 'z': + transformations.rotationZ = angle; + check++; + break; + default: + return "unable to parse rotation axis of the rotation transformation "; + } + break; + + case "scale": + var coords = this.parseScaleCoordinates3D(transformationsNode[i], "scale transformation "); + + // Error check + if (typeof coords === 'string') + return coords; + + transformations.scale = coords; + check++; + break; + + default: + return "unknown transformation tag <" + transformationsNode[i].nodeName + ">"; + } + } + + if (check < 5) + return "missing correct transformations specifications "; + + return transformations; + } + + /** + * Parse boolean value from a node + * @param {block element} node + * @param {string} name - the name of the XML value to be verified + * @param {string} messageError - message to be displayed in case of error + */ + parseBoolean(node, name, messageError) { + var boolVal = this.reader.getBoolean(node, name); + if (!(boolVal != null && !isNaN(boolVal) && (boolVal == true || boolVal == false))) { + this.onXMLMinorError("unable to parse value component " + messageError + "; assuming 'value = 1'"); + return true; + } + + return boolVal; + } + + /** + * Parse integer value from a node + * @param {block element} node + * @param {string} name - the name of the XML value to be verified + * @param {string} messageError - message to be displayed in case of error + */ + parseInteger(node, name, messageError) { + var intVal = this.reader.getInteger(node, name); + if (!(intVal != null && !isNaN(intVal))) + return "unable to parse " + name + " of the " + messageError; + + return intVal; + } + + /** + * Parse float value from a node + * @param {block element} node + * @param {string} name - the name of the XML value to be verified + * @param {string} messageError - message to be displayed in case of error + */ + parseFloat(node, name, messageError) { + var floatVal = this.reader.getFloat(node, name); + if (!(floatVal != null && !isNaN(floatVal))) + return "unable to parse " + name + " of the " + messageError; + + return floatVal; + } + + /** + * Parse the coordinates from a node + * @param {block element} node + * @param {string} messageError - message to be displayed in case of error + */ + parseCoordinates3D(node, messageError) { + var position = []; + + // x + var x = this.reader.getFloat(node, 'x'); + if (!(x != null && !isNaN(x))) + return "unable to parse x-coordinate of the " + messageError; + + // y + var y = this.reader.getFloat(node, 'y'); + if (!(y != null && !isNaN(y))) + return "unable to parse y-coordinate of the " + messageError; + + // z + var z = this.reader.getFloat(node, 'z'); + if (!(z != null && !isNaN(z))) + return "unable to parse z-coordinate of the " + messageError; + + position.push(...[x, y, z]); + + return position; + } + + /** + * Parse the scale coordinates from a node + * @param {block element} node + * @param {string} messageError - message to be displayed in case of error + */ + parseScaleCoordinates3D(node, messageError) { + var position = []; + + // sx + var sx = this.reader.getFloat(node, 'sx'); + if (!(sx != null && !isNaN(sx))) + return "unable to parse x-coordinate of the " + messageError; + + // sy + var sy = this.reader.getFloat(node, 'sy'); + if (!(sy != null && !isNaN(sy))) + return "unable to parse y-coordinate of the " + messageError; + + // sz + var sz = this.reader.getFloat(node, 'sz'); + if (!(sz != null && !isNaN(sz))) + return "unable to parse z-coordinate of the " + messageError; + + position.push(...[sx, sy, sz]); + + return position; + } + + /** + * Parse the coordinates from a node + * @param {block element} node + * @param {string} messageError - message to be displayed in case of error + */ + parseCoordinates4D(node, messageError) { + var position = []; + + //Get x, y, z + position = this.parseCoordinates3D(node, messageError); + + if (!Array.isArray(position)) + return position; + + + // w + var w = this.reader.getFloat(node, 'w'); + if (!(w != null && !isNaN(w))) + return "unable to parse w-coordinate of the " + messageError; + + position.push(w); + + return position; + } + + /** + * Parse the color components from a node + * @param {block element} node + * @param {string} messageError - message to be displayed in case of error + */ + parseColor(node, messageError) { + var color = []; + + // R + var r = this.reader.getFloat(node, 'r'); + if (!(r != null && !isNaN(r) && r >= 0 && r <= 1)) + return "unable to parse R component of the " + messageError; + + // G + var g = this.reader.getFloat(node, 'g'); + if (!(g != null && !isNaN(g) && g >= 0 && g <= 1)) + return "unable to parse G component of the " + messageError; + + // B + var b = this.reader.getFloat(node, 'b'); + if (!(b != null && !isNaN(b) && b >= 0 && b <= 1)) + return "unable to parse B component of the " + messageError; + + // A + var a = this.reader.getFloat(node, 'a'); + if (!(a != null && !isNaN(a) && a >= 0 && a <= 1)) + return "unable to parse A component of the " + messageError; + + color.push(...[r, g, b, a]); + + return color; + } + + /** + * Displays the scene, processing each node, starting in the root node. + */ + displayScene() { + this.processNode(this.idRoot, "null", "null") + } + + /** + * Processes a node. + * @param {string} id + * @param {string} materialParent + * @param {string} textureParent + */ + processNode(id, materialParent, textureParent) { + + var node = this.nodes[id]; + var appliedMaterial = undefined; + var appliedTexture = undefined; + + if (node == undefined) { + // this.onXMLError("node " + id + " is not defined"); + return; + } + + if (node.animationID == null || this.animations[node.animationID].isActive) { + + this.scene.pushMatrix(); + + if (node.transformations != null) + this.scene.multMatrix(node.transformations); + + if (node.animationID != null) { + this.animations[node.animationID].apply(); + } + + // Set applied material to the material of the node itself + if (node.materialID != "null") + appliedMaterial = node.materialID; + // Set applied material to the parent's material + else if (materialParent != "null") + appliedMaterial = materialParent; + + // Set applied texture to the texture of the node itself + if (node.textureID != "null") + appliedTexture = node.textureID; + // Set applied texture to the parent's texture + else if (textureParent != "null") + appliedTexture = textureParent; + + + // Apply material & texture + if (this.scene.materials[appliedMaterial] != undefined) { + switch (appliedTexture) { + case "clear": + this.scene.materials[appliedMaterial].setTexture(null); + break; + case undefined: + this.onXMLError("Processing nodes error - Undefined Texture (parent and child have null texture)"); + return; + default: + this.scene.materials[appliedMaterial].setTexture(this.scene.textures[appliedTexture]); + break; + } + + this.scene.materials[appliedMaterial].apply(); + } + else { + this.onXMLError("Processing nodes error - Undefined Material (parent and child have null materials)"); + return; + } + + + // Display primitives + node.primitives.forEach(primitive => { + if (typeof primitive.setAmplification == 'function') { + primitive.setAmplification(node.afs, node.aft); + } + primitive.display() + }); + + // Display sprites + node.sprites.forEach(sprite => { sprite.display() }); + + // Process descendant nodes + for (var i = 0; i < node.descendants.length; i++) { + + if (this.nodes[node.descendants[i]] == undefined) { + this.onXMLError("node " + node.descendants[i] + " is not defined"); + node.descendants.splice(i, 1); // Remove undefined nodes from descendants + i--; + } + else + this.processNode(node.descendants[i], appliedMaterial, appliedTexture); + } + + this.scene.popMatrix(); + } + } +} \ No newline at end of file diff --git a/frontend/XMLscene.js b/frontend/XMLscene.js new file mode 100644 index 0000000..933785f --- /dev/null +++ b/frontend/XMLscene.js @@ -0,0 +1,268 @@ +/** + * XMLscene class, representing the scene that is to be rendered. + * https://paginas.fe.up.pt/~ruirodrig/pub/sw/webcgf/docs/#!/api + */ +class XMLscene extends CGFscene { + /** + * @constructor + * @param {MyInterface} myinterface + */ + constructor(myinterface) { + super(); + + this.interface = myinterface; + } + + /** + * Initializes the scene, setting some WebGL defaults, initializing the camera and the axis. + * @param {CGFApplication} application + */ + init(application) { + super.init(application); + + this.sceneInited = false; + + this.enableTextures(true); + + this.gl.clearDepth(100.0); + this.gl.enable(this.gl.DEPTH_TEST); + this.gl.enable(this.gl.CULL_FACE); + this.gl.depthFunc(this.gl.LEQUAL); + + this.axis = new CGFaxis(this); + this.setUpdatePeriod(100); + + this.loadingProgressObject = new MyRectangle(this, -1, -.1, 1, .1); + this.loadingProgress = 0; + + // Default CGF appearance + this.defaultAppearance = new CGFappearance(this); + + // Default CGF camera + this.camera = new CGFcamera(0.4, 0.1, 500, vec3.fromValues(15, 15, 0), vec3.fromValues(0, 0, 0)); + + this.cameras = []; // To store CGFcamera & CGFcameraOrtho objects + this.selectedCamera = null; // interface's currently selected Camera + this.materials = []; // To store CGFappearance objects + this.textures = []; // To store CGFtexture objects + this.spritesheets = []; + } + + /** + * Initializes the scene cameras. + */ + initCameras() { + + // Reads the views from the scene graph. + for (var key in this.graph.views) { + + if (this.graph.views.hasOwnProperty(key)) { + var graphView = this.graph.views[key]; + + if (graphView[0] == "perspective") { + this.cameras[key] = new CGFcamera( + Utils.degToRad(graphView[3]), // angle + graphView[1], // near + graphView[2], // far + vec3.fromValues(...graphView[4]), // from + vec3.fromValues(...graphView[5])); // to + } + else if (graphView[0] == "ortho") { + this.cameras[key] = new CGFcameraOrtho( + graphView[3], // left + graphView[4], // right + graphView[5], // bottom + graphView[6], // top + graphView[1], // near + graphView[2], // far + vec3.fromValues(...graphView[7]), // from + vec3.fromValues(...graphView[8]), // to + vec3.fromValues(...graphView[9])); // up + } + } + } + + // XML default camera ID + this.camera = this.cameras[this.selectedCamera]; + this.interface.setActiveCamera(this.camera); + + // Add Selected Camera Option to Interface Gui + this.interface.initCameras(this, 'selectedCamera', this.cameras); + } + + /** + * Initializes the scene lights with the values read from the XML file. + */ + initLights() { + + // Lights index. + var i = 0; + + // Reads the lights from the scene graph. + for (var key in this.graph.lights) { + if (i >= 8) + break; // Only eight lights allowed by WebCGF on default shaders. + + if (this.graph.lights.hasOwnProperty(key)) { + var graphLight = this.graph.lights[key]; + + this.lights[i].setPosition(...graphLight[1]); + this.lights[i].setAmbient(...graphLight[2]); + this.lights[i].setDiffuse(...graphLight[3]); + this.lights[i].setSpecular(...graphLight[4]); + this.lights[i].name = key; + + this.lights[i].setVisible(true); + if (graphLight[0]) + this.lights[i].enable(); + else + this.lights[i].disable(); + + this.lights[i].update(); + + i++; + } + } + + // Add Lights Enabled Option to Interface Gui + this.interface.initLights(this.lights, 'enabled'); + } + + initMaterials() { + // Reads the materials from the scene graph + for (var key in this.graph.materials) { + if (this.graph.materials.hasOwnProperty(key)) { + var graphMaterial = this.graph.materials[key]; + + this.materials[key] = new CGFappearance(this); + + this.materials[key].setShininess(graphMaterial[0]); + this.materials[key].setAmbient(...graphMaterial[1]); + this.materials[key].setDiffuse(...graphMaterial[2]); + this.materials[key].setSpecular(...graphMaterial[3]); + this.materials[key].setEmission(...graphMaterial[4]); + + this.materials[key].setTextureWrap('REPEAT', 'REPEAT'); + } + } + } + + initTextures() { + // Reads the textures from the scene graph + for (var key in this.graph.textures) { + if (this.graph.textures.hasOwnProperty(key)) { + var graphTexturePath = this.graph.textures[key]; + + this.textures[key] = new CGFtexture(this, graphTexturePath); + } + } + this.textures['mySpriteTextTexture'] = new CGFtexture(this, './scenes/images/spriteTextBlack.png'); + } + + initSpritesheets() { + // Reads the spritesheets from the scene graph + for (var key in this.graph.spritesheets) { + if (this.graph.spritesheets.hasOwnProperty(key)) { + var graphSpritesheet = this.graph.spritesheets[key]; + + var texture = new CGFtexture(this, graphSpritesheet[0]); + var sizeM = graphSpritesheet[1]; + var sizeN = graphSpritesheet[2]; + + this.spritesheets[key] = new MySpriteSheet(this, texture, sizeM, sizeN); + } + } + } + + /** + * Handler called when the graph is finally loaded. + * As loading is asynchronous, this may be called already after the application has started the run loop + */ + onGraphLoaded() { + this.axis = new CGFaxis(this, this.graph.referenceLength); + + this.gl.clearColor(...this.graph.background); + + this.setGlobalAmbientLight(...this.graph.ambient); + + this.initCameras(); + + this.initLights(); + + this.initMaterials(); + + this.initSpritesheets(); + + this.initTextures(); + + this.sceneInited = true; + } + + update(t) { + + if (this.sceneInited) { + // Update Animations + for (var key in this.graph.animations) { + if (this.graph.animations.hasOwnProperty(key)) { + this.graph.animations[key].update(t); + } + } + + // Update SpriteAnimations + for (var key in this.graph.spriteAnims) { + if (this.graph.spriteAnims.hasOwnProperty(key)) { + this.graph.spriteAnims[key].update(t); + } + } + } + } + + /** + * Displays the scene. + */ + display() { + // ---- BEGIN Background, camera and axis setup + + // Clear image and depth buffer everytime we update the scene + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + + // Initialize Model-View matrix as identity (no transformation + this.updateProjectionMatrix(); + this.loadIdentity(); + + // Apply transformations corresponding to the camera position relative to the origin + this.applyViewMatrix(); + + this.pushMatrix(); + + for (var i = 0; i < this.lights.length; i++) { + this.lights[i].setVisible(true); + this.lights[i].enable(); + } + + if (this.sceneInited) { + // Draw axis + this.axis.display(); + + this.defaultAppearance.apply(); + + // Displays the scene (MySceneGraph function). + this.graph.displayScene(); + + } + else { + // Show some "loading" visuals + this.defaultAppearance.apply(); + + this.rotate(-this.loadingProgress / 10.0, 0, 0, 1); + + this.loadingProgressObject.display(); + this.loadingProgress++; + } + + this.popMatrix(); + // ---- END Background, camera and axis setup + } + +} \ No newline at end of file diff --git a/frontend/animation/CameraAnimation.js b/frontend/animation/CameraAnimation.js new file mode 100644 index 0000000..61236da --- /dev/null +++ b/frontend/animation/CameraAnimation.js @@ -0,0 +1,71 @@ +/** + * CameraAnimation class, representing an animated move from the initialCamera to the finalCamera. + */ +class CameraAnimation extends MyAnimation { + constructor(scene, initialCamera, finalCamera) { + super(scene, 0, 2000, null, null); + + this.initCameras(initialCamera, finalCamera); + + // Total time of the animation + this.totalTime = this.finalTime - this.initialTime; + + // Total time passed + this.sumT = 0; + + // Last t from update function arg + this.lastT = 0; + } + + initCameras(initialCamera, finalCamera) { + + this.initialCamera = initialCamera; + + this.initPosition = this.initialCamera.position.slice(0, 3); + this.initTarget = this.initialCamera.target.slice(0, 3); + this.initNear = this.initialCamera.near; + this.initFar = this.initialCamera.far; + this.initFov = this.initialCamera.fov; + + this.finalCamera = finalCamera; + this.finalPosition = this.finalCamera.position.slice(0, 3); + this.finalTarget = this.finalCamera.target.slice(0, 3); + this.finalNear = this.finalCamera.near; + this.finalFar = this.finalCamera.far; + this.finalFov = this.finalCamera.fov; + } + + update(t) { + // If animation time has ended + if (this.sumT > this.totalTime) { + this.scene.camera = this.finalCamera; + return false; + } + + if (this.lastT == 0) + this.lastT = t; + + var deltaT = t - this.lastT; + this.lastT = t; + + this.sumT += deltaT; + + let elapsedPercentage = Math.max((this.totalTime - this.sumT) / this.totalTime, 0); + + let position = [0, 0, 0], target = [0, 0, 0]; + + vec3.lerp(position, this.finalPosition, this.initPosition, elapsedPercentage); + vec3.lerp(target, this.finalTarget, this.initTarget, elapsedPercentage); + + let near = this.finalNear + elapsedPercentage * (this.initNear - this.finalNear); + let far = this.finalFar + elapsedPercentage * (this.initFar - this.finalFar); + let fov = this.finalFov + elapsedPercentage * (this.initFov - this.finalFov); + + this.scene.camera = new CGFcamera(fov, near, far, position, target); + + return true; + } + + // For the Animator calling + animate(t) { return this.update(t); } +} \ No newline at end of file diff --git a/frontend/animation/Keyframe.js b/frontend/animation/Keyframe.js new file mode 100644 index 0000000..930eaec --- /dev/null +++ b/frontend/animation/Keyframe.js @@ -0,0 +1,21 @@ +/** + * Keyframe + * @constructor + * @param scene - Reference to MyScene object + * @param instant + * @param transformations + */ + +class Keyframe { + + constructor(scene, instant, transformations) { + this.scene = scene; + + this.instant = instant; + + this.transformations = transformations; + this.translation = transformations.translation; + this.rotation = [transformations.rotationX, transformations.rotationY, transformations.rotationZ]; + this.scale = transformations.scale; + } +} diff --git a/frontend/animation/KeyframeAnimation.js b/frontend/animation/KeyframeAnimation.js new file mode 100644 index 0000000..4a4b2f8 --- /dev/null +++ b/frontend/animation/KeyframeAnimation.js @@ -0,0 +1,127 @@ +/** + * KeyframeAnimation + * @constructor + * @param scene - Reference to MyScene object + * @param keyframes - Array of keyframes + */ +class KeyframeAnimation extends MyAnimation { + + constructor(scene, keyframes, callback = null) { + // Animation class initializes initialTime, lastTime, initialTransformations and finalTransformations + super(scene, 0, keyframes[keyframes.length - 1].instant, + { translation: [0, 0, 0], rotationX: 0, rotationY: 0, rotationZ: 0, scale: [1, 1, 1] }, keyframes[keyframes.length - 1].transformations); + + // Total time of the animation + this.totalTime = this.finalTime - this.initialTime; + + // Total time passed + this.sumT = 0; + + // Last t from update function arg + this.lastT = 0; + + // First keyframe => Neutral transformations when t=0 + this.keyframes = [new Keyframe(scene, 0, this.initialTransformations)]; + + // Remaining keyframes + this.keyframes.push(...keyframes); + + // Index of actual and next keyframes + this.actualKF = 0; + this.nextKF = 1; + + // Percentage of time elapsed within the two keyframes + this.elapsedPercentage = 0; + + // Current Animation Matrix + this.currentState = mat4.create(); + + // If only one keyframe was set in the XML make the transformation sticky + if(this.keyframes.length == 2){ + this.keyframes[0] = new Keyframe(scene, this.keyframes[1].instant, this.keyframes[1].transformations); + this.buildAnimation(); + } + + // If Animation has started or not + this.isActive = false; + + this.isEnded = false; + this.callback = callback; + } + + update(t) { + + // If animation time has ended + if (this.sumT > this.totalTime) { + this.isActive = true; // guarantee that it shows the node in time, if only 1 keyframe was set + if(!this.isEnded){ + if(this.callback != null) + this.callback(); + this.isEnded = true; + } + return; + } + + if (this.lastT == 0) + this.lastT = t; + + var deltaT = t - this.lastT; + this.lastT = t; + + this.sumT += deltaT; + + if(this.keyframes[this.nextKF] == undefined) + return; + + // If still in the current keyframe + if (this.keyframes[this.nextKF].instant > this.sumT) { + + if (this.actualKF == 0) + return; + + this.isActive = true; + + // Total time elapsed within the two keyframes + let elapsedTime = this.sumT - this.keyframes[this.actualKF].instant; + + // Percentage of time passed within the two keyframes + this.elapsedPercentage = elapsedTime / (this.keyframes[this.nextKF].instant - this.keyframes[this.actualKF].instant); + + this.buildAnimation(); + } + else { // Next keyframe + this.elapsedPercentage = 1; + this.buildAnimation(); + this.actualKF++; + this.nextKF++; + } + } + + buildAnimation() { + + // Interpolate translate, rotate and scale values based on percentage + this.currentState = mat4.create(); + + var translation = [0, 0, 0]; + vec3.lerp(translation, this.keyframes[this.actualKF].translation, this.keyframes[this.nextKF].translation, this.elapsedPercentage); + + mat4.translate(this.currentState, this.currentState, translation); + + var rotation = [0, 0, 0]; + vec3.lerp(rotation, this.keyframes[this.actualKF].rotation, this.keyframes[this.nextKF].rotation, this.elapsedPercentage); + + mat4.rotate(this.currentState, this.currentState, rotation[0], [1, 0, 0]); + mat4.rotate(this.currentState, this.currentState, rotation[1], [0, 1, 0]); + mat4.rotate(this.currentState, this.currentState, rotation[2], [0, 0, 1]); + + var scale = [0, 0, 0]; + vec3.lerp(scale, this.keyframes[this.actualKF].scale, this.keyframes[this.nextKF].scale, this.elapsedPercentage); + + mat4.scale(this.currentState, this.currentState, scale); + } + + apply() { + this.scene.multMatrix(this.currentState); + } + +} diff --git a/frontend/animation/MyAnimation.js b/frontend/animation/MyAnimation.js new file mode 100644 index 0000000..3411f8b --- /dev/null +++ b/frontend/animation/MyAnimation.js @@ -0,0 +1,35 @@ +/** + * MyAnimation + * @constructor + * @param scene - Reference to MyScene object + * @param initialTime + * @param finalTime + * @param initialTransformations + * @param finalTransformations + */ + +class MyAnimation { + + constructor(scene, initialTime, finalTime, initialTransformations, finalTransformations) { + + if (this.constructor === MyAnimation) { + throw new TypeError('MyAnimation is an abstract class'); + } + + this.scene = scene; + + this.initialTime = initialTime; + this.finalTime = finalTime; + + this.initialTransformations = initialTransformations; + this.finalTransformations = finalTransformations; + } + + update(t) { + + } + + apply() { + + } +} diff --git a/frontend/assets/background.png b/frontend/assets/background.png new file mode 100644 index 0000000..f2100d1 Binary files /dev/null and b/frontend/assets/background.png differ diff --git a/frontend/game/GameOrchestrator.js b/frontend/game/GameOrchestrator.js new file mode 100644 index 0000000..33c4bb4 --- /dev/null +++ b/frontend/game/GameOrchestrator.js @@ -0,0 +1,167 @@ +/** + * GameOrchestrator class, representing the game scene that is to be rendered. + * https://paginas.fe.up.pt/~ruirodrig/pub/sw/webcgf/docs/#!/api + */ +class GameOrchestrator extends GameScene { + constructor(myinterface) { + super(myinterface); + this.animator = new MyAnimator(); + this.prolog = new PrologController(8081); + this.stateMachine = new StateMachine(this, this.prolog, this.animator); + + this.gameStates = []; + + // players[0] - black player level; players[1] - white player level + // 0 - human, 1 - random, 2 - greedy + this.players = [0, 0]; + } + + init(application) { + super.init(application); + + // enable picking + this.setPickEnabled(true); + } + + setGame(settings) { + + // Get initial board + this.prolog.send('initial', `[${settings['board']},B]`, null, null, false); + + // Set initial game state + let gameState = new MyGameState(this.prolog.getResult()[1]); // Save received board + this.stateMachine.setGameState(gameState); + + // Init Board Tiles and Pieces + let theme = this.themes[this.selectedTheme]; + this.board.initTiles(this.prolog.getResult()[1], theme); + + + // Set Computers + if (settings['player-p2'] == 'computer') + this.players[1] = parseInt(settings['level-p2']); + + if (settings['player-p1'] == 'computer') + this.players[0] = parseInt(settings['level-p1']); + + this.prolog.send( + 'valid_moves', + `[${gameState.getBoard()},${gameState.player},M]`, + null, + null, + true); + + this.timer.setState(TimerState.START); + } + + display() { + //-- For Picking + if (this.getPlayer() == 0) + this.handlePicking(); + this.clearPickRegistration(); + if ((this.stateMachine.state > State.PLAYER) || (this.getPlayer() != 0) || (this.timer.state > TimerState.STOP)) + this.stateMachine.next_state(); + + super.display(); + } + + update(t) { + super.update(t); + + this.timer.update(t); + + this.animator.animate(t); + } + + handlePicking() { + + if (this.pickMode == false) { + if (this.pickResults != null && this.pickResults.length > 0) { + for (var i = 0; i < this.pickResults.length; i++) { + var obj = this.pickResults[i][0]; + if (obj) { + if (this.stateMachine.state == State.INIT) + this.stateMachine.next_state(obj, null) + else if (this.stateMachine.state == State.PLAYER) + this.stateMachine.next_state(null, obj) + } + } + this.pickResults.splice(0, this.pickResults.length); + } + } + } + + pushState(gameState) { + this.gameStates.push(gameState); + } + + popState() { + if (this.gameStates.length > 0) + return this.gameStates.pop(); + return new MyGameState(this.board.initialBoard); + } + + undo() { + let sum = this.players[0] + this.players[1]; + let mult = this.players[0] * this.players[1]; + if (sum == 0) { // ONLY HUMANS + let gameState = this.popState(); + if (gameState != null) { + this.stateMachine.state = State.UNDO; + this.stateMachine.setGameState(gameState); + } + } + else if (mult == 0 && this.getPlayer() == 0) { // CURRENT PLAYER HUMAN + let gameState = this.popState(); + if (gameState != null) { + this.stateMachine.state = State.DOUBLE_UNDO1; + this.stateMachine.setGameState(gameState); + } + } + } + + getPlayer() { + return this.stateMachine.gameState.player == 1 ? this.players[0] : this.players[1]; + } + + replay() { + + let sum = this.players[0] + this.players[1]; + let mult = this.players[0] * this.players[1]; + + + // ONLY HUMANS OR CURRENT PLAYER HUMAN OR GAMEOVER + if (sum == 0 || (mult == 0 && this.getPlayer() == 0) || this.stateMachine.gameOver) { + + // Pause the timer + this.timer.setState(TimerState.PAUSE); + + if (!this.stateMachine.gameOver) { + let validMoves = this.prolog.getResult()[2]; + for (let validMove of validMoves) { + let tile = this.board.getTile(validMove[1], validMove[2]) + tile.setInvalid(); + } + + if (this.stateMachine.elem1 != null) + this.stateMachine.elem1.deselect(); + } + + for (let e of this.playerPieces) { + e.moveToPiece(e.initPosition); + if (e.initColor != e.color) { + e.turn(); + } + e.rotate(180 * (e.color == "black")); + } + + for (let e of this.bonusPieces) { + e.moveToPiece(e.initPosition); + } + + this.bonusBoard.available = 0; + this.stateMachine.replayState = 0; + this.stateMachine.state = State.REPLAY; + } + } +} \ No newline at end of file diff --git a/frontend/game/GameScene.js b/frontend/game/GameScene.js new file mode 100644 index 0000000..e37a175 --- /dev/null +++ b/frontend/game/GameScene.js @@ -0,0 +1,449 @@ +/** + * GameScene class, representing the scene that is to be rendered. + * https://paginas.fe.up.pt/~ruirodrig/pub/sw/webcgf/docs/#!/api + */ +class GameScene extends CGFscene { + constructor(myinterface) { + super(); + this.interface = myinterface; + + this.themes = [] + this.selectedTheme = null + + this.board = null + this.bonusBoard = null + this.piecesBoard = null + this.marker = null + this.timer = null + this.playerPieces = []; + this.bonusPieces = []; + this.wallsPieces = []; + this.jokerPieces = []; + } + + /** + * Initializes the scene, setting some WebGL defaults, initializing the camera and the axis. + * @param {CGFApplication} application + */ + init(application) { + + super.init(application); + + this.sceneInited = false; + + this.enableTextures(true); + + this.gl.clearDepth(100.0); + this.gl.enable(this.gl.DEPTH_TEST); + this.gl.enable(this.gl.CULL_FACE); + this.gl.depthFunc(this.gl.LEQUAL); + + this.axis = new CGFaxis(this); + this.setUpdatePeriod(100); + + this.loadingProgressObject = new MyRectangle(this, -1, -.1, 1, .1); + this.loadingProgress = 0; + + // Default CGF appearance + this.defaultAppearance = new CGFappearance(this); + + // Default CGF camera + this.camera = new CGFcamera(0.4, 0.1, 500, vec3.fromValues(15, 15, 0), vec3.fromValues(0, 0, 0)); + + this.cameras = []; // To store CGFcamera & CGFcameraOrtho objects + this.selectedCamera = null; // interface's currently selected Camera + this.materials = []; // To store CGFappearance objects + this.textures = []; // To store CGFtexture objects + this.textures['mySpriteTextTexture'] = new CGFtexture(this, './scenes/images/spriteTextBlack.png'); + this.spritesheets = []; + this.waterShader = new CGFshader(this.gl, "./shaders/water.vert", "./shaders/water.frag"); + this.waterShader.setUniformsValues({ uSampler2: 1 }); + this.waterShader.setUniformsValues({ timeFactor: 0 }); + + } + + /** + * Initializes the scene cameras. + */ + initCameras(graph) { + + // Reads the views from the scene graph. + for (var key in graph.views) { + + if (graph.views.hasOwnProperty(key)) { + var graphView = graph.views[key]; + + if (graphView[0] == "perspective" && this.cameras[key] == undefined) { + this.cameras[key] = new CGFcamera( + Utils.degToRad(graphView[3]), // angle + graphView[1], // near + graphView[2], // far + vec3.fromValues(...graphView[4]), // from + vec3.fromValues(...graphView[5])); // to + } + else if (graphView[0] == "ortho" && this.cameras[key] == undefined) { + this.cameras[key] = new CGFcameraOrtho( + graphView[3], // left + graphView[4], // right + graphView[5], // bottom + graphView[6], // top + graphView[1], // near + graphView[2], // far + vec3.fromValues(...graphView[7]), // from + vec3.fromValues(...graphView[8]), // to + vec3.fromValues(...graphView[9])); // up + } + } + } + // XML default camera ID + this.camera = this.cameras[this.selectedCamera]; + + // Add Selected Camera Option to Interface Gui + this.interface.initCameras(this, 'selectedCamera'); + } + + /** + * Initializes the scene lights with the values read from the XML file. + */ + initLights(graph) { + + for (let light of this.lights) { + delete light.name; + light.disable(); + light.update(); + } + + // Lights index. + let i = 0; + + // Reads the lights from the scene graph. + for (let key in graph.lights) { + if (i >= 8) + break; // Only eight lights allowed by WebCGF on default shaders. + + if (graph.lights.hasOwnProperty(key)) { + let graphLight = graph.lights[key]; + + this.lights[i].setPosition(...graphLight[1]); + this.lights[i].setAmbient(...graphLight[2]); + this.lights[i].setDiffuse(...graphLight[3]); + this.lights[i].setSpecular(...graphLight[4]); + this.lights[i].name = key; + + this.lights[i].setVisible(true); + if (graphLight[0]) + this.lights[i].enable(); + else + this.lights[i].disable(); + + this.lights[i].update(); + + i++; + } + } + + // Add Lights Enabled Option to Interface Gui + this.interface.initLights(this.lights, 'enabled'); + } + + /** + * Initializes the game pieces with the default theme. + */ + initPieces() { + + let theme = this.themes[this.selectedTheme]; + + // Init Main Board + this.board = new MyBoard(this, + theme.boardGraph, + theme.playableTileGraph, + theme.nonPlayableTileGraph); + + // Init Bonus Board + this.bonusBoard = new MyAuxiliarBoard(this, + theme.bonusBoardGraph, + theme.nonPlayableTileGraph, 0, -3.675, 1, 8, 7); + + // Init Pieces Board + this.piecesBoard = new MyAuxiliarBoard(this, + theme.piecesBoardGraph, + theme.nonPlayableTileGraph, -3.15, -3.675, 7, 8, -10); + + // Init Points Marker + this.marker = new MyMarker(this, theme.markerGraph); + + // Init Timer + this.timer = new MyTimer(this, theme.timerGraph); + + // Init Player Pieces + let piecePickId = 53; + let offsetX = -13.15; + let offsetZ; + + for (let x = 0; x < 7; x++) { + offsetZ = -3.675; + for (let z = 0; z < ((x == 0) ? 4 : 8); z++) { + let playerPiece = new MyPlayerPiece(this, theme.playerGraph, 'white', piecePickId); + playerPiece.moveTo(offsetX + x, 0.27, offsetZ + z); + playerPiece.setInitPosition(); + this.playerPieces.push(playerPiece); + + offsetZ += 0.05; + piecePickId++; + } + offsetX += 0.05; + } + + } + + /** + * Sets the Themes and creates the dropdown interface. + */ + setThemes(sthemes) { + this.themes = []; + let defaultTheme = sthemes[0]; + for (let i = sthemes.length - 1; i >= 0; i--) { + let theme = sthemes[i]; + this.themes[theme] = new MyTheme(this, theme, i == 0) + } + this.setCurrentTheme(defaultTheme, false); + this.interface.initThemes(this, "selectedTheme", Object.keys(this.themes)); + this.initPieces(); + } + + /** + * Updates the pieces graphs with the current theme settings. + */ + setCurrentTheme(stheme, update) { + this.selectedTheme = stheme + let theme = this.themes[this.selectedTheme]; + + if (update) { + // Update Main Board Graph + this.board.setGraph(theme.boardGraph, + theme.playableTileGraph, + theme.nonPlayableTileGraph); + + // Update Bonus Board Graph + this.bonusBoard.setGraph(theme.bonusBoardGraph, + theme.nonPlayableTileGraph); + + // Update Pieces Board Graph + this.piecesBoard.setGraph(theme.piecesBoardGraph, + theme.nonPlayableTileGraph); + + // Update Player Pieces Board Graph + this.playerPieces.forEach(element => { + element.setGraph(theme.playerGraph); + }) + + // Update Bonus Pieces Graph + this.bonusPieces.forEach(element => { + element.setGraph(theme.bonusGraph); + }) + + // Update Wall Pieces Graph + this.wallsPieces.forEach(element => { + element.setGraph(theme.wallGraph); + }) + + // Update Joker Pieces Graph + this.jokerPieces.forEach(element => { + element.setGraph(theme.jokerGraph); + }) + + // Update Points Marker Graph + this.marker.setGraph(theme.markerGraph); + + // Update Timer Marker Graph + this.timer.setGraph(theme.timerGraph); + + this.gl.clearColor(...theme.graph.background); + this.setGlobalAmbientLight(...theme.graph.ambient); + this.initLights(theme.graph); + } + } + + /** + * Adds loaded textures from graph into the scene textures list. + */ + addTexturesFromGraph(graph) { + for (var key in graph.textures) { + if (graph.textures.hasOwnProperty(key)) { + var graphTexturePath = graph.textures[key]; + + this.textures[key] = new CGFtexture(this, graphTexturePath); + } + } + } + + /** + * Adds loaded materials from graph into the scene materials list. + */ + addMaterialsFromGraph(graph) { + // Reads the materials from the scene graph + for (var key in graph.materials) { + if (graph.materials.hasOwnProperty(key)) { + var graphMaterial = graph.materials[key]; + + this.materials[key] = new CGFappearance(this); + + this.materials[key].setShininess(graphMaterial[0]); + this.materials[key].setAmbient(...graphMaterial[1]); + this.materials[key].setDiffuse(...graphMaterial[2]); + this.materials[key].setSpecular(...graphMaterial[3]); + this.materials[key].setEmission(...graphMaterial[4]); + + this.materials[key].setTextureWrap('REPEAT', 'REPEAT'); + } + } + } + + /** + * Adds loaded spritesheets from graph into the scene spritesheets list. + */ + addSpritesheetsFromGraph(graph) { + // Reads the spritesheets from the scene graph + for (var key in graph.spritesheets) { + if (graph.spritesheets.hasOwnProperty(key)) { + var graphSpritesheet = graph.spritesheets[key]; + + var texture = new CGFtexture(this, graphSpritesheet[0]); + var sizeM = graphSpritesheet[1]; + var sizeN = graphSpritesheet[2]; + + this.spritesheets[key] = new MySpriteSheet(this, texture, sizeM, sizeN); + } + } + } + + onGraphLoaded(graph) { + + this.axis = new CGFaxis(this, graph.referenceLength); + + this.gl.clearColor(...graph.background); + + this.setGlobalAmbientLight(...graph.ambient); + + this.initLights(graph); + + this.initCameras(graph); + + this.sceneInited = true; + + console.log("Scene Initiated"); + } + + update(t) { + + // Update Animations + for (var key in this.themes[this.selectedTheme].graph.animations) { + if (this.themes[this.selectedTheme].graph.animations.hasOwnProperty(key)) { + this.themes[this.selectedTheme].graph.animations[key].update(t); + } + } + + // Update SpriteAnimations + for (var key in this.themes[this.selectedTheme].graph.spriteAnims) { + if (this.themes[this.selectedTheme].graph.spriteAnims.hasOwnProperty(key)) { + this.themes[this.selectedTheme].graph.spriteAnims[key].update(t); + } + } + + this.waterShader.setUniformsValues({ timeFactor: t / 500 % 1000 }); + } + + /** + * Displays all the Game pieces and boards. + */ + displayGame() { + + this.themes[this.selectedTheme].graph.displayScene(); + + this.board.display(); + this.bonusBoard.display(); + this.piecesBoard.display(); + this.marker.display(); + this.timer.display(); + + this.playerPieces.forEach(element => { + element.display(); + }) + this.bonusPieces.forEach(element => { + element.display(); + }) + this.wallsPieces.forEach(element => { + element.display(); + }) + this.jokerPieces.forEach(element => { + element.display(); + }) + + } + + display() { + + // ---- BEGIN Background, camera and axis setup + + // Clear image and depth buffer everytime we update the scene + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + this.gl.enable(this.gl.DEPTH_TEST); + + // Initialize Model-View matrix as identity (no transformation + this.updateProjectionMatrix(); + this.loadIdentity(); + + // Apply transformations corresponding to the camera position relative to the origin + this.applyViewMatrix(); + + this.pushMatrix(); + + if(!this.updatedLights){ + for (let light of this.lights) + light.update(); + this.updatedLights = true; + } + + if (this.sceneInited) { + // Draw axis + this.axis.display(); + + this.defaultAppearance.apply(); + + // Displays the game pieces and boards. + this.displayGame(); + } + else { + // Show some "loading" visuals + this.defaultAppearance.apply(); + + this.rotate(-this.loadingProgress / 10.0, 0, 0, 1); + + this.loadingProgressObject.display(); + this.loadingProgress++; + } + + this.popMatrix(); + // ---- END Background, camera and axis setup + } + + /** + * Returns an available piece to be picked. + */ + getAvailablePiece() { + for (let piece of this.playerPieces) { + if (piece.canPick) { + return piece; + } + } + } + + /** + * Returns the current theme name. + */ + getThemeName() { + return this.selectedTheme; + } + +} \ No newline at end of file diff --git a/frontend/game/MyAnimator.js b/frontend/game/MyAnimator.js new file mode 100644 index 0000000..252de8c --- /dev/null +++ b/frontend/game/MyAnimator.js @@ -0,0 +1,38 @@ +/** +* MyAnimator class, for animating the GameMoves. +*/ +class MyAnimator { + constructor() { + this.moves = []; + } + + addMove(move) { + this.moves.push(move); + } + + /** + * Called when the GameScene is updating. + * It animates all the assigned GameMoves and deletes the finished ones. + */ + animate(t) { + + let new_moves = []; + + for (let i = 0; i < this.moves.length; i++) { + if (this.moves[i].animate(t)) + new_moves.push(this.moves[i]) + } + + this.moves = new_moves; + } + + /** + * If there is no GameMove remaining, returns false. + */ + isActive() { + return this.moves.length > 0; + } +} + + + diff --git a/frontend/game/MyGameMove.js b/frontend/game/MyGameMove.js new file mode 100644 index 0000000..45089c6 --- /dev/null +++ b/frontend/game/MyGameMove.js @@ -0,0 +1,142 @@ +/** +* MyGameMove class, responsible for animating a specific piece. +*/ +class MyGameMove { + constructor(piece) { + this.piece = piece; + } + + /** + * Factory Method, returning an already prepared KeyframeAnimation. + * This animation translates the piece from one position to the other + * while rotating it in the air, if needed. + * When finished, executes the callback function. + */ + static animateTranslateRotate(scene, initP, finalP, rotate, callback) { + let xTranslate = finalP.x - initP.x; + let zTranslate = finalP.z - initP.z; + + let pi = Utils.degToRad(180); + + let initialT = { + translation: [0, 0, 0], + rotationX: 0, + rotationY: 0, + rotationZ: 0, + scale: [1, 1, 1] + }; + + let intermediateT1 = { + translation: [xTranslate / 3, 1.8, zTranslate / 3], + rotationX: 0, + rotationY: 0, + rotationZ: rotate ? -pi / 3 : 0, + scale: [1, 1, 1] + }; + + let intermediateT2 = { + translation: [2 * xTranslate / 3, 1, 2 * zTranslate / 3], + rotationX: 0, + rotationY: 0, + rotationZ: rotate ? -2 * pi / 3 : 0, + scale: [1, 1, 1] + }; + + let finalT = { + translation: [xTranslate, 0, zTranslate], + rotationX: 0, + rotationY: 0, + rotationZ: rotate ? -pi : 0, + scale: [1, 1, 1] + }; + + let keyFrames = [ + new Keyframe(scene, 0, initialT), + new Keyframe(scene, 250, intermediateT1), + new Keyframe(scene, 500, intermediateT2), + new Keyframe(scene, 1000, finalT)]; + + return new KeyframeAnimation(scene, keyFrames, callback); + } + + /** + * Factory Method, returning an already prepared KeyframeAnimation. + * This animation rotates the piece in the place. + * When finished, executes the callback function. + */ + static animateRotate(scene, initAngle, finalAngle, callback) { + + let deltaAngle = Utils.degToRad(finalAngle) - Utils.degToRad(initAngle); + + let offset = 1; + if (deltaAngle < 0) offset = -1; + + let initialT = { + translation: [0, 0, 0], + rotationX: 0, + rotationY: 0, + rotationZ: 0, + scale: [1, 1, 1] + }; + + let intermediateT1 = { + translation: [0, offset * 1, 0], + rotationX: 0, + rotationY: 0, + rotationZ: deltaAngle / 4, + scale: [1, 1, 1] + }; + + let intermediateT2 = { + translation: [0, offset * 0.8, 0], + rotationX: 0, + rotationY: 0, + rotationZ: deltaAngle / 2, + scale: [1, 1, 1] + }; + + let intermediateT3 = { + translation: [0, offset * 0.8, 0], + rotationX: 0, + rotationY: 0, + rotationZ: 3 * deltaAngle / 4, + scale: [1, 1, 1] + }; + + let finalT = { + translation: [0, offset * 0.4, 0], + rotationX: 0, + rotationY: 0, + rotationZ: deltaAngle, + scale: [1, 1, 1] + }; + + let keyFrames = [ + new Keyframe(scene, 1000, initialT), + new Keyframe(scene, 1250, intermediateT1), + new Keyframe(scene, 1500, intermediateT2), + new Keyframe(scene, 1750, intermediateT3), + new Keyframe(scene, 2000, finalT)]; + + return new KeyframeAnimation(scene, keyFrames, callback); + } + + setAnimation(animation) { + this.piece.animation = animation; + } + + /** + * This is called by the animator and returns false when the animation has ended. + */ + animate(t) { + if (this.piece.animation != null) { + this.piece.animation.update(t); + if (this.piece.animation.isEnded) { + this.piece.animation = null; + return false; + } + return true; + } + return false; + } +} \ No newline at end of file diff --git a/frontend/game/MyGameState.js b/frontend/game/MyGameState.js new file mode 100644 index 0000000..72be6f2 --- /dev/null +++ b/frontend/game/MyGameState.js @@ -0,0 +1,36 @@ +/** +* MyGameState class, saving the current gameState information and board. +*/ +class MyGameState { + constructor(board, player = 1, blackPoints = 0, whitePoints = 0) { + this.board = board; + this.blackPoints = blackPoints; + this.whitePoints = whitePoints; + this.player = player; + this.turns = []; + this.piece = null; + this.tile = null; + this.bonus = null; + this.initPosition = null; + } + + setPlayer(player) { + this.player = player; + } + + setTurns(turns) { + this.turns = turns; + } + + setPieces(piece, tile, bonus = null) { + this.piece = piece; + this.initPosition = { x: piece.x, y: piece.y, z: piece.z }; + this.tile = tile; + this.bonus = bonus; + } + + getBoard() { + return JSON.stringify(this.board).replaceAll("\"", ""); + } + +} diff --git a/frontend/game/MyTheme.js b/frontend/game/MyTheme.js new file mode 100644 index 0000000..5095f36 --- /dev/null +++ b/frontend/game/MyTheme.js @@ -0,0 +1,52 @@ +/** +* MyTheme class, saving the current theme information and graphs. +*/ +class MyTheme { + constructor(scene, name, def) { + this.name = name + + this.graph = new ScenarioSceneGraph(this.name + "/scene.xml", scene, def); + this.boardGraph = new GameSceneGraph(this.name + "/board.xml", scene) + this.bonusBoardGraph = new GameSceneGraph(this.name + "/bonus_board.xml", scene) + this.piecesBoardGraph = new GameSceneGraph(this.name + "/pieces_board.xml", scene) + this.playerGraph = new GameSceneGraph(this.name + "/pieces/player.xml", scene) + this.bonusGraph = new GameSceneGraph(this.name + "/pieces/bonus.xml", scene) + this.wallGraph = new GameSceneGraph(this.name + "/pieces/wall.xml", scene) + this.jokerGraph = new GameSceneGraph(this.name + "/pieces/joker.xml", scene) + this.nonPlayableTileGraph = new GameSceneGraph(this.name + "/non_playable_tile.xml", scene) + this.playableTileGraph = new GameSceneGraph(this.name + "/playable_tile.xml", scene) + this.markerGraph = new GameSceneGraph(this.name + "/marker.xml", scene); + this.timerGraph = new GameSceneGraph(this.name + "/timer.xml", scene); + + this.graphs = [this.graph, this.boardGraph, this.bonusBoardGraph, this.piecesBoardGraph, this.playerGraph, this.bonusGraph, this.wallGraph, this.jokerGraph, this.nonPlayableTileGraph, this.playableTileGraph, this.markerGraph]; + } + + /** + * Returns true if all graphs were loaded, false otherwise. + */ + is_loaded() { + for (const g of this.graphs) { + if (!g.loadedOk) { + return false; + } + } + + return true; + } + + /** + * Returns the total percentage of graphs loaded. + */ + loading_percentage() { + let total = this.graphs.length; + let current = total; + + for (const g of this.graphs) { + if (!g.loadedOk) { + current--; + } + } + this.loaded = Math.floor(current / total * 100); + return this.loaded; + } +} \ No newline at end of file diff --git a/frontend/game/PrologController.js b/frontend/game/PrologController.js new file mode 100644 index 0000000..5e14279 --- /dev/null +++ b/frontend/game/PrologController.js @@ -0,0 +1,64 @@ +/** +* PrologController class, responsible for sending requests to the Prolog Server. +*/ +class PrologController { + + constructor(port = 8081) { + this.port = port; + this.request = undefined; + } + + /** + * Sends a GET request to the Prolog Server + * Request is of the form pred-args, args being a list of arguments for the predicate to be called + */ + send(pred, args, onSuccess, onError, asyncr = true, timeout = 3000, onTimeout) { + let request = new XMLHttpRequest(); + request.open( + "GET", + "http://localhost:" + this.port + "/" + pred + "-" + args, + asyncr + ); + + request.onload = + onSuccess || + function (data) { + // console.log("Request successful. Reply: ", JSON.parse(data.target.response)); + request.result = JSON.parse(data.target.response); + }; + + request.onerror = + onError || + function () { + console.log("Error waiting for response"); + }; + + if (asyncr) + request.timeout = timeout; + + request.ontimeout = + onTimeout || + function () { + console.log("Timeout for response. Sending again"); + request.send(); + }; + + request.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded; charset=UTF-8" + ); + + request.send(); + this.request = request; + + return request; + } + + getRequest() { + return this.request; + } + + getResult() { + return this.request.result; + } +} \ No newline at end of file diff --git a/frontend/game/StateMachine.js b/frontend/game/StateMachine.js new file mode 100644 index 0000000..41dec9b --- /dev/null +++ b/frontend/game/StateMachine.js @@ -0,0 +1,576 @@ +const State = { "INIT": 1, "PLAYER": 2, "TILE": 3, "ANIM": 4, "UNDO": 5, "OVER": 6, "END": 7, "REPLAY": 8, "DOUBLE_UNDO1": 9, "DOUBLE_UNDO2": 10, "TIMEUP": 11 } +Object.freeze(State) + +/** +* StateMachine class, the game logic controller for each gameState. +*/ +class StateMachine { + constructor(scene, prolog, animator) { + this.elem1 = null; + this.elem2 = null; + this.elem3 = null; + + this.scene = scene; + this.prolog = prolog; + this.animator = animator; + this.state = State.INIT; + this.gameState = null; + this.nextGameState = null; + + this.replayState = 0; + this.gameOver = false; + } + + setGameState(gameState) { + this.gameState = gameState; + } + + getWinner() { + if (this.gameState.blackPoints > this.gameState.whitePoints) + return 1; + else if (this.gameState.whitePoints > this.gameState.blackPoints) + return -1; + else + return 0; + } + + init_state(elem1) { + + // Time Up + if (this.scene.timer.isTimeUp()) + return State.TIMEUP; + + + // Wait for previous animations + if (this.animator.isActive()) return State.INIT; + + + // Restart countdown + if (this.scene.timer.isInactive()) { + this.scene.timer.setState(TimerState.START) + return State.INIT; + } + // Resume countdown + if (this.scene.timer.isPaused()) { + this.scene.timer.setState(TimerState.ACTIVE) + return State.INIT; + } + + + let result = this.prolog.getResult(); + if (result == undefined) return State.INIT; + + // Start the move + this.elem1 = null; + this.elem2 = null; + this.elem3 = null; + + // ---- Computer Move ---- + // Select player piece + if (this.scene.getPlayer() != 0) { + this.elem1 = this.scene.getAvailablePiece(); + this.elem1.select(); + return State.PLAYER; + } + + // ---- Human move ---- + + // Select player piece + if (elem1 != null && elem1.constructor.name == "MyPlayerPiece") { + this.elem1 = elem1; + this.elem1.select(); + + // Show valid moves + for (let validMove of result[2]) { + let tile = this.scene.board.getTile(validMove[1], validMove[2]) + tile.setValid(); + } + return State.PLAYER; + } + + return State.INIT; + } + + player_state(elem2) { + + if (this.scene.timer.isTimeUp()) + return State.TIMEUP; + + let turns = []; + let result = this.prolog.getResult(); + let validMoves = result[2]; + + // ---- Human move ---- + if (this.scene.getPlayer() == 0) { + + // The second piece is a Player Piece + if (elem2 != null && elem2.constructor.name == "MyPlayerPiece") { + this.elem1.deselect(); + this.elem1 = elem2; + this.elem1.select(); + return State.PLAYER; + } + + // The second piece is not a Tile nor a Bonus + if (elem2 == null || (elem2.constructor.name != "MyTile" && elem2.constructor.name != "MyBonus")) + return State.INIT; + + // The second piece is a Tile or a Bonus + this.elem2 = elem2; + + // Check if the move is valid - loop through list of valid moves + for (let validMove of validMoves) { + if (validMove[1] == this.elem2.row && validMove[2] == this.elem2.col) { + turns = validMove[3]; + break; + } + } + + } + + // ---- Computer move ---- + else { + + let validMove; + + // Random Computer Level + if (this.scene.getPlayer() == 1) + validMove = validMoves[Math.floor(Math.random() * validMoves.length)] + + // Greedy Computer Level + else + validMove = validMoves[validMoves.length - 1] + + let chosenTile = this.scene.board.getTile(validMove[1], validMove[2]); + if (chosenTile.piece != null && chosenTile.piece.constructor.name == "MyBonus") + this.elem2 = chosenTile.piece; + else this.elem2 = chosenTile; + turns = validMove[3]; + } + + // If the move is valid, it will turn at least a piece + if (turns.length == 0) return State.PLAYER; + + // Stop and reset the timer + this.scene.timer.setState(TimerState.STOP); + + // Move is valid + // Disable picking + this.elem1.disablePick(); + this.elem2.disablePick(); + + // Add Piece to Tile + if (this.elem2.constructor.name == "MyBonus") { // Destiny is a Tile with a Bonus + + let tile = this.elem2.getTile(); + this.elem2.removeTile(); + tile.addPiece(this.elem1); + + this.elem3 = this.elem2; + this.elem2 = tile; + } + else // Destiny is an empty Tile + this.elem2.addPiece(this.elem1); + + if (this.scene.getPlayer() == 0) { + for (let validMove of validMoves) { + let tile = this.scene.board.getTile(validMove[1], validMove[2]) + tile.setInvalid(); + } + } + + + // Get new game state + this.prolog.send( + 'move', + `[${this.gameState.getBoard()},${this.gameState.player},${this.elem2.row},${this.elem2.col},${JSON.stringify(turns)},${this.gameState.blackPoints},${this.gameState.whitePoints},NewBP,NewWP,NewGameState]`, + null, null, true); + + if (this.gameState.player == 1) + this.elem1.turn(); + + this.gameState.setTurns(turns); + this.gameState.setPieces(this.elem1, this.elem2, this.elem3); + this.scene.pushState(this.gameState); + + return State.TILE; + } + + tile_state() { + + let result = this.prolog.getResult(); + if (result == undefined) + return State.TILE; + + // Creates an animation for the player piece + let move = new MyGameMove(this.elem1); + move.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + this.elem1, + this.elem2, + this.elem1.color == 'black', + () => { + this.elem1.moveToPiece(this.elem2); + if (this.elem1.color == 'black') + this.elem1.rotate(180); + })); + + this.animator.addMove(move); + + // Creates an animation for the bonus piece + if (this.elem3 != null && this.elem3.constructor.name == 'MyBonus') { + let bonusMove = new MyGameMove(this.elem3); + let tile = this.scene.bonusBoard.availableTile(); + bonusMove.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + this.elem3, + tile, + false, + () => { + this.elem3.moveToPiece(tile); + })); + this.animator.addMove(bonusMove); + } + + // Creates an animation for the turnable pieces + for (let turn of this.gameState.turns) { + let piece = this.scene.board.getTile(turn[0], turn[1]).piece; + piece.turn(); + + let turnMove = new MyGameMove(piece); + + let angles = { init: 0, final: 180 }; + if (this.elem1.color != 'black') + angles = { init: 180, final: 0 }; + + turnMove.setAnimation(MyGameMove.animateRotate( + this.scene, + angles.init, + angles.final, + () => { + piece.rotate(angles.final); + })); + this.animator.addMove(turnMove); + } + + // Sets the new gameState + let nextGameState = new MyGameState( + result[result.length - 1], + -this.gameState.player, + result[result.length - 3], + result[result.length - 2]); + this.nextGameState = nextGameState; + + // Gets valid moves for the next player + this.prolog.send('valid_moves', `[${nextGameState.getBoard()},${nextGameState.player},M]`, null, null, true); + + return State.ANIM; + } + + undo_state() { + this.scene.timer.setState(TimerState.STOP); + + // Resets the tile material for the valid moves + if (!this.gameOver) { + + let validMoves = this.prolog.getResult()[2]; + for (let validMove of validMoves) { + let tile = this.scene.board.getTile(validMove[1], validMove[2]) + tile.setInvalid(); + } + } + + if (this.gameState.piece == null) { + if (this.elem1 != null) + this.elem1.deselect(); + return State.INIT; + } + + if (this.elem1 != this.gameState.piece) { + this.elem1.deselect(); + } + + // Creates an animation for the player piece + let move = new MyGameMove(this.gameState.piece); + this.gameState.piece.rotate(0); + move.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + this.gameState.piece, + this.gameState.initPosition, + false, + () => { + this.gameState.piece.moveToPiece(this.gameState.initPosition); + this.gameState.piece.color = 'white'; + this.gameState.piece.enablePick(); + this.gameState.piece.deselect(); + this.gameState.tile.removePiece(); + this.gameState.tile.enablePick(); + })); + + this.animator.addMove(move); + + // Creates an animation for the bonus piece + if (this.gameState.bonus != null) { + let bonusMove = new MyGameMove(this.gameState.bonus); + bonusMove.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + this.gameState.bonus, + this.gameState.tile, + false, + () => { + this.gameState.bonus.moveToPiece(this.gameState.tile); + this.gameState.bonus.enablePick(); + this.gameState.tile.addPiece(this.gameState.bonus); + this.gameState.bonus.setTile(this.gameState.tile); + this.scene.bonusBoard.restoreTile(); + })); + this.animator.addMove(bonusMove); + } + + // Creates an animation for the turnable pieces + for (let turn of this.gameState.turns) { + let piece = this.scene.board.getTile(turn[0], turn[1]).piece; + piece.turn(); + + let turnMove = new MyGameMove(piece); + + let angles = { init: 180, final: 0 }; + if (this.gameState.piece.color != 'black') + angles = { init: 0, final: 180 }; + + turnMove.setAnimation(MyGameMove.animateRotate( + this.scene, + angles.init, + angles.final, + () => { + piece.rotate(angles.final); + })); + this.animator.addMove(turnMove); + } + + // Gets valid moves for the next player + this.prolog.send('valid_moves', `[${this.gameState.getBoard()},${this.gameState.player},M]`, null, null, false); + + this.scene.marker.setMarker(this.gameState.blackPoints, this.gameState.whitePoints, this.gameState.player); + + this.gameOver = false; + + return State.INIT; + } + + animation_state() { + + let result = this.prolog.getResult(); + + if (!this.animator.isActive() && result != undefined) { + + let moves = result[2]; + + // If the current player cannot play + if (moves.length == 0) { + + this.prolog.send('valid_moves', `[${this.nextGameState.getBoard()},${-this.nextGameState.player},M]`, null, null, false); + result = this.prolog.getResult(); + moves = result[2]; + + // If the next player also cannot play + if (moves.length == 0) { + // GameOver was reached + this.prolog.send('get_total_points', `[${this.nextGameState.getBoard()},${this.nextGameState.blackPoints},${this.nextGameState.whitePoints},TB,TW]`) + this.scene.timer.setState(TimerState.STOP) + return State.OVER; + } + + // Changing player + this.nextGameState.setPlayer(-this.nextGameState.player); + + } + this.scene.marker.setMarker(this.nextGameState.blackPoints, this.nextGameState.whitePoints, this.nextGameState.player) + this.gameState = this.nextGameState; + this.scene.timer.setState(TimerState.START) + return State.INIT; + } + + return State.ANIM; + } + + game_over_state() { + let result = this.prolog.getResult(); + + if (result != undefined) { + this.gameOver = true; + + let gameState = new MyGameState( + this.gameState.board, + -this.gameState.player, + result[result.length - 2], + result[result.length - 1]); + + this.gameState = gameState; + + this.scene.marker.setMarker(this.gameState.blackPoints, this.gameState.whitePoints, this.gameState.player, this.getWinner()) + + return State.END; + } + + return State.OVER; + + } + + replay_state() { + + if (!this.animator.isActive()) { + if (this.replayState < this.scene.gameStates.length) { + + let gameState = this.scene.gameStates[this.replayState]; + gameState.piece.elevated = false; + gameState.piece.select(); + + // Creates an animation for the player piece + let move = new MyGameMove(gameState.piece); + move.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + gameState.piece, + gameState.tile, + gameState.player == 1, + () => { + gameState.piece.moveToPiece(gameState.tile); + if (gameState.player == 1) { + gameState.piece.rotate(180); + gameState.piece.turn(); + } + })); + + this.animator.addMove(move); + + // Creates an animation for the bonus piece + if (gameState.bonus != null) { + let bonusMove = new MyGameMove(gameState.bonus); + let tile = this.scene.bonusBoard.availableTile(); + bonusMove.setAnimation(MyGameMove.animateTranslateRotate( + this.scene, + gameState.bonus, + tile, + false, + () => { + gameState.bonus.moveToPiece(tile); + })); + this.animator.addMove(bonusMove); + } + + // Creates an animation for the turnable pieces + for (let turn of gameState.turns) { + let piece = this.scene.board.getTile(turn[0], turn[1]).piece; + piece.turn(); + + let turnMove = new MyGameMove(piece); + + let angles = { init: 0, final: 180 }; + if (gameState.player != 1) + angles = { init: 180, final: 0 }; + + turnMove.setAnimation(MyGameMove.animateRotate( + this.scene, + angles.init, + angles.final, + () => { + piece.rotate(angles.final); + })); + this.animator.addMove(turnMove); + } + + this.scene.marker.setMarker(this.scene.gameStates[this.replayState].blackPoints, this.scene.gameStates[this.replayState].whitePoints, this.scene.gameStates[this.replayState].player) + + this.replayState++; + + return State.REPLAY; + } + + this.scene.marker.setMarker(this.gameState.blackPoints, this.gameState.whitePoints, this.gameState.player) + + if (this.gameOver) + return State.OVER; + + return State.INIT; + } + + return State.REPLAY; + } + + timeup_state() { + + if (!this.gameOver) { + + let validMoves = this.prolog.getResult()[2]; + for (let validMove of validMoves) { + let tile = this.scene.board.getTile(validMove[1], validMove[2]) + tile.setInvalid(); + } + + if (this.elem1 != null) + this.elem1.deselect(); + } + + this.prolog.send('get_total_points', `[${this.gameState.getBoard()},${this.gameState.blackPoints},${this.gameState.whitePoints},TB,TW]`) + return State.OVER; + } + + double_undo_state1() { + this.undo_state(); + return State.DOUBLE_UNDO2; + } + + double_undo_state2() { + let result = this.prolog.getResult(); + + if (!this.animator.isActive() && result != undefined) { + let gameState = this.scene.popState(); + if (gameState != null) { + this.setGameState(gameState); + } + return this.undo_state(); + } + return State.DOUBLE_UNDO2; + } + + next_state(elem1 = null, elem2 = null) { + + switch (this.state) { + case State.INIT: + this.state = this.init_state(elem1); + break; + case State.PLAYER: + this.state = this.player_state(elem2); + break; + case State.TILE: + this.state = this.tile_state(); + break; + case State.UNDO: + this.state = this.undo_state(); + break; + case State.DOUBLE_UNDO1: + this.state = this.double_undo_state1(); + break; + case State.DOUBLE_UNDO2: + this.state = this.double_undo_state2(); + break; + case State.ANIM: + this.state = this.animation_state(); + break; + case State.OVER: + this.state = this.game_over_state(); + break; + case State.END: + break; + case State.REPLAY: + this.state = this.replay_state(); + break; + case State.TIMEUP: + this.state = this.timeup_state(); + break; + default: + this.state = State.INIT; + break; + } + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyAuxiliarBoard.js b/frontend/game/elements/MyAuxiliarBoard.js new file mode 100644 index 0000000..4e374f5 --- /dev/null +++ b/frontend/game/elements/MyAuxiliarBoard.js @@ -0,0 +1,68 @@ +/** + * MyAuxiliarBoard class, representing an auxiliar game board for the pieces to be moved. + */ +class MyAuxiliarBoard extends MyGameElement { + constructor(scene, graph, tileGraph, startX, startZ, columns, rows, boardX, pickId = -1) { + super(scene, graph, pickId); + + this.startX = startX; + this.startZ = startZ; + this.columns = columns; + this.rows = rows; + this.tiles = []; + this.tileGraph = tileGraph; + this.available = 0; + + this.moveTo(boardX, 0, 0); + this.initTiles(); + } + + initTiles() { + + let offsetX = this.startX + this.x; + let offsetZ; + + for (let x = 0; x < this.columns; x++) { + offsetZ = this.startZ; + for (let z = 0; z < this.rows; z++) { + let tile = new MyTile(this.scene, this.tileGraph); + tile.moveTo(offsetX + x, 0.26, offsetZ + z); + this.tiles.push(tile); + + offsetZ += 0.05; + } + offsetX += 0.05; + } + } + + setGraph(graph, tileGraph) { + super.setGraph(graph); + this.tileGraph = tileGraph + + this.tiles.forEach(element => { + element.setGraph(this.tileGraph); + }) + } + + display() { + this.scene.pushMatrix(); + this.scene.translate(this.x, this.y, this.z); + this.graph.display(); + this.scene.popMatrix(); + + this.scene.pushMatrix(); + this.tiles.forEach(element => { + element.display(); + }) + this.scene.popMatrix(); + } + + availableTile() { + this.available++; + return this.tiles[this.available - 1]; + } + + restoreTile() { + this.available--; + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyBoard.js b/frontend/game/elements/MyBoard.js new file mode 100644 index 0000000..7d88af9 --- /dev/null +++ b/frontend/game/elements/MyBoard.js @@ -0,0 +1,126 @@ +/** + * MyBoard class, representing the main game board. + */ +class MyBoard extends MyGameElement { + constructor(scene, graph, tileGraph, wallGraph, pickId = -1) { + super(scene, graph, pickId); + + this.initialBoard = null; + this.tiles = []; + this.tileGraph = tileGraph; + this.wallGraph = wallGraph; + } + + /** + * Sets the Tiles and its Pieces - picking ids from 1 to 52 (64 spaces - 8 walls - 4 central pieces ) + */ + initTiles(initialBoard, theme) { + this.initialBoard = initialBoard; + let pickId = 1; + let offsetX = -4.725, offsetZ; + let tile, piece; + + for (let x = 0; x < 10; x++) { + offsetZ = 4.725; + for (let z = 0; z < 10; z++) { + + // Set type of tile and row/col + if (x > 0 && x < 9 && z > 0 && z < 9) { + tile = new MyTile(this.scene, this.tileGraph); + tile.setRowCol(x, z); + } + else { + tile = new MyTile(this.scene, this.wallGraph); + tile.setRowCol(x, z); + } + + // Set tile piece + switch (initialBoard[x][z]) { + case 'wall': + piece = new MyPiece(this.scene, theme.wallGraph); + piece.moveTo(offsetX + x, 0.27, offsetZ - z); + tile.addPiece(piece); + this.scene.wallsPieces.push(piece); + break; + + case 'joker': + piece = new MyPiece(this.scene, theme.jokerGraph); + piece.moveTo(offsetX + x, 0.27, offsetZ - z); + tile.addPiece(piece); + this.scene.jokerPieces.push(piece); + break; + + case 'bonus': + piece = new MyBonus(this.scene, theme.bonusGraph, pickId); + piece.moveTo(offsetX + x, 0.27, offsetZ - z); + tile.addPiece(piece); + piece.setTile(tile); // Bidirectional association when a tile has a bonus + piece.setInitPosition(); + this.scene.bonusPieces.push(piece); + pickId++; + break; + + case 'black': + piece = new MyPlayerPiece(this.scene, theme.playerGraph, 'black'); + piece.moveTo(offsetX + x, 0.27, offsetZ - z); + piece.rotate(180); + tile.addPiece(piece); + piece.setInitPosition(); + this.scene.playerPieces.push(piece); + break; + + case 'white': + piece = new MyPlayerPiece(this.scene, theme.playerGraph, 'white'); + piece.moveTo(offsetX + x, 0.27, offsetZ - z); + tile.addPiece(piece); + piece.setInitPosition(); + this.scene.playerPieces.push(piece); + break; + + case 'empty': + tile.setPickId(pickId); // Enable picking when the tile is empty + pickId++; + break; + default: + break; + } + + tile.moveTo(offsetX + x, 0.26, offsetZ - z); + this.tiles.push(tile); + offsetZ -= 0.05; + } + offsetX += 0.05; + } + } + + getTile(row, col) { + // row and col from 0 to 9 + let idx = 10 * row + col; + return this.tiles[idx] + } + + setGraph(graph, tileGraph, wallGraph) { + super.setGraph(graph); + this.tileGraph = tileGraph; + this.wallGraph = wallGraph; + + for (let i = 0; i < 100; i++) { + if ((i < 10) || (i > 89) || (i % 10 == 0) || (i % 10 == 9)) + this.tiles[i].setGraph(this.wallGraph); + else + this.tiles[i].setGraph(this.tileGraph); + } + } + + display() { + this.scene.pushMatrix(); + this.scene.translate(this.x, this.y, this.z); + + this.tiles.forEach(element => { + element.display(); + }) + + this.graph.display(); + this.scene.popMatrix(); + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyBonus.js b/frontend/game/elements/MyBonus.js new file mode 100644 index 0000000..12fad9d --- /dev/null +++ b/frontend/game/elements/MyBonus.js @@ -0,0 +1,27 @@ +/** + * MyBonus class, representing the bonus pieces that can be collected and moved to the auxiliar board. + */ +class MyBonus extends MyPiece { + constructor(scene, graph, pickId = -1) { + super(scene, graph, pickId); + } + + setTile(tile) { + this.tile = tile; + this.setRowCol(tile.row, tile.col) + } + + getTile() { + return this.tile; + } + + removeTile() { + this.tile.removePiece(); + this.tile = null; + } + + setRowCol(row, col) { + this.row = row; + this.col = col; + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyGameElement.js b/frontend/game/elements/MyGameElement.js new file mode 100644 index 0000000..ca9d9a2 --- /dev/null +++ b/frontend/game/elements/MyGameElement.js @@ -0,0 +1,74 @@ +/** + * MyGameElement class, representing all the game elements. + */ +class MyGameElement { + constructor(scene, graph, pickId = -1) { + this.scene = scene; + this.graph = graph; + this.x = 0; + this.y = 0; + this.z = 0; + this.r = 0; + + this.pickId = pickId; + this.canPick = (this.pickId != -1) ? true : false; + } + + setGraph(graph) { + this.graph = graph + } + + getPosition() { + return { x: this.x, y: this.y, z: this.z }; + } + + moveTo(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + + moveToPiece(piece) { + this.x = piece.x; + this.y = piece.y; + this.z = piece.z; + } + + rotate(r) { + this.r = Utils.degToRad(r); + } + + display() { + this.scene.pushMatrix(); + + this.scene.translate(this.x, this.y, this.z); + + if (this.r > 0) + this.scene.translate(0, 0.4, 0); + this.scene.rotate(this.r, 0, 0, 1); + + if (this.animation != null) + this.animation.apply(); + + if (this.canPick) + this.scene.registerForPick(this.pickId, this); + + this.graph.display(); + + this.scene.clearPickRegistration() + this.scene.popMatrix(); + } + + enablePick() { + this.canPick = true; + } + + setPickId(pickId) { + this.pickId = pickId; + this.enablePick(); + } + + disablePick() { + this.canPick = false; + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyMarker.js b/frontend/game/elements/MyMarker.js new file mode 100644 index 0000000..57c0b09 --- /dev/null +++ b/frontend/game/elements/MyMarker.js @@ -0,0 +1,67 @@ +/** + * MyMarker class, representing the game marker that displays the points, the players and the game final state. + */ +class MyMarker extends MyGameElement { + constructor(scene, graph, pickId = -1) { + super(scene, graph, pickId) + this.spritesLoaded = false; + + this.blackPoints = 0; + this.whitePoints = 0; + this.player = 1; + this.winner = null; + } + + setGraph(graph) { + this.graph = graph; + this.setSprites(); + this.setMarker(this.blackPoints, this.whitePoints, this.player, this.winner); + } + + setSprites() { + this.blackMarker = this.graph.nodes['blackMarker'].sprites[0] + this.whiteMarker = this.graph.nodes['whiteMarker'].sprites[0] + this.playerMarker = this.graph.nodes['playerMarker'].sprites[0] + this.spritesLoaded = true; + } + + setMarker(blackPoints, whitePoints, player, winner = null) { + + this.blackPoints = blackPoints; + this.whitePoints = whitePoints; + this.player = player; + this.winner = winner; + + this.blackMarker.setText(String(this.blackPoints)); + this.whiteMarker.setText(String(this.whitePoints)); + + if (this.winner == null) { + this.playerMarker.setText((player == 1 ? "Black" : "White") + "'s turn") + return; + } + + switch (this.winner) { + case 0: + this.playerMarker.setText("It's a tie!") + break; + + case 1: + this.playerMarker.setText("Black won!") + break; + + case -1: + this.playerMarker.setText("White won!") + break; + } + + } + + display() { + + if (!this.spritesLoaded) + this.setSprites(); + + this.graph.display(); + } + +} diff --git a/frontend/game/elements/MyPiece.js b/frontend/game/elements/MyPiece.js new file mode 100644 index 0000000..6b20d6f --- /dev/null +++ b/frontend/game/elements/MyPiece.js @@ -0,0 +1,17 @@ +/** + * MyPiece class, representing the game movable and animatable pieces. + */ +class MyPiece extends MyGameElement { + constructor(scene, graph, pickId = -1) { + super(scene, graph, pickId); + this.animation = null; + } + + setAnimation(animation) { + this.animation = animation; + } + + setInitPosition() { + this.initPosition = this.getPosition(); + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyPlayerPiece.js b/frontend/game/elements/MyPlayerPiece.js new file mode 100644 index 0000000..7cbf098 --- /dev/null +++ b/frontend/game/elements/MyPlayerPiece.js @@ -0,0 +1,34 @@ +/** + * MyPlayerPiece class, representing the playable pieces that change state. + */ +class MyPlayerPiece extends MyPiece { + constructor(scene, graph, color, pickId = -1) { + super(scene, graph, pickId); + this.elevated = false; + this.color = color; + } + + turn() { + (this.color == 'white') ? (this.color = 'black') : (this.color = 'white'); + } + + select() { + if (!this.elevated) { + this.elevated = true; + this.moveTo(this.x, this.y + 1, this.z); + } + } + + deselect() { + if (this.elevated) { + this.elevated = false; + this.moveTo(this.x, this.y - 1, this.z); + } + } + + setInitPosition() { + super.setInitPosition(); + this.initColor = this.color; + } + +} \ No newline at end of file diff --git a/frontend/game/elements/MyTile.js b/frontend/game/elements/MyTile.js new file mode 100644 index 0000000..bb4caf9 --- /dev/null +++ b/frontend/game/elements/MyTile.js @@ -0,0 +1,44 @@ +/** + * MyTile class, representing the tile elements that reference game positions for the moves to be made. + */ +class MyTile extends MyGameElement { + constructor(scene, graph, pickId = -1) { + super(scene, graph, pickId); + + this.piece = null; + this.valid = false; + } + + setRowCol(row, col) { + this.row = row; + this.col = col; + } + + addPiece(piece) { + this.piece = piece; + } + + removePiece() { + this.piece = null; + } + + setValid() { + this.valid = true; + } + + setInvalid() { + this.valid = false; + } + + display() { + let materialID = this.graph.nodes[this.graph.idRoot].materialID; + + if (this.valid) + this.graph.nodes[this.graph.idRoot].materialID = this.scene.getThemeName() + 'ValidTileMaterial'; + + super.display(); + + if (this.valid) + this.graph.nodes[this.graph.idRoot].materialID = materialID; + } +} \ No newline at end of file diff --git a/frontend/game/elements/MyTimer.js b/frontend/game/elements/MyTimer.js new file mode 100644 index 0000000..e405634 --- /dev/null +++ b/frontend/game/elements/MyTimer.js @@ -0,0 +1,119 @@ +const TimerState = { "START": 1, "ACTIVE": 2, "STOP": 3, "PAUSE": 4, "INACTIVE": 5, "TIMEUP": 6 } +Object.freeze(TimerState) + +/** + * MyTimer class, representing the countdown timer. + */ +class MyTimer extends MyGameElement { + constructor(scene, graph, pickId = -1) { + super(scene, graph, pickId) + + this.spriteLoaded = false; + this.turnTime = 60; // The time each player has to play + + this.lastTime = 0; + this.timePassed = 0; + + this.state = TimerState.INACTIVE; + } + + setGraph(graph) { + this.graph = graph; + this.setSprite(); + this.setTime(); + } + + setSprite() { + this.timeMarker = this.graph.nodes['timeMarker'].sprites[0] + this.spriteLoaded = true; + } + + setTime() { + this.timeMarker.setText(String(this.turnTime - Math.round(this.timePassed))); + } + + setState(state) { + this.state = state; + } + + + update(t) { + + if (!this.spriteLoaded) return; + + switch (this.state) { + case TimerState.START: + this.start_state(t); + break; + case TimerState.ACTIVE: + this.active_state(t); + break; + case TimerState.PAUSE: + this.pause_state(t); + break; + case TimerState.STOP: + this.stop_state(); + break; + default: + break; + } + + } + + + start_state(t) { + this.lastTime = t; + this.timePassed = 0; + this.setState(TimerState.ACTIVE); + } + + active_state(t) { + + this.timePassed += (t - this.lastTime) / 1000; + + if (this.timePassed > this.turnTime) { + this.lastTime = 0; + this.timePassed = 60; + + this.setTime(); + this.setState(TimerState.TIMEUP); + return; + } + + this.lastTime = t; + this.setTime(); + } + + stop_state() { + this.lastTime = 0; + this.timePassed = 0; + this.setTime(); + this.setState(TimerState.INACTIVE); + } + + pause_state(t) { + this.lastTime = t; + } + + + + isInactive() { + return this.state == TimerState.INACTIVE; + } + + isTimeUp() { + return this.state == TimerState.TIMEUP; + } + + isPaused() { + return this.state == TimerState.PAUSE; + } + + display() { + + if (!this.spriteLoaded) + this.setSprite(); + + this.graph.display(); + } +} \ No newline at end of file diff --git a/frontend/game/graphs/GameSceneGraph.js b/frontend/game/graphs/GameSceneGraph.js new file mode 100644 index 0000000..9ac24d6 --- /dev/null +++ b/frontend/game/graphs/GameSceneGraph.js @@ -0,0 +1,108 @@ +/** + * GameSceneGraph class, representing a game element scene graph. + */ +class GameSceneGraph extends MySceneGraph { + constructor(filename, scene) { + super(filename, scene) + } + + /** + * Parses the XML file, processing each block. + * @param {XML root element} rootElement + */ + parseXMLFile(rootElement) { + if (rootElement.nodeName != "lsf") + return "root tag missing"; + + var nodes = rootElement.children; + + // Reads the names of the nodes to an auxiliary buffer. + var nodeNames = []; + + for (var i = 0; i < nodes.length; i++) { + nodeNames.push(nodes[i].nodeName); + } + + var error; + + // Processes each node, verifying errors. + + // + var index; + if ((index = nodeNames.indexOf("parsererror")) != -1) { // Parsing Error + var errorNode = nodes[index].children; + return errorNode[1].innerHTML; + } + + // + if ((index = nodeNames.indexOf("initials")) == -1) + return "tag missing"; + else { + if (index != 0) + this.onXMLMinorError("tag out of order " + index); + + //Parse initials block + if ((error = this.parseInitials(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("textures")) == -1) + return "tag missing"; + else { + if (index != 1) + this.onXMLMinorError("tag out of order"); + + //Parse textures block + if ((error = this.parseTextures(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("materials")) == -1) + return "tag missing"; + else { + if (index != 2) + this.onXMLMinorError("tag out of order"); + + //Parse materials block + if ((error = this.parseMaterials(nodes[index])) != null) + return error; + } + + // + if ((index = nodeNames.indexOf("nodes")) == -1) + return "tag missing"; + else { + if (index != 3) + this.onXMLMinorError("tag out of order"); + + //Parse nodes block + if ((error = this.parseNodes(nodes[index])) != null) + return error; + } + + } + + onXMLReady() { + this.log("XML Loading finished."); + var rootElement = this.reader.xmlDoc.documentElement; + + // Here should go the calls for different functions to parse the various blocks + var error = this.parseXMLFile(rootElement); + + if (error != null) { + this.onXMLError(error); + return; + } + + this.scene.addTexturesFromGraph(this) + this.scene.addMaterialsFromGraph(this) + this.scene.addSpritesheetsFromGraph(this) + this.loadedOk = true; + } + + display() { + this.displayScene(); + } +} \ No newline at end of file diff --git a/frontend/game/graphs/ScenarioSceneGraph.js b/frontend/game/graphs/ScenarioSceneGraph.js new file mode 100644 index 0000000..9ea3f9e --- /dev/null +++ b/frontend/game/graphs/ScenarioSceneGraph.js @@ -0,0 +1,34 @@ +/** + * GameSceneGraph class, representing a game scenario scene graph. + */ +class ScenarioSceneGraph extends MySceneGraph { + constructor(filename, scene, def = false) { + super(filename, scene) + this.default = def + } + + onXMLReady() { + this.log("XML Loading finished."); + var rootElement = this.reader.xmlDoc.documentElement; + + // Here should go the calls for different functions to parse the various blocks + var error = this.parseXMLFile(rootElement); + + if (error != null) { + this.onXMLError(error); + return; + } + + this.scene.addTexturesFromGraph(this) + this.scene.addMaterialsFromGraph(this) + this.scene.addSpritesheetsFromGraph(this) + if (this.default) + this.scene.onGraphLoaded(this) + + this.loadedOk = true; + } + + display() { + this.displayScene(); + } +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f52e146 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,190 @@ + + + + + + LAIG - Mapello + + + + + + + + + +
+

MAPELLO

+
+
+ +
+ + +
+ +
+

VS

+
+ +
+ + +
+ + + +
+
+ + +
+ +
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/lib/CGF.js b/frontend/lib/CGF.js new file mode 100644 index 0000000..2edd03a --- /dev/null +++ b/frontend/lib/CGF.js @@ -0,0 +1 @@ +CGFversion="2.0.6";CGFdate=" (20201119)";console.log("WebCGF - Library for Computer Graphics @ FEUP (WebGL) - v"+CGFversion+CGFdate);var Detector={canvas:!!window.CanvasRenderingContext2D,webgl:function(){try{var canvas=document.createElement("canvas");return!!(window.WebGLRenderingContext&&canvas.getContext("webgl2"))}catch(e){return false}}(),workers:!!window.Worker,fileapi:window.File&&window.FileReader&&window.FileList&&window.Blob,getWebGLErrorMessage:function(){var element=document.createElement("div");element.id="webgl-error-message";element.style.fontFamily="monospace";element.style.fontSize="13px";element.style.fontWeight="normal";element.style.textAlign="center";element.style.background="#fff";element.style.color="#000";element.style.padding="1.5em";element.style.width="400px";element.style.margin="5em auto 0";if(!this.webgl){element.innerHTML=window.WebGLRenderingContext?['Your graphics card does not seem to support WebGL.
','Find out how to get it here.'].join("\n"):['Your browser does not seem to support WebGL.
','Find out how to get it here.'].join("\n")}return element},addGetWebGLMessage:function(parameters){var parent,id,element;parameters=parameters||{};parent=parameters.parent!==undefined?parameters.parent:document.body;id=parameters.id!==undefined?parameters.id:"oldie";element=Detector.getWebGLErrorMessage();element.id=id;parent.appendChild(element)}};if(typeof module==="object"){module.exports=Detector}(function(){"use strict";var shim={};if(typeof exports==="undefined"){if(typeof define=="function"&&typeof define.amd=="object"&&define.amd){shim.exports={};define(function(){return shim.exports})}else{shim.exports=window}}else{shim.exports=exports}(function(exports){if(!GLMAT_EPSILON){var GLMAT_EPSILON=1e-6}if(!GLMAT_ARRAY_TYPE){var GLMAT_ARRAY_TYPE=typeof Float32Array!=="undefined"?Float32Array:Array}var glMatrix={};glMatrix.setMatrixArrayType=function(type){GLMAT_ARRAY_TYPE=type};if(typeof exports!=="undefined"){exports.glMatrix=glMatrix}var vec2={};vec2.create=function(){var out=new GLMAT_ARRAY_TYPE(2);out[0]=0;out[1]=0;return out};vec2.clone=function(a){var out=new GLMAT_ARRAY_TYPE(2);out[0]=a[0];out[1]=a[1];return out};vec2.fromValues=function(x,y){var out=new GLMAT_ARRAY_TYPE(2);out[0]=x;out[1]=y;return out};vec2.copy=function(out,a){out[0]=a[0];out[1]=a[1];return out};vec2.set=function(out,x,y){out[0]=x;out[1]=y;return out};vec2.add=function(out,a,b){out[0]=a[0]+b[0];out[1]=a[1]+b[1];return out};vec2.subtract=function(out,a,b){out[0]=a[0]-b[0];out[1]=a[1]-b[1];return out};vec2.sub=vec2.subtract;vec2.multiply=function(out,a,b){out[0]=a[0]*b[0];out[1]=a[1]*b[1];return out};vec2.mul=vec2.multiply;vec2.divide=function(out,a,b){out[0]=a[0]/b[0];out[1]=a[1]/b[1];return out};vec2.div=vec2.divide;vec2.min=function(out,a,b){out[0]=Math.min(a[0],b[0]);out[1]=Math.min(a[1],b[1]);return out};vec2.max=function(out,a,b){out[0]=Math.max(a[0],b[0]);out[1]=Math.max(a[1],b[1]);return out};vec2.scale=function(out,a,b){out[0]=a[0]*b;out[1]=a[1]*b;return out};vec2.distance=function(a,b){var x=b[0]-a[0],y=b[1]-a[1];return Math.sqrt(x*x+y*y)};vec2.dist=vec2.distance;vec2.squaredDistance=function(a,b){var x=b[0]-a[0],y=b[1]-a[1];return x*x+y*y};vec2.sqrDist=vec2.squaredDistance;vec2.length=function(a){var x=a[0],y=a[1];return Math.sqrt(x*x+y*y)};vec2.len=vec2.length;vec2.squaredLength=function(a){var x=a[0],y=a[1];return x*x+y*y};vec2.sqrLen=vec2.squaredLength;vec2.negate=function(out,a){out[0]=-a[0];out[1]=-a[1];return out};vec2.normalize=function(out,a){var x=a[0],y=a[1];var len=x*x+y*y;if(len>0){len=1/Math.sqrt(len);out[0]=a[0]*len;out[1]=a[1]*len}return out};vec2.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]};vec2.cross=function(out,a,b){var z=a[0]*b[1]-a[1]*b[0];out[0]=out[1]=0;out[2]=z;return out};vec2.lerp=function(out,a,b,t){var ax=a[0],ay=a[1];out[0]=ax+t*(b[0]-ax);out[1]=ay+t*(b[1]-ay);return out};vec2.transformMat2=function(out,a,m){var x=a[0],y=a[1];out[0]=m[0]*x+m[2]*y;out[1]=m[1]*x+m[3]*y;return out};vec2.transformMat2d=function(out,a,m){var x=a[0],y=a[1];out[0]=m[0]*x+m[2]*y+m[4];out[1]=m[1]*x+m[3]*y+m[5];return out};vec2.transformMat3=function(out,a,m){var x=a[0],y=a[1];out[0]=m[0]*x+m[3]*y+m[6];out[1]=m[1]*x+m[4]*y+m[7];return out};vec2.transformMat4=function(out,a,m){var x=a[0],y=a[1];out[0]=m[0]*x+m[4]*y+m[12];out[1]=m[1]*x+m[5]*y+m[13];return out};vec2.forEach=function(){var vec=vec2.create();return function(a,stride,offset,count,fn,arg){var i,l;if(!stride){stride=2}if(!offset){offset=0}if(count){l=Math.min(count*stride+offset,a.length)}else{l=a.length}for(i=offset;i0){len=1/Math.sqrt(len);out[0]=a[0]*len;out[1]=a[1]*len;out[2]=a[2]*len}return out};vec3.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]};vec3.cross=function(out,a,b){var ax=a[0],ay=a[1],az=a[2],bx=b[0],by=b[1],bz=b[2];out[0]=ay*bz-az*by;out[1]=az*bx-ax*bz;out[2]=ax*by-ay*bx;return out};vec3.lerp=function(out,a,b,t){var ax=a[0],ay=a[1],az=a[2];out[0]=ax+t*(b[0]-ax);out[1]=ay+t*(b[1]-ay);out[2]=az+t*(b[2]-az);return out};vec3.transformMat4=function(out,a,m){var x=a[0],y=a[1],z=a[2];out[0]=m[0]*x+m[4]*y+m[8]*z+m[12];out[1]=m[1]*x+m[5]*y+m[9]*z+m[13];out[2]=m[2]*x+m[6]*y+m[10]*z+m[14];return out};vec3.transformQuat=function(out,a,q){var x=a[0],y=a[1],z=a[2],qx=q[0],qy=q[1],qz=q[2],qw=q[3],ix=qw*x+qy*z-qz*y,iy=qw*y+qz*x-qx*z,iz=qw*z+qx*y-qy*x,iw=-qx*x-qy*y-qz*z;out[0]=ix*qw+iw*-qx+iy*-qz-iz*-qy;out[1]=iy*qw+iw*-qy+iz*-qx-ix*-qz;out[2]=iz*qw+iw*-qz+ix*-qy-iy*-qx;return out};vec3.forEach=function(){var vec=vec3.create();return function(a,stride,offset,count,fn,arg){var i,l;if(!stride){stride=3}if(!offset){offset=0}if(count){l=Math.min(count*stride+offset,a.length)}else{l=a.length}for(i=offset;i0){len=1/Math.sqrt(len);out[0]=a[0]*len;out[1]=a[1]*len;out[2]=a[2]*len;out[3]=a[3]*len}return out};vec4.dot=function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]+a[3]*b[3]};vec4.lerp=function(out,a,b,t){var ax=a[0],ay=a[1],az=a[2],aw=a[3];out[0]=ax+t*(b[0]-ax);out[1]=ay+t*(b[1]-ay);out[2]=az+t*(b[2]-az);out[3]=aw+t*(b[3]-aw);return out};vec4.transformMat4=function(out,a,m){var x=a[0],y=a[1],z=a[2],w=a[3];out[0]=m[0]*x+m[4]*y+m[8]*z+m[12]*w;out[1]=m[1]*x+m[5]*y+m[9]*z+m[13]*w;out[2]=m[2]*x+m[6]*y+m[10]*z+m[14]*w;out[3]=m[3]*x+m[7]*y+m[11]*z+m[15]*w;return out};vec4.transformQuat=function(out,a,q){var x=a[0],y=a[1],z=a[2],qx=q[0],qy=q[1],qz=q[2],qw=q[3],ix=qw*x+qy*z-qz*y,iy=qw*y+qz*x-qx*z,iz=qw*z+qx*y-qy*x,iw=-qx*x-qy*y-qz*z;out[0]=ix*qw+iw*-qx+iy*-qz-iz*-qy;out[1]=iy*qw+iw*-qy+iz*-qx-ix*-qz;out[2]=iz*qw+iw*-qz+ix*-qy-iy*-qx;return out};vec4.forEach=function(){var vec=vec4.create();return function(a,stride,offset,count,fn,arg){var i,l;if(!stride){stride=4}if(!offset){offset=0}if(count){l=Math.min(count*stride+offset,a.length)}else{l=a.length}for(i=offset;i=1){if(out!==a){out[0]=ax;out[1]=ay;out[2]=az;out[3]=aw}return out}halfTheta=Math.acos(cosHalfTheta);sinHalfTheta=Math.sqrt(1-cosHalfTheta*cosHalfTheta);if(Math.abs(sinHalfTheta)<.001){out[0]=ax*.5+bx*.5;out[1]=ay*.5+by*.5;out[2]=az*.5+bz*.5;out[3]=aw*.5+bw*.5;return out}ratioA=Math.sin((1-t)*halfTheta)/sinHalfTheta;ratioB=Math.sin(t*halfTheta)/sinHalfTheta;out[0]=ax*ratioA+bx*ratioB;out[1]=ay*ratioA+by*ratioB;out[2]=az*ratioA+bz*ratioB;out[3]=aw*ratioA+bw*ratioB;return out};quat.invert=function(out,a){var a0=a[0],a1=a[1],a2=a[2],a3=a[3],dot=a0*a0+a1*a1+a2*a2+a3*a3,invDot=dot?1/dot:0;out[0]=-a0*invDot;out[1]=-a1*invDot;out[2]=-a2*invDot;out[3]=a3*invDot;return out};quat.conjugate=function(out,a){out[0]=-a[0];out[1]=-a[1];out[2]=-a[2];out[3]=a[3];return out};quat.length=vec4.length;quat.len=quat.length;quat.squaredLength=vec4.squaredLength;quat.sqrLen=quat.squaredLength;quat.normalize=vec4.normalize;quat.fromMat3=function(){var s_iNext=[1,2,0];return function(out,m){var fTrace=m[0]+m[4]+m[8];var fRoot;if(fTrace>0){fRoot=Math.sqrt(fTrace+1);out[3]=.5*fRoot;fRoot=.5/fRoot;out[0]=(m[7]-m[5])*fRoot;out[1]=(m[2]-m[6])*fRoot;out[2]=(m[3]-m[1])*fRoot}else{var i=0;if(m[4]>m[0])i=1;if(m[8]>m[i*3+i])i=2;var j=s_iNext[i];var k=s_iNext[j];fRoot=Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k]+1);out[i]=.5*fRoot;fRoot=.5/fRoot;out[3]=(m[k*3+j]-m[j*3+k])*fRoot;out[j]=(m[j*3+i]+m[i*3+j])*fRoot;out[k]=(m[k*3+i]+m[i*3+k])*fRoot}return out}}();quat.str=function(a){return"quat("+a[0]+", "+a[1]+", "+a[2]+", "+a[3]+")"};if(typeof exports!=="undefined"){exports.quat=quat}})(shim.exports)})();!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.dat={})}(this,function(e){"use strict";function t(e,t){var n=e.__state.conversionName.toString(),o=Math.round(e.r),i=Math.round(e.g),r=Math.round(e.b),s=e.a,a=Math.round(e.h),l=e.s.toFixed(1),d=e.v.toFixed(1);if(t||"THREE_CHAR_HEX"===n||"SIX_CHAR_HEX"===n){for(var c=e.hex.toString(16);c.length<6;)c="0"+c;return"#"+c}return"CSS_RGB"===n?"rgb("+o+","+i+","+r+")":"CSS_RGBA"===n?"rgba("+o+","+i+","+r+","+s+")":"HEX"===n?"0x"+e.hex.toString(16):"RGB_ARRAY"===n?"["+o+","+i+","+r+"]":"RGBA_ARRAY"===n?"["+o+","+i+","+r+","+s+"]":"RGB_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+"}":"RGBA_OBJ"===n?"{r:"+o+",g:"+i+",b:"+r+",a:"+s+"}":"HSV_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+"}":"HSVA_OBJ"===n?"{h:"+a+",s:"+l+",v:"+d+",a:"+s+"}":"unknown format"}function n(e,t,n){Object.defineProperty(e,t,{get:function(){return"RGB"===this.__state.space?this.__state[t]:(I.recalculateRGB(this,t,n),this.__state[t])},set:function(e){"RGB"!==this.__state.space&&(I.recalculateRGB(this,t,n),this.__state.space="RGB"),this.__state[t]=e}})}function o(e,t){Object.defineProperty(e,t,{get:function(){return"HSV"===this.__state.space?this.__state[t]:(I.recalculateHSV(this),this.__state[t])},set:function(e){"HSV"!==this.__state.space&&(I.recalculateHSV(this),this.__state.space="HSV"),this.__state[t]=e}})}function i(e){if("0"===e||S.isUndefined(e))return 0;var t=e.match(U);return S.isNull(t)?0:parseFloat(t[1])}function r(e){var t=e.toString();return t.indexOf(".")>-1?t.length-t.indexOf(".")-1:0}function s(e,t){var n=Math.pow(10,t);return Math.round(e*n)/n}function a(e,t,n,o,i){return o+(e-t)/(n-t)*(i-o)}function l(e,t,n,o){e.style.background="",S.each(ee,function(i){e.style.cssText+="background: "+i+"linear-gradient("+t+", "+n+" 0%, "+o+" 100%); "})}function d(e){e.style.background="",e.style.cssText+="background: -moz-linear-gradient(top, #ff0000 0%, #ff00ff 17%, #0000ff 34%, #00ffff 50%, #00ff00 67%, #ffff00 84%, #ff0000 100%);",e.style.cssText+="background: -webkit-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -o-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: -ms-linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);",e.style.cssText+="background: linear-gradient(top, #ff0000 0%,#ff00ff 17%,#0000ff 34%,#00ffff 50%,#00ff00 67%,#ffff00 84%,#ff0000 100%);"}function c(e,t,n){var o=document.createElement("li");return t&&o.appendChild(t),n?e.__ul.insertBefore(o,n):e.__ul.appendChild(o),e.onResize(),o}function u(e){X.unbind(window,"resize",e.__resizeHandler),e.saveToLocalStorageIfPossible&&X.unbind(window,"unload",e.saveToLocalStorageIfPossible)}function _(e,t){var n=e.__preset_select[e.__preset_select.selectedIndex];n.innerHTML=t?n.value+"*":n.value}function h(e,t,n){if(n.__li=t,n.__gui=e,S.extend(n,{options:function(t){if(arguments.length>1){var o=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:o,factoryArgs:[S.toArray(arguments)]})}if(S.isArray(t)||S.isObject(t)){var i=n.__li.nextElementSibling;return n.remove(),f(e,n.object,n.property,{before:i,factoryArgs:[t]})}},name:function(e){return n.__li.firstElementChild.firstElementChild.innerHTML=e,n},listen:function(){return n.__gui.listen(n),n},remove:function(){return n.__gui.remove(n),n}}),n instanceof q){var o=new Q(n.object,n.property,{min:n.__min,max:n.__max,step:n.__step});S.each(["updateDisplay","onChange","onFinishChange","step","min","max"],function(e){var t=n[e],i=o[e];n[e]=o[e]=function(){var e=Array.prototype.slice.call(arguments);return i.apply(o,e),t.apply(n,e)}}),X.addClass(t,"has-slider"),n.domElement.insertBefore(o.domElement,n.domElement.firstElementChild)}else if(n instanceof Q){var i=function(t){if(S.isNumber(n.__min)&&S.isNumber(n.__max)){var o=n.__li.firstElementChild.firstElementChild.innerHTML,i=n.__gui.__listening.indexOf(n)>-1;n.remove();var r=f(e,n.object,n.property,{before:n.__li.nextElementSibling,factoryArgs:[n.__min,n.__max,n.__step]});return r.name(o),i&&r.listen(),r}return t};n.min=S.compose(i,n.min),n.max=S.compose(i,n.max)}else n instanceof K?(X.bind(t,"click",function(){X.fakeEvent(n.__checkbox,"click")}),X.bind(n.__checkbox,"click",function(e){e.stopPropagation()})):n instanceof Z?(X.bind(t,"click",function(){X.fakeEvent(n.__button,"click")}),X.bind(t,"mouseover",function(){X.addClass(n.__button,"hover")}),X.bind(t,"mouseout",function(){X.removeClass(n.__button,"hover")})):n instanceof $&&(X.addClass(t,"color"),n.updateDisplay=S.compose(function(e){return t.style.borderLeftColor=n.__color.toString(),e},n.updateDisplay),n.updateDisplay());n.setValue=S.compose(function(t){return e.getRoot().__preset_select&&n.isModified()&&_(e.getRoot(),!0),t},n.setValue)}function p(e,t){var n=e.getRoot(),o=n.__rememberedObjects.indexOf(t.object);if(-1!==o){var i=n.__rememberedObjectIndecesToControllers[o];if(void 0===i&&(i={},n.__rememberedObjectIndecesToControllers[o]=i),i[t.property]=t,n.load&&n.load.remembered){var r=n.load.remembered,s=void 0;if(r[e.preset])s=r[e.preset];else{if(!r[se])return;s=r[se]}if(s[o]&&void 0!==s[o][t.property]){var a=s[o][t.property];t.initialValue=a,t.setValue(a)}}}}function f(e,t,n,o){if(void 0===t[n])throw new Error('Object "'+t+'" has no property "'+n+'"');var i=void 0;if(o.color)i=new $(t,n);else{var r=[t,n].concat(o.factoryArgs);i=ne.apply(e,r)}o.before instanceof z&&(o.before=o.before.__li),p(e,i),X.addClass(i.domElement,"c");var s=document.createElement("span");X.addClass(s,"property-name"),s.innerHTML=i.property;var a=document.createElement("div");a.appendChild(s),a.appendChild(i.domElement);var l=c(e,a,o.before);return X.addClass(l,he.CLASS_CONTROLLER_ROW),i instanceof $?X.addClass(l,"color"):X.addClass(l,H(i.getValue())),h(e,l,i),e.__controllers.push(i),i}function m(e,t){return document.location.href+"."+t}function g(e,t,n){var o=document.createElement("option");o.innerHTML=t,o.value=t,e.__preset_select.appendChild(o),n&&(e.__preset_select.selectedIndex=e.__preset_select.length-1)}function b(e,t){t.style.display=e.useLocalStorage?"block":"none"}function v(e){var t=e.__save_row=document.createElement("li");X.addClass(e.domElement,"has-save"),e.__ul.insertBefore(t,e.__ul.firstChild),X.addClass(t,"save-row");var n=document.createElement("span");n.innerHTML=" ",X.addClass(n,"button gears");var o=document.createElement("span");o.innerHTML="Save",X.addClass(o,"button"),X.addClass(o,"save");var i=document.createElement("span");i.innerHTML="New",X.addClass(i,"button"),X.addClass(i,"save-as");var r=document.createElement("span");r.innerHTML="Revert",X.addClass(r,"button"),X.addClass(r,"revert");var s=e.__preset_select=document.createElement("select");if(e.load&&e.load.remembered?S.each(e.load.remembered,function(t,n){g(e,n,n===e.preset)}):g(e,se,!1),X.bind(s,"change",function(){for(var t=0;t=0;n--)t=[e[n].apply(this,t)];return t[0]}},each:function(e,t,n){if(e)if(A&&e.forEach&&e.forEach===A)e.forEach(t,n);else if(e.length===e.length+0){var o=void 0,i=void 0;for(o=0,i=e.length;o1?S.toArray(arguments):arguments[0];return S.each(O,function(t){if(t.litmus(e))return S.each(t.conversions,function(t,n){if(T=t.read(e),!1===L&&!1!==T)return L=T,T.conversionName=n,T.conversion=t,S.BREAK}),S.BREAK}),L},B=void 0,N={hsv_to_rgb:function(e,t,n){var o=Math.floor(e/60)%6,i=e/60-Math.floor(e/60),r=n*(1-t),s=n*(1-i*t),a=n*(1-(1-i)*t),l=[[n,a,r],[s,n,r],[r,n,a],[r,s,n],[a,r,n],[n,r,s]][o];return{r:255*l[0],g:255*l[1],b:255*l[2]}},rgb_to_hsv:function(e,t,n){var o=Math.min(e,t,n),i=Math.max(e,t,n),r=i-o,s=void 0,a=void 0;return 0===i?{h:NaN,s:0,v:0}:(a=r/i,s=e===i?(t-n)/r:t===i?2+(n-e)/r:4+(e-t)/r,(s/=6)<0&&(s+=1),{h:360*s,s:a,v:i/255})},rgb_to_hex:function(e,t,n){var o=this.hex_with_component(0,2,e);return o=this.hex_with_component(o,1,t),o=this.hex_with_component(o,0,n)},component_from_hex:function(e,t){return e>>8*t&255},hex_with_component:function(e,t,n){return n<<(B=8*t)|e&~(255<this.__max&&(n=this.__max),void 0!==this.__step&&n%this.__step!=0&&(n=Math.round(n/this.__step)*this.__step),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"setValue",this).call(this,n)}},{key:"min",value:function(e){return this.__min=e,this}},{key:"max",value:function(e){return this.__max=e,this}},{key:"step",value:function(e){return this.__step=e,this.__impliedStep=e,this.__precision=r(e),this}}]),t}(),Q=function(e){function t(e,n,o){function i(){l.__onFinishChange&&l.__onFinishChange.call(l,l.getValue())}function r(e){var t=d-e.clientY;l.setValue(l.getValue()+t*l.__impliedStep),d=e.clientY}function s(){X.unbind(window,"mousemove",r),X.unbind(window,"mouseup",s),i()}F(this,t);var a=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,o));a.__truncationSuspended=!1;var l=a,d=void 0;return a.__input=document.createElement("input"),a.__input.setAttribute("type","text"),X.bind(a.__input,"change",function(){var e=parseFloat(l.__input.value);S.isNaN(e)||l.setValue(e)}),X.bind(a.__input,"blur",function(){i()}),X.bind(a.__input,"mousedown",function(e){X.bind(window,"mousemove",r),X.bind(window,"mouseup",s),d=e.clientY}),X.bind(a.__input,"keydown",function(e){13===e.keyCode&&(l.__truncationSuspended=!0,this.blur(),l.__truncationSuspended=!1,i())}),a.updateDisplay(),a.domElement.appendChild(a.__input),a}return D(t,W),P(t,[{key:"updateDisplay",value:function(){return this.__input.value=this.__truncationSuspended?this.getValue():s(this.getValue(),this.__precision),j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),q=function(e){function t(e,n,o,i,r){function s(e){e.preventDefault();var t=_.__background.getBoundingClientRect();return _.setValue(a(e.clientX,t.left,t.right,_.__min,_.__max)),!1}function l(){X.unbind(window,"mousemove",s),X.unbind(window,"mouseup",l),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}function d(e){var t=e.touches[0].clientX,n=_.__background.getBoundingClientRect();_.setValue(a(t,n.left,n.right,_.__min,_.__max))}function c(){X.unbind(window,"touchmove",d),X.unbind(window,"touchend",c),_.__onFinishChange&&_.__onFinishChange.call(_,_.getValue())}F(this,t);var u=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n,{min:o,max:i,step:r})),_=u;return u.__background=document.createElement("div"),u.__foreground=document.createElement("div"),X.bind(u.__background,"mousedown",function(e){document.activeElement.blur(),X.bind(window,"mousemove",s),X.bind(window,"mouseup",l),s(e)}),X.bind(u.__background,"touchstart",function(e){1===e.touches.length&&(X.bind(window,"touchmove",d),X.bind(window,"touchend",c),d(e))}),X.addClass(u.__background,"slider"),X.addClass(u.__foreground,"slider-fg"),u.updateDisplay(),u.__background.appendChild(u.__foreground),u.domElement.appendChild(u.__background),u}return D(t,W),P(t,[{key:"updateDisplay",value:function(){var e=(this.getValue()-this.__min)/(this.__max-this.__min);return this.__foreground.style.width=100*e+"%",j(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"updateDisplay",this).call(this)}}]),t}(),Z=function(e){function t(e,n,o){F(this,t);var i=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n)),r=i;return i.__button=document.createElement("div"),i.__button.innerHTML=void 0===o?"Fire":o,X.bind(i.__button,"click",function(e){return e.preventDefault(),r.fire(),!1}),X.addClass(i.__button,"button"),i.domElement.appendChild(i.__button),i}return D(t,z),P(t,[{key:"fire",value:function(){this.__onChange&&this.__onChange.call(this),this.getValue().call(this.object),this.__onFinishChange&&this.__onFinishChange.call(this,this.getValue())}}]),t}(),$=function(e){function t(e,n){function o(e){u(e),X.bind(window,"mousemove",u),X.bind(window,"touchmove",u),X.bind(window,"mouseup",r),X.bind(window,"touchend",r)}function i(e){_(e),X.bind(window,"mousemove",_),X.bind(window,"touchmove",_),X.bind(window,"mouseup",s),X.bind(window,"touchend",s)}function r(){X.unbind(window,"mousemove",u),X.unbind(window,"touchmove",u),X.unbind(window,"mouseup",r),X.unbind(window,"touchend",r),c()}function s(){X.unbind(window,"mousemove",_),X.unbind(window,"touchmove",_),X.unbind(window,"mouseup",s),X.unbind(window,"touchend",s),c()}function a(){var e=R(this.value);!1!==e?(p.__color.__state=e,p.setValue(p.__color.toOriginal())):this.value=p.__color.toString()}function c(){p.__onFinishChange&&p.__onFinishChange.call(p,p.__color.toOriginal())}function u(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__saturation_field.getBoundingClientRect(),n=e.touches&&e.touches[0]||e,o=n.clientX,i=n.clientY,r=(o-t.left)/(t.right-t.left),s=1-(i-t.top)/(t.bottom-t.top);return s>1?s=1:s<0&&(s=0),r>1?r=1:r<0&&(r=0),p.__color.v=s,p.__color.s=r,p.setValue(p.__color.toOriginal()),!1}function _(e){-1===e.type.indexOf("touch")&&e.preventDefault();var t=p.__hue_field.getBoundingClientRect(),n=1-((e.touches&&e.touches[0]||e).clientY-t.top)/(t.bottom-t.top);return n>1?n=1:n<0&&(n=0),p.__color.h=360*n,p.setValue(p.__color.toOriginal()),!1}F(this,t);var h=V(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e,n));h.__color=new I(h.getValue()),h.__temp=new I(0);var p=h;h.domElement=document.createElement("div"),X.makeSelectable(h.domElement,!1),h.__selector=document.createElement("div"),h.__selector.className="selector",h.__saturation_field=document.createElement("div"),h.__saturation_field.className="saturation-field",h.__field_knob=document.createElement("div"),h.__field_knob.className="field-knob",h.__field_knob_border="2px solid ",h.__hue_knob=document.createElement("div"),h.__hue_knob.className="hue-knob",h.__hue_field=document.createElement("div"),h.__hue_field.className="hue-field",h.__input=document.createElement("input"),h.__input.type="text",h.__input_textShadow="0 1px 1px ",X.bind(h.__input,"keydown",function(e){13===e.keyCode&&a.call(this)}),X.bind(h.__input,"blur",a),X.bind(h.__selector,"mousedown",function(){X.addClass(this,"drag").bind(window,"mouseup",function(){X.removeClass(p.__selector,"drag")})}),X.bind(h.__selector,"touchstart",function(){X.addClass(this,"drag").bind(window,"touchend",function(){X.removeClass(p.__selector,"drag")})});var f=document.createElement("div");return S.extend(h.__selector.style,{width:"122px",height:"102px",padding:"3px",backgroundColor:"#222",boxShadow:"0px 1px 3px rgba(0,0,0,0.3)"}),S.extend(h.__field_knob.style,{position:"absolute",width:"12px",height:"12px",border:h.__field_knob_border+(h.__color.v<.5?"#fff":"#000"),boxShadow:"0px 1px 3px rgba(0,0,0,0.5)",borderRadius:"12px",zIndex:1}),S.extend(h.__hue_knob.style,{position:"absolute",width:"15px",height:"2px",borderRight:"4px solid #fff",zIndex:1}),S.extend(h.__saturation_field.style,{width:"100px",height:"100px",border:"1px solid #555",marginRight:"3px",display:"inline-block",cursor:"pointer"}),S.extend(f.style,{width:"100%",height:"100%",background:"none"}),l(f,"top","rgba(0,0,0,0)","#000"),S.extend(h.__hue_field.style,{width:"15px",height:"100px",border:"1px solid #555",cursor:"ns-resize",position:"absolute",top:"3px",right:"3px"}),d(h.__hue_field),S.extend(h.__input.style,{outline:"none",textAlign:"center",color:"#fff",border:0,fontWeight:"bold",textShadow:h.__input_textShadow+"rgba(0,0,0,0.7)"}),X.bind(h.__saturation_field,"mousedown",o),X.bind(h.__saturation_field,"touchstart",o),X.bind(h.__field_knob,"mousedown",o),X.bind(h.__field_knob,"touchstart",o),X.bind(h.__hue_field,"mousedown",i),X.bind(h.__hue_field,"touchstart",i),h.__saturation_field.appendChild(f),h.__selector.appendChild(h.__field_knob),h.__selector.appendChild(h.__saturation_field),h.__selector.appendChild(h.__hue_field),h.__hue_field.appendChild(h.__hue_knob),h.domElement.appendChild(h.__input),h.domElement.appendChild(h.__selector),h.updateDisplay(),h}return D(t,z),P(t,[{key:"updateDisplay",value:function(){var e=R(this.getValue());if(!1!==e){var t=!1;S.each(I.COMPONENTS,function(n){if(!S.isUndefined(e[n])&&!S.isUndefined(this.__color.__state[n])&&e[n]!==this.__color.__state[n])return t=!0,{}},this),t&&S.extend(this.__color.__state,e)}S.extend(this.__temp.__state,this.__color.__state),this.__temp.a=1;var n=this.__color.v<.5||this.__color.s>.5?255:0,o=255-n;S.extend(this.__field_knob.style,{marginLeft:100*this.__color.s-7+"px",marginTop:100*(1-this.__color.v)-7+"px",backgroundColor:this.__temp.toHexString(),border:this.__field_knob_border+"rgb("+n+","+n+","+n+")"}),this.__hue_knob.style.marginTop=100*(1-this.__color.h/360)+"px",this.__temp.s=1,this.__temp.v=1,l(this.__saturation_field,"left","#fff",this.__temp.toHexString()),this.__input.value=this.__color.toString(),S.extend(this.__input.style,{backgroundColor:this.__color.toHexString(),color:"rgb("+n+","+n+","+n+")",textShadow:this.__input_textShadow+"rgba("+o+","+o+","+o+",.7)"})}}]),t}(),ee=["-moz-","-o-","-webkit-","-ms-",""],te={load:function(e,t){var n=t||document,o=n.createElement("link");o.type="text/css",o.rel="stylesheet",o.href=e,n.getElementsByTagName("head")[0].appendChild(o)},inject:function(e,t){var n=t||document,o=document.createElement("style");o.type="text/css",o.innerHTML=e;var i=n.getElementsByTagName("head")[0];try{i.appendChild(o)}catch(e){}}},ne=function(e,t){var n=e[t];return S.isArray(arguments[2])||S.isObject(arguments[2])?new Y(e,t,arguments[2]):S.isNumber(n)?S.isNumber(arguments[2])&&S.isNumber(arguments[3])?S.isNumber(arguments[4])?new q(e,t,arguments[2],arguments[3],arguments[4]):new q(e,t,arguments[2],arguments[3]):S.isNumber(arguments[4])?new Q(e,t,{min:arguments[2],max:arguments[3],step:arguments[4]}):new Q(e,t,{min:arguments[2],max:arguments[3]}):S.isString(n)?new J(e,t):S.isFunction(n)?new Z(e,t,""):S.isBoolean(n)?new K(e,t):null},oe=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){setTimeout(e,1e3/60)},ie=function(){function e(){F(this,e),this.backgroundElement=document.createElement("div"),S.extend(this.backgroundElement.style,{backgroundColor:"rgba(0,0,0,0.8)",top:0,left:0,display:"none",zIndex:"1000",opacity:0,WebkitTransition:"opacity 0.2s linear",transition:"opacity 0.2s linear"}),X.makeFullscreen(this.backgroundElement),this.backgroundElement.style.position="fixed",this.domElement=document.createElement("div"),S.extend(this.domElement.style,{position:"fixed",display:"none",zIndex:"1001",opacity:0,WebkitTransition:"-webkit-transform 0.2s ease-out, opacity 0.2s linear",transition:"transform 0.2s ease-out, opacity 0.2s linear"}),document.body.appendChild(this.backgroundElement),document.body.appendChild(this.domElement);var t=this;X.bind(this.backgroundElement,"click",function(){t.hide()})}return P(e,[{key:"show",value:function(){var e=this;this.backgroundElement.style.display="block",this.domElement.style.display="block",this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)",this.layout(),S.defer(function(){e.backgroundElement.style.opacity=1,e.domElement.style.opacity=1,e.domElement.style.webkitTransform="scale(1)"})}},{key:"hide",value:function(){var e=this,t=function t(){e.domElement.style.display="none",e.backgroundElement.style.display="none",X.unbind(e.domElement,"webkitTransitionEnd",t),X.unbind(e.domElement,"transitionend",t),X.unbind(e.domElement,"oTransitionEnd",t)};X.bind(this.domElement,"webkitTransitionEnd",t),X.bind(this.domElement,"transitionend",t),X.bind(this.domElement,"oTransitionEnd",t),this.backgroundElement.style.opacity=0,this.domElement.style.opacity=0,this.domElement.style.webkitTransform="scale(1.1)"}},{key:"layout",value:function(){this.domElement.style.left=window.innerWidth/2-X.getWidth(this.domElement)/2+"px",this.domElement.style.top=window.innerHeight/2-X.getHeight(this.domElement)/2+"px"}}]),e}(),re=function(e){if(e&&"undefined"!=typeof window){var t=document.createElement("style");return t.setAttribute("type","text/css"),t.innerHTML=e,document.head.appendChild(t),e}}(".dg ul{list-style:none;margin:0;padding:0;width:100%;clear:both}.dg.ac{position:fixed;top:0;left:0;right:0;height:0;z-index:0}.dg:not(.ac) .main{overflow:hidden}.dg.main{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear}.dg.main.taller-than-window{overflow-y:auto}.dg.main.taller-than-window .close-button{opacity:1;margin-top:-1px;border-top:1px solid #2c2c2c}.dg.main ul.closed .close-button{opacity:1 !important}.dg.main:hover .close-button,.dg.main .close-button.drag{opacity:1}.dg.main .close-button{-webkit-transition:opacity .1s linear;-o-transition:opacity .1s linear;-moz-transition:opacity .1s linear;transition:opacity .1s linear;border:0;line-height:19px;height:20px;cursor:pointer;text-align:center;background-color:#000}.dg.main .close-button.close-top{position:relative}.dg.main .close-button.close-bottom{position:absolute}.dg.main .close-button:hover{background-color:#111}.dg.a{float:right;margin-right:15px;overflow-y:visible}.dg.a.has-save>ul.close-top{margin-top:0}.dg.a.has-save>ul.close-bottom{margin-top:27px}.dg.a.has-save>ul.closed{margin-top:0}.dg.a .save-row{top:0;z-index:1002}.dg.a .save-row.close-top{position:relative}.dg.a .save-row.close-bottom{position:fixed}.dg li{-webkit-transition:height .1s ease-out;-o-transition:height .1s ease-out;-moz-transition:height .1s ease-out;transition:height .1s ease-out;-webkit-transition:overflow .1s linear;-o-transition:overflow .1s linear;-moz-transition:overflow .1s linear;transition:overflow .1s linear}.dg li:not(.folder){cursor:auto;height:27px;line-height:27px;padding:0 4px 0 5px}.dg li.folder{padding:0;border-left:4px solid rgba(0,0,0,0)}.dg li.title{cursor:pointer;margin-left:-4px}.dg .closed li:not(.title),.dg .closed ul li,.dg .closed ul li>*{height:0;overflow:hidden;border:0}.dg .cr{clear:both;padding-left:3px;height:27px;overflow:hidden}.dg .property-name{cursor:default;float:left;clear:left;width:40%;overflow:hidden;text-overflow:ellipsis}.dg .c{float:left;width:60%;position:relative}.dg .c input[type=text]{border:0;margin-top:4px;padding:3px;width:100%;float:right}.dg .has-slider input[type=text]{width:30%;margin-left:0}.dg .slider{float:left;width:66%;margin-left:-5px;margin-right:0;height:19px;margin-top:4px}.dg .slider-fg{height:100%}.dg .c input[type=checkbox]{margin-top:7px}.dg .c select{margin-top:5px}.dg .cr.function,.dg .cr.function .property-name,.dg .cr.function *,.dg .cr.boolean,.dg .cr.boolean *{cursor:pointer}.dg .cr.color{overflow:visible}.dg .selector{display:none;position:absolute;margin-left:-9px;margin-top:23px;z-index:10}.dg .c:hover .selector,.dg .selector.drag{display:block}.dg li.save-row{padding:0}.dg li.save-row .button{display:inline-block;padding:0px 6px}.dg.dialogue{background-color:#222;width:460px;padding:15px;font-size:13px;line-height:15px}#dg-new-constructor{padding:10px;color:#222;font-family:Monaco, monospace;font-size:10px;border:0;resize:none;box-shadow:inset 1px 1px 1px #888;word-wrap:break-word;margin:12px 0;display:block;width:440px;overflow-y:scroll;height:100px;position:relative}#dg-local-explain{display:none;font-size:11px;line-height:17px;border-radius:3px;background-color:#333;padding:8px;margin-top:10px}#dg-local-explain code{font-size:10px}#dat-gui-save-locally{display:none}.dg{color:#eee;font:11px 'Lucida Grande', sans-serif;text-shadow:0 -1px 0 #111}.dg.main::-webkit-scrollbar{width:5px;background:#1a1a1a}.dg.main::-webkit-scrollbar-corner{height:0;display:none}.dg.main::-webkit-scrollbar-thumb{border-radius:5px;background:#676767}.dg li:not(.folder){background:#1a1a1a;border-bottom:1px solid #2c2c2c}.dg li.save-row{line-height:25px;background:#dad5cb;border:0}.dg li.save-row select{margin-left:5px;width:108px}.dg li.save-row .button{margin-left:5px;margin-top:1px;border-radius:2px;font-size:9px;line-height:7px;padding:4px 4px 5px 4px;background:#c5bdad;color:#fff;text-shadow:0 1px 0 #b0a58f;box-shadow:0 -1px 0 #b0a58f;cursor:pointer}.dg li.save-row .button.gears{background:#c5bdad url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAANCAYAAAB/9ZQ7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQJJREFUeNpiYKAU/P//PwGIC/ApCABiBSAW+I8AClAcgKxQ4T9hoMAEUrxx2QSGN6+egDX+/vWT4e7N82AMYoPAx/evwWoYoSYbACX2s7KxCxzcsezDh3evFoDEBYTEEqycggWAzA9AuUSQQgeYPa9fPv6/YWm/Acx5IPb7ty/fw+QZblw67vDs8R0YHyQhgObx+yAJkBqmG5dPPDh1aPOGR/eugW0G4vlIoTIfyFcA+QekhhHJhPdQxbiAIguMBTQZrPD7108M6roWYDFQiIAAv6Aow/1bFwXgis+f2LUAynwoIaNcz8XNx3Dl7MEJUDGQpx9gtQ8YCueB+D26OECAAQDadt7e46D42QAAAABJRU5ErkJggg==) 2px 1px no-repeat;height:7px;width:8px}.dg li.save-row .button:hover{background-color:#bab19e;box-shadow:0 -1px 0 #b0a58f}.dg li.folder{border-bottom:0}.dg li.title{padding-left:16px;background:#000 url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlI+hKgFxoCgAOw==) 6px 10px no-repeat;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.2)}.dg .closed li.title{background-image:url(data:image/gif;base64,R0lGODlhBQAFAJEAAP////Pz8////////yH5BAEAAAIALAAAAAAFAAUAAAIIlGIWqMCbWAEAOw==)}.dg .cr.boolean{border-left:3px solid #806787}.dg .cr.color{border-left:3px solid}.dg .cr.function{border-left:3px solid #e61d5f}.dg .cr.number{border-left:3px solid #2FA1D6}.dg .cr.number input[type=text]{color:#2FA1D6}.dg .cr.string{border-left:3px solid #1ed36f}.dg .cr.string input[type=text]{color:#1ed36f}.dg .cr.function:hover,.dg .cr.boolean:hover{background:#111}.dg .c input[type=text]{background:#303030;outline:none}.dg .c input[type=text]:hover{background:#3c3c3c}.dg .c input[type=text]:focus{background:#494949;color:#fff}.dg .c .slider{background:#303030;cursor:ew-resize}.dg .c .slider-fg{background:#2FA1D6;max-width:100%}.dg .c .slider:hover{background:#3c3c3c}.dg .c .slider:hover .slider-fg{background:#44abda}\n");te.inject(re);var se="Default",ae=function(){try{return!!window.localStorage}catch(e){return!1}}(),le=void 0,de=!0,ce=void 0,ue=!1,_e=[],he=function e(t){var n=this,o=t||{};this.domElement=document.createElement("div"),this.__ul=document.createElement("ul"),this.domElement.appendChild(this.__ul),X.addClass(this.domElement,"dg"),this.__folders={},this.__controllers=[],this.__rememberedObjects=[],this.__rememberedObjectIndecesToControllers=[],this.__listening=[],o=S.defaults(o,{closeOnTop:!1,autoPlace:!0,width:e.DEFAULT_WIDTH}),o=S.defaults(o,{resizable:o.autoPlace,hideable:o.autoPlace}),S.isUndefined(o.load)?o.load={preset:se}:o.preset&&(o.load.preset=o.preset),S.isUndefined(o.parent)&&o.hideable&&_e.push(this),o.resizable=S.isUndefined(o.parent)&&o.resizable,o.autoPlace&&S.isUndefined(o.scrollable)&&(o.scrollable=!0);var i=ae&&"true"===localStorage.getItem(m(this,"isLocal")),r=void 0,s=void 0;if(Object.defineProperties(this,{parent:{get:function(){return o.parent}},scrollable:{get:function(){return o.scrollable}},autoPlace:{get:function(){return o.autoPlace}},closeOnTop:{get:function(){return o.closeOnTop}},preset:{get:function(){return n.parent?n.getRoot().preset:o.load.preset},set:function(e){n.parent?n.getRoot().preset=e:o.load.preset=e,E(this),n.revert()}},width:{get:function(){return o.width},set:function(e){o.width=e,w(n,e)}},name:{get:function(){return o.name},set:function(e){o.name=e,s&&(s.innerHTML=o.name)}},closed:{get:function(){return o.closed},set:function(t){o.closed=t,o.closed?X.addClass(n.__ul,e.CLASS_CLOSED):X.removeClass(n.__ul,e.CLASS_CLOSED),this.onResize(),n.__closeButton&&(n.__closeButton.innerHTML=t?e.TEXT_OPEN:e.TEXT_CLOSED)}},load:{get:function(){return o.load}},useLocalStorage:{get:function(){return i},set:function(e){ae&&(i=e,e?X.bind(window,"unload",r):X.unbind(window,"unload",r),localStorage.setItem(m(n,"isLocal"),e))}}}),S.isUndefined(o.parent)){if(this.closed=o.closed||!1,X.addClass(this.domElement,e.CLASS_MAIN),X.makeSelectable(this.domElement,!1),ae&&i){n.useLocalStorage=!0;var a=localStorage.getItem(m(this,"gui"));a&&(o.load=JSON.parse(a))}this.__closeButton=document.createElement("div"),this.__closeButton.innerHTML=e.TEXT_CLOSED,X.addClass(this.__closeButton,e.CLASS_CLOSE_BUTTON),o.closeOnTop?(X.addClass(this.__closeButton,e.CLASS_CLOSE_TOP),this.domElement.insertBefore(this.__closeButton,this.domElement.childNodes[0])):(X.addClass(this.__closeButton,e.CLASS_CLOSE_BOTTOM),this.domElement.appendChild(this.__closeButton)),X.bind(this.__closeButton,"click",function(){n.closed=!n.closed})}else{void 0===o.closed&&(o.closed=!0);var l=document.createTextNode(o.name);X.addClass(l,"controller-name"),s=c(n,l);X.addClass(this.__ul,e.CLASS_CLOSED),X.addClass(s,"title"),X.bind(s,"click",function(e){return e.preventDefault(),n.closed=!n.closed,!1}),o.closed||(this.closed=!1)}o.autoPlace&&(S.isUndefined(o.parent)&&(de&&(ce=document.createElement("div"),X.addClass(ce,"dg"),X.addClass(ce,e.CLASS_AUTO_PLACE_CONTAINER),document.body.appendChild(ce),de=!1),ce.appendChild(this.domElement),X.addClass(this.domElement,e.CLASS_AUTO_PLACE)),this.parent||w(n,o.width)),this.__resizeHandler=function(){n.onResizeDebounced()},X.bind(window,"resize",this.__resizeHandler),X.bind(this.__ul,"webkitTransitionEnd",this.__resizeHandler),X.bind(this.__ul,"transitionend",this.__resizeHandler),X.bind(this.__ul,"oTransitionEnd",this.__resizeHandler),this.onResize(),o.resizable&&y(this),r=function(){ae&&"true"===localStorage.getItem(m(n,"isLocal"))&&localStorage.setItem(m(n,"gui"),JSON.stringify(n.getSaveObject()))},this.saveToLocalStorageIfPossible=r,o.parent||function(){var e=n.getRoot();e.width+=1,S.defer(function(){e.width-=1})}()};he.toggleHide=function(){ue=!ue,S.each(_e,function(e){e.domElement.style.display=ue?"none":""})},he.CLASS_AUTO_PLACE="a",he.CLASS_AUTO_PLACE_CONTAINER="ac",he.CLASS_MAIN="main",he.CLASS_CONTROLLER_ROW="cr",he.CLASS_TOO_TALL="taller-than-window",he.CLASS_CLOSED="closed",he.CLASS_CLOSE_BUTTON="close-button",he.CLASS_CLOSE_TOP="close-top",he.CLASS_CLOSE_BOTTOM="close-bottom",he.CLASS_DRAG="drag",he.DEFAULT_WIDTH=245,he.TEXT_CLOSED="Close Controls",he.TEXT_OPEN="Open Controls",he._keydownHandler=function(e){"text"===document.activeElement.type||72!==e.which&&72!==e.keyCode||he.toggleHide()},X.bind(window,"keydown",he._keydownHandler,!1),S.extend(he.prototype,{add:function(e,t){return f(this,e,t,{factoryArgs:Array.prototype.slice.call(arguments,2)})},addColor:function(e,t){return f(this,e,t,{color:!0})},remove:function(e){this.__ul.removeChild(e.__li),this.__controllers.splice(this.__controllers.indexOf(e),1);var t=this;S.defer(function(){t.onResize()})},destroy:function(){if(this.parent)throw new Error("Only the root GUI should be removed with .destroy(). For subfolders, use gui.removeFolder(folder) instead.");this.autoPlace&&ce.removeChild(this.domElement);var e=this;S.each(this.__folders,function(t){e.removeFolder(t)}),X.unbind(window,"keydown",he._keydownHandler,!1),u(this)},addFolder:function(e){if(void 0!==this.__folders[e])throw new Error('You already have a folder in this GUI by the name "'+e+'"');var t={name:e,parent:this};t.autoPlace=this.autoPlace,this.load&&this.load.folders&&this.load.folders[e]&&(t.closed=this.load.folders[e].closed,t.load=this.load.folders[e]);var n=new he(t);this.__folders[e]=n;var o=c(this,n.domElement);return X.addClass(o,"folder"),n},removeFolder:function(e){this.__ul.removeChild(e.domElement.parentElement),delete this.__folders[e.name],this.load&&this.load.folders&&this.load.folders[e.name]&&delete this.load.folders[e.name],u(e);var t=this;S.each(e.__folders,function(t){e.removeFolder(t)}),S.defer(function(){t.onResize()})},open:function(){this.closed=!1},close:function(){this.closed=!0},onResize:function(){var e=this.getRoot();if(e.scrollable){var t=X.getOffset(e.__ul).top,n=0;S.each(e.__ul.childNodes,function(t){e.autoPlace&&t===e.__save_row||(n+=X.getHeight(t))}),window.innerHeight-t-20GUI\'s constructor:\n\n \n\n
\n\n Automatically save\n values to localStorage on exit.\n\n
The values saved to localStorage will\n override those passed to dat.GUI\'s constructor. This makes it\n easier to work incrementally, but localStorage is fragile,\n and your friends may not see the same values you do.\n\n
\n\n
\n\n'),this.parent)throw new Error("You can only call remember on a top level GUI.");var e=this;S.each(Array.prototype.slice.call(arguments),function(t){0===e.__rememberedObjects.length&&v(e),-1===e.__rememberedObjects.indexOf(t)&&e.__rememberedObjects.push(t)}),this.autoPlace&&w(this,this.width)},getRoot:function(){for(var e=this;e.parent;)e=e.parent;return e},getSaveObject:function(){var e=this.load;return e.closed=this.closed,this.__rememberedObjects.length>0&&(e.preset=this.preset,e.remembered||(e.remembered={}),e.remembered[this.preset]=x(this)),e.folders={},S.each(this.__folders,function(t,n){e.folders[n]=t.getSaveObject()}),e},save:function(){this.load.remembered||(this.load.remembered={}),this.load.remembered[this.preset]=x(this),_(this,!1),this.saveToLocalStorageIfPossible()},saveAs:function(e){this.load.remembered||(this.load.remembered={},this.load.remembered[se]=x(this,!0)),this.load.remembered[e]=x(this),this.preset=e,g(this,e,!0),this.saveToLocalStorageIfPossible()},revert:function(e){S.each(this.__controllers,function(t){this.getRoot().load.remembered?p(e||this.getRoot(),t):t.setValue(t.initialValue),t.__onFinishChange&&t.__onFinishChange.call(t,t.getValue())},this),S.each(this.__folders,function(e){e.revert(e)}),e||_(this.getRoot(),!1)},listen:function(e){var t=0===this.__listening.length;this.__listening.push(e),t&&C(this.__listening)},updateDisplay:function(){S.each(this.__controllers,function(e){e.updateDisplay()}),S.each(this.__folders,function(e){e.updateDisplay()})}});var pe={Color:I,math:N,interpret:R},fe={Controller:z,BooleanController:K,OptionController:Y,StringController:J,NumberController:W,NumberControllerBox:Q,NumberControllerSlider:q,FunctionController:Z,ColorController:$},me={dom:X},ge={GUI:he},be=he,ve={color:pe,controllers:fe,dom:me,gui:ge,GUI:be};e.color=pe,e.controllers=fe,e.dom=me,e.gui=ge,e.GUI=be,e.default=ve,Object.defineProperty(e,"__esModule",{value:!0})});function setProperty(object,prop,value){var fields=prop.split(/[\.\[\]]/).filter(function(elem){return elem.length>0});var tempObj=object;for(var i=0;i1&&uniformInfo.name.substr(-3)==="[0]";if(type===gl.FLOAT&&isArray){return function(v){gl.uniform1fv(location,v)}}if(type===gl.FLOAT){return function(v){gl.uniform1f(location,v)}}if(type===gl.FLOAT_VEC2){return function(v){gl.uniform2fv(location,v)}}if(type===gl.FLOAT_VEC3){return function(v){gl.uniform3fv(location,v)}}if(type===gl.FLOAT_VEC4){return function(v){gl.uniform4fv(location,v)}}if(type===gl.INT&&isArray){return function(v){gl.uniform1iv(location,v)}}if(type===gl.INT){return function(v){gl.uniform1i(location,v)}}if(type===gl.INT_VEC2){return function(v){gl.uniform2iv(location,v)}}if(type===gl.INT_VEC3){return function(v){gl.uniform3iv(location,v)}}if(type===gl.INT_VEC4){return function(v){gl.uniform4iv(location,v)}}if(type===gl.BOOL){return function(v){gl.uniform1i(location,v)}}if(type===gl.BOOL_VEC2){return function(v){gl.uniform2iv(location,v)}}if(type===gl.BOOL_VEC3){return function(v){gl.uniform3iv(location,v)}}if(type===gl.BOOL_VEC4){return function(v){gl.uniform4iv(location,v)}}if(type===gl.FLOAT_MAT2){return function(v){gl.uniformMatrix2fv(location,false,v)}}if(type===gl.FLOAT_MAT3){return function(v){gl.uniformMatrix3fv(location,false,v)}}if(type===gl.FLOAT_MAT4){return function(v){gl.uniformMatrix4fv(location,false,v)}}if((type===gl.SAMPLER_2D||type===gl.SAMPLER_CUBE)&&isArray){var units=[];for(var ii=0;ii0){var currTime=Date.now();if(currTime-this.lastUpdate>=this.updatePeriod){this.update(currTime);this.lastUpdate=currTime}}}display(){this.gl.viewport(0,0,this.gl.canvas.width,this.gl.canvas.height);this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT);this.gl.clearColor(0,0,0,1)}displayWithPick(){var request=this.getNextPickRequest();if(request!=null){var prevShader=this.activeShader;var x_in_canvas=request[0][0];var y_in_canvas=request[0][1];this.setActiveShader(this.pickShader);var pixels=new Uint8Array(4);this.pickMode=true;var temp=this.texturesEnabled;this.texturesEnabled=false;this.display();this.texturesEnabled=temp;this.pickMode=false;this.gl.readPixels(x_in_canvas,y_in_canvas,1,1,this.gl.RGBA,this.gl.UNSIGNED_BYTE,pixels);if(pixels!=null&&pixels!=undefined){this.pickResults.splice(0,this.pickResults.length);var data=this.getPickData(pixels);if(data!=null){this.pickResults.push([data[0],data[1]])}else{this.pickResults.push([undefined,undefined])}}this.setActiveShader(prevShader)}this.display()}setGlobalAmbientLight(r,g,b,a){this.activeShader.bind();this.gl.uniform4f(this.activeShader.uniforms.uGlobalAmbient,r,g,b,a)}onPick(ev){if(this.pickEnabled==false)return;var x=ev.clientX,y=ev.clientY;var rect=ev.target.getBoundingClientRect();if(rect.left<=x&&x>16;var g=h%65536>>8;var b=h%256;return[r/255,g/255,b/255,1]}getPickData(color){var colorId=65536*color[0]+256*color[1]+color[2];return this.pickData[colorId]}setPickEnabled(enable){this.pickEnabled=enable}setActiveShader(sh){if(this.pickMode==false){sh.importUniforms(this.activeShader);this.activeShader=sh;this.activeShader.bind()}return}setActiveShaderSimple(sh){if(this.pickMode==false){var pMatrix=this.activeShader.getUniformValue("uPMatrix");this.activeShader=sh;this.activeShader.bind();sh.setUniformsValues({uPMatrix:pMatrix})}return}updateInverseMatrix(){mat4.invert(this.invMatrix,this.activeMatrix);mat4.transpose(this.invMatrix,this.invMatrix);return}}var CGFcameraAxis=Object.freeze({X:vec3.fromValues(1,0,0),Y:vec3.fromValues(0,1,0),Z:vec3.fromValues(0,0,1)});var CGFcameraAxisID=Object.freeze({X:0,Y:1,Z:2});class CGFcamera{constructor(fov,near,far,position,target){this.fov=fov;this.near=near;this.far=far;this.position=vec4.fromValues(position[0],position[1],position[2],0);this.target=vec4.fromValues(target[0],target[1],target[2],0);this.direction=this.calculateDirection();this._up=vec3.fromValues(0,1,0);this._viewMatrix=mat4.create();this._projectionMatrix=mat4.create()}getViewMatrix(){mat4.lookAt(this._viewMatrix,this.position,this.target,this._up);return this._viewMatrix}getProjectionMatrix(width,height){var aspect=width/height;mat4.perspective(this._projectionMatrix,this.fov,aspect,this.near,this.far);return this._projectionMatrix}calculateDirection(){return vec4.normalize(vec4.create(),vec4.subtract(vec4.create(),this.target,this.position))}setPosition(newPosition){vec3.copy(this.position,newPosition);this.direction=this.calculateDirection()}setTarget(newTarget){vec3.copy(this.target,newTarget);this.direction=this.calculateDirection()}translate(vecValue){var tZ=vec4.scale(vec4.create(),this.direction,-vecValue[2]);var tUp=vec4.fromValues(0,vecValue[1],0,0);var temp=vec3.create();vec3.scale(temp,vec3.cross(temp,vec3.fromValues(0,1,0),this.direction),vecValue[0]);var tLeft=vec4.fromValues(temp[0],temp[1],temp[2],0);var temp1=vec4.create();temp1=vec4.add(temp1,tZ,vec4.add(temp1,tUp,tLeft));vec4.add(this.position,this.position,temp1);vec4.add(this.target,this.position,this.direction)}rotate(axis,angle){vec4.transformMat4(this.direction,this.direction,mat4.rotate(mat4.create(),mat4.create(),angle,axis));vec4.add(this.target,this.position,this.direction)}orbit(axisID,angle){var revLook=vec4.sub(vec4.create(),this.position,this.target);revLook[3]=0;var rotRevLook;if(axisID==CGFcameraAxisID.X){var rotAxis=vec3.create();vec3.normalize(rotAxis,vec3.cross(rotAxis,revLook,this._up));var rotMatrix=mat4.rotate(mat4.create(),mat4.create(),angle,rotAxis);rotRevLook=vec4.transformMat4(vec4.create(),revLook,rotMatrix);vec3.normalize(this._up,vec3.cross(this._up,rotAxis,rotRevLook))}else rotRevLook=vec4.transformMat4(vec4.create(),revLook,mat4.rotate(mat4.create(),mat4.create(),angle,this._up));vec4.add(this.position,this.target,rotRevLook);this.direction=this.calculateDirection()}pan(vecValue){var sscale=.05*vec3.distance(this.target,this.position);var right=vec3.cross(vec3.create(),this.direction,this._up);var horVec=vec4.scale(vec4.create(),vec3.normalize(right,right),vecValue[0]*sscale);horVec[3]=0;var vertVec=vec4.scale(vec4.create(),this._up,vecValue[1]*sscale);vertVec[3]=0;vec4.add(this.position,this.position,horVec);vec4.add(this.target,this.target,horVec);vec4.add(this.position,this.position,vertVec);vec4.add(this.target,this.target,vertVec)}zoom(value){if(vec4.distance(this.position,this.target)>value)vec4.add(this.position,this.position,vec4.scale(vec4.create(),this.direction,value));else console.warn("CGFcamera: zoom exceeds target position, ignoring request.")}}class CGFinterfaceCamera extends CGFcamera{constructor(fov,near,far){super(fov,near,far,[10,10,10],[0,0,0]);this.translation=[0,0,0];this.rotation=[.52,.79,0];this.distance=50;this._positionMatrix=mat4.create();this._invPositionMatrix=mat4.create()}getViewMatrix(){vec4.set(this.position,0,0,this.distance,1);vec4.set(this.target,0,0,0,1);vec3.set(this._up,0,1,0);vec4.set(this.direction,0,0,-1,0);mat4.lookAt(this._viewMatrix,this.position,this.target,this._up);mat4.identity(this._positionMatrix);mat4.rotateZ(this._positionMatrix,this._positionMatrix,this.rotation[2]);mat4.rotateX(this._positionMatrix,this._positionMatrix,this.rotation[0]);mat4.rotateY(this._positionMatrix,this._positionMatrix,-this.rotation[1]);mat4.translate(this._positionMatrix,this._positionMatrix,this.translation);mat4.invert(this._invPositionMatrix,this._positionMatrix);vec4.transformMat4(this.position,this.position,this._invPositionMatrix);vec4.transformMat4(this.target,this.target,this._invPositionMatrix);vec4.transformMat4(this.direction,this.direction,this._invPositionMatrix);vec3.transformMat4(this._up,this._up,this._invPositionMatrix);mat4.multiply(this._viewMatrix,this._viewMatrix,this._positionMatrix);return this._viewMatrix}setDistance(d){this.distance=d;this.clampDistance()}clampDistance(){if(this.distancethis.far)this.distance=this.far}roll(angle){this.rotate(CGFcameraAxis.Z,angle)}orbit(axisID,angle){this.rotation[axisID]+=angle}rotate(axisID,angle){this.rotation[axisID]+=angle}zoom(value){this.distance-=value;this.clampDistance()}translate(vecValue){vec4.add(this.translation,this.translation,vecValue)}pan(vecValue){vecValue[3]=0;vec4.transformMat4(vecValue,vecValue,this._invPositionMatrix);vec4.sub(this.translation,this.translation,vecValue)}}class CGFcameraOrtho{constructor(left,right,bottom,top,near,far,position,target,up){this.left=left;this.right=right;this.bottom=bottom;this.top=top;this.near=near;this.far=far;this.position=vec4.fromValues(position[0],position[1],position[2],0);this.target=vec4.fromValues(target[0],target[1],target[2],0);this.direction=this.calculateDirection();this._up=up;this._viewMatrix=mat4.create();this._projectionMatrix=mat4.create()}getViewMatrix(){mat4.lookAt(this._viewMatrix,this.position,this.target,this._up);return this._viewMatrix}getProjectionMatrix(width,height){var aspect=width/height;mat4.ortho(this._projectionMatrix,this.left,this.right,this.bottom,this.top,this.near,this.far);return this._projectionMatrix}calculateDirection(){return vec4.normalize(vec4.create(),vec4.subtract(vec4.create(),this.target,this.position))}setPosition(newPosition){vec3.copy(this.position,newPosition);this.direction=this.calculateDirection()}setTarget(newTarget){vec3.copy(this.target,newTarget);this.direction=this.calculateDirection()}translate(vecValue){var tZ=vec4.scale(vec4.create(),this.direction,-vecValue[2]);var tUp=vec4.fromValues(0,vecValue[1],0,0);var temp=vec3.create();vec3.scale(temp,vec3.cross(temp,vec3.fromValues(0,1,0),this.direction),vecValue[0]);var tLeft=vec4.fromValues(temp[0],temp[1],temp[2],0);var temp1=vec4.create();temp1=vec4.add(temp1,tZ,vec4.add(temp1,tUp,tLeft));vec4.add(this.position,this.position,temp1);vec4.add(this.target,this.position,this.direction)}rotate(axis,angle){vec4.transformMat4(this.direction,this.direction,mat4.rotate(mat4.create(),mat4.create(),angle,axis));vec4.add(this.target,this.position,this.direction)}orbit(axisID,angle){var revLook=vec4.sub(vec4.create(),this.position,this.target);revLook[3]=0;var rotRevLook;if(axisID==CGFcameraAxisID.X){var rotAxis=vec3.create();vec3.normalize(rotAxis,vec3.cross(rotAxis,revLook,this._up));var rotMatrix=mat4.rotate(mat4.create(),mat4.create(),angle,rotAxis);rotRevLook=vec4.transformMat4(vec4.create(),revLook,rotMatrix);vec3.normalize(this._up,vec3.cross(this._up,rotAxis,rotRevLook))}else rotRevLook=vec4.transformMat4(vec4.create(),revLook,mat4.rotate(mat4.create(),mat4.create(),angle,this._up));vec4.add(this.position,this.target,rotRevLook);this.direction=this.calculateDirection()}pan(vecValue){var sscale=.05*vec3.distance(this.target,this.position);var right=vec3.cross(vec3.create(),this.direction,this._up);var horVec=vec4.scale(vec4.create(),vec3.normalize(right,right),vecValue[0]*sscale);horVec[3]=0;var vertVec=vec4.scale(vec4.create(),this._up,vecValue[1]*sscale);vertVec[3]=0;vec4.add(this.position,this.position,horVec);vec4.add(this.target,this.target,horVec);vec4.add(this.position,this.position,vertVec);vec4.add(this.target,this.target,vertVec)}zoom(value){if(vec4.distance(this.position,this.target)>value)vec4.add(this.position,this.position,vec4.scale(vec4.create(),this.direction,value));else console.warn("CGFcameraOrtho: zoom exceeds target position, ignoring request.")}setUp(up){this._up=up}}class CGFlight extends CGFobject{constructor(scene,id){super(scene);this.scene=scene;this.id=id;console.log("Created Light "+id);this.setPosition(0,0,0,1);this.setAmbient(.1,.1,.1,1);this.setDiffuse(.5,.5,.5,1);this.setSpecular(.5,.5,.5,1);this.setSpotDirection(0,-1,0);this.setSpotExponent(10);this.setSpotCutOff(180);this.setConstantAttenuation(1);this.setLinearAttenuation(0);this.setQuadraticAttenuation(0);this.visible=false;this.initBuffers()}initBuffers(){this.vertices=[-.5,0,0,0,.5,0,.5,0,0,0,-.5,0,0,0,.5,0,0,-.5];this.indices=[1,4,0,1,2,4,1,5,2,1,0,5,3,0,4,3,4,2,3,2,5,3,5,0];this.normals=[1,0,0,0,-1,0,-1,0,0,0,1,0,0,0,-1,0,0,1];this.primitiveType=this.scene.gl.TRIANGLES;this.initGLBuffers()}enable(){this.enabled=true}disable(){this.enabled=false}setPosition(x,y,z,w){this.position=[x,y,z,w]}setAmbient(r,g,b,a){this.ambient=[r,g,b,a]}setDiffuse(r,g,b,a){this.diffuse=[r,g,b,a]}setSpecular(r,g,b,a){this.specular=[r,g,b,a]}setSpotDirection(x,y,z){this.spot_direction=[x,y,z,1]}setSpotExponent(e){this.spot_exponent=e}setSpotCutOff(a){this.spot_cutoff=a}setConstantAttenuation(kc){this.constant_attenuation=kc}setLinearAttenuation(kl){this.linear_attenuation=kl}setQuadraticAttenuation(kq){this.quadratic_attenuation=kq}update(){this.tPosition=[0,0,0,0];this.tDirection=[0,0,0,0];this.scene.updateInverseMatrix();vec4.transformMat4(this.tDirection,this.spot_direction,this.scene.invMatrix);vec4.transformMat4(this.tPosition,this.position,this.scene.activeMatrix);this.updateShader();if(this.visible){this.scene.setDiffuse(.5,.5,.5,1);this.scene.pushMatrix();this.scene.translate(this.position[0],this.position[1],this.position[2]);this.scene.scale(.3,.3,.3);this.display();this.scene.popMatrix()}}updateShader(){var gl=this.scene.gl;if(!this.scene.pickMode)try{var uLight=this.scene.activeShader.uniforms.uLight[this.id];gl.uniform1i(uLight.enabled,this.enabled);gl.uniform4fv(uLight.position,this.tPosition);gl.uniform4fv(uLight.ambient,this.ambient);gl.uniform4fv(uLight.diffuse,this.diffuse);gl.uniform4fv(uLight.specular,this.specular);gl.uniform3fv(uLight.spot_direction,[this.tDirection[0],this.tDirection[1],this.tDirection[2]]);gl.uniform1f(uLight.spot_exponent,this.spot_exponent);gl.uniform1f(uLight.spot_cutoff,this.spot_cutoff);gl.uniform1f(uLight.constant_attenuation,this.constant_attenuation);gl.uniform1f(uLight.linear_attenuation,this.linear_attenuation);gl.uniform1f(uLight.quadratic_attenuation,this.quadratic_attenuation)}catch(e){console.log("CGFlight.updateShader: Problem updating light "+this.id)}}setVisible(v){this.visible=v}}class CGFapplication{constructor(element){this.element=element;this.initialized=false;this.gl=null}init(){if(this.initialized){return true}var canvas=document.createElement("canvas");this.gl=canvas.getContext("webgl2",{antialias:true});if(!this.gl){Detector.addGetWebGLMessage({parent:this.element});return false}this.initialized=true;this.element.appendChild(this.gl.canvas);this.initScene();this.initInterface();window.addEventListener("resize",this.resizeCanvas(this.gl));this.resizeCanvas(this.gl)();return true}resizeCanvas(gl){return function(){console.log("resize");if(!gl)return;var width=window.innerWidth;var height=window.innerHeight;console.log("clientWidth: "+width+", clientHeight: "+height);if(gl.canvas.width!=width||gl.canvas.height!=height){console.log("width: "+gl.canvas.width+", height: "+gl.canvas.height);gl.canvas.width=width;gl.canvas.height=height}}}setScene(scene){this.scene=scene;if(this.initialized){this.scene.init(this)}}setInterface(iFace){this.interface=iFace;if(this.initialized){this.interface.init(this)}}initScene(){if(this.scene&&this.initialized){return this.scene.init(this)}return false}initInterface(){if(this.interface&&this.initialized){return this.interface.init(this)}return false}run(){var self=this;function renderLoop(){requestAnimationFrame(renderLoop,self.gl.canvas);if(self.interface){self.interface.update()}if(self.scene){self.scene.checkUpdate();self.scene.displayWithPick()}}renderLoop()}}class CGFplane extends CGFobject{constructor(scene,numDivisions){super(scene);this.numDivisions=numDivisions?numDivisions+1:2;this.initBuffers();this.wireframe=false}initBuffers(){this.vertices=[];this.indices=[];this.normals=[];for(var x=0;x0){if(typeof parserObj.onXMLError==="function"){parserObj.onXMLError("File "+xmlfile+" has errors.")}}if(typeof parserObj.onXMLReady==="function")parserObj.onXMLReady();if(self.getErrorMessage()!=null){if(typeof parserObj.onXMLError==="function")parserObj.onXMLError(self.getErrorMessage());return}}else{if(typeof parserObj.onXMLError==="function")parserObj.onXMLError(xmlhttpasync.status+": "+self.xmlfile+", "+xmlhttpasync.statusText)}}};this.xmlhttp.onerror=function(event){if(typeof parserObj.onXMLError==="function")parserObj.onXMLError("[CGFXMLreader] Error",xmlhttpasync.statusText)};this.xmlhttp.open("GET",xmlfile,true);this.xmlhttp.setRequestHeader("Content-Type","text/xml");try{this.xmlhttp.send(null)}catch(error){if(typeof parserObj.onXMLError==="function")parserObj.onXMLError("[CGFXMLreader] Error",xmlhttpasync.statusText)}}else{if(typeof parserObj.onXMLError==="function")parserObj.onXMLError("The XMLHttpRequest is not supported");return}}getRGBA(element,attributeName,required){if(required==undefined)required=true;if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("color (rgba) attribute name is null.");return null}var value=element.getAttribute(attributeName);if(value==null){if(required){console.error("color (rgba) value is null for attribute "+attributeName+".")}return null}var temp=value.split(" ");if(temp.length!=4){console.error("invalid "+temp.length+" number of color components for color (rgba) in attribute "+attributeName+".");return null}var rgba=new Array;for(var i=0;i<4;i++){rgba.push(parseFloat(temp[i]))}return rgba}getVector3(element,attributeName,required){if(required==undefined)required=true;if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("vector3 attribute name is null.");return null}var value=element.getAttribute(attributeName);if(value==null){if(required){console.error("vector3 value is null for attribute "+attributeName+".")}return null}var temp=value.split(" ");if(temp.length!=3){console.error("invalid "+temp.length+" number of components for a vector3, in attribute "+attributeName+".");return null}var vector3=new Array;for(var i=0;i<3;i++){vector3.push(parseFloat(temp[i]))}return vector3}getVector2(element,attributeName,required){if(required==undefined)required=true;if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("vector3 attribute name is null.");return null}var value=element.getAttribute(attributeName);if(value==null){if(required){console.error("vector2 value is null for attribute "+attributeName+".")}return null}var temp=value.split(" ");if(temp.length!=2){console.error("invalid "+temp.length+" number of components for a vector2, in attribute "+attributeName+".");return null}var vector2=new Array;for(var i=0;i<2;i++){vector2.push(parseFloat(temp[i]))}return vector2}getItem(element,attributeName,choices,required){if(required==undefined)required=true;if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("item attribute name is null.");return null}var value=element.getAttribute(attributeName);if(value==null){if(required){console.error("item value is null for attribute "+attributeName+".")}return null}value=value.toLowerCase();var index=indexOf.call(choices,value);if(index<0){console.error("value '"+value+"' is not a choice in ["+choices.toString()+"]");return null}return value}getString(element,attributeName,required){if(required==undefined)required=true;if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("string attribute name is null.");return null}var value=element.getAttribute(attributeName);if(value==null&&required){console.error("string value is null for attribute "+attributeName+".");return null}return value}hasAttribute(element,attributeName){if(element==null){console.error("element is null.");return null}if(attributeName==null){console.error("string attribute name is null.");return null}var value=element.getAttribute(attributeName);return value!=null}getBoolean(element,attributeName,required){if(required==undefined)required=true;var value=this.getItem(element,attributeName,["true","t","1","false","f","0"],required);if(value==null){return null}if(value=="1"||value=="true"||value=="t")return true;return false}getInteger(element,attributeName,required){if(required==undefined)required=true;var value=this.getString(element,attributeName,required);if(value==null){return null}return parseInt(value)}getFloat(element,attributeName,required){if(required==undefined)required=true;var value=this.getString(element,attributeName,required);if(value==null){return null}return parseFloat(value)}}CGFnurbsUtils={findSpan:function(p,u,U){var n=U.length-p-1;if(u>=U[n]){return n-1}if(u<=U[p]){return p}var low=p;var high=n;var mid=Math.floor((low+high)/2);while(u=U[mid+1]){if(u=k){a[s2][0]=a[s1][0]/ndu[pk+1][rk];d=a[s2][0]*ndu[rk][pk]}var j1=rk>=-1?1:-rk;var j2=r-1<=pk?k-1:p-r;for(var j=j1;j<=j2;++j){a[s2][j]=(a[s1][j]-a[s1][j-1])/ndu[pk+1][rk+j];d+=a[s2][j]*ndu[rk+j][pk]}if(r<=pk){a[s2][k]=-a[s1][k-1]/ndu[pk+1][r];d+=a[s2][k]*ndu[r][pk]}ders[k][r]=d;var j=s1;s1=s2;s2=j}}var r=p;for(var k=1;k<=n;++k){for(var j=0;j<=p;++j){ders[k][j]*=r}r*=p-k}return ders},calcBSplineDerivatives:function(p,U,P,u,nd){var du=nd= 0) + float linear_attenuation; // Default: 0 (value must be >= 0) + float quadratic_attenuation; // Default: 0 (value must be >= 0) + bool enabled; // Deafult: false +}; + +struct materialProperties { + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 emission; // Default: (0, 0, 0, 1) + float shininess; // Default: 0 (possible values [0, 128]) +}; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; + +uniform bool uLightEnabled; +uniform bool uLightModelTwoSided; + +#define NUMBER_OF_LIGHTS 8 + +uniform vec4 uGlobalAmbient; + +uniform lightProperties uLight[NUMBER_OF_LIGHTS]; + +uniform materialProperties uFrontMaterial; +uniform materialProperties uBackMaterial; + +out vec4 vFinalColor; + +vec4 lighting(vec4 vertex, vec3 E, vec3 N) { + + vec4 result = vec4(0.0, 0.0, 0.0, 0.0); + + for (int i = 0; i < NUMBER_OF_LIGHTS; i++) { + if (uLight[i].enabled) { + + float att = 1.0; + float spot_effect = 1.0; + vec3 L = vec3(0.0); + + if (uLight[i].position.w == 1.0) { + L = (uLight[i].position - vertex).xyz; + float dist = length(L); + L = normalize(L); + + if (uLight[i].spot_cutoff != 180.0) { + vec3 sd = normalize(vec3(uLight[i].spot_direction)); + float cos_cur_angle = dot(sd, -L); + float cos_inner_cone_angle = cos(radians(clamp(uLight[i].spot_cutoff, 0.0, 89.0))); + + spot_effect = pow(clamp(cos_cur_angle/ cos_inner_cone_angle, 0.0, 1.0), clamp(uLight[i].spot_exponent, 0.0, 128.0)); + } + + att = 1.0 / (uLight[i].constant_attenuation + uLight[i].linear_attenuation * dist + uLight[i].quadratic_attenuation * dist * dist); + + } else { + L = normalize(uLight[i].position.xyz); + } + + float lambertTerm = max(dot(N, L), 0.0); + + vec4 Ia = uLight[i].ambient * uFrontMaterial.ambient; + + vec4 Id = uLight[i].diffuse * uFrontMaterial.diffuse * lambertTerm; + + vec4 Is = vec4(0.0, 0.0, 0.0, 0.0); + + if (lambertTerm > 0.0) { + vec3 R = reflect(-L, N); + float specular = pow( max( dot(R, E), 0.0 ), uFrontMaterial.shininess); + + Is = uLight[i].specular * uFrontMaterial.specular * specular; + } + + if (uLight[i].position.w == 1.0) + result += att * max(spot_effect * (Id + Is), Ia); + else + result += att * spot_effect * (Ia + Id + Is); + } + } + + result += uGlobalAmbient * uFrontMaterial.ambient + uFrontMaterial.emission; + result = clamp(result, vec4(0.0), vec4(1.0)); + + return result; +} + +void main() { + + // Transformed Vertex position + vec4 vertex = uMVMatrix * vec4(aVertexPosition, 1.0); + + // Transformed normal position + vec3 N = normalize(vec3(uNMatrix * vec4(aVertexNormal, 1.0))); + + vec3 eyeVec = -vec3(vertex.xyz); + vec3 E = normalize(eyeVec); + + vFinalColor = lighting(vertex, E, N); + + gl_Position = uPMatrix * vertex; +} + diff --git a/frontend/lib/CGF/shaders/Gouraud/textured/fragment.glsl b/frontend/lib/CGF/shaders/Gouraud/textured/fragment.glsl new file mode 100644 index 0000000..dcb84a7 --- /dev/null +++ b/frontend/lib/CGF/shaders/Gouraud/textured/fragment.glsl @@ -0,0 +1,24 @@ +#version 300 es +precision highp float; + +in vec4 vFinalColor; +in vec2 vTextureCoord; + +out vec4 fragColor; + +uniform sampler2D uSampler; + +uniform bool uUseTexture; + +void main() { + // Branching should be reduced to a minimal. + // When based on a non-changing uniform, it is usually optimized. + if (uUseTexture) + { + vec4 textureColor = texture(uSampler, vTextureCoord); + fragColor = textureColor * vFinalColor; + } + else + fragColor = vFinalColor; + +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/Gouraud/textured/multiple_light-vertex.glsl b/frontend/lib/CGF/shaders/Gouraud/textured/multiple_light-vertex.glsl new file mode 100644 index 0000000..f777b29 --- /dev/null +++ b/frontend/lib/CGF/shaders/Gouraud/textured/multiple_light-vertex.glsl @@ -0,0 +1,129 @@ +#version 300 es +precision highp float; + +in vec3 aVertexPosition; +in vec3 aVertexNormal; +in vec2 aTextureCoord; + +uniform bool uUseTexture; + +struct lightProperties { + vec4 position; // Default: (0, 0, 1, 0) + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 half_vector; + vec3 spot_direction; // Default: (0, 0, -1) + float spot_exponent; // Default: 0 (possible values [0, 128] + float spot_cutoff; // Default: 180 (possible values [0, 90] or 180) + float constant_attenuation; // Default: 1 (value must be >= 0) + float linear_attenuation; // Default: 0 (value must be >= 0) + float quadratic_attenuation; // Default: 0 (value must be >= 0) + bool enabled; // Default: false +}; + +struct materialProperties { + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 emission; // Default: (0, 0, 0, 1) + float shininess; // Default: 0 (possible values [0, 128]) +}; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; + +uniform bool uLightEnabled; // not being used +uniform bool uLightModelTwoSided; // not being used + +#define NUMBER_OF_LIGHTS 8 + +uniform vec4 uGlobalAmbient; + +uniform lightProperties uLight[NUMBER_OF_LIGHTS]; + +uniform materialProperties uFrontMaterial; +uniform materialProperties uBackMaterial; + +out vec4 vFinalColor; +out vec2 vTextureCoord; + +vec4 lighting(vec4 vertex, vec3 E, vec3 N) { + + vec4 result = vec4(0.0, 0.0, 0.0, 0.0); + + for (int i = 0; i < NUMBER_OF_LIGHTS; i++) { + if (uLight[i].enabled) { + + float att = 1.0; + float spot_effect = 1.0; + vec3 L = vec3(0.0); + + if (uLight[i].position.w == 1.0) { + L = (uLight[i].position - vertex).xyz; + float dist = length(L); + L = normalize(L); + + if (uLight[i].spot_cutoff != 180.0) { + vec3 sd = normalize(vec3(uLight[i].spot_direction)); + float cos_cur_angle = dot(sd, -L); + float cos_inner_cone_angle = cos(radians(clamp(uLight[i].spot_cutoff, 0.0, 89.0))); + + spot_effect = pow(clamp(cos_cur_angle/ cos_inner_cone_angle, 0.0, 1.0), clamp(uLight[i].spot_exponent, 0.0, 128.0)); + } + + att = 1.0 / (uLight[i].constant_attenuation + uLight[i].linear_attenuation * dist + uLight[i].quadratic_attenuation * dist * dist); + + } else { + L = normalize(uLight[i].position.xyz); + } + + float lambertTerm = max(dot(N, L), 0.0); + + vec4 Ia = uLight[i].ambient * uFrontMaterial.ambient; + + vec4 Id = uLight[i].diffuse * uFrontMaterial.diffuse * lambertTerm; + + vec4 Is = vec4(0.0, 0.0, 0.0, 0.0); + + if (lambertTerm > 0.0) { + vec3 R = reflect(-L, N); + float specular = pow( max( dot(R, E), 0.0 ), uFrontMaterial.shininess); + + Is = uLight[i].specular * uFrontMaterial.specular * specular; + } + + if (uLight[i].position.w == 1.0) + result += att * max(spot_effect * (Id + Is), Ia); + else + result += att * spot_effect * (Ia + Id + Is); + } + } + + result += uGlobalAmbient * uFrontMaterial.ambient + uFrontMaterial.emission; + result = clamp(result, vec4(0.0), vec4(1.0)); + + return result; +} + +void main() { + + // Transformed Vertex position + vec4 vertex = uMVMatrix * vec4(aVertexPosition, 1.0); + + // Transformed normal position + vec3 N = normalize(vec3(uNMatrix * vec4(aVertexNormal, 1.0))); + + vec3 eyeVec = -vec3(vertex.xyz); + vec3 E = normalize(eyeVec); + + vFinalColor = lighting(vertex, E, N); + + gl_Position = uPMatrix * vertex; + + if (uUseTexture) + vTextureCoord = aTextureCoord; + +} + diff --git a/frontend/lib/CGF/shaders/Phong/multiple_light-phong-fragment.glsl b/frontend/lib/CGF/shaders/Phong/multiple_light-phong-fragment.glsl new file mode 100644 index 0000000..537956a --- /dev/null +++ b/frontend/lib/CGF/shaders/Phong/multiple_light-phong-fragment.glsl @@ -0,0 +1,119 @@ +#version 300 es +precision highp float; + +#define NUMBER_OF_LIGHTS 4 + +struct lightProperties { + vec4 position; // Default: (0, 0, 1, 0) + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 half_vector; + vec3 spot_direction; // Default: (0, 0, -1) + float spot_exponent; // Default: 0 (possible values [0, 128] + float spot_cutoff; // Default: 180 (possible values [0, 90] or 180) + float constant_attenuation; // Default: 1 (value must be >= 0) + float linear_attenuation; // Default: 0 (value must be >= 0) + float quadratic_attenuation; // Default: 0 (value must be >= 0) + bool enabled; // Deafult: false +}; + +struct materialProperties { + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 emission; // Default: (0, 0, 0, 1) + float shininess; // Default: 0 (possible values [0, 128]) +}; + +uniform mat4 uPMatrix; + +uniform bool uLightEnabled; +uniform bool uLightModelTwoSided; + +// uniform vec4 uGlobalAmbient; + +uniform lightProperties uLight[NUMBER_OF_LIGHTS]; + +uniform materialProperties uFrontMaterial; +uniform materialProperties uBackMaterial; + +in vec3 vNormal; +in vec3 vLightDir[NUMBER_OF_LIGHTS]; +in vec3 vEyeVec; + +out vec4 fragColor; + +vec4 calcDirectionalLight(int i, vec3 E, vec3 L, vec3 N) { + float lambertTerm = dot(N, -L); + + vec4 Ia = uLight[i].ambient * uFrontMaterial.ambient; + + vec4 Id = vec4(0.0, 0.0, 0.0, 0.0); + + vec4 Is = vec4(0.0, 0.0, 0.0, 0.0); + + if (lambertTerm > 0.0) { + Id = uLight[i].diffuse * uFrontMaterial.diffuse * lambertTerm; + + vec3 R = reflect(L, N); + float specular = pow( max( dot(R, E), 0.0 ), uFrontMaterial.shininess); + + Is = uLight[i].specular * uFrontMaterial.specular * specular; + } + return Ia + Id + Is; +} + +vec4 calcPointLight(int i, vec3 E, vec3 N) { + float dist = length(vLightDir[i]); + vec3 direction = normalize(vLightDir[i]); + + vec4 color = calcDirectionalLight(i, E, vLightDir[i], N); + float att = 1.0 / (uLight[i].constant_attenuation + uLight[i].linear_attenuation * dist + uLight[i].quadratic_attenuation * dist * dist); + return color * att; +} + +vec4 calcSpotLight(int i, vec3 E, vec3 N) +{ + vec3 direction = normalize(vLightDir[i]); + float spot_factor = dot(direction, uLight[i].spot_direction); + + if (spot_factor > uLight[i].spot_cutoff) { + vec4 color = calcPointLight(i, E, N); + return color * (1.0 - (1.0 - spot_factor) * 1.0/(1.0 - uLight[i].spot_cutoff)); + } + else { + return vec4(0,0,0,0); + } +} + +vec4 lighting(vec3 E, vec3 N) { + + vec4 result = vec4(0.0, 0.0, 0.0, 0.0); + + for (int i = 0; i < NUMBER_OF_LIGHTS; i++) { + if (uLight[i].enabled) { + if (uLight[i].position.w == 0.0) { + // Directional Light + result += calcDirectionalLight(i, E, normalize(uLight[i].position.xyz), N); + } else if (uLight[i].spot_cutoff == 180.0) { + // Point Light + result += calcPointLight(i, E, N); + } else { + result += calcSpotLight(i, E, N); + } + } + } + + return result; +} + +void main() { + + // Transformed normal position + vec3 N = normalize(vNormal); + + vec3 E = normalize(vEyeVec); + + fragColor = lighting(E, N); +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/Phong/multiple_light-phong-vertex.glsl b/frontend/lib/CGF/shaders/Phong/multiple_light-phong-vertex.glsl new file mode 100644 index 0000000..2eb3ba0 --- /dev/null +++ b/frontend/lib/CGF/shaders/Phong/multiple_light-phong-vertex.glsl @@ -0,0 +1,50 @@ +#version 300 es +precision highp float; + +#define NUMBER_OF_LIGHTS 4 + +in vec3 aVertexPosition; +in vec3 aVertexNormal; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; + +struct lightProperties { + vec4 position; // Default: (0, 0, 1, 0) + vec4 ambient; // Default: (0, 0, 0, 1) + vec4 diffuse; // Default: (0, 0, 0, 1) + vec4 specular; // Default: (0, 0, 0, 1) + vec4 half_vector; + vec3 spot_direction; // Default: (0, 0, -1) + float spot_exponent; // Default: 0 (possible values [0, 128] + float spot_cutoff; // Default: 180 (possible values [0, 90] or 180) + float constant_attenuation; // Default: 1 (value must be >= 0) + float linear_attenuation; // Default: 0 (value must be >= 0) + float quadratic_attenuation; // Default: 0 (value must be >= 0) + bool enabled; // Deafult: false +}; + +uniform lightProperties uLight[NUMBER_OF_LIGHTS]; + +out vec3 vNormal; +out vec3 vLightDir[NUMBER_OF_LIGHTS]; +out vec3 vEyeVec; + +void main() { + vec4 vertex = uMVMatrix * vec4(aVertexPosition, 1.0); + + vNormal = vec3(uNMatrix * vec4(aVertexNormal, 1.0)); + + vEyeVec = -vec3(vertex.xyz); + + gl_Position = uPMatrix * vertex; + + for (int i = 0; i < NUMBER_OF_LIGHTS; i++) { + if (uLight[i].enabled) { + if (uLight[i].position.w == 1.0) { + vLightDir[i] = (uMVMatrix * vec4(aVertexPosition, 1.0)).xyz - uLight[i].position.xyz; + } + } + } +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/Phong/phong-fragment.glsl b/frontend/lib/CGF/shaders/Phong/phong-fragment.glsl new file mode 100644 index 0000000..75c3522 --- /dev/null +++ b/frontend/lib/CGF/shaders/Phong/phong-fragment.glsl @@ -0,0 +1,51 @@ +#version 300 es +precision highp float; + +uniform mat4 uMVMatrix; + +uniform float uShininess; +uniform vec3 uLightDirection; + +uniform vec4 uLightAmbient; +uniform vec4 uLightDiffuse; +uniform vec4 uLightSpecular; + +uniform vec4 uMaterialAmbient; +uniform vec4 uMaterialDiffuse; +uniform vec4 uMaterialSpecular; + +in vec3 vNormal; +in vec3 vEyeVec; + +out vec4 fragColor; + +void main() { + // Normalize light to calculate lambertTerm + vec3 L = normalize(vec3(uMVMatrix * vec4(uLightDirection, 0.0))); + + // Transformed normal position + vec3 N = normalize(vNormal); + + // Lambert's cosine law + float lambertTerm = dot(N, -L); + + vec4 Ia = uLightAmbient * uMaterialAmbient; + + vec4 Id = vec4(0.0, 0.0, 0.0, 1.0); + + vec4 Is = vec4(0.0, 0.0, 0.0, 1.0); + + if (lambertTerm > 0.0) { + Id = uLightDiffuse * uMaterialDiffuse * lambertTerm; + + vec3 E = normalize(vEyeVec); + vec3 R = reflect(L, N); + float specular = pow( max( dot(R, E), 0.0 ), uShininess); + + Is = uLightSpecular * uMaterialSpecular * specular; + } + + vec4 finalColor = Ia + Id + Is; + + fragColor = finalColor; +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/Phong/phong-vertex.glsl b/frontend/lib/CGF/shaders/Phong/phong-vertex.glsl new file mode 100644 index 0000000..466b60b --- /dev/null +++ b/frontend/lib/CGF/shaders/Phong/phong-vertex.glsl @@ -0,0 +1,22 @@ +#version 300 es +precision highp float; + +in vec3 aVertexPosition; +in vec3 aVertexNormal; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; + +out vec3 vNormal; +out vec3 vEyeVec; + +void main() { + vec4 vertex = uMVMatrix * vec4(aVertexPosition, 1.0); + + vNormal = vec3(uNMatrix * vec4(aVertexNormal, 1.0)); + + vEyeVec = -vec3(vertex.xyz); + + gl_Position = uPMatrix * vertex; +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/picking/fragment.glsl b/frontend/lib/CGF/shaders/picking/fragment.glsl new file mode 100644 index 0000000..21d9e88 --- /dev/null +++ b/frontend/lib/CGF/shaders/picking/fragment.glsl @@ -0,0 +1,9 @@ +#version 300 es +precision highp float; + +in vec4 vFinalColor; +out vec4 fragColor; + +void main() { + fragColor = vFinalColor; +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/picking/vertex.glsl b/frontend/lib/CGF/shaders/picking/vertex.glsl new file mode 100644 index 0000000..e79302c --- /dev/null +++ b/frontend/lib/CGF/shaders/picking/vertex.glsl @@ -0,0 +1,24 @@ +#version 300 es +precision highp float; + +in vec3 aVertexPosition; + +uniform bool uUseTexture; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; + +out vec4 vFinalColor; + +uniform vec4 uPickColor; + +void main() { + + vec3 pt = vec3(aVertexPosition); + + // Transformed Vertex position + vec4 vertex = uMVMatrix * vec4(pt, 1.0); + gl_Position = uPMatrix * vertex; + + vFinalColor = uPickColor; +} diff --git a/frontend/lib/CGF/shaders/viz/normals-fragment.glsl b/frontend/lib/CGF/shaders/viz/normals-fragment.glsl new file mode 100644 index 0000000..9a20046 --- /dev/null +++ b/frontend/lib/CGF/shaders/viz/normals-fragment.glsl @@ -0,0 +1,8 @@ +#version 300 es +precision highp float; + +out vec4 fragColor; + +void main() { + fragColor = vec4(1.0, 0.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/frontend/lib/CGF/shaders/viz/normals-vertex.glsl b/frontend/lib/CGF/shaders/viz/normals-vertex.glsl new file mode 100644 index 0000000..ab5adb3 --- /dev/null +++ b/frontend/lib/CGF/shaders/viz/normals-vertex.glsl @@ -0,0 +1,11 @@ +#version 300 es +precision highp float; + +in vec3 aVertexPosition; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; + +void main() { + gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); +} diff --git a/frontend/main.js b/frontend/main.js new file mode 100644 index 0000000..e69e146 --- /dev/null +++ b/frontend/main.js @@ -0,0 +1,96 @@ +//From https://github.com/EvanHahn/ScriptInclude +include = function () { function f() { var a = this.readyState; (!a || /ded|te/.test(a)) && (c--, !c && e && d()) } var a = arguments, b = document, c = a.length, d = a[c - 1], e = d.call; e && c--; for (var g, h = 0; c > h; h++)g = b.createElement("script"), g.src = arguments[h], g.async = !0, g.onload = g.onerror = g.onreadystatechange = f, (b.head || b.getElementsByTagName("head")[0]).appendChild(g) }; +serialInclude = function (a) { var b = console, c = serialInclude.l; if (a.length > 0) c.splice(0, 0, a); else b.log("Done!"); if (c.length > 0) { if (c[0].length > 1) { var d = c[0].splice(0, 1); b.log("Loading " + d + "..."); include(d, function () { serialInclude([]); }); } else { var e = c[0][0]; c.splice(0, 1); e.call(); }; } else b.log("Finished."); }; serialInclude.l = new Array(); + +let app; + +//Include additional files here +serialInclude(['../lib/CGF.js', 'XMLscene.js', 'MySceneGraph.js', 'MyInterface.js', + 'primitives/MyRectangle.js', 'primitives/MyCylinder.js', 'primitives/MyCircle.js', + 'primitives/MyTriangle.js', 'primitives/MySphere.js', 'primitives/MyTorus.js', 'primitives/MyCube.js', 'primitives/MyWater.js', 'utils.js', 'MyNode.js', + 'animation/Keyframe.js', 'animation/MyAnimation.js', 'animation/KeyframeAnimation.js', 'animation/CameraAnimation.js', 'sprites/MySpriteSheet.js', + 'sprites/MySpriteText.js', 'sprites/MySpriteAnimation.js', 'nurbs/MyPlane.js', 'nurbs/MyPatch.js', 'nurbs/MyBarrel.js', + + 'game/MyTheme.js', 'game/GameScene.js', 'game/GameOrchestrator.js', 'game/StateMachine.js', + 'game/graphs/ScenarioSceneGraph.js', 'game/graphs/GameSceneGraph.js', + 'game/elements/MyGameElement.js', 'game/elements/MyBoard.js', 'game/elements/MyPiece.js', 'game/elements/MyTile.js', + 'game/elements/MyAuxiliarBoard.js', 'game/elements/MyPlayerPiece.js', 'game/elements/MyBonus.js', 'game/elements/MyMarker.js', + 'game/elements/MyTimer.js', + + 'game/MyAnimator.js', 'game/MyGameMove.js', 'game/PrologController.js', 'game/MyGameState.js', + + main = function () { + // Standard application, scene and interface setup + app = new CGFapplication(document.body); + var myInterface = new MyInterface(); + var myScene = new GameOrchestrator(myInterface); + + app.init(); + + app.setScene(myScene); + app.setInterface(myInterface); + myInterface.initButtons(myScene); + myInterface.setActiveCamera(myScene.camera); + + // Creates and load the themes and associates them to scene. + let sthemes = ["river-side", "living-room"] + myScene.setThemes(sthemes); + + // Starts the progress bar while loading the scene + progress_bar(); + } + +]); + +function progress_bar() { + var id = setInterval(frame, 20); + + function frame() { + let themes = app.scene.themes; + let width = 0; + let count = 0; + for (const theme in themes) { + if (themes.hasOwnProperty(theme)) { + const t = themes[theme]; + width += t.loading_percentage() + count++; + } + } + width = Math.floor(width / count); + var elem = document.getElementById("bar"); + elem.style.width = width + "%"; + if (width >= 98) { + clearInterval(id); + elem.style.display = "none"; + document.getElementById("container").style.display = "flex"; + } + } +} + +function initGame() { + try { + let theme = app.scene.themes[app.scene.selectedTheme]; + if (theme.is_loaded() && app.scene.sceneInited) { + let form_elems = document.querySelector("#container").elements; + let settings = {} + for (const e of form_elems) { + settings[e.name] = e.value.toLowerCase(); + } + + // The game settings and board generated by the prolog + app.scene.setGame(settings); + + let interface = document.querySelector("#interface"); + interface.remove(); + app.run(); + } + else { + console.log("Not Loaded Yet!", theme.loading_percentage()); + } + } + catch (err) { + console.log("Prolog not active"); + } + + +} \ No newline at end of file diff --git a/frontend/nurbs/MyBarrel.js b/frontend/nurbs/MyBarrel.js new file mode 100644 index 0000000..56f7400 --- /dev/null +++ b/frontend/nurbs/MyBarrel.js @@ -0,0 +1,87 @@ +/** + * MyBarrel + * @constructor + * @param scene - Reference to MyScene object + * @param base - the radius in the xy plane + * @param middle - the radius in the middle + * @param height - the height of the barrel + * @param alfa - the alfa angle + * @param slices - number of divisions in u for each barrel surface + * @param stacks - number of divisions in v + */ +class MyBarrel extends CGFobject { + constructor(scene, base, middle, height, alfa, slices, stacks) { + super(scene); + this.r = base; + this.R = middle; + this.L = height; + this.alfa = alfa; + + this.udegree = 3; + this.vdegree = 3; + + this.udivs = slices; + this.vdivs = stacks; + + this.h = this.r * 4/3; + this.H = (this.R - this.r) * 4/3; + this.tgAlfa = Math.tan(this.alfa); + + this.controlpointsTop = [ + // U = 0 + // V =0..4 + [ + [this.r, 0, 0, 1], + [this.r + this.H, 0, this.H/this.tgAlfa, 1], + [this.r + this.H, 0, this.L - this.H/this.tgAlfa, 1], + [this.r, 0, this.L, 1], + ], + + // U = 1 + // V =0..4 + [ + [this.r, this.h, 0, 1], + [this.r + this.H, (4/3) * (this.r + this.H), this.H/this.tgAlfa, 1], + [this.r + this.H, (4/3) * (this.r + this.H), this.L - this.H/this.tgAlfa, 1], + [this.r, this.h, this.L, 1], + ], + + // U = 2 + // V = 0..4 + [ + [-this.r, this.h, 0, 1], + [-this.r - this.H, (4/3) * (this.r + this.H), this.H/this.tgAlfa, 1], + [-this.r - this.H, (4/3) * (this.r + this.H), this.L - this.H/this.tgAlfa, 1], + [-this.r, this.h, this.L, 1], + ], + + // U = 3 + // V =0..4 + [ + [-this.r, 0, 0, 1], + [-this.r - this.H, 0, this.H/this.tgAlfa, 1], + [-this.r - this.H, 0, this.L - this.H/this.tgAlfa, 1], + [-this.r, 0, this.L, 1], + ], + + ]; + + + this.barrelSurfaceTop = new CGFnurbsSurface(this.udegree, this.vdegree, this.controlpointsTop); + this.barrelObjectTop = new CGFnurbsObject(this.scene, this.udivs, this.vdivs, this.barrelSurfaceTop); + } + + display() { + + this.scene.pushMatrix(); + + // Display Top Barrel + this.barrelObjectTop.display(); + + // Display Bottom Barrel + this.scene.rotate(Math.PI, 0,0,1); + this.barrelObjectTop.display(); + + this.scene.popMatrix(); + } +} \ No newline at end of file diff --git a/frontend/nurbs/MyPatch.js b/frontend/nurbs/MyPatch.js new file mode 100644 index 0000000..dbba745 --- /dev/null +++ b/frontend/nurbs/MyPatch.js @@ -0,0 +1,38 @@ +/** + * MyPatch + * @constructor + * @param scene - Reference to MyScene object + * @param udivs - points in u + * @param vdivs - points in v + * @param udivs - divs in u + * @param vdivs - divs in v + * @param controlpoints - controlpoints array + */ +class MyPatch extends CGFobject { + constructor(scene, upoints, vpoints, udivs, vdivs, controlpoints) { + super(scene); + this.udegree = upoints - 1; + this.vdegree = vpoints - 1; + this.udivs = udivs; + this.vdivs = vdivs; + + this.controlpoints = []; + let index = 0; + + for (let i = 0; i < upoints; i++) { + let aux = []; + for (let j = 0; j < vpoints; j++) { + aux.push(controlpoints[index + j]); + } + index += vpoints; + this.controlpoints.push(aux); + } + + this.nurbsSurface = new CGFnurbsSurface(this.udegree, this.vdegree, this.controlpoints); + this.nurbsObject = new CGFnurbsObject(this.scene, this.udivs, this.vdivs, this.nurbsSurface); + } + + display() { + this.nurbsObject.display(); + } +} \ No newline at end of file diff --git a/frontend/nurbs/MyPlane.js b/frontend/nurbs/MyPlane.js new file mode 100644 index 0000000..d330baa --- /dev/null +++ b/frontend/nurbs/MyPlane.js @@ -0,0 +1,37 @@ +/** + * MyPlane + * @constructor + * @param scene - Reference to MyScene object + * @param udivs - divs in u + * @param vdivs - divs in v + */ +class MyPlane extends CGFobject { + constructor(scene, udivs, vdivs) { + super(scene); + this.udivs = udivs; + this.vdivs = vdivs; + + this.nurbsSurface = new CGFnurbsSurface(1, 1, + [ + // U = 0 + [ // V = 0..1; + [-0.5, 0.0, 0.5, 1], + [-0.5, 0.0, -0.5, 1], + ], + // U = 1 + [ // V = 0..1 + [0.5, 0.0, 0.5, 1], + [0.5, 0.0, -0.5, 1], + ] + ] + ); + + this.nurbsObject = new CGFnurbsObject(this.scene, this.udivs, this.vdivs, this.nurbsSurface); + + } + + display() { + this.nurbsObject.display(); + } + +} diff --git a/frontend/primitives/MyCircle.js b/frontend/primitives/MyCircle.js new file mode 100644 index 0000000..aabb02c --- /dev/null +++ b/frontend/primitives/MyCircle.js @@ -0,0 +1,65 @@ +/** + * MyCircle + * @constructor + * @param scene - Reference to MyScene object + * @param radius + * @param slices + */ +class MyCircle extends CGFobject { + constructor(scene, radius, slices) { + super(scene); + this.slices = slices; + this.radius = radius; + + this.initBuffers(); + } + + initBuffers() { + this.vertices = []; + this.indices = []; + this.normals = []; + this.texCoords = []; + + var ang = 0; + var alphaAng = 2 * Math.PI / this.slices; + + this.vertices.push(0, 0, 0); + this.normals.push(0, 0, 1); + this.texCoords.push(0.5, 0.5); + + + var ca, sa, x, y, u, v; + + // Last vertices need to be repeated for the circle to close + for (var i = 0; i <= this.slices; i++) { + + ca = Math.cos(ang); + sa = Math.sin(ang); + + x = ca * this.radius; + y = sa * this.radius; + + this.vertices.push(x, y, 0); + this.normals.push(0, 0, 1); + + u = 0.5 + x / (2 * this.radius); + v = 0.5 + y / (2 * this.radius); + + this.texCoords.push(u, v); + + ang += alphaAng; + } + + var i = 0; + for (var j = 0; j < this.slices; j++) { + this.indices.push(i, i + 1, 0); + i++; + } + + // For the circle to close + this.indices.push(i, 1, 0); + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } +} \ No newline at end of file diff --git a/frontend/primitives/MyCube.js b/frontend/primitives/MyCube.js new file mode 100644 index 0000000..5968cf3 --- /dev/null +++ b/frontend/primitives/MyCube.js @@ -0,0 +1,53 @@ +class MyCube extends CGFobject { + constructor(scene) { + super(scene); + this.initBuffers(); + + this.square = new MyRectangle(this.scene, -0.5, -0.5, 0.5, 0.5); + } + + display() { + + //front + this.scene.pushMatrix(); + this.scene.translate(0.0, 0.0, 0.5); + this.square.display(); + this.scene.popMatrix(); + + //back + this.scene.pushMatrix(); + this.scene.translate(0.0, 0.0, -0.5); + this.scene.rotate(-Math.PI,0.0, 1.0,0.0); + this.square.display(); + this.scene.popMatrix(); + + //left + this.scene.pushMatrix(); + this.scene.translate(-0.5, 0.0, 0.0); + this.scene.rotate(-Math.PI/2,0.0, 1.0,0.0); + this.square.display(); + this.scene.popMatrix(); + + //right + this.scene.pushMatrix(); + this.scene.translate(0.5, 0.0, 0.0); + this.scene.rotate(Math.PI/2,0.0, 1.0,0.0); + this.square.display(); + this.scene.popMatrix(); + + //bottom + this.scene.pushMatrix(); + this.scene.translate(0.0, -0.5, 0.0); + this.scene.rotate(Math.PI/2,1.0, 0.0,0.0); + this.square.display(); + this.scene.popMatrix(); + + //top + this.scene.pushMatrix(); + this.scene.translate(0.0, 0.5, 0.0); + this.scene.rotate(-Math.PI/2,1.0, 0.0,0.0); + this.square.display(); + this.scene.popMatrix(); + } + +} \ No newline at end of file diff --git a/frontend/primitives/MyCylinder.js b/frontend/primitives/MyCylinder.js new file mode 100644 index 0000000..16714f1 --- /dev/null +++ b/frontend/primitives/MyCylinder.js @@ -0,0 +1,109 @@ +/** + * MyCylinder + * @constructor + * @param scene - Reference to MyScene object + * @param bottomRadius + * @param topRadius + * @param height + * @param slices + * @param stacks + */ +class MyCylinder extends CGFobject { + constructor(scene, bottomRadius, topRadius, height, slices, stacks) { + super(scene); + this.stacks = stacks; + this.slices = slices; + this.height = height; + this.topRadius = topRadius; + this.bottomRadius = bottomRadius; + + this.top = new MyCircle(scene, topRadius, slices); + this.bottom = new MyCircle(scene, bottomRadius, slices); + + this.initBuffers(); + } + + initBuffers() { + this.vertices = []; + this.indices = []; + this.normals = []; + this.texCoords = []; + + var ang = 0; + var alphaAng = 2 * Math.PI / this.slices; + var stackInc = this.height / this.stacks; + var radiusInc = (this.topRadius - this.bottomRadius) / this.stacks; + + var ca, sa, x, y; + var r = this.bottomRadius, h = 0; + // Last vertices need to be repeated for the cylinder to close + for (var j = 0; j <= this.stacks; j++) { + + ang = 0; + + for (var i = 0; i <= this.slices; i++) { + + ca = Math.cos(ang); + sa = Math.sin(ang); + + x = ca * r; + y = sa * r; + + this.vertices.push(x, y, h); + this.normals.push(ca, sa, 0); + + ang += alphaAng; + } + r += radiusInc; + h += stackInc; + } + + /* + (i+inc)_______(i+inc+1) + | | + | | + i___________(i+1) + */ + + var i = 0, inc = this.slices + 1, s = 0; + for (var k = 0; k < this.stacks; k++) { + i = s; + for (var j = 0; j < this.slices; j++) { + this.indices.push(i, i + 1, i + inc); + this.indices.push(i + inc, i + 1, i + 1 + inc); + i++; + } + s += inc; + } + + var sliceTexInc = 1 / this.slices; + var stackTexInc = 1 / this.stacks; + for (var k = this.stacks; k >= 0; k--) { + for (var j = 0; j <= this.slices; j++) + this.texCoords.push(j * sliceTexInc, k * stackTexInc); + } + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } + + display() { + super.display(); + + this.scene.pushMatrix(); + this.scene.rotate(Math.PI, 1, 0, 0); + this.bottom.display(); + this.scene.popMatrix(); + + this.scene.pushMatrix(); + this.scene.translate(0, 0, this.height); + this.top.display(); + this.scene.popMatrix(); + } + + enableNormalViz() { + super.enableNormalViz(); + this.top.enableNormalViz(); + this.bottom.enableNormalViz(); + } +} \ No newline at end of file diff --git a/frontend/primitives/MyRectangle.js b/frontend/primitives/MyRectangle.js new file mode 100644 index 0000000..8fe4717 --- /dev/null +++ b/frontend/primitives/MyRectangle.js @@ -0,0 +1,95 @@ +/** + * MyRectangle + * @constructor + * @param scene - Reference to MyScene object + * @param x1 - x coordinate corner 1 + * @param y1 - y coordinate corner 1 + * @param x2 - x coordinate corner 2 + * @param y2 - y coordinate corner 2 + * @param afs - afs texture coordinate + * @param aft - aft texture coordinate + */ +class MyRectangle extends CGFobject { + constructor(scene, x1, y1, x2, y2, afs = 1, aft = 1) { + super(scene); + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + this.afs = afs; + this.aft = aft; + this.xLength = this.x2-this.x1; + this.yLength = this.y2-this.y1; + this.initBuffers(); + } + + initBuffers() { + this.vertices = [ + this.x1, this.y1, 0, //0 + this.x2, this.y1, 0, //1 + this.x1, this.y2, 0, //2 + this.x2, this.y2, 0 //3 + ]; + + //Counter-clockwise reference of vertices + this.indices = [ + 0, 1, 2, + 1, 3, 2 + ]; + + //Facing Z positive + this.normals = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 1, + 0, 0, 1 + ]; + + /* + Texture coords (s,t) + +----------> s + | + | + | + v + t + */ + + this.setTexCoords(); + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } + + /** + * Calculates the new values for the TexCoords + */ + setTexCoords() { + return (this.texCoords = [ + 0, this.yLength / this.aft, + this.xLength / this.afs, this.yLength / this.aft, + 0, 0, + this.xLength / this.afs, 0 + ]); + } + + /** + * Sets the new values for afs and aft properties, updating the TexCoords + * @param afs + * @param aft + */ + setAmplification(afs, aft) { + this.afs = afs; + this.aft = aft; + this.updateTexCoords(this.setTexCoords()); + } + + /** + * Updates the list of texture coordinates of the rectangle + * @param {Array} coords - Array of texture coordinates + */ + updateTexCoords(coords) { + this.texCoords = [...coords]; + this.updateTexCoordsGLBuffers(); + } +} diff --git a/frontend/primitives/MySphere.js b/frontend/primitives/MySphere.js new file mode 100644 index 0000000..b695bc1 --- /dev/null +++ b/frontend/primitives/MySphere.js @@ -0,0 +1,82 @@ +/** + * MySphere + * @constructor + * @param scene - MyScene object + * @param slices - number of slices around Y axis + * @param stacks - number of stacks along Y axis, from the center to the poles (half of sphere) + */ +class MySphere extends CGFobject { + + constructor(scene, radius, slices, stacks) { + super(scene); + this.radius = radius; + this.latDivs = stacks * 2; + this.longDivs = slices; + + this.initBuffers(); + } + + /** + * @method initBuffers + * Initializes the sphere buffers + */ + initBuffers() { + this.vertices = []; + this.indices = []; + this.normals = []; + this.texCoords = []; + + var phi = 0; + var theta = 0; + var phiInc = Math.PI / this.latDivs; + var thetaInc = (2 * Math.PI) / this.longDivs; + var latVertices = this.longDivs + 1; + var latFac = 1 / this.latDivs; + var longFac = 1 / this.longDivs; + + // build an all-around stack at a time, starting on "north pole" and proceeding "south" + for (let latitude = 0; latitude <= this.latDivs; latitude++) { + var sinPhi = Math.sin(phi); + var cosPhi = Math.cos(phi); + + // in each stack, build all the slices around, starting on longitude 0 + theta = 0; + for (let longitude = 0; longitude <= this.longDivs; longitude++) { + //--- Vertices coordinates + var x = Math.sin(-theta) * sinPhi; + var y = Math.cos(theta) * sinPhi; + var z = cosPhi; + this.vertices.push(this.radius * x, this.radius * y, this.radius * z); + + //--- Indices + if (latitude < this.latDivs && longitude < this.longDivs) { + var current = latitude * latVertices + longitude; + var next = current + latVertices; + // pushing two triangles using indices from this round (current, current+1) + // and the ones directly south (next, next+1) + // (i.e. one full round of slices ahead) + + this.indices.push(current + 1, current, next); + this.indices.push(current + 1, next, next + 1); + } + + //--- Normals + // at each vertex, the direction of the normal is equal to + // the vector from the center of the sphere to the vertex. + // in a sphere of radius equal to one, the vector length is one. + // therefore, the value of the normal is equal to the position vector + this.normals.push(x, y, z); + theta += thetaInc; + + //--- Texture Coordinates + var s = 0.6 + longFac * longitude; + var t = latFac * latitude; + this.texCoords.push(s, t); + } + phi += phiInc; + } + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } +} diff --git a/frontend/primitives/MyTorus.js b/frontend/primitives/MyTorus.js new file mode 100644 index 0000000..bdd7bf9 --- /dev/null +++ b/frontend/primitives/MyTorus.js @@ -0,0 +1,80 @@ +/** + * MyTorus + * @constructor + * @param scene - MyScene object + * @param inner + * @param outer + * @param slices - number of slices around Y axis + * @param loops - number of loops in 1/4 of the torus + */ +class MyTorus extends CGFobject { + + constructor(scene, inner, outer, slices, loops) { + super(scene); + this.inner = inner; + this.outer = outer; + this.slices = slices; + this.loops = 4 * loops; + + this.initBuffers(); + } + + /** + * @method initBuffers + * Initializes the torus buffers + */ + initBuffers() { + this.vertices = []; + this.indices = []; + this.normals = []; + this.texCoords = []; + + var phi = 0; + var theta; + var phiInc = (2 * Math.PI) / this.loops; + var thetaInc = (2 * Math.PI) / this.slices; + + var x, y, z, nx, ny, nz; + for (var i = 0; i <= this.loops; i++) { + theta = 0; + for (var j = 0; j <= this.slices; j++) { + x = (this.outer + this.inner * Math.cos(theta)) * Math.cos(phi); + y = (this.outer + this.inner * Math.cos(theta)) * Math.sin(phi); + z = this.inner * Math.sin(theta); + + nx = Math.cos(theta) * Math.cos(phi); + ny = Math.cos(theta) * Math.sin(phi); + nz = Math.sin(theta); + + this.vertices.push(x, y, z); + this.normals.push(nx, ny, nz); + + theta += thetaInc; + } + + phi += phiInc; + } + + var i = 0, inc = this.slices + 1, s = 0; + for (var k = 0; k < this.loops; k++) { + i = s; + for (var j = 0; j < this.slices; j++) { + this.indices.push(i + inc, i + 1, i); + this.indices.push(i + inc + 1, i + 1, i + inc); + i++; + } + s += inc; + } + + var sliceTexInc = 1 / this.slices; + var loopTexInc = 1 / this.loops; + for (var k = 0; k <= this.loops; k++) { + for (var j = 0; j <= this.slices; j++) { + this.texCoords.push(k * loopTexInc, j * sliceTexInc); + } + } + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } +} diff --git a/frontend/primitives/MyTriangle.js b/frontend/primitives/MyTriangle.js new file mode 100644 index 0000000..88dc977 --- /dev/null +++ b/frontend/primitives/MyTriangle.js @@ -0,0 +1,101 @@ +/** + * MyTriangle + * @constructor + * @param scene - Reference to MyScene object + * @param x1 - x coordinate vertex 1 + * @param y1 - y coordinate vertex 1 + * @param x2 - x coordinate vertex 2 + * @param y2 - y coordinate vertex 2 + * @param x3 - x coordinate vertex 3 + * @param y3 - y coordinate vertex 3 + * @param afs - afs texture coordinate + * @param aft - aft texture coordinate + */ +class MyTriangle extends CGFobject { + constructor(scene, x1, y1, x2, y2, x3, y3, afs = 1, aft = 1) { + super(scene); + this.x1 = x1; + this.x2 = x2; + this.x3 = x3; + this.y1 = y1; + this.y2 = y2; + this.y3 = y3; + this.afs = afs; + this.aft = aft; + + this.a = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); + this.b = Math.sqrt(Math.pow(x3 - x2, 2) + Math.pow(y3 - y2, 2)); + this.c = Math.sqrt(Math.pow(x1 - x3, 2) + Math.pow(y1 - y3, 2)); + + this.cosa = (Math.pow(this.a, 2) - Math.pow(this.b, 2) + Math.pow(this.c, 2)) / (2 * this.a * this.c); + this.sina = Math.sqrt(1 - Math.pow(this.cosa, 2)); + + this.initBuffers(); + } + + initBuffers() { + this.vertices = [ + this.x1, this.y1, 0, //0 + this.x2, this.y2, 0, //1 + this.x3, this.y3, 0 //2 + ]; + + //Counter-clockwise reference of vertices + this.indices = [ + 0, 1, 2 + ]; + + //Facing Z positive + this.normals = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 1 + ]; + + /* + Texture coords (s,t) + +----------> s + | + | + | + v + t + */ + + this.setTexCoords(); + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } + + /** + * Calculates the new values for the TexCoords + */ + setTexCoords() { + return (this.texCoords = [ + 0,1, + this.a / this.afs, 1, + this.c*this.cosa / this.afs, 1 - this.c*this.sina / this.aft + ]); + } + + /** + * Sets the new values for afs and aft properties, updating the TexCoords + * @param afs + * @param aft + */ + setAmplification(afs, aft) { + this.afs = afs; + this.aft = aft; + this.updateTexCoords(this.setTexCoords()); + } + + /** + * Updates the list of texture coordinates of the rectangle + * @param {Array} coords - Array of texture coordinates + */ + updateTexCoords(coords) { + this.texCoords = [...coords]; + this.updateTexCoordsGLBuffers(); + } +} diff --git a/frontend/primitives/MyWater.js b/frontend/primitives/MyWater.js new file mode 100644 index 0000000..783e182 --- /dev/null +++ b/frontend/primitives/MyWater.js @@ -0,0 +1,95 @@ +class MyWater extends CGFobject { + constructor(scene, x1, y1, x2, y2, afs = 1, aft = 1) { + super(scene); + this.x1 = x1; + this.x2 = x2; + this.y1 = y1; + this.y2 = y2; + this.afs = afs; + this.aft = aft; + this.xLength = this.x2-this.x1; + this.yLength = this.y2-this.y1; + this.initBuffers(); + } + + initBuffers() { + this.vertices = [ + this.x1, this.y1, 0, //0 + this.x2, this.y1, 0, //1 + this.x1, this.y2, 0, //2 + this.x2, this.y2, 0 //3 + ]; + + //Counter-clockwise reference of vertices + this.indices = [ + 0, 1, 2, + 1, 3, 2 + ]; + + //Facing Z positive + this.normals = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 1, + 0, 0, 1 + ]; + + /* + Texture coords (s,t) + +----------> s + | + | + | + v + t + */ + + this.setTexCoords(); + + this.primitiveType = this.scene.gl.TRIANGLES; + this.initGLBuffers(); + } + + /** + * Calculates the new values for the TexCoords + */ + setTexCoords() { + return (this.texCoords = [ + 0, this.yLength / this.aft, + this.xLength / this.afs, this.yLength / this.aft, + 0, 0, + this.xLength / this.afs, 0 + ]); + } + + /** + * Sets the new values for afs and aft properties, updating the TexCoords + * @param afs + * @param aft + */ + setAmplification(afs, aft) { + this.afs = afs; + this.aft = aft; + this.updateTexCoords(this.setTexCoords()); + } + + /** + * Updates the list of texture coordinates of the rectangle + * @param {Array} coords - Array of texture coordinates + */ + updateTexCoords(coords) { + this.texCoords = [...coords]; + this.updateTexCoordsGLBuffers(); + } + + display(){ + this.scene.pushMatrix(); + this.scene.setActiveShader(this.scene.waterShader); + this.scene.defaultAppearance.setTexture(this.scene.textures['waterTex']); + this.scene.defaultAppearance.setTextureWrap('REPEAT', 'REPEAT'); + this.scene.textures['waterMap'].bind(1); + super.display(); + this.scene.setActiveShader(this.scene.defaultShader); + this.scene.popMatrix(); + } +} \ No newline at end of file diff --git a/frontend/scenes/images/ball.jpg b/frontend/scenes/images/ball.jpg new file mode 100644 index 0000000..db6902b Binary files /dev/null and b/frontend/scenes/images/ball.jpg differ diff --git a/frontend/scenes/images/benchWood.jpg b/frontend/scenes/images/benchWood.jpg new file mode 100644 index 0000000..8e77058 Binary files /dev/null and b/frontend/scenes/images/benchWood.jpg differ diff --git a/frontend/scenes/images/blackDoor.jpg b/frontend/scenes/images/blackDoor.jpg new file mode 100644 index 0000000..8170543 Binary files /dev/null and b/frontend/scenes/images/blackDoor.jpg differ diff --git a/frontend/scenes/images/blackWood.jpg b/frontend/scenes/images/blackWood.jpg new file mode 100644 index 0000000..0c45a8d Binary files /dev/null and b/frontend/scenes/images/blackWood.jpg differ diff --git a/frontend/scenes/images/chairPillowTexture.jpg b/frontend/scenes/images/chairPillowTexture.jpg new file mode 100644 index 0000000..5a3576b Binary files /dev/null and b/frontend/scenes/images/chairPillowTexture.jpg differ diff --git a/frontend/scenes/images/darkWood.jpg b/frontend/scenes/images/darkWood.jpg new file mode 100644 index 0000000..97e87ce Binary files /dev/null and b/frontend/scenes/images/darkWood.jpg differ diff --git a/frontend/scenes/images/floor.jpg b/frontend/scenes/images/floor.jpg new file mode 100644 index 0000000..43f8ad9 Binary files /dev/null and b/frontend/scenes/images/floor.jpg differ diff --git a/frontend/scenes/images/gardenFloor.jpg b/frontend/scenes/images/gardenFloor.jpg new file mode 100644 index 0000000..9b1f087 Binary files /dev/null and b/frontend/scenes/images/gardenFloor.jpg differ diff --git a/frontend/scenes/images/lamp.jpg b/frontend/scenes/images/lamp.jpg new file mode 100644 index 0000000..fc052fb Binary files /dev/null and b/frontend/scenes/images/lamp.jpg differ diff --git a/frontend/scenes/images/leather.jpg b/frontend/scenes/images/leather.jpg new file mode 100644 index 0000000..3125433 Binary files /dev/null and b/frontend/scenes/images/leather.jpg differ diff --git a/frontend/scenes/images/lightWood.jpg b/frontend/scenes/images/lightWood.jpg new file mode 100644 index 0000000..1c65964 Binary files /dev/null and b/frontend/scenes/images/lightWood.jpg differ diff --git a/frontend/scenes/images/lightWood.webp b/frontend/scenes/images/lightWood.webp new file mode 100644 index 0000000..f53d2f8 Binary files /dev/null and b/frontend/scenes/images/lightWood.webp differ diff --git a/frontend/scenes/images/mediumWood.jpg b/frontend/scenes/images/mediumWood.jpg new file mode 100644 index 0000000..fd2b8ae Binary files /dev/null and b/frontend/scenes/images/mediumWood.jpg differ diff --git a/frontend/scenes/images/metalTexture.jpg b/frontend/scenes/images/metalTexture.jpg new file mode 100644 index 0000000..de31782 Binary files /dev/null and b/frontend/scenes/images/metalTexture.jpg differ diff --git a/frontend/scenes/images/rugTexture.jpg b/frontend/scenes/images/rugTexture.jpg new file mode 100644 index 0000000..da89b35 Binary files /dev/null and b/frontend/scenes/images/rugTexture.jpg differ diff --git a/frontend/scenes/images/scoreBoard.jpg b/frontend/scenes/images/scoreBoard.jpg new file mode 100644 index 0000000..1dff0fa Binary files /dev/null and b/frontend/scenes/images/scoreBoard.jpg differ diff --git a/frontend/scenes/images/sidewalk.jpg b/frontend/scenes/images/sidewalk.jpg new file mode 100644 index 0000000..b2ac654 Binary files /dev/null and b/frontend/scenes/images/sidewalk.jpg differ diff --git a/frontend/scenes/images/spriteTextBlack.png b/frontend/scenes/images/spriteTextBlack.png new file mode 100644 index 0000000..cf40afa Binary files /dev/null and b/frontend/scenes/images/spriteTextBlack.png differ diff --git a/frontend/scenes/images/tableTexture.jpg b/frontend/scenes/images/tableTexture.jpg new file mode 100644 index 0000000..70624f5 Binary files /dev/null and b/frontend/scenes/images/tableTexture.jpg differ diff --git a/frontend/scenes/images/texture.jpg b/frontend/scenes/images/texture.jpg new file mode 100644 index 0000000..bcc6ee1 Binary files /dev/null and b/frontend/scenes/images/texture.jpg differ diff --git a/frontend/scenes/images/tv.jpg b/frontend/scenes/images/tv.jpg new file mode 100644 index 0000000..a18ff45 Binary files /dev/null and b/frontend/scenes/images/tv.jpg differ diff --git a/frontend/scenes/images/tv_show.png b/frontend/scenes/images/tv_show.png new file mode 100644 index 0000000..f79c107 Binary files /dev/null and b/frontend/scenes/images/tv_show.png differ diff --git a/frontend/scenes/images/wall.jpg b/frontend/scenes/images/wall.jpg new file mode 100644 index 0000000..0f9996f Binary files /dev/null and b/frontend/scenes/images/wall.jpg differ diff --git a/frontend/scenes/images/waterMap.jpg b/frontend/scenes/images/waterMap.jpg new file mode 100644 index 0000000..26ba3eb Binary files /dev/null and b/frontend/scenes/images/waterMap.jpg differ diff --git a/frontend/scenes/images/waterTex.jpg b/frontend/scenes/images/waterTex.jpg new file mode 100644 index 0000000..619d862 Binary files /dev/null and b/frontend/scenes/images/waterTex.jpg differ diff --git a/frontend/scenes/images/whiteMarble.jpg b/frontend/scenes/images/whiteMarble.jpg new file mode 100644 index 0000000..baad375 Binary files /dev/null and b/frontend/scenes/images/whiteMarble.jpg differ diff --git a/frontend/scenes/images/whiteWood.jpg b/frontend/scenes/images/whiteWood.jpg new file mode 100644 index 0000000..013d53f Binary files /dev/null and b/frontend/scenes/images/whiteWood.jpg differ diff --git a/frontend/scenes/images/woodGardenTable.jpg b/frontend/scenes/images/woodGardenTable.jpg new file mode 100644 index 0000000..a3a1762 Binary files /dev/null and b/frontend/scenes/images/woodGardenTable.jpg differ diff --git a/frontend/scenes/living-room/board.xml b/frontend/scenes/living-room/board.xml new file mode 100644 index 0000000..ad7614b --- /dev/null +++ b/frontend/scenes/living-room/board.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/bonus_board.xml b/frontend/scenes/living-room/bonus_board.xml new file mode 100644 index 0000000..8d602f6 --- /dev/null +++ b/frontend/scenes/living-room/bonus_board.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/marker.xml b/frontend/scenes/living-room/marker.xml new file mode 100644 index 0000000..67acc2b --- /dev/null +++ b/frontend/scenes/living-room/marker.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/non_playable_tile.xml b/frontend/scenes/living-room/non_playable_tile.xml new file mode 100644 index 0000000..5aca609 --- /dev/null +++ b/frontend/scenes/living-room/non_playable_tile.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/pieces/bonus.xml b/frontend/scenes/living-room/pieces/bonus.xml new file mode 100644 index 0000000..033e996 --- /dev/null +++ b/frontend/scenes/living-room/pieces/bonus.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/pieces/joker.xml b/frontend/scenes/living-room/pieces/joker.xml new file mode 100644 index 0000000..d91e0af --- /dev/null +++ b/frontend/scenes/living-room/pieces/joker.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/pieces/player.xml b/frontend/scenes/living-room/pieces/player.xml new file mode 100644 index 0000000..a55b02e --- /dev/null +++ b/frontend/scenes/living-room/pieces/player.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/pieces/wall.xml b/frontend/scenes/living-room/pieces/wall.xml new file mode 100644 index 0000000..8522a6a --- /dev/null +++ b/frontend/scenes/living-room/pieces/wall.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/pieces_board.xml b/frontend/scenes/living-room/pieces_board.xml new file mode 100644 index 0000000..07cd3d9 --- /dev/null +++ b/frontend/scenes/living-room/pieces_board.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/playable_tile.xml b/frontend/scenes/living-room/playable_tile.xml new file mode 100644 index 0000000..dbc3a78 --- /dev/null +++ b/frontend/scenes/living-room/playable_tile.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/scene.xml b/frontend/scenes/living-room/scene.xml new file mode 100644 index 0000000..cd43cac --- /dev/null +++ b/frontend/scenes/living-room/scene.xml @@ -0,0 +1,856 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/living-room/timer.xml b/frontend/scenes/living-room/timer.xml new file mode 100644 index 0000000..5a21ac4 --- /dev/null +++ b/frontend/scenes/living-room/timer.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/board.xml b/frontend/scenes/river-side/board.xml new file mode 100644 index 0000000..1b9a61f --- /dev/null +++ b/frontend/scenes/river-side/board.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/bonus_board.xml b/frontend/scenes/river-side/bonus_board.xml new file mode 100644 index 0000000..04cade1 --- /dev/null +++ b/frontend/scenes/river-side/bonus_board.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/marker.xml b/frontend/scenes/river-side/marker.xml new file mode 100644 index 0000000..631e971 --- /dev/null +++ b/frontend/scenes/river-side/marker.xml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/non_playable_tile.xml b/frontend/scenes/river-side/non_playable_tile.xml new file mode 100644 index 0000000..bdd2ad3 --- /dev/null +++ b/frontend/scenes/river-side/non_playable_tile.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/pieces/bonus.xml b/frontend/scenes/river-side/pieces/bonus.xml new file mode 100644 index 0000000..fbe9d62 --- /dev/null +++ b/frontend/scenes/river-side/pieces/bonus.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/scenes/river-side/pieces/joker.xml b/frontend/scenes/river-side/pieces/joker.xml new file mode 100644 index 0000000..cfd5e71 --- /dev/null +++ b/frontend/scenes/river-side/pieces/joker.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/pieces/player.xml b/frontend/scenes/river-side/pieces/player.xml new file mode 100644 index 0000000..4cf11ed --- /dev/null +++ b/frontend/scenes/river-side/pieces/player.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/pieces/wall.xml b/frontend/scenes/river-side/pieces/wall.xml new file mode 100644 index 0000000..130dc16 --- /dev/null +++ b/frontend/scenes/river-side/pieces/wall.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/pieces_board.xml b/frontend/scenes/river-side/pieces_board.xml new file mode 100644 index 0000000..442ca8f --- /dev/null +++ b/frontend/scenes/river-side/pieces_board.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/playable_tile.xml b/frontend/scenes/river-side/playable_tile.xml new file mode 100644 index 0000000..07412bd --- /dev/null +++ b/frontend/scenes/river-side/playable_tile.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/scenes/river-side/scene.xml b/frontend/scenes/river-side/scene.xml new file mode 100644 index 0000000..63aa0a3 --- /dev/null +++ b/frontend/scenes/river-side/scene.xml @@ -0,0 +1,1133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/scenes/river-side/timer.xml b/frontend/scenes/river-side/timer.xml new file mode 100644 index 0000000..fb52412 --- /dev/null +++ b/frontend/scenes/river-side/timer.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/shaders/spriteShader.frag b/frontend/shaders/spriteShader.frag new file mode 100644 index 0000000..12d234e --- /dev/null +++ b/frontend/shaders/spriteShader.frag @@ -0,0 +1,14 @@ +#ifdef GL_ES +precision highp float; +#endif + +varying vec2 vTextureCoord; + +uniform sampler2D uSampler; + +void main() { + + // fragment color is retrieved from the texture (sampler2D) based on the ST coordinate + // calculated at the vertex shader. + gl_FragColor = texture2D(uSampler, vTextureCoord); +} \ No newline at end of file diff --git a/frontend/shaders/spriteShader.vert b/frontend/shaders/spriteShader.vert new file mode 100644 index 0000000..431883f --- /dev/null +++ b/frontend/shaders/spriteShader.vert @@ -0,0 +1,27 @@ +attribute vec3 aVertexPosition; +attribute vec3 aVertexNormal; +attribute vec2 aTextureCoord; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; + +varying vec2 vTextureCoord; + +uniform sampler2D uSampler2; + +uniform float sizeM; +uniform float sizeN; +uniform float m; +uniform float n; + + +void main() { + + // calculate gl_position + gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); + + // calculate ST (vec2) coordinates based on [c,l] and [sizet_c, size_l] + vTextureCoord = vec2(aTextureCoord[0]/sizeM + m/sizeM, aTextureCoord[1]/sizeN + n/sizeN); + +} \ No newline at end of file diff --git a/frontend/shaders/water.frag b/frontend/shaders/water.frag new file mode 100644 index 0000000..ed9cfb7 --- /dev/null +++ b/frontend/shaders/water.frag @@ -0,0 +1,15 @@ +#ifdef GL_ES +precision highp float; +#endif + +varying vec2 vTextureCoord; + +uniform sampler2D uSampler; +uniform float timeFactor; + +void main() { + float offset = timeFactor * 0.01; + vec4 color = texture2D(uSampler, vTextureCoord+vec2(offset, offset)); + + gl_FragColor = color; +} \ No newline at end of file diff --git a/frontend/shaders/water.vert b/frontend/shaders/water.vert new file mode 100644 index 0000000..926943f --- /dev/null +++ b/frontend/shaders/water.vert @@ -0,0 +1,23 @@ +attribute vec3 aVertexPosition; +attribute vec3 aVertexNormal; +attribute vec2 aTextureCoord; + +uniform mat4 uMVMatrix; +uniform mat4 uPMatrix; +uniform mat4 uNMatrix; +uniform float timeFactor; + +varying vec2 vTextureCoord; +uniform sampler2D uSampler2; + +void main() { + vec3 offset=vec3(0.0,0.0,0.0); + + vTextureCoord = aTextureCoord; + vec2 offsetWaves = vec2(mod(vTextureCoord.x + sin(timeFactor*0.01),1.0), mod(vTextureCoord.y + sin(timeFactor*0.01),1.0)); + + offset = aVertexNormal * texture2D(uSampler2, offsetWaves).b * 0.03 * sin(timeFactor * 0.5); + + gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition+offset, 1.0); +} + diff --git a/frontend/sprites/MySpriteAnimation.js b/frontend/sprites/MySpriteAnimation.js new file mode 100644 index 0000000..e1b97a5 --- /dev/null +++ b/frontend/sprites/MySpriteAnimation.js @@ -0,0 +1,81 @@ +/** + * MySpriteAnimation + * @constructor + * @param scene - Reference to MyScene object + * @param spritesheetId - the id of the spritesheet + * @param startCell - the cell where the animation starts + * @param endCell - the cell where de animation ends + * @param duration - the duration of the animation + */ + +class MySpriteAnimation extends CGFobject { + constructor(scene, spritesheetId, startCell, endCell, duration){ + super(scene); + this.spritesheetId = spritesheetId; + this.startCell = startCell; + this.endCell = endCell; + this.duration = duration; + + this.board = new MyRectangle(this.scene, -0.5, -0.5, 0.5, 0.5); + + this.currentState = this.startCell; + this.cellTimes = []; + this.setCellTimes(); + + // Total time passed + this.sumT = 0; + + // Last t from update function arg + this.lastT = 0; + } + + update(t){ + + // End animation + if (this.sumT > this.duration){ + this.currentState = this.startCell; + this.sumT = 0; + } + + if(this.lastT == 0) + this.lastT = t; + + var deltaT = t - this.lastT; + this.lastT = t; + + this.sumT += deltaT; + + // Get the index of the next cell time + var nextCellTimeIdx = this.currentState - this.startCell + 1; + + // Check if the next cell time has started & if the cell should be changed + if (this.nextCellTimeIdx > this.endCell || this.cellTimes[nextCellTimeIdx] > this.sumT) + return; + + // Change currentState if time of the previous cell has ended + if(this.currentState + 1 <= this.endCell) + this.currentState++; + } + + display(){ + // Activate current cell + this.scene.spritesheets[this.spritesheetId].activateCellP(this.currentState); + + // Display the rectangle + this.board.display(); + + // Resume default shader + this.scene.setActiveShader(this.scene.defaultShader); + } + + setCellTimes(){ + // Calculate the start time of each cell + var ncells = this.endCell - this.startCell + 1; + var cellTime = this.duration / ncells; + + for(var i = 0; i < ncells; i++) + this.cellTimes[i] = i * cellTime; + + } + +} \ No newline at end of file diff --git a/frontend/sprites/MySpriteSheet.js b/frontend/sprites/MySpriteSheet.js new file mode 100644 index 0000000..78a77cb --- /dev/null +++ b/frontend/sprites/MySpriteSheet.js @@ -0,0 +1,42 @@ + +/** + * MySpriteSheet + * @constructor + * @param scene - Reference to MyScene object + * @param texture - the texture to be applied + * @param sizeM - the number of columns of the spritesheet + * @param sizeN - the number of line of the spritesheet + */ + +class MySpriteSheet extends CGFobject { + constructor(scene, texture, sizeM, sizeN){ + super(scene); + this.texture = texture; + this.sizeM = sizeM; + this.sizeN = sizeN; + + // Create new shader object from .vert and .frag file + this.spriteShader = new CGFshader(this.scene.gl, "shaders/spriteShader.vert", "shaders/spriteShader.frag"); + } + + activateCellMN(m,n){ + + // Activate shader + this.scene.setActiveShaderSimple(this.spriteShader); + this.texture.bind(); + + // Define shader variables + this.spriteShader.setUniformsValues({ sizeM: this.sizeM, sizeN: this.sizeN, m: m, n: n}); + + } + + activateCellP(p){ + // Get column and row assuming p = 0 at the top left cell and moving left to right and top to bottom + var m = p % this.sizeM; + var n = Math.floor(p / this.sizeM); + + this.activateCellMN(m,n); + } + +} + diff --git a/frontend/sprites/MySpriteText.js b/frontend/sprites/MySpriteText.js new file mode 100644 index 0000000..7d0cde9 --- /dev/null +++ b/frontend/sprites/MySpriteText.js @@ -0,0 +1,53 @@ +/** + * MySpriteText + * @constructor + * @param scene - Reference to MyScene object + * @param text- the tex to be written + */ + +class MySpriteText extends CGFobject { + constructor(scene, text){ + super(scene); + this.text = text; + + this.texture = this.scene.textures['mySpriteTextTexture']; + + this.spriteSheet = new MySpriteSheet(this.scene, this.texture, 16, 16); + + this.board = new MyRectangle(this.scene, -this.text.length/2 -1, -0.5, -this.text.length/2, 0.5); + } + + getCharacterPosition(character){ + // Unicode + // Supports all important characters: [a-z], [A-Z], [0-9], [!#$%/()=?.-,+-_;*^~\@'], [<, >, "] + return character.charCodeAt(0); + } + + display(){ + + var cell; + + // For each char in text + for(var i = 0; i < this.text.length; i++){ + + // Get c,l position for the given char + cell = this.getCharacterPosition(this.text[i]); + + this.scene.translate(1,0,0); + + this.spriteSheet.activateCellP(cell); + + // Display the rectangle + this.board.display(); + + } + + // Resume default shader + this.scene.setActiveShader(this.scene.defaultShader); + } + + setText(text) { + this.text = text; + } + +} \ No newline at end of file diff --git a/frontend/utils.js b/frontend/utils.js new file mode 100644 index 0000000..4a3535b --- /dev/null +++ b/frontend/utils.js @@ -0,0 +1,22 @@ +/** + * Utils Class with static methods to be used + */ +class Utils { + static pi = Math.PI; + + /** + * Converts degrees to radians + * @param degrees + */ + static degToRad(degrees) { + return degrees * (this.pi / 180); + } + + /** + * Converts radians to degrees + * @param radians + */ + static radToDeg(radians) { + return radians * (180 / this.pi); + } +} diff --git a/images/LAIG3_T01_G03_1.png b/images/LAIG3_T01_G03_1.png new file mode 100644 index 0000000..8857fea Binary files /dev/null and b/images/LAIG3_T01_G03_1.png differ diff --git a/images/LAIG3_T01_G03_2.png b/images/LAIG3_T01_G03_2.png new file mode 100644 index 0000000..f11ac1a Binary files /dev/null and b/images/LAIG3_T01_G03_2.png differ diff --git a/images/LAIG3_T01_G03_3.png b/images/LAIG3_T01_G03_3.png new file mode 100644 index 0000000..f152a89 Binary files /dev/null and b/images/LAIG3_T01_G03_3.png differ diff --git a/images/LAIG3_T01_G03_4.png b/images/LAIG3_T01_G03_4.png new file mode 100644 index 0000000..5ca5846 Binary files /dev/null and b/images/LAIG3_T01_G03_4.png differ diff --git a/images/LAIG3_T01_G03_5.png b/images/LAIG3_T01_G03_5.png new file mode 100644 index 0000000..c6a9904 Binary files /dev/null and b/images/LAIG3_T01_G03_5.png differ diff --git a/images/interface.png b/images/interface.png new file mode 100644 index 0000000..6b815da Binary files /dev/null and b/images/interface.png differ diff --git a/images/mapello-example.png b/images/mapello-example.png new file mode 100644 index 0000000..76f0998 Binary files /dev/null and b/images/mapello-example.png differ diff --git a/images/mapello-init.png b/images/mapello-init.png new file mode 100644 index 0000000..42d7fb0 Binary files /dev/null and b/images/mapello-init.png differ diff --git a/images/mapello.gif b/images/mapello.gif new file mode 100644 index 0000000..2f58280 Binary files /dev/null and b/images/mapello.gif differ diff --git a/images/start-menu.png b/images/start-menu.png new file mode 100644 index 0000000..4dbf38c Binary files /dev/null and b/images/start-menu.png differ