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

от автора

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

И реализовал, демо.

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

Полупрозрачное, пунктирное и точечное подчеркивания весьма просто делаются через 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), отправляя напрямую поток битов индексированных цветов.

Итог: демо, painter.less, png.js.

ссылка на оригинал статьи http://habrahabr.ru/post/181746/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *