Canvas "на коленке"

12.05.2013 15:25
0
Небольшая библиотека для работы с canvas.
Подходит для быстрого прототипирования каких-либо графических эффектов или алгоритмов.

Пожалуйста, не используйте ее для продакшена, и вообще для чего-либо серьезного - для этого есть много готовых, быстрых и хорошо поддерживаемых библиотек.

Класс CANVAS

function CANVAS( IdOrObj, w, h ) {
	this.c = null;
 
   	if ( IdOrObj && typeof( IdOrObj ) == "object" ) this.cDOM = IdOrObj;
  	else {
   		this.cDOM = document.createElement( "CANVAS" );
   		if ( IdOrObj ) this.cDOM.setAttribute( "id", IdOrObj );
   	}
 
	if ( !w || !h ) {
		w = Number( this.cDOM.getAttribute( "width" ) );
		h = Number( this.cDOM.getAttribute( "height" ) );
	}
 
	if ( !w || !h || w < 32 || h < 32 ) {
		w = 32;
		h = 32;
	}
 
	if ( Number( this.cDOM.getAttribute( "width" ) ) != w || Number( this.cDOM.getAttribute( "height" ) != h ) ) {
		this.cDOM.setAttribute( "width", w );
		this.cDOM.setAttribute( "height", h );
	}
 
   	this.w = w;
   	this.h = h;
 
	this.maxAddr = this.w * this.h * this.bpp - 1;
	this.c = this.cDOM.getContext( "2d" );
 
	if ( !this.c ) {
		this.cDOM = null;
		this.c = null;
	}
}
 
CANVAS.prototype = {
	cDOM: null,
	c: null,
 
	w: 0,
	h: 0,
 
	setSize: function( w, h ) {
		this.w = w;
		this.h = h;
 
		this.cDOM.setAttribute( "width", w );
		this.cDOM.setAttribute( "height", h );
 
		this.maxAddr = this.w * this.h * this.bpp - 1;
		this.c = this.cDOM.getContext( "2d" );
	},
 
	get: function() {
		return {
			c: this.c,
			dom: this.cDOM,
			w: this.w,
			h: this.h
		};
	},
 
	clear: function() {
		this.c.save();
		this.c.setTransform( 1, 0, 0, 1, 0, 0 );
		this.c.clearRect( 0, 0, this.w, this.h );
		this.c.restore();
 
		//this.c.clearRect( 0, 0, this.w, this.h );
	},
 
	requestRedraw: function( renderer ) {
			if ( window.requestAnimationFrame ) return window.requestAnimationFrame( renderer );
			else if ( window.webkitRequestAnimationFrame ) return window.webkitRequestAnimationFrame( renderer );
			else if ( window.mozRequestAnimationFrame ) return window.mozRequestAnimationFrame( renderer );
			else if ( window.oRequestAnimationFrame ) return window.oRequestAnimationFrame( renderer );
			else if ( window.msRequestAnimationFrame ) return window.msRequestAnimationFrame( renderer );
			else return window.setTimeout( function() { renderer }, 1000 / 60 );
	},
 
	cancelRedraw: function( id ) {
			if ( !id ) return;
			if ( window.cancelAnimationFrame ) window.cancelAnimationFrame( id );
			else if ( window.webkitCancelAnimationFrame ) window.webkitCancelRequestAnimationFrame( id );
			else if ( window.mozCancelAnimationFrame ) window.mozCancelAnimationFrame( id );
			else if ( window.oCancelAnimationFrame ) window.oCancelAnimationFrame( id );
			else if ( window.msCancelAnimationFrame ) window.msCancelAnimationFrame( id );
			else window.clearTimeout( id );
	}
};
 
function DBUF( canvas, w, h ) {
	this.reset();
	this.setup( canvas, w, h );
};
 
DBUF.prototype = {
	canvas: null,
	buf: null,
 
	w: 0,
	h: 0,
	bpp: 4,
 
	maxAddr: 0,
 
	reset: function() {
		this.canvas = null;
		this.buf = null;
		this.w = this.h = this.maxAddr = 0;
	},
 
	setup: function( canvas, w, h ) {
   		if ( canvas instanceof( CANVAS ) ) this.canvas = canvas;
   		else {
   			if ( typeof( canvas ) == "string" && document.getElementById( canvas ) ) this.canvas = new CANVAS( document.getElementById( canvas ), w, h );
   			else this.canvas = new CANVAS( canvas, w, h );
   		}
 
		this.w = w ? w : this.canvas.w;
   		this.h = h ? h : this.canvas.h;
 
   		this.create();
 
		this.maxAddr = this.w * this.h * this.bpp - 1;
	},
 
	create: function() {
		if ( !this.canvas ) return;
		this.buf = this.canvas.c.createImageData( this.w, this.h );
	},
 
	getCanvas: function() {
		return this.canvas;
	},
 
	setSize: function( w, h ) {
		this.canvas.setSize( w, h );
		var t = this.canvas.get();
		this.w = t.w;
		this.h = t.h;
		this.buf = t.c.createImageData( t.w, t.h );
		this.maxAddr = this.w * this.h * this.bpp - 1;
	},
 
	get: function( merge ) {
		if ( !this.canvas ) return;
 
		if ( merge ) {
			var b = this.canvas.c.getImageData( 0, 0, this.w, this.h );
			for ( var i = 0; i < this.maxAddr; i+= this.bpp ) {
 
				var sa = b.data[i + 3] / 255;
				var nsa = 1 - sa;
				var sr = b.data[i] * sa;
				var sg = b.data[i + 1] * sa;
				var sb = b.data[i + 2] * sa;
 
				var da = this.buf.data[i + 3] * nsa / 255;
				var dr = this.buf.data[i] * nsa;
				var dg = this.buf.data[i + 1] * nsa;
				var db = this.buf.data[i + 2] * nsa;
 
				this.buf.data[i]		= ( sr + dr ) & 255;
				this.buf.data[i + 1]	= ( sg + dg ) & 255;
				this.buf.data[i + 2]	= ( sb + db ) & 255;
				this.buf.data[i + 3]	= ( ( sa + da ) * 255 ) & 255;
			}
 
			b = undefined;
		}
		else this.buf = this.canvas.c.getImageData( 0, 0, this.w, this.h );
	},
 
	put: function( x, y ) {
		if ( !this.canvas ) return;
 
		if ( !x || !y ) {
			x = 0;
			y = 0;
		}
		this.canvas.c.putImageData( this.buf, x, y );
	},
 
	clear: function( r, g, b, a ) {
		if ( r == undefined ) r = 0;
		if ( g == undefined ) g = 0;
		if ( b == undefined ) b = 0;
		if ( a == undefined ) a = 255;
 
		for ( var i = 0; i <= this.maxAddr; i += 4 ) {
			this.buf.data[i] = r;
			this.buf.data[i + 1] = g;
			this.buf.data[i + 2] = b;
			this.buf.data[i + 3] = a;
		}
	},
 
	requestRedraw: function( renderer ) {
		return this.canvas.requestRedraw( renderer );
	},
 
	cancelRedraw: function( id ) {
		this.canvas.cancelRedraw( id );
	},
 
	getPixel: function( x, y ) {
		return this.getPixelA( ( y * this.w + x ) * this.bpp );
	},
 
	getPixelA: function( addr ) {
		if ( addr > this.maxAddr ) return null;
 
		return {
			"r": this.buf.data[addr],
			"g": this.buf.data[addr + 1],
			"b": this.buf.data[addr + 2],
			"a": this.buf.data[addr + 3]
		};
	},
 
	setPixel: function( x, y, r, g, b, a ) {
		this.setPixelA( ( y * this.w + x ) * this.bpp, r, g, b, a );
	},
 
	setPixelA: function( addr, r, g, b, a ) {
		if ( !this.buf || addr < 0 || addr > this.maxAddr ) return;
		if ( a == undefined ) a = 255;
 
		this.buf.data[addr] = r;
		this.buf.data[addr + 1] = g;
		this.buf.data[addr + 2] = b;
		this.buf.data[addr + 3] = a;
	},
 
	mergePixel: function( x, y, r, g, b, a ) {
		this.mergePixelA( ( y * this.w + x ) * this.bpp, r, g, b, a );
	},
 
	mergePixelA: function( addr, r, g, b, a ) {
		if ( a == undefined || a == 255 ) {
			return this.setPixelA( addr, r, g, b, 255 );
		}
 
		if ( !this.buf || addr < 0 || addr > this.maxAddr ) return;
 
		a = a / 255;
		var na = 1 - a;
		r *= a;
		g *= a;
		b *= a;
 
		var da = this.buf.data[addr + 3] * na / 255;
		var dr = this.buf.data[addr] * na;
		var dg = this.buf.data[addr + 1] * na;
		var db = this.buf.data[addr + 2] * na;
 
		this.buf.data[addr]		= ( r + dr ) & 255;
		this.buf.data[addr + 1]	= ( g + dg ) & 255;
		this.buf.data[addr + 2]	= ( b + db ) & 255;
		this.buf.data[addr + 3]	= ( ( a + da ) * 255 ) & 255;
	},
 
	drawImage: function( img, x, y ) {
		if ( !this.canvas ) return;
		if ( !x ) x = 0;
		if ( !y ) y = 0;
 
		this.canvas.c.drawImage( img, 0, 0 );
		this.get();
	},
 
	blur: function() {
		for ( var x = 1; x < this.w - 1; x++ ) {
			for ( var y = 1; y < this.h - 1; y++ ) {
				var p1 = this.getPixel( x - 1, y );
				var p2 = this.getPixel( x + 1, y );
				var p3 = this.getPixel( x, y - 1 );
				var p4 = this.getPixel( x, y + 1 );
 
				var r = ( ( p1.r + p2.r + p3.r + p4.r ) / 4 ) & 255;
				var g = ( ( p1.g + p2.g + p3.g + p4.g ) / 4 ) & 255;
				var b = ( ( p1.b + p2.b + p3.b + p4.b ) / 4  ) & 255;
 
				this.setPixel( x, y, r, g, b, 255 )
			}
		}
	}
};

Класс CANVAS

Класс CANVAS это основа для работы с канвасом.
Создание
При создании классу можно передать в качестве аргументов:
строкутогда создастся элемент canvas с таким ID, элемент будет свободным - т.е.ни куда не добавится
указатель на DOM объект canvasтогда класс "привяжется" к нему

Если заданы размеры - они будут установлены для данного canvas.
Если же нет - то в случае существующего элемента - возьмутся у него, или же будут заданы по-умолчанию, равными 32px.

Методы
getвернет массив с параметрами canvas: c - context 2d, dom - элемент canvas, w и h - его размеры
clearочистить canvas используя текущую кисть
requestRedrawв качестве аргумента передается функция, которая будет рендерить картинку для канваса,
когда браузер будет готов к этому, но в то же время не меньше 60 кадров в секунду, возвращает дескриптор, который может быть использован в методе cancelRedraw для отмены запроса на перерисовку
cancelRedraw( id )в качестве аргумента принимает значение, возвращаемое методом requestRedraw и отменяет запрос на перерисовку


Класс DBUF

DBUF (dynamic buffer) - это класс для создания так называемых backbuffers в которых обычно и происходит вся отрисовка, и только затем она показывается.

Они нужны для избавления от артефактов типа "мерцания" и "дрожания" которые часто бывают при прямой отрисовке, а так-же позволяют добиться более плавной смены изображения.
Кроме того у одного canvas может быть несколько таких буферов, в каждом из которых может происходить своя отрисовка, а затем они все "сводятся" в canvas используя например альфа-канал, либо же по разным координатам (их размер может быть больше или меньше размера canvas).

Создание
Аргументы практически аналогичны классу CANVAS:

строкасоздастся внутренний экземпляр класса CANVAS с таким ID, элемент будет свободным, а DBUF "привяжется" к нему
указатель на DOM объект элемента canvasсоздастся внутренний экземпляр класса CANVAS, а DBUF "привяжется" к нему
указатель на объект класса CANVASDBUF "привяжется" к нему

Если CANVAS будет создан согласно аргументам, то размеры будут заданы аналогично созданию класса CANVAS.
Иначе они будут установлены только для данного DBUF следующим образом - если заданы - будут использованы, если нет - возьмутся у CANVAS.

Методы
getCanvas()вернуть указатель на CANVAS, привязанный к данному DBUF
get( [merge] )заполнить DBUF содержимым CANVAS, если merge = false - содержимое CANVAS просто скопируется в DBUF, если merge = true - содержимое CANVAS объеденится при копировании с содержимым DBUF, используя альфа-канал
put( x, y )скопировать содержимое DBUF в CANVAS по координатам x и y
clear( [r, g, b, a] )заполнить DBUF цветом r, g, b, a, если задано, или 0, 0, 0, 255 если нет
requestRedraw( renderer )аналогично requestRedraw у CANVAS
cancelRedraw( id )аналогично cancelRedraw у CANVAS
getPixel( x, y )вернуть значение пикселя из DBUF по координатам x, y в формате { r: ..., g: ..., b: ..., a: ... }
getPixelA( addr )вернуть значение пикселя из DBUF по адресу addr в формате { r: ..., g: ..., b: ..., a: ... }
setPixel( x, y, r, g, b, [a] )установить значение пикселя по координатам x, y и цветом r, g, b, a
setPixelA( addr, r, g, b, [a] )установить значение пикселя по адресу addr и цветом r, g, b, a
mergePixel( x, y, r, g, b, [a] )комбинировать значение пикселя по координатам x, y и цветом r, g, b, a через альфа-канал
mergePixelA( addr, r, g, b, [a] )комбинировать значение пикселя по адресу addr и цветом r, g, b, a через альфа-канал
drawImage( img, x, y )скопировать картинку img (new Image(...)) в DBUF по координатам x и y
blurразмыть изображение в DBUF
Диапазон адресов 0 - (w * h * 4).
Диапазон цветов 0 - 255.
Диапазон альфа-канала 0 (полностью прозрачный) - 255 (полностью непрозрачный).
У setPixel / setPixelA можно опускать значение альфа канала - тогда будет использовано 255;

Пример работы

Создаем html, в body добавляем вот этот скрипт:
( function() {
	var c = new CANVAS( "c", 320, 240 );
	document.body.appendChild( c.get().dom );
	var b = new DBUF( c, 320, 240 );
	b.clear();
	for ( var i = 1; i <= 318 ; i++ ) b.setPixel( i, 1, 255, 0, 255 );
	b.put();
} )();

Тоже самое можно получить так:
( function() {
	var b = new DBUF( "c", 320, 240 );
	document.body.appendChild( b.getCanvas().get().dom );
	b.clear();
	for ( var i = 1; i <= 318; i++ ) b.setPixel( i, 1, 255, 0, 255 );
	b.put();
} )();

Он сам создаст элемент canvas размером 320 на 240 пикселей, добавит его в body, и нарисует на нем линию розового цвета.

Скачать canvas.js, версия 1.0

скачать

12.05.2013, Protocoder
Написать комментарий