×
Namespaces

Variants
Actions

在Windows Phone和Windows 8上绘制数学函数图形

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon UI.png
WP Metro Icon WP8.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 12 Jul 2012 创建
最后由 hamishwillee 在 22 Jul 2013 编辑

Contents

简介

众所周知,Windows Phone和Windows 8 Metro Style applications主要用于开发针对消费者的应用程序。但是面向消费者的应用程序这个范围十分广泛。虽然市面上绝大多数的程序都是和新闻、购物相关或者是休闲游戏,但是我们的思路可不能就这样被局限住啊。今天我们来给大家介绍一个可能相对比较少受人关注但是十分有用的场景:绘制数学函数图形。仔细想一下你会发现,其实这个功能以及他的变种的应用场景非常广泛。例如,学习软件常常免不了要提供绘图的功能,很多游戏都离不开基本的数学和物理计算,就连股票走势其实也是一个二维函数。

你可以自File:SimpleGraph.zip下载本文提供的示例程序。

Functions.png

基本思路

很多函数都是曲线,但是用电脑是无法直接绘制曲线的。显卡通常只支持点、直线、三角。三角通常用于3D图形。今天我们只讨论简单的2D图形,所以就只有点和直线可以使用了。当然,像Direct2D和HTML canvas这样的API也提供了高级功能,帮助我们绘制椭圆,贝塞尔曲线,等等,但是它们还是没有提供直接绘制数学函数的功能,所以我们必须手工绘制。

一个比较简单的思路(也是本文将使用的思路)是将一条曲线拆分成很多很多小的直线。如果每段直线的间隔很小,基本上用肉眼就很难看出是直线,而会感觉是连贯的曲线了。这个方案的优点是想法简单,实现起来方便,解释起来清楚。缺点则是,如果你把间隔分的很小很小,就需要做很多很多计算。若是需要即时绘制动画,可能效率就会低下了。这种时候可以考虑使用贝塞尔曲线。贝塞尔曲线在一定范围内,可以模拟其它曲线。为了使图形平滑,使用直线你很可能需要将线条分成1000份,但是使用贝塞尔曲线可能连100份都不用。虽然贝塞尔曲线本身的计算相对于直线而言稍微复杂了一点点,但是总体上来说通常还是可以提高性能的,因为分块少了。

不过,为了简单起见,本文就只会单纯使用直线分割法。

技术选择

无论使用哪种方法,都可能需要绘制很多图形。假如你使用XAML和SVG这种retained graphics模型,为每一段直线(或者贝塞尔曲线)都要创建一个元素,很可能会占用太多内存,虽然如果要制作动画会比较简单。而使用Direct2D和HTML canvas这样的immediate graphics模型,占用的内存通常会少很多,不过要制作动画会比较麻烦。今天我们不考虑动画,所以,就让我们使用HTML canvas吧,它既可以用于Windows Phone(放在网页中,让用户通过访问网站的形式观看,或者嵌入到一个WebBrowser中),也可以用于Windows 8(可以作为Metro程序),而且要比Direct2D简单很多。按照微软的blog来看,Direct2D也会在Windows Phone 8中得到支持,不过由于使用起来比较复杂,通常在需要高级功能的场合下(例如shader effect)才会使用。若是你想要了解Direct2D如何使用,请观看我们之前的一篇wiki:Windows 8: 混用CSharp和C++创建DirectX程序

实现

如前所述,我们使用HTML canvas来绘制图形。首先,我们先创建一个使用JavaScript的Windows 8 Metro程序,之后可以将它迁移到网页中去。之后,我们需要准备好基本的canvas。

<canvas id="mainCanvas"/>

canvas不会自动根据环境改变大小。当然,你可以使用绝对坐标,但是如果你想让不同大小的屏幕都能正常显示你的图形,就还是需要使用相对坐标。下面的代码定义了一些和大小相关的变量,并且处理window.onresize事件,从而能够针对不同大小的屏幕绘制不同大小的图画。

var mainCanvas = null;
var width = 0;
var height = 0;
 
app.onactivated = function (args) {
// Automatically generated code omitted.
window.addEventListener("resize", onresize);
mainCanvas = document.getElementById("mainCanvas");
onresize();
};

在onresize中,获得屏幕大小。本文决定让canvas撑满整个屏幕。注意canvas不支持CSS的width和height属性,你必须显示设置canvas本身的width和height属性。

function onresize() {
width = document.body.clientWidth;
mainCanvas.width = width;
height = document.body.clientHeight;
mainCanvas.height = height;
ondraw();
}

接下来,就可以开始画画了。目前canvas只支持2D图形。要做的第一件事是通过getContext来获取一个context。在Direct2D中,有一堆Device,DeviceContext,RenderTarget之类的复杂的概念。而在HTML canvas中,全部的一切都帮你封装好了,你只需要使用context着一个对象就可以了,是很方便的呢。这就是高层API的最大优势:使用起来很简单。缺点则在于有些功能只能通过底层的API才能实现。请不要忘记,IE在后台就是使用Direct2D进行渲染的!

通过context,我们既可以设置各种属性,例如颜色,也可以调用各种方法来进行绘图。例如,beginPath意味着接下来的这些东西都使用之前设置好的属性进行绘制,直到调用stoke方法结束。接下来可以是另一次beginPath和stroke的组合。这就类似于Direct2D中的BeginDraw和EndDraw的组合(虽然Direct2D没有那么好的属性可以使用,而必须调用一堆方法做各种配置)。绘图的指令有很多,你可以绘制直线,椭圆,还有贝塞尔曲线。例如,要画直线,就调用lineTo。而moveTo指令则用于设置直线的起始点。例如,通常坐标轴(一根长长的线加一个箭头)无法实现一笔画,当你画好长线以及半个箭头之后,需要将起始点移回到长线的结尾,才能画另外半个箭头。这时候除了使用moveTo,还可以设置translate transform。通常moveTo用于在一个图形中改变直线的起始点,它只能用于画直线。而translate transform则用于改变某个图形的位置(但是它可以完全取代moveTo,只是要稍为多写一点代码而已)。此外,如果要显示文字,可以用fillText方法,它使用之前在context上设置的和文字相关的属性(别忘了在DirectX中,你必须使用DirectWrite设置文字属性,使用Direct2D绘制。可以看到canvas这样高层的API位我们封装了多少东西!)。

下面的代码做了一些准备工作,然后画了两条坐标轴。每条坐标轴包括一根长线,一个箭头,还有x/y这样的文字。为了省力,我们没有画0,1,2,3这样的标识,也没有在坐标轴上划一根根的短线来代表刻度。如果你有时间和精力,可以去画上。

function ondraw() {
var context = mainCanvas.getContext("2d");
context.strokeStyle = "white";
context.fillStyle = "white";
context.beginPath();
var halfWidth = width / 2;
var halfHeight = height / 2;
 
// x axis
context.moveTo(10, halfHeight);
context.lineTo(width - 10, halfHeight);
context.lineTo(width - 20, halfHeight - 10);
context.moveTo(width - 10, halfHeight);
context.lineTo(width - 20, halfHeight + 10);
context.font = "30px Arial";
context.textAlign = "left";
context.textBaseline = "top";
context.fillText("x", width - 20, halfHeight + 10);
 
// y axis
context.moveTo(halfWidth, 10);
context.lineTo(halfWidth, height - 10);
context.moveTo(halfWidth - 10, 20);
context.lineTo(halfWidth, 10);
context.lineTo(halfWidth + 10, 20);
context.fillText("y", halfWidth - 20, 10);
context.stroke();
 
// more code to be added...
}

以上,我们介绍了如何使用HTML canvas进行画画,并且展示了它相对于Direct2D有多么简单易用。接下来就要做正事了:绘制函数。

之前曾经说过,我们打算采用一个简单的方法:将一条曲线分割成很多很多小的直线,并且连接起来。每条直线的两个端点的x/y的坐标,很自然要由数学函数的x/y值获得。但是,屏幕坐标和数学坐标并不是一一对应的。我们说过,我们必须根据屏幕大小改变canvas和其中所有画面的大小,而数学坐标则是绝对的,0就是0,1就是1,不会随屏幕大小改变。所以,我们必须做一个坐标变换。为此,我们要求指定函数在x轴上的区间。有了这个区间就好办了,只要计算出区间和屏幕宽度的比例,就可以将数学坐标映射到屏幕坐标了(为了保持数学坐标的长宽比例,我们不考虑屏幕的高度)!还请注意屏幕坐标的原点在左上角,向右向下伸展。数学坐标的原点在中心,向右向上伸展。在映射坐标时一定要把它们也考虑进去。

下面的代码传入一个func参数,它是一个委托,代表具体的数学函数。首先我们将屏幕x坐标转换成数学x坐标,通过调用func获取数学y坐标,再转换成屏幕y坐标,就可以画画了呢(切记不要往数学函数中传入屏幕坐标!)。

function drawFunc(func, context, min, max) {
 
// Type check omitted...
if (min >= max) {
throw "min must be less than max.";
}
var interval = max - min;
var halfWidth = width / 2;
var halfHeight = height / 2;
var oldY = null;
for (var i = -halfWidth; i < halfWidth; i++) {
 
// i is the screen coordinate.
// Translate x to math coordinate.
var x = i * interval / width;
 
// Translate y to screen coordinate.
// y is negative because the math coordinate is reflected from screen coordinate.
// + halfHeight so that 0 starts at the center.
var y = -func(x) / interval * width + halfHeight;
if (oldY === null) {
oldY = y;
context.moveTo(i + halfWidth, y);
}
else {
context.lineTo(i + halfWidth, y);
oldY = y;
}
}
}

func可以是任何能用代码表示的函数,例如下面的代码代表一次函数,二次函数,三次函数。当然,这个函数并不一定要能用数学公式表示。你也完全可以写一个函数,传入一个国家的边界线的纬度,返回经度,或者传入某一时刻,返回某地的天气或者某个股票的价格,等等。

function line(x) {
return x;
}
 
function square(x) {
return Math.pow(x, 2);
}
 
function cube(x) {
return Math.pow(x, 3);
}

现在我们可以在onDraw中添加代码画出一个二次函数和一个一次函数。

        context.beginPath();
context.strokeStyle = "blue";
 
// y = x
drawFunc(line, context, -10, 10);
context.stroke();
context.beginPath();
context.strokeStyle = "red";
 
// y = x ^ 2
drawFunc(square, context, -10, 10);
context.stroke();

为了显示这样的程序是有用的,我们再添加一个功能:画出某个函数的微分曲线。如果你忘记了什么是微分,下面是它的定义的简化版:

derivative = lim(dx->0) dy/dx

这里的dx和dy是delta x和delta y的缩写,这也就是说当x的间隔变得无穷小的时候,斜率(dy/dx)的值就是这一刻的微分。微分函数则是将某个函数的每一点分别计算出微分,并且组成一个新的函数,它(正如斜率那样)代表着原始函数的变化情况。微分是很有用的,因为很多时候我们真的不仅仅要知道某条曲线本身的情况,还要知道它变化的情况。例如,假如一条曲线代表销售额,它的微分曲线就代表销售额的增减速率,微分曲线的y值越高,就说明这一刻的销售额增加得越快。如果微分的值为0,很有可能(但并不是一定)意味着原始函数在这一刻达到了极值(具体是不是极值,还有是极大值或者极小值,需要做进一步判断),这一点,很自然,会受到那些思考盈利的人的关注,因为大家都希望盈利能够最大嘛。

如果你还记得大学里的数学课,很有可能会觉得计算微分很头疼,要记住一大堆的公式,要用什么替换法之类的东西。但是,请你千万不要害怕,因为我们有电脑!我们根本不需要手工计算任何东西!使用电脑,我们只需要知道微分的定义,就可以设计出程序来计算它了。而且这是最通用的计算方式,不需要任何数学技巧!

微分函数的图形也是曲线,所以我们可以用几乎和之前一模一样的方式来画它,只是计算方式稍有不同而已。这一次,我们不仅仅要计算y,还要计算斜率,并且用斜率取代y作为纵坐标画到屏幕上。

function drawDerivative(func, context, min, max) {
 
// Type check omitted...
if (min >= max) {
throw "min must be less than max.";
}
var interval = max - min;
var halfWidth = width / 2;
var halfHeight = height / 2;
var oldX = null;
var oldY = null;
var oldTangent = null;
for (var i = -halfWidth; i < halfWidth; i++) {
 
// i is the screen coordinate.
// Translate x to math coordinate.
var x = i * interval / width;
var y = func(x);
if (oldX === null) {
oldX = x;
oldY = y;
}
else {
var dy = y - oldY;
var dx = x - oldX;
oldX = x;
oldY = y;
 
// tangent is negative because the math coordinate is different from screen coordinate.
var tangent = -dy / dx / interval * width + halfHeight;
if (oldTangent === null) {
oldTangent = tangent;
context.moveTo(i + halfWidth, tangent);
}
else {
context.lineTo(i + halfWidth, tangent);
oldTangent = tangent;
}
}
}
}

作为测试,让我们画出二次函数的微分曲线。这个简单的公式你应该还记得,y=x^2的微分函数是y=2x,也就是一条斜率为2的直线。

        context.beginPath();
context.strokeStyle = "green";
 
// The derivative of y = x ^ 2 is y = 2x
drawDerivative(square, context, -10, 10);
context.stroke();

正如本文开始的那张截图所示,微分曲线(绿色)的确是y=2x,你可以将它和y=x(蓝色)做个对比,验证它的正确性。

最后,我们可以画上一些说明性的文字:

// Legend
context.fillStyle = "red";
context.fillText("y = x ^ 2", 10, 10);
context.fillStyle = "blue";
context.fillText("y = x", 10, 40);
context.fillStyle = "green";
context.fillText("The derivative of y = x ^ 2 is y = 2x", 10, 70);

总结

请不要小看了Windows Phone,不要认为Metro就只能做做简单的天气预报,买东西,之类的事情。事实上它的用途非常广泛!本文只是指出了其中的一种应用而已。画函数图本身并没有什么,但是想象一下股票走势图,销售额度图,游戏中的分数图,等等等等。它的应用场景是非常多的!本文的代码虽然是针对Windows 8 Metro应用程序写的,但是它可以很好地被移植到网页中,从而在Windows Phone中显示。

This page was last modified on 22 July 2013, at 08:18.
84 page views in the last 30 days.