65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 HTML5 Canvas & JavaScript 在浏览器中实现命令控制台

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (14投票s)

2016年1月27日

CPOL

16分钟阅读

viewsIcon

59282

downloadIcon

1229

HTML5 Canvas 入门教程,教你如何在浏览器中创建命令控制台。

引言

更新说明 2016-04-28:我修复了处理退格键和回车键的问题。起初只有 Firefox 不工作,后来 Chrome 也不工作了。现在一切都已修复。你可以在最后一个代码示例中看到代码更改。

我一直在学习 HTML5 Canvas 元素的强大功能以及它的用途。我想看看能否在浏览器中复制命令控制台,以后可以用它来娱乐和恶作剧。以下是我的做法。

在下图中,我截取了我浏览器中的控制台(在后面那个)的快照,其中包括一个真实控制台窗口的快照(用于比较)。我能够捕捉到显示两个光标的图像,尽管光标会闪烁——以告知用户控制台窗口已准备好输入。我的版本也会闪烁光标

查看实时演示

你可以在浏览器中实时查看: http://raddev.us/console/console.htm (在新标签页/窗口中打开)

Console In Browser compared with real console window

背景

HTML5 Canvas 元素功能强大且易于上手,但仍然存在很多 Flash(不幸的是)。我认为这个关于 Canvas 元素入门有多简单的例子将激励你开始做自己的项目。

一本进一步深入学习的好书是 HTML5 Canvas (O'Reilly 出版) - 亚马逊链接 (在新窗口/标签页中打开)。这是一本很棒的书,它将引导你进入这项技术,并让你走得很远。写作良好,包含了你需要的细节。

设置我们的 HTML

第一步是为 HTML5 和 Canvas 工作设置我们的 HTML——大部分工作实际上是在 JavaScript 中完成的(正如你可能预期的那样)。这是简单 HTML 的完整列表。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>console</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<canvas id="gamescreen">You're browser does not support HTML5.</canvas>
<script src="js/console.js"></script>

</body>
</html>

第一行包含预期的 DOCTYPE 定义。这是给浏览器的消息,它应该期望接下来的内容是 HTML。

我添加了自然语言标签,因为它将是英语。

链接 CSS 

接下来要注意的是,我放置了一个指向样式表的链接,我将其命名为 main.css,位于名为 css 的子文件夹中。

我喜欢将一切分开,就像它应该的那样,以获得干净的代码。即使是一个短程序,这一点也很重要,因为它使组织一切变得更容易。

这是极其简单的 CSS

/* main.css */
body,html {margin:0;padding:0;}

此 CSS 除了移除浏览器自动分配的所有内边距和外边距距离外,什么都不做。如果可能,我不想让 Canvas 元素和浏览器内部客户区域之间有任何空间。

最后,HTML 中只有另外两行是重要的。

设置 Canvas 元素

第一个设置了 Canvas 元素,并给它一个 id:gamescreen。这使得我可以在 JavaScript 中引用该元素,我们很快就会看到。

<canvas id="gamescreen">You're browser does not support HTML5.</canvas>

Canvas 元素开始和结束之间的文本仅在用户浏览器不支持 HTML5 时显示。这是一种简单的方式,可以让他们知道应用程序在他们的浏览器中将无法工作。

接下来,我包含了一个指向将驱动整个程序的 JavaScript 的引用。

<script src="js/console.js"></script>

同样,我将 JavaScript 放在一个名为 js 的子文件夹中以保持组织性。

为什么 JavaScript 包含在最后?

JavaScript 必须在 Canvas 元素之后加载,这一点非常重要,因为 JavaScript 中的代码将引用 Canvas 元素。还有其他方法可以解决这个问题:使用 jQuery 的 ready() 方法或页面的 OnLoad() 事件。但是,为了使这篇文章保持入门级,我选择将 JavaScript 放在 Canvas 元素之后,这样我就可以确保 JavaScript 在 Canvas 元素之后加载。

事件基本摘要

现在我们已经设置好了,我们的小应用将按以下方式运行:

1. 页面将加载

2. CSS 将加载

3. JavaScript 将加载并执行一些操作。

了解代码的基本运行方式是控制代码如何工作的第一个也是最有力的步骤。现在,让我们深入研究并检查实际工作的代码。

所有代码都可以在 console.js 文件中找到。

JavaScript 驱动 Canvas 元素

整个 console.js 文件有 220 多行代码——不算多,但一次看太多了——所以我会把它分解开来,然后分段讲解代码。然后,你可以下载代码并自己检查完整的列表。

应用程序初始化

首先,让我们看看我需要初始化的基本项目以及为什么我需要在应用程序开始在 Canvas 上绘制之前设置它们。

//consle.js

var ctx = null;
var theCanvas = null;
var lineHeight = 20;

var widthOffset = 2;
var cursorWidth = 8;
var cursorHeight = 3;
var fontColor = "#C0C0C0";
var outputFont = '12pt Consolas';
var charWidth;

var allUserCmds = [ ]; // array of strings to hold the commands user types
var currentCmd = ""; // string to hold current cmd user is typing

var PROMPT = "c:\\>";
var promptWidth = null;
var promptPad = 3;
var leftWindowMargin = 2;
var cursor = null;
window.addEventListener("load", initApp);
var flashCounter = 1;

JavaScript 如何运行

首先要理解的一件事是 JavaScript 如何运行。如你所见,console.js 在文件顶部进行了大量的初始化。但是,那些代码会运行吗?答案是,一旦浏览器加载了 JavaScript 文件,那么任何不在函数外部的代码都会按自上而下的顺序运行。这意味着上面所示的代码——没有包装在单独函数中的代码——会从上到下运行。

全局变量:快跑 & 尖叫

这是懒惰程序员初始化一些我们需要的项的方式。在较大的应用程序中,你不希望这样做,因为那些变量将是全局变量,并且应该让你跑得尖叫,因为任何人都可以从其他模块更改它们的值,从而完全损坏你的程序。

全局变量太容易了:反模式定义

这是一个谈论 JavaScript 中创建全局变量非常容易的事实的好时机,它常常被缺乏经验的开发人员复制,而他们甚至不知道。这为 JavaScript 的创建方式形成了一种模式,而这种模式实际上是一种反模式。反模式是指某种东西变得极其普遍但仍然是错误的。如果你从这个例子中学到这样做是错误的,那么你将远远领先于许多其他开发人员。

现在,让我们看看我的控制台如何使用那些变量。

Canvas 上下文对象

这个样本中第一个也是最重要的变量是名为 ctx 的变量。

你可以看到我只是将该对象初始化为 null。它只是为了让我有一个命名的对象,我可以在以后初始化并使用它。

跳到最后一行,看起来像

window.addEventListener("load", initApp);

这是使用纯 JavaScript 添加 EventListener 的标准方法。这告诉浏览器,每当 load 事件(第一个参数)触发时,就调用我们定义的名为 initApp 的方法。

请注意,第一个参数是字符串(用双引号括起来),而最后一个参数是对象(没有双引号),即名为 initApp 的函数。如果你仔细阅读,你就学会了 JavaScript 函数是第一类对象。这仅仅意味着 JavaScript 中的函数是对象,并且可以使用它们的名称轻松引用。

谁触发了 Load 事件?

当文档加载完成时,浏览器本身会触发 load 事件。我们正在告诉浏览器在文档加载完成后通知我们。我们可以提供许多其他具有不同名称的事件作为第一个参数。另外,请记住,如果我们使用了“loaded”或“loads”这样的词,我们会得到一个错误,因为 JavaScript 解释器不知道这些事件。事件由 JavaScript 创建者预定义。你可以谷歌搜索 JavaScript 事件来查找更多信息。

好的,所以当文档加载时,我希望收到通知,并希望我的 initApp() 方法被调用。

因此,在所有变量初始化后,我注册了 Load 事件,这将确保我的 initApp 方法被调用。

现在让我们跳转到 initApp() 函数,这样我们就可以看到它做了什么工作。

近距离观察 initApp() 函数

initApp 函数的全部代码如下所示:

function initApp()
{
    theCanvas = document.getElementById("gamescreen");
    ctx = theCanvas.getContext("2d");
    ctx.font = outputFont;
    var metrics = ctx.measureText("W");
    // rounded to nearest int
    charWidth = Math.ceil(metrics.width);
    promptWidth = charWidth * PROMPT.length + promptPad;
    cursor = new appCursor({x:promptWidth,y:lineHeight,width:cursorWidth,height:cursorHeight});

    window.addEventListener("resize", draw);
    window.addEventListener("keydown",keyDownHandler);
    window.addEventListener("keypress",showKey);
    initViewArea();
    setInterval(flashCursor,300);
    function appCursor (cursor){
        this.x = cursor.x;
        this.y = cursor.y;
        this.width = cursor.width;
        this.height = cursor.height;
    }
}

你可以看到,我首先调用了一个标准的 document 方法,称为 getElementById(),并将我们在 HTML 中之前定义的 id 传递进去。这代表了 HTML 中的 Canvas 元素。

一旦我们有了该元素,我们就可以调用一个HTML5 特有方法,所有支持 HTML5 的浏览器都会为我们定义该方法:getContext("2d")

当我们调用该方法时,它将返回一个图形上下文对象,我们将把它存储在我们的 ctx 变量中,以便我们可以绘制到 Canvas 上。每个想要在 Canvas 上绘制的 HTML5 应用都将有一个类似的调用。这个调用通过提供该字符串作为参数来告诉它我们想要进行 2D 绘制。

利用上下文对象的强大功能

加载该对象的整个想法是,HTML5 开发人员和浏览器开发人员已经包含了一个我们可以使用的库功能。由于我们已经获得了一个 context 对象,我们现在可以调用已经为我们定义好的方法。调用它们就像:

1. 知道函数名

2. 知道它们期望的参数(如果有)

要了解更多关于 Context 对象的信息,我只需谷歌搜索:HTML5 context object。

第一个链接是

https://mdn.org.cn/en-US/docs/Web/API/CanvasRenderingContext2D^

这是一个很棒的 Mozilla(Firefox 组织)开发者网站,上面有很多信息。

在 Canvas 上绘制的每个字母的宽度

我需要知道的一件事——以便处理绘制在 Canvas 上的文本——是每个出现字母的宽度。

没有简单的方法可以获得这个值。经过大量阅读,我找到了我认为最好的方法:

1. 设置字体系列和大小(outputFont),这样当我们测量字符宽度时,它是基于将要输出的字体的。我们调用 Canvas context 属性,该属性恰如其分地命名为 font

ctx.font = outputFont;

2. 选择我字体集中最宽的字母:字母 W 就可以了。

3. 调用 Context 方法 measureText() 来获取它的宽度。

4. 将该值向上舍入到最接近的整数并存储以备后用。

之后,我就有了控制台窗口中每个字符的基本宽度。

输出字体

我们还设置了将在 Canvas 上绘制文本(稍后详述)时使用的字体系列和字体大小。在本例中,我了解到控制台窗口的字体通常是 Consolas,我选择了 12pt。

字体颜色

当然,我们也希望字体与 Windows 控制台的颜色相同,所以我做了一些实验,发现它是在我们之前看到的初始化中使用以下值:

var fontColor = "#C0C0C0";

大量工作

这是很多工作!是的,学徒,伟大的力量伴随着巨大的责任。注意:这是蜘蛛侠语录和星球大战梗的混合引用。我是超级极客!!尽量不要嫉妒。:)

******* 侧边栏:移动端问题 *****************

此时,我在我的 Android 平板电脑上加载了示例,并发现了一些加载问题

1. 字体非常小。

2. 你无法输入,因为 Canvas 元素不会激活移动键盘。

我们可以解决这两个问题,我可能会在文章的后续更新中进行,但目前,请理解这是一个限制,此应用程序目前_仅_适用于桌面浏览器

好的,现在让我们真正地过一遍代码。

接下来我们需要检查的是 appCursor 函数/对象。

appCursor 函数

appCursor 函数在初始代码中声明,就在我们的  之前,看起来像这样:

function appCursor (cursor)
{
    this.x = cursor.x;
    this.y = cursor.y;
    this.width = cursor.width;
    this.height = cursor.height;
}

这很简单,但让我们把它分解一下。

我将此函数命名为 appCursor,这样任何开发人员用户都会立即了解其用途。此函数将在 JavaScript 中为我们创建一个对象,并使用一个强大的初始化约定。

要创建一个新的 appCursor 对象,你只需调用它并传入一个具有所需值的光标即可。

现在,好处是你可以轻松使用 JSON(JavaScript 对象表示法)来即时创建对象,传入它并获得一个代表应用程序光标(闪烁的下划线)的已初始化对象。

当我一次在 console.js 中调用此函数时,它看起来像这样:

cursor = new appCursor({x:promptWidth,y:lineHeight,width:cursorWidth,height:cursorHeight});

如果你从未见过 JSON,或者只接触过一点点,它可能会让你觉得奇怪。

JSON 只是一个或多个键值对,用逗号分隔,其中键和值用冒号分隔。

这里有两个简单的 JSON 示例:

{name:value}

{color:green}

由于大括号 { } 是 JavaScript 中的对象初始化器,因此你实际上是在创建一个具有属性的对象,该属性的名称设置为 value。

这是另一个例子

var myFont = {color:green};

现在你可以使用以下语法访问该对象来获取其颜色值:

console.log(myFont.color);

然后输出将是: green

注册 DOM 事件

我们在 initApp 函数中做的下一件事是设置一些 DOM 事件。

 window.addEventListener("resize", draw);
 window.addEventListener("keydown",keyDownHandler);
 window.addEventListener("keypress",showKey);

addEventListener 是纯 JavaScript 方法,用于在浏览器事件触发时将你的函数注册为回调。

第一个参数是 JavaScript 定义的事件(resizekeydownkeypress)作为字符串。第二个参数是你的函数名,应该被调用,但请注意它不是字符串。它是函数引用——因为 JavaScript 中的函数是第一类对象。

Keypress 和 Keydown

你可以看到我已经注册了一些特殊工作,当浏览器窗口大小调整、发生 keydown 和 keypress 事件时。是的,最后两个略有不同。

由于 initApp 中会发生更多事情,在完成代码审查之后,我们将回来向你展示这些函数具体做了什么。

接下来发生的是调用 initViewArea()

function initViewArea() {
    
    
    // the -5 in the two following lines makes the canvas area, just slightly smaller
    // than the entire window.  this helps so the scrollbars do not appear.
    ctx.canvas.width  =  window.innerWidth-5;
    ctx.canvas.height = window.innerHeight-5;
    
    ctx.fillStyle = "#000000";
    ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
    
    ctx.font = outputFont;
    ctx.fillStyle = fontColor;
    var textOut = PROMPT;

    ctx.fillText  (textOut,leftWindowMargin, cursor.y);
    draw();
}

我们首先要做的就是初始化 Canvas 的宽度和高度,使其占据整个窗口,除了 5 像素宽的小边框。

之后,我们将 fillStyle 设置为黑色,并调用 fillRect,将整个区域填充为黑色。

这是我们控制台窗口的背景。

字体绘制在 Canvas 上

最后,我将字体设置为我们之前定义的 outputFont。我查阅了 Windows 控制台字体,并使用它,以便伪控制台窗口看起来像真实的。

接下来,我必须绘制提示文本(由 c:\> 表示)。请记住,Canvas 元素需要绘制所有内容。我们使用 Canvas 上下文函数 fillText 来绘制文本。

draw() 函数:主循环

最后,我们调用 draw() 函数。当用户调整窗口大小时,将运行相同的函数

每次窗口更改时都必须重绘。这个 draw() 函数最终成为某种主循环。

function draw()
{
    ctx.canvas.width  = window.innerWidth-5;
    ctx.canvas.height = window.innerHeight-5;
    
    ctx.fillStyle = "#000000";
    ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
    ctx.font = outputFont;
    ctx.fillStyle = fontColor;
    
    for (var i=0;i<allUserCmds.length;i++)
    {
        drawPrompt(i+1);
        if (i == 0)
        {
            xVal = promptWidth;
        }
        else
        {
            xVal = promptWidth-charWidth;
        }
            
        ctx.font = outputFont;
        ctx.fillStyle = fontColor;
        for (var letterCount = 0; letterCount < allUserCmds[i].length;letterCount++)
        {
            ctx.fillText(allUserCmds[i][letterCount], xVal, lineHeight * (i+1));
            xVal+=charWidth;
        }
    }
    if (currentCmd != "")
    {
        drawPrompt(Math.ceil(cursor.y/lineHeight));
        ctx.font = outputFont;
        ctx.fillStyle = fontColor;
        xVal = promptWidth-charWidth;
        for (var letterCount = 0; letterCount < currentCmd.length;letterCount++)
        {
            ctx.fillText(currentCmd[letterCount], xVal, cursor.y);
            xVal += charWidth;
        }
    }
    else
    {
        drawPrompt(Math.ceil(cursor.y/lineHeight));
    }
}

draw() 函数中需要关注的两件事

这里的有趣代码是我必须做的计算每个字母的宽度的工作——这样我才能将光标移到足够远的位置。而且,如果用户按退格键,我需要能够覆盖(擦除)该字符并将光标移回正确的位置。这项工作量很大,让我对创建控制台窗口的真实开发者们更加敬佩。:)

已键入命令列表

另外,请注意我将已键入命令列表保存在一个数组中,然后我必须在窗口大小调整或更改时重新绘制它们。这需要大量工作,但效果相当好。粗体代码循环显示了我遍历该列表以在屏幕上绘制它们。

闪烁光标,如同真实控制台

我认为这是让它感觉非常真实的一部分。在 initApp 中,我使用 JavaScript 方法 setInterval 设置了一个函数在指定的时间间隔运行。

设置看起来像:

setInterval(flashCursor,300);

这设置了一个回调方法——在这种情况下是我的名为 flashCursor 的函数——它将每 300 毫秒(0.3 秒)运行一次。

flashCursor 是一个非常简单的方法,看起来像:

function flashCursor(){
    
    var flag = flashCounter % 3;

    switch (flag)
    {
        case 1 :
        case 2 :
        {
            ctx.fillStyle = fontColor;
            ctx.fillRect(cursor.x,cursor.y,cursor.width, cursor.height);
            flashCounter++;
            break;
        }
        default:
        {
            ctx.fillStyle = "#000000";
            ctx.fillRect(cursor.x,cursor.y,cursor.width, cursor.height);
            flashCounter= 1;
        }
    }
}

我进行模除运算生成一个介于 1 和 3 之间的值,然后切换标志。这使我能够改变光标的颜色,使其有时是黑色的,不显示,然后其他时候是正常的字体颜色。这一切都造成了光标闪烁的错觉。

最后,我们将看看我编写的 showKeykeydownHandler 方法,这样你将拥有自己使用代码所需的所有知识。

function showKey(e){
    blotOutCursor();

    ctx.font = outputFont;
    ctx.fillStyle = fontColor;

    ctx.fillText  (String.fromCharCode(e.charCode),cursor.x, cursor.y);
    cursor.x += charWidth;
    currentCmd += String.fromCharCode(e.charCode);
}

当用户按下按键时,会调用 showKey。我获取 charCode 并使用 fillText 绘制它,使其显示在屏幕上。但然后我也必须管理屏幕上光标的位置,因为它现在会更靠右。

为什么要使用 KeyDown?

KeyDown 允许我捕获非打印字符,如退格键、回车键等。

对于这些中的每一个,我都需要做一些计算,然后绘制文本和光标到新的正确位置。

function keyDownHandler(e){
    
    var currentKey = null;
    if (e.code !== undefined)
    {
        currentKey = e.code;
        console.log("e.code : " + e.code);
    }
    else
    {
        currentKey = e.keyCode;
        console.log("e.keyCode : " + e.keyCode);
    }
    console.log(currentKey);
    // handle backspace key
    if((currentKey === 8 || currentKey === 'Backspace') && document.activeElement !== 'text') {
            e.preventDefault();
            // promptWidth is the beginning of the line with the c:\>
            if (cursor.x > promptWidth)
            {
                blotPrevChar();
                if (currentCmd.length > 0)
                {
                    currentCmd = currentCmd.slice(0,-1);
                }
            }
    }
    // handle <ENTER> key
    if (currentKey == 13 || currentKey == 'Enter')
    {
        blotOutCursor();
        drawNewLine();
        cursor.x=promptWidth-charWidth;
        cursor.y+=lineHeight;
        if (currentCmd.length > 0)
        {
            allUserCmds.push(currentCmd);
            currentCmd = "";
        }
    }
}

对于如此简单的事情,付出了惊人的工作量

对于看似如此简单的事情,付出了惊人的工作量,不是吗?

限制:无滚动

我没有实现滚动,所以如果你输入的命令太多以至于它们移出了屏幕,我还没有处理。我把这个留给你。

供你扩展

我创建这个愚蠢的应用是为了好玩和学习。它可以很容易地扩展来做更多的事情。接下来,你可以在用户键入特定命令时执行一些有趣的输出。这会很有趣,而且会欺骗很多人。

 

Using the Code

  1. 获取下载
  2. 解压缩
  3. 将其放入一个文件夹
  4. 双击 console.htm 文件,它将在你的默认浏览器中加载。

注意:显然,你的浏览器必须支持 HTML5 和 Canvas。

玩得开心。

历史

更新的文章(最后代码列表和修复处理退格键和回车键问题代码: 04-28-2016

文章首次发布:2015-01-27

© . All rights reserved.