No, the title doesnβt contain a typo. Because if you dare touch this slider, itβll unleash its fury!
A Pen by Jon Kantner on CodePen.
| <form> | |
| <div class="rage" id="rageslider1"> | |
| <input class="rage__input" type="range" name="rage" value="0" min="0" max="20"> | |
| <div class="rage__track"></div> | |
| <canvas class="rage__flame-area"></canvas> | |
| <div class="rage__face"> | |
| <div class="rage__face-eye"></div> | |
| <div class="rage__face-eye"></div> | |
| <div class="rage__face-mouth"></div> | |
| <div class="rage__value">0</div> | |
| </div> | |
| </div> | |
| </form> |
No, the title doesnβt contain a typo. Because if you dare touch this slider, itβll unleash its fury!
A Pen by Jon Kantner on CodePen.
| window.addEventListener("DOMContentLoaded",() => { | |
| let rageslider1 = new RageSlider({id: "rageslider1"}); | |
| }); | |
| class RageSlider { | |
| constructor(args) { | |
| let el = document.querySelector(`#${args.id}`), | |
| isMobile = "ontouchstart" in document.documentElement, | |
| clientDownEvent = isMobile ? "touchstart" : "mousedown", | |
| clientUpEvent = isMobile ? "touchend" : "mouseup"; | |
| this.track = el.querySelector(".rage__input"); | |
| this.flameArea = el.querySelector(".rage__flame-area"); | |
| this.flameAreaContext = this.flameArea.getContext("2d"); | |
| this.flameAreaScale = 2; | |
| this.face = el.querySelector(".rage__face"); | |
| this.value = el.querySelector(".rage__value"); | |
| this.kbdActiveClass = "rage__input--active"; | |
| this.isBurning = false; | |
| this.maxParticles = 32; | |
| this.particle = () => ({ | |
| x: this.randomX(), | |
| y: this.flameArea.height / this.flameAreaScale - this.getHandleHeight() / 2, | |
| rStart: this.getHandleHeight() / 3, | |
| speed: this.getHandleHeight() / 10 | |
| }); | |
| this.particles = []; | |
| // assign listeners | |
| window.addEventListener("resize",() => { | |
| this.adjustCanvas(); | |
| }); | |
| this.track.addEventListener(clientDownEvent,e => { | |
| if (e.which !== 3) { | |
| this.isBurning = true; | |
| this.startFlame(); | |
| } | |
| }); | |
| this.track.addEventListener(clientUpEvent,() => { | |
| this.isBurning = false; | |
| }); | |
| this.track.addEventListener("keydown",e => { | |
| let kc = e.keyCode; | |
| if (kc >= 37 && kc <= 40) { | |
| this.isBurning = true; | |
| this.startFlame(); | |
| this.track.classList.add(this.kbdActiveClass); | |
| } | |
| }); | |
| this.track.addEventListener("keyup",() => { | |
| this.isBurning = false; | |
| this.track.classList.remove(this.kbdActiveClass); | |
| }); | |
| this.track.addEventListener("input",() => { | |
| this.updateFacePos(); | |
| }); | |
| // initiate | |
| this.adjustCanvas(); | |
| this.updateFacePos(); | |
| } | |
| adjustCanvas() { | |
| let S = this.flameAreaScale, | |
| OW = this.flameArea.offsetWidth, | |
| OH = this.flameArea.offsetHeight; | |
| // use natural canvas dimensions affected by ems | |
| this.flameArea.width = OW * S; | |
| this.flameArea.height = OH * S; | |
| this.flameArea.style.width = OW + "px"; | |
| this.flameArea.style.height = OH + "px"; | |
| this.flameAreaContext.scale(S,S); | |
| } | |
| getHandleHeight() { | |
| let CS = window.getComputedStyle(this.face), | |
| height = CS.getPropertyValue("height"), | |
| heightNoPx = parseFloat(height.split("px")[0]); | |
| return heightNoPx; | |
| } | |
| getRangePercent() { | |
| let max = this.track.max, | |
| min = this.track.min, | |
| relativeValue = this.track.value - min, | |
| ticks = max - min, | |
| percent = relativeValue / ticks; | |
| return percent; | |
| } | |
| randomX() { | |
| let handleSize = this.getHandleHeight(), | |
| handleRad = handleSize/2, | |
| // get the current handle position | |
| xLimit = this.flameArea.width / this.flameAreaScale - handleSize, | |
| placeX = handleRad + (this.getRangePercent() * xLimit), | |
| // randomly adjust between the handle center and edge | |
| flip = Math.random() < 0.5 ? -1 : 1, | |
| displace = Math.random() * (handleRad - handleSize/3) * flip, | |
| x = placeX + displace; | |
| return x; | |
| } | |
| startFlame() { | |
| if (!this.particles.length) | |
| this.updateFlame(); | |
| } | |
| updateFlame() { | |
| let c = this.flameAreaContext, | |
| S = this.flameAreaScale, | |
| W = this.flameArea.width / S, | |
| H = this.flameArea.height / S, | |
| faceCenter = H - this.getHandleHeight()/2; | |
| c.clearRect(0,0,W,H); | |
| // supply particles | |
| if (this.particles.length < this.maxParticles && this.isBurning) | |
| this.particles.push(this.particle()); | |
| // particle ascension | |
| for (let p of this.particles) { | |
| let faceCenterToTopPct = p.y / faceCenter, | |
| pRadius = p.rStart * faceCenterToTopPct; | |
| p.y -= p.speed; | |
| if (p.y <= 0) { | |
| // particles shouldnβt regenerate if the user stops interacting | |
| if (this.isBurning) { | |
| p.x = this.randomX(); | |
| p.y = faceCenter; | |
| } else { | |
| this.particles.shift(); | |
| } | |
| } else { | |
| // draw the particle | |
| let hue = 50 * faceCenterToTopPct; | |
| c.fillStyle = `hsl(${hue},90%,50%)`; | |
| c.beginPath(); | |
| c.arc(p.x,p.y,pRadius,0,2*Math.PI); | |
| c.fill(); | |
| c.closePath(); | |
| } | |
| } | |
| requestAnimationFrame(() => { | |
| if (this.particles.length) | |
| this.updateFlame(); | |
| }); | |
| } | |
| updateFacePos() { | |
| let percent = this.getRangePercent(), | |
| left = percent * 100, | |
| emAdjust = percent * 1.5; | |
| this.face.style.left = `calc(${left}% - ${emAdjust}em)`; | |
| this.value.innerHTML = this.track.value; | |
| } | |
| } |
| * { | |
| border: 0; | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| :root { | |
| --bg: #d8d8d8; | |
| --fg: #171717; | |
| --fgT: #17171700; | |
| --rageLight: #f13d17; | |
| --rageDark: #962417; | |
| --track: #969696; | |
| --animDur: 0.2s; | |
| --transDur: 0.1s; | |
| font-size: calc(32px + (48 - 32)*(100vw - 320px)/(2560 - 320)); | |
| } | |
| body, input { | |
| color: var(--fg); | |
| font: 1em/1.5 "Oswald", sans-serif; | |
| } | |
| body { | |
| background: var(--bg); | |
| display: flex; | |
| height: 100vh; | |
| } | |
| form { | |
| margin: auto; | |
| width: 8.5em; | |
| } | |
| .rage { | |
| position: relative; | |
| } | |
| .rage__input, .rage__track, .rage__flame-area { | |
| width: 100%; | |
| } | |
| .rage__input { | |
| background: transparent; | |
| display: block; | |
| outline: transparent; | |
| margin: 2.25em 0; | |
| height: 0.75em; | |
| -webkit-appearance: none; | |
| -moz-appearance: none; | |
| appearance: none; | |
| } | |
| .rage__input::-webkit-slider-thumb { | |
| background: transparent; | |
| border: 0; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| width: 1.5em; | |
| height: 1.5em; | |
| -webkit-appearance: none; | |
| appearance: none; | |
| } | |
| .rage__input::-moz-range-thumb { | |
| background: transparent; | |
| border: 0; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| width: 1.5em; | |
| height: 1.5em; | |
| } | |
| .rage__input::-moz-focus-outer { | |
| border: 0; | |
| } | |
| /* .rage__input--active is for keyboard interaction */ | |
| .rage__input:active + .rage__track, | |
| .rage__input--active + .rage__track { | |
| background: var(--rageDark); | |
| } | |
| .rage__input:active ~ .rage__face, | |
| .rage__input--active ~ .rage__face, | |
| .rage__input:active ~ .rage__face:after, | |
| .rage__input--active ~ .rage__face:after { | |
| background: var(--rageLight); | |
| } | |
| .rage__input:active ~ .rage__face:before, | |
| .rage__input--active ~ .rage__face:before { | |
| animation: pulse var(--animDur) var(--transDur) linear infinite; | |
| transform: scale(1); | |
| } | |
| .rage__input:active ~ .rage__face:after, | |
| .rage__input--active ~ .rage__face:after { | |
| transform: scaleY(1); | |
| } | |
| .rage__input:active ~ .rage__face .rage__face-mouth, | |
| .rage__input--active ~ .rage__face .rage__face-mouth { | |
| transform: scaleY(-1); | |
| } | |
| .rage__track, .rage__flame-area, .rage__face, .rage__face:before, .rage__face:after, .rage__value { | |
| position: absolute; | |
| } | |
| .rage__track, .rage__flame-area, .rage__face { | |
| left: 0; | |
| } | |
| .rage__track, .rage__face { | |
| transition: background var(--transDur) linear; | |
| } | |
| .rage__track, .rage__face:before, .rage__face:after { | |
| content: ""; | |
| display: block; | |
| } | |
| .rage__track { | |
| background: var(--track); | |
| border-radius: 0.75em; | |
| top: 0; | |
| height: 0.75em; | |
| z-index: -3; | |
| } | |
| .rage__flame-area { | |
| bottom: -0.375em; | |
| width: 100%; | |
| height: 3em; | |
| z-index: -2; | |
| } | |
| .rage__face, .rage__face:before { | |
| border-radius: 50%; | |
| } | |
| .rage__face { | |
| background: #fff; | |
| box-shadow: 0 0 0 0.1em #0003 inset; | |
| display: flex; | |
| justify-content: center; | |
| align-content: center; | |
| flex-wrap: wrap; | |
| top: -0.375em; | |
| width: 1.5em; | |
| height: 1.5em; | |
| will-change: transform; | |
| z-index: -1; | |
| } | |
| .rage__face:before, .rage__face-mouth { | |
| transition: transform var(--transDur) linear; | |
| } | |
| .rage__face:before { | |
| background-image: | |
| radial-gradient(100% 100% at 50% 0,var(--fgT) 16%,var(--fg) 18% 31%,var(--fgT) 33%), | |
| radial-gradient(100% 100% at 100% 50%,var(--fgT) 16%,var(--fg) 18% 31%,var(--fgT) 33%), | |
| radial-gradient(100% 100% at 50% 100%,var(--fgT) 16%,var(--fg) 18% 31%,var(--fgT) 33%), | |
| radial-gradient(100% 100% at 0 50%,var(--fgT) 16%,var(--fg) 18% 31%,var(--fgT) 33%); | |
| top: -0.2em; | |
| right: -0.2em; | |
| width: 0.6em; | |
| height: 0.6em; | |
| transform: scale(0); | |
| } | |
| .rage__face:after { | |
| background: #f1f1f1; | |
| clip-path: polygon(0 0,100% 0,50% 100%); | |
| -webkit-clip-path: polygon(0 0,100% 0,50% 100%); | |
| top: 0.3em; | |
| left: calc(50% - 0.4em); | |
| width: 0.8em; | |
| height: 0.4em; | |
| transition: background var(--transDur) linear, transform var(--transDur) linear; | |
| transform: scaleY(0); | |
| transform-origin: 50% 0; | |
| } | |
| .rage__face-eye { | |
| background: #171717; | |
| border-radius: 50%; | |
| margin: 0 0.125em 0.2em; | |
| width: 0.2em; | |
| height: 0.4em; | |
| } | |
| .rage__face-mouth { | |
| border-radius: 0 0 50% 50% / 0 0 100% 100%; | |
| box-shadow: 0 -0.1em 0 #171717 inset; | |
| width: 0.75em; | |
| height: 0.25em; | |
| } | |
| .rage__value { | |
| top: 100%; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #242424; | |
| --fg: #f1f1f1; | |
| --fgT: #f1f1f100; | |
| --track: #575757; | |
| } | |
| } | |
| @keyframes pulse { | |
| from, to { transform: scale(1); } | |
| 50% { transform: scale(1.25); } | |
| } |
| <link href="https://fonts.googleapis.com/css?family=Oswald&display=swap&text=0123456789" rel="stylesheet" /> |