blog icon indicating copy to clipboard operation
blog copied to clipboard

【译】催眠方块—Hypnotic Squares

Open JChehe opened this issue 5 years ago • 0 comments

原文:Hypnotic Squares

William Kolomyjec 的工作让我们再次想起一些以前(old school)的生成艺术,专注于简单图形、平铺和递归。

今天我们要复现他的作品之一——催眠方块。

页面中仅有一个 320x320 像素的 <canvas>

老规矩,以下是初始化代码,其中包括设置 canvas 大小和使用 window.devicePixelRatio 缩放 canvas 以适配视网膜屏幕。

var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');

var size = window.innerWidth;
var dpr = window.devicePixelRatio;
canvas.width = size * dpr;
canvas.height = size * dpr;
context.scale(dpr, dpr);
context.lineWidth = 2;

现在,需要定义一些变量和创建 draw 函数。该函数会被递归调用,不断在方块内绘制方块,直至方块达到指定的最小尺寸。

如果对递归不太了解也没关系,后续会讲解。

var finalSize = 10;
var startSize = size;
var startSteps = 5;

function draw(x, y, width, height, xMovement, yMovement, steps) {
  // We will fill this in
}

draw(0, 0, startSize, startSize, 0, 0, startSteps);

draw 函数内的 steps 参数 代表方块递归的次数。目前是固定值,但随后我们会赋予它一些随机性。

finalSize 是绘制方块的最小尺寸,即当方块达到此尺寸时,我们将停止绘制。

startSteps 在递归时用于计算越来越小的方块尺寸。

我们先在 draw 函数内单纯画一个方块。

context.beginPath();
context.rect(x, y, width, height);
context.stroke();

01

现在有了一个方块,接着进行递归操作,即 draw 函数会调用自身多次,直至满足某个条件。该条件非常重要,否则会造成死循环。这里我们使用 steps 作为倒数的开始点。

if(steps >= 0) {
  var newSize = (startSize) * (steps / startSteps) + finalSize;
  var newX = x + (width - newSize) / 2
  var newY = y + (height - newSize) / 2
  draw(newX, newY, newSize, newSize, xMovement, yMovement, steps - 1);
}

02

哇喔,这就是递归!下面让我解释一下上述代码。

  • newSize 基于剩余方块数量计算得出。
  • newX & newY 确保新方块能放置在上一层方块内的合适位置上。
  • steps - 1 是 draw 函数的最后一个参数,它会越来越接近 0。

不同的 startSteps 就会有不同的递归程度。

var startSteps = 8

03

var startSteps = 4

04

当为该值赋予随机数时会有不同的效果。但现在先让我们添加其他效果。xMovementyMovement 两个变量是用于将方块放置在特定方向上。

先更改 draw 函数的调用参数,即 xMovement 和 yMovement 均置为 1。此举是为了让方块放置在右下方。

draw(0, 0, startSize, startSize, 1, 1, startSteps);

然后计算下方变量。

newX = newX - ((x - newX) / (steps + 2)) * xMovement
newY = newY - ((y - newY) / (steps + 2)) * yMovement

05

这看起来有点复杂。我们计算了相邻方块的间距,然后将其按照剩步数(译者注:代码中是 steps + 2)切分。+2 是为了确保新方块永远不会触碰到上一个方块的边界(译者注:若 + 1,则当 steps 最后为 0 时,会导致发生碰撞)。

依次更改 draw 函数的 xMovementyMovement 两个参数,你将会看到它是如何移动的。

draw(0, 0, startSize, startSize, 1, 0, startSteps);

06

draw(0, 0, startSize, startSize, 1, -1, startSteps);

07

draw(0, 0, startSize, startSize, 0, -1, startSteps);

08

draw(0, 0, startSize, startSize, -1, -1, startSteps);

09

接着我们需要做先前 瓷砖线 教程的事了——将其作为一个瓷砖,然后平铺。

首先定义变量,如方块的平铺密度、方块的偏移量等。我们会将最终尺寸变得更小,并根据 canvas 宽度减去 offset 后的大小分割成想要的份数(7)。directions 数组存储着所有可能的方向:-1, 0 & 1

var finalSize = 3;
var startSteps;
var offset = 2;
var tileStep = (size - offset * 2) / 7;
var startSize = tileStep;
var directions = [-1, 0, 1];

10

现在也许看起来有点奇怪,因为我们还没将这些变量应用上。

for( var x = offset; x < size - offset; x += tileStep) {
  for( var y = offset; y < size - offset; y += tileStep) {
    startSteps = 3
    draw(x, y, startSize, startSize, 1, 1, startSteps - 1);
  }
}

11

现在可以开始玩耍啦,可以为 steps 赋予随机数。

startSteps = 2 + Math.ceil(Math.random() * 3)

12

再设置随机方向!

var xDirection = directions[Math.floor(Math.random() * directions.length)]
var yDirection = directions[Math.floor(Math.random() * directions.length)]
draw(x, y, startSize, startSize, xDirection, yDirection, startSteps - 1);

13

这就是最终效果——催眠方块。这是使用递归的好例子,也是一副易于上色的艺术作品,特别是在较大的 canvas 上。

JChehe avatar Sep 05 '18 08:09 JChehe