Rang slider

Rang slider developed using HTML, JavaScript, and CSS. No, the title doesn’t contain a typo. Because if you dare touch this slider, it’ll unleash its fury!.

See the Pen Rage Slider by Jon Kantner (@jkantner) on CodePen.

Created on March 11, 2020 Updated on March 12, 2020. A Pen by Jon Kantner on CodePen. License.

<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>
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 + .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" />
Categories:
W3TWEAKS
Latest posts by W3TWEAKS (see all)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *