Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="head">
	<h1>Engine simulator</h1>
</div>

<div class="dashboard">
	<div class="meter meter--rpm meter--big-label"></div>
	<div class="meter meter--gear"><div>1</div></div>
	<div class="meter meter--speed"></div>
</div>

<div class="help">
	Accelerate / brake : up/down arrow<br>
	Gears +/- : left/right arrows<br>
</div>

<button class="btn-volume active">♩</button>

<div class="loader active">Loading...</div>


              
            
!

CSS

              
                $meter-bg-color: white;

$ring-size: 3.5%;
$ring-color: #333;

$grad-font-size: 1.25rem;
$grad-font-size-big: 2rem;
$grad-color: #555;

/*
$meter-bg-color: rgb(32,32,32);
$ring-color: #eee;
$grad-color: #9affff;
*/

$tick-color: $grad-color;

$needle-width: 5px;
$needle-side-width: 3px;
$needle-height: 140px;
$needle-color: #e30;
$needle-shading: 5%;

$needle-axle-bg-color: $ring-color;
$needle-axle-color: #eee;
$needle-axle-size: 15%;
$needle-axle-size-big: 37.5%;

$label-color: $grad-color;


@mixin shadow ($level: 2, $inset: false) {

   @if $inset {

		 @if $level == 1 {
			 box-shadow: inset 0 1px 3px rgba(0,0,0,0.12), inset 0 1px 2px rgba(0,0,0,0.24);
		 } @else if $level == 2 {
			 box-shadow: inset 0 3px 6px rgba(0,0,0,0.16), inset 0 3px 6px rgba(0,0,0,0.23);
		 } @else if $level == 3 {
			 box-shadow: inset 0 10px 20px rgba(0,0,0,0.19), inset 0 6px 6px rgba(0,0,0,0.23);
		 } @else if $level == 4 {
			 box-shadow: inset 0 14px 28px rgba(0,0,0,0.25), inset 0 10px 10px rgba(0,0,0,0.22);
		 } @else if $level == 5 {
			 box-shadow: inset 0 19px 38px rgba(0,0,0,0.30), inset 0 15px 12px rgba(0,0,0,0.22);
		 }

	}	@else {
		@if $level == 1 {
			box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
		} @else if $level == 2 {
			box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
		} @else if $level == 3 {
			box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
		} @else if $level == 4 {
			box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
		} @else if $level == 5 {
			box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
		}
	}
}




//@import url(https://fonts.googleapis.com/css?family=Coda:400,800);
@import url(https://fonts.googleapis.com/css?family=Coda:400|Roboto+Mono:400,400italic);

* {
	box-sizing: border-box;
//	transition: all 0.2s ease-in-out;
}

.dashboard {
	display: inline-block;
	padding: 2rem;
//	background: #ddd;
	font-family: 'Coda', cursive;
}

.meter {
	display: inline-block;
	position: relative;
	width: 20rem;
	height: 20rem;
	background-color: $meter-bg-color;
	border-radius: 50%;
//	border: 4px solid black;
	margin: 0 0.5rem;
	@include shadow(2, true);
	
	&:before {
		content: "";
		display: block;
		position: absolute;
		width: 100% + $ring-size * 2;
		left: -$ring-size;
		top: -$ring-size;
		bottom: -$ring-size;
		border-radius: 50%;
		background: $ring-color;
		z-index: -1;
		@include shadow(3);
	}
}

.label {
	position: absolute;
	left: 50%;
	transform: translate3d(-50%, -50%, 0);
	text-align: center;
	color: $label-color;

	&.label-value {
		color: $needle-axle-color;
	}
	
	span {
		text-transform: uppercase;
	}
}

.label-unit {
	top: 82.5%;
	
	div {
		
		font-size: 1rem;

		.meter--big-label & {
			font-size: 0;
		}
	}
}

.label-value {
	top: 50%;
	line-height: 1.25;
	
	div, span {
		font-size: 0;

		.meter--big-label & {
			font-size: 2.5rem;
		}
	}
	
	div {
		font-family: 'Roboto Mono', monospace;
		font-style: italic;
	}
	
	span {
		.meter--big-label & {
			font-size: 1rem;
		}
	}
	
	span {
		display: block;
		position: absolute;
		left: 50%;
		transform: translate3d(-50%, 0, 0);
	}
}

.needle,
.needle-axle {
	
}

.needle {
	position: absolute;
	top: 50%;
	left: 50%;
	display: block;
	width: $needle-width;
	height: $needle-height;
	transform: translate3d(-50%, 0, 0);
	transform-origin: 50% 0;
	background-color: $needle-color;
	@include shadow(1);

	&:before,
	&:after {
		display: block;
		position: absolute;
		content: "";
		width: 0;
		height: 0;
		border-style: solid;
	}
	
	&:before {
		left: -$needle-side-width;
		border-width: 0 $needle-side-width $needle-height 0;
		border-color: transparent lighten($needle-color, $needle-shading * 2) transparent transparent;
	}
	
	&:after {
		right: -$needle-side-width;
		border-width: $needle-height $needle-side-width 0 0;
		border-color: darken($needle-color, $needle-shading) transparent transparent transparent;
	}
}

.needle-axle {
	position: absolute;
	top: 50%;
	left: 50%;
	display: block;
	width: $needle-axle-size;
	height: $needle-axle-size;
	transform: translate3d(-50%, -50%, 0);
	border-radius: 50%;
	background-color: $needle-axle-bg-color;
	color: $needle-axle-color;
	@include shadow(2);
	cursor: pointer;
	
	&:hover {
		background-color: darken($needle-axle-bg-color, 20%);
	}
	
	.meter--big-label & {
		width: $needle-axle-size-big;
		height: $needle-axle-size-big;
	}
}

.grad {
	position: absolute;
	display: inline-block;
	font-size: $grad-font-size;
	line-height: 1;
	color: $grad-color;
	transform: translate3d(-50%, -50%, 0);
	font-weight: bold;
	
	&.redzone {
		color: $needle-color;
	}

	.meter--rpm & {
		font-size: $grad-font-size-big;
	}
}

.grad-tick,
.grad-tick--half,
.grad-tick--quarter {

	&.redzone {
		background-color: $needle-color;
	}
}


.grad-tick {
	position: absolute;
	display: block;
	width: 4px;
	height: 0.75em;
	transform-origin: 50% 0;
	background-color: $tick-color;
}

.grad-tick--half {
	width: 2px;
	height: 0.5em;
}

.grad-tick--quarter {
	width: 2px;
	height: 0.25em;
}


// Gear-meter

.meter--gear {
	width: 5rem;
	height: 5rem;
	margin: 0;
	margin-left: -2rem;
	margin-right: -2rem;
	@include shadow(2, true);

	&:before {
		width: 100% + $ring-size * 4 * 2;
		height: 100% + $ring-size * 4 * 2;
		left: -$ring-size * 4;
		top: -$ring-size * 4;
		bottom: -$ring-size * 4;
	}

	div {
		font-size: 2.5rem;
		text-align: center;
		line-height: 5rem;
		font-family: 'Roboto Mono', monospace;
		font-style: italic;
		font-weight: bold;
		color: $grad-color;

		&.redzone {
			color: $needle-color;
		}
	}

}


// UI

.loader {
	display: none;
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate3d(-50%, -50%, 0);
	background: tomato;
	color: white;
	padding: 2rem;
	text-transform: uppercase;
	
	&.active {
		display: inline-block;
	}
}

.btn-volume {
	position: fixed;
	top: 0.5rem;
	right: 0.5rem;

	width: 2.5rem;
	height: 2.53rem;
	line-height: 2rem;
	border-radius: 50%;
	background: transparent;
	color: gray;
	border: 4px solid;
	font-size: 1.5rem;
	font-weight: bold;
	
	&.active {
		color: tomato;
		border-color: tomato;
	}
}

// DEMO STYLES

body {
	text-align: center;
	// Carbon fiber FTW
/*	background:
		linear-gradient(27deg, #151515 5px, transparent 5px) 0 5px,
		linear-gradient(207deg, #151515 5px, transparent 5px) 10px 0px,
		linear-gradient(27deg, #222 5px, transparent 5px) 0px 10px,
		linear-gradient(207deg, #222 5px, transparent 5px) 10px 5px,
		linear-gradient(90deg, #1b1b1b 10px, transparent 10px),
		linear-gradient(#1d1d1d 25%, #1a1a1a 25%, #1a1a1a 50%, transparent 50%, transparent 75%, #242424 75%, #242424);
		background-color: #131313;
		background-size: 20px 20px;
*/
}

.head,
.help {
	padding: 1rem;
	color: #888;
	text-align: center;
	font-style: italic;
}

.help {
	border-top: 1px solid;
}

.head {
	border-bottom: 1px solid;
}

h1 {
	margin: 0;
//	font-family: 'Roboto Mono', monospace;
	font-weight: normal;
	text-transform: uppercase;
}
              
            
!

JS

              
                let motorSamplePath = 'https://freesound.org/data/previews/127/127980_2335231-lq.ogg';

// METER

let Meter = function Meter($elm, config) {
	
	// DOM
	let $needle, $value;
	
	// Others
	
	let steps = (config.valueMax - config.valueMin) / config.valueStep,
			angleStep = (config.angleMax - config.angleMin) / steps;
	
	let margin = 10; // in %
	let angle = 0; // in degrees
	
	let value2angle = function(value) {
		let angle = ((value / (config.valueMax - config.valueMin)) * (config.angleMax - config.angleMin) + config.angleMin);

		return angle;
	};
	
	this.setValue = function(v) {
		$needle.style.transform = "translate3d(-50%, 0, 0) rotate(" + Math.round(value2angle(v)) + "deg)";
		$value.innerHTML = config.needleFormat(v);
	};
	
	let switchLabel = function(e) {
		e.target.closest(".meter").classList.toggle('meter--big-label');
	};
	
	let makeElement = function(parent, className, innerHtml, style) {

		let	e = document.createElement('div');
		e.className = className;

		if (innerHtml) {
			e.innerHTML = innerHtml;
		}

		if (style) {
			for (var prop in style) {
				e.style[prop] = style[prop];
			}
		}

		parent.appendChild(e);
		
		return e;
	};

	// Label unit
	makeElement($elm, "label label-unit", config.valueUnit);

	for (let n=0; n < steps+1; n++) {
		let value = config.valueMin + n * config.valueStep;
		angle = config.angleMin + n * angleStep;
		
		// Graduation numbers
		
		// Red zone
		let redzoneClass = "";
		if (value > config.valueRed) {
			redzoneClass = " redzone";
		}
		
		makeElement($elm, "grad grad--" + n + redzoneClass, config.labelFormat(value), {
			left: (50 - (50 - margin) * Math.sin(angle * (Math.PI / 180))) + "%",
			top: (50 + (50 - margin) * Math.cos(angle * (Math.PI / 180))) + "%"
		});	

		// Tick
		makeElement($elm, "grad-tick grad-tick--" + n + redzoneClass, "", {
			left: (50 - 50 * Math.sin(angle * (Math.PI / 180))) + "%",
			top: (50 + 50 * Math.cos(angle * (Math.PI / 180))) + "%",
			transform: "translate3d(-50%, 0, 0) rotate(" + (angle + 180) + "deg)"
		});

		// Half ticks
		angle += angleStep / 2;
		
		if (angle < config.angleMax) {
			makeElement($elm, "grad-tick grad-tick--half grad-tick--" + n + redzoneClass, "", {
				left: (50 - 50 * Math.sin(angle * (Math.PI / 180))) + "%",
				top: (50 + 50 * Math.cos(angle * (Math.PI / 180))) + "%",
				transform: "translate3d(-50%, 0, 0) rotate(" + (angle + 180) + "deg)"
			});
		}

		// Quarter ticks
		angle += angleStep / 4;
		
		if (angle < config.angleMax) {
			makeElement($elm, "grad-tick grad-tick--quarter grad-tick--" + n + redzoneClass, "", {
				left: (50 - 50 * Math.sin(angle * (Math.PI / 180))) + "%",
				top: (50 + 50 * Math.cos(angle * (Math.PI / 180))) + "%",
				transform: "translate3d(-50%, 0, 0) rotate(" + (angle + 180) + "deg)"
			});
		}

		angle -= angleStep / 2;
		
		if (angle < config.angleMax) {
			makeElement($elm, "grad-tick grad-tick--quarter grad-tick--" + n + redzoneClass, "", {
				left: (50 - 50 * Math.sin(angle * (Math.PI / 180))) + "%",
				top: (50 + 50 * Math.cos(angle * (Math.PI / 180))) + "%",
				transform: "translate3d(-50%, 0, 0) rotate(" + (angle + 180) + "deg)"
			});
		}
	}
	
	// NEEDLE
	
	angle = value2angle(config.value);
	
	$needle = makeElement($elm, "needle", "", {
		transform: "translate3d(-50%, 0, 0) rotate(" + angle + "deg)"
	});
	
	let $axle = makeElement($elm, "needle-axle").addEventListener("click", switchLabel);
	makeElement($elm, "label label-value", "<div>" + config.labelFormat(config.value) + "</div>" + "<span>" + config.labelUnit + "</span>").addEventListener("click", switchLabel);
	
	$value = $elm.querySelector(".label-value div");
};


// DOM LOADED FIESTA

document.addEventListener("DOMContentLoaded",	function() {	
	
	let rpmMeter = new Meter(document.querySelector(".meter--rpm"), {
		value: 6.3,
		valueMin: 0,
		valueMax: 8000,
		valueStep: 1000,
		valueUnit: "<div>RPM</div><span>x1000</span>",
		angleMin: 30,
		angleMax: 330,
		labelUnit: "RPM",
		labelFormat: function(v) { return Math.round(v / 1000); },
		needleFormat: function(v) { return Math.round(v / 100) * 100; },
		valueRed: 6500
	});

	let speedMeter = new Meter(document.querySelector(".meter--speed"), {
		value: 203,
		valueMin: 0,
		valueMax: 220,
		valueStep: 20,
		valueUnit: "<span>Speed</span><div>Km/h</div>",
		angleMin: 30,
		angleMax: 330,
		labelUnit: "Km/h",
		labelFormat: function(v) { return Math.round(v); },
		needleFormat: function(v) { return Math.round(v); }
	});

	let gearMeter = document.querySelector('.meter--gear div');
	
	// USER INPUTS
	
	document.onkeydown = keyDown;
	document.onkeyup = keyUp;

	function keyDown(e) {

		e = e || window.event;

		if (e.keyCode == '38') { 			// up arrow
			isAccelerating = true;
		}
		else if (e.keyCode == '40') { // down arrow
			isBraking = true;
		}
		else if (e.keyCode == '37') { // left arrow
		}
		else if (e.keyCode == '39') { // right arrow
		}

	}	

	function keyUp(e) {

		e = e || window.event;

		if (e.keyCode == '38') {			// up arrow
			isAccelerating = false;
		}
		else if (e.keyCode == '40') { // down arrow
			isBraking = false;
		}
		else if (e.keyCode == '37') { // left arrow
			gearDown();
		}
		else if (e.keyCode == '39') { // right arrow
			gearUp();
		}

	}	
	
	function gearUp() {
		if (gear < gears.length - 1) {
			gear++;
			gearMeter.innerHTML = gear;
		}
	}

	function gearDown() {
		if (gear > 1) {
			gear--;
			gearMeter.innerHTML = gear;
		}
	}

	
	// VEHICLE CONFIG

	let 
			mass = 1000,
			cx = 0.28,
			gears = [0, 0.4, 0.7, 1.0, 1.3, 1.5, 1.68],
			transmitionRatio = 0.17,
			transmitionLoss = 0.15,
			wheelDiameter = 0.5,
			brakeTorqueMax = 300,

			gear = 1,
			speed = 10,	// in km/h
			overallRatio,
			wheelRpm,
			wheelTorque,
			brakeTorque,
			resistance,
			acceleration;
	
	// MOTOR CONFIG
	
	let 
			rpmIdle = 1200,
			rpmMax = 8000,
			rpmRedzone = 6500,
			torqueMin = 20, // in m.kg
			torqueMax = 45, // in m.kg

			torque,
			rpm = 0,
			isAccelerating = false,
			isBraking = false;
	

	// Helper functions
	
	let torqueByRpm = function(rpm) {
		let torque = torqueMin + (rpm / rpmMax) * (torqueMax - torqueMin);
		return torque;
	};
	
	function kmh2ms(speed) {	// Km/h to m/s
		return speed / 3.6;
	}
	
	// Physics 101
	/* 
	 * P = C w
	 * P(hp) = C(m.kg) w(rpm) / 716
	 *
	 * F = m.a
	 * Force(newton) = mass(kg) * acceleration (m/s)
	 *
	 * a = Cr / (r.m)
	 * acceleration (m/s) = torqueWheel (m.kg) / (wheelRadius (m) * mass (kg))
	 */

	let lastTime = new Date().getTime(),
			nowTime,
			delta;
	
	
	// MAIN LOOP
	
	(function loop(){
		window.requestAnimationFrame(loop);

		// Delta time
		nowTime = new Date().getTime();
		delta = (nowTime - lastTime) / 1000; // in seconds
		lastTime = nowTime;
		
		let oldSpeed = speed,
				oldRpm = rpm;
		
		// Torque
		
		if (isAccelerating && rpm < rpmMax) {	// Gas!
			torque = torqueByRpm(rpm);

		} else {
			torque = -(rpm * rpm / 1000000);
		}
		
		if (isBraking) {	// Ooops...
			brakeTorque = brakeTorqueMax;
		} else {
			brakeTorque = 0;
		} 
		
		
		overallRatio = transmitionRatio * gears[gear];
		wheelTorque = torque / overallRatio - brakeTorque;
		
		acceleration = 20 * wheelTorque / (wheelDiameter * mass / 2);
		resistance = 0.5 * 1.2 * cx * kmh2ms(speed)^2;

		// Speed
		
		speed += (acceleration - resistance) * delta;
		
		
		if (speed < 0) { speed = 0; }
		
		wheelRpm = speed / (60 * (Math.PI * wheelDiameter / 1000));
		rpm = speed / (60 * transmitionRatio * gears[gear] * (Math.PI * wheelDiameter / 1000));

		// Idle
		if (rpm < rpmIdle) {
			rpm = oldRpm;
			speed = oldSpeed;
		}
		
		// Gear shifter
		if (rpm > rpmRedzone) {
			gearMeter.classList.add('redzone');

		} else {
			gearMeter.classList.remove('redzone');
		}
		
		// Update GUI
		
		speedMeter.setValue(speed);
		rpmMeter.setValue(rpm);

		// Update engine sound
		if (source) {
			source.playbackRate.value = rpm / 4000;
		}

		if (source2) {
			source2.playbackRate.value = speed / 500;
		}

	})();
	
	///////////////////////////////////////////////
	// WEBAUDIO
	
	// Courtesy of https://mdn.github.io/decode-audio-data/
	
	// define variables

	var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
	var source,source2,
			gainNode;
	var songLength;

	var loader = document.querySelector('.loader');
	var btnVolume = document.querySelector('.btn-volume');

	// use XHR to load an audio track, and
	// decodeAudioData to decode it and stick it in a buffer.
	// Then we put the buffer into the source

	function getData() {
		source = audioCtx.createBufferSource();
		source2 = audioCtx.createBufferSource();
		let request = new XMLHttpRequest();

		request.open('GET', motorSamplePath, true);
		request.responseType = 'arraybuffer';

		request.onload = function() {
			var audioData = request.response;

			audioCtx.decodeAudioData(audioData, function(buffer) {
				let myBuffer = buffer;	// local buffer ?
				let myBuffer2 = buffer;
//				songLength = buffer.duration; // in seconds
				source.buffer = myBuffer;
				source2.buffer = myBuffer2;

				source.loop = true;
				source2.loop = true;

				// Hacky granular engine sound!
				source.loopStart = 0.1; // Tune this
				source.loopEnd = 0.1735; // Tune this

				source2.loopStart = 0.605;
				source2.loopEnd = 0.650;
				
				source.playbackRate.value = 1;
				source2.playbackRate.value = 1;

				// Create a gain node.
				gainNode = audioCtx.createGain();
				// Connect the source to the gain node.
				source.connect(gainNode);
				source2.connect(gainNode);
				// Connect the gain node to the destination.
				gainNode.connect(audioCtx.destination);

				// Remove loader
				loader.classList.remove('active');
			},

				function(e){"Error with decoding audio data" + e.err});
		}

		request.send();
	}

	// wire up buttons

	btnVolume.onclick = function() {
		this.classList.toggle('active');

		if (this.classList.contains('active')) {
			gainNode.gain.value = 1;
		} else {
			gainNode.gain.value = 0;
		}
	}

	// Load the sample
	getData();
	// Launch loop playing
	source.start(0);
	source2.start(0);
	
	
});
              
            
!
999px

Console