Решил я однажды реализовать гибкий способ стилизации подчеркивания ссылок — чтобы просто делать полупрозрачные подчеркивания, регулировать паттерн в 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). Используется следующим образом:
<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 в примесь и написав интерфейс к нему, в итоге получился следующий код:
//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