<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss2full.xsl"?><?xml-stylesheet type="text/css" media="screen" href="http://feeds.feedburner.com/~d/styles/itemcontent.css"?><rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0" version="2.0">

<channel>
	<title>Сергей Чикуёнок</title>
	
	<link>http://chikuyonok.ru</link>
	<description>веб-разработчик</description>
	<pubDate>Wed, 07 Jul 2010 07:46:31 +0000</pubDate>
	<generator>http://wordpress.org/?v=2.7.1</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="self" type="application/rss+xml" href="http://feeds.feedburner.com/chikuyonok/vikk" /><feedburner:info uri="chikuyonok/vikk" /><atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" rel="hub" href="http://pubsubhubbub.appspot.com/" /><item>
		<title>От простого к сложному</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/Mb5mPmonMvY/</link>
		<comments>http://chikuyonok.ru/2010/07/simple-things/#comments</comments>
		<pubDate>Tue, 06 Jul 2010 12:24:44 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Статьи]]></category>

		<category><![CDATA[javascript]]></category>

		<category><![CDATA[интерфейс]]></category>

		<category><![CDATA[формы]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=666</guid>
		<description><![CDATA[Насколько сложной может быть форма из двух полей, отправляющая данные аяксом на сервер? С современными фреймворками вроде jQuery, код может выглядеть примерно так:

$.post('my-url.php', $('form#my-form').serialize());

Но это минимальный код; код из серии «создаём свой блог/цмс за 5 минут», которым так любят заманивать создатели всяких движков и фреймворков. А что нужно сделать, чтобы такая форма стала удобной и [...]]]></description>
			<content:encoded><![CDATA[<p>Насколько сложной может быть форма из двух полей, отправляющая данные аяксом на сервер? С современными фреймворками вроде jQuery, код может выглядеть примерно так:</p>
<pre class="brush: js">
$.post('my-url.php', $('form#my-form').serialize());
</pre>
<p>Но это минимальный код; код из серии «создаём свой блог/цмс за 5 минут», которым так любят заманивать создатели всяких движков и фреймворков. А что нужно сделать, чтобы такая форма стала удобной и понятной для конечного пользователя, то есть «человеческой»?</p>
<p>Недавно мы запустили новый скидочный проект <a href="http://www.bigbuzzy.ru">BigBuzzy</a>. Так как на рынке уже существует масса подобных проектов, единственный способ выделиться из толпы — это сделать проект очень удобным для пользователя, как с концептуальной точки зрения (не надо ждать, пока наберётся определённая группа; удобные способы оплаты), так и с технологической.</p>
<p>Одна из фич проекта — отправка купленного купона себе на телефон по СМС. Так как на месте проведения акции нужно предъявить только номер купона, нет особого смысла печатать его на бумаге, можно показать и с телефона. Увидеть форму в действии можно <s>на бигбаззи, купив 10 купонов</s> на <a href="http://media.chikuyonok.ru/coupon/">этой странице</a>. В этой форме как раз два поля: код и номер телефона. Посмотрим, что можно в ней улучшить.</p>
<h3>Алгоритм работы</h3>
<p>Прежде, чем приступать к работе, нам нужно определиться, какие функции несёт в себе эта форма. Когда пользователь оплатил купон, ему предоставляется на выбор два действия: распечатать или отправить по СМС. Чтобы отправить по СМС, нужно знать номер телефона пользователя. В принципе, можно заставить его ввести этот номер при регистрации, однако это с большой долей вероятности отпугнёт потенциальных покупателей: с чего это вдруг сайт требует с меня номер телефона? Поэтому форму будем показывать только тогда, когда пользователь нажмёт на кнопку «Отправить по СМС»: в этом случае ему будет понятно, почему требуется ввести номер телефона.</p>
<p>А что делать, если пользователь уже ввёл номер при отправке первого купона и хочет отправить второй? Логично, что он захочет отправить купон на тот же самый номер. Нужно ли в этом случае показывать форму? Решаем, что нужно. Во-первых, пользователь может захотеть отправить купон своему другу в качестве подарка или этот друг только что позвонил и попросил выручить, докупив ещё два купона на боулинг. Заставлять пользователя идти в Личный кабинет и менять там номер телефона как-то уж совсем неправильно. Во-вторых, появляющаяся форма будет своеобразным подтверждением действия (вдруг он случайно нажал на эту кнопку?). Поэтому на нажатие конпки будем запускать анимацию появления формы, а повторное нажатие на эту же кнопку отправить данные на сервер:</p>
<pre class="brush: js">
$(document.body).delegate('.coupon-button', 'click', function(/* Event */ evt) {
	var elem = $(this);
	if (elem.hasClass('coupon-button-sms')) {
		var coupon = elem.closest('.coupon');
		if (!coupon.length)
			return;

		if (!coupon.hasClass('coupon-sms-mode')) {
			// переключаем купон в режим отправки СМС
			switchSMSMode(coupon);
		} else  {
			// пытаемся отправить данные на сервер
			tryToSendSMS(coupon);
		}
	}
});
</pre>
<p>Функцию анимации появления формы с вводом номера телефона я тут не привожу, так как она довольно большая и скучная (желающие могут посмотреть исходник). Первая проблема, которая может возникнуть на этом шаге: пользователь может случайно дважды кликнуть на кнопку, что приведёт к моментальной отправке СМС, если там уже был вбит номер. Поэтому нужно поставить <em>блокировку на выполнение действия</em>. Это стандартный подход в анимированных интерфейсах, который позволяет избежать случайных нажатий или даже поломки всего интерфейса (может наложиться несколько анимаций друг на друга). Введём переменную <code>is_animating</code>, которая будет равна <code>true</code> в момент анимации. Обнулять эту переменную будем после завершения анимации через callback-функцию для <code>switchSMSMode()</code>:</p>
<pre class="brush: js">
$(document.body).delegate('.coupon-button', 'click', function(/* Event */ evt) {
	var elem = $(this),
		is_animating = false;

	if (elem.hasClass('coupon-button-sms')) {
		var coupon = elem.closest('.coupon');
		if (!coupon.length || is_animating)
			return;

		if (!coupon.hasClass('coupon-sms-mode')) {
			// переключаем купон в режим отправки СМС
			is_animating = true;
			switchSMSMode(coupon, function(){
				is_animating = false;
			});
		} else  {
			// пытаемся отправить данные на сервер
			tryToSendSMS(coupon);
		}
	}
});
</pre>
<h3>Ввод данных</h3>
<p>Форму мы показали, теперь нужно упростить пользователю ввод данных. Тут надо помнить, что существует множество моделей поведения пользователя. Кто-то быстро введёт код и переключится табом на следующее поле, кто-то будет минуту набирать три цифры и схватиться за мышку, чтобы переключиться на следующее поле, кто-то будет ожидать, что введённая последовательность из 10 цифр автоматически распределиться между полями. Нужно постараться удовлетворить максимальное количество пользователей. Поэтому будем следить за полем «код», и когда там будет 3 цифры — автоматически перебросим фокус на поле с номером:</p>
<pre class="brush: js">
$('.phone-code').keyup(function(){
	if (this.value.length == 3)
		$(this).next('.phone-num').focus();
});
</pre>
<p>Так делают многие и так делать ни в коем случае нельзя. Если пользователь допустил ошибку в коде, то попытка исправить её используя только клавиатуру превратиться в сущий ад: например, нажатие на стрелки клавиатуры всегда будет приводить к перебросу фокуса, потому что сработает событие событие keyup, а в поле уже будет 3 символа. Чтобы сделать поведение формы более естественным, нужно следить за вводом данных: перебрасывать фокус будем только если в предыдущем нажатии на кнопку (<code>last_length</code>) было меньше символов, чем в этом нажатии, количество символов равно трём и каретка находится в самом конце поля:</p>
<pre class="brush:js">
$('.phone-code').bind('keyup change', function(/* Event */ evt) {
	var field = $(evt.target),
		last_length = field.data('last_length') || 0,
		cur_length = field.val().length,
		max_length = parseInt(field.attr('maxlength')) || 3,
		selection = getSelectionRange(field[0]);

	if (cur_length > last_length &amp;&amp; cur_length == max_length &amp;&amp; selection &amp;&amp; selection.start == cur_length) {
		field.next('.phone-number').focus();
	}

	field.data('last_length', cur_length);
});

function getSelectionRange(elem) {
	if ('selectionStart' in elem) { // W3C's DOM
		return {
			start: elem.selectionStart,
			end: elem.selectionEnd
		};
	} else if (document.selection) { // IE
		elem.focus();

		var range = document.selection.createRange(),
			content = elem.value;

		if (range === null) {
			return {
				start: 0,
				end: content.length
			};
		}

		var re = elem.createTextRange();
		var rc = re.duplicate();
		re.moveToBookmark(range.getBookmark());
		rc.setEndPoint('EndToStart', re);

		return {
			start: rc.text.length,
			end: rc.text.length + range.text.length
		};
	} else {
		return null;
	}
}
</pre>
<p>Но тут возникает ещё одна проблема. Далеко не каждый пользователь догадается, что при заполнении поля с кодом фокус перекинется на следующее поле. Он может не глядя на монитор набрать 3 цифры, нажать на таб и набрать ещё 7 цифр. В этом случае наша «помощь» только помешает пользователю, потому что фокус будет где-то совершенно в другом месте. Чтобы такого не случилось, нужно временно заблокировать нажатие на таб на поле с номером телефона после того, как фокус был автоматически переброшен:</p>
<pre class="brush: js">
$('.phone-code').bind('keyup change', function(/* Event */ evt) {
	var field = $(evt.target),
		last_length = field.data('last_length') || 0,
		cur_length = field.val().length,
		max_length = parseInt(field.attr('maxlength')) || 3,
		selection = getSelectionRange(field[0]);

	if (cur_length > last_length &amp;&amp; cur_length == max_length &amp;&amp; selection &amp;&amp; selection.start == cur_length) {
		var num_field = field.next('.phone-number');

		// временно блокируем Tab на следующем поле
		num_field.data('tab_locked', true);
		setTimeout(function() {
			num_field.data('tab_locked', false);
		}, 1000);

		num_field.focus();
	}

	field.data('last_length', cur_length);
});

$('.phone-number').bind('keydown keyup keypress', function(/* Event */ evt) {
	var field = $(evt.target);
	if (field.data('tab_locked') === true &#038;&#038; evt.keyCode == 9) {
		// блокируем работу клавиши Tab
		evt.preventDefault();
	}
});
</pre>
<h3>Валидация данных</h3>
<p>Перед отправкой данных на сервер нужно сделать простую валидацию данных, чтобы убедиться, что введённые данные похожи на номер телефона. Если нет — то намекнём об этом пользователю.</p>
<p>Вводить жёсткий формат записи числовых данных вроде номера телефона или кредитной карты — первый признак полового бессилия разработчика. Для номера телефона нам нужно 10 цифр (код + сам номер), и не важно, написаны цифры слитно или разделены пробелами или дефисами. Пользователь должен написать номер в привычном ему формате, чтобы легче было проверять правильность. Поэтому при валидации удалим из полей все не числовые символы и проверим длину того, что осталось:</p>
<pre class="brush: js">
function validatePhoneNumber(coupon) {
	coupon = $(coupon);

	var re_num = /[^0-9]/g,
		phone_code = coupon.find('.phone-code'),
		phone_num = coupon.find('.phone-number'),
		is_valid_code = phone_code.val().replace(re_num, '').length == 3,
		is_valid_num = phone_num.val().replace(re_num, '').length == 7;

	if (!is_valid_code)
		blinkField(phone_code);
	if (!is_valid_num)
		blinkField(phone_num);

	return is_valid_code &#038;&#038; is_valid_num;
}
</pre>
<p>Если введённые данные не верны, то мы должны сообщить об этом пользователю. Пространство у нас очень ограничено, поэтому будем «моргать» полем, указывая, что там что-то не так. Функция добавляет и удаляет определённый класс у элемента через заданный промежуток времени, в CSS укажем этому классу красный цвет:</p>
<pre class="brush: js">
function blinkField(fld) {
	fld = $(fld);
	if (fld.data('blink_timer'))
		clearInterval(fld.data('blink_timer'));

	var blink_count = 6;

	fld.data('blink_timer', setInterval(function() {
		fld.toggleClass('coupon-field-warning');
		if (blink_count-- < 0) {
			fld.removeClass('coupon-field-warning');
			clearInterval(fld.data('blink_timer'));
		}
	}, 100));
}
</pre>
<h3>Отправка данных</h3>
<p>Данные от пользователя получили и проверили, настало время отправить их на сервер. Опять же, тут есть две потенциальные проблемы. Первая — это слишком медленный интернет. Нужно показать пользователю, что процесс запущен и скоро что-то произойдёт. Вторая проблема не так очевидна — это <em>слишком быстрый интернет</em>. Настолько быстрый, что пользователь может и не понять, а произошло ли что-то, отправились ли данные или возникла ошибка? Чтобы избавиться от этих проблем, нужно показать показать некий индикатор процесса, причём этот индикатор должен искусственно замедлить получение фидбэка от сервера. Он должен отображаться на странице некоторое время, чтобы пользователь успел понять, что запущен некий процесс.</p>
<p>Кто-то решает эту задачу так: показывает индикатор, ждёт определённое время и уже после этого отправляет запрос на сервер, а после получения ответа прячет индикатор. Так делать не правильно: в случае с медленным интернетом таймаут слишком сильно увеличивается; сам запрос мог бы запросто отработать за то время, пока показывается индикатор. Поэтому мы устроим небольшую гонку: что последним закончится выполняться — таймер индикатора или запрос на сервер — то и покажет фидбэк пользователю:</p>
<pre class="brush: js">
function sendSMSRequest(coupon) {
	coupon = $(coupon);
	var phone_code = coupon.find('.phone-code').val(),
		phone_num = coupon.find('.phone-number').val();

	coupon.addClass('coupon-preloader');
	// запускаем индикатор
	preloader.start();

	// два условия отображения фидбэка: закончился таймер и пришёл ответ от сервера
	var timer_reached = false,
		response_received = false;

	function checkStatus() {
		if (timer_reached &#038;&#038; response_received) {
			// останавливаем индикатор
			preloader.start();
			coupon.removeClass('coupon-preloader');

			// прячем форму с номером телефона
			switchSMSMode(coupon);
		}
	}

	// устраиваем гонку между таймером и запросом
	setTimeout(function() {
		timer_reached = true;
		checkStatus();
	}, 1000);

	$.get(sms_url, {
		id: coupon.attr('id'),
		action: 'send_sms',
		phone: phone_code + phone_num
	}, function(data) {
		response_received = true;
		checkStatus();
	}, 'json');
}
</pre>
<p>Вот так, двигаясь от простого к сложному, всего за несколько часов работы мы улучшили форму из целых двух полей, которую не стыдно показывать людям. Можно, конечно, улучшить ещё немного, явно сообщая пользователю о статусе отправки сообщения, но это уже другая история. Для сравнения: что было и что стало.</p>
<p><img src="http://chikuyonok.ru/u/2010/07/code-example.png" alt="code-example" title="code-example" width="451" height="1310" class="alignnone size-full wp-image-670" /></p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=666" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/Mb5mPmonMvY" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/07/simple-things/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/07/simple-things/</feedburner:origLink></item>
		<item>
		<title>Content assist для элемента textarea</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/zNnYeiA1Jwk/</link>
		<comments>http://chikuyonok.ru/2010/06/content-assist-for-textarea/#comments</comments>
		<pubDate>Mon, 14 Jun 2010 18:09:24 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Прочее]]></category>

		<category><![CDATA[content assist]]></category>

		<category><![CDATA[textarea]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=655</guid>
		<description><![CDATA[
	Очередной эксперимент в поисках идеального средства для простого и быстрого написания кода. 

&#60;textarea&#62;, да и веб-технологии в целом привлекают меня своей простотой, доступностью и возможностью сделать так, как нужно мне. Есть Zen Coding, который позволяет быстро и удобно работать с большим объёмом XHTML-данных, но для пущего удобства не хватает автодополнения кода. Поиск в интернете не [...]]]></description>
			<content:encoded><![CDATA[<p>
	<em>Очередной эксперимент в поисках идеального средства для простого и быстрого написания кода. </em>
</p>
<p><code>&lt;textarea&gt;</code>, да и веб-технологии в целом привлекают меня своей простотой, доступностью и возможностью сделать так, как нужно мне. Есть <a href="http://code.google.com/p/zen-coding/">Zen Coding</a>, который позволяет быстро и удобно работать с большим объёмом XHTML-данных, но для пущего удобства не хватает автодополнения кода. Поиск в интернете не дал никаких вменяемых результатов, поэтому решил написать свой:</p>
<p><img src="http://chikuyonok.ru/u/2010/06/content-assist.png" alt="content-assist" title="content-assist" width="493" height="134" class="alignnone size-full wp-image-656" /></p>
<p>Проверить автодополнение можно на <a href="http://media.chikuyonok.ru/content-assist/">тестовой странице</a>. Попробуйте написать несколько английских слов, начинающихся на «a» (awesome, animal, amazing).  Должно работать везде, включая IE6.</p>
<p>Само по себе автодополнение, по большому счёту, практически бесполезно: непонятно, в какой момент какие данные нужно показывать. Поэтому решение представляет из себя несколько отдельных классов, каждый из которых можно переопределить, расширить и дополнить. За основу была взята архитектура автодополнения для Eclipse IDE, поэтому ряд классов и методов называются одинаково.</p>
<ul>
<li><code>TextViewer</code> — по сути, обёртка для <code>&lt;textarea&gt;</code>. Основное предназначение: дать удобный интерфейс для получения содержимого элемента. Один из основных методов — <code>getCharacterCoords(char_ix)</code>, возвращает координаты для определённого символа. Метод возвращает объект со свойствами <code>x</code> и <code>y</code> в пикселях, отсчёт идёт от левого верхнего угла <code>&lt;textarea&gt;</code> до <em>левого верхнего угла</em> нужного символа. Метод <code>getAbsoluteCharacterCoords(char_ix)</code> возвращает то же самое, только относительно <code>offsetParent</code>. Как не трудно догадаться, этот класс можно переписать для работы с content-editable блоком, и у вас будет автодополнение для более серьёзного редактора.</li>
<li><code>ContentAssistProcessor</code> — процессор, который высчитывает и предлагает список дополнений. По умолчанию ищет левую и правую границу английского слова (см. метод <code>isAllowedChar</code>), ищет префикс (от левой границы слова до каретки) и предлагет по этому префиксу список дополнений.</li>
<li><code>CompletionProposal</code> — предложение для автодополнения. Главный метод — <code>apply()</code>, который применяет текущее предложение к <code>TextViewer</code>-объекту.</li>
<li><code>ContentAssist</code> - основной класс. Связывает между собой <code>TextViewer</code> и <code>ContentAssistProcessor</code>, а также управляет всплывающим окном с подсказками.</li>
<li><code>tx_utils</code> — набор всяких DOM-утилит.</li>
</ul>
<p>Для добавления поддержки автодополнения для <code>&lt;textarea&gt;</code> нужно инициализировать все эти классы:</p>
<pre class="brush: js">
var viewer = new TextViewer(document.getElementById('my-textarea'));
var processor = new ContentAssistProcessor(['word1', 'word2', 'word3']);
var content_assist = new ContentAssist(viewer, processor);
</pre>
<p>Для удобства есть класс <code>BasicContentAssist</code>, который делает всё то же самое для простых случаев (автодополнение по фиксированному словарю):</p>
<pre class="brush: js">
new BasicContentAssist(document.getElementById('my-textarea'), ['word1', 'word2', 'word3']);
</pre>
<p>У класса <code>ContentAssistProcessor</code> есть метод <code>completitionProposalFactory</code>, который должен возвращать объект класса <code>CompletionProposal</code>. Внешний вид попапа с предложениями был нагло позаимствован из редактора <a href="http://macrabbit.com/espresso/">Espresso</a> и полностью управляется через CSS. Окошко с предложениями можно вызвать принудительно по Ctrl+Space (не работает в Опере и немного тупит в Файерфоксе) либо по Alt+Space (хвала разработчикам Оперы: это тоже не работает, по крайней мере на Маке) и заменить текущее слово (зависит от позиции каретки).</p>
<p>Как видите, все классы можно удобно перекрывать, расширять и наследовать для достижения необходимого функционала. Баги, предложения и улучшения принимаются в комментариях и на <a href="http://github.com/sergeche/tx-content-assist/">странице проекта</a>.</p>
<p><strong>UPD:</strong> немного дотюнил процессор, теперь он находит границы слов вне зависимости от языка (то есть можно отдавать словарь с русскими словами), а также немного улучшил поддержку больших словарей.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=655" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/zNnYeiA1Jwk" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/06/content-assist-for-textarea/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/06/content-assist-for-textarea/</feedburner:origLink></item>
		<item>
		<title>Как создавалась Айчиталка. Часть 1: движок</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/M8GV0az1hYc/</link>
		<comments>http://chikuyonok.ru/2010/06/booq-1/#comments</comments>
		<pubDate>Wed, 02 Jun 2010 08:55:34 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Верстка]]></category>

		<category><![CDATA[Статьи]]></category>

		<category><![CDATA[html]]></category>

		<category><![CDATA[iphone]]></category>

		<category><![CDATA[javascript]]></category>

		<category><![CDATA[оптимизация]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=653</guid>
		<description><![CDATA[Мы потихоньку переходим на Хабр с корпоративным блогом Аймобилко, где я и мои коллеги будем рассказывать о том, как создавались наши сервисы. Первая статья: рассказ про создание движка онлайн-читалки booq (так называется движок, сам сервис называется Айчиталка).
Пока пишу с корпоративного аккаунта, а если суппорт Хабра таки отдуплится и поправит баги, то буду писать со своего.
 [...]]]></description>
			<content:encoded><![CDATA[<p>Мы потихоньку переходим на Хабр с <a href="http://habrahabr.ru/company/imobilco/blog/">корпоративным блогом Аймобилко</a>, где я и мои коллеги будем рассказывать о том, как создавались наши сервисы. Первая статья: <a href="http://habrahabr.ru/company/imobilco/blog/95249/">рассказ про создание движка онлайн-читалки booq</a> (так называется движок, сам сервис называется Айчиталка).</p>
<p>Пока пишу с корпоративного аккаунта, а если суппорт Хабра таки отдуплится и поправит баги, то буду писать со своего.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=653" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/M8GV0az1hYc" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/06/booq-1/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/06/booq-1/</feedburner:origLink></item>
		<item>
		<title>Узнаём строку CSS-правила в Safari/WebKit/Chrome Web Inspector</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/8By60bBxUNA/</link>
		<comments>http://chikuyonok.ru/2010/03/web-inspector-css-hack/#comments</comments>
		<pubDate>Sat, 13 Mar 2010 16:02:20 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Прочее]]></category>

		<category><![CDATA[chrome]]></category>

		<category><![CDATA[css]]></category>

		<category><![CDATA[safari]]></category>

		<category><![CDATA[web inspector]]></category>

		<category><![CDATA[webkit]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=648</guid>
		<description><![CDATA[В браузерах на движке WebKit есть замечательный инструмент для отладки веб-страниц — Web Inspector. По сути, это аналог Firebug, со своими сильными и слабыми сторонами. В последнее время я всё чаще предпочитаю пользоваться Web Inspector’ом, однако один его недостаток постоянно не давал мне покоя — это отсутствие номера строки редактируемого CSS-правила: 

Если погуглить, то можно [...]]]></description>
			<content:encoded><![CDATA[<p>В браузерах на движке WebKit есть замечательный инструмент для отладки веб-страниц — Web Inspector. По сути, это аналог Firebug, со своими сильными и слабыми сторонами. В последнее время я всё чаще предпочитаю пользоваться Web Inspector’ом, однако один его недостаток постоянно не давал мне покоя — это отсутствие номера строки редактируемого CSS-правила: </p>
<p><img src="http://chikuyonok.ru/u/2010/03/css-tab.png" alt="css-tab" title="css-tab" width="209" height="259" class="alignnone size-full wp-image-650" /></p>
<p>Если погуглить, то можно убедиться, что не одного меня беспокоит эта проблема. Руководствуясь правилом «если хочешь что-то сделать — сделай это сам» я написал небольшой хак, который позволяет узнать номер строки CSS-правила:</p>
<p><a href="http://github.com/sergeche/webkit-css/">Web Inspector CSS hack</a></p>
<p>Установка довольно простая: нужно всего лишь подключить файл <code>SC-CSSAdditions.js</code> в <em>inspector.html</em> (Safari, WebKit) или <em>devtools.html</em> (Google Chrome). Подробная инструкция написана на странице проекта.</p>
<h2>Как это работает</h2>
<p>Весь Web Inspector практически полностью базируется на возможностях браузера, предоставляемых спецификацией W3C. Соответственно, номер строки CSS-правила просто так не узнаешь. Я написал альтернативный CSS-парсер, который достаёт из исходника правила, но при этом сохраняет их позицию в коде. Перед тем, как Web Inspector выводит список правил, принадлежащих элементу, происходит сопоставление этих правил с моими (по позиции в списке или по селектору) и к имени файла добавляется номер строки:</p>
<p><img src="http://img638.yfrog.com/img638/715/q0b.png" alt="" /></p>
<p>Пока на всех протестированных мной сайтах проблем не обнаружено. Максимум, что может произойти — это выведется неправильный номер строки. Если найдёте такую проблему, напишите, пожалуйста, мне в <a href="http://github.com/sergeche/webkit-css/issues">багтрэкер</a> и не забудьте приложить пример CSS-кода.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=648" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/8By60bBxUNA" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/03/web-inspector-css-hack/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/03/web-inspector-css-hack/</feedburner:origLink></item>
		<item>
		<title>Ambilight для тэга video</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/8Kxt9koIx84/</link>
		<comments>http://chikuyonok.ru/2010/03/ambilight-video/#comments</comments>
		<pubDate>Thu, 04 Mar 2010 01:23:12 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Прочее]]></category>

		<category><![CDATA[canvas]]></category>

		<category><![CDATA[html5]]></category>

		<category><![CDATA[javascript]]></category>

		<category><![CDATA[video]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=633</guid>
		<description><![CDATA[В некоторых топовых моделях телевизоров Philips есть такая прикольная штука, как Ambilight. По сути, это светодиодная подсветка телевизора, которая меняет цвет в зависимости от цвета картинки. Смотреть кино на таком телевизоре — одно удовольствие.
На флэше уже есть реализации такой подсветки, ну а чем мы — фронтовики — хуже? Дабы в очередной раз разобраться, на что [...]]]></description>
			<content:encoded><![CDATA[<p>В некоторых топовых моделях телевизоров Philips есть такая прикольная штука, как <a href="http://en.wikipedia.org/wiki/Ambilight">Ambilight</a>. По сути, это светодиодная подсветка телевизора, которая меняет цвет в зависимости от цвета картинки. Смотреть кино на таком телевизоре — одно удовольствие.</p>
<p>На флэше уже есть реализации такой подсветки, ну а чем мы — фронтовики — хуже? Дабы в очередной раз разобраться, на что способны современные браузеры, на свет появился очередной эксперимент:</p>
<p><a href="/ambilight/">Ambilight для тэга &lt;video&gt;</a> <span style="font-size:0.75em">(Firefox 3.5, Opera 10.5, Safari 4, Google Chrome 4)</span></p>
<p>Далее рассмотрим, как это было сделано.</p>
<h2>Алгоритм</h2>
<p>Прежде, чем начать что-то писать, нужно составить алгоритм, по которому будет работать наша подсветка.</p>
<p>Настоящая подсветка в телевизоре работает примерно так. На задней панели располагается ряд ярких светодиодов, которые светятся разными цветами. Причём цвет диода примерно соответствует цвету области изображения, напротив которой он находится. Когда картинка меняется, светодиод плавно меняет свой цвет на другой.</p>
<p>Исходя из этого описания, нам нужно проделать следующее: определить цвет каждого диода для текущего кадра и отрисовать его свечение. Что ж, приступим.</p>
<h2>Определяем цвет диода</h2>
<p>Для удобства предположим, что в нашем «телевизоре» всего по 5 светодиодов с каждой стороны. Соответственно, нужно взять фрагмент кадра, разделить его на области по количеству диодов и найти усреднённый цвет в каждой области — это и будут цвета подсветки:</p>
<p><img src="http://chikuyonok.ru/u/2010/03/get-color.png" alt="get-color" title="get-color" width="403" height="106" class="alignnone size-full wp-image-634" /></p>
<p>Чтобы получить изображение текущего видео-кадра, достаточно отрисовать его в <code>&lt;canvas&gt;</code> через метод <code>drawImage()</code>:</p>
<pre class="brush: js">
var canvas = document.createElement('canvas'),
	video = document.getElementsByTagName('video')[0],
	ctx = canvas.getContext('2d');

// обязательно выставляем размер холста
canvas.width = video.width;
canvas.height = video.height;

// рисуем кадр
ctx.drawImage(video, 0, 0, video.width, video.height);
</pre>
<p>Текущий кадр получили, теперь нужно узнать, какого цвета пиксели сбоку изображения. Для этого воспользуемся методом <code>getImageData()</code>:</p>
<pre class="brush: js">
/** Ширина области, которую будем анализировать */
var block_width = 50;

var pixels = ctx.getImageData(0, 0, block_width, canvas.height);
</pre>
<p>В объекте <code>pixels</code> есть свойство <code>data</code>, в котором содержатся цвета всех пикселей. Причём хранятся они в немного необычном формате: это массив RGBA-компонетнов всех пикселей. К примеру, чтобы узнать цвет и прозрачность первого пикселя, нужно взять первые 4 элемента массива <code>data</code>, второго пикселя — следующие 4 и так далее:</p>
<pre class="brush:js">
var pixel1 = {
	r: pixels.data[0],
	g: pixels.data[1],
	b: pixels.data[2],
	a: pixels.data[3]
};

var pixel2 = {
	r: pixels.data[4],
	g: pixels.data[5],
	b: pixels.data[6],
	a: pixels.data[7]
};
</pre>
<p>Нам нужно разделить все полученные пиксели на 5 групп (по количеству светодиодов, которое мы выбрали ранее) и проанализировать каждую группу по очереди:</p>
<pre class="brush:js">
function getMidColors() {
	var width = canvas.width,
		height = canvas.height,
		lamps = 5, //количество светодиодов
		block_width = 50, // ширина анализируемой области
		block_height = Math.ceil(height / lamps), // высота анализируемого блока
		pxl = block_width * block_height * 4, // сколько всего RGBA-компонентов в одной области
		result = [],

		img_data = ctx.getImageData(0, 0, block_width, h),
		total = img_data.data.length;

	for (var i = 0; i < lamps; i++) {
		var from = i * width * block_width;
		result.push( calcMidColor(img_data.data, i * pxl, Math.min((i + 1) * pxl, total_pixels - 1)) );
	}

	return result;
}
</pre>
<p>В этой функции мы просто пробегаемся по анализируемым блокам и считаем для них усреднённый цвет с помощью функции <code> calcMidColor()</code>. Нам не нужно применять всякие хитрые формулы, чтобы посчитать усреднённый цвет на области исходя из интенсивности цветов в ней, достаточно посчитать среднее арифметическое для каждого цветового компонента:</p>
<pre class="brush:js">
function calcMidColor(data, from, to) {
	var result = [0, 0, 0];
	var total_pixels = (to - from) / 4;

	for (var i = from; i <= to; i += 4) {
		result[0] += data[i];
		result[1] += data[i + 1];
		result[2] += data[i + 2];
	}

	result[0] = Math.round(result[0] / total_pixels);
	result[1] = Math.round(result[1] / total_pixels);
	result[2] = Math.round(result[2] / total_pixels);

	return result;
}
</pre>
<p>Итак, мы получили цвета для светодиодов, но они слишком тусклые: ведь диоды светят очень ярко чтобы добиться достаточного уровня свечения. Нужно увеличить яркость цветов, а также увеличить насыщенность, чтобы добавить глубины свечению. Для этих целей очень удобно пользоваться цветовой моделью HSV — hue, saturation, value, — достаточно домножить два последних компонента на некий коэффициент. Но цвета у нас хранятся в модели RGB, поэтому сначала конвертируем цвет в HSV, увеличиваем яркость и насыщенность, а затем обратно конвертируем в RGB (формулы конвертирования RGB→HSV и обратно легко находятся в интернетах):</p>
<pre class="brush:js">
function adjustColor(color) {
	color = rgb2hsv(color);
	color[1] = Math.min(100, color[1] * 1.4); // насыщенность
	color[2] = Math.min(100, color[2] * 2.7); // яркость
	return hsv2rgb(color);
}
</pre>
<h2>Рисуем свечение</h2>
<p>Светодиоды — это всенаправленные источники света. Для их отображения лучше всего подходят радиальные градиенты: для каждого диода свой градиент. Однако для достижения хорошего визуального результата придётся делать очень много сложных расчётов: нужно учитывать позицию диода, диаметр и затухание свечения, смешивание соседних цветов и так далее. Поэтому мы немного сжульничаем: нарисуем обычный — линейный — градиент, а сверху наложим специальную маску, которая создаст ощущение правдоподобного свечения.</p>
<p>Градиент рисуется просто: сначала создаём его с помощью <code>createLinearGradient()</code>, а потом добавляем цвета через <code>addColorStop()</code> и отрисовываем его:</p>
<pre class="brush:js">
// для свечения создаём новый холст
var light_canvas = document.createElement('canvas'),
	light_ctx = light_canvas.getContext('2d');

light_canvas.width = 200;
light_canvas.height = 200;

var midcolors = getMidColors(), // полчаем усреднённые цвета

	grd = ctx.createLinearGradient(0, 0, 0, canvas.height); // градиент

for (var i = 0, il = midcolors.length; i < il; i++) {
	grd.addColorStop(i / il, 'rgb(' + adjustColor(midcolors[i]).join(',') + ')');
}

// рисуем градиент
light_ctx.fillStyle = grd;
light_ctx.fillRect(0, 0, light_canvas.width, light_canvas.height);
</pre>
<p>Получим что-то вроде этого:</p>
<p><img src="http://chikuyonok.ru/u/2010/03/gradient.png" alt="gradient" title="gradient" width="200" height="327" class="alignnone size-full wp-image-635" /></p>
<h2>Маска</h2>
<p>Маску мы нарисуем в фотошопе. Есть замечательный фильтр Lightning Effects (Filters→Render→ Lightning Effects&#8230;), который позволяет создавать источники света. Заливаем слой белым цветом и вызываем этот фильтр примерно с такими настройками:</p>
<p><img src="http://chikuyonok.ru/u/2010/03/lightning.png" alt="lightning" title="lightning" width="500" height="458" class="alignnone size-full wp-image-636" /></p>
<p>Получим вот такое световое пятно:</p>
<p><img src="http://chikuyonok.ru/u/2010/03/spot.png" alt="spot" title="spot" width="400" height="400" class="alignnone size-full wp-image-637" /></p>
<p>Меняем режим наложения на Lighten, дублируем, крутим, меняем масштаб, играемся с прозрачностью, правим уровни и получаем вот такой результат:<br />
<img src="http://chikuyonok.ru/u/2010/03/spot-grid.png" alt="spot-grid" title="spot-grid" width="310" height="310" class="alignnone size-full wp-image-638" /></p>
<p>Так как изображение чёрно-белое, из него очень легко получить маску, где белый цвет будет прозрачным. И если эту маску наложить поверх градиента, то получим вполне себе симпатичное свечение:</p>
<p><img src="http://chikuyonok.ru/u/2010/03/result.jpg" alt="result" title="result" width="585" height="240" class="alignnone size-full wp-image-639" /></p>
<p>Но самое главное — мы легко сможем менять внешний вид и интенсивность свечения, не прибегая к программированию.</p>
<p>Свечение для левой стороны готово, осталось проделать то же самое для правой стороны, добавить плавную смену подсветок и написать контроллер, который с определённым интервалом будет эту подсветку обновлять. Расписывать это — долго и нудно, проще посмотреть <a href="/ambilight/ambilight.js">исходник</a>.</p>
<p><strong>UPD:</strong> как показал эксперимент, далеко не у всех нормально работает HD-видео (изначально размер ролика был 1280×544), снижение разрешения до 592×256 решило проблему.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=633" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/8Kxt9koIx84" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/03/ambilight-video/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/03/ambilight-video/</feedburner:origLink></item>
		<item>
		<title>Улучшаем text-align: justify</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/_tPWHKyi3DE/</link>
		<comments>http://chikuyonok.ru/2010/02/better-justify/#comments</comments>
		<pubDate>Sat, 27 Feb 2010 10:04:51 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Верстка]]></category>

		<category><![CDATA[css]]></category>

		<category><![CDATA[justify]]></category>

		<category><![CDATA[text-align]]></category>

		<category><![CDATA[word-spacing]]></category>

		<category><![CDATA[переносы]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=628</guid>
		<description><![CDATA[Считается, что выравнивание текста на всю ширину делают только дилетанты. Причина: огромные просветы между словами. Даже если в тексте расставлены переносы, результат всё равно получается не самый лучший:

Не люб&#173;лю я вос&#173;крес&#173;ные ве&#173;че&#173;ра. Как бы объ&#173;яс&#173;нить… все, что с ни&#173;ми свя&#173;за&#173;но, то есть са&#173;му об&#173;ста&#173;нов&#173;ку вос&#173;крес&#173;но&#173;го ве&#173;че&#173;ра, я не люб&#173;лю. С при&#173;бли&#173;же&#173;ни&#173;ем вос&#173;крес&#173;но&#173;го ве&#173;че&#173;ра у ме&#173;ня [...]]]></description>
			<content:encoded><![CDATA[<p>Считается, что выравнивание текста на всю ширину делают только дилетанты. Причина: огромные просветы между словами. Даже если в тексте расставлены переносы, результат всё равно получается не самый лучший:</p>
<div style="width:260px;text-align:justify;line-height:1.4">
<p>Не люб&shy;лю я вос&shy;крес&shy;ные ве&shy;че&shy;ра. Как бы объ&shy;яс&shy;нить… все, что с ни&shy;ми свя&shy;за&shy;но, то есть са&shy;му об&shy;ста&shy;нов&shy;ку вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра, я не люб&shy;лю. С при&shy;бли&shy;же&shy;ни&shy;ем вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра у ме&shy;ня обя&shy;за&shy;тель&shy;но на&shy;чи&shy;на&shy;ет дер&shy;гать в го&shy;ло&shy;ве. Ког&shy;да силь&shy;нее, ког&shy;да сла&shy;бее. Но дер&shy;га&shy;ет обя&shy;за&shy;тель&shy;но. Внут&shy;ри вис&shy;ков, в сан&shy;ти&shy;мет&shy;ре или двух от ко&shy;жи, дер&shy;га&shy;ет с обе&shy;их сто&shy;рон так, как буд&shy;то что-то тя&shy;нет мяг&shy;кий бе&shy;лый сгус&shy;ток пло&shy;ти на&shy;ру&shy;жу. Ощу&shy;ще&shy;ние, что из се&shy;ре&shy;ди&shy;ны вис&shy;ка вы&shy;ле&shy;за&shy;ет не&shy;ви&shy;ди&shy;мая нить, а кто-то из&shy;да&shy;ле&shy;ка, схва&shy;тив за са&shy;мый кон&shy;чик, по&shy;ти&shy;хонь&shy;ку дер&shy;га&shy;ет за нее. Мне не осо&shy;бен&shy;но-то и боль&shy;но. Я бы не уди&shy;вил&shy;ся, если бы бы&shy;ло боль&shy;но, но, как ни стран&shy;но, не боль&shy;но. Буд&shy;то глу&shy;бо&shy;ко вве&shy;ли длин&shy;ную иг&shy;лу в оне&shy;мев&shy;шее от нар&shy;ко&shy;за мес&shy;то.</p>
</div>
<p>Но в случае с переносами текст можно заметно улучшить. Достаточно всего лишь уменьшить расстояние между словами с помощью CSS-свойства <code>word-spacing</code>, выставив минимально допустимое расстояние. Важно помнить, что в <code>word-spacing</code> указывается не само расстояние, а его отклонение от стандартного значения: <code>word-spacing: 0</code> — стандартное расстояние, <code>word-spacing: 10px;</code> — стандартное расстояние + 10 пикселей. </p>
<p>Немного подтянем слова друг к другу:</p>
<pre class="brush: css">
p {
	text-align: justify;
	word-spacing: -0.3ex;
}
</pre>
<p>&#8230;и текст выглядит заметно лучше:</p>
<div style="width:260px;text-align:justify;line-height:1.4;float:left;margin-right:50px">
<h4>Было</h4>
<p>Не люб&shy;лю я вос&shy;крес&shy;ные ве&shy;че&shy;ра. Как бы объ&shy;яс&shy;нить… все, что с ни&shy;ми свя&shy;за&shy;но, то есть са&shy;му об&shy;ста&shy;нов&shy;ку вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра, я не люб&shy;лю. С при&shy;бли&shy;же&shy;ни&shy;ем вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра у ме&shy;ня обя&shy;за&shy;тель&shy;но на&shy;чи&shy;на&shy;ет дер&shy;гать в го&shy;ло&shy;ве. Ког&shy;да силь&shy;нее, ког&shy;да сла&shy;бее. Но дер&shy;га&shy;ет обя&shy;за&shy;тель&shy;но. Внут&shy;ри вис&shy;ков, в сан&shy;ти&shy;мет&shy;ре или двух от ко&shy;жи, дер&shy;га&shy;ет с обе&shy;их сто&shy;рон так, как буд&shy;то что-то тя&shy;нет мяг&shy;кий бе&shy;лый сгус&shy;ток пло&shy;ти на&shy;ру&shy;жу. Ощу&shy;ще&shy;ние, что из се&shy;ре&shy;ди&shy;ны вис&shy;ка вы&shy;ле&shy;за&shy;ет не&shy;ви&shy;ди&shy;мая нить, а кто-то из&shy;да&shy;ле&shy;ка, схва&shy;тив за са&shy;мый кон&shy;чик, по&shy;ти&shy;хонь&shy;ку дер&shy;га&shy;ет за нее. Мне не осо&shy;бен&shy;но-то и боль&shy;но. Я бы не уди&shy;вил&shy;ся, если бы бы&shy;ло боль&shy;но, но, как ни стран&shy;но, не боль&shy;но. Буд&shy;то глу&shy;бо&shy;ко вве&shy;ли длин&shy;ную иг&shy;лу в оне&shy;мев&shy;шее от нар&shy;ко&shy;за мес&shy;то.</p>
</div>
<div style="width:260px;text-align:justify;line-height:1.4;float:left;word-spacing:-0.3ex">
<h4>Стало</h4>
<p>Не люб&shy;лю я вос&shy;крес&shy;ные ве&shy;че&shy;ра. Как бы объ&shy;яс&shy;нить… все, что с ни&shy;ми свя&shy;за&shy;но, то есть са&shy;му об&shy;ста&shy;нов&shy;ку вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра, я не люб&shy;лю. С при&shy;бли&shy;же&shy;ни&shy;ем вос&shy;крес&shy;но&shy;го ве&shy;че&shy;ра у ме&shy;ня обя&shy;за&shy;тель&shy;но на&shy;чи&shy;на&shy;ет дер&shy;гать в го&shy;ло&shy;ве. Ког&shy;да силь&shy;нее, ког&shy;да сла&shy;бее. Но дер&shy;га&shy;ет обя&shy;за&shy;тель&shy;но. Внут&shy;ри вис&shy;ков, в сан&shy;ти&shy;мет&shy;ре или двух от ко&shy;жи, дер&shy;га&shy;ет с обе&shy;их сто&shy;рон так, как буд&shy;то что-то тя&shy;нет мяг&shy;кий бе&shy;лый сгус&shy;ток пло&shy;ти на&shy;ру&shy;жу. Ощу&shy;ще&shy;ние, что из се&shy;ре&shy;ди&shy;ны вис&shy;ка вы&shy;ле&shy;за&shy;ет не&shy;ви&shy;ди&shy;мая нить, а кто-то из&shy;да&shy;ле&shy;ка, схва&shy;тив за са&shy;мый кон&shy;чик, по&shy;ти&shy;хонь&shy;ку дер&shy;га&shy;ет за нее. Мне не осо&shy;бен&shy;но-то и боль&shy;но. Я бы не уди&shy;вил&shy;ся, если бы бы&shy;ло боль&shy;но, но, как ни стран&shy;но, не боль&shy;но. Буд&shy;то глу&shy;бо&shy;ко вве&shy;ли длин&shy;ную иг&shy;лу в оне&shy;мев&shy;шее от нар&shy;ко&shy;за мес&shy;то.</p>
</div>
<div class="clear"></div>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=628" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/_tPWHKyi3DE" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/02/better-justify/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/02/better-justify/</feedburner:origLink></item>
		<item>
		<title>Data:URL средствами браузера</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/s2fDX4gc8wg/</link>
		<comments>http://chikuyonok.ru/2010/02/browser-data-url/#comments</comments>
		<pubDate>Tue, 23 Feb 2010 22:13:30 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Верстка]]></category>

		<category><![CDATA[animation]]></category>

		<category><![CDATA[base64]]></category>

		<category><![CDATA[css]]></category>

		<category><![CDATA[data:url]]></category>

		<category><![CDATA[firefox]]></category>

		<category><![CDATA[javascript]]></category>

		<category><![CDATA[transition]]></category>

		<category><![CDATA[webkit]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=608</guid>
		<description><![CDATA[Думаю, зачем нужен data:url объяснять не стоит. Несмотря на проблемы с применением в IE, data:url незаменим, когда нужно отдавать HTML-страницу в виде одного файла (очень удобно такие файлы кэшировать в приложениях). Мне в последнее время приходится с ним довольно много работать и я был озадачен поиском хорошего инструмента, который смог бы легко кодировать графические файлы [...]]]></description>
			<content:encoded><![CDATA[<p>Думаю, зачем нужен <a href="http://en.wikipedia.org/wiki/Data_URI_scheme">data:url</a> объяснять не стоит. Несмотря на проблемы с применением в IE, data:url незаменим, когда нужно отдавать HTML-страницу в виде одного файла (очень удобно такие файлы кэшировать в приложениях). Мне в последнее время приходится с ним довольно много работать и я был озадачен поиском хорошего инструмента, который смог бы легко кодировать графические файлы в base64. Критерии к инструменту следующие:</p>
<ul>
<li>drag’n’drop;</li>
<li>возможность конвертации сразу нескольких файлов;</li>
<li>удобное копирование конечного результата;</li>
<li>должен работать на Маке;</li>
<li>няшный интерфейс с прЕкольными анимашками )))))))</li>
</ul>
<p>Беглый поиск в интернете удовлетворительных результатов не дал. Есть <a href="http://www.sveinbjorn.org/dataurlmaker">утилитка от Sveinbjorn Thordarson</a>, которой я пользовался раньше, но она очень неудобная: принимает только по одному файлу, а результат нужно вычленять из &lt;img&gt;-тэга (там есть ссылка на маковский дроплет, который у меня не завёлся). <a href="http://duris.ru/">Duris</a> смущает тем, что требует готовую страницу, доступную где-то в интернете (слишком сложно для преобразования 2—3 файлов).</p>
<p>Поэтому я решил создать свой веб-сервис, который будет удовлетворять этим требованиям. Когда я только принялся его писать, возникло здоровое чувство жлобства: зачем мне создавать онлайн-сервис, которым бесплатно будут пользоваться десятки, а то и сотни человек ежедневно, да ещё и за трафик платить из своего кармана? Может, современные браузеры уже могут делать такие штуки без помощи сервера?</p>
<p>Как оказалось, вполне себе могут. <a href="/data-url/">Вот что у меня получилось.</a> Работает, правда, не везде: только Firefox 3.6, Safari 4, последний Google Chrome (только Windows-версия). Для Safari и Chrome нужно сначала <a href="/data-url/browser-data-url.zip">скачать</a> себе эту страничку и запустить её локально (почему так узнаете, дочитав статью).  Несмотря на такие ограничения, поддержки этих браузеров вполне достаточно для современного веб-разработчика. </p>
<p>Дальше будет много текста про то, как создавался этот сервис.</p>
<h2>Начало</h2>
<p>Помимо выполнения сугубо утилитарных функций (кодирование бинарного изображения в base64 налету), этот сервис в первую очередь был плацдармом для обкатки новых браузерных технологий, о которых все говорят, но толком не используют. В частности, это CSS Transforms, CSS Transitions и CSS Animations. Поэтому работа была более исследовательской, нежели практической. В качестве инструментария я выбрал <a href="http://jquery.com/">jQuery</a> для рутинных DOM-операций и свой <a href="http://code.google.com/p/jtweener/">jTweener</a> для анимаций (его очень легко адаптировать под любые CSS-свойства). Итак, начнём.</p>
<h2>Firefox</h2>
<p>В новом Firefox 3.6 появилось много нововведений, одно из которых — <a href="https://developer.mozilla.org/en/Using_files_from_web_applications">поддержка drag’n’drop</a>. Причём таскать и бросать можно не только локальные блоки на веб-странице, но и внешние файлы. Перетаскиваемые файлы можно «поймать» с помощью JS и что-нибудь с ними сделать, не перезагружая страницу. Для этих целей существуют специальные объекты вроде <code>Clipboard</code> и <code>FileReader</code>, позволяюще получить и тут же прочитать файлы.</p>
<p>Вообще, в Firefox 3.6 задача кодирования перетаскиваемых файлов в base64 решается элементарно: вешаем на окно обработчик события <code>drop</code>, в котором из объекта события достаём список перетаскиваемых файлов и читаем их с помощью <code>FileReader</code>. Примерный код решения:</p>
<pre class="brush: js">
function handleFiles(evt) {
	// запрещаем бразеру открывать перетаскиваемые файлы
	evt.stopPropagation();
	evt.preventDefault();

	// список перетаскиваемых файлов
	var files = evt.dataTransfer.files;

	// читаем файлы
	for(var i = 0; i < files.length; i++) {
		var reader = new FileReader();
		reader.onloadend = function(e) {
			// в e.target.result содержится изображение в формате data:url
			console.log(e.target.result);
		};
		reader.readAsDataURL(files[i]);
	}
}

document.addEventListener('drop', handleFiles, false);
</pre>
<p>Обратите внимание вот на что: если используете jQuery, то привязать событие <code>drop</code> через <code>$(document).bind('drop', handleFiles);</code> не получится, привязывать нужно именно через стандартный DOM-метод <code>addEventListener()</code>. Похоже, jQuery ещё не знает о таком событии.</p>
<p>В принципе, на кодировании изображений только в Firefox 3.6 можно было и остановиться. Но моим главным инструментом является Safari, поэтому решил попытаться реализовать поддержку и это браузера.</p>
<h2>Safari (Webkit)</h2>
<p>Четвёртый Safari тоже поддерживает drag’n’drop, но получить содержимое файла в этом браузере оказалось сложнее (в то же время гораздо интереснее).</p>
<p>Начнём с того, что Safari не поддерживает <code>FileReader</code>, то есть читать файлы просто нечем. В <code>evt.dataTransfer.files</code> (см. предыдущий пример) содержится список объектов класса <a href="http://developer.apple.com/mac/library/documentation/AppleApplications/Reference/WebKitDOMRef/File_idl/Classes/File/index.html#//apple_ref/js/cl/File"><code>File</code></a>, у которого есть только 2 публичных свойства: <code>fileName</code> и <code>fileSize</code>. Мы можем получить только имя файла, но не полный путь к нему.</p>
<p>Нужно найти способ, как получить полный путь к файлу, чтобы можно было хоть как-то загрузить его в браузер. Рассматриваем внимательно объект <code>evt.dataTransfer</code>, который является объектом класса <a href="http://developer.apple.com/mac/library/documentation/AppleApplications/Reference/WebKitDOMRef/Clipboard_idl/Classes/Clipboard/index.html#//apple_ref/js/cl/Clipboard"><code>Clipboard</code></a>. У него есть метод <code>getData()</code>, позволяющий получить содержимое буффера обмена. Но этому методу нужно передать строку с названием типа данных, в котором хотим получить данные. Доступные типы определены в свойстве <code>types</code>. Простым перебором выясняем, что если передать тип <code>text/uri-list</code>, то получим список абсолютных путей ко всем файлам, разделённый переводами строк:</p>
<pre class="brush: js">
function handleFiles(evt) {
	// список перетаскиваемых файлов
	var files = evt.dataTransfer.getData('text/uri-list');

	console.log(files);
	// выведет:
	// file:///path/to/image1.png
	// file:///path/to/image2.jpg
}
</pre>
<p>Итак, полдела сделано: мы получили список абсолютных путей к файлам, которые можем загрузить в браузер. Первая мысль, которая у меня возникла: создать <code>&lt;img&gt;</code> тэг, указав в качестве <code>src</code> путь к файлу, а после загрузки отрисовать его в <code>canvas</code> и получить data:url через метод <code>toDataURL()</code>. Однако эту мысль сразу же отбросил: во-первых, мы получим совершенно новое изображение, а не то, которое отдавали. Во-вторых, не понятно, что делать в JPEG-изображениями. В <code>toDataURL()</code> можно отдать тип, в котором хотим получить файл, но это будет то же самое, что сохранить JPEG как PNG, а потом обратно сохранить в JPEG, но уже с неизвестными параметрами сжатия.</p>
<p>Второй способ, который пришёл мне в голову, это воспользоваться старым добрым <code>XMLHttpRequest</code> для загрузки файла. В принципе, идея неплохая, но есть одно жирное <strong>«но»</strong>: данные в <code>responseText</code> будут автоматически перекодированы браузером в текущую кодировку, что, естественно, нарушит целостность данных. В свежих версиях <code>XMLHttpRequest</code> (например, в IE8) есть свойство <code>responseStream</code>, в котором содержатся «чистые» байты файла, но Safari его не поддерживает.</p>
<p>Выходом оказался хак, найденный где-то на <a href="https://developer.mozilla.org/">MDC</a>. Суть его заключается в том, что если у объекта класса <code>XMLHttpRequest</code> переопределить тип и кодировку файла с помощью метода <code>overrideMimeType()</code>, то в <code>responseText</code> окажутся правильные данные. В последних версиях jQuery у метода <code>jQuery.ajax()</code> в качестве параметра можно отдать метод <code>xhr()</code>, который должен вернуть <code>XMLHttpRequest</code>, с помощью которого будут загружаться данные. Вот как можно загрузить «чистые» данные:</p>
<pre class="brush: js">
$.ajax({
	url: file_path,
	xhr: function(x) {
		var xhr = new XMLHttpRequest();
		xhr.overrideMimeType('text/plain; charset=x-user-defined');
		return xhr;
	},
	success: function(data) {
		console.log(base64_encode(data));
	}
});
</pre>
<p>В метод <code>success</code> пришло правильное содержание файла, которое теперь нужно просто закодировать в base64 (соответствующая JS-реализация была найдена на просторах интернета).</p>
<h2>Копирование в буфер</h2>
<p>Файлы мы загрузили, перекодировали и вывели, теперь нужно придумать, как их удобно скопировать в буфер. Выводить здровенное &lt;textarea&gt;-поле и заставлять пользователя каждый раз выделять и копировать эти данные как-то совсем некошерно. Нужно сделать кнопочку, по нажатию на которую в буфер обена будут попадать нужные данные. В этом случае не нужно будет перегружать интерфейс ненужным данными, а также можно будет вывести несколько кнопок, которые будут копировать данные в разных форматах: обычный data:url, картинка и <code>background-image</code>.</p>
<p>Единственный известный мне способ программно запихнуть данные в буфер обмена, это использование небольшой флэшки (флэшукапец, ага). Подробно об этом написано <a href="http://cssing.org.ua/2009/04/15/copy-to-clipboard-javascrip/">в блоге CSSing.org.ua</a>, я же использовал слегка допиленную версию <a href="http://beckelman.net/post/2009/01/22/Copy-to-Clipboard-with-ZeroClipboard-Flash-10-and-jQuery.aspx">ZeroClipboard</a>.</p>
<p>Проблемы использования флэша на сайте уже много раз обсуждались, я же напишу о тех, которые возникли в разрабатываемом сервисе. Не знаю, как на винде, но на Маке одно только присутствие флэша на странице уже нагружает процессор, даже если эта флэшка ничего не делает. Причём, чем больше флэшек, тем больше нагрузка. Уже на 3-х загруженных файлах (у каждого 3 кнопки копирования; итого 3×3=9 флэшек) мой Core 2 Duo был загружен на 25%, при том что ни одного пикселя на странице не шевелилось. Ещё один побочный эффект — это влияние на анимацию. Когда пользователь удаляет блок с картинкой, содержимое блока тут же пропадало.</p>
<p>Поэтому я сделал так: флэшки добавляются только тогда, когда пользователь наводит курсор на блок файла, и исчезают, когда уводит. Обе проблемы были решены, но владельцам Firefox достался немного неприятный побочный эффект в виде моргающих надписей на кнопках, когда флэшки исчезают (надеюсь, меня простят за это).</p>
<p>Только я хотел нажать на кнопку «Опубликовать статью», как внутренне чувство заставило проверить всё ещё раз. Инстинкт меня не подвёл. На Маке я использую флэш-плагин версии 10,0,42,34, а на винде поставил самый свежий — версии 10.0.45.2. Несмотря на небольшую разницу в версиях <em>в самой свежей сборке не работает копирование в буфер при локальном просмотре страницы</em>. То есть пользователи Chrome, скачавшие страничку к себе на компьютер, попросту не смогли бы ничего скопировать. Похоже, Adobe решил окончательно закрутить гайки с буфером обмена.</p>
<p>После часа безуспешного гугления был придуман хак, который позволяет копировать данные без использования флэша. Суть его заключается в том, что нужно создать на странице блок со свойством <code>contentEditable="true"</code>, записать туда необходимую строчку через <code>innerHTML</code>, навести фокус (в этот момент браузер автоматически выделяет содержимое блока) и вызвать <code>document.execCommand('copy')</code>:</p>
<pre class="brush: html">
&lt;div id=&quot;copy-clip&quot; contenteditable=&quot;true&quot;&gt;&lt;/div&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
	function doCopy() {
		var obj = document.getElementById('copy-clip');
		obj.innerHTML = 'my new data';
		obj.focus();
		document.execCommand('copy')
	}
&lt;/script&gt;
</pre>
<p>По крайней мере при локальном запуске это работает, причём так, как надо.</p>
<h2>Анимация</h2>
<p>А теперь самое вкусное. Я давно присматривался к CSS трансформациям и анимациям, хотелось попробовать их на реальных проектах (подчёркиваю, речь идёт о <em>качественной реализации</em> реальных задач, а не о дурацких демках из разряда «смотрите, оно крутится!»). На данный момент CSS-трансформации поддерживают Safari (Webkit) и Firefox 3.6, CSS Transitions and Animations — только Webkit. Про Opera 10.5 молчу, ибо пользоваться этим абсолютно невозможно из-за чудовищных тормозов.</p>
<p>Что я могу сказать про CSS-анимации в Safari: реализованы они довольно плохо. Есть довольно много неочевидных проблем. Для своего сервиса мне удалось их кое-как исправить, но не знаю, насколько эти проблемы могут быть решены в более крупных проектах.</p>
<p>Вот что мне удалось узнать после работы с модными CSS-свойствами:</p>
<ol>
<li>Свойства вроде <code>-moz-box-shadow</code> очень сильно влияют на производительность. Уже на 7-ом блоке с файлом, у которого указано это свойство, начали появляться тормоза. Чем больше блоков — тем больше тормозит. Пользуйтесь CSS-декорациями вмеру.</li>
<li>Одна из главных проблем при использовании CSS Transitions — это не совсем очевидный способ указания блоку начальных координат. Например, вы указали, что у блока должны плавно изменяться свойства <code>left</code> и <code>top</code>: <code>-webkit-transition-property: left, top</code>. Для того, чтобы переда началом анимации переместить блок в некие начальные координаты, плавные переходы нужно отключить. Сделать это можно обнулив либо <code>-webkit-transition-property</code>, либо <code>-webkit-transition-duration</code>:
<pre class="brush: js">
$(elem).css({
	'-webkit-transition-duration': '0',
	top: 10,
	left: 20
});
</pre>
<p>	А вот для того, чтобы включить плавный переход, нужно сначала вернуть отключённое transition-свойство, а уже потом, через <code>setTimeout</code> выставить нужные координаты:</p>
<pre class="brush: js">
$(elem).css({
	'-webkit-transition-duration': '0.5s'
});

setTimeout(function(){
	$(elem).css({
		top: 100,
		left: 200
	});
}, 1);
	</pre>
</li>
<li>В Webkit-браузерах для анимации перемещения объектов лучше использовать связку CSS Transition/Animation и <code>translate(x, y)</code> из CSS Transforms. Трансформации получают аппаратное ускорение (по крайней мере на Маке), а анимации включают <em>субпиксельное сглаживание</em>, что даёт полее плавное и естественное движение:
<div class="image" style="margin:1.3em 0;"><img src="http://chikuyonok.ru/u/2010/02/subpixel.png" alt="subpixel" title="subpixel" width="438" height="200" class="alignnone size-full wp-image-609" /></div>
<p>	Простое указание дробных пикселей (например, так: <code>-webkit-transform: translate(5.5px, 1.6px);</code>), к сожалению, подобного эффекта не дают.
	</li>
<li>В следствие аппаратного ускорения CSS-преобразований, советую следить за размером блока. Если анимируете блок, ширина или высота которого больше 2000 пикселей (для айфона — 1024 пикселя), перед началом анимации блок «моргнёт». Судя по всему, это как-то связано с размером текстуры в OpenGL.</li>
<li>После того, как такая анимация отработала, блок с файлом начало в прямом смысле слова «колбасить»:<br />
	при наведении курсора на блок (после того, как отработала анимация появления) кнопки копирования соседних блоков начали прыгать куда-то вверх, а у надписей «поломалось» сглаживание:</p>
<div class="image" style="margin:1.3em 0;">
		<img src="http://chikuyonok.ru/u/2010/02/bug1.png" alt="bug1" title="bug1" width="516" height="294" class="alignnone size-full wp-image-610" />
	</div>
<p>	Помогло, как ни странно, указание контейнеру с блоками <code>position: relative;</code>, а текстовым надписям — <code>background: #fff;</code> (здравствуй, старина IE).
	</li>
<li>При наведении курсора на картинку она увеличивается/уменьшается с помощью CSS Animations (в Firefox — JS-анимация масштаба). Работает вроде неплохо, за исключением иногда «моргающей» тени и другого типа сглаживания у картинки. Но когда я указал <code>z-index</code> контейнеру с файлами, увидел вот такую картину:
<div class="image" style="margin:1.3em 0;">
		<img src="http://chikuyonok.ru/u/2010/02/bug2.png" alt="bug2" title="bug2" width="326" height="153" class="alignnone size-full wp-image-611" />
	</div>
<p>	Блоки произвольно исчезали и появлялись во время анимации. Помогло удаление <code>z-index</code> у контейнера. В моём случае это не критично, но подозреваю, в более крупных проектах с этим будут проблемы.
	</li>
</ol>
<h2>Заключение</h2>
<p>Ещё года 2—3 назад задачи вроде перекодирования файлов прямо в окне браузера считались невыполнимыми. Но уже сегодня можно смело говорить, что браузеры могут не только страницы показывать, но и выполнять вполе утилитарные задачи. Теперь необязательно изучать фреймворки вроде Qt, чтобы написать кросс-платформенное GUI-приложение. Вполне достаточно накопленных в веб-разработке знаний, а современные JavaScript-движки способны быстро переваривать довольно большие объемы данных. Кто знает, может, скоро начнётся эра повального портирования существующих десктопных инструментов на JavaScript <img src='http://chikuyonok.ru/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' /> Лично я получил огромное удовольствие не только от результата, но и от процесса создания этого небольшого сервиса. </p>
<p><strong>UPD:</strong> <a href="http://www.vanyamikhailov.ru/">Иван Михайлов</a> сделал <a href="http://www.vanyamikhailov.ru/projects/BrowserDataUrlApp/BrowserDataUrlApp.zip">Cocoa-оболочку</a> для этого сервиса, теперь его можно запускать как обычное Мак-приложение.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=608" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/s2fDX4gc8wg" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/02/browser-data-url/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/02/browser-data-url/</feedburner:origLink></item>
		<item>
		<title>Равномерный фон под текстом</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/nLv89vR6MT0/</link>
		<comments>http://chikuyonok.ru/2010/01/uniform-text-background/#comments</comments>
		<pubDate>Tue, 19 Jan 2010 13:39:55 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Верстка]]></category>

		<category><![CDATA[background]]></category>

		<category><![CDATA[css]]></category>

		<category><![CDATA[html]]></category>

		<category><![CDATA[outline]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=597</guid>
		<description><![CDATA[Одно из модных направлений веб-дизайна последних лет — оформление заголовков контрастным фоном. Например, вот так:

Простая, на первый взгляд, задача решается не так уж и просто:  первая же мысль «добавить padding» натыкается на то, что отступ добавляется исключительно в начале и в конце текста, игнорируя переносы:

Ближе всех к решению задачи когда-то подошёл akella, добавив border [...]]]></description>
			<content:encoded><![CDATA[<p>Одно из модных направлений веб-дизайна последних лет — оформление заголовков контрастным фоном. Например, вот так:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/example.png" alt="example" title="example" width="194" height="150" class="alignnone size-full wp-image-598" /></p>
<p>Простая, на первый взгляд, задача решается не так уж и просто:  первая же мысль «добавить padding» натыкается на то, что отступ добавляется исключительно в начале и в конце текста, игнорируя переносы:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/padding.png" alt="padding" title="padding" width="185" height="150" class="alignnone size-full wp-image-599" /></p>
<p>Ближе всех к решению задачи когда-то <a href="http://cssing.org.ua/2008/05/21/5-things-to-remembe/">подошёл akella</a>, добавив <code>border</code> у родительского элемента. Но проблема не решена на 100%: в конце первой строки (место, где переносятся слова) всё равно нет отступа. Решения остальных ребят, которые присылались <a href="http://twitter.com/chikuyonok">мне в твиттер</a>, грешили одной и той же проблемой: нужно точно указать место разрыва строк.</p>
<p>В простейшем случае, когда нужно добавить небольшой отступ, решение оказалось до боли простым. Есть одно «паразитное» CSS-свойство, от которого кодеры обычно избавляются — это свойство <code>outline</code>. Его особенность заключается в том, что во всех браузерах (по крайней мере в тех, в которых я проверял: Safari 4, Firefox 3.5, Opera 10, IE8) <em>контур outline точно повторяет границы текстового элемента</em>. Соответственно, эта строчка кода полностью решает нашу проблему:</p>
<pre class="brush: css">
span.uniform-bg {
	outline: red solid 0.3em;
}
</pre>
<p>Не обошлось без ложки дёгтя: в данном случае «плохим мальчиком» оказался Firefox. Во-первых, он иногда рисует контур с небольшим отступом от границ текста, а во-вторых — прочерчивает его между строками, перекрывая текст:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/firefox.png" alt="firefox" title="firefox" width="212" height="150" class="alignnone size-full wp-image-600" /></p>
<p>Первая проблема решается довольно просто: достаточно немного «втянуть» контур с помощью CSS-свойства <code>outline-offset</code> (либо <code>-moz-outline-offset</code>, эксклюзивно для Firefox), вторая — добавлением ещё одной обёртки с <code>position:relative</code>, чтобы поднять текст над контуром.</p>
<p>Итоговое решение <a href="http://chikuyonok.ru/u/uniform-bg.html">выглядит так</a>:</p>
<pre class="brush: html">
&lt;style type=&quot;text/css&quot;&gt;
	.uniform-bg {
		background:red;
		position:relative;
		outline: red solid 0.3em;
		-moz-outline-offset:-0.04em;
	}

	.uniform-bg span {
		position:relative;
	}
&lt;/style&gt;
&lt;h2&gt;&lt;span class=&quot;uniform-bg&quot;&gt;&lt;span&gt;Hello everyone&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
</pre>
<p>Оно не работает в IE6—7, в них не поддерживается свойство <code>outline</code>. Но, так как это исключительно декоративная задача, можно объединить её с решением от akella и получить почти идеальное решение.</p>
<p><strong>UPD:</strong> читатель Roman указал на баг в отрисовке фона в месте переноса строк в IE6—7. Немножко доработать напильником — и получится вполне приличное решение (обновил <a href="/u/uniform-bg.html">пример</a>).</p>
<p>Лично меня такой способ полностью не устраивает, так как есть ограничения на высоту строки (при больших значениях появляются проблемы между строками) и толщину контура (слишком толстый контур выглядит очень некрасиво). Предлагаю читателям подумать вместе со мной над более гибким решением, а также — совсем крутота — как такое же провернуть с фоном-картинкой.</p>
<h3>Второй способ <span style="font-size:0.6em;font-weight:normal;color:#ccc">(upd)</span></h3>
<p>Тот же читатель Roman <a href="#comment-1775">предложил</a> ещё один способ решения задачи. Он основан на смещении трех слоёв относительно друг друга: например, если отступ равен <code>x</code>, то второй слой смещается на <code>2x</code> вправо, а третий на <code>–x</code>, влево:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/method2.png" alt="method2" title="method2" width="213" height="320" class="alignnone size-full wp-image-605" /></p>
<p>Его можно немного упростить, убрав один слой и добавив левый <code>border</code> у контейнера. Способ более гибкий, чем с <code>outline</code> (который, как выяснилось, не очень дружит с Opera). На основе него я хотел сделать решение с фоновой картинкой, но столкнулся проблемой: вместе с переносом строк переносится и фон, то есть во второй строке фон начинается там же, где заканчивается в первой:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/bg-problem.png" alt="bg-problem" title="bg-problem" width="192" height="120" class="alignnone size-full wp-image-606" /></p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=597" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/nLv89vR6MT0" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/01/uniform-text-background/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/01/uniform-text-background/</feedburner:origLink></item>
		<item>
		<title>CSS-свойство content: копировать или нет?</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/egfZ39z_OxY/</link>
		<comments>http://chikuyonok.ru/2010/01/css-content/#comments</comments>
		<pubDate>Fri, 15 Jan 2010 18:01:51 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Верстка]]></category>

		<category><![CDATA[content]]></category>

		<category><![CDATA[css]]></category>

		<category><![CDATA[html]]></category>

		<category><![CDATA[верстка]]></category>

		<category><![CDATA[заметка для себя]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=585</guid>
		<description><![CDATA[Решил я для очередного своего проекта воспользоваться модным CSS-свойством content, чтобы немного облегчить страницу и сделать настройку внешнего вида более гибкой. Так как проект ориентирован на веб-разработчиков, об обратной совместимости со старыми браузерами (IE6—7) можно было не беспокоится. Но, к сожалению, меня ожидало большое разочарование от использования этого свойства. Нет, всё отображалось правильно, но конечному [...]]]></description>
			<content:encoded><![CDATA[<p>Решил я для очередного своего проекта воспользоваться модным CSS-свойством <code>content</code>, чтобы немного облегчить страницу и сделать настройку внешнего вида более гибкой. Так как проект ориентирован на веб-разработчиков, об обратной совместимости со старыми браузерами (IE6—7) можно было не беспокоится. Но, к сожалению, меня ожидало большое разочарование от использования этого свойства. Нет, всё отображалось правильно, но конечному пользователю было бы неудобно пользоваться результатом.</p>
<h3>Что такое CSS-свойство <code>content</code></h3>
<p>Кому ещё не удалось познакомиться с этим замечательным свойством, кратко расскажу о нём. Само название этого свойства говорит о том, что оно управляет неким содержимым. Согласно спецификации CSS2 это свойство применяется только к псевдо-элементам <code>:before</code> и <code>:after</code>, а с версии CSS3 станет доступно и для обычных элементов (небольшой реверанс в сторону Opera, которая это уже поддерживает).</p>
<p>С помощью свойства <code>content</code> мы можем через CSS задавать текстовое содержимое для (псевдо-)элементов. Классический пример применения этого свойства — вывод содержимого ссылки рядом с элементом в версии сайта для печати:</p>
<pre class="brush:html">
&lt;style type=&quot;text/css&quot;&gt;
	a:after {
		content: ' (' attr(href) ')';
	}
&lt;/style&gt;
&lt;p&gt;В нашем &lt;a href=&quot;/catalog/&quot;&gt;каталоге&lt;/a&gt; вы найдёте много чего интересного.&lt;/p&gt;
</pre>
<p>С помощью псевдо-элемента <code>:after</code> мы задали некое содержимое <em>после</em> тэга <code>&lt;a&gt;</code>. Этим содержимым является результат конкатенации строк и функции <code>attr()</code>, которая выводит содержимое атрибута  контекстного элемента.  Браузер, полностью поддерживающий CSS2, изобразит этот код примерно так:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/content-example.png" alt="content-example" title="content-example" width="458" height="46" class="alignnone size-full wp-image-587" /></p>
<p>Интересующиеся могут подробнее почитать об этом свойстве в интернетах, мы же перейдём к сути проблемы его использования.</p>
<h3>Проблема</h3>
<p>Так как для конечного пользователя результат работы свойства <code>content</code> выглядит как обычный текст, возникает вопрос: <em>а может ли пользователь выделить и скопировать такой текст</em>?</p>
<p>Проект, который я сейчас делаю, является инструментом для удобной работы с XML-документами. Для вывода раскрашенного дерева элементов я и решил воспользоваться вышеозначенным свойством:</p>
<pre class="brush: html">
&lt;style type=&quot;text/css&quot;&gt;
	.tag:before {
		content:'&lt;';
	}

	.tag:after {
		content:'&gt;';
	}

	.attr-value {
		quotes:'&quot;' '&quot;';
	}

	.attr-value:before {
		content:open-quote;
	}

	.attr-value:after {
		content:close-quote;
	}
&lt;/style&gt;
&lt;div class=&quot;tag&quot;&gt;div &lt;span class=&quot;attr-name&quot;&gt;class&lt;/span&gt;=&lt;q class=&quot;attr-value&quot;&gt;demo&lt;/q&gt;&lt;/div&gt;
</pre>
<p>Плюсы такого подхода: потребуется гораздо меньше элементов для раскраски тэгов (через псевдо-элементы <code>:before</code> и <code>:after</code> я могу задать произвольный цвет у угловых скобок). Для значений атрибутов я воспользовался тэгом <code>&lt;q&gt;</code> и CSS-свойством <code>quotes</code>, через которое определяются открывающие и закрывающие кавычки. Кому-то нравится использовать двойные кавычки в коде, кому-то — ординарные, при таком подходе их можно на лету поменять у всего документа. Как оказалось, выбор этого тэга и CSS-свойства стал важной частью эксперимента.</p>
<p>В браузерах этот код выглядит замечательно:</p>
<p><img src="http://chikuyonok.ru/u/2010/01/content-example2.png" alt="content-example2" title="content-example2" width="139" height="33" class="alignnone size-full wp-image-588" /></p>
<p>Но XML-документ должен не только красиво выглядеть, но и правильно работать: пользователь имеет право без проблем выделить и скопировать фрагмента документа, чтобы воспользоваться им, например, в своём любимом редакторе. И тут меня ожидало полное разочарование от использования свойства <code>content</code>. Я проверил результат копирования в своих браузерах — Safari 4, Opera 10, Firefox 3.5, IE8 — и получил вот такой результат:</p>
<ul class="result">
<li>Safari: <code>div class=demo</code></li>
<li>Opera: <code>&lt;div class=&quot;demo&quot;&gt;</code></li>
<li>Firefox: <code>div class="demo"</code></li>
<li>IE8: <code>&lt;div class=&quot;demo&quot;&gt;</code></li>
</ul>
<p>Как видите, все скопировали текст по-разному: Safari не скопировал <code>content</code>-данные в принципе, Opera и IE8 скопировали всё, а Firefox скопировал только кавычки вокруг атрибута.</p>
<p>Затем я решил вместо вместо элемента <code>&lt;q&gt;</code> написать обычный <code>&lt;span&gt;</code>, и получил вот такой результат:</p>
<ul class="result">
<li>Safari: <code>div class=demo</code></li>
<li>Opera: <code>&lt;div class=&quot;demo&quot;&gt;</code></li>
<li>Firefox: <code>div class=demo</code></li>
<li>IE8: <code>&lt;div class=&quot;demo&quot;&gt;</code></li>
</ul>
<p>Всё то же самое, но Firefox уже не скопировал кавычки.</p>
<h3>Выводы</h3>
<p>Из этого небольшого эксперимента я сделал для себя следующие выводы:</p>
<ul>
<li>Safari в принципе не понимает CSS-свойство <code>quotes</code>. То, что браузер отобразил кавычки вокруг <code>&lt;q&gt;</code> элемента — исключительно стандартная реакция на него. Кавычки нельзя будет поменять через свойство <code>quotes</code>, например, на ординарные — они так и останутся двойными. А если элемент переименовать в <code>&lt;span&gt;</code>, то и вовсе пропадут.</li>
<li>Firefox при копировании текста обращает внимание на название элемента: если это <code>&lt;q&gt;</code> — кавычки скопируются, для другого элемента получите пустоту.</li>
<li>Firefox <em>всегда</em> копирует двойные кавычки для тэга <code>&lt;q&gt;</code>, даже если вы измените их на что-нибудь другое (на «ёлочки», например). То есть сделать трюк с управлением копирования символов у вас не получится. Либо двойные кавычки, либо ничего.</li>
<li>IE8 при копировании обращает внимание на тип элемента: например, если прописать тэгу <code>display: list-item</code>, то скопируется буллит (хотя на странице он не будет отображаться).</li>
</ul>
<p>В общем, выводы далеко не самые приятные. С помощью свойства <code>content</code> я не смогу сделать кроссбраузерное решение: и когда нужно копировать эти данные, и когда не нужно (например, нумерация строк в листинге кода). Как это часто бывает, радостные вопли неискушённых кодеров и красивые демонстрации маркетологов ломаются в момент «боевого» применения, когда нужно вкладывать смысл в свою работу, а не просто следовать модным тенденциям. Поэтому до сих пор приходится делать всё по-старинке.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=585" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/egfZ39z_OxY" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/01/css-content/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/01/css-content/</feedburner:origLink></item>
		<item>
		<title>Ищу помощника</title>
		<link>http://feedproxy.google.com/~r/chikuyonok/vikk/~3/-HUF75pglQU/</link>
		<comments>http://chikuyonok.ru/2010/01/vacancy/#comments</comments>
		<pubDate>Mon, 11 Jan 2010 12:45:54 +0000</pubDate>
		<dc:creator>Сергей Чикуенок</dc:creator>
		
		<category><![CDATA[Без рубрики]]></category>

		<category><![CDATA[вакансия]]></category>

		<guid isPermaLink="false">http://chikuyonok.ru/?p=580</guid>
		<description><![CDATA[Мне в компанию очень нужен помощник или помощница.
UPD: ваканся закрыта, но скоро мне ещё понадобятся сотрудники. Если интересно — пишите.
Исходные данные
Есть проект Аймобилко, где я являюсь техдиректором и лицом, всячески заинтересованным в повышении количества продаж и качества предоставляемых услуг. Сайт написан на Java (этим занимается отдельный человек), использует связку XML/XSL для генерации результата. На сайте [...]]]></description>
			<content:encoded><![CDATA[<p>Мне в компанию очень нужен помощник или помощница.</p>
<p><strong>UPD:</strong> ваканся закрыта, но скоро мне ещё понадобятся сотрудники. Если интересно — пишите.</p>
<h2>Исходные данные</h2>
<p>Есть проект <a href="http://www.imobilco.ru">Аймобилко</a>, где я являюсь техдиректором и лицом, всячески заинтересованным в повышении количества продаж и качества предоставляемых услуг. Сайт написан на Java (этим занимается отдельный человек), использует связку XML/XSL для генерации результата. На сайте много JS-скриптов (исходники весят около 1,5 МБ, без учёта подпроектов).</p>
<p>Моя основная цель — сделать удобный проект для продажи цифрового контента. Именно поэтому там нет мудацкой регистрации с капчей или обязательным подтверждением по электронной почте, а любую покупку можно совершить буквально в два клика без регистрации. Это как бы намекает, что мы не перекладываем программерские проблемы (вроде большого количества «левых» регистраций) на пользователя, а сами их успешно решаем, но на это требуется больше времени и сил.</p>
<p>В работе используются инструменты по следующим ключевым словам: Eclipse, Ant, XSL, XML, HTML, CSS, JavaScript, Python, SVN, Redmine, Mylyn.</p>
<h2>Что требуется</h2>
<p>Нужен человек <em>на полный рабочий день</em>, который будет верстать новые и дорабатывать старые макеты, делать из них XSL-шаблоны и прикручивать к сайту. Знание XSL очень желательно, но не обязательно; если надо — научу. Это минимум.</p>
<p>В дальнейшем нужно будет стать властелином длинных скриптов, обслуживающих ключевые компоненты проекта: авторизация, регистрация, покупка, корзина. Все они учитывают десятки вариантов поведения пользователя и реакции на них сервера, причём делают это с приятными анимациями.</p>
<h2>Что делать</h2>
<p>Если вам интересна эта вакансия, напишите мне на <a href="mailto:serge.che@gmail.com?subject=Вакансия">serge.che@gmail.com</a> письмо с кратким рассказом о себе, а также 3—4 ссылки на свои самые интересные работы. Хочу сразу предупредить, что:</p>
<ul>
<li>меня тянет блевать от словосочетаний вроде «валидная семантичная вёрстка», «пора переходить на html5» и прочих задротских умопомешательств;</li>
<li>за слова «это сделать невозможно» буду сразу бить по башке;</li>
<li>я до сих пор поддерживаю IE6 на необходимом минимуме: главное, чтобы всё работало и выглядело прилично, но можно без тенюшек и скруглённых уголков;</li>
<li>нужен человек, который будет участвовать в жизни проекта, а не делать работу за зарплату (которая по результатам собеседования).</li>
</ul>
<p>Офис находится в районе м. Павелецкая.</p>
 <img src="http://chikuyonok.ru/wp-content/plugins/feed-statistics.php?view=1&post_id=580" width="1" height="1" style="display: none;" /><img src="http://feeds.feedburner.com/~r/chikuyonok/vikk/~4/-HUF75pglQU" height="1" width="1"/>]]></content:encoded>
			<wfw:commentRss>http://chikuyonok.ru/2010/01/vacancy/feed/</wfw:commentRss>
		<feedburner:origLink>http://chikuyonok.ru/2010/01/vacancy/</feedburner:origLink></item>
	</channel>
</rss>
