Project Categories
Project Setting | Academic |
Team Size | 1 |
Role(s) | Creator / Developer |
Languages | JavaScript HTML CSS C# |
Software/Tools Used | Visual Studio, Visual Studio Code, Brackets, Xamarin, Vue, Node.js, Express.js, React, SCSS/SASS |
Status | Complete |
Time Period | 2019 - 2021 |
About
Yet Another App Clone series is a name I've given to a collection of various web apps and other projects created for my work as a student at RIT. Rather than list every project individually, each one being made in at most a month or two off-and-on, it made more sense to group them all together.
My Work
Yet Another Weather App
Yet Another Weather App (November 2019) is a project created for IGME 235 - Intro to Game Web Tech. The goal of this project was to find a third party API and create a web app using it. YAWA makes as much use out of the OpenWeatherMap Weather API as is possible with a free tier and the time frame given (approximately a couple weeks). There are some features I would've liked to implemented into the project, but did not due to time constraints.
Please note: If testing the API, please limit API calls (i.e. searching a location, updating units, or updating view), as the free tier has a limit of 60 calls per minute.
Interface
- Minimalistic, dark-themed, fully-responsive layout befitting the weather app
- Scalable vector graphics for custom weather icons
- Easy controls based on user feedback
Code
- Grab current weather or five day forecast based on zip code from OWM API
- Translated API object data into useful information, including converting from meteorological wind degrees to wind directions
- Save user preference for zip code, units, and five-day versus current, which automatically loads upon the next page load
Spleeper (Yet Another Minesweeper Clone)
Spleeper (December 2019) is an academic project demonstrating DOM manipulation via JavaScript. Spleeper combines the main gameplay of minesweeper (flagging mines based off of numerical neighbor indicators) and the added quick movement and 'falling' from the Spleef minigame from Minecraft. The game is fully functional on desktop (mobile/portrait-based windows not supported), and offers a couple ways to play (keyboard + mouse or keyboard-only). Many classmates were interested in the game both during a demonstration and feedback day as well as the final presentation.
Note: there is a bug of unknown cause that makes rendering the game area odd in Chrome browsers on high resolution (2K+) monitors. This is either a Chrome issue or a graphical issue out of Spleeper's control.
There are multiple features I wanted to add but couldn't due to time constraints. There was also plenty of expressed interest in the project. If I were to continue with this concept, I would remake it either with PixiJS or a game engine such as Unity or Godot.
Interface
- Three displays (instructions menu, play area + game info, and death menu) representing the three main game states
- Retro-cyberpunk theme fitting the game's story
- Animations added to give a more lively feedback
Assets
- Custom sound effects for movement, death/falling, tile revealing, level up, tile flagging, etc.
- Custom music for the instructions menu, main game, and death menu
- Custom SVG player graphic
- Symbolic instructions menu that is easy to read
Code
- Translated Rosetta Code's implementation of Minesweeper in C# and based recursive revealing algorithm from it
- 2D vertical/horizontal player movement based off of keyboard input
- Flagging (intended for tiles that the player believes are access points; if not, corrupts the tile) / Revealing (acts in very similar manner to Minesweeper, with added twist that revealing an access point corrupts it and reduces the number of access points in the level) capabilities using R/F (respectively) keys or right/left (respectively) mouse-clicking
- Game loop that allows for a time limit (entire level turns into corrupted tiles after expiration) as well as a timer for tiles that are corrupted (in state of corrupting - player has chance to get off of it) or access points (must wait until this timer expires to be able to use it to advance a level)
- Difficulty that increases as the player levels up: decreases overall time limit, decreases corrupt/access timer, decreases number of access tiles initially available, and increases number of pre-corrupted tiles in tile generation. Each of these scales independently with different %-chances for each of them. All four have limits in place (i.e. difficulty reaches a highest-point).
- Local storage keeps track of highest achieved score
Yet Another Brick Breaker
Yet Another Brick Breaker (February 2021 - March 2021) is a project created for IGME 330 - Rich Media Web App Development. The goal of this project was to create some sort of experience utilizing the Canvas API. YABB was created with my own mini update/rendering framework that made creating Canvas-based elements and scenes as simple to make and re-usable as possible with the time frame given (approximately a couple weeks). There are some features I would've liked to implemented into the project, but did not due to time constraints.
Interface
- Minimalistic, dark-themed, moderately responsive (desktop only) with a theme befitting the game
- Simple controls with two sets of options
- Ability to pause the game and choose things such as scene and components of enemy difficulty
Code
- Clear and well documented class/module structure with inheritance broken up between files like
scenes
,shapes
,structs
, andclasses
- Utility methods to perform common tasks such as getting random colors
Yet Another News Story Morphing App
Yet Another News Story Morphing App (April 2021 - May 2021) is a project created for IGME 330 - Rich Media Web App Development. The goal of this project was to create some sort of meaningful/useful mix of two API. YANSMA used RiTa and TheNewsAPI (with Vue for re-useable UI components) to create "An app that takes existing articles and replaces certain words in the results to generate ideas for writing/drawing/etc, with the ability to store results locally" with the time frame given (approximately a few weeks). There are some features I would've liked to implemented into the project, but did not due to time constraints.
Interface
- Minimalistic, light-themed, moderately responsive (potentially some untested mobile issues) with a theme befitting the app
- Simple three-panel layout with search options on the left, search/manipulated results in the middle, and stored results on the right.
Code
- Clear and well documented class/module structure with inheritance broken up between files like
components
,classes
, andapp
- Clear and thorough error handling and form validation with statuses used in place of results that are reported back to the user
Cryptid Keeper (Yet Another RPG Character Creator)
Cryptid Keeper (October 2021) is a project created for IGME 430 - Rich Media Web App Development 2. The goal of this project was to create any sort of web app with a server API backend implemented by a web app frontend. Requirements included supporting basic HTTP status codes, GET/HEAD/POST requests, AJAX, and query parameters. My app allows the user to create characters with 6 DnD-like stats and a custom pixel-art appearance of species, fur/skin pattern, outfit, and weapon from custom-made tilesheets.
Front-End
- Basic, partially responsive three-panel layout, with character search on top, appearance options on left, and stats on right
- Implements HEAD requests by checking if a character exists that the user enters into the search name box; GET requests load existing characters into the UI for editing
- Implements POST requests by saving character data from the UI, identified by name
- AJAX handled by XHR
- User can choose species from top row of left panel; when this is updated, the tilesheet image sources update to account for its change in the left/right panels
- User can choose fur/skin pattern, outfit, and weapon in left panel; choices are reflected in a preview (which animates changing) on the right
- User can set 6 stats (Perception, Wit, Willpower, Agility, Strength, Endurance) and character name in right panel under the preview
- User has ability to reset the UI back to defaults
- All inputs other than appearance choices have tippy.js tooltips (appearance choices have always-present labels beneath them) and are validated client-side
Back-End
- Serves client-side files (HTML, organized JS files, images, etc.) onRequest
- Has HEAD API for checking the existence of characters saved in memory (params: character name)
- Has GET API for serving characters saved in memory (params: character name)
- Has POST API for saving/updating characters in memory (params: appearance, name, stats)
- Character POST validated server-side
- Properly handles various relevant status codes and manually handles routing for requests
Yet Another Task App
Yet Another Task App (November 2021 - December 2021) is a fullstack project for IGME 430 - Rich Media Web App Development 2. The goal of this project was to serve a web app utilizing a database for user account management and some other database model(s) for user app data. The frontend is built with React and implements MarkdownIt for supporting markdown in task descriptions, and the backend is built with Express.js and uses MongoDB and Redis for database management. The server compiles SCSS and Handlebars for views and serves them to the client.
Front-End
- Utilized React to create various window-views and components of the task app with conditionals for handling edit-state and premium-status of task editor
- Added markdown parsing using MarkdownIt to task description component
- Implemented Moment.js for displaying due dates on tasks
Back-End
- Implemented account model and login system previously created for a prior preparatory class project
- Expanded account system with premium status and password changing
- Added experience points transferred from task model to user model to make task completion act as a sort of rudimentary RPG task app
Yet Another Idle Clicker
Yet Another Idle Clicker (October 2021 - December 2021) is a cross-platform mobile app written with Xamarin for IGME 340 - Multi Platform Media App Development. The goal of this project was to use Xamarin to create a cookie clicker clone that makes use of various Xamarin controls and styling.
Samples
These are some samples from the projects.
Videos
Spleeper
Gameplay video of Spleeper with captions describing different aspects of the game not already described on the instructions screen
Yet Another Brick Breaker
Demonstration of the game with textual commentary
Yet Another News Story Morphing App
Demonstration of the app with textual commentary
Screenshots
Yet Another Weather App

Example of the five day forecast in imperial units

Example of the current weather in metric units
Spleeper

Instructions screen

Demonstration of corrupt tiles - light orange is in the process of corrupting, reddish is a corrupted tile

Demonstration of access point tiles - light turquoise is in the process of accessing, medium blue is a fully usable access point

View of impending doom when the timer expires

Death menu after running out of lives; displays score from the played round + the highest scored
Yet Another Brick Breaker

Example of gameplay, with droplets from bricks falling toward player and projectiles bouncing around. The player is the bottom paddle, the enemy AI is the top paddle

Screenshot of the pause menu

Screenshot of the settings panel
Code
Yet Another Brick Breaker
This is the method which initializes each game scene when the scene is changed/the page is loaded. Depending on the index, there are different mathematical formulas used for generating the scene. Depends on some utility methods such as getRandomInt and a scene-related method getPalette (which returns a color based on the index of the scene and what index of the palette to get)
/**
* Gets a SceneObject from a requested index
* @param {number} index The index of the scene to laod
* @returns {SceneObject} The scene requested
*/
function getScene(index) {
let breakables = [];
let count;
switch(index) {
case "0":
count = 30;
for (let i = 0; i < count; i++) {
let theta = ((i + 1) / count) * Math.PI * 2;
breakables.push(new Breakable(CONSTANTS.LAYERS.ENEMY, new Vec2(900/(count + 5) * i + ((i + 1) * 40), Math.sin(theta) * 50 + 250), new Vec2(40, 20), getPalette(index, 0)));
breakables.push(new Breakable(CONSTANTS.LAYERS.ENEMY, new Vec2(900/(count + 5) * i + ((i + 1) * 40), Math.sin(theta) * 25 + 350), new Vec2(40, 20), getPalette(index, 0)));
breakables.push(new Breakable(CONSTANTS.LAYERS.PLAYER, new Vec2(900/(count + 5) * i + ((i + 1) * 40), -Math.sin(theta) * 50 + 750), new Vec2(40, 20), getPalette(index, 1)));
breakables.push(new Breakable(CONSTANTS.LAYERS.PLAYER, new Vec2(900/(count + 5) * i + ((i + 1) * 40), -Math.sin(theta) * 25 + 650), new Vec2(40, 20), getPalette(index, 1)));
}
return new SceneObject(breakables);
case "1":
count = 50;
for (let i = 0; i < count; i++) {
let theta = ((i + 1) / count * Math.PI * 2);
// center circle
breakables.push(new Breakable(CONSTANTS.LAYERS.UNIVERSAL, new Vec2(CONSTANTS.CANVAS_SIZE / 2 + 350 * Math.cos(theta), CONSTANTS.CANVAS_SIZE / 2 + 350 * Math.sin(theta)), new Vec2(25 * Math.abs(Math.cos(theta / 2)) + 5, 25 * Math.abs(Math.cos(theta / 2)) + 5), getPalette(index, 0), new StrokeProperties(), Shapes.drawEllipse));
}
return new SceneObject(breakables);
case "2":
count = 50;
for (let i = 0; i < count; i++) {
breakables.push(new Breakable(CONSTANTS.LAYERS.ENEMY, new Vec2(getRandomInt(100, 900), getRandomInt(100, 900)), new Vec2(30, 30), getPalette(index, 0)));
breakables.push(new Breakable(CONSTANTS.LAYERS.PLAYER, new Vec2(getRandomInt(100, 900), getRandomInt(100, 900)), new Vec2(30, 30), getPalette(index, 1)));
}
return new SceneObject(breakables);
}
}
A projectile represents the circles which bounce around the game area. It inherits from GenericObject, which is a generic class used for the basic rendering and updating of the object. It uses classes defined in structs.js
like Vec2, StrokeProperties, etc., and defaults to the drawEllipse
shape method defined in shapes.js
. It has methods for moving over time with a constant velocity, reflecting (complete 180-degree change in direction) when hitting a paddle, and bouncing (angle is determined by the difference of centers) when hitting the bounds or a brick.
class Projectile extends GenericObject {
/**
* A typically circular object which is used for achieving the main objective (bypassing opponent) and destroying breakables
* @param {Vec2} [position = new Vec2()] The position of the object on the canvas
* @param {Vec2} [size = new Vec2()] The size of the object on the canvas
* @param {FillStyle} [fill = new FillStyle()] The fill of the object
* @param {StrokeProperties} [stroke = new StrokeProperties()] The stroke of the object
* @param {function} [shape = Shapes.drawEllipse] The shape rendering method for the object
* @param {number} [shapeEdges = 3] The number of edges to render the shape with (only applies when drawing a polygon)
*/
constructor(position = new Vec2(), size = new Vec2(), fill = new FillStyle(), stroke = new StrokeProperties(), shape = Shapes.drawEllipse, shapeEdges = 3) {
super(position, size, fill, stroke, shape, shapeEdges);
this.velocity = new Vec2();
}
/**
* Updates projectile information over time
* @param {number} dt delta time in ms
*/
update(dt) {
super.update(dt);
this.move(dt);
}
/**
* Handle moving the projectile over time
* @param {number} dt delta time in ms
*/
move(dt) {
let move = {x: this.velocity.x * dt, y: this.velocity.y * dt};
this.position.add(move);
}
/**
* Handle reflection of the projectile when it bumps into something
* @param {string} axis The axis to apply a -1 modifier on; this is a string, which is used as a key for the velocity vector (velocity[axis])
* @param {number} dt delta time in ms
*/
reflect(axis, dt) {
this.velocity[axis] *= -1;
// adds an extra frame of move to prevent projectile getting stuck
this.move(dt);
}
/**
* Bounces a projectile off of an object
* @param {string} axis The axis of which to perform bounce on in regards to the projectile's velocity
* @param {Vec2} centerOther The center point of the other object the projectile is bouncing off of
* @param {number} dt delta time in ms
*/
bounce(axis, centerOther, dt) {
this.velocity[axis] += (this.bounds.center()[axis] - centerOther[axis]) / centerOther[axis] * CONSTANTS.PROJECTILE_BOUNCE_FACTOR;
this.velocity = this.velocity.normalized();
// adds an extra frame of move to prevent projectile getting stuck
this.move(dt);
}
}
Yet Another News Story Morphing App
This is the search method, which sets up and utilizes an AJAX request to TheNewsAPI. Each step has a comment; overall it first checks if the page passed validation (the UI repsonds to this.result
being a number rather than an object by reporting the status of the UI and any errors if applicable), and if so, it then checks if the search parameters have changed since the last search button click (to limit requests; if it's the same, it just calls useResult again, as the button calls this "Refresh Search"). Finally, it forms the search URL and performs an XHR whose onload calls the callback passed in to the downloadFile method.
/**
* (bound to search button) Performs search and result manipulation
*/
search() {
if (this.validate()) {
// update the current search params object
this.searchParams = {
query: this.query,
count: this.count,
page: this.page,
from: this.from,
to: this.to,
genre: this.genre,
category: this.category,
newstype: this.newstype,
}
localStorage.setItem(lastSearchKey, JSON.stringify(this.searchParams));
// only search if the search parameters haven't changed from lastSearchParams
if (JSON.stringify(this.lastSearchParams) !== JSON.stringify(this.searchParams)) {
// set result to -1 to update the computed resultHTML while search is occuring
this.result = -1;
this.resultUpdated = false;
// set up the get url
let get = `${url}${this.newstype}?api_token=${api}&language=en&limit=${this.count}&page=${this.page}&published_before=${this.to}&published_after=${this.from}&categories=${this.category}`;
// ensure an empty string isn't sent, to allow for searching without a query (which is possible with the API)
if (this.query.length > 0)
get += `&search=${this.query}`;
// perform AJAX request
downloadFile(get, (jsonStr) => {
// set the current newsResult object from parsed JSON, then use it with a random index
this.newsResult = JSON.parse(jsonStr).data;
this.useResult(getRandomInt(0, this.newsResult.length - 1));
});
}
else
this.useResult(getRandomInt(0, this.newsResult.length - 1));
// update the lastSearchParams with the current searchParams
this.lastSearchParams = this.searchParams;
}
else
this.result = 400;
}
This is the useResult method, which uses the current result (as long as it's not a status code but rather an object) to form a manipulated result using the RiTa API and randomization from a defined WordDatabase organized by genre then by part-of-speech (POS).
/**
* Uses the current newsResult to form the current Result class instance
* @param {number} [index = 0] The index in the newsResult to manipulate
*/
useResult(index = 0) {
// update lastIndex so that this.swap uses the same news result from the collection
this.lastIndex = index;
if (this.newsResult.length > 0) {
// gets the title to work with...
let title = this.newsResult[index].title;
// ...gets a collection of words...
let words = title.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,'').trim().split(' ').filter(elem => elem);
// ...and parts of speech...
let pos = RiTa.pos(title.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g,'').trim().split(' ').filter(elem => elem));
// ...in order to create changed words
let changed = [];
// loop through the collections to change valid words
for (let i = 0; i < pos.length; i++) {
let word;
// replace nouns
if (pos[i] == 'nn' || pos[i] == 'nns' || pos[i] == 'nnp' || pos[i] == 'nnps') {
// replace with random word from current genre
word = WordDatabase[this.genre].nn[getRandomInt(0, WordDatabase[this.genre].nn.length - 1)];
// pluralize if the original was plural
if (pos[i] == 'nns' || pos[i] == 'nnps')
word = RiTa.pluralize(word);
}
// push to the changed array
if (word)
changed.push(new classes.ChangedWord(
words[i], i,
word,
this.genre,
this.swapSingleWord
));
}
// set the current result to a new class instance of Result with the current working title and the changed words array
this.result = new classes.Result({ title: title }, changed);
this.resultUpdated = true;
}
else {
this.resultUpdated = false;
this.result = 404;
}
}
Cryptid Keeper
Below are routing handlers for getting/saving characters along with the generic method for all responses (including routers for Not Found, Bad Request, etc, not shown due to simplicity/similarity). Note: characterUtils.*
and responseUtils.*
are imported custom utility modules
/**
* General response writing method
* @param {object} request XHR request object
* @param {object} response XHR response object
* @param {number} status HTML status code
* @param {string} type HTML Content-Type
* @param {object} data data to write
*/
const respond = (request, response, status, type, data) => {
const validatedType = responseUtils.getValidType(type);
response.writeHead(status, { 'Content-Type': validatedType });
if (data) {
let toWrite = data;
if (validatedType === 'text/xml') {
toWrite = responseUtils.toXML(data);
} else {
toWrite = JSON.stringify(data);
}
response.write(toWrite);
}
response.end();
};
/**
* Routing handler for /getCharacter
* @param {object} request XHR request object
* @param {object} response XHR response object
* @param {object} params GET/POST parameters, if any
*/
const getCharacter = (request, response, params) => {
const indexOfCharacter = characterUtils.findCharacter(
characters, { name: params.name },
);
if (indexOfCharacter > -1) {
if (request.method === 'HEAD') {
respond(request, response, 204, request.headers.accept);
} else {
respond(request, response, 200, request.headers.accept, characters[indexOfCharacter]);
}
} else {
notFound(request, response);
}
};
/**
* Routing handler for /saveCharacter
* @param {object} request XHR request object
* @param {object} response XHR response object
* @param {object} params POST parameters
*/
const saveCharacter = (request, response, params) => {
const indexOfCharacter = characterUtils.findCharacter(
characters, { name: params.charToAdd.name },
);
if (indexOfCharacter > -1) {
characters[indexOfCharacter] = params.charToAdd;
respond(request, response, 204, request.headers.accept);
} else {
characters.push(params.charToAdd);
respond(request, response, 201, request.headers.accept);
}
};
Places
Yet Another Weather App | Project 2 @ people.rit.edu |
Spleeper | Spleeper @ people.rit.edu |
Yet Another Brick Breaker | Project 1 @ people.rit.edu |
Yet Another News Story Morphing App | Project 2 @ people.rit.edu |
Cryptid Keeper (No Longer Deployed) | Web App |
Yet Another Task App (No Longer Deployed) | Web App |