如何使用 JavaScript 和 Canvas 创建星形图案
英文 | https://javascript.plainenglish.io/how-to-create-a-star-pattern-using-javascript-and-canvas-322f8af61ce1
翻译 | 杨小二
在本教程中,我们将使用 JavaScript 和 HTML5 Canvas API 创建下图横幅的星形图案。
因为即使创建一颗星也需要多行代码,所以我们将使用 drawJS——我构建的一个处理星形渲染的迷你库。
在此过程中,我们将使用对象字面量、嵌套的 for 循环和余数运算符来创建这种模式——以及一个 CSS 媒体查询来使横幅在窄屏幕宽度下调整大小。
使用 drawJS 的星形图案演示地址:https://codepen.io/nevkatz/pen/jOmexaK
drawStar 方法
图案是通过为图案中的每个星星调用 drawJS 库中的 drawStar 方法来创建的。假设我们只想创建图案中的第一颗星,如下所示。
在这种情况下,我们可以使用下面的代码调用一次 drawStar。
drawJS.drawStar({
cx:50,
cy:50,
numPoints:5,
stroke:'#622569',
fill:'#b8a9c9',
lineWidth:4,
innerRadius: 20,
outerRadius:35
});
现在,由于我们想要创建多个,因此,我们最终将编写一些更复杂的 JavaScript。但首先,让我们编写 HTML 和 CSS 并思考模式的特征。
打好基础
让我们从一些标记和样式开始。
HTML 和 CSS
HTML中的<canvas> 元素。
<canvas id="myCanvas" width="1300" height="600"></canvas>
我们的第一个 CSS 将使其成为块元素并将其居中。
canvas {
display: block;
margin: 10px auto;
}
为了使画布具有响应性,我们需要添加一个媒体查询,它将在屏幕宽度低于 1200 像素时将宽度设置为 100%。
我们还可以使用height: auto 来帮助我们的画布在调整大小时保持其纵横比。
@media screen and (max-width: 1200px) {
canvas {
width: 100%;
height: auto;
}
}
规划模式
在我们开始编写最初的 JavaScript 之前,让我们再次浏览一下星形模式并考虑制作它需要什么。
下面是模式的前两行。
要以这种方式排列星星,我们需要编写设置几个值的逻辑:
将星星隔开
连续的星星数
行数
我们的程序还需要设置每颗星星的独特属性,包括以下内容:
点数
描边颜色
填充颜色
星星还有一些共同的特性:
边界宽度
内半径,或恒星中心到每个角的距离
外半径,或从恒星中心到每个外点的距离
最后,我们需要设置以下内容:
第一颗星的位置
笔触和填充颜色的调色板(每种四种颜色)
最小和最大星点数
由于这是一种模式,因此应该定期重复变化的星形属性。例如,你会注意到点数从 5 开始,然后从一颗星到下一颗增加 1,直到达到 12 点。一旦发生这种情况,下一位明星将获得 5 分。
这些是我们需要转换为代码的模式属性。
设置模式属性
现在我们已经概述了模式的要求,让我们卷起袖子,用两个函数编写它的逻辑:
getPatternProps,它建立星形图案的属性。
init,它将遍历每个星星的位置,设置每个星星的属性,并调用 drawJS.drawStar 方法。
getPatternProps 函数将返回一个包含我们模式属性的对象。
function getPatternProps() {
let patternProps = {
start: {
x: 50,
y: 50,
},
numPoints: {
min: 5,
max: 12
},
space: {
x: 100,
y: 100
},
count: {
x: 12,
y: 6
},
fills: ['#b8a9c9', '#98d1d6', '#fed8b1', '#cff7d5'],
strokes: ['#622569', '#4C858A', '#f06d06', '#008000']
};
return patternProps;
}
让我们来看看发生了什么:
start 将第一颗星的 x 和 y 坐标设置为 (50,50)。
start: { x:50, y:50 }
numPoints 将使星星可以拥有的最少点数设为 5,并将最大点数设置为 12。
numPoints: { min:5, max:12 }
space 将使星星在垂直和水平方向上相距 100 像素。
space: {x: 100, y: 100 }
count 告诉我们在一行 (x) 和六行 (y) 中有 12 颗星。
count: { x: 12, y: 6}
fills 将确定作为重复序列出现的四种填充颜色。
fills: ['#b8a9c9', '#98d1d6', '#fed8b1', '#cff7d5']
笔画以相同的方式工作,使用数组来设置星形边框颜色的重复序列。
strokes: ['#622569', '#4C858A', '#f06d06', '#00ff00']
启动我们的 init 函数
我们的 init 函数将执行以下操作:
设置图案的背景颜色
检索模式属性
遍历每个星位置
设置每颗星的属性
为每个星星调用 drawJS.drawStar
让我们启动我们的 init 函数并调用 setBackground,一个用于设置画布背景颜色的 drawJS 方法。
function init() {
drawJS.setBackground('#215875');
}
此 setBackground方法使用 Canvas API 的 rect 和 fill 方法,并且是你将在库中看到的第一个方法。
现在,我们有了背景颜色,让我们通过调用 getPatternProps 来获取星形图案的属性。
function init() {
drawJS.setBackground('#215875');
let props = getPatternProps();
}
设置循环
在我们的 init 函数中,让我们创建一个填充星星的嵌套循环。我们将逐步建立这个。
让我们首先遍历行。我们可以使用 props.count.y 值访问行数。
let props = getPatternProps();
for (var y = 0; y < props.count.y; ++y) {
}
现在让我们看看 props.count.x 中每行的星星数。
for (var y = 0; y < props.count.y; ++y) {
for (var x = 0; x < props.count.x; ++x) {
}
}
我们现在拥有的这些 x 和 y 索引将派上用场。
这是我们的 init 函数的开始:
function init() {
// set background on canvas
drawJS.setBackground('#215875');
// get star properties
let props = getPatternProps();
// loop through rows
for (var y = 0; y < props.count.y; ++y) {
// loop through stars in a row
for (var x = 0; x < props.count.x; ++x) {
}
}
}
好的,这是 init 函数的一个很好的开始。到目前为止,我们已经设置了我们的背景颜色,定义了我们的图案属性,并设置了我们的循环。
确定每个星星的位置
在这个嵌套循环中,让我们想象一下它正在绘制的当前星星。要确定这颗星星的位置,它必须使用 props 中的 start 和 space 属性。
我们的目标是计算恒星的中心坐标并将它们存储在两个变量中:cx 和 cy。
让我们从星星的左上角位置开始:props.start.x 和 props.start.y。那是在横幅的左上角,第一颗星星的中心所在。
接下来,让我们找出每颗星星相对于第一颗星星的位置应该在哪里。这取决于它在序列中的位置(x 和 y)以及星间距(props.space.x 和 props.space.y)。
要找出离第一个位置有多远,让我们将当前的 x 或 y 索引乘以相应的空间值。
props.space.x * x
props.space.y * y
下面的表达式将 props.start.x 添加到 props.space.x 和 x 的乘积,给我们当前恒星的水平位置。
let cx = props.start.x + props.space.x * x;
这个表达式以同样的方式为我们提供了垂直位置。
let cy = props.start.y + props.space.y * y;
我们现在可以获得每颗恒星的中心。
找出每颗星星的点数
在模式中,点数增加,直到达到 12。之后,我们回到 5。然后我们再次开始增加。但是我们如何确定每颗星星上的点数呢?为了找到这个,我们首先需要知道每个恒星在序列中的位置。
如果我们把整个模式看作一个数组,这个位置可以看作是星星的索引。
寻找明星指数
下面你可以看到每个星星的索引在模式的前两行中应该是什么。
我们可以在循环外声明一个计数器变量并每次递增它,但让我们用数学方法找到索引,以便我们可以更多地探索这些 x 和 y 值。
让我们首先找到当前星星所在行上方的行数,即 y。现在让我们将 y ,先前行的数量,乘以 props.count.x,每行的星星数。
y * props.count.x
如果我们当前的星星在第三排呢?
我们知道 props.count.x 总是 12。
在我们的第三行中,y 是 2。
所以上面几行的星星数是 12 • 2 = 24。
然后我们应该添加 x,它是当前行中先前星的数量。然后我们可以用这个方程定义 star_idx。
let star_idx = x + props.count.x + y;
寻找点的范围
当我们从一颗星到下一颗时,我们希望点数从 5 增加到 12,然后从 5 重新开始并重复。
要知道何时从 5 点重新开始,我们必须知道我们可以拥有多少个不同的点数。所以让我们找出最小点数和最大点数之间的范围,包括 5 和 12。
在代码中,我们将称其为 diff。
let diff = props.numPoints.max - props.numPoints.min + 1;
由于 star.numPoints.max 是 12 并且 star.numPoints.min 是 5,因此在这种情况下 diff 最终为 8。
使用余数运算符
现在让我们找出每颗星星的点数。每颗星至少有五分,但有些可能有更多分。
为了计算出还有多少,我们将使用 num、星级索引和我们刚刚计算的差异。我们使用余数运算符来查找将 num 除以 diff 所得的余数。
let morePoints = num % diff;
morePoints 可以得到的最高值是 7。例如,15 除以8,我们将 15 除以 8 得到的余数是 7。如果我们增加到 16,那么 16 除以8 会重置为零。以下是两行中这种重复增加的情况:
为了获得当前星星上的点数,我们取 morePoints 并添加最小点数——props.numPoints.min——即 5。
let numPoints = props.numPoints.min + morePoints
以下是两行中点数的外观:
万岁!已找到每颗星上的点数。
使星星变小
为了使星星变小,让我们放置一个从 1 开始但在每行之后逐渐变小的乘数。
let multiplier = (props.count.y - y) / props.count.y;
因为我们的行索引 y 从零开始,props.count.y — y 在我们的第一行中是 6 — 0 或 6。
结果,props.count.y — 1 / props.count.y = 6/6 = 1。
但在第二行,y 是 1。
所以 props.count.y — y 是 6 — 1 或 5。
因此,如果我们四舍五入到最接近的百分位,props.count.y — 1 / props.count.y = 5/6 = 0.83。
我们现在可以使用乘数来调整星星的外半径和内半径,我们将分别初始化为 35 和 20。
let outerRadius = 35, innerRadius = 20;
现在让我们用乘数改变每一个。
outerRadius *= multiplier;
innerRadius *= multiplier;
完整的乘法器逻辑如下所示:
查找颜色、线宽和旋转
现在我们想要一个重复的颜色模式。请记住,我们有四种笔触颜色和四种填充颜色,正如我们在 stars 对象中的数组中所指定的:
fills: ['#b8a9c9', '#98d1d6', '#fed8b1', '#cff7d5'],
strokes: ['#622569', '#4C858A', '#f06d06', '#008000']
为了确定填充和笔画,我们将再次使用余数运算符,但这一次是为了确定笔画和填充数组中应该有什么索引,我们将假设它们具有相同的长度。我们将此值称为 color_idx。
let color_idx = num % props.fills.length
我们的 props.fills 数组的长度为 4,它成为我们的除数——所以我们可能的余数值在 0 到 3 之间。
为了获得填充和描边,我们使用 color_idx 索引到每个数组。
let fill = props.fills[color_idx];
let stroke = props.strokes[color_idx];
一旦一颗星星用数组中的最后一种颜色渲染,程序就会为下一颗星星使用第一种颜色。
最后,让我们设置 lineWidth 和rotate,这对每个星星都是一样的:
const lineWidth = 4, rotate = 0;
至此,我们需要的所有属性都已经设置好了!
回顾
让我们回顾一下我们现在拥有的九个属性。
cx 和 cy 是恒星中心点的坐标。
externalRadius 和innerRadius 决定了恒星的大小。
填充和描边是星星的颜色。
每颗星的线宽和旋转保持不变。
LnumPoints 确定每颗星上的点。
现在让我们将所有这些打包成一个对象,并将其直接传递给 drawStar 方法。
drawJS.drawStar({
cx,
cy,
outerRadius,
innerRadius,
numPoints,
lineWidth,
stroke,
fill,
rotate
});
由于每个键的名称都与其在该对象中的值相同,因此我们可以对键和值使用一个术语——例如,cx 代替 cx:cx,rotate 代替rotate:rotate。
这是完成的 init 函数的样子。
function init() {
// set background on canvas
drawJS.setBackground('#215875');
// get star properties
let props = getPatternProps();
// loop through rows
for (var y = 0; y < props.count.y; ++y) {
// loop through columns
for (var x = 0; x < props.count.x; ++x) {
// determine star's center position
let cx = props.start.x + props.space.x * x;
let cy = props.start.y + props.space.y * y;
// determine number of points
let star_idx = x + props.count.x * y;
let diff = props.numPoints.max - props.numPoints.min + 1;
let morePoints = star_idx % diff;
let numPoints = props.numPoints.min + morePoints;
// determine star size
let multiplier = (props.count.y - y) / props.count.y
let outerRadius = 35, innerRadius = 20;
outerRadius *= multiplier;
innerRadius *= multiplier;
// determine star color
let color_idx = star_idx % props.fills.length;
let fill = props.fills[color_idx];
let stroke = props.strokes[color_idx];
// determine linewidth and rotation
const lineWidth = 4, rotate = 0;
// call the method
drawJS.drawStar({
cx,
cy,
outerRadius,
innerRadius,
numPoints,
lineWidth,
stroke,
fill,
rotate
});
} // end inner loop
} // end outer loop
}
就是这样!这就是代码。下面是我们的演示,因此,你可以看到所有内容如何组合在一起。
查看演示地址:https://codepen.io/nevkatz/pen/jOmexaK
总结
恭喜你!到这里已完成本教程,你已经成功地使用了许多 JavaScript 概念以及相当多的数学来创建这个星形图案。如果你想了解这方面的更多信息,以下是我建议的一些后续步骤:
修改 getPatternProps 以便你可以想出不同的模式。
尝试为一颗星星制作动画——然后尝试将多颗星星排列成一个图案。
尝试使用 Math.random() 和一些模式修改来创建夜空。
希望你能从我的这篇文章中学会用 JavaScript 和 Canvas 绘制星星。
谢谢阅读!
学习更多技能
请点击下方公众号