diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/js-small-apps/color-cycle | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (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.md | 13 | ||||
| -rw-r--r-- | public/projects/js-small-apps/color-cycle/app.js | 208 | ||||
| -rw-r--r-- | public/projects/js-small-apps/color-cycle/index.html | 117 | ||||
| -rw-r--r-- | public/projects/js-small-apps/color-cycle/style.css | 132 |
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; +} |
