aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/js-small-apps/color-cycle
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-20 16:11:50 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-20 16:15:08 +0100
commit73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch)
treec8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps/color-cycle
parentb01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff)
chore!: restructure repo
I separated public files from the config/dev files. It improves repo readability. I also moved dotenv helper to public/inc directory and extract the Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/js-small-apps/color-cycle')
-rw-r--r--public/projects/js-small-apps/color-cycle/README.md13
-rw-r--r--public/projects/js-small-apps/color-cycle/app.js208
-rw-r--r--public/projects/js-small-apps/color-cycle/index.html117
-rw-r--r--public/projects/js-small-apps/color-cycle/style.css132
4 files changed, 470 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/color-cycle/README.md b/public/projects/js-small-apps/color-cycle/README.md
new file mode 100644
index 0000000..e9d5970
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/README.md
@@ -0,0 +1,13 @@
+# Color Cycle
+
+An app to cycle a color value through incremental changes. In other words: play with hexadecimal colors!
+
+You can find more details about the implementation here: https://github.com/florinpop17/app-ideas/blob/master/Projects/1-Beginner/Color-Cycle-App.md
+
+## Preview
+
+You can see a live preview here: https://demo.armandphilippot.com/#color-cycle
+
+## License
+
+This project is open-source and available under the [MIT License](../LICENSE).
diff --git a/public/projects/js-small-apps/color-cycle/app.js b/public/projects/js-small-apps/color-cycle/app.js
new file mode 100644
index 0000000..2ed8e7f
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/app.js
@@ -0,0 +1,208 @@
+let isRunning = false;
+let intervalId;
+
+/**
+ * Check if the provided value is a valid hexadecimal.
+ * @param {String} value - Two hexadecimal symbols.
+ * @returns {Boolean} True if value is a valid hexadecimal ; false otherwise.
+ */
+function isValidHex(value) {
+ const hexSymbols = "0123456789ABCDEF";
+ const hexArray = value.split("");
+ if (hexArray.length !== 2) return false;
+ if (!hexSymbols.includes(hexArray[0].toUpperCase())) return false;
+ if (!hexSymbols.includes(hexArray[1].toUpperCase())) return false;
+ return true;
+}
+
+/**
+ * Print a message to notify user.
+ * @param {String} msg A message to print.
+ */
+function notify(msg) {
+ const body = document.querySelector(".body");
+ const notification = document.createElement("div");
+ notification.classList.add("notification");
+ notification.textContent = msg;
+ body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.remove();
+ }, 3000);
+}
+
+/**
+ * Check if colors are set and are valid hexadecimal.
+ * @param {String} red - Hexadecimal string for red color.
+ * @param {String} green - Hexadecimal string for green color.
+ * @param {String} blue - Hexadecimal string for blue color.
+ * @returns {Boolean} True if colors are ready; false otherwise.
+ */
+function areColorsReady(red, green, blue) {
+ if (!red || !green || !blue) return false;
+
+ if (!isValidHex(red)) {
+ notify("Red is not hexadecimal.");
+ return false;
+ }
+
+ if (!isValidHex(green)) {
+ notify("Green is not hexadecimal.");
+ return false;
+ }
+
+ if (!isValidHex(blue)) {
+ notify("Blue is not hexadecimal.");
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Update the preview color with the provided color.
+ * @param {String} color - Hexadecimal string with a leading hash (CSS format).
+ */
+function updatePreviewColor(color) {
+ const preview = document.querySelector(".preview");
+ preview.style.backgroundColor = color;
+}
+
+/**
+ * Initialize the preview with user settings.
+ * @param {Object} ui - The different element corresponding to the user interface.
+ */
+function setPreview(ui) {
+ const red = ui.colors.red.value;
+ const green = ui.colors.green.value;
+ const blue = ui.colors.blue.value;
+
+ if (areColorsReady(red, green, blue)) {
+ const currentColor = `#${red}${green}${blue}`;
+ updatePreviewColor(currentColor);
+ }
+}
+
+/**
+ * Generate a new color.
+ * @param {Integer} color - An integer corresponding to a RGB value.
+ * @param {Integer} increment - Add this value to color argument.
+ */
+function* getColor(color, increment) {
+ let nextColor = color;
+ yield nextColor;
+
+ while (true) {
+ if (nextColor + increment > 255) {
+ nextColor = nextColor - 255 + increment;
+ } else {
+ nextColor = nextColor + increment;
+ }
+
+ yield nextColor;
+ }
+}
+
+/**
+ * Convert an RGB color to hexadecimal format.
+ * @param {Integer} red - An integer representing red color with RGB format.
+ * @param {Integer} green - An integer representing green color with RGB format.
+ * @param {Integer} blue - A value representing blue color with RGB format.
+ * @returns {String} The color in hexadecimal format with a leading hash.
+ */
+function getNexHexColor(red, green, blue) {
+ return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}`;
+}
+
+/**
+ * Disable or enable inputs.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function toggleInputs(ui) {
+ ui.colors.red.disabled = isRunning;
+ ui.colors.green.disabled = isRunning;
+ ui.colors.blue.disabled = isRunning;
+ ui.increments.red.disabled = isRunning;
+ ui.increments.green.disabled = isRunning;
+ ui.increments.blue.disabled = isRunning;
+ ui.interval.disabled = isRunning;
+}
+
+/**
+ * Start or stop the preview.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function start(ui) {
+ const red = ui.colors.red.value;
+ const green = ui.colors.green.value;
+ const blue = ui.colors.blue.value;
+
+ if (areColorsReady(red, green, blue)) {
+ isRunning = !isRunning;
+ ui.button.textContent = isRunning ? "Stop" : "Start";
+ } else {
+ notify("Colors are not correctly set.");
+ }
+
+ const redIncrement = Number(ui.increments.red.value);
+ const greenIncrement = Number(ui.increments.green.value);
+ const blueIncrement = Number(ui.increments.blue.value);
+ const redGenerator = getColor(parseInt(red, 16), redIncrement);
+ const greenGenerator = getColor(parseInt(green, 16), greenIncrement);
+ const blueGenerator = getColor(parseInt(blue, 16), blueIncrement);
+ const timing = ui.interval.value;
+
+ if (isRunning) {
+ toggleInputs(ui);
+ intervalId = setInterval(() => {
+ const nextRed = redGenerator.next().value;
+ const nextGreen = greenGenerator.next().value;
+ const nextBlue = blueGenerator.next().value;
+ const newColor = getNexHexColor(nextRed, nextGreen, nextBlue);
+ updatePreviewColor(newColor);
+ }, timing);
+ } else {
+ toggleInputs(ui);
+ clearInterval(intervalId);
+ }
+}
+
+/**
+ * Listen form, buttons and inputs.
+ * @param {Object} ui - The HTMLElements corresponding to user interface.
+ */
+function listen(ui) {
+ ui.form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ });
+ ui.button.addEventListener("click", () => start(ui));
+ ui.colors.red.addEventListener("change", () => setPreview(ui));
+ ui.colors.green.addEventListener("change", () => setPreview(ui));
+ ui.colors.blue.addEventListener("change", () => setPreview(ui));
+}
+
+/**
+ * Initialize the app.
+ */
+function init() {
+ const ui = {
+ button: document.querySelector(".btn"),
+ form: document.querySelector(".form"),
+ colors: {
+ red: document.getElementById("color-red"),
+ green: document.getElementById("color-green"),
+ blue: document.getElementById("color-blue"),
+ },
+ increments: {
+ red: document.getElementById("increment-red"),
+ green: document.getElementById("increment-green"),
+ blue: document.getElementById("increment-blue"),
+ },
+ interval: document.getElementById("time-interval"),
+ };
+
+ setPreview(ui);
+ listen(ui);
+}
+
+init();
diff --git a/public/projects/js-small-apps/color-cycle/index.html b/public/projects/js-small-apps/color-cycle/index.html
new file mode 100644
index 0000000..bb99787
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/index.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Color Cycle</title>
+ <link rel="stylesheet" href="style.css" />
+ </head>
+ <body class="body">
+ <header class="header">
+ <h1 class="branding">Color Cycle</h1>
+ </header>
+ <main class="main">
+ <div class="color-cycle">
+ <div class="color-cycle__preview">
+ <div class="preview"></div>
+ <button class="btn" type="button">Start</button>
+ </div>
+ <form action="" class="color-cycle__settings form">
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Hex Color</legend>
+ <div class="form__item">
+ <label for="color-red" class="form__label">Red</label
+ ><input
+ type="text"
+ name="color-red"
+ id="color-red"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ <div class="form__item">
+ <label for="color-green" class="form__label">Green</label
+ ><input
+ type="text"
+ name="color-green"
+ id="color-green"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ <div class="form__item">
+ <label for="color-blue" class="form__label">Blue</label
+ ><input
+ type="text"
+ name="color-blue"
+ id="color-blue"
+ class="form__input"
+ size="2"
+ value="FF"
+ />
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Increment</legend>
+ <div class="form__item">
+ <label for="increment-red" class="form__label">Red</label
+ ><input
+ type="number"
+ name="increment-red"
+ id="increment-red"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ <div class="form__item">
+ <label for="increment-green" class="form__label">Green</label
+ ><input
+ type="number"
+ name="increment-green"
+ id="increment-green"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ <div class="form__item">
+ <label for="increment-blue" class="form__label">Blue</label
+ ><input
+ type="number"
+ name="increment-blue"
+ id="increment-blue"
+ class="form__input"
+ size="2"
+ value="0"
+ />
+ </div>
+ </fieldset>
+ <fieldset class="form__fieldset">
+ <legend class="form__legend">Time</legend>
+ <div class="form__item">
+ <label for="time-interval" class="form__label"
+ >Interval (ms)</label
+ >
+ <input
+ type="range"
+ name="time-interval"
+ id="time-interval"
+ min="1"
+ max="1000"
+ value="250"
+ />
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ </main>
+ <footer class="footer">
+ <p class="copyright">Color Cycle. MIT 2021. Armand Philippot.</p>
+ </footer>
+ <script src="app.js"></script>
+ </body>
+</html>
diff --git a/public/projects/js-small-apps/color-cycle/style.css b/public/projects/js-small-apps/color-cycle/style.css
new file mode 100644
index 0000000..62e0e47
--- /dev/null
+++ b/public/projects/js-small-apps/color-cycle/style.css
@@ -0,0 +1,132 @@
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: #fff;
+ color: #000;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+ Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-size: 16px;
+ line-height: 1.618;
+ display: flex;
+ flex-flow: column nowrap;
+ min-height: 100vh;
+}
+
+.main {
+ flex: 1;
+ margin: 3rem;
+ position: relative;
+}
+
+.header,
+.main,
+.footer {
+ width: min(calc(100vw - 2rem), 80ch);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.header,
+.footer {
+ padding: 1rem 0;
+}
+
+.branding {
+ color: hsl(219, 64%, 35%);
+ text-align: center;
+}
+
+.copyright {
+ font-size: 0.9rem;
+ text-align: center;
+}
+
+.color-cycle {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 2rem;
+ align-items: center;
+}
+
+.color-cycle__preview,
+.color-cycle__settings {
+ flex: 1 1 min(calc(100vw - 2rem), calc(80ch / 2 - 1rem));
+}
+
+.preview {
+ border: 1px solid #ccc;
+ height: 20rem;
+ margin-bottom: 2rem;
+}
+
+.form__fieldset {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 1rem;
+ margin: 1rem 0;
+ padding: 0.8rem 1rem 1.2rem;
+}
+
+.form__legend {
+ color: hsl(219, 64%, 35%);
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ padding: 0 0.8rem;
+}
+
+.form__label {
+ display: block;
+ cursor: pointer;
+}
+
+.form__input {
+ padding: 0.2rem 0.5rem;
+}
+
+.btn,
+.form__input {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.btn {
+ background: #fff;
+ border: 2px solid hsl(219, 64%, 35%);
+ border-radius: 5px;
+ color: hsl(219, 64%, 35%);
+ font-weight: 600;
+ cursor: pointer;
+ display: block;
+ margin: auto;
+ padding: 0.5rem 1rem;
+}
+
+.btn:hover,
+.btn:focus {
+ background: hsl(219, 64%, 35%);
+ color: #fff;
+}
+
+.btn:active {
+ background: hsl(219, 64%, 25%);
+ color: #fff;
+}
+
+.notification {
+ background: #fff;
+ border: 2px solid hsl(0, 64%, 35%);
+ color: hsl(0, 64%, 35%);
+ font-weight: 600;
+ padding: 1rem;
+ position: absolute;
+ right: 1rem;
+ bottom: 1rem;
+}