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

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



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

    Демо.





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


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


    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.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 26

      +2
      Решил я однажды реализовать гибкий способ стилизации подчеркивания ссылок


      Firefox и IE уже не браузеры? Идея конечно хороша, но я считаю, что если делать, то сразу добросовестно.
        0
        +1
          +1
          Исправил. Должно быть теперь кросс-браузерно.
          Единственное, теперь в разных браузерах разное смещение. Над этим предстоит подумать.
            0
            Вот теперь здорово :)
            Но слева примеры не работают в FF и IE. Сам текст нормально.
            Жаль еще в IE текст искажается.
              0
              Достаточно подкорректировать background-position. В скором времени придумаю, как решить проблему качественно.
                0
                Все, понял. background-position следует делать в em, а не в px, тогда всё будет ок.
            +1
            Хорошее решение. Плюсую. Кстати обсуждали уже регуляцию высоты подчеркивания тут, и про то как делать то-же самое руками есть тут и тут.

            Но Ваш вариант лучше.
              0
              А Вы не пробовали делать с помощью :before?
                0
                Пробовал. Не получилось. :before у инлайн-элементов не может в точности повторить прямоугольники, образуемые строками.
                  +1
                  А .svg в замену .png не пробовали?
                    0
                    Думал над этим. По-идее, гибкости будет ещё больше. Вызывает смущение потенциальная некроссбраузерность.
                    Вообще хочу дописать photoshopr, чтобы конвертить любой вектор из фотошопа сразу в background data-uri SVG, но это время.
                      0
                      Судя по CanIUse можно без страха применять svg.
                0
                Firefox 21 — к сожалению, не совсем работает:
                Image #1819033, 211.1 KB
                (большая версия по клику)
                  0
                  Зато тот самый text-decoration, что по стандарту должен быть, как раз таки работает, но с префиксом "-moz-".
                  0
                  Возмжоно меня сочтут недостаточно просвещенным, но я хочу сказать Вам отдельное спасибо за текст саги о пешеходах! Спасибо, Вы «сделали мой день!»
                    0
                    Эмм… Чего то я не понял аналогии про пешеходов. Вы имели ввиду, что вы про такое не знали и для вас это не велосипед, поэтому это сага о пешеходах? :) Или вы про какую ту другую статью автора? :)
                      0
                      Смотрим картинку в посте.
                        0
                        Увидел. :) А то я тут придумал уже. :)

                        Спасибо!
                      0
                      Более «полная» версия :) «Золотой теленок», Ильф и Петров
                      +1
                      Может, мелочь, но при зуме страницы [Ctrl <+/->] пропадают часть линий (Chrome 25)
                        0
                        И на маке беда. Очень мелкое подчёркивание, если увеличить шрифт — пропадает.
                          0
                          Попробуйте UPD2.
                            0
                            Вах, теперь работает! Но теперь непонятно, чем отличается «Смещенное подчеркивание» от «Простое подчеркивание».
                              0
                              Да, теперь offset надо реализовывать через саму генерацию png… Как-нибудь реализую, если понадобится.
                        –1
                        Изменишь шрифт — и эти волнистые линии сразу пропадут и придётся редактировать стили. Не, в реальности такое решение неприменимо.
                          0
                          background-position:0 bottom, и всё хорошо.

                        Only users with full accounts can post comments. Log in, please.