diff --git a/.glitch-assets b/.glitch-assets new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index b61d3e2b..08d127c3 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,23 @@ -Assignment 4 - Creative Coding: Interactive Multimedia Experiences -=== +# Readme -Due: October 11th, by 11:59 PM. +## Project Title: Firework Simulator -For this assignment we will focus on client-side development using popular audio/graphics/visualization technologies; the server requirements are minimal. The goal of this assignment is to refine our JavaScript knowledge while exploring the multimedia capabilities of the browser. +The project is hosted at https://a4-lfleming-314.glitch.me/. -Baseline Requirements ---- +## Goal -Your application is required to implement the following functionalities: +This application is a visual experience which allows users to launch and customize virtual fireworks. Users can control the shape, size, and colors of the fireworks. The virtual environment simulates the effects of wind and gravity on these particles. -- A server created using Express. This server can be as simple as needed. -- A client-side interactive experience using at least one of the following web technologies frameworks. - - [Three.js](https://threejs.org/): A library for 3D graphics / VR experiences - - [D3.js](https://d3js.org): A library that is primarily used for interactive data visualizations - - [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API): A 2D raster drawing API included in all modern browsers - - [SVG](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API): A 2D vector drawing framework that enables shapes to be defined via XML. - - [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API): An API for audio synthesis, analysis, processing, and file playback. -- A user interface for interaction with your project, which must expose at least six parameters for user control. [dat.gui](https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage) is highly recommended for this. You might also explore interaction by tracking mouse movement via the `window.onmousemove` event handler in tandem with the `event.clientX` and `event.clientY` properties. Consider using the [Pointer Events API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) to ensure that that mouse and touch events will both be supported in your app. -- Your application should display basic documentation for the user interface when the application first loads. This documentation should be dismissable, however, users should be able to redisplay it via either a help buton (this could, for example, be inside a dat.gui interface) or via a keyboard shortcut (commonly the question mark). -- Your HTML and CSS should validate. There are options/plugins for most IDEs to check validation. +The project uses the Canvas API to draw the fireworks. -The interactive experience should possess a reasonable level of complexity. Some examples: -### Three.js -- A generative algorithm creates simple agents that move through a virtual world. Your interface controls the behavior / appearance of these agents. -- A simple 3D game -- An 3D audio visualization of a song of your choosing. User interaction should control aspects of the visualization. -### Canvas -- Implement a generative algorithm such as [Conway's Game of Life](https://bitstorm.org/gameoflife/) (or 1D cellular automata) and provide interactive controls. Note that the Game of Life has been created by 100s of people using ; we'll be checking to ensure that your implementation is not a copy of these. -- Design a 2D audio visualizer of a song of your choosing. User interaction should control visual aspects of the experience. -### Web Audio API -- Create a screen-based musical instrument using the Web Audio API. You can use projects such as [Interface.js](http://charlie-roberts.com/interface/) or [Nexus UI](https://nexus-js.github.io/ui/api/#Piano) to provide common musical interface elements, or use dat.GUI in combination with mouse/touch events (use the Pointer Events API). Your GUI should enable users to control aspects of sound synthesis. -### D3.js -- Create visualizations using the datasets found at [Awesome JSON Datasets](https://github.com/jdorfman/Awesome-JSON-Datasets). Experiment with providing different visualizations of the same data set, and providing users interactive control over visualization parameters and/or data filtering. Alternatively, create a single visualization with using one of the more complicated techniques shown at [d3js.org](d3js.org) and provide meaningful points of interaction for users. +## Challenges -Deliverables ---- +Some of the biggest challenges on making this application were drawing teardrop shapes and making the different explosions. -Do the following to complete this assignment: +The teardrop shapes were tricky partly because I did not have much experience with raster drawing, and so it took some work to figure out what kind of arcs and lines were needed to draw that shape, and how to adjust it for different sizes and lengths. The other part was that I needed the orientation of the teardrops to match the current direction of the particle, so I had to make trigonometric functions that worked properly for all the possible angles of velocity and translate that into the raster drawings. -1. Implement your project with the above requirements. -3. Test your project to make sure that when someone goes to your main page on Glitch/Heroku/etc., it displays correctly. -4. Ensure that your project has the proper naming scheme `a4-firstname-lastname` so we can find it. -5. Fork this repository and modify the README to the specifications below. *NOTE: If you don't use Glitch for hosting (where we can see the files) then you must include all project files that you author in your repo for this assignment*. -6. Create and submit a Pull Request to the original repo. Name the pull request using the following template: `a4-firstname-lastname`. +For the explosions, I looked into various types of firework shapes that are used in the real world and tried to break them down into their base components. I built each firework shape as an object class, with its own draw() and explode() method. More complicated fireworks launch simpler fireworks as part of their explosions. It took a lot of experimenting with timing and angles to make the different explosion shapes look right. -Sample Readme (delete the above when you're ready to submit, and modify the below so with your links and descriptions) ---- +## Instructions -## Your Web Application Title - -your hosting link e.g. http://a4-charlieroberts.glitch.me - -Include a very brief summary of your project here. Images are encouraged when needed, along with concise, high-level text. Be sure to include: - -- the goal of the application -- challenges you faced in realizing the application -- the instructions you present in the website should be clear enough to use the application, but if you feel any need to provide additional instructions please do so here. +Fireworks are created by clicking within the black canvas on the webpage. Parameters for the fireworks and the environment can be adjusted via the form underneath the canvas. Instructions, with more detailed descriptions of the parameters, are displayed when the page first loads or by clicking the "Help" button in the top right corner of the page. \ No newline at end of file diff --git a/canvas.html b/canvas.html new file mode 100644 index 00000000..51b95fb3 --- /dev/null +++ b/canvas.html @@ -0,0 +1,30 @@ + + + + + Fireworks! + + + + + + + + + + +
+ +
+ +
+ + diff --git a/index.html b/index.html new file mode 100644 index 00000000..386b61a4 --- /dev/null +++ b/index.html @@ -0,0 +1,512 @@ + + + + + + + Fireworks! + + + + + + + + + + +
+

Firework Simulator

+ +
+
+

Click the black canvas to launch a firework!

+

+ The controls below the canvas allow you to customize your fireworks. +

+ +

+ Launch Options controls the initial speed and size. + +

+ + +

Colors allows you to select the color(s) of the fireworks.

+ +

Shapes lets you choose the type(s) of explosions.

+ +

+ Environment defines the external forces that act on particles. + +

+ + +

+ Advanced Options modifies more specific properties of the + fireworks. + +

+ + +

+ View a larger version of the canvas + +

+ +

+ You can always re-open these instructions via the "Help" button in the + upper right corner. +

+ +
+
+ +
+
+ Launch Options: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Colors: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Shapes: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Environment: +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ Advanced Options: +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..77826f7d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,374 @@ +{ + "name": "firework-simulator", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", + "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + }, + "mime-types": { + "version": "2.1.28", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", + "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "requires": { + "mime-db": "1.45.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b56c5a55 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "//1": "describes your app and its dependencies", + "//2": "https://docs.npmjs.com/files/package.json", + "//3": "updating this file will download and update your packages", + "name": "firework-simulator", + "version": "0.0.1", + "description": "An interactive animation that simulates fireworks.", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.17.1" + }, + "engines": { + "node": "12.x" + }, + "repository": { + "url": "https://glitch.com/edit/#!/hello-express" + }, + "license": "MIT", + "keywords": [ + "node", + "glitch", + "express" + ] +} \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 00000000..58c5c8e3 --- /dev/null +++ b/public/script.js @@ -0,0 +1,704 @@ +//SET CANVAS DIMENSIONS +// set canvas dimensions, relative to window size +const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); +const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); +var canvas = document.getElementById("canvas"); +canvas.height = vh * 0.45; +canvas.width = vw * 0.95; +if (window.location.href.includes("/canvas")) { + canvas.height = vh * 0.95; + canvas.width = vw * 0.98; +} +var ctx = canvas.getContext('2d'); + +// INITIALIZATION +// initialize circ +const circ = 2 * Math.PI; //circumference of a circle in radians + +// define color lists +//firework color will be randomly chosen from whichever list(s) are selected +const reds = ["#be0032", "#ff0000", "#e2062c", "#b22222", "#7f1734", "#92000a", "#ff3800", "#ff004f"]; +const oranges = ["#da9100", "#ffa500", "#ff8c00", "#ff7f50", "#ffc87c", "#ff7518", "#fb4f14", "#fbceb1"]; +const yellows = ["#ffff00", "#f5c71a", "#fffacd", "#f0e130", "#e3ff00", "#e6e200", "#fcc200", "#e4d00a"]; +const greens = ["#00ff00", "#32cd32", "#bfff00", "#6b8e23", "#98fb98", "#008000", "#00fa9a", "#ace1af"]; +const blues = ["#7fffd4", "#40e0d0", "#20b2aa", "#00ffff", "#9bddff", "#00b7eb", "#0000ff", "#333399"]; +const purples = ["#dcd0ff", "#800080", "#8a2be2", "#9370db", "#4b0082", "#dda0dd", "#c9a0dc", "#bb3385"]; +const pinks = ["#ffc0cb", "#ffddf4", "#ff69b4", "#ff1493", "#db7093", "#ff6fff", "#fc5a8d", "#da1d81"]; +const browns = ["#ffe4c4", "#f4a460", "#bc8f8f", "#cd853f", "#a0522d", "#8b4513", "#965a3e", "#ba8759"]; +//define list of possible trail colors- these are not user-controlled +const trailColors = ["#ffeebf", "#ffa200", "#969696", "#636363", "#f7f7f7", "#1c1c1c"]; + +//initialize worldlist to store the fireworks +var worldlist = []; + +// PROCESS USER INPUT +//initialize object to store user-controlled variables +//keeping a local copy here so that the program doesn't have to +//make a fetch request every time it makes a firework! +var properties = {}; + +//get properties from server +//and start animation +window.onload = function() { + if(window.location.href.includes("canvas")) { //on canvas page + //get info from server + fetch("/properties", {method: "GET"}).then(response => response.json()).then(json => { + console.log("canvas fetched"); + for (var parameter in json) { + properties[parameter] = json[parameter]; + } + }).then(console.log("loaded properties from server")); + } else { //on index page + //get info from server and populate html forms + fetch("/properties", {method: "GET"}).then(response => response.json()).then(json => { + checkBox("reds", json.reds); + checkBox("oranges", json.oranges); + checkBox("yellows", json.yellows); + checkBox("greens", json.greens); + checkBox("blues", json.blues); + checkBox("purples", json.purples); + checkBox("pinks", json.pinks); + checkBox("browns", json.browns); + checkBox("stardust", json.stardust); + checkBox("comet", json.comet); + checkBox("ring", json.ring); + checkBox("bouquet", json.bouquet); + checkBox("spiral", json.spiral); + checkBox("peony", json.peony); + checkBox("dahlia", json.dahlia); + checkBox("crossette", json.crossette); + checkBox("airResistance", json.airResistance); + fillNumber("windspeed", json.windspeed); + fillNumber("gravity", json.gravity); + checkBox("xyFlip", json.xyFlip); + fillNumber("launchSpeed", json.launchSpeed); + fillNumber("launchAngle", json.launchAngle); + fillNumber("fuse", json.fuse); + fillNumber("radius", json.radius); + fillNumber("length", json.length); + fillNumber("duration", json.duration); + fillNumber("explodeSpeed", json.explodeSpeed); + fillNumber("ringParticles", json.ringParticles); + fillNumber("bouquetParticles", json.bouquetParticles); + fillNumber("spiralParticles", json.spiralParticles); + fillNumber("crossetteBursts", json.crossetteBursts); + fillNumber("spiralLoops", json.spiralLoops); + fillNumber("emitterRings", json.emitterRings); + }).then(console.log("loaded properties from server")); + } + //set animation interval + window.setInterval(updateWorld, 100); //100ms +} + +//functions to populate html form +//checks or unchecks checkbox #id depending on whether status is true or false +//and updates local copy of properties +const checkBox = function(id, status) { + document.getElementById(id).checked = status; + properties[id] = status; +} +//fills in input #id with the value of status +//and updates local copy of properties +const fillNumber = function(id, status) { + document.getElementById(id).value = status; + properties[id] = status; +} + +//helper functions to update from user input +//update checkboxes +const toggle = function(propName) { + var newSetting = !(properties[propName]); + properties[propName] = newSetting; + updateServer(propName, newSetting); +} +//update number entry fields +//returns float +const updateNumber = function(value, propName) { + var newSetting = validateFloat(value); + properties[propName] = newSetting; + updateServer(propName, newSetting); +} +//returns positive float (absolute value) +const updatePosNumber = function(value, propName) { + var newSetting = Math.abs(validateFloat(value)); + properties[propName] = newSetting; + updateServer(propName, newSetting); +} +//return positive integer- (absolute value rounded down) +const updatePosInt = function(value, propName) { + var newSetting = Math.floor(Math.abs(validateFloat(value))); + properties[propName] = newSetting; + updateServer(propName, newSetting); +} +//convert input to float, or set to 0 if NaN +const validateFloat = function(num) { + var parsedNum = parseFloat(num); + if (isNaN(parsedNum)) { + parsedNum = 0; + } + return parsedNum; +} + +//update server with changed property +const updateServer = function(propName, newSetting) { + var updateData = { + propName: propName, + newSetting: newSetting + }; + fetch("update", { + method: "POST", + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(updateData) + }).then(console.log("updated", propName, newSetting)); +} + +//DEFINE FIREWORK CLASSES +//Particle is the base class, disappears when it explodes +//circle with smaller white circle inside +class Particle { + constructor(x, y, color, vx, vy, fuse, radius) { + this.x = x; //x position on canvas + this.y = y; //y position + this.color = color; //explosion color + this.vx = vx; //x velocity + this.vy = vy; //y velocity + this.fuse = fuse; //counts down to explosion + this.radius = radius; //defines size + this.exploded = false; //has it exploded yet? + } + + //draw the firework onto the canvas + draw() { + drawDot(this.x, this.y, translucent(this.color, this.fuse), this.radius); //circle + drawDot(this.x, this.y, translucent("#FFFFFF", this.fuse-1), this.radius/2); //smaller white circle inside + } + + //runs when fuse == 0 + explode() { + this.exploded = true; + } + + //update values for next time it gets rendered + iterate() { + this.draw(); + this.x += this.vx; //horizontal motion + this.vx = accelerateX(this.vx); //horizontal acceleration + this.y += this.vy; //vertical motion + this.vy = accelerateY(this.vy); //vertical acceleration + this.fuse -= 1; + if (this.fuse <= 0) { + this.explode(); + } + } +} +//Trails add a length property +class Trail extends Particle { + constructor(x, y, color, vx, vy, fuse, radius, length) { + super(x, y, color, vx, vy, fuse, radius); + this.length = length; //length of teardrop + } +} +//Stardust has a teardrop shape +class Stardust extends Trail { + draw() { + drawTeardrop(this.x, this.y, translucent(this.color, this.fuse), this.radius, this.vx, this.vy, this.length); + } +} +//Comet has a teardrop with a white circle inside +class Comet extends Trail { + draw() { + drawTeardrop(this.x, this.y, translucent(this.color, this.fuse), this.radius, this.vx, this.vy, this.length); + drawDot(this.x, this.y, translucent("#FFFFFF", this.fuse-1), this.radius/2); + } +} + +//Firework uses a small, randomly-colored teardrop shape +//and have added parameters to do more complicated things when they explode +class Firework extends Trail { + constructor(x, y, color, vx, vy, ffuse, radius, length, explodeSpeed, duration, trailColor, numParticles) { + super(x, y, color, vx, vy, ffuse, radius, length); + this.explodeSpeed = explodeSpeed; //base speed of exploded particles + this.duration = duration; //how long the exploded particles endure + this.trailColor = trailColor; //color before explosion + this.numParticles = numParticles; //how many particles are released by the explosion + } + + draw() { + drawTeardrop(this.x, this.y, this.trailColor, this.radius/2, this.vx, this.vy, this.length); + } +} +//Bouquet explodes into a 120-degree arc of Comets +class Bouquet extends Firework { + draw() { //made to look like a Comet + drawTeardrop(this.x, this.y, this.color, this.radius, this.vx, this.vy, this.length); + drawDot(this.x, this.y, "#FFFFFF", this.radius/2); + } + explode() { + this.exploded = true; + var vAngle = angle(this.vx, this.vy); + var increment = (circ / 3) / this.numParticles; + var startAngle = vAngle + circ/ (2 * this.numParticles); + for (var p = 0; p < this.numParticles; p++) { + var pAngle = startAngle + p * increment; + var pV = calcLaunchSpeeds(this.explodeSpeed, pAngle); + worldlist.push(new Comet(this.x + pV[0], this.y + pV[1], this.color, pV[0], pV[1], this.duration, this.radius, this.length)) + } + } +} +//Ring explodes into a circle of Comets +class Ring extends Firework { + explode() { + this.exploded = true; + var increment = circ / this.numParticles; //angle between exploded particles + for (var p = 0; p < this.numParticles; p++) { + var pAngle = p * increment; + var pV = calcLaunchSpeeds(this.explodeSpeed, pAngle); + worldlist.push(new Comet(this.x, this.y, this.color, pV[0], pV[1], this.duration, this.radius, this.length/2)) + } + } +} +//Crossette explodes into a circle of Rings +class Crossette extends Firework { + constructor(x, y, color, vx, vy, ffuse, radius, length, explodeSpeed, duration, trailColor, numParticles, bursts) { + super(x, y, color, vx, vy, ffuse, radius, length, explodeSpeed, duration, trailColor, numParticles); + this.bursts = bursts; //number of particles in the explosion rings + } + explode() { + this.exploded = true; + var vAngle = angle(this.vx, this.vy); + var increment = 360 / this.numParticles; + for (var p = 0; p < this.numParticles; p++) { + var pAngle = vAngle + p * increment; + var pV = calcLaunchSpeeds(this.explodeSpeed, degToRad(pAngle)); + worldlist.push(new Ring(this.x, this.y, this.color, pV[0], pV[1], this.duration/2, this.radius, this.length, this.explodeSpeed, this.duration/2, this.color, this.bursts)); + } + } +} + +//Emitter adds functionality to do more complicated things before the explosion +class Emitter extends Firework { + constructor(x, y, color, vx, vy, ffuse, radius, length, explodeSpeed, duration, trailColor, numParticles, special) { + super(x, y, color, vx, vy, ffuse, radius, length, explodeSpeed, duration, trailColor, numParticles); + this.special = special; //additional parameter used by emit() + this.emitting = false; //turns on when fuse reaches 0 + this.counter = 0; //incremented by emit() + } + //called each iteration after fuse reaches 0, calls explode() when finished + emit() { + this.explode() + } + + //altered version that calls emit() + iterate() { + if(this.emitting) { + this.emit() + } else { + this.draw(); + this.x += this.vx; //horizontal motion + this.vx = accelerateX(this.vx); //horizontal acceleration + this.y += this.vy; //vertical motion + this.vy = accelerateY(this.vy); //vertical acceleration + this.fuse -= 1; + if (this.fuse <= 0) { + this.emitting = true; + this.emit(); + } + } + + } +} +//Spiral emits a rotating series of Comets +class Spiral extends Emitter { + //this.special = spiralLoops (number of rotations) + emit() { + if(this.counter < this.numParticles) { + var vAngle = angle(this.vx, this.vy); + var pAngle = (circ * this.special * this.counter / this.numParticles) + vAngle; + var pV = calcLaunchSpeeds(this.explodeSpeed, pAngle); + worldlist.push(new Comet(this.x + this.vx, this.y + this.vy, this.color, pV[0], pV[1], this.duration, this.radius, this.length/2)); + this.counter += 1; + } else { + this.explode(); + } + } +} +//Dahlia emits concentric circle(s) of Stardust +class Dahlia extends Emitter { + emit() { + if (this.counter >= this.special) { // this.special = emitterRings + this.explode(); + } else { + var increment = circ / this.numParticles; + var offset = this.counter * circ / (2 * this.numParticles); + for (var p = 0; p < this.numParticles; p++) { + var pAngle = p * increment + offset; + var pV = calcLaunchSpeeds(this.explodeSpeed, pAngle); + worldlist.push(new Stardust(this.x, this.y, this.color, pV[0], pV[1], this.duration, this.radius/2, this.radius)) + } + } + this.counter += 1; + } +} +//Peony emits alternating concentric circle(s) of Particles +class Peony extends Emitter { + emit() { + if (this.counter >= this.special * 2) { // this.special = emitterRings + this.explode(); + } else if (this.counter % 2 == 0) { + var increment = circ / this.numParticles; + var offset = 0.5 * this.counter/2; + for (var p = 0; p < this.numParticles; p++) { + var pAngle = (p + offset) * increment; + var pV = calcLaunchSpeeds(this.explodeSpeed, pAngle); + worldlist.push(new Particle(this.x, this.y, this.color, pV[0], pV[1], this.duration, this.radius, this.radius/2)) + } + } + this.counter += 1; + } +} + +// BUILDING FIREWORKS +// color functions +// choose firework color from color list(s) +const chooseColor = function() { + // create list to hold colors + var optionList = []; + // add colorlists to options if they are selected + if (properties.reds) { + optionList = optionList.concat(reds); + } + if (properties.oranges) { + optionList = optionList.concat(oranges); + } + if (properties.yellows) { + optionList = optionList.concat(yellows); + } + if (properties.greens) { + optionList = optionList.concat(greens); + } + if (properties.blues) { + optionList = optionList.concat(blues); + } + if (properties.purples) { + optionList = optionList.concat(purples); + } + if (properties.pinks) { + optionList = optionList.concat(pinks); + } + if (properties.browns) { + optionList = optionList.concat(browns); + } + // randomly choose color from optionList + var len = optionList.length; + if (len == 0) { + return "ffffff"; //white is the default color + } else { + var randomIndex = Math.floor(Math.random() * len); + return optionList[randomIndex]; + } +} +// randomly select trail color +const chooseTrailColor = function() { + var len = trailColors.length; + var randomIndex = Math.floor(Math.random() * len); + return trailColors[randomIndex]; +} +//reduces the opacity of a color if the timer is low enough +const translucent = function(baseColor, timer) { + if (timer > 1) { + var transparency = 1; //fully opaque + } else if (timer == 1) { + var transparency = 0.5; //50% transparency + } else { + var transparency = 0; //fully transparent + } + //convert "#xxxxxx" string into rgba() format which can specify opacity + var redHex = baseColor.slice(1,3); + var greenHex = baseColor.slice(3,5); + var blueHex = baseColor.slice(5,); + var red = parseInt(redHex, 16); + var green = parseInt(greenHex, 16); + var blue = parseInt(blueHex, 16); + return "rgba("+red+","+green+","+blue+","+transparency+")"; +} + +//angular functions +//calculate x and y launch speeds based on angle (uses radians) +const calcLaunchSpeeds = function(launchSpeed, launchAngle) { + var v0y = launchSpeed * Math.cos(launchAngle); + var v0x = launchSpeed * Math.sin(launchAngle); + if (properties.xyFlip) { + return [v0y, -1 * v0x]; + } else { + return [v0x, -1 * v0y]; + } +} +//calculates angle from x and y velocities (returns radians) +const angle = function(x, y) { + if (Math.abs(x) < 0.001) { + if (y > 0) { + return circ/4; + } else { + return -circ/4; + } + } else if (Math.abs(y) < 0.01) { + if (x > 0) { + return 0; + } else { + return circ/2; + } + } else { + if (y < 0) { + return -Math.atan(x/y)-circ/4; + } else { + if (x > 0) { + return -Math.atan(x/y)+circ/4; + } else { + return Math.atan(y/x)+circ/2; + } + } + } +} +//converts degrees to radians +const degToRad = function(deg) { + return deg * Math.PI / 180; +} +//converts radians to degrees +const radToDeg = function(rad) { + return rad * 180 / Math.PI; +} + +//choose shape +//returns that type of object (built with the current set of user-defined parameters) +const createShape = function(x, y, color, v0x, v0y) { + //make list of (strings representing) active shapes + var shapeList = []; + var options = ["stardust", "comet", "ring", "bouquet", "spiral", "peony", "dahlia", "crossette"]; + var numOptions = options.length; + for (var i = 0; i < numOptions; i++) { + var shape = options[i]; + if (properties[shape]) { + shapeList.push(shape); + } + } + + if (shapeList.length == 0) { //if none are selected, make a default Particle + return new Particle(x, y, color, v0x, v0y, properties.fuse, properties.radius); + } else { + //get the current parameters + var sIndex = Math.floor(Math.random() * shapeList.length); + var fShape = shapeList[sIndex]; + var fuse = properties.fuse; + var radius = properties.radius; + var length = properties.length; + var trailColor = chooseTrailColor(); + var duration = properties.duration; + var explodeSpeed = properties.explodeSpeed; + var ringParticles = properties.ringParticles; + var bouquetParticles = properties.bouquetParticles; + var spiralParticles = properties.spiralParticles; + var crossetteBursts = properties.crossetteBursts; + var spiralLoops = properties.spiralLoops; + var emitterRings = properties.emitterRings; + //return the chosen shape + switch (fShape) { + case "comet": + return new Comet(x, y, color, v0x, v0y, fuse, radius, length/2); + case "stardust": + return new Stardust(x, y, color, v0x, v0y, fuse, radius, length); + case "ring": + return new Ring(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, ringParticles); + case "bouquet": + return new Bouquet(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, bouquetParticles); + case "spiral": + return new Spiral(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, spiralParticles, spiralLoops); + case "peony": + return new Peony(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, ringParticles, emitterRings); + case "dahlia": + return new Dahlia(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, ringParticles, emitterRings); + case "crossette": + return new Crossette(x, y, color, v0x, v0y, fuse, radius, length, explodeSpeed, duration, trailColor, ringParticles, crossetteBursts); + default: + return new Particle(x, y, color, v0x, v0y, fuse, radius); + } + } +} + +//drawing functions +//draw dot on canvas +const drawDot = function(xCenter, yCenter, color, radius) { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.arc(xCenter, yCenter, radius, 0, circ); + ctx.fill(); +} +//draw teardrop shape on canvas +const drawTeardrop = function(xCenter, yCenter, color, radius, vx, vy, trailLen) { + ctx.fillStyle = color; + ctx.beginPath(); + if (vx == 0 && vy == 0) { //if speed = 0, just draw a circle + ctx.arc(xCenter, yCenter, radius, 0, circ) + } else { //if moving, angle of teardrop depends on direction of motion + var vAngle = angle(vx, vy); + var tailX = xCenter - Math.cos(vAngle)*radius*trailLen; + var tailY = yCenter - Math.sin(vAngle)*radius*trailLen; + ctx.arc(xCenter, yCenter, radius, vAngle - circ/4, vAngle + circ/4); + ctx.lineTo(tailX, tailY); + } + ctx.fill(); +} + +// LAUNCHER +// listener that launches a firework when the canvas is clicked +const launcher = canvas.addEventListener("click", function(e) { + //get x and y positions of click + var boundary = canvas.getBoundingClientRect() + var xPos = e.clientX - boundary.left; + var yPos = e.clientY - boundary.top; + //pick color from active lists + var color = chooseColor(); + //calculate initial x and y speeds from launchSpeed and launchAngle + var v0xy = calcLaunchSpeeds(properties.launchSpeed, degToRad(properties.launchAngle)); + //create new object and add to worldlist + worldlist.push(createShape(xPos, yPos, color, v0xy[0], v0xy[1])); +}) + +//ANIMATION +//update all the elments in worldlist +//called by window.setInterval() +const updateWorld = function() { + var toRemove = []; //fireworks to be removed are placed here + + //clear the canvas + ctx.clearRect(0,0,canvas.width,canvas.height); + + //iterate through worldlist to render each firework + var worldlength = worldlist.length; + for (var i = 0; i < worldlength; i++) { + var f = worldlist[i]; + + f.iterate(); + + if (outofBounds(f.x, f.y) || f.exploded) { + toRemove.push(f); + } + } + + //remove each object in toRemove + for (var k = 0; k < toRemove.length; k++) { + removeMe(toRemove[k]); + } +} + +//acceleration functions +//horizontal acceleration +const accelerateX = function(vx) { + if (properties.xyFlip) { + return gravitate(vx); + } else { //normal + return windchange(vx); + } +} +//vertical acceleration +const accelerateY = function(vy) { + if (properties.xyFlip) { + return windchange(vy); + } else { //normal + return gravitate(vy); + } +} +//calculate acceleration from wind +const windchange = function(v) { + if (properties.xyFlip) { + if (properties.airResistance) { + return accelerateTo(v, -properties.windspeed); + } else { //no air resistance + return accelerateUntil(v, -properties.windspeed); + } + } else { + if (properties.airResistance) { + return accelerateTo(v, properties.windspeed); + } else { //no air resistance + return accelerateUntil(v, properties.windspeed); + } + } +} +//calculate acceleration due to gravity +const gravitate = function(v) { + if(properties.xyFlip) { + if (properties.airResistance) { + var terminal = sqroot(2 * -properties.gravity); //an approximation of terminal velocity + return accelerateTo(v, terminal); + } else { //no air resistance + return v - properties.gravity; + } + } else { //normal, no xyFlip + if (properties.airResistance) { + var terminal = sqroot(2 * properties.gravity); //an approximation of terminal velocity + return accelerateTo(v, terminal); + } else { //no air resistance + return v + properties.gravity; + } + } +} +//acceleration function with a terminal velocity +const accelerateTo = function(v, target) { + var diff = target - v; + var change = sqroot(diff); + return v + change; +} +//similar to accelerateTo, but won't slow down fireworks faster than the target speed +const accelerateUntil = function(v, target) { + if ((Math.abs(v) >= Math.abs(target) && (v * target) >= 0)) { + return v; + } else { + return accelerateTo(v, target); + } +} +//helper function for square roots, returns negative root if num is negative +const sqroot = function(num) { + if (num < 0) { + return -1 * Math.sqrt(Math.abs(num)); + } else { + return Math.sqrt(num); + } +} + +//remove a firework from the worldlist +const removeMe = function(firework) { + var myIndex = worldlist.indexOf(firework); + if (myIndex > -1) { + worldlist.splice(myIndex, 1); + } +} + +//returns true if firework is too far outside the canvas +//(sometimes fireworks "fall" back in if they are close enough) +const outofBounds = function(x, y) { + var border = 50; //how many px beyond the canvas it can be before getting removed + var bounds = canvas.getBoundingClientRect(); + var xMax = bounds.width + border; + var yMax = bounds.height + border; + var xMin = 0 - border; + var yMin = 0 - border; + var isOutside = false; + if ((x < xMin) || (x > xMax) || (y < yMin) || (y > yMax)) { + isOutside = true; + } + return isOutside; +} + +//expand submenus in index page help section +const expand = function(button, section) { + var thisButton = document.getElementById(button); + var thisSection = document.getElementById(section); + thisSection.classList.remove("hidden"); + thisButton.classList.add("hidden"); +} + +const collapse = function(button, section) { + var thatButton = document.getElementById(button); + var thisSection = document.getElementById(section); + thatButton.classList.remove("hidden"); + thisSection.classList.add("hidden"); +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 00000000..cd3dcc2e --- /dev/null +++ b/public/style.css @@ -0,0 +1,79 @@ +/* used to hide elements */ +.hidden { + display: none; +} + +/* make the whole background light grey */ +html { + background-color: lightgrey; +} + +/* vertical flexbox layout for body */ +body { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + font-family: sans-serif; +} + +/* make the canvas background black */ +canvas { + background-color:black; +} + +/* Google font for important text */ +h1, legend, button, b { + font-family: 'Walter Turncoat'; +} + +/* blue rounded buttons */ +button { + color: white; + background-color: #0067fa; + border-radius: 12px; +} + +/* horizontal flexbox layout for form, arranged fieldsets in a row */ +form { + display: flex; + flex-flow: row wrap; + align-items: top; + justify-content: center; +} + +/* vertical flexbox, arranges inputs/checkboxes into columns */ +fieldset { + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: flex-end; + text-align: left; + background-image: linear-gradient(lightgray, lightgray, #b1cffa) +} + +/* blue text color for labels */ +label { + color: #02429e; +} + +/* put the "help" button at the top right corner */ +#helpButton { + position: absolute; + top: 0px; + right: 0px; +} + +/* border and light blue background for the instructions/help */ +#helpSection { + border: 1px solid #0067fa; + padding: 5px; + margin-bottom: 5px; + background-color: #b1cffa; +} + +/* border and brighter blue background for the help subsections */ +.boxDiv { + border: 1px solid #0067fa; + background-color: #74acfc; +} diff --git a/server.js b/server.js new file mode 100644 index 00000000..36e00622 --- /dev/null +++ b/server.js @@ -0,0 +1,81 @@ +// create the express server +const express = require("express"); +const bodyParser = require("body-parser"); +const app = express(); + +// make all the files in 'public' available +app.use(express.static("public")); + +//parse json +app.use(bodyParser.json()); + +// load the index page on request +app.get("/", (request, response) => { + response.sendFile(__dirname + "/index.html"); +}); +app.get("/index.html", (request, response) => { + response.sendFile(__dirname + "/index.html"); +}) +app.get("/index", (request, response) => { + response.sendFile(__dirname + "/index.html"); +}) + +//load the canvas page on request +app.get("/canvas.html", (request, response) => { + response.sendFile(__dirname + "/canvas.html"); +}) +app.get("/canvas", (request, response) => { + response.sendFile(__dirname + "/canvas.html"); +}) + +//store properties on server +var properties = { + "reds": true, + "oranges": true, + "yellows": true, + "greens": true, + "blues": true, + "purples": true, + "pinks": true, + "browns": true, + "stardust": true, + "comet": true, + "ring": true, + "bouquet": true, + "spiral": true, + "peony": true, + "dahlia": true, + "crossette": true, + "airResistance": false, //factor air resistance into equations of motion? + "windspeed": 0, //horizontal (x-axis) wind speed + "gravity": 0, //vertical (y-axis) acceleration due to gravity + "xyFlip": false, //swap the x and y axes + "launchSpeed": 5, //speed of fireworks when launched + "launchAngle": 0, //angle of fireworks when launched (relative to y-axis) + "fuse": 10, //number of 100ms-cycles before firework explodes + "radius": 4, //radius of particles (in px) + "length": 6, //length of trails (in px) + "duration": 10, //number of 100ms-cycles after explosion before particles disappear + "explodeSpeed": 10, //initial speed of particles after explosion + "ringParticles": 7, //number of particles per ring (used in Rings, Crossettes, Peonies, and Dahlias) + "bouquetParticles": 4, //number of particles in Bouquet + "spiralParticles": 11, //number of particles in Spiral + "crossetteBursts": 4, //number of particles that each post-explosion Crossette particle splits INTO + "spiralLoops": 2, //number of rotations in Spiral explosion (positive = clockwise, negative = counterclockwise) + "emitterRings": 3 //number of rings in Peony and Dahlia explosions +}; + +//fetch properties from server +app.get("/properties", (request, response) => { + response.json(properties); +}) + +//update a property when changed by the user +app.post("/update", (request, response) => { + properties[request.body.propName] = request.body.newSetting; +}) + +// listen for requests +const listener = app.listen(process.env.PORT, () => { + console.log("Your app is listening on port " + listener.address().port); +}); diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml new file mode 100644 index 00000000..64625d4e --- /dev/null +++ b/shrinkwrap.yaml @@ -0,0 +1,396 @@ +dependencies: + express: 4.17.1 +packages: + /accepts/1.3.7: + dependencies: + mime-types: 2.1.25 + negotiator: 0.6.2 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + /array-flatten/1.1.1: + dev: false + resolution: + integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + /body-parser/1.19.0: + dependencies: + bytes: 3.1.0 + content-type: 1.0.4 + debug: 2.6.9 + depd: 1.1.2 + http-errors: 1.7.2 + iconv-lite: 0.4.24 + on-finished: 2.3.0 + qs: 6.7.0 + raw-body: 2.4.0 + type-is: 1.6.18 + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + /bytes/3.1.0: + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + /content-disposition/0.5.3: + dependencies: + safe-buffer: 5.1.2 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + /content-type/1.0.4: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + /cookie-signature/1.0.6: + dev: false + resolution: + integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + /cookie/0.4.0: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + /debug/2.6.9: + dependencies: + ms: 2.0.0 + dev: false + resolution: + integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + /depd/1.1.2: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + /destroy/1.0.4: + dev: false + resolution: + integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + /ee-first/1.1.1: + dev: false + resolution: + integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + /encodeurl/1.0.2: + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + /escape-html/1.0.3: + dev: false + resolution: + integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + /etag/1.8.1: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + /express/4.17.1: + dependencies: + accepts: 1.3.7 + array-flatten: 1.1.1 + body-parser: 1.19.0 + content-disposition: 0.5.3 + content-type: 1.0.4 + cookie: 0.4.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 1.1.2 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.3.0 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.5 + qs: 6.7.0 + range-parser: 1.2.1 + safe-buffer: 5.1.2 + send: 0.17.1 + serve-static: 1.14.1 + setprototypeof: 1.1.1 + statuses: 1.5.0 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + dev: false + engines: + node: '>= 0.10.0' + resolution: + integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + /finalhandler/1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + /forwarded/0.1.2: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + /fresh/0.5.2: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + /http-errors/1.7.2: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + /http-errors/1.7.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + /iconv-lite/0.4.24: + dependencies: + safer-buffer: 2.1.2 + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + /inherits/2.0.3: + dev: false + resolution: + integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + /inherits/2.0.4: + dev: false + resolution: + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + /ipaddr.js/1.9.0: + dev: false + engines: + node: '>= 0.10' + resolution: + integrity: sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + /media-typer/0.3.0: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + /merge-descriptors/1.0.1: + dev: false + resolution: + integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + /methods/1.1.2: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + /mime-db/1.42.0: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + /mime-types/2.1.25: + dependencies: + mime-db: 1.42.0 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== + /mime/1.6.0: + dev: false + engines: + node: '>=4' + hasBin: true + resolution: + integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + /ms/2.0.0: + dev: false + resolution: + integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + /ms/2.1.1: + dev: false + resolution: + integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + /negotiator/0.6.2: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + /on-finished/2.3.0: + dependencies: + ee-first: 1.1.1 + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + /parseurl/1.3.3: + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + /path-to-regexp/0.1.7: + dev: false + resolution: + integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + /proxy-addr/2.0.5: + dependencies: + forwarded: 0.1.2 + ipaddr.js: 1.9.0 + dev: false + engines: + node: '>= 0.10' + resolution: + integrity: sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + /qs/6.7.0: + dev: false + engines: + node: '>=0.6' + resolution: + integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + /range-parser/1.2.1: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + /raw-body/2.4.0: + dependencies: + bytes: 3.1.0 + http-errors: 1.7.2 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + /safe-buffer/5.1.2: + dev: false + resolution: + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + /safer-buffer/2.1.2: + dev: false + resolution: + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + /send/0.17.1: + dependencies: + debug: 2.6.9 + depd: 1.1.2 + destroy: 1.0.4 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 1.7.3 + mime: 1.6.0 + ms: 2.1.1 + on-finished: 2.3.0 + range-parser: 1.2.1 + statuses: 1.5.0 + dev: false + engines: + node: '>= 0.8.0' + resolution: + integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + /serve-static/1.14.1: + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.17.1 + dev: false + engines: + node: '>= 0.8.0' + resolution: + integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + /setprototypeof/1.1.1: + dev: false + resolution: + integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + /statuses/1.5.0: + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + /toidentifier/1.0.0: + dev: false + engines: + node: '>=0.6' + resolution: + integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + /type-is/1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.25 + dev: false + engines: + node: '>= 0.6' + resolution: + integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + /unpipe/1.0.0: + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + /utils-merge/1.0.1: + dev: false + engines: + node: '>= 0.4.0' + resolution: + integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + /vary/1.1.2: + dev: false + engines: + node: '>= 0.8' + resolution: + integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +registry: 'https://registry.npmjs.org/' +shrinkwrapMinorVersion: 9 +shrinkwrapVersion: 3 +specifiers: + express: ^4.17.1