使用了多个不同的形状图来组成这朵代码玫瑰。共使用了 31 个形状:24 个花瓣,4 个萼片,2 个叶子和 1 根花茎,其中每一个形状图都用代码进行描绘。
首先,来定义一个采样范围:
<script> function surface(a, b) { // 使用 a 和 b 作为采样范围的参数 return { x: a*50, y: b*50 }; // 该表面是一个 50*50 单元区域 } </script>
编写形状描绘代码:
<script> var canvas = document.body.appendChild(document.createElement("canvas")), context = canvas.getContext("2d"), a, b, position; for (a = 0; a < 1; a += .1) { for (b = 0; b < 1; b += .1) { position = surface(a, b); context.fillRect(position.x, position.y, 1, 1); } } </script>
看到的效果是这样的:
看到的效果是这样的:
因为采样间隔越来越密集,点越来越接近,到最高密度时,相邻点之间的距离小于一个像素,肉眼就看不到间隔(见 0.01)。为了不造成太大的视觉差,再进一步缩小采样间隔,此时,绘制区已经填满(比较结果为 0.01 和 0.001)。
接下来,我用这个公式来绘制一个圆形:(X-X0)^ 2 +(Y-Y0)^ 2 <半径^ 2,其中(X0,Y0)为圆心:
<script> function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { // 圆内 return { x: x, y: y }; } else { // 圆外 return null; } } </script>
为了防止溢出,还要加上一个采样条件
<script> if (position = surface(a, b)) { context.fillRect(position.x, position.y, 1, 1); } </script>
现在让圆变形,以使它看起来更像是一个花瓣:
<script> function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { return { x: x, y: y * (1 + b) / 2 // 变形 }; } else { return null; } }
添加色彩
<script> function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { return { x: x, y: y * (1 + b) / 2, r: 100 + Math.floor((1 - b) * 155), // 添加梯度 g: 50, b: 50 }; } else { return null; } } for (a = 0; a < 1; a += .01) { for (b = 0; b < 1; b += .001) { if (point = surface(a, b)) { context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(point.x, point.y, 1, 1); } } }
3D 曲面和透视投影
定义三维表面
<script> function surface(a, b) { var angle = a * Math.PI * 2, radius = 100, length = 400; return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: b * length - length / 2, // 减去一般的长度,使得焦点在三维坐标中心点(0,0,0) r: 0, g: Math.floor(b * 255), b: 0 }; } </script>
添加投影透视图
<script> var pX, pY, // 画布 X 和 Y 轴的坐标 perspective = 350, halfHeight = canvas.height / 2, halfWidth = canvas.width / 2, cameraZ = -700; for (a = 0; a < 1; a += .001) { for (b = 0; b < 1; b += .01) { if (point = surface(a, b)) { pX = (point.x * perspective) / (point.z - cameraZ) + halfWidth; pY = (point.y * perspective) / (point.z - cameraZ) + halfHeight; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } </script>
z-buffer
在为物件进行着色时,执行“隐藏面消除”工作,使隐藏物件背后的部分就不会被显示出来。
<script> var zBuffer = [], zBufferIndex; for (a = 0; a < 1; a += .001) { for (b = 0; b < 1; b += .01) { if (point = surface(a, b)) { pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth); pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight); zBufferIndex = pY * canvas.width + pX; if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) { zBuffer[zBufferIndex] = point.z; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } } </script>
旋转-欧拉旋转
将之前编写的管状物进行旋转,实现绕 Y 轴旋转:
<script> var angle = a * Math.PI * 2, radius = 100, length = 400, x = Math.cos(angle) * radius, y = Math.sin(angle) * radius, z = b * length - length / 2, yAxisRotationAngle = -.4, // 弧度 rotatedX = x * Math.cos(yAxisRotationAngle) + z * Math.sin(yAxisRotationAngle), rotatedZ = x * -Math.sin(yAxisRotationAngle) + z * Math.cos(yAxisRotationAngle); return { x: rotatedX, y: y, z: rotatedZ, r: 0, g: Math.floor(b * 255), b: 0 }; } </script>
关于采样时间,间隔过大过小都会引起极差的视觉感受,所以,需要设置合理的采样间隔,这里使用蒙特卡罗方法。
<script> var i; window.setInterval(function () { for (i = 0; i < 10000; i++) { if (point = surface(Math.random(), Math.random())) { pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth); pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight); zBufferIndex = pY * canvas.width + pX; if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) { zBuffer[zBufferIndex] = point.z; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } }, 0); </script>
完整版
<!DOCTYPE HTML> <html> <head> <title>Rose</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body style="margin-left:200px"> <div style="text-align: center"> <canvas id="c"></canvas> </div> <script type="text/javascript"> var canvas = document.getElementsByTagName('canvas')[0]; var context = canvas.getContext('2d'); var a = context; var b = document.body; var c = canvas; document.body.clientWidth; var zBuffer = []; var SIZE = 777; canvas.width = canvas.height = SIZE; var h = -350; function surface(a, b, c) { if (c > 60) { return { x : Math.sin(a * 7) * (13 + 5 / (.2 + Math.pow(b * 4, 4))) - Math.sin(b) * 50, y : b * SIZE + 50, z : 625 + Math.cos(a * 7) * (13 + 5 / (.2 + Math.pow(b * 4, 4))) + b * 400, r : a * 1 - b / 2, g : a }; } var A = a * 2 - 1; var B = b * 2 - 1; if (A * A + B * B < 1) { if (c > 37) { var j = c & 1; var n = j ? 6 : 4; var o = .5 / (a + .01) + Math.cos(b * 125) * 3 - a * 300; var w = b * h; return { x : o * Math.cos(n) + w * Math.sin(n) + j * 610 - 390, y : o * Math.sin(n) - w * Math.cos(n) + 550 - j * 350, z : 1180 + Math.cos(B + A) * 99 - j * 300, r : .4 - a * .1 + Math.pow(1 - B * B, -h * 6) * .15 - a * b * .4 + Math.cos(a + b) / 5 + Math.pow(Math.cos((o * (a + 1) + (B > 0 ? w : -w)) / 25), 30) * .1 * (1 - B * B), g : o / 1e3 + .7 - o * w * 3e-6 }; } if (c > 32) { c = c * 1.16 - .15; var o = a * 45 - 20; var w = b * b * h; var z = o * Math.sin(c) + w * Math.cos(c) + 620; return { x : o * Math.cos(c) - w * Math.sin(c), y : 28 + Math.cos(B * .5) * 99 - b * b * b * 60 - z / 2 - h, z : z, r : (b * b * .3 + Math.pow((1 - (A * A)), 7) * .15 + .3) * b, g : b * .7 }; } var o = A * (2 - b) * (80 - c * 2); var w = 99 - Math.cos(A) * 120 - Math.cos(b) * (-h - c * 4.9) + Math.cos(Math.pow(1 - b, 7)) * 50 + c * 2; var z = o * Math.sin(c) + w * Math.cos(c) + 700; return { x : o * Math.cos(c) - w * Math.sin(c), y : B * 99 - Math.cos(Math.pow(b, 7)) * 50 - c / 3 - z / 1.35 + 450, z : z, r : (1 - b / 1.2) * .9 + a * .1, g : Math.pow((1 - b), 20) / 4 + .05 }; } } setInterval(function() { for ( var i = 0; i < 10000; i++) { var part = i % 46; var c = part / .74; var point = surface(Math.random(), Math.random(), c); if (point) { var z = point.z; var x = parseInt(point.x * SIZE / z - h); var y = parseInt(point.y * SIZE / z - h); var zBufferIndex = y * SIZE + x; if ((typeof zBuffer[zBufferIndex] === "undefined") || (zBuffer[zBufferIndex] > z)) { zBuffer[zBufferIndex] = z; var r = -parseInt(point.r * h); var g = -parseInt(point.g * h); var b = -parseInt(point.r * point.r * -80); context.fillStyle = "rgb(" + r + "," + g + "," + b + ")"; context.fillRect(x, y, 1, 1); } } } }, 0); </script> </body> </html>