Реализация стилей подчеркивания в LESS через генерацию png в data-URI

в 16:05, , рубрики: css, css3, data:uri, javascript, less, PNG, метки: , , ,

Решил я однажды реализовать гибкий способ стилизации подчеркивания ссылок — чтобы просто делать полупрозрачные подчеркивания, регулировать паттерн в dashed/dotted-border, делать волнистые подчеркивания и вообще иметь настройки CSS3 text-decoration, которые еще ни один браузер не умеет.

Реализация стилей подчеркивания в LESS через генерацию png в data URI

В результате получился генератор PNG в data-URI на LESS.

Демо.

Варианты реализации

Реализация стилей подчеркивания в LESS через генерацию png в data URIПолупрозрачное, пунктирное и точечное подчеркивания весьма просто делаются через border-bottom ☞.

Интересное начинается, когда хочется сместить линию ближе к тексту.
Можно соорудить конструкцию вида

<a class="link"><span>Some link text here</span></a>

и регулировать line-height элемента span (или a), задав ему display:inline-block, но тогда возникает проблема на многострочном тексте: inline-block становится настоящим block'ом в плане отображения бордера (иллюстрация справа).

После размышлений и экспериментов, я пришел к выводу, что самым «чистым» и удобным решением было-бы класть паттерн подчеркивания в background с высотой, равной line-height. Осталось только понять, откуда брать этот паттерн.

  • Генерировать картинку где-то на стороне и подключать её как файл — негибко и неудобно для разработки, каждое изменение будет убивать нервы.
  • Использовать генератор PNG через canvas (такой, к примеру), но это также неудобно в разработке: каждый раз генерировать data-URI на стороне.
  • Генерировать Repeating-gradient, но это весьма ненадежный способ, так как есть риск не попасть точно в пиксель линии подчеркивания, да и пунктирные подчеркивания не реализовать.

Самым логичным оставалось генерировать PNG динамически и вставлять в data-URI. Из вопроса на stackoverflow выяснилось, что один человек уже сумел генерировать GIF-картинку в один пиксель (тут), но, надо сказать, весьма прямолинейно и негибко: изменение размеров этой картинки было-бы задачей, равносильной переписыванию всего кода.

Гряли выходные, и я решил наконец перестать фрустрироваться грязной реализацией подчеркивания ссылок и разобраться с генерацией PNG.

PNG.js

После нескольких часов изучения спецификаций PNG, ZLIB Data Format и DEFLATE Data Format, а также примера сериреализации png и небольшого реверс-инжиниринга (тут пример генерации сырого png), был создан js-класс для работы с PNG, пригодный для распила на куски в LESS.

Класс PNG умеет генерировать несжатый PNG с индексированным цветом (indexed-color) или битмапа (truecolor with alpha). Используется следующим образом:

PNG.js usecase

<script src="png.js"></script>
<script>
var png = new PNG();
png.set({
width: w,
height: h,
chunks: {
	PLTE: plte, //palette string (sequence of colors, 3 bytes per color), e.g. "000000ffffff" ⇒ black, white
	tRNS: trns //transparency string (alpha-values according to the palette colors, 1 byte per value), e.g. "00ff" ⇒ 0, 1
},
data: data //string of color indexes (or bitmap), 1 byte per color index, e.g. "00010100" ⇒ black, white, white, black
})
result = png.toDataURL() //⇒ data:image/png;base64,iV...
</script>

Запуск JS в LESS

Как оказалось, LESS весьма гибок для запуска JS. К примеру, функции можно запускать следующим обазом:

	@test: `function(a){
		return a
	}`;
	test: `(@{test})(3)`; //test: 3

Переместив png.js в примесь и написав интерфейс к нему, в итоге получился следующий код:

painter.less

//Painting functions
@text: black;
@red: red;
@green: green;

.underline(@height: 20, @color: @text, @thickness: 1){
	@patternGen: `function(h, thick){
		var space = "", line = "";
		//make line
		for (var i = 0; i < thick; i++){
			line += "01"
		}
		//make space
		for (var i = 0; i < h - thick; i++){
			space += "00"
		}
		return space + line;
	}`;
	@pattern: `(@{patternGen})(@{height}, @{thickness})`;	
	.png(@stream: @pattern, @w: 1, @h: unit(@height), @color: @color);
}
.underline{
	.underline();
	
}
.underline.thick{
	.underline(@thickness: 2);
}
.underline.offset{
	
}
.underline.transparent{
	.underline(@color: fade(@text, 30%), @thickness: 1);
}

.waved(@height: 20, @color: @red, @thickness: 2, @width: 4){
	@patternGen: `function(h, w, thick){
		var space = "", wave = "";
		//make wave
		for (var y = 0; y < thick; y++){
			for (var x = 0; x < w; x++){
				if (x < w/2){
					if (y < thick/2) {
						wave += "00"
					} else{
						wave += "01"
					}
				} else {
					if (y < thick/2) {
						wave += "01"
					} else{
						wave += "00"
					}
				}
			}
		}
		//make space
		for (var i = 0; i < (h - thick)*w; i++){
			space += "00"
		}
		return space + wave;
	}`;
	@pattern: `(@{patternGen})(@{height}, @{width}, @{thickness})`;
	ptrn: @pattern;
	.png(@stream: @pattern, @w: unit(@width), @h: unit(@height), @color: @color);
}
.waved{
	.waved();
	
}
.waved.alt{
	.waved(@color: @green, @thickness: 2, @width: 6);
}

.dotted(@height: 20, @color: @text, @width: 3, @thickness: 1){
	@patternGen: `function(h, thick, w){
		var space = "", line = "";
		//make line
		for (var i = 0; i < thick; i++){
			for(var x = 0; x < thick; x++){
				line += "01";
			}
			for(var x = thick; x < w; x++){
				line += "00";
			}
		}
		//make space
		for (var i = 0; i < (h - thick)*w; i++){
			space += "00"
		}
		return space + line;
	}`;
	@pattern: `(@{patternGen})(@{height}, @{thickness}, @{width})`;	
	.png(@stream: @pattern, @w: unit(@width), @h: unit(@height), @color: @color);
}
.dotted{
	.dotted;
	
}
.dotted.rare{
	.dotted(@width: 6);
}
.dotted.thick{
	.dotted(@width: 6, @thickness: 2);
}
.dashed(@height: 20, @color: @text, @width: 8, @thickness: 1, @length: 4){
	@patternGen: `function(h, thick, w, l){
		var space = "", line = "";
		//make line
		for (var i = 0; i < thick; i++){
			for(var x = 0; x < l; x++){
				line += "01";
			}
			for(var x = l; x < w; x++){
				line += "00";
			}
		}
		//make space
		for (var i = 0; i < (h - thick)*w; i++){
			space += "00"
		}
		return space + line;
	}`;
	@pattern: `(@{patternGen})(@{height}, @{thickness}, @{width}, @{length})`;	
	.png(@stream: @pattern, @w: unit(@width), @h: unit(@height), @color: @color);
}
.dashed{
	.dashed;
	
}
.dashed.rare{
	.dashed(@width: 6);
}
.dashed.thick{
	.dashed(@width: 10, @thickness: 2, @length: 6);
}
.dot-dashed(@height: 20, @color: @text, @width: 10, @thickness: 1){
	@patternGen: `function(h, thick, w){
		var space = "", line = "";
		//make line
		for (var i = 0; i < thick; i++){
			for(var x = 0; x < w; x++){
				switch (true){
					case (x > w*.75):
						line += "00";
						break;
					case (x > w*.375):
						line += "01";
						break;
					case (x > w*.125):
						line += "00";
						break;
					default:
						line += "01";
				}
			}
		}
		//make space
		for (var i = 0; i < (h - thick)*w; i++){
			space += "00"
		}
		return space + line;
	}`;
	@pattern: `(@{patternGen})(@{height}, @{thickness}, @{width})`;
	.png(@stream: @pattern, @w: unit(@width), @h: unit(@height), @color: @color);
}
.dot-dashed{
	.dot-dashed;
	
}
.dot-dashed.thick{
	.dot-dashed(@width: 10, @thickness: 2);
}

.pattern(@height: 20, @color: @text, @width: 8, @thickness: 1, @length: 4, @pattern: ". -"){
	
}



//Mixin that generates PNG to background
.png(@stream: "0001", @w: 2, @h: 2, @color: black){

	@r: red(@color);
	@g: green(@color);
	@b: blue(@color);
	@hexColor: rgb(red(@color),green(@color),blue(@color));
	@PLTE: `"ffffff" + ("@{hexColor}").substr(1)`; //Make bytes palette: first-white, rest-passed color;

	@a: alpha(@color);
	@tRNS: `"ff" + (function(){ var a = Math.round(@{a} * 255).toString(16); return (a.length == 1 ? "0" + a : a) })()`;

	//png.js: https://github.com/dfcreative/graphics/blob/master/src/PNG.js
	@initPNG: `(function(){ /*...copy-pasted png.js: https://github.com/dfcreative/graphics/blob/master/src/PNG.js */)()`;

	@background: `(function(){
		var png = new PNG();
		
		png.set({
			width: @{w},
			height: @{h},
			chunks:{
				PLTE: @{PLTE},
				tRNS: @{tRNS}
			},
			data: @{stream}
		})

		return "url(" + png.toDataURL() + ")";
	})()`;

	background-image: ~"@{background}";
}

.png{
	.png();
}

Как использовать?

1. Подключить painter.less и less.js, как в демо

<link rel="stylesheet/less" type="text/css" href="painter.less" />
<script src="less.js" type="text/javascript"></script>
2. Использовать классы для span-элементов:

<span class="underline">Простое подчеркивание</span>
<span class="underline thick">Толcтое подчеркивание</span>
<span class="underline offset">Смещенное подчеркивание</span>
<span class="underline transparent">Полупрозрачное подчеркивание</span>
<span class="waved">Волнистое подчеркивание</span>
<span class="waved alt">Волнистое подчеркивание 2</span>
<span class="dotted">Точечное частое подчеркивание</span>
<span class="dotted rare">Точечное редкое подчеркивание</span>
<span class="dotted thick">Точечное толстое подчеркивание</span>
<span class="dashed">Пунктирное подчеркивание</span>
<span class="dashed thick">Пунктирное толстое подчеркивание</span>
<span class="dot-dashed">Штрих-пунктирное подчеркивание</span>

И отрегулировать позицию background:
span { background-posiion: 0 -5px; }

3. Доступные миксины:

  • .underline(@height: 20, @color: @text, @thickness: 1)
  • .waved(@height: 20, @color: @red, @thickness: 2, @width: 4)
  • .dotted(@height: 20, @color: @text, @width: 3, @thickness: 1)
  • .dashed(@height: 20, @color: @text, @width: 8, @thickness: 1, @length: 4)
  • .dot-dashed(@height: 20, @color: @text, @width: 10, @thickness: 1)

Можно также использовать миксин .png(@stream: "0001", @w: 2, @h: 2, @color: black), отправляя напрямую поток битов индексированных цветов.

Итог: демо, репозиторий на github.

Автор: Dmitry_f

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js