- var dir = ['', 'X', 'Y'], n = dir.length;

pre: code
	span.token--prp transform-origin
	| :
	- for(var i = 1; i < n; i++) {
		- var cd = dir[i].toLowerCase();
		|  
		span.token--prc.trigger(id='o#{cd}')
			span.curr
				span.val 50
				| %
			span.popup.hidden
				input(type='range' id='ro#{cd}' 
							min='-25' max='125' 
							value='50')
				label(for='ro#{cd}') origin #{cd} coord
	- }
	| ;
	br
	span.token--prp transform
	| : 
	span#fnc.token--fnc.trigger(data-dir='')
		span.curr: span.val scale
		span.popup.hidden
			- for(var i = 0; i < n; i++) {
				input(type='radio' name='fnc' 
							id='fnc#{i}' 
							checked=(i?null:true))
				label(for='fnc#{i}') scale#{dir[i]}
				- if(!i) {
					input(type='checkbox' id='iso')
					label(for='iso') isotropic
				- }
			- }
	span.values
		span (
		- for(var i = 1; i < n; i++) {
			- var cd = dir[i].toLowerCase();
			|  
			span.token--no.trigger(id='f#{cd}')
				span.curr: span.val 1
				span.popup.hidden
					input(type='range' id='rf#{cd}' 
								min='-1.25' max='1.25' 
								value='1' step='.01')
					label(for='rf#{cd}')
						span #{cd} 
						| scale factor
			- if(i === 1) 
				span.punct ,
		- }
		span )

section
	.boxes
		.box#original original
		.box#scaled scaled
	.points
		- for(var i = 0; i < n; i++) {
			- var y = i*50;
			- for(var j = 0; j < n; j++) {
				- var x = j*50;
				.point(data-ox=x data-oy=y)
			- }
		- }
	.box.no-events
		.point#origin
View Compiled
$theme-main: #e18728;
$lh: 1.5;
$fs: .625em;
$ctrl-bg: rgba(#000, .9);
$ctrl-ll: #eee;
$ctrl-hl: $theme-main;
$input-lat: .5em;
$track-w: 8em;
$track-h: .25em;
$thumb-d: 1.25em;
$radio-d: 1.25em;
$box-w: 25vw;
$box-h: 20vh;
$n: 3;
$m: $n - 1;
$point-d: 1.5em;
$point-u: .25*$point-d;

@mixin track() {
	width: $track-w; height: $track-h;
	background: $ctrl-ll;
}

@mixin fill() {
	background: $ctrl-hl;
}

@mixin thumb() {
	border: none;
	width: $thumb-d; height: $thumb-d;
	border-radius: 50%;
	background: $ctrl-hl;
}

* { margin: 0; padding: 0; font: inherit }

html {
	overflow: hidden;
	min-width: 160px; height: 100vh;
	font: #{$fs}/ #{$lh} courier, monospace;
}

body {
	display: flex;
	flex-direction: column;
	height: 100vh;
	
	&:after {
		position: absolute;
		bottom: 0; left: 50%;
		transform: translate(-50%);
		font-weight: 900;
		content: 'change values in code'
	}
}

/* ======= CODE PART ======== */
pre {
	padding: .25em;
	background: $ctrl-bg;
	color: $ctrl-ll;
}

[class*='token']:not(.trigger), .curr {
	display: inline-block;
	padding: 0 .25em;
	border-radius: 3px;
}

.trigger { position: relative; }

.hl { z-index: 2; }

.curr {	
	cursor: pointer;
	
	:hover > & { background: $ctrl-hl; }
	
	.hl & {
		box-shadow: 0 0 2px 2px $ctrl-hl;
	}
}

.values {
	font-size: 0;
	> span {
		font-size: 1rem;
	}
}
[id='fnc']:not([data-dir='']) ~ .values .punct, 
[data-dir='x'] ~ .values [id='fy'], 
[data-dir='iso'] ~ .values [id='fy'], 
[data-dir='iso'] ~ .values [id='fx'] label span, 
[data-dir='y'] ~ .values [id='fx'] {
	display: none;
}

/* ------- Editing menus ------- */

.popup {
	position: absolute;
	top: 100%; left: 0;
	margin-top: $lh*.25em;
	padding: .5em;
	border-radius: 3px;
	background: $ctrl-bg;
	white-space: normal;
	
	* { display: block; }
}

.hidden {
	opacity: .001;
	pointer-events: none;
}

input {
	&[type='radio'], &[type='checkbox'] {
		position: absolute;
		opacity: .001;
		pointer-events: none;
		
		+ label {
			margin: .25em 0;
			padding: 0 .25em .25em 0;
			white-space: nowrap;
			cursor: pointer;
			
			&:before {
				box-sizing: border-box;
				display: inline-block;
				margin: 0 .5em 0 .25em;
				border: solid 2px $ctrl-ll;
				width: $radio-d; height: $radio-d;
				background: transparent content-box;
				vertical-align: middle;
				content: '';
			}
		}
		
		&:focus + label {
			outline: solid 2px $ctrl-hl;
		}
		
		&:checked + label { color: $ctrl-hl; }
	}
	
	&[type='radio'] {
		+ label:before {
			padding: 3px;
			border-radius: 50%;
		}
		
		&:checked + label:before {
			background-color: $ctrl-hl;
		}
	}
	
	&[type='checkbox'] {
		+ label {
			margin: 0 0 1em 1.5*$radio-d;
			opacity: .5;
			-webkit-filter: grayscale(1);
			filter: grayscale(1);
			pointer-events: none;
			
			[id='fnc0']:checked ~ & {
				opacity: .999;
				-webkit-filter: none;
				filter: none;
				pointer-events: auto;
			}
		}
		
		&:checked + label:before {
			font-weight: 700;
			line-height: $radio-d;
			text-align: center;
			content: '✓';
		}
	}
	
	&[type='range'] {
		border: solid 0 transparent;
		border-width: $input-lat;
		width: $track-w; height: 6*$track-h;
		background: transparent;
		cursor: pointer;
		
		&::-webkit-slider-runnable-track, 
		&::-webkit-slider-thumb, & {
			-webkit-appearance: none
		}
		
		&::-webkit-slider-runnable-track {
			@include track();
		}
		
		&::-moz-range-track {
			@include track();
		}
		
		&::-ms-track {
			color: transparent;
			@include track();
		}
		
		&::-moz-range-progress {
			@include fill();
		}
		
		&::-ms-fill-lower { 
			@include fill();
		}
		
		&::-webkit-slider-thumb {
			margin-top: .5*($track-h - $thumb-d);
			@include thumb();
		}
		
		&::-moz-range-thumb {
			@include thumb();
		}
		
		&::-ms-thumb { 
			@include thumb();
		}
		
		&:focus {
			outline: solid 2px $ctrl-hl;
		}
		
		+ label { 
			margin-top: .375em;
			text-align: center;
			white-space: nowrap;
			
			span { display: inline; }
		}
	}
}

@media(min-width: 15em) {
	$fs: .875em;
	
	html { font-size: $fs; }
	pre { padding: .25em .5em; }
}

@media(min-width: 20em) {
	$fs: 1em;
	
	html { font-size: $fs; }
	.curr { padding: 0 .5em; }
}


/* ======= CODE PART ======== */
section {
	position: relative;
	flex: 1;
	
	*, :before, :after {
		position: absolute;
		top: 50%; left: 50%;
	}
}

.box {
	margin: -.5*$box-h (-.5*$box-w);
	width: $box-w; height: $box-h;
	font: 700 1em trebuchet ms, verdana, 
		arial, sans-serif;
	text-align: center;
	text-transform: uppercase;
	user-select: none;
}

.no-events { pointer-events: none; }

[id='original'] {
	background: rgba(silver, .7);
	line-height: .5*$box-h;
}

[id='scaled'] {
	background: rgba($theme-main, .7);
	line-height: $box-h;
}

.point {
	box-sizing: border-box;
	border: solid $point-u transparent;
	padding: 2px;
	width: $point-d; height: $point-d;
	border-radius: 50%;
	opacity: .56;
	background: #d14730 content-box;
	color: #a048b9;
	cursor: pointer;
	
	&[id='origin'] {
		margin: -.5*$point-d;
		opacity: 1;
		background-color: currentColor;
		pointer-events: none;
	}
	
	&:before {
		top: auto; bottom: 100%;
		padding: 0 .25em;
		border-radius: 3px;
		transform: 
			translate(-50%, -.3*$point-d) 
			scale(0);
		opacity: .0001;
		background: #1c9edc;
		color: #000;
		white-space: nowrap;
	}
	
	@for $i from 0 to $n {
		$y: ($i - .5*$m)*$box-h/$m;
		
		@for $j from 0 to $n {
			$x: ($j - .5*$m)*$box-w/$m;
			$id: $i*$n + $j + 1;
			
			.points &:nth-child(#{$id}) {
				margin: 
					calc(#{$y} - #{.5*$point-d}) 
					calc(#{$x} - #{.5*$point-d});
				
				&:before {
					content: attr(data-ox) '% ' 
									 attr(data-oy) '%'
				}
			}
		}
	}
	
	&:hover {
		border-color: currentColor;
		opacity: .999;
		
		&:before {
			transform: 
				translate(-50%, -.3*$point-d);
			opacity: .9999;
		}
	}
}
const types = ['range', 'opt', 'bool'], 
			ilist = [
				'ox', 'oy', 
				'fnc', 'iso', 'fx', 'fy'
			], 
			tlist = [0, 0, 1, 2], 
			np = ilist.length;

var pset;

var Param = function(set, typ, id) {
	var trig, form, v, range, bval;
	
	this.select = function() {
		trig.classList.toggle('hl');
		form.classList.toggle('hidden');
	};
	
	var update = function() {
		var sgn, abv, val = range.value*1;
		
		sgn = sign(val);
		abv = abs(val);
		val = ('' + abv).substr(0, 4);
		val = sgn*val;
		
		set.update(id, val);
	};
	
	this.update = function(val, f) {
		if(typ !== 'bool') {
			if(val) // if non-zero
				val = ('' + val).replace('0.', '.');
			v.textContent = val;
			
			if(f && range) {
				range.value = val;
			}
		}
		else bval = !bval;
	};
	
	this.value = function() {
		if(typ !== 'bool') 
			return v.textContent;
		else return bval;
	};
	
	this.init = function() {
		var base = '#' + id, sel;
				
		if(typ !== 'bool') {			
			trig = $$(base);
			sel = base + ' .popup';
			form = $$(sel);
			sel = base + ' .val';
			v = $$(sel);
			
			if(typ === 'range') {
				range = $$('#r' + id);
			}
		}
		else {
			v = $$(base);
			bval = v.checked;
		}
	};
	
	this.init();
	
	if(range) {
		range.addEventListener('input', update, false);
		range.addEventListener('update', update, false);
	}
};

var trigger = function(el) {
	var n = 3;
	
	while(n--) {
		if(el && el.classList && 
			 el.classList.contains('trigger'))
			return el;
		el = (el && el.parentNode) ? 
			el.parentNode : null;
	}
	
	return null;
};

(function init() {
	const scaled = $$('#scaled'), 
				origin = $$('#origin')
		
	pset = {};
	pset.params = {};
	
	pset.init = function() {
		for(var i = 0; i < np; i++) {
			this.params[ilist[i]] = new Param(pset, types[tlist[i] || 0], ilist[i]);
		}
		
		this.active = null;
	};
	
	pset.select = function(id) {
		if(this.active !== id) {
			if(this.active)
				this.params[this.active].select();
			
			id = this.params[id] ? id : null;
			this.active = id;
			
			if(id)
				this.params[this.active].select();
		}
	};
	
	pset.update = function(id, value, f) {
		var px, py;
		
		this.params[id].update(value, f);
				
		if(id.charAt(0) === 'o') {
			px = this.params.ox.value() + '%';
			py = this.params.oy.value() + '%';
			origin.style.top = py;
			origin.style.left = px;
			scaled.style.transformOrigin = 
				[px, py].join(' ')
		}
		else {
			if(id === 'fx' && 
				 this.params.iso.value() && 
				 this.params.fnc.value() === 'scale')
				this.params.fy.update(value, 1);
			
			scaled.style.transform = 
				'scale(x, y)'
				.replace('x', this.params.fx.value())
				.replace('y', this.params.fy.value());
		}
	};
	
	pset.init();
})();

addEventListener('click', function(e) {
	var tg = e.target, trig, i, ci, 
			fa, d = ['', 'x', 'y'], tmp;
	
	if(tg.classList.contains('point')) {
		pset.update('ox', tg.dataset.ox);
		pset.update('oy', tg.dataset.oy);
	}
	
	if(tg.tagName.toLowerCase() === 'label') {
		trig = trigger(tg);
		fa = tg._attr('for');
		
		if(fa.match('fnc')) {
			i = 1*fa.replace('fnc', '');
			ci = 3 - i;
			
			if(i === 2)
				tmp = 1*pset.params.fy.value();
			
			if(i) {
				pset.update('f' + d[ci], 1, 1);
				trig.dataset.dir = d[i];
			}
			else {
				if(pset.params.iso.value()) {
					trig.dataset.dir = 'iso';
					pset.update('fy', pset.params.fx.value(), 1);
				}
				else {
					trig.dataset.dir = '';
				}
			}
			
			pset.update(trig.id, tg.textContent)
			if(tmp) pset.update('fy', tmp, 1);
		}
		if(fa === 'iso') {
			pset.update('iso');
			if(pset.params.iso.value()) {
				trig.dataset.dir = 'iso';
				pset.update('fy', pset.params.fx.value(), 1);
			}
			else {
				trig.dataset.dir = '';
			}
		}
	}
	
	tg = trigger(tg);
	if(tg) pset.select(tg.id);
	else pset.select(null);
}, false);

addEventListener('keyup', function(e) {
	var tg;
	
	if(e.keyCode === 9) {
		tg = trigger($.activeElement);
		if(tg) pset.select(tg.id);
		else pset.select(null);
	}
}, false);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://codepen.io/thebabydino/pen/wWMWqW.js