Pers.narod.ru. JavaScript. Рисуем график на канве JavaScript, кроссбраузерное решение

Узаконенный в 5 версии HTML элемент canvas позволяет работать на Web-странице с графической канвой.

 Об элементе canvas на w3.org

Приведём пример тега создания канвы размером со стандартный экран VGA (640x480 пикселов), обведенного штриховой (dashed) рамкой. Кроме атрибутов стиля, ширины и высоты, тегу следует присвоить уникальную для документа метку id, так как мы будем рисовать на канве методами JavaScript, а также вставить в качестве содержимого тега текст, отображаемый браузерами, не поддерживающими канву (к их числу относится и Internet Explorer всех версий, но проблема с последним решаема):

<canvas style="border:1px dashed #888;" id="canvasId" 
width="640" height="480">
Извините, тег Canvas недоступен!</canvas>

Для обращения к канве и определения её размеров в пикселах достаточно следующего кода JavaScript:

//Переменная-ссылка на канву
var canvas = document.getElementById('canvasId').getContext('2d');
//Определить размеры канвы
var Width=document.getElementById('canvasId').width;
var Height=document.getElementById('canvasId').height;

В следующих версиях реализации ожидается и поддержка трёхмерного (3d) контекста.

Возможности канвы HTML по программному рисованию объектов пока скромные, но, по крайней мере, имеются привычные разработчикам методы moveTo, lineTo, fill, а также установки стилей. К сожалению, нет никаких средств для добавления на канву текста, но это можно сделать через набор изображений, так как объекты Image размещать можно.

Приведём законченный пример рисования контура "паровозика", выполняемого по нажатию кнопки Draw:

<html>
<head>
<title>Javascript Canvas</title>
</head>
<body>
<!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
<script type="text/javascript">
//<![CDATA[
 function drawPoly (id, arr) { //id тега <canvas> и массив координат
  var canvas = document.getElementById(id).getContext('2d');
  // Отрисовка
  canvas.beginPath();
  for (var i = 0; i < arr.length; i++) {
   if (i == 0) canvas.moveTo(arr[i][0], arr[i][1]);    // Ставим 1-ю точку
   else canvas.lineTo(arr[i][0], arr[i][1]); // Линии
  }
  canvas.fillStyle = "rgba(255,128,128,0.5)";  // Цвет заливки RGBA
  canvas.fill(); // Заливка
 }
 
 var coordArray = [ //Массив координат
  [10,70],
  [10,80],
  [130,80],
  [130,30],
  [140,30],
  [140,20],
  [80,20],
  [80,40],
  [40,40],
  [40,10],
  [30,10],
  [30,40],
  [20,40],
  [20,70],
  [10,70]
 ];
//]]>
</script>
 
<form>
<canvas style='border:1px dashed #888;' id='canvasId' width='150' height='100'>
Извините, тег Canvas недоступен!
</canvas>
<br>
<input style="width:auto;" type="button" value="Draw" onclick="drawPoly('canvasId', coordArray)">
</form>
 
</body>
</html>

Самое интересное в этом коде то, что он будет работать и в IE. Это достигается строкой вызова скрипта excanvas.js, преобразующего инструкции, написанные для canvas (Opera 9 и выше FireFox 1.5 и выше работают с canvas отлично) в инструкции языка VML (Vector Markup Language), понимаемого всеми версиями Internet Explorer. Вот стандартный рекомендуемый вызов этого скрипта (предполагается, что он размещён в той же папке, что и документ):

<!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->

А вот все нужные ссылки:

 Мой пример с рисованием "паровозика" в работе

 Canvas Tutorial по-русски - описание методов, примеры

 Explorer Canvas (excanvas), реализация canvas для IE. Вызывать тегом, показанным выше

 Язык VML (Vector Markup Language)

HTTPS-ссылка на Canvas Tutorial какая-то дурная, но раза со второго обычно открывается :)

Поскольку JavaScript умеет и работать с данными форм, и оценивать код динамически, нетрудно придумать массу графических кроссбраузерных приложений, выводящих данные на графическую канву. В качестве примера напишем небольшой графопостроитель, строящий график функции, вводимой пользователем. Такой скрипт я уже некогда писал (вот он, новое окно), но там реализация была через таблицы table, поскольку графической канвы ещё не было.

Вся реализация синтаксического разбора вводимой строки document.f1.func.value, построенная на методe eval, берётся из старого скрипта, а функцию Main, выводящую график, придётся написать заново:

function Main () {
 var ff=trim(document.f1.func.value);
 if ((ff=='y=') || (ff=='')) {
  window.alert ("Пожалуйста, задайте допустимое выражение для функции"); return;
 }
 splitFunc (ff);
 var errormsg=
 "Недопустимые данные в полях ввода!\r\n" +
 "Требования: Начальное и конечное X - числа,\r\n" +
 "Начальное X меньше конечного.\r\n" +
 "Пожалуйста, исправьте ввод и нажмите ОК\r\n";
 var minx = document.f1.x1.value;
 var maxx = document.f1.x2.value;
 if ((isNaN(minx)==true) || (isNaN(maxx)==true)) {
  window.alert (errormsg); return;
 }
 minx = parseFloat(eval(replaceSpecialSequence(minx)));
 maxx = parseFloat(eval(replaceSpecialSequence(maxx)));
 if (minx>=maxx) {
  window.alert (errormsg); return;
 }
 else {
  //Определить размеры канвы
  var Width=document.getElementById('canvasId').width;
  var Height=document.getElementById('canvasId').height;
  var dx=(maxx-minx)/(Width-1);  //Шаг по X соотв. 1 пикселу
  //Создать массив значений Y:
  ar = new Array();
  var k=0;
  for (var i=minx; i<=maxx+1e-7; i+=dx) { ar[k] = yVal(i); k++; }
   //Без точного кол-ва элементов будет на 1 эл-т меньше и "Опера" не выполнит
  var miny=ar[0];
  var maxy=miny;
  for (i=0; i<ar.length; i++) {
   if (ar[i]<miny) miny=ar[i];
   else if (ar[i]>maxy) maxy=ar[i];
  }
 
  var pc=Okrugl(100/((maxx-minx)/dx+1),2);
  var p=0;
  var scry=0;
  var canvas = document.getElementById('canvasId').getContext('2d');
  canvas.strokeStyle="rgb(0,0,0)";
  canvas.beginPath();
  var xcoef=(Width-1)/(maxx-minx);
  var ycoef=(Height-1)/(maxy-miny);
  for (i=0; i<Width; i++) {
   scry=Okrugl((ar[i]-miny)*ycoef,0);
   if (i==0) canvas.moveTo(0,Height-1-scry); // Ставим точку
   else canvas.lineTo(i,Height-1-scry); // Линии
   window.status='График: '+Okrugl(p,2)+'%';
   p+=pc;
  }
  canvas.stroke();
  //Красным дорисовать оси координат, если попадают в область
  canvas.strokeStyle="rgb(255,0,0)";
  if ((minx<=0) && (0<=maxx)) {
   scrx=Okrugl(-minx*xcoef,0);
   canvas.beginPath();
   canvas.moveTo(scrx,0);
   canvas.lineTo(scrx,Height-1);
   canvas.stroke();
  }
  scry=Okrugl(-miny*ycoef,0);
  if ((0<=scry) && (scry<Height)) {
   canvas.beginPath();
   canvas.moveTo(0,Height-1-scry);
   canvas.lineTo(Width-1,Height-1-scry);
   canvas.stroke();
  }
  
  //Вывести координаты угловых точек
  var sx=DrawNumber (''+Okrugl(minx,2),4,Height-10);
  DrawNumber (''+Okrugl(miny,2),sx+10,Height-10);
  var t=''+Okrugl(maxx,2)+10+Okrugl(maxy,2);
  var sx=DrawNumber (''+Okrugl(maxx,2),Width-1-t.length*7,10);
  DrawNumber (''+Okrugl(maxy,2),sx+10,10);
  window.status='Готово';
 }
}

Взятая из старого скрипта функция разбора splitFunc со своими служебными методами выглядит так:

function root(a, b) { //Корень степени a из b
 return Math.pow(b, 1 / a);
}
 
function logab(a, b) { //Логарифм от b по основанию a
 return Math.log(b) / Math.log(a);
}
 
function fact(a) { //Факториал от a
 if (a <= 0) return 1;
 var f=1;
 for (var i=1; i<=f; i++) f*=i;
 return f;
}
 
function replaceSpecialSequence(str) { 
 //Тригонометрические функции
 str = str.split("cos").join("Math.cos");
 str = str.split("sin").join("Math.sin");
 str = str.split("tan").join("Math.tan");
 str = str.split("acos").join("Math.acos");
 str = str.split("asin").join("Math.asin");
 str = str.split("atan").join("Math.atan");
 str = str.split("pi").join("Math.PI");
 str = str.split("ln2").join("Math.LN2");
 str = str.split("ln10").join("Math.LN10");
 str = str.split("log2e").join("Math.LOG2E");
 str = str.split("log10e").join("Math.LOG10E");
 str = str.split("sqrt1_2").join("Math.SQRT1_2");
 str = str.split("sqrt2").join("Math.SQRT2");
 str = str.split("abs").join("Math.abs");
 str = str.split("ceil").join("Math.ceil");
 	str = str.split("exp").join("Math.exp");
 str = str.split("floor").join("Math.floor");
 str = str.split("ln").join("Math.log");
 	str = str.split("max").join("Math.max");
 str = str.split("min").join("Math.min");
 str = str.split("pow").join("Math.pow");
 str = str.split("round").join("Math.round");
 str = str.split("lg").join("logab");
 str = str.split("sqrt").join("Math.sqrt");
 	str = str.split("e").join("Math.E");
 return str;
}
 
function splitFunc(func) { //Начальный разбор строки
 //Убрать "Y="
 var expr = func.substring(func.indexOf("=") + 1, func.length);
 //Переписать тригонометрические функции на Javascript
 expr = replaceSpecialSequence(expr);
 //Разбить строку на массив строк по разделителю "x"
 funcArray = expr.split("X");
}

trim - это просто удаление лишних пробелов с помощью регулярного выражения JavaScript:

function trim(string) {
 return string.replace (/(^\s+)|(\s+$)/g, "");
}

Перед рисованием массив значений функции ar для указанных пределов по x (а шаг по x выбран соответсвующим 1 пикселу на канве) получается с помощью метода yVal, "собирающего" и оценивающего выражение функции с нужным аргументом:

function yVal(xVal) { //Построить выражение с аргументом xVal вместо x
 var expr = funcArray.join(xVal);
 return eval(expr);
}

Наконец, метод Okrugl нужен для округления значения a с нужным числом знаков после запятой (2-й аргумент eps). Это требуется как при выводе процента выполнения в строку статуса, так и при расчёте координат в пикселах и отображении значений x и y на канве:

function Okrugl (a,eps) {//Округление, eps-число знаков после запятой
 a=Math.round(a*eval('1e'+eps));
 a*=eval('1e-'+eps); a+='';
 a+=(a.indexOf('.')==-1 ? '.' : '');
 return parseFloat(a.substring (0,a.indexOf('.')+eps+1)); 
}

Остаётся ещё одна проблема - отображение на канве чисел (напомню, что метода для вывода текста на canvas пока нет). Заготовив маленькие картинки с изображением цифр 0-9, минуса и десятичной точки:

напишем метод DrawNumber (s,x,y) с аргументами s (выводимое число как String) и x,y - координаты для вывода на канве:

function DrawNumber (s,x,y) {
 //Вывод числа s с помощью картинок с позиции x,y
 var canvas = document.getElementById('canvasId').getContext('2d');
 for (var i=0; i<s.length; i++) {
  var img = new Image();
  var c=s.substring(i,i+1);
  if (c=='.') c='dot';
  else if (c=='-') c='minus';
  img.src = c+'.gif';
  canvas.drawImage(img,x+i*5,y);
 }
 return x+i*5;
}

К сожалению, имеющаяся у меня версия скрипта excanvas.js неправильно конвертирует позиции картинок на канве, так что надписей в IE8 я не увидел, а скриншот из Оперы получился вот таким:

График с масштабированием по размеру окна (index.html)

Разумеется, для полноты картины нужно привести код формы HTML, из которой вызывается функция Main():

<form name="f1">
<table border=0 width=90% align=center>
<tr><td align=right>
Функция: <input type="text" name="func" maxlength="60" size="60" value="y=">
</td>
<td>
<a href="help.html" target=_blank>Окно помощи</a>
</td>
</tr>
<tr>
<td align=right>Начальное X: <input type="text" name="x1" maxlength="6" size="6" value="0"></td>
<td>Конечное X:  <input type="text" name="x2" maxlength="6" size="6" value="1"></td>
</tr>
<tr>
<td align=right><input type="button" name="OK"    value="  ОК  " onClick="Main()"></td>
<td><input type="reset" name="Cancel" value="Очистить" onClick="Clear()"></td>
</tr><tr><td colspan=3 align=center valign=middle>
 <canvas style="border:1px dashed #888;" id="canvasId" width="640" height="480">
  Извините, тег Canvas недоступен!
 </canvas>
</td></tr></table>
</form>

...и код функции Clear(), вызываемой по нажатию кнопки "Очистить":

function Clear () {
 document.f1.x1.value='';
 document.f1.x2.value='';
 document.f1.func.value='y=';
 var canvas = document.getElementById('canvasId').getContext('2d');
 canvas.fillStyle="rgb(255,255,255)";
 canvas.fillRect(0,0,document.getElementById('canvasId').width,document.getElementById('canvasId').height);
 canvas.fill();
}

Собрав всё воедино, получаем скрипт-графопостроитель, работающий по этому адресу:

 Графопостроитель на Javascript Canvas

Можно счесть недостатком примера то, что график масштабируется отдельно по осям X и Y, то есть, всегда занимает всё окно канвы, но выводится не в пропорции. Это легко исправить, если применять при расчётах вместо ycoef коэффициент для оси X xcoef. Тогда масштабирование по обеим осям станет пропорционально, но график может не поместиться целиком на экране. Вот что выдаст в этом случае скрипт для показанной выше картинки:

График с пропорциональным масштабированием (index2.html)

Здесь всё выглядит хорошо, потому что пределы изменения Y меньше, чем у X. Если бы было наоборот (функция растёт или убывает быстро), мы увидели бы только часть графика. Однако, с помощью кнопок и реализации "прокрутки" графика (отрисовки новой части по нажатию кнопки) Вы можете легко улучшить этот подход. Изменённый пример доступен по ссылке ниже.

 Графопостроитель на Javascript Canvas (в масштабе оси X)

Обратите внимание на помощь по этим скриптам и то, что аргумент для функций должен вводиться как X (большая латинская).

Рейтинг@Mail.ru
вверх гостевая; E-mail
Hosted by uCoz