diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9f04a71 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..04b7ea4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prj.iml b/.idea/prj.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/prj.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/images/il_570xN.3812195829_4zqo.avif b/images/il_570xN.3812195829_4zqo.avif new file mode 100644 index 0000000..897b21a Binary files /dev/null and b/images/il_570xN.3812195829_4zqo.avif differ diff --git a/images/washing-machine.svg b/images/washing-machine.svg new file mode 100644 index 0000000..e1fece2 --- /dev/null +++ b/images/washing-machine.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/images/zmm20d38gb-fr-1500x1500 (1) (1).png b/images/zmm20d38gb-fr-1500x1500 (1) (1).png new file mode 100644 index 0000000..ddaadc1 Binary files /dev/null and b/images/zmm20d38gb-fr-1500x1500 (1) (1).png differ diff --git a/images/zwf8240sb5-1500x1500.avif b/images/zwf8240sb5-1500x1500.avif new file mode 100644 index 0000000..80795fb Binary files /dev/null and b/images/zwf8240sb5-1500x1500.avif differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd628ba --- /dev/null +++ b/index.html @@ -0,0 +1,28 @@ + + + + + + Document + + + +
+
+ + +
+
+ +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..b0c022a --- /dev/null +++ b/script.js @@ -0,0 +1,193 @@ +const prizes = [ + { + text: "Microwave 10", + color: "hsl(197 30% 43%)", + reaction: "dancing", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "Microwave 15", + color: "hsl(173 58% 39%)", + reaction: "shocked", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "Microwave 20", + color: "hsl(43 74% 66%)", + reaction: "shocked", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "Microwave 25", + color: "hsl(27 87% 67%)", + reaction: "shocked", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "Microwave 30 pro", + color: "hsl(12 76% 61%)", + reaction: "dancing", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "10% Off", + color: "hsl(350 60% 52%)", + reaction: "laughing", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "10% Off", + color: "hsl(91 43% 54%)", + reaction: "laughing", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + }, + { + text: "10% Off", + color: "hsl(140 36% 74%)", + reaction: "dancing", + image: "./images/zmm20d38gb-fr-1500x1500 (1) (1).png" + } + ]; + + const wheel = document.querySelector(".deal-wheel"); + const spinner = wheel.querySelector(".spinner"); + const trigger = wheel.querySelector(".btn-spin"); + const ticker = wheel.querySelector(".ticker"); + const reaper = wheel.querySelector(".grim-reaper"); + const prizeSlice = 360 / prizes.length; + const prizeOffset = Math.floor(180 / prizes.length); + const spinClass = "is-spinning"; + const selectedClass = "selected"; + const spinnerStyles = window.getComputedStyle(spinner); + let tickerAnim; + let rotation = 0; + let currentSlice = 0; + let prizeNodes; + + const createPrizeNodes = () => { + prizes.forEach(({ text, color, reaction, image }, i) => { + const rotation = ((prizeSlice * i) * -1) - prizeOffset; + + spinner.insertAdjacentHTML( + "beforeend", + `
  • + ${text} + ${text} +
  • ` + ); + }); + }; + + const createConicGradient = () => { + spinner.setAttribute( + "style", + `background: conic-gradient( + from -90deg, + ${prizes + .map(({ color }, i) => `${color} 0 ${(100 / prizes.length) * (prizes.length - i)}%`) + .reverse() + } + );` + ); + }; + + const setupWheel = () => { + createConicGradient(); + createPrizeNodes(); + prizeNodes = wheel.querySelectorAll(".prize"); + }; + + const spinertia = (min, max) => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + const runTickerAnimation = () => { + // https://css-tricks.com/get-value-of-css-rotation-through-javascript/ + const values = spinnerStyles.transform.split("(")[1].split(")")[0].split(","); + const a = values[0]; + const b = values[1]; + let rad = Math.atan2(b, a); + + if (rad < 0) rad += (2 * Math.PI); + + const angle = Math.round(rad * (180 / Math.PI)); + const slice = Math.floor(angle / prizeSlice); + + if (currentSlice !== slice) { + ticker.style.animation = "none"; + setTimeout(() => ticker.style.animation = null, 10); + currentSlice = slice; + } + + tickerAnim = requestAnimationFrame(runTickerAnimation); + }; + + const selectPrize = () => { + const selected = Math.floor(rotation / prizeSlice); + prizeNodes[selected].classList.add(selectedClass); + reaper.dataset.reaction = prizeNodes[selected].dataset.reaction; + }; + + trigger.addEventListener("click", () => { + if (reaper.dataset.reaction !== "resting") { + reaper.dataset.reaction = "resting"; + } + + trigger.disabled = true; + rotation = Math.floor(Math.random() * 360 + spinertia(2000, 5000)); + prizeNodes.forEach((prize) => prize.classList.remove(selectedClass)); + wheel.classList.add(spinClass); + spinner.style.setProperty("--rotate", rotation); + ticker.style.animation = "none"; + runTickerAnimation(); + }); + +spinner.addEventListener("transitionend", () => { + cancelAnimationFrame(tickerAnim); + trigger.disabled = false; + trigger.focus(); + rotation %= 360; + selectPrize(); + wheel.classList.remove(spinClass); + spinner.style.setProperty("--rotate", rotation); +}); + +setupWheel(); + +// Power +const button = document.querySelector(".btn-spin"); +const powerRectangle = document.querySelector(".power-rectangle"); + +let holdStartTime = 0; +let intervalId = null; + +const updatePowerIndicator = () => { +const maxHoldDuration = 2000; +const holdDuration = Date.now() - holdStartTime; +const power = Math.min(100, (holdDuration / maxHoldDuration) * 100); + +powerRectangle.style.width = `${power}%`; +powerRectangle.style.backgroundColor = `rgba(0, 128, 0, ${power / 100})`; +}; + +button.addEventListener("mousedown", () => { +holdStartTime = Date.now(); +powerRectangle.style.width = '0'; +powerRectangle.style.backgroundColor = 'rgba(0, 128, 0, 0)'; + +intervalId = setInterval(updatePowerIndicator, 50); +}); + +button.addEventListener("mouseup", () => { +clearInterval(intervalId); +powerRectangle.style.width = '0'; +powerRectangle.style.backgroundColor = 'rgba(0, 128, 0, 0)'; +}); + +button.addEventListener("mouseleave", () => { +clearInterval(intervalId); +powerRectangle.style.width = '0'; +powerRectangle.style.backgroundColor = 'rgba(0, 128, 0, 0)'; +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..fc3f49d --- /dev/null +++ b/styles.css @@ -0,0 +1,226 @@ +@import url("https://fonts.googleapis.com/css2?family=Girassol&display=swap"); + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + display: grid; + place-items: center; + overflow: hidden; +} + +.deal-wheel { + --size: clamp(250px, 80vmin, 700px); + --lg-hs: 0 3%; + --lg-stop: 50%; + --lg: linear-gradient( + hsl(var(--lg-hs) 0%) 0 var(--lg-stop), + hsl(var(--lg-hs) 20%) var(--lg-stop) 100% + ); + + position: relative; + display: grid; + grid-gap: calc(var(--size) / 20); + align-items: center; + grid-template-areas: + "spinner" + "trigger"; + font-family: "Girassol", sans-serif; + font-size: calc(var(--size) / 21); + line-height: 1; + text-transform: lowercase; +} + +.deal-wheel > * { + grid-area: spinner; +} + +.deal-wheel .btn-spin { + grid-area: trigger; + justify-self: center; +} + +.spinner { + position: relative; + display: grid; + align-items: center; + grid-template-areas: "spinner"; + width: var(--size); + height: var(--size); + transform: rotate(calc(var(--rotate, 25) * 1deg)); + border-radius: 50%; + box-shadow: inset 0 0 0 calc(var(--size) / 40) hsl(0deg 0% 0% / 0.06); +} + +.spinner * { + grid-area: spinner; +} + +.prize { + position: relative; + display: flex; + align-items: center; + padding: 0 calc(var(--size) / 6) 0 calc(var(--size) / 20); + width: 50%; + height: 50%; + transform-origin: center right; + transform: rotate(var(--rotate)); + user-select: none; +} + +.cap { + --cap-size: calc(var(--size) / 4); + position: relative; + justify-self: center; + width: var(--cap-size); + height: var(--cap-size); +} + +/* Hide select dropdown from SVG import file */ +.cap select { + display: none; +} + +.cap svg { + width: 100%; +} + +.ticker { + position: relative; + left: calc(var(--size) / -15); + width: calc(var(--size) / 10); + height: calc(var(--size) / 20); + background: var(--lg); + z-index: 1; + clip-path: polygon(20% 0, 100% 50%, 20% 100%, 0% 50%); + transform-origin: center left; +} + +.btn-spin { + color: hsl(0deg 0% 100%); + background: var(--lg); + border: none; + font-family: inherit; + font-size: inherit; + line-height: inherit; + text-transform: inherit; + padding: 0.9rem 2rem 1rem; + border-radius: 0.25rem; + cursor: pointer; + transition: opacity 200ms ease-out; +} + +.btn-spin:focus { + outline-offset: 2px; +} + +.btn-spin:active { + transform: translateY(1px); +} + +.btn-spin:disabled { + cursor: progress; + opacity: 0.25; +} + +/* Spinning animation */ +.is-spinning .spinner { + transition: transform 8s cubic-bezier(0.1, -0.01, 0, 1); +} + +.is-spinning .ticker { + animation: tick 700ms cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes tick { + 40% { + transform: rotate(-12deg); + } +} + +/* Selected prize animation */ +.prize.selected .text { + color: white; + animation: selected 800ms ease; +} + +@keyframes selected { + 25% { + transform: scale(1.25); + text-shadow: 1vmin 1vmin 0 hsla(0 0% 0% / 0.1); + } + 40% { + transform: scale(0.92); + text-shadow: 0 0 0 hsla(0 0% 0% / 0.2); + } + 60% { + transform: scale(1.02); + text-shadow: 0.5vmin 0.5vmin 0 hsla(0 0% 0% / 0.1); + } + 75% { + transform: scale(0.98); + } + 85% { + transform: scale(1); + } +} + +.deal-wheel .spinner .prize img { + width:50%; +} +.hold-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 100px; + background-color: rgba(0, 0, 0, 0.5); + border-radius: 5px; + display: none; + } + + .indicator-bar { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + height: 0; + background-color: #fff; + } + + .arrow { + position: absolute; + top: -10px; + left: 50%; + transform: translateX(-50%); + border: solid transparent; + border-width: 10px; + border-bottom-color: #fff; + } + + /* Power*/ + .power-indicator { + position: relative; + width: 50%; + height: 100px; + margin-top: 10px; + background: #f0f0f0; + border: 1px solid #ccc; + overflow: hidden; + } + + .power-rectangle { + width: 0; + height: 100%; + background: rgba(0, 128, 0, 0.7); + transition: width 0.1s, background-color 0.1s; + } + \ No newline at end of file