前言
1.创作背景
前些天庆祝1024的时候,我用java写了一个《星运测试:1024特别版》桌面程序。大致功能是通过让用户回答几道测试题,来给出未来一年的运势预测。我本想把jar包打包成exe文件,发给一位特别的朋友,但是试了好几种方法,无果而终。
于是最近我就把这个程序复刻为网页版,《星运测试:万圣夜特别版》。成品是一个html文件,可以发给朋友,用浏览器打开。效果如上图。
2.阅读本文需要的编程基础
(1)基本的html和css语法
(2)基本的JavaScript语法(按钮单击事件、自定义函数、画布canvas的2d简单应用等)
3.我的编程环境
(1)64位win10系统,代码编辑器VS Code
(2)已经亲自测试通过的浏览器
①windows系统自带的Edge浏览器
②360安全浏览器(默认为极速模式打开,一切正常;手动切换为兼容模式(IE内核)时,汉字会显示乱码)
③手机浏览器(我没有做响应式布局,只做了电脑端网页,但是用我小米手机自带的浏览器可以正常打开。)
测试时间:2024年10月30日
ps.理论上来说,支持html5的浏览器(绝大多数现代浏览器)都可正常打开这个网页
目录
一、目标效果
1.功能
在页面上依次显示几道运势测试的题目,用户点击按钮进行选择,最后程序给出运势测试结果。
2.文件
前端静态网页,且只有一个html文件。没有其它文件,也不走服务器。
3.页面布局
采用最简单的流式布局。
①顶部一个横幅(banner),其中用画布(canvas)画一些万圣夜元素的图案;
②中间一个文本区域(textarea),用于显示测试题目及测试结果;
③底部四个按钮(button),显示测试题的选项。
ps.我没有做响应式布局,但是我自己的手机浏览器可以打开,只是"关闭页面"这个按钮无效。
二、实现方法
本文最后会给出完整的代码,这里只说一些关键性的部分。
1.定义全局变量
注:
这个网页做好之后,以后某个时候可能会回来修改文案。修改文案只需要修改以下5个全局变量即可。以下这些全局变量的声明,我都放在html的<head>部分的JS代码中。我在代码中做了标记,一打开代码,可以比较直观地看到可以自定义文案的区域,而其它区域的代码无需改动。这样,以后打开代码修改文案时比较方便,不容易误改代码。
①当前页面编号pageIndex
程序要依次显示多个页面,所以可声明一个pageIndex变量来存储当前显示的页面编号,这相当于程序的进度条。初始值为0。
②最后一个页面的编号pageLast
再用一个pageLast变量来存储最后一个页面的编号。在程序其它地方都只调用这个变量,而不使用具体的页面编号。这样,程序的扩展性比较好。之后如果要修改文案,比如要增减页面的个数,那么只要更改变量的初始值即可。
③存储问题及选项的数组questions[][]
测试的问题及对应的选项,用一个二维数组question[][]来存储。行数应该等于页面个数,列数应该是5(一个问题加上4个选项)。有些页面可能是导语(或者其它情况),不需要那么多选项,那么选项的内容就用"0"来表示。这是我自定义的,可以修改,但是不能没有。
④存储结语库的数组results[][]
测试的每一个选项对应的一句结语,用另一个二维数组results[][]来存储。在用户对每一个页面按下按钮后,程序都要根据用户按下的按钮的编号,来决定将数组中哪一句结语添加到最后要给用户生成的运势测试结果中。这里假定最后一个页面是用来呈现测试结果的,那这最后一个页面就没有结语需要存储了。所以这个数组的行数应该是页面个数-1,列数是4(即选项的个数)。
⑤存储用户个性化结语的数组yourResult[]
还要有一个一维数组yourResult[],用来存储用户选的选项所对应的结语。待用户回答完所有问题后,只需要将这个数组中所有元素串接起来,就是要给用户生成的测试结果了。同样地假定最后一个页面是用来呈现测试结果的,所以这个数组的长度应该是页面个数-1。
2.按钮点击事件
用户每次按下按钮之后,程序要做两件事:一是要根据用户的选择进行处理(把相应的一句结语添加到yourResult[]数组中,以便最后能为用户生成运势测试结果);二是要刷新页面,即显示下一个页面。
(1)根据选项添加对应结语
先来说第一件事。
这是四个选项都有的功能,所以我写了一个通用的函数。函数功能是,根据用户按下的按钮编号,到结语库数组results[][]中,查找对应的那句结语,存储到用户的个性化结语数组yourResult[]中。
然后要将进度条+1,为显示下一个页面做好准备。
代码如下。
javascript
// 定义一个函数来实现按钮单击时的通用功能
function clickGeneral(buttonIndex){ //处理当前非最后一个页面时的情况
//处理选项所对应的结语
yourResult[pageIndex] = results[pageIndex][buttonIndex]; //将当前选项相应结语添加到为用户生成的结语数组中
if(pageIndex == pageLast-1){ //如果当前已经是最后一道题的页面,接下来是结语页面
questions[pageLast][0] = yourResult.join("\n"); //将为用户生成的结语数组用换行符串接成一个字符串,存储到最后一个页面的题干文本中
}
//显示下一道题目
pageIndex++; //进度条变到下一个页面
displayQuestion(pageIndex); //显示下一个页面的文本
}
而在按钮的单击事件中,只要调用这个函数,并且将按钮的编号作为参数传递即可。
javascript
// 为按钮3添加点击事件监听器
button3.addEventListener('click', function() {
clickGeneral(2); //采用通用处理程序
});
(2)显示下一个页面
这里我写了一个通用的函数,用于显示指定页面编号的页面内容。这个函数的基本内容如下。
javascript
// 定义一个函数来显示指定编号的页面
function displayQuestion(index) { //index是题目序号,从0开始
// 获取题干和选项的文本
var question = questions[index][0];
var options = questions[index].slice(1);
//获取页面元素
var ta = document.getElementById("textarea1");
var b1 = document.getElementById("button1");
var b2 = document.getElementById("button2");
var b3 = document.getElementById("button3");
var b4 = document.getElementById("button4");
// 判断按钮是否应该显示
// 显示题干和选项的文本
ta.innerHTML = question;
b1.innerHTML = options[0];
b2.innerHTML = options[1];
b3.innerHTML = options[2];
b4.innerHTML = options[3];
}
在这个函数中,要单独处理的一个地方是,要判断每个选项的内容是否为"0"(这是我自定义的标记,你也可以定义别的,但是不能不写),如果是,那说明这个页面不应该显示出这个选项按钮,所以要设置按钮不可见,同时腾出空间;否则就要设置可见。这部分代码如下。
javascript
// 判断按钮是否应该显示
//按钮1
if("0" === questions[index][1]){ //如果选项文本为"0"
b1.style.display = "none"; //则设置相应按钮不可见,且腾出空间
}else{
b1.style.display = "block"; //否则设置为可见
}
3.使用canvas画图
canvas功能很强大,这里我只使用了canvas 2d的很简单的应用。我用到了以下几个方面的语法:填充背景、画线条/画填充图形、画直线/画弧线、写文字。具体画图的内容很简单,只要找到合适的参数即可。这里我把基本语法总结如下。
(1)绘制背景
首先要设置画布元素的2D渲染上下文,然后设置高度和宽度,再绘制背景。
javascript
var canvas = document.getElementById("canvas"); // 获取页面的canvas元素
var ctx = canvas.getContext('2d'); // 获取画布的 2D 渲染上下文
// 绘制背景
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
(2)画线条/填充图形
无论是直线还是弧线,JS中都分为"线条描边"和"填充图形"两种类型(名字是我自己起的),使用的语法不同。
①绘制线条
javascript
// 绘制直线线条 示例代码
ctx.beginPath(); //开始路径
ctx.strokeStyle = "black"; //设置画笔颜色
ctx.lineWidth = 8; //设置线条宽度
//画点什么
ctx.stroke(); //绘制轮廓
ctx.closePath(); //结束路径
②绘制填充图形
javascript
//绘制填充图形
ctx.beginPath(); //开始一条路径
ctx.fillStyle = "orange"; //画笔颜色
//画点什么
ctx.closePath(); //结束一条路径
ctx.fill(); //填充图形
(3)画直线/弧线
①画直线
javascript
//绘制填充图形
ctx.beginPath(); //开始一条路径
ctx.fillStyle = "orange"; //画笔颜色
//画直线
ctx.moveTo(100, 75); //移动画笔
ctx.lineTo(110, 95); //画一条直线
ctx.lineTo(120,120); //再画一条
// 继续画
ctx.closePath(); //结束一条路径
.fill(); //填充图形
②画弧线
javascript
//绘制填充图形
ctx.beginPath(); //开始一条路径
ctx.fillStyle = "orange"; //画笔颜色
//画一条弧线
ctx.arc(120, 70, 50, 0, Math.PI * 2);
// 继续画
ctx.closePath(); //结束一条路径
.fill(); //填充图形
//arc()函数 参数解释
x:弧的圆心的x坐标。单位是像素。
y:弧的圆心的y坐标。
radius:弧的半径。
startAngle:弧的起始角度,以弧度表示。三点钟方向为0,逆时针为正。
endAngle:弧的结束角度,以弧度表示。
anticlockwise(可选):一个布尔值,表示绘制弧形是顺时针还是逆时针方向,默认为false,即顺时针方向。
(4)写文字
javascript
// 绘制文字 示例
ctx.font = 'bold 48px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.fillText('Happy Halloween!', 30, 50); //文字内容及宽高
三、完整代码
文案自拟。
html
<!--Coded by Dr_Cheeze on 26 October 2024. -->
<!DOCTYPE html>
<html>
<head>
<title>2024万圣夜快乐!</title>
<script>
/*
************************************************************
********************以下文案可自定义*************************
*/
// 声明全局变量
// 声明关于页面编号的变量
var pageIndex = 0; //存储当前页面编号,从0开始
var pageLast = 6; //存储最后一个页面的编号,即页面总数-1
// 创建一个二维数组来存储题干和选项
var questions = [
["嗨,朋友!\nHappy Halloween!\n这里是【快乐星运:万圣夜特别版】。\n\n接下来将会有4道题目,\n为你测试未来一年内的运势。", "开始吧", "0", "0", "0"],
["第1题\n题干内容", "A.选项 ", "B.选项", "C.选项", "D.选项"],
["第2题\n题干内容", "A.选项 ", "B.选项", "C.选项", "D.选项"],
["第3题\n题干内容", "A.选项 ", "B.选项", "C.选项", "D.选项"],
["第4题\n题干内容", "A.选项 ", "B.选项", "C.选项", "D.选项"],
["Ok!下面揭晓测试结果。", "好的", "0", "0", "0"],
["", "关闭页面", "再来一次", "0", "0"]
];
// 创建一个二维数组来存储题目选项对应的结语
var results = [
["【快乐星运】测试结果:\n你在接下来的一年里,","","",""],
["结语1-A",
"结语1-B",
"结语1-C",
"结语1-D"],
["结语2-A",
"结语2-B",
"结语2-C",
"结语2-D"],
["结语3-A",
"结语3-B",
"结语3-C",
"结语3-D"],
["结语4-A",
"结语4-B",
"结语4-C",
"结语4-D"],
["祝你好运!\n\n","","",""]
];
// 创建一个一维数组来存储为用户生成的结语
var yourResult = ["","","","","",""]; //数组长度应该=pageLast
/*
********************以下代码慎动*****************************
************************************************************
*/
// 定义一个函数来显示横幅内容
function showBanner(){
// 声明变量
var bananer = document.getElementById("banner"); // 获取页面的banner元素
var canvas = document.getElementById("canvas"); // 获取页面的canvas元素
var container = document.getElementById("container"); // 获取页面的container元素
var ctx = canvas.getContext('2d'); // 获取画布的 2D 渲染上下文
var width = container.offsetWidth; //获取container整体大小,即css样式所设置的大小
var height = banner.offsetHeight; //offsetWidth只读,width可读可写,是内容宽度
//设置画布的内容大小
canvas.width = width
canvas.height = height;
// 清除画布
ctx.clearRect(0, 0, width, height);
// 绘制背景
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height);
// 绘制文字
ctx.font = 'bold 48px Arial';
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.fillText('Happy Halloween!', width / 2, height / 1.7);
// 绘制南瓜灯的轮廓
ctx.beginPath();
ctx.fillStyle = "orange";
ctx.arc(120, 70, 50, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// 绘制南瓜灯的帽子
ctx.beginPath();
ctx.fillStyle = "green";
var hatStart = Math.PI /6;
var hatEnd = 5 * Math.PI /6;
ctx.arc(120,5,30, hatStart,hatEnd,false);
ctx.lineTo(120,7);
ctx.closePath();
ctx.fill();
// 绘制南瓜灯的眼睛
ctx.beginPath();
ctx.fillStyle = "black";
ctx.arc(100, 55, 10, 0, Math.PI * 2);
ctx.arc(140, 55, 10, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
// 绘制作者信息
ctx.font = 'normal 12px Arial';
ctx.fillStyle = 'silver';
ctx.textAlign = 'center';
ctx.fillText('Coded by Dr_Cheeze', width/6*4, height/7*6);
// 绘制南瓜灯的嘴巴
ctx.beginPath();
ctx.strokeStyle = "black";
ctx.lineWidth = 8;
ctx.moveTo(100, 75);
ctx.lineTo(110, 95);
ctx.lineTo(120, 85);
ctx.lineTo(130, 95);
ctx.lineTo(140, 75);
ctx.stroke();
ctx.closePath();
// 绘制糖果
ctx.beginPath(); //左边糖纸
ctx.fillStyle = "rgb(0,162,232)"; //浅蓝色
ctx.moveTo(754, 42);
ctx.lineTo(740, 11);
ctx.lineTo(715, 60);
ctx.lineTo(747, 53);
ctx.closePath();
ctx.fill();
ctx.beginPath(); //中间糖体
ctx.fillStyle = "olive";
ctx.moveTo(762, 26);
ctx.lineTo(739, 68);
ctx.lineTo(821, 97);
ctx.lineTo(842, 58);
ctx.lineTo(762, 26);
ctx.closePath();
ctx.fill();
ctx.beginPath(); //右边糖纸
ctx.fillStyle = "teal";
ctx.moveTo(832, 74);
ctx.lineTo(865, 65);
ctx.lineTo(842, 114);
ctx.lineTo(826, 85);
ctx.closePath();
ctx.fill();
ctx.beginPath(); //纹路1
ctx.fillStyle = "silver";
ctx.moveTo(794, 42);
ctx.lineTo(749, 57);
ctx.lineTo(751, 65);
ctx.lineTo(798, 49);
ctx.closePath();
ctx.fill();
ctx.beginPath(); //纹路2
ctx.fillStyle = "orange";
ctx.moveTo(826, 53);
ctx.lineTo(771, 75);
ctx.lineTo(781, 79);
ctx.lineTo(831, 57);
ctx.lineTo(826, 53);
ctx.closePath();
ctx.fill();
}
// 定义一个函数来显示指定编号的页面
function displayQuestion(index) { //index是题目序号,从0开始
// 获取题干和选项的文本
var question = questions[index][0];
var options = questions[index].slice(1);
//获取页面元素
var ta = document.getElementById("textarea1");
var b1 = document.getElementById("button1");
var b2 = document.getElementById("button2");
var b3 = document.getElementById("button3");
var b4 = document.getElementById("button4");
// 禁止用户编辑文本
if(index==0){
ta.setAttribute("readonly", "true"); //将文本区域设置为只读
}
// 判断按钮是否应该显示
//按钮1
if("0" === questions[index][1]){ //如果选项文本为"0"
b1.style.display = "none"; //则设置相应按钮不可见,且腾出空间
}else{
b1.style.display = "block"; //否则设置为可见
}
//按钮2
if("0" === questions[index][2]){ //如果选项文本为"0"
b2.style.display = "none"; //则设置相应按钮不可见,且腾出空间
}else{
b2.style.display = "block"; //否则设置为可见
}
//按钮3
if("0" === questions[index][3]){ //如果选项文本为"0"
b3.style.display = "none"; //则设置相应按钮不可见,且腾出空间
}else{
b3.style.display = "block"; //否则设置为可见
}
//按钮4
if("0" === questions[index][4]){ //如果选项文本为"0"
b4.style.display = "none"; //则设置相应按钮不可见,且腾出空间
}else{
b4.style.display = "block"; //否则设置为可见
}
// 显示题干和选项的文本
ta.innerHTML = question;
b1.innerHTML = options[0];
b2.innerHTML = options[1];
b3.innerHTML = options[2];
b4.innerHTML = options[3];
}
// 定义一个函数来实现按钮单击时的通用功能
function clickGeneral(buttonIndex){ //处理当前非最后一个页面时的情况
//处理选项所对应的结语
yourResult[pageIndex] = results[pageIndex][buttonIndex]; //将当前选项相应结语添加到为用户生成的结语数组中
if(pageIndex == pageLast-1){ //如果当前已经是最后一道题的页面,接下来是结语页面
questions[pageLast][0] = yourResult.join("\n"); //将为用户生成的结语数组用换行符串接成一个字符串,存储到最后一个页面的题干文本中
}
//显示下一道题目
pageIndex++; //进度条变到下一个页面
displayQuestion(pageIndex); //显示下一个页面的文本
}
// 定义一个函数来获取当天日期,并存储到结语数组中
function getDateToArray(){
var currentDate = new Date(); //创建一个日期对象
var year = currentDate.getFullYear(); //获取年、月、日
var month = currentDate.getMonth() + 1; // 注意月份从0开始,所以要加1
var day = currentDate.getDate();
var dateString = year + '年' + month + '月' + day + '日'; //自由组合成想要的日期形式
results[pageLast-1][0] = results[pageLast-1][0] + dateString; //把日期字符串存储到数组中
}
</script>
<style>
/* 定义横幅样式 */
#banner {
width: 100%;
height: 130px;
margin:0;
padding:0;
background-color:black;
display: flex; /*采用流式布局*/
justify-content: center; /*子元素水平居中*/
}
/* 定义容器样式 */
.container {
width: 900px; /*容器宽度,同时也是页面、横幅内画布的宽度*/
margin: 0 auto;
padding: 20px;
}
/* 定义文本区域样式 */
textarea {
width: 100%;
height: 420px;
resize: none;
font-size: 28px;
line-height:1.5; /*设置行间距*/
white-space: pre-wrap; /* 可以保留空格和换行符 */
overflow-wrap: break-word; /* 文本会在单词间换行,确保不会超出容器 */
overflow-y: auto; /*当文本超出区域时,自动出现垂直滚动条*/
resize:vertical; /*允许用户手动调节高度*/
}
/* 定义按钮容器样式 */
.buttons {
display: flex;
justify-content: space-evenly; /*元素均匀分布*/
margin-top: 10px;
}
/* 定义按钮样式 */
button {
padding: 20px;
font-size: 20px;
}
</style>
</head>
<body>
<!-- 横幅元素 -->
<div id="banner">
<canvas id="canvas">
<p>此浏览器不支持canvas,图片无法显示。请换一款浏览器吧!</p>
</canvas>
</div>
<div id="container" class="container">
<!-- 表单元素 -->
<form>
<!-- 文本区域 -->
<textarea id="textarea1" name="content" placeholder="请输入文本"></textarea>
<!-- 按钮容器 -->
<div class="buttons">
<!-- 按钮1 -->
<button id="button1" type="button">按钮1</button>
<!-- 按钮2 -->
<button id="button2" type="button">按钮2</button>
<!-- 按钮3 -->
<button id="button3" type="button">按钮3</button>
<!-- 按钮4 -->
<button id="button4" type="button">按钮4</button>
</div>
</form>
</div>
<script>
//显示初始页面
showBanner(); //显示横幅
getDateToArray(); //获取当天日期,并存储到results数组中
displayQuestion(0); //显示编号为0的页面
// 为按钮1添加点击事件监听器
button1.addEventListener('click', function() {
if(pageIndex == pageLast){ //如果是最后一个页面,则这个选项对应的是"关闭测试"
window.close(); //关闭网页
}else{ //如果不是最后一个页面,则采用通用处理程序
clickGeneral(0);
}
});
// 为按钮2添加点击事件监听器
button2.addEventListener('click', function() {
if(pageIndex == pageLast){ //如果是最后一个页面,则这个选项对应的是"再来一次"
pageIndex = 0; //将进度条拨回编号为0的页面,重新开始测试
displayQuestion(pageIndex); //显示初始页面
}else{ //如果不是最后一个页面,则采用通用处理程序
clickGeneral(1);
}
});
// 为按钮2添加右击事件监听器
button2.addEventListener("contextmenu", function(event) {
if(pageIndex == pageLast){ //如果是最后一个页面,则这个选项对应的是"再来一次"
event.preventDefault(); // 阻止默认右击菜单的弹出
var caidan = "Coded by 奶酪博士 2024.10.26";
var ta = document.getElementById("textarea1"); //获取文本区域元素
ta.textContent = caidan; //在文本区域显示文本
}
});
// 为按钮3添加点击事件监听器
button3.addEventListener('click', function() {
clickGeneral(2); //采用通用处理程序
});
// 为按钮4添加点击事件监听器
button4.addEventListener('click', function() {
clickGeneral(3); //采用通用处理程序
});
</script>
</body>
</html>
<!--由奶酪博士编写于2024年10月26日。-->