Skip to content

Instantly share code, notes, and snippets.

@arestov
Created April 29, 2010 11:37
Show Gist options
  • Save arestov/383477 to your computer and use it in GitHub Desktop.
Save arestov/383477 to your computer and use it in GitHub Desktop.
/*!
* Rocon: library that creates rounded corners
* @author Sergey Chikuyonok ([email protected])
* @link http://code.google.com/p/rocon/
*/
/**
* Общие методы и свойства для rocon
* @author Sergey Chikuyonok ([email protected])
* @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
* @include "worker.js"
*/
var re_rule = /\.rc(\d+)\b/,
re_class = /\brc(\d+(?:\-\d+){0,3})(\-.+?)?\b/,
re_shape_flag = /\brc-shape\b/,
/** Базовый класс для создаваемых элементов */
base_class = 'rocon',
/** Префиск для создаваемых CSS-правил */
rule_prefix = base_class + '__',
/** Результат, возвращаемый в объект <code>rocon</code> */
result = {
/**
* Добавление/обновление уголков для динамически созданных элементов.
* Может принимать неограниченное количество элементов либо массивов
* элементов, у которых нужно обновить уголки
*/
update: function(){},
process: function(context) {
processRoundedElements(context);
}
},
/** @type {CSSStyleSheet} Таблица стилей для уголков */
corners_ss = null,
css_text = '',
/** Список функций, которые нужно выполнить при загрузке DOM-дерева */
dom_ready_list = [],
/** Загрузился ли DOM? */
is_ready = false,
/** Привязано ли событие, ожидающее загрузку DOM? */
readyBound = false,
userAgent = navigator.userAgent.toLowerCase(),
/** Тип и версия браузера пользователя. Взято с jQuery */
browser = {
version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
safari: /webkit/.test( userAgent ),
opera: /opera/.test( userAgent ),
msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
};
/** код не из основной ветки **/
window.rocon = result;
result.update = function(node) {
var elems = [];
elems.push(node);
if (elems.length) {
walkArray(elems, function(i, n){
worker.add(this.params, this.node);
});
worker.run();
}
}
/**
* Выполняет все функции, добавленные на событие onDomContentLoaded.
* Взято с jQuery
*/
function fireReady() {
//Make sure that the DOM is not already loaded
if (!is_ready) {
// Remember that the DOM is ready
is_ready = true;
// If there are functions bound, to execute
if ( dom_ready_list.length ) {
walkArray(dom_ready_list, function(){
this.call(document);
}, true);
// Reset the list of functions
dom_ready_list = null;
}
}
}
/**
* Добавляет слушателя на событие onDomContentLoaded
* @type {Function} fn Слушатель
*/
function addDomReady(fn) {
dom_ready_list.push(fn);
}
/**
* Проверка на наступление события onDomContentLoaded.
* Взято с jQuery
*/
function bindReady(){
if ( readyBound ) return;
readyBound = true;
if ( document.readyState === "complete" ) {
fireReady();
return
}
// Mozilla, Opera and webkit nightlies currently support this event
if ( document.addEventListener ) {
// Use the handy event callback
document.addEventListener( "DOMContentLoaded", function(){
document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
fireReady();
}, false );
// If IE event model is used
} else if ( document.attachEvent ) {
// ensure firing before onload,
// maybe late but safe also for iframes
document.attachEvent("onreadystatechange", function(){
if ( document.readyState === "complete" ) {
document.detachEvent( "onreadystatechange", arguments.callee );
fireReady();
}
});
// If IE and not an iframe
// continually check to see if the document is ready
if ( document.documentElement.doScroll && !window.frameElement ) (function(){
if ( is_ready ) return;
try {
// If IE is used, use the trick by Diego Perini
// http://javascript.nwbox.com/IEContentLoaded/
document.documentElement.doScroll("left");
} catch( error ) {
setTimeout( arguments.callee, 0 );
return;
}
// and execute any waiting functions
fireReady();
})();
}
}
/**
* Вспомогательная функция, которая пробегается по всем элементам массива
* <code>ar</code> и выполняет на каждом элементе его элементе функцию
* <code>fn</code>. <code>this</code> внутри этой функции указывает на
* элемент массива
* @param {Array} ar Массив, по которому нужно пробежаться
* @param {Function} fn Функция, которую нужно выполнить на каждом элементе массива
* @param {Boolean} forward Перебирать значения от начала массива (п умолчанию: с конца)
*/
function walkArray(ar, fn, forward) {
if (forward) {
for (var i = 0, len = ar.length; i < len; i++)
if (fn.call(ar[i], i, ar[i]) === false)
break;
} else {
for (var i = ar.length - 1; i >= 0; i--)
if (fn.call(ar[i], i, ar[i]) === false)
break;
}
}
/**
* Преобразует один массив элементов в другой с помощью функции callback.
* Взято в jQuery
* @param {Array} elems
* @param {Function} callback
* @return {Array}
*/
function mapArray(elems, callback) {
var ret = [];
// Go through the array, translating each of the items to their
// new value (or values).
for ( var i = 0, length = elems.length; i < length; i++ ) {
var value = callback( elems[ i ], i );
if ( value != null )
ret[ ret.length ] = value;
}
return ret.concat.apply( [], ret );
}
// TODO Добавить исключение при правильной работе border-radius
/**
* Преобразует цвет из RGB-предствления в hex
* @param {String} color
* @return {String}
*/
function convertColorToHex(color) {
var result;
function s(num) {
var n = parseInt(num, 10).toString(16);
return (n.length == 1) ? n + n : n;
}
function p(num) {
return s(Math.round(num * 2.55));
}
if (result = /rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/.exec(color))
return '#' + s(result[1]) + s(result[2]) + s(result[3]);
// Look for rgb(num%,num%,num%)
if (result = /rgb\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*\)/.exec(color))
return '#' + p(result[1]) + p(result[2]) + p(result[3]);
// Look for #a0b1c2
if (result = /#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/i.exec(color))
return '#' + result[1] + result[2] + result[3];
if (result = /#([a-f0-9])([a-f0-9])([a-f0-9])/i.exec(color))
return '#' + result[1] + result[1] + result[2] + result[2] + result[3] + result[3];
s = null;
p = null;
return color;
}
/**
* Создает HTML-элемент <code>name</code> с классом <code>class_name</code>
* @param {String} name Название элемента
* @param {String} class_name Класс элемента
* @return {Element}
*/
function createElement(name, class_name) {
var elem = document.createElement(name);
if (class_name) {
elem.className = class_name;
}
return elem;
}
/**
* Простая проверка наличия определенного класса у элемента
* @param {HTMLElement} elem
* @param {String} class_name
* @return {Boolean}
*/
function hasClass(elem, class_name) {
var re = new RegExp('\\b' + class_name + '\\b');
return elem.nodeType == 1 && re.test(elem.className || '');
}
/**
* Add new class to the element
* @param {String} class_name New class
* @param {HTMLElement} elem Element to add class
*/
function addClass(class_name, elem) {
elem.className += ' ' + class_name;
}
var toCamelCase = (function(){
var cache = {},
camel = function(str, p1){return p1.toUpperCase();},
re = /\-(\w)/g;
return function(name) {
if (!cache[name])
cache[name] = name.replace(re, camel);
return cache[name];
}
})();
/**
* Возвращает значение CSS-свойства <b>name</b> элемента <b>elem</b>
* @author John Resig (http://ejohn.org)
* @param {Element} elem Элемент, у которого нужно получить значение CSS-свойства
* @param {String|Array} name Название CSS-свойства
* @return {String|Object}
*/
function getStyle(elem, name) {
var cs, result = {}, n, name_camel, is_array = name instanceof Array;
var _name = is_array ? name : [name];
for (var i = 0; i < _name.length; i++) {
n = _name[i];
name_camel = toCamelCase(n);
// If the property exists in style[], then it's been set
// recently (and is current)
if (elem.style[name_camel]) {
result[n] = result[name_camel] = elem.style[name_camel];
}
//Otherwise, try to use IE's method
else if (browser.msie) {
result[n] = result[name_camel] = elem.currentStyle[name_camel];
}
// Or the W3C's method, if it exists
else if (document.defaultView && document.defaultView.getComputedStyle) {
if (!cs)
cs = document.defaultView.getComputedStyle(elem, "");
result[n] = result[name_camel] = cs && cs.getPropertyValue(n);
}
}
// walkArray(name instanceof Array ? name : [name], function(i, n){
//
// });
return is_array ? result : result[toCamelCase(name)];
}
/**
* Разворачивает краткую запись четырехзначного свойства в полную:<br>
* — a -&gt; a,a,a,a<br>
* — a_b -&gt; a,b,a,b<br>
* — a_b_с -&gt; a,b,с,b<br>
*
* @param {String} prop Значение, которое нужно раскрыть
* @return {Array} Массив с 4 значениями
*/
function expandProperty(prop) {
var chunks = (prop || '').split(/[\-_]+/);
switch (chunks.length) {
case 1:
return [chunks[0], chunks[0], chunks[0], chunks[0]];
case 2:
return [chunks[0], chunks[1], chunks[0], chunks[1]];
case 3:
return [chunks[0], chunks[1], chunks[2], chunks[1]];
case 4:
return chunks;
}
return null;
}
/**
* Возвращает цвет фона элемента
* @type {Function}
* @param {Element} elem Элемент, для которого нужно достать цвет фона
* @return {String}
*/
var getBg = (function() {
var session_elems = [],
default_color = '#ffffff';
/**
* Основной цикл с использованием кэширования
*/
function mainLoopCache(elem) {
var c;
do {
if (elem.nodeType != 1)
break;
if (elem.rocon_bg) { // цвет был найден ранее
return elem.rocon_bg;
} else { // цвет еще не найден
session_elems.push(elem);
c = getStyle(elem, 'background-color');
if (c != 'transparent' && c != 'rgba(0, 0, 0, 0)')
return convertColorToHex(c);
}
} while (elem = elem.parentNode);
return default_color;
}
/**
* Основной цикл без кэширования
*/
function mainLoopNoCache(elem) {
var c;
do {
if (elem.nodeType != 1)
break;
c = getStyle(elem, 'background-color');
if (c != 'transparent')
return convertColorToHex(c);
} while (elem = elem.parentNode);
return default_color;
}
return function(elem){
var cl = /* String */elem.className,
bg = null;
if (getBg.use_cache) {
session_elems = [];
bg = mainLoopCache(elem);
// закэшируем цвет фона у всех элементов, по которым проходились
walkArray(session_elems, function(){
this.rocon_bg = bg;
getBg.processed_elems.push(this);
});
session_elems = null;
} else {
bg = mainLoopNoCache(elem);
}
return bg;
};
})();
getBg.use_cache = true;
getBg.processed_elems = [];
var css_rules_cache = [];
/**
* Добавляет CSS-правило в стиль
* @param {String} selector CSS-селектор, для которого нужно добавить правила
* @param {String} rules CSS-правила
*/
function addRule(selector, rule) {
css_rules_cache.push([selector, rule]);
}
/**
* Создает новую таблицу стилей на странице, куда будут добавляться правила
* для описания скругленных уголков
* @return {CSSStyleSheet}
*/
function createStylesheet() {
if (!corners_ss) {
if (document.createStyleSheet) {
corners_ss = document.createStyleSheet();
} else {
var style = createElement('style');
document.getElementsByTagName('head')[0].appendChild(style);
corners_ss = style.sheet;
}
}
return corners_ss;
}
function applyCSS() {
if (css_rules_cache.length) {
var sheet = createStylesheet();
if (sheet.addRule) {
// msie way
for (var j = 0; j < css_rules_cache.length; j++) {
var r = css_rules_cache[j];
sheet.addRule(r[0], r[1]);
}
} else {
// W3C way
for (var j = 0; j < css_rules_cache.length; j++) {
var r = css_rules_cache[j];
sheet.insertRule(r[0] + '{' + r[1] + '}', j);
}
}
css_rules_cache.length = 0;
}
}
/**
* Возвращает массив элементов, которым нужно добавить скругленные уголки.
* Элементом массива является объект со свойствами <code>node</code>
* и <code>radius</code>
* @param {Element} [context] Откуда брать элементы
* @return {Array}
*/
function getElementsToProcess(context) {
var elems = [], m;
walkArray((context || document).getElementsByTagName('*'), function(){
if (m = re_class.exec(this.className || '')) {
var p = (m[2] || '');
elems.push({
node: this,
params: {
radius: expandProperty(m[1]),
shape: (p.indexOf('shape') != -1 ),
force: (p.indexOf('force') != -1 )
}
});
}
});
return elems;
}
/**
* Обрабатывает все элементы на странице, которым нужно добавить скругленные
* уголки
*/
function processRoundedElements(context){
createStylesheet();
var elems = getElementsToProcess(context);
if (elems.length) {
walkArray(elems, function(i, n){
worker.add(this.params, this.node);
});
worker.run();
}
}
/**
* Применяет уголки к элементам, переданным в массиве. В основном вызывается из
* <code>rocon.update()</code>
* @param {arguments} args Аргументы функции
* @param {Function} fn Функция, которую нужно выполнить на каждом элементе
*/
function applyCornersToArgs(args, fn) {
walkArray(args, function(){
walkArray((this instanceof Array) ? this : [this], fn);
});
}
/**
* Делает копию объекта
* @param {Object} obj
* @return {Object}
*/
function copyObj(obj) {
var result = {};
for (var p in obj)
if (obj.hasOwnProperty(p))
result[p] = obj[p];
return result;
}
/**
* Canvas adapter for drawing rounded corners
* @author Sergey Chikuyonok ([email protected])
* @link http://chikuyonok.ru
*
* @include "common.js"
* @include "/js-libs/canvas-doc.js"
*/
var canvas = (function(){
var composite_op = 'destination-out',
/** @type {Element} Canvas where all images are drawn */
cv,
/** @type {Element} Canvas for stroke */
stroke_cv,
/** @type {CanvasRenderingContext2D} Drawing context */
ctx,
/** @type {CanvasRenderingContext2D} Stroke drawing context */
stroke_ctx,
/** Type of returnerd object of <code>draw()</code> method (1 — image string, 2 — canvas element) */
return_type = 1;
// debug only
// document.body.appendChild(cv);
/**
* Prepare canvas for drawing: removes old paintings, restores original
* transform matrix, scales canvas
*
* @param {Number} params.radius Corner radius
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
* @param {String} params.type Corner type: tl, tr, bl, br
*/
function prepare(params) {
ctx.restore();
cv.width = params.width;
cv.height = params.height;
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.save();
}
/**
* Draw border shape (stroke) on <code>ctx</code>. Canvas must be already
* scaled to the size of corner.
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {String} params.left_color Left border color
* @param {Number} params.top Top border width, pixels
* @param {String} params.top_color Top border color
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
*/
function drawStroke(params) {
// create short-hand vars for better YUICompressor mungin
var radius = params.radius,
border_left = params.left,
border_top = params.top;
if (!(border_left + border_top))
// nothing to draw
return;
stroke_cv.width = params.width;
stroke_cv.height = params.height;
stroke_ctx.clearRect(0, 0, params.width, params.height);
if (border_top) {
stroke_ctx.fillStyle = params.top_color;
stroke_ctx.fillRect(radius, 0, cv.width, border_top);
}
if (border_left) {
stroke_ctx.fillStyle = params.left_color;
stroke_ctx.fillRect(0, radius, border_left, cv.height);
}
stroke_ctx.save();
if (params.top_color != params.left_color) {
var grd = ctx.createLinearGradient(0, border_top, 0, radius);
grd.addColorStop(0, border_top ? params.top_color : params.left_color);
grd.addColorStop(1, border_left ? params.left_color : params.top_color);
stroke_ctx.fillStyle = grd;
} else {
stroke_ctx.fillStyle = params.top_color;
}
// start drawing two circles to create border arc
ctx.save();
stroke_ctx.beginPath();
stroke_ctx.arc(radius, radius, radius, 0, Math.PI * 2, true);
stroke_ctx.fill();
ctx.restore();
// change composite operation so the next circle will be extracted from the previous one
stroke_ctx.globalCompositeOperation = composite_op;
stroke_ctx.translate(border_left, border_top);
if (radius) {
stroke_ctx.scale(Math.max(1 - border_left / radius, 0) || 0.01, Math.max(1 - border_top / radius, 0) || 0.01);
}
stroke_ctx.beginPath();
stroke_ctx.arc(radius, radius, radius, 0, Math.PI * 2, true);
stroke_ctx.fill();
stroke_ctx.restore();
// leave only one quarter of circle
stroke_ctx.clearRect(radius, border_top, radius, cv.height);
stroke_ctx.clearRect(border_left, radius, cv.width, cv.height);
ctx.drawImage(stroke_cv, 0, 0);
}
/**
* Rotate canvas for specified corner type
*
* @param {String} Corner type: tl, tr, bl, br
* @param {Number} radius Corner radius
*/
function rotate(params) {
switch (params.type) {
case 'tr': // top right corner
ctx.scale(-1, 1);
ctx.translate(-params.width, 0);
break;
case 'bl': // bottom left corner
ctx.scale(1, -1);
ctx.translate(0, -params.height);
break;
case 'br': // bottom right corner
ctx.scale(-1, -1);
ctx.translate(-params.width, -params.height);
break;
}
}
/**
* Draw background for corner image
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {Number} params.top Top border width, pixels
* @param {String} params.color Color of background
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
*/
function drawBackground(params) {
var radius = params.radius,
offset_x = 0,
offset_y = 0;
ctx.save();
ctx.fillStyle = params.color;
if (!params.use_shape) {
ctx.fillRect(0, 0, radius, radius);
if (params.left)
offset_x = .5;
if (params.top)
offset_y = .5;
ctx.globalCompositeOperation = composite_op;
} else {
if (params.left)
offset_x = 1;
if (params.top)
offset_y = 1;
}
ctx.translate(offset_x, offset_y);
ctx.beginPath();
ctx.fillStyle = params.color;
ctx.arc(radius, radius, radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.restore();
if (params.use_shape) {
ctx.fillStyle = params.color;
ctx.fillRect(radius, 0, cv.width, cv.height);
ctx.fillRect(0, radius, cv.width, cv.height);
}
}
return {
init: function() {
cv = createElement('canvas');
ctx = cv.getContext('2d');
cv.width = 10;
cv.height = 10;
stroke_cv = createElement('canvas');
stroke_ctx = stroke_cv.getContext('2d');
},
/**
* Draw rounded corner
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {String} params.left_color Left border color
* @param {Number} params.top Top border width, pixels
* @param {String} params.top_color Top border color
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
* @param {String} params.color Color of background
* @param {String} params.type Corner type: tl, tr, bl, br
*
* @return {String} Image in data:URL format
*/
draw: function(params) {
// TODO delete this
params.width = params.width || params.radius;
params.height = params.height || params.radius;
if (return_type == 2) {
cv = createElement('canvas', 'rocon-cn');
ctx = cv.getContext('2d');
}
prepare(params);
rotate(params);
drawBackground(params);
drawStroke(params);
return (return_type == 2) ? cv : cv.toDataURL();
},
returnType: function(type) {
if (typeof(type) != 'undefined')
return_type = type;
return return_type;
}
};
})();
/**
* VML adapter for drawing rounded corners
* @author Sergey Chikuyonok ([email protected])
* @link http://chikuyonok.ru
*
* @include "common.js"
*/
var vml = (function(){
// init VML (took from Raphaёl lib)
var ns = 'rc_vml',
doc = document,
vml_ns = '',
common_style = 'position:absolute;display:inline-block;',
is_inited = false;
/** Stroke drawing raw string */
var stroke_path = '<' + ns + ':shape ' + vml_ns + ' class="' + ns + ' ' + ns + '_stroke" fillcolor="%top_color%" style="' + common_style + 'width:%width%px;height:%height%px;%offset%;%rotation%" stroked="false" coordsize="%c_width% %c_height%" coordorigin="5 5" ' +
'path="m0,%c_height% l0,%radius% qy%radius%,0 l%c_width%,0 r0,%top% l%radius%,%top% qx%left%,%radius% l%left%,%c_height% l0,%c_height% e x">' +
'<' + ns + ':fill ' + vml_ns + ' class="' + ns + '" color="%left_color%" color2="%top_color%" type="gradient" colors="%grd_start%% %grd_color_start%, %grd_end%% %grd_color_end%"/>' +
'</' + ns + ':shape>',
/** Fill drawing raw string */
fill_path = '<' + ns + ':shape ' + vml_ns + ' class="' + ns + '" fillcolor="%color%" stroked="false" style="' + common_style + 'width:%width%px;height:%height%px;%rotation%;%offset%" coordsize="%c_width% %c_height%" coordorigin="5 5" ' +
'path="m%offset_x%,%c_height% l%offset_x%,%radius% qy%radius%,%offset_y% l%c_width%,%offset_y% l%c_width%,0 %shape% l0,%c_height% e x"></' + ns + ':shape>',
/** All numeric properies of <code>params</code> object */
num_properties = ['top', 'left', 'radius', 'c_width', 'c_height'],
/** VML coordinate space multiplier for crispy edges of shapes */
multiplier = 10;
/**
* Replaces entities (like %entity%) in string with values passed in
* <code>params</code>: {entity1: value, entity2: value, ...}
* @param {String} str Raw string
* @param {Object} params Entity values
* @return {String} New string with replaced entities
*/
function replaceEntities(str, params) {
// merge objects
var _params = {};
for (var i = 1; i < arguments.length; i++) {
var obj = arguments[i];
if (obj)
for (var a in obj) if (obj.hasOwnProperty(a))
_params[a] = obj[a];
}
// replace entities
return str.replace(/\%(\w+)\%/g, function(s, p1){
return _params[p1];
});
}
/**
* Create border shape (stroke).
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {String} params.left_color Left border color
* @param {Number} params.top Top border width, pixels
* @param {String} params.top_color Top border color
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
*
* @return {String} String containing SVG tags
*/
function drawStroke(params) {
var extra_params = {
grd_start: (1 - params.top / params.radius) * 100,
grd_color_start: params.top_color,
grd_end: (1 - params.radius / params.height) * 100,
grd_color_end: params.left_color
};
if ((params.type || '').charAt(0) == 'b') {
// swap colors
var tmp_color = params.top_color;
params.top_color = params.left_color;
params.left_color = tmp_color;
extra_params.grd_start = 100 - extra_params.grd_start;
extra_params.grd_end = 100 - extra_params.grd_end;
}
return (params.top + params.left) ?
// replaceEntities(stroke_gradient, params, extra_params) +
replaceEntities(stroke_path, params, extra_params)
: '';
}
/**
* Draw background for corner image
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {Number} params.top Top border width, pixels
* @param {String} params.color Color of background
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
*/
function drawBackground(params) {
var t = {
shape: params.use_shape ? 'r0,' + params.c_height : 'l0,0'
};
return replaceEntities(fill_path, params, t);
}
/**
* Returns transformation params for different types of corners
* @param {String} Corner type: tl, tr, bl, br
* @param {Number} radius Corner radius
*/
function rotate(type, radius) {
switch (type) {
case 'tr': // top right corner
return 'flip:x;margin-left:-1px;';
case 'bl': // bottom left corner
return 'flip:y;margin-top:-1px';
case 'br': // bottom right corner
return 'rotation:180;margin-left:-1px;margin-top:-1px;';
default:
return '';
}
}
return {
init: function() {
if (!is_inited) {
doc.createStyleSheet().addRule("." + ns, "behavior:url(#default#VML)");
try {
!doc.namespaces[ns] && doc.namespaces.add(ns, "urn:schemas-microsoft-com:vml");
} catch (e) {
vml_ns = 'xmlns="urn:schemas-microsoft.com:vml"';
}
}
is_inited = true;
},
/**
* Draw rounded corner
*
* @param {Number} params.radius Corner radius
* @param {Number} params.left Left border width, pixels
* @param {String} params.left_color Left border color
* @param {Number} params.top Top border width, pixels
* @param {String} params.top_color Top border color
* @param {Boolean} params.use_shape Flag, indicating that shape, not counter-shape, must be drawn
* @param {String} params.color Color of background
* @param {String} params.type Corner type: tl, tr, bl, br
*
* @return {String} Image in data:URL format
*/
draw: function(params) {
params = copyObj(params);
params.width = params.c_width = params.width || params.radius;
params.height = params.c_height = params.height || params.radius;
params.offset = '';
if (params.type.charAt(0) == 'b')
params.offset += 'top: 0px;';
if (params.type.charAt(1) == 'r')
params.offset += 'left: 0px;';
else if (params.use_shape) {
params.offset += 'left: ' + Math.max(0, params.radius - params.left - params.opposite.left) + 'px;';
}
params.offset_x = (!params.use_shape && params.left) ? 0.5 * multiplier : 0;
params.offset_y = (!params.use_shape && params.top) ? 0.5 * multiplier : 0;
params.rotation = rotate(params.type);
// multiply all numeric values
walkArray(num_properties, function(i, n){
params[n] *= multiplier;
});
var data = /* '<v:group class="vml" ' + replaceEntities(group_data, params) + '>' + */
drawBackground(params) +
drawStroke(params) +
/* '</v:group>' */
'';
// if (params.type == 'tl')
// alert(data);
// var elem = createElement('div', 'rocon-cn');
// elem.innerHTML = data;
return data;
},
returnType: function() {
return 2;
}
};
})();
/**
* Main worker and caching mechanism used for storing and applying of drawn
* corners to the elements
*
* @author Sergey Chikuyonok ([email protected])
* @link http://chikuyonok.ru
* @include "common.js"
* @include "canvas.js"
*/
var worker = (function(){
/** @type {canvas} Drawing adapter (canvas, SVG, VML) */
var adapter = null,
/** Cached corners */
cache = {},
/** Number of keys in cache */
key_count = 0,
/** Native browser properties used to draw browser corners */
native_props = [],
queue = [],
/** Corner type index in array */
type_pos = {tl: 0, tr: 1, br: 2, bl: 3},
/** CSS properties for corner types */
corner_type_map = {
tl: {
left: 'border-left-width',
left_color: 'border-left-color',
top: 'border-top-width',
top_color: 'border-top-color'
},
tr: {
left: 'border-right-width',
left_color: 'border-right-color',
top: 'border-top-width',
top_color: 'border-top-color'
},
br: {
left: 'border-right-width',
left_color: 'border-right-color',
top: 'border-bottom-width',
top_color: 'border-bottom-color'
},
bl: {
left: 'border-left-width',
left_color: 'border-left-color',
top: 'border-bottom-width',
top_color: 'border-bottom-color'
}
},
opposite_corners = {
tl: 'tr',
tr: 'tl',
bl: 'br',
br: 'bl'
},
css_props = [];
//fill-up CSS properties
walkArray(['left', 'top', 'right', 'bottom'], function(i, n){
var prefix = 'border-' + n;
css_props.push(prefix + '-color', prefix + '-width');
});
/**
* Check if we need to add extra width for image (for shape-type corners)
* @param {Object} params
* @param {String} params
* @return {Boolean}
*/
function needExtraWidth(params, type) {
return params.shape && (type || 'tl').charAt(1) == 'l';
}
/**
* Returns the smallest numbers
* @return {Number}
*/
function min() {
var args = mapArray(arguments, function(n, i){
return parseInt(n) || 0;
});
if (args.length == 2) {
return args[0] < args[1] ? args[0] : args[1];
} else {
var result = args[0];
for (var i = 1; i < args.length; i++) {
if (args[i] < result)
result = args[i];
}
return result;
}
}
/**
* Returns the highest numbers
* @return {Number}
*/
function max() {
var args = mapArray(arguments, function(n, i){
return parseInt(n) || 0;
});
if (args.length == 2)
return args[0] > args[1] ? args[0] : args[1];
else {
var result = args[0];
for (var i = 1; i < args.length; i++) {
if (args[i] > result)
result = args[i];
}
return result;
}
}
/**
* Returns key used to store image data
* @param {Array|Object} params Corner parameters
* @return {String}
*/
function cacheKey(params) {
var result = [];
if (params instanceof Array)
for (var i = 0; i < params.length; i++)
result.push(params[i]);
else
for (var a in params) if (params.hasOwnProperty(a))
result.push(a + ':' + params[a]);
return result.join(';');
}
/**
* Add value to cache
* @param {String} key Cache key
* @param {String} [value] Cache value. If not set, internal class name will be used
* @return {String} Internal class name
*/
function addToCache(key, value) {
if (getCachedValue(key))
return getCachedValue(key);
key_count++;
cache[key] = value || rule_prefix + key_count;
return rule_prefix + key_count;
}
/**
* Returns cached value
* @param {String} key cache key
* @return {String|null}
*/
function getCachedValue(key) {
return cache[key];
}
/**
* Add native CSS corners to the element
* @param {Array} radius Set of border radius values
* @param {HTMLElement} elem Element to add corners
*/
function addNativeCorners(radius, elem) {
var key = cacheKey(radius),
css_class = getCachedValue(key),
css_rules = '';
if (!css_class) {
css_class = addToCache(key);
walkArray(native_props, function(i, n){
css_rules += n + ':' + radius[i] + 'px;';
});
addRule('.' + css_class, css_rules);
}
addClass(css_class, elem);
}
/**
* Creates, if necessary, new corner elements and returns them
* @param {HTMLElement} elem Container element
* @return {Object}
*/
function createCornerElements(elem) {
var result = {},
found = 0,
re = new RegExp('\\b' + base_class + '-([tblr]{2})\\b', 'i'),
m;
walkArray(elem.childNodes, function(i, /* HTMLElement */ n){
if (m = re.exec(n.className)) {
found++;
result[m[1]] = n;
}
});
if (!found) {
walkArray(['tl', 'tr', 'bl', 'br'], function(i, n) {
var e = createElement('span', base_class + ' ' + base_class +'-' + n);
elem.appendChild(e);
result[n] = e;
});
}
return result;
}
/**
* Add element to drawing queue
* @param {Object} params Parent element params
* @param {HTMLElement} parent Parent element
* @param {Object} corners Corner elements
*/
function addToQueue(params, parent, corners) {
queue.push([params, parent, corners]);
}
/**
* Removes old internal classes from element
* @param {HTMLElement} elem
* @param {String} new_class New class name to add (optional)
* @return {HTMLElement}
*/
function cleanUp(elem, new_class) {
elem.className = elem.className.replace(/\s*rocon__\d+/, '');
if (new_class)
elem.className += ' ' + new_class;
return elem;
}
/**
* Returns width that must be set for specified corner type
* @param {Object} params Corner's container element params
* @param {String} type Corner type (tl, tr, bl, br)
* @return {Number}
*/
function getCornerWidth(params, type) {
return needExtraWidth(params, type) ? 2000 : params.radius[type_pos[type]];
}
/**
* Returns height that must be set for specified corner type
* @param {Object} params Corner's container element params
* @param {String} type Corner type (tl, tr, bl, br)
* @return {Number}
*/
function getCornerHeight(params, type) {
var cur_pos = type_pos[type],
op_pos = type_pos[ opposite_corners[type] ];
return Math.max(params.radius[cur_pos], params.radius[op_pos]);
}
/**
* Draw corners using adapter
* @param {Object} params Parent element params
* @param {HTMLElement} parent Parent element
* @param {HTMLElement[]} corners Corner elements
* @param {Object} style Cached CSS properties
*/
function drawCorners(params, parent, corners, style) {
// gather all params for all corners
var all_params = {}, css_map, elem, style;
for (var corner_type in corners) if (corners.hasOwnProperty(corner_type)) {
css_map = corner_type_map[corner_type];
/** @type {HTMLElement} */
elem = corners[corner_type];
style = style || getStyle(parent, css_props);
// debugger;
// gather params for current corner
var r = parseInt(params.radius[ type_pos[corner_type] ]);
all_params[corner_type] = {
radius: r,
top: min(style[css_map.top], r),
real_top: parseInt(style[css_map.top]) || 0,
top_color: convertColorToHex(style[css_map.top_color]),
left: min(style[css_map.left], r),
real_left: parseInt(style[css_map.left]) || 0,
left_color: convertColorToHex(style[css_map.left_color]),
color: getBg(params.shape ? parent : parent.parentNode),
use_shape: params.shape,
type: corner_type,
width: max(getCornerWidth(params, corner_type), parseInt(style[css_map.left]) || 0),
height: getCornerHeight(params, corner_type)
};
}
// create corners
for (var corner_type in corners) {
/** @type {HTMLElement} */
elem = corners[corner_type];
var cparams = all_params[corner_type];
// calculate X and Y offsets
cparams.offset_x = -cparams.real_left;
cparams.offset_y = -(params.shape ? max(cparams.height, cparams.radius, cparams.real_top) : cparams.real_top);
var op_corner = opposite_corners[corner_type],
key = cacheKey(cparams);
cparams.opposite = all_params[op_corner];
if (needExtraWidth(params, corner_type)) {
//add extra properties for cache key
key += '--' + (cparams.left + all_params[op_corner].left) +
':' + params.radius[ type_pos[op_corner] ];
}
var css_class = getCachedValue(key);
if (!css_class) {
// image is not yet created
css_class = addToCache(key);
var css_rules = 'height:' + cparams.height + 'px;';
if (adapter.returnType() != 2) // adapter draws image in data:url format
css_rules += 'background-image:url(' + adapter.draw(cparams) + ');';
var offset_top = -(params.shape ? Math.max(cparams.height, cparams.radius) : cparams.top);
css_rules += ( corner_type.charAt(0) == 't' ? 'top:' : 'bottom:' ) + cparams.offset_y + 'px;';
css_rules += ( corner_type.charAt(1) == 'l' ? 'left:' : 'right:' ) + cparams.offset_x + 'px;';
if (needExtraWidth(params, corner_type)) {
var size_delta = max(cparams.left + cparams.opposite.left - cparams.radius, 0);
css_rules += 'width:100%;' +
'background-position:' + Math.max(0, cparams.radius - cparams.left - cparams.opposite.left) + 'px 0px;' +
'left:' + min(-cparams.radius + cparams.opposite.left - size_delta, cparams.offset_x) + 'px;' +
'padding-right:' + (size_delta + max(cparams.real_left - cparams.radius, 0)) + 'px';
// css_rules += 'width:100%;' +
// 'padding-left:' + (cparams.real_left + all_params[op_corner].left) + 'px;' +
// 'clip:rect(auto,auto,auto,' + params.radius[ type_pos[op_corner] ] + 'px);' +
// 'background-position:top right;';
} else {
css_rules += 'width:' + cparams.width + 'px;';
}
addRule('.' + css_class, css_rules);
}
cleanUp(elem, css_class);
if (adapter.returnType() == 2) {
var corner = adapter.draw(cparams);
if (typeof(corner) == 'string') {
elem.innerHTML = corner;
} else {
elem.innerHTML = '';
elem.appendChild(corner);
}
}
}
cleanUp(parent);
if (!hasClass(parent, 'rocon-init'))
addClass('rocon-init', parent);
if (params.shape)
adjustBox(params, parent);
}
/**
* Add corners drawn by <code>adapter</code> to the element
* @param {Array} params.radius Array of radius values
* @param {Boolean} params.shape Use shape instead of counter-shape
* @param {HTMLElement} elem Element to apply corners
* @param {Boolean} [is_immediate] Draw corners immediately instead adding element to the queue (may cause performance issues in Opera)
*/
function addAdapterCorners(params, elem, is_immediate) {
/*
* Due to stupid Opera bug (http://chikuyonok.ru/playground/opera-bug/)
* we need to split this single process into two loops:
* 1. Add corner elements to the container
* 2. Draw images and apply them to the corners
*/
var corners = createCornerElements(elem);
if (is_immediate)
drawCorners(params, elem, corners);
else
addToQueue(params, elem, corners);
}
/**
* Main processing function
* @param {Array} params.radius Array of radius values
* @param {Boolean} params.shape Use shape instead of counter-shape
* @param {Boolean} params.force {Boolean} Force usage of drawing adapter instead of native properties
* @param {HTMLElement} elem Element to apply corners
* @param {Boolean} [is_immediate] Draw corners immediately instead adding element to the queue (may cause performance issues in Opera)
*/
function process(params, elem, is_immediate) {
if (native_props.length && !params.force) {
addNativeCorners(params.radius, elem);
} else {
addAdapterCorners(params, elem, is_immediate);
}
}
/**
* Корректирует CSS-свойства элемента для правильного рисования уголков в виде
* формы
* @param {Object} params Corner params
* @param {HTMLElement} elem Элемент, который нужно подкорректировать
*/
function adjustBox(params, elem) {
var pt = 'padding-top',
pb = 'padding-bottom',
mt = 'margin-top',
mb = 'margin-bottom',
btw = 'border-top-width',
bbw = 'border-bottom-width',
getProp = function(prop) {
return parseInt(elem_styles[prop], 10) || 0;
},
addCSSRule = function(property, value) {
return property + ':' + value + 'px !important;';
},
elem_styles = getStyle(elem, [pt, pb, mt, mb, btw, bbw]),
offset_top = Math.max(params.radius[ type_pos['tl'] ], params.radius[ type_pos['tr'] ]),
offset_bottom = Math.max(params.radius[ type_pos['bl'] ], params.radius[ type_pos['br'] ]),
offset_border_top = Math.min(offset_top, getProp(btw)),
offset_border_bottom = Math.min(offset_bottom, getProp(bbw));
/*
* Используем форму, поэтому у блока снижаем верхние и нижние
* бордюры, а также на величину радиуса снижаем верхний
* и нижний паддинг
*/
var padding_top = Math.max(getProp(pt) - offset_top + offset_border_top, 0),
padding_bottom = Math.max(getProp(pb) - offset_bottom + offset_border_bottom, 0),
margin_top = getProp(mt) + offset_top,
margin_bottom = getProp(mb) + offset_bottom,
border_top_width = getProp(btw) - offset_border_top,
border_bottom_width = getProp(bbw) - offset_border_bottom,
key = cacheKey([padding_top, padding_bottom, margin_top, margin_bottom, border_top_width, border_bottom_width]),
class_name = getCachedValue(key);
if (!class_name) {
class_name = addToCache(key);
addRule('.' + class_name,
addCSSRule(btw, border_top_width) +
addCSSRule(bbw, border_bottom_width) +
addCSSRule(pt, padding_top) +
addCSSRule(pb, padding_bottom) +
addCSSRule(mt, margin_top) +
addCSSRule(mb, margin_bottom));
}
elem.className += ' ' + class_name;
}
return {
/**
* Set drawing adapter
*/
setAdapter: function(obj) {
adapter = obj;
if ('init' in adapter)
adapter.init();
},
/**
* Set native CSS properties for drawing rounded borders
* @param {String} tl Top-left corner
* @param {String} tr Top-right corner
* @param {String} br Bottom-right corner
* @param {String} bl Bottom-left corner
*/
nativeProperties: function(tl, tr, br, bl) {
native_props = [tl, tr, br, bl];
},
/**
* Enqueue element for corners addition
* @param {Array} params.radius Array of radius values
* @param {Boolean} params.shape Use shape instead of counter-shape
* @param {Boolean} params.force {Boolean} Force usage of drawing adapter instead of native properties
* @param {HTMLElement} elem Element to apply corners
*/
add: function(params, elem) {
process(params, elem);
},
/**
* Add corners to enqueued elements
*/
run: function() {
var start = (new Date).getTime();
walkArray(queue, function(i, n){
drawCorners(n[0], n[1], n[2]);
});
var stop1 = (new Date).getTime();
applyCSS();
var stop2 = (new Date).getTime();
// alert('Draw: ' + (stop1 - start) + ' ms, CSS: ' + (stop2 - stop1) + ' ms');
},
/**
* Add corners to the element
*/
apply: function(params, elem) {
process(params, elem, true);
}
}
})();
/**
* @author Sergey Chikuyonok ([email protected])
* @link http://chikuyonok.ru
* @include "common.js"
* @include "worker.js"
* @include "canvas.js"
* @include "vml.js"
*/
addDomReady(processRoundedElements);
// после того, как добавили уголки, необходимо очистить кэш фона,
// иначе будут проблемы с динамическим обновлением блоков
addDomReady(function(){
walkArray(getBg.processed_elems, function(){
this.removeAttribute('rocon_bg');
});
getBg.use_cache = false;
});
// set browser-specific properties
if (browser.msie) {
worker.setAdapter(vml);
addDomReady(function(){
// corners_ss.cssText += css_text;
// css_text = '';
// addRule = corners_ss.addRule;
});
} else {
worker.setAdapter(canvas);
//canvas.returnType(2);
if (browser.safari) {
canvas.returnType(1);
// worker.nativeProperties(
// '-webkit-border-top-left-radius',
// '-webkit-border-top-right-radius',
// '-webkit-border-bottom-right-radius',
// '-webkit-border-bottom-left-radius'
// );
} else if (browser.mozilla) {
// worker.nativeProperties(
// '-moz-border-radius-topleft',
// '-moz-border-radius-topright',
// '-moz-border-radius-bottomright',
// '-moz-border-radius-bottomleft'
// );
}
//else if (browser.opera)
// worker.setAdapter(svg);
}
bindReady();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment