前言
分形理论是一种非常重要的科学概念,它被广泛应用于物理学、数学、生物学等领域。分形理论描述了一种重复自相似的结构,这种结构在不同的尺度上都具有类似的形态。由于分形理论的应用广泛且深远,了解分形理论可以帮助人们更好地理解自然界和人造世界中的现象。
作为一个科学的文化人,了解分形理论可以让我们更深入地理解我们生活中的事物和现象。分形理论帮助我们认识到自然界中的很多形态都是由简单的重复而构成的,这种认识使我们对世界的理解更加深入和全面。此外,分形理论还可以被用于解释复杂系统的行为,为我们提供了一种新的思考和分析问题的方式。
因此,了解分形理论是成为一个科学的文化人的基本要求之一。它帮助我们更好地理解和解释世界,提供了一种新的思考和分析问题的方式。对于科学的研究和文化的发展都是非常重要的。
从欧几里得到分形
从欧几里得几何的两千多年来
在大自然复杂表面下的内在数学秩序里面,有很多奇奇怪怪的分形结构:Sierpinski triangle三角形、Weierstrass函数、皮亚诺曲线、Koch雪花。分形提供了新的描述自然的方式。其复杂的背后,隐藏着局部和整体之间“自相似”的本质联系。
虽然许多分形是自相似的 ,但一个更好的定义是,分形是具有非整数维的形状。
比如说,瓦茨拉夫·谢尔宾斯基在1915年提出的Sierpinski三角形、Sierpinski地毯。还有PaulLévy在1938年提出的LévyC曲线。这类重复的或者自身相似的数学图形,1975年,Mandelbrot正式提出了“分形”一词,并用醒目的计算机构建的可视化效果说明了他的数学定义。
利用加斯顿·朱利亚创立迭代理论和公式z = z² + c,通过高性能计算机对数字进行了成千上万次的运算和处理,最终成功绘制出一个上帝的指纹。
曼德布罗特集(Mandelbrot Set)
公式采用变量z和参数c,映射了复平面上的数值。其中x轴测量复数的实数部分,而 y 轴测量复数的虚数部分。
迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
在Mandelbrot集中,你会明白分形是一种具有自相似特性的现象、图像或者物理过程。
可以说分形的核心就是自相似性,就是取任一部分进行适当放大,仍可得到与原来整个图形相似的图形,就相当于不断的克隆,一个比一个小,不停的重复下去。
谢尔宾斯基三角形(Sierpinski triangle)
Sierpinski triangle 是一个由三个等边三角形组成的无限分形结构。它的构造方法是通过将一个等边三角形不断地分割成四个互相相似的小三角形,并去掉中间的那个三角形。这个过程可以无限循环下去,形成一个具有自相似性的图案。
皮亚诺曲线(Peano curve)
1890年,意大利数学家皮亚诺(Peano G)发明能填满一个正方形的曲线,叫做皮亚诺曲线。后来,由希尔伯特作出了这条曲线,又名希尔伯特曲线。Hilbert-Peano曲线是一种分形图形,它可以画得无限复杂。它的初始图元是正方形,在迭代生成的过程中,不断细化出小的正方形,图中的线段其实是用于连接各正方形的连线。它的特点是蜿蜒曲折、一气呵成,能经过平面上某一正方形区域内所有的点。希尔伯特曲线是一种奇妙的曲线,只要恰当选择函数,画出一条连续的参数曲线,当参数t在0,1区间取值时,曲线将遍历单位正方形中所有的点,得到一条充满空间的曲线。 希尔伯特曲线是一条连续而又不可导的曲线。
分形维数表明它比普通线更有效地局部填充空间。
科赫雪花(Koch Snowflake)
科赫雪花是一条分形曲线,也被称为科赫岛,由赫尔格·冯·科赫于 1904 年首次描述。它是通过从一个等边三角形开始构建的,移除每条边的内三分之一,在移除该边的位置构建另一个等边三角形,然后无限期地重复该过程。Koch 雪花可以简单地编码为 Lindenmayer 系统,初始字符串为“F--F--F”,字符串重写规则为“F”->“F+F--F+F”,角度为 60 度。
构造的第 0 次到第 3 次迭代。
三角形的每个分形边有时被称为科赫曲线。一般而言,我们在测量非分形曲线时,都是将其放大到足够大,再用直线拟合一小段曲线,在一小段范围内取一阶泰勒展开,近似为直线,最后求总长度。但这样的方法,对分形曲线根本行不通。因为你会发现,分形图案是无限迭代的,无论缩放到多小,细节总会不断地出现。
Koch curve 是经典的迭代分形曲线。它是通过迭代缩放起始段而形成的理论构造。每个新段按 1/3 的比例分为 4 个首尾相连的新片段,其中 2 个中间片段在其他两个片段之间相互倾斜,因此如果它们是三角形,其底边就是中间的片段的长度,这样整个新段就适合传统上测量的前一个段端点之间的长度。虽然动画只显示了几次迭代,但理论曲线以这种方式无限缩放。在这么小的图像上超过大约 6 次迭代,细节就会丢失。
科赫雪花的生长规则是:从一个正三角形出发,把每条边三等分,然后以各边的中间部分1/3的长度为底边,分别向外作正三角形,再把“底边”线段抹掉,得到一个“六角星”。再把每条边三等分,以各中间部分的长度为底边,向外作正三角形后,抹掉底边线段。反复进行这一过程,就会得到一个类似于“雪花”的图形。我们将这种雪花称为“科赫雪花”。
从一个单一的等边三角形开始,但是,我们没有 在每个步骤中去移除 更小的三角形,而是沿边缘 添加 更小的三角形,每个三角形的边长是上一步骤中三角形的1/3。无限生长下去,它将是一个无限大周长,但却有有限的面积的几何图形。我们称产生的形状称为科赫雪花(Koch Snowflake)是一个自相似的分形。
它由一个等边三角形组成,并在每个边上递归添加了较小的等边三角形。以瑞典数学家尼尔斯·法比安·Helge von Koch(1870年-1924年)命名。他首先描述了以他命名的分形科赫雪花,并证明了黎曼假设与素数分布之间的关系。
雪花边缘的小部分看起来与整体大的部分完全一样。当我们将科赫雪花(Koch Snowflake)的一个边缘部分放大3倍时,其长度变为四倍。使用与上面提到的维度与缩放比例之间的关系,可以得到公式3/d=4。 这意味着科赫雪花(Koch Snowflake)的维度为d=log3/4≈1.262。
创建科赫雪花(Koch Snowflake)就像创建一个递归序列,序列的递归公式将序列每项的值xn表示为其前面项的函数。我们知道起始的形状(一个三角形),并且知道如何从一个项到下一个项的方式(通过在每个边上添加更多的三角形):
第一次迭代后,每一步新的三角形数量增加了4倍,同时,每一个新的三角形的面积是上一步大三角形面积的9分之一。
假设第一个三角形的面积为1。然后接下来的三个三角形的总面积为3×1/9=1.3,后面的步骤依此类推,形成等比数列,它们的公比为4/9。
使用无穷等比数列几何级数是几何序列中各项的无穷大,其中连续项的比率为r。例如, 1+12+14+18+… 是具有恒定比率12的几何级数。面积求和公式,我们可以计算出科赫雪花(Koch Snowflake)的总面积为A=1+1/3×11−4/9=8/5=1.6。
我们还可以尝试计算科赫雪花(Koch Snowflake)的周长。正如我们之前已经看到的,周长在每一步都会改变43。这意味着我们有一个等比数列,但是在本例中,它不收敛。 这意味着科赫雪花(Koch Snowflake)的周长实际上是无限长。我们在每一步都将周长乘以4/3,并且我们会无限次这样做。
一个有限面积和一个无限周长的形状
层数 | 边数 | 小三角形数量 | 周长 | 面积 |
1 | 3 | 0 | L1 | S1 |
2 | 3*4 | 3 | L1*4/3 | S1+3*(1/9) |
3 | 3*4^2 | 3*4 | L2*(4/3)^2 | S2+3*4*(1/9)^2 |
... | ... | ... | ... | ... |
n | 3*4^(n-1) | 3*4^(n-2) | L(n-1)*(4/3)^(n-1) | S(n-1)+3*4^(n-2)*(1/9)^(n-1) |
这简直不可思议,但这只是分形的许多意想不到的特性之一。
工程应用举例
在通讯领域,天线设计是手机的重中之重,它将影响手机能支持多少频段以及可以实现的最高上/下行速率。天线的工作原理是通过电场和磁场的相互转换,完成电磁能量的辐射和接收。除了2G、3G、4G乃至5G移动通讯信号以外,Wi-Fi、蓝牙、GPS、NFC和无线充电(线圈)等功能同样需要天线来作为接收和发送信号的载体。
分形天线的自相似结构使它们能够在一定频率范围内进行接收和发送。
在计算机图形领域,分形对地理信息系统的地形进行迭代建模,可以构建出更加自然的结构。
在医学上,借助CT扫描和MRI机器等现代成像设备生成的大量的数据,即使是训练有素的专家,也没有办法又快速又准确弄清所有数据。但有了分形理论就不一样了,因为人体内到处都是分形的身影,我们可以使用分形数学来量化,描述和诊断,以达到治愈疾病的目的。
在工程学上,工程师会采用分形理论构建高强度电缆,从而实现巨型悬索桥的建造。科学家们现在利用分形几何来定位石油,识别地质断层,并可能预测地震。酸雨和腐蚀可以使用分形几何建模。即使是大爆炸理论和对宇宙结构的理解也可以用分形来提高。弹簧行业使用分形几何在3分钟内而不是3天内测试弹簧丝。使用分形几何的统计模型用于测试石油钻井平台上的应力载荷和飞机上的湍流效应。用于存储图像和保持清晰度的图像压缩算法使用分形几何。天气和货币市场本质上都是分形的。军用分形“足迹”可用于识别航空测绘和跟踪潜艇上的人为特征与自然特征。分形在电影中用于风景、恐龙皮肤纹理等。例如,《侏罗纪公园》中恐龙皮肤上的雨滴是使用分形模型绘制的。
使用HTML canvas小试牛刀
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>分形之科赫雪花</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<style>
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.box {
margin: 10px;
padding: 20px;
border: 2px dashed #ccc;
}
.leabel {
margin-top: 5px;
font-size: 14px;
font-weight: bold;
color: gray;
}
label {
width: 40px;
}
input {
width: 100px;
margin: 5px 10px;
overflow:auto;
word-break:break-all;
}
canvas {
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div class="wrapper">
<div id="paper" class="box">
<div>
<label for="txtDepth">维数:</label>
<input id="txtDepth" type="number" value="1">
<input type="checkbox" id="clear" style="width: auto;" name="clear" checked />
<label for="clear">清空</label>
<input type="checkbox" id="grid" style="width: auto;" name="grid" checked />
<label for="grid">网格</label>
<button onclick="draw()">绘制</button>
</div>
<div>
<label for="x1">x1:</label>
<input id="x1" name="x1" type="number" value="400"/>
<label for="y1">y1:</label>
<input id="y1" name="y1" type="number" value="150"/>
<label for="x2">x2:</label>
<input id="x2" name="x2" type="number" value="100"/>
<label for="y2">y2:</label>
<input id="y2" name="y2" type="number" value="150"/>
</div>
<div>
<label for="scaleX">scaleX:</label>
<input id="scaleX" name="scaleX" type="number" value="1"/>
<label for="scaleY">scaleY:</label>
<input id="scaleX" name="scaleY" type="number" value="1"/>
<label for="cx">cx:</label>
<input id="cx" name="cx" type="number" value="0"/>
<label for="cy">cy:</label>
<input id="cy" name="cy" type="number" value="0"/>
<label for="gap">gap:</label>
<input id="gap" name="gap" type="number" value="50"/>
</div>
</div>
<div><span class="leabel">科赫雪花</span></div>
<div style="display: flex;justify-content: center;">
<canvas id="snow"></canvas>
</div>
</div>
</body>
<script>
// 获取浏览器可视区域宽高(兼容性比较好,不包括工具栏和滚动条)
const browserWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const browserHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
const canvas = document.getElementById("snow");//获取canvas对象
canvas.width = browserWidth-40;
canvas.height = browserHeight-240;
let cx = 0, cy = 0, r = 30, num = 1.0; //绘制图形的参数
let ctx = null, scaleX = 1, scaleY = 1;
function createPoint(x,y){
ctx.save();//主要用来保存目前Canvas的状态。通过save()函数它会将目前Canvas的状态推到绘图堆栈中。
//ctx.translate(0,0);
//绘制点
ctx.beginPath();//开始绘制
ctx.lineWidth = 10;
//线的端点是圆,还可以是butt(正方),square(正方,但是追加一段长为线段厚度一半的矩形区域)
ctx.lineCap = 'round';
ctx.strokeStyle = 'red';
ctx.moveTo(x, y);//坐标起点
ctx.lineTo(x, y);//终点,或者理解为下一个点
ctx.stroke();//进行绘制
ctx.restore();//是从绘图堆栈中弹出上一个Canvas的状态。
}
//创建图形
function createArcBlock(a, b, r) {
ctx.beginPath();
ctx.fillStyle = '#654321';
ctx.arc(a, b, r, 0, Math.PI * 2);
ctx.fill();
}
function createLine(p1,p2){
//开始绘制,把刚才所划定的区域“勾勒”轮廓。fill方法用来填充。
ctx.save();//主要用来保存目前Canvas的状态。通过save()函数它会将目前Canvas的状态推到绘图堆栈中。
ctx.beginPath(); //丢弃任何当前定义的路径并且开始一条新的路径。它把当前的点设置为 (0,0)。
ctx.strokeStyle = "red"; //设置画笔的颜色,支持css样式的颜色表现方式,可以用rgb(r, g, b)和rgba(r, g, b, a)这样的方式
ctx.moveTo(p1.x, p1.y); //移动“画笔”到点(5, 5),就像把笔拿起来,然后放到(5, 5)的位置上
ctx.lineTo(p2.x, p2.y); //画线到点(10, 10),从现在的画笔落点,画直线一直到点(10, 10)
ctx.stroke();
ctx.restore();//是从绘图堆栈中弹出上一个Canvas的状态。
}
function originText(width, height, ctx) {
ctx.save();//主要用来保存目前Canvas的状态。通过save()函数它会将目前Canvas的状态推到绘图堆栈中。
ctx.translate(width / 2, height / 2);
ctx.font = "16px Arial";
ctx.fillStyle = "red";
ctx.beginPath();
ctx.fillStyle = 'red';
ctx.arc(0, 0, 5, 0, Math.PI * 2, false);
ctx.fillText("画布中心("+(width / 2).toFixed(0)+","+(height / 2).toFixed(0)+")", -65, 20);
ctx.arc(-width / 2, -height / 2, 5, 0, Math.PI * 2, false);
ctx.fill();
ctx.fillText("画布原点(0,0)", -width / 2 + 20, -height / 2 + 20);
ctx.restore();//是从绘图堆栈中弹出上一个Canvas的状态。
}
function grid(width, height, interval) {
ctx.save();//主要用来保存目前Canvas的状态。通过save()函数它会将目前Canvas的状态推到绘图堆栈中。
for (let y = 50; y < height; y = y + interval) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
for (let x = 50; x < width; x = x + interval) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
ctx.restore();//是从绘图堆栈中弹出上一个Canvas的状态。
}
function setScale(){
scaleX = parseFloat(document.querySelector('[name="scaleX"]').value)
scaleY = parseFloat(document.querySelector('[name="scaleY"]').value)
ctx.scale(scaleX, scaleY); //执行该句后;后续绘制每像素单位为之前的x倍
}
function setCenter(){
cx = parseFloat(document.querySelector('[name="cx"]').value)
cy = parseFloat(document.querySelector('[name="cy"]').value)
ctx.translate(cx,cy);
}
function createGrid(){
// 绘制网格
grid(canvas.width, canvas.height, parseFloat(document.querySelector('[name="gap"]').value));
originText(canvas.width, canvas.height, ctx);
}
// ctx为绘图对象,x1,y1,x2,y2为两头的点,n为当前维度,m为维度
function KochSnowflake(ctx, x1, y1, x2, y2, n, m){
if(m < 0)return;
if(m == 0){
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
return false;
}
let x3 = (x2 - x1) / 3 + x1;
let y3 = (y2 - y1) /3 + y1;
let x4 = (x2 - x1) / 3 * 2 + x1;
let y4 = (y2 - y1) / 3 * 2 + y1;
let x5 = x3 + ((x2 - x1) - (y2 - y1) * Math.sqrt(3)) / 6;
let y5 = y3 + ((x2 - x1) * Math.sqrt(3) + (y2 - y1)) / 6;
n = n + 1;
pList.push(x1)
pList.push(y1)
pList.push(0)
pList.push(x3)
pList.push(y3)
pList.push(0)
pList.push(x5)
pList.push(y5)
pList.push(0)
pList.push(x4)
pList.push(y4)
pList.push(0)
pList.push(x2)
pList.push(y2)
pList.push(0)
if(n == m){
ctx.moveTo(x1, y1);
ctx.lineTo(x3, y3);
ctx.lineTo(x5, y5);
ctx.lineTo(x4, y4);
ctx.lineTo(x2, y2);
ctx.stroke();
return false;
}
KochSnowflake(ctx, x1, y1, x3, y3, n, m)
KochSnowflake(ctx, x3, y3, x5, y5, n, m)
KochSnowflake(ctx, x5, y5, x4, y4, n, m)
KochSnowflake(ctx, x4, y4, x2, y2, n, m)
}
function getRadioBoxValue(radioName){
let obj = document.getElementsByName(radioName);
for(let i=0; i<obj.length;i++){
if(obj[i].checked){
return obj[i].value;
}
}
return "undefined";
}
function getCheckBoxValue(checkName){
let obj = document.getElementsByName(checkName) && document.getElementsByName(checkName)[0].checked
return obj;
}
//十六进制颜色随机
function color16(){
let r = Math.floor(Math.random()*256);
let g = Math.floor(Math.random()*256);
let b = Math.floor(Math.random()*256);
let a = Math.floor(Math.random()*256);
let color = '#'+r.toString(16)+g.toString(16)+b.toString(16)+'66';
return color;
}
function createRotateRectangle(){
ctx.save();
ctx.translate(250,240);
//let colors = ['#D0021B','#F5A623','#8B572A','#417505','#9013FE','#000000']
for ( let i = 0; i < 360; i++ ){
ctx.fillStyle = color16();
ctx.fillRect(0,0,30,15);
ctx.rotate(i * Math.PI / 180);
}
ctx.restore();
}
function createConchThread(){
ctx.save();
ctx.translate(220,55);
ctx.lineWidth=5;
let r=10;
ctx.beginPath();
ctx.arc(4*r,6*r,1*r,Math.PI,Math.PI*2,false);
ctx.arc(3*r,6*r,2*r,0,Math.PI/2,false);
ctx.arc(3*r,5*r,3*r,Math.PI/2,Math.PI,false);
ctx.arc(5*r,5*r,5*r,Math.PI,Math.PI*3/2,false);
ctx.arc(5*r,8*r,8*r,Math.PI*3/2,Math.PI*2,false);
ctx.arc(0,8*r,13*r,0,Math.PI/2,false);
ctx.strokeStyle = 'yellow';
ctx.stroke();
ctx.restore();
}
function draw(){
snowCenter = {
x1:parseFloat(document.querySelector('[name="x1"]').value),
y1:parseFloat(document.querySelector('[name="y1"]').value),
x2:parseFloat(document.querySelector('[name="x2"]').value),
y2:parseFloat(document.querySelector('[name="y2"]').value)
}
//console.log(snowCenter)
if(canvas.getContext){
//画布清空
if(getCheckBoxValue("clear")){
canvas.height = canvas.height;
//ctx.clearRect(0, 0, canvas.width, canvas.height);
}
if(getCheckBoxValue("grid")){
createGrid()
}
setCenter();
setScale();
let x1 = snowCenter.x1;
let y1 = snowCenter.y1;
let x2 = snowCenter.x2;
let y2 = snowCenter.y2;
let x11 = x2 + (x1 - x2) / 2;
let y11 = y1 + Math.sin(Math.PI / 3) * (x1 - x2);
//取得一个文本框的值,可以调整维度,这里没有进行输入判断。
let depth = parseInt(document.getElementById("txtDepth").value);
ctx.beginPath();
//console.log(JSON.stringify(pList));
KochSnowflake(ctx, x1, y1, x2, y2, 0, depth);
KochSnowflake(ctx, x11, y11, x1, y1, 0, depth);
KochSnowflake(ctx, x2, y2, x11, y11, 0, depth);
createRotateRectangle();
createLine({x:x1,y:y1},{x:x2,y:y2});
createPoint(snowCenter.x1,snowCenter.y1);
createPoint(snowCenter.x2,snowCenter.y2);
createConchThread();
}else{
alert("不支持Canvas");
}
}
if(canvas.getContext){ //可以通过这种方式检测浏览器是否支持canvas标签
canvas.height = canvas.height; //重设canvas的宽或高可以清空标签中的图案,相当于clear()
ctx = canvas.getContext("2d"); //获取画布上的绘图环境
ctx.strokeStyle = "#154db9";
ctx.shadowColor = 'rgba(0,193,25,0.2)';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.globalCompositeOperation = "destination-over";
ctx.scale(scaleX, scaleY); //执行该句后;后续绘制每像素单位为之前的x倍
}else{
console.log("不支持Canvas");
}
let pList = [];
let snowCenter = {
x1:400.00,
y1:150.00,
x2:100.00,
y2:150.00,
}
let timeOutEvent = 0; //区分拖拽和点击的参数
//移动
canvas.onmousedown = function(ev) {
let e = ev || event;
let x = e.clientX;
let y = e.clientY;
timeOutEvent = setTimeout('longPress()', 500);
e.preventDefault();
document.querySelector('[name="cx"]').value = x - 20
document.querySelector('[name="cy"]').value = y - 205
drag(x, y, r);
}
//缩放
canvas.onmousewheel = function(ev) {
let e = ev || event;
//wheelDelta的值为正(120,240)则是鼠标向上;为负(-120,-240)则是向下。
if(e.wheelDelta){
//console.log(e.wheelDelta);
if(e.wheelDelta>0&&num>0){
num = (parseFloat(num) + 0.1).toFixed(1);
}else if(e.wheelDelta<0&&num>0){
num = (parseFloat(num) - 0.1).toFixed(1);
}else{
num = 0.01
}
}else if(e.detail){
//console.log(e.detail);
if(e.detail>0&&num>0){
num = (parseFloat(num) + 0.1).toFixed(1);
}else if(e.detail<0&&num>0){
num = (parseFloat(num) - 0.1).toFixed(1);
}else{
num = 0.01
}
}
canvas.height = canvas.height;
document.querySelector('[name="scaleX"]').value = num;
document.querySelector('[name="scaleY"]').value = num;
//ctx.clearRect(0, 0, canvas.width, canvas.height);
draw();
}
//拖拽函数
function drag(x, y, r) {
if (ctx.isPointInPath(x, y)) {
//路径正确,鼠标移动事件
canvas.onmousemove = function(ev) {
let e = ev || event;
let x = e.clientX;
let y = e.clientY;
e.preventDefault();
document.querySelector('[name="cx"]').value = x - 20
document.querySelector('[name="cy"]').value = y - 205
clearTimeout(timeOutEvent);
timeOutEvent = 0;
canvas.height = canvas.height;
//鼠标移动每一帧都清除画布内容,然后重新画圆
//ctx.clearRect(0, 0, canvas.width, canvas.height);
draw();
};
//鼠标移开事件
canvas.onmouseup = function() {
canvas.onmousemove = null;
canvas.onmouseup = null;
clearTimeout(timeOutEvent);
if (timeOutEvent != 0) {
alert('请点击画布,不要拖拽');
}
};
}
}
function longPress() {
timeOutEvent = 0;
}
// (x-a)²+(y-b)²=r²
// 弧度 = 角度 * Math.PI/180
function animation() {
requestAnimationFrame(animation);
draw();
}
animation();
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<meta name="theme-color" content="#4F4F4F">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CANVAS TREE</title>
<style>
body {
--color: white;
}
.container{
display: flex;
}
.overlap {
text-align: center;
text-transform: uppercase;
font-size: 48px;
font-family: emoji;
letter-spacing: -10px;
background: #00bcd4;
color: var(--color);
overflow: hidden;
margin: auto
}
.overlap span {
position: relative;
text-shadow: 10px 0px 10px #00bcd4;
}
ul{
list-style:none;
margin: 0;
padding: 0 0 0 10px;
}
li{
width: 100px;
text-align: center;
height: 30px;
line-height: 30px;
box-shadow: 10px 0 10px #123456;
border: 2px solid green;
border-radius: 5px;
margin: 5px;
background: aqua;
}
li:first-child {
color: #06ff00;
font-weight: bold;
background: #ff0000;
}
section{
width: 100%;
height: 100vh;
display: flex;
background-size: cover;
background-position: center;
}
section:nth-child(1) {
background-image: url("https://s.cn.bing.net/th?id=OHR.MunichBeerfest_ZH-CN0304560562_1920x1080.webp");
}
section:nth-child(2) {
background-image: url("https://s.cn.bing.net/th?id=OHR.OcracokeLight_ZH-CN9810840077_1920x1080.webp");
}
section:nth-child(3) {
background-image: url("https://s.cn.bing.net/th?id=OHR.ElbowRiver_ZH-CN9580175593_1920x1080.webp");
}
section:nth-child(4) {
background-image: url("https://s.cn.bing.net/th?id=OHR.GujoHachiman_ZH-CN9192289658_1920x1080.webp");
}
section:nth-child(5) {
background-image: url("https://s.cn.bing.net/th?id=OHR.MidAutumnFestival2024_ZH-CN9096556094_1920x1080.webp");
}
section:nth-child(6) {
background-image: url("https://s.cn.bing.net/th?id=OHR.SunriseWallabies_ZH-CN8725891401_1920x1080.webp");
}
section:nth-child(7) {
background-image: url("https://s.cn.bing.net/th?id=OHR.CalabriaPeperoncino_ZH-CN8603617212_1920x1080.webp");
}
section:nth-child(8) {
background-image: url("https://s.cn.bing.net/th?id=OHR.MunichBeerfest_ZH-CN0304560562_1920x1080.webp");
}
h1 {
font-size: 120px;
margin: auto;
color: white;
}
</style>
<script src="https://cdn.bootcdn.net/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.3/ScrollTrigger.min.js"></script>-->
</head>
<body>
<div class="container">
<h1 class="overlap">canvas network</h1>
<div>
<button class="btn">First Last Inver Play<br/>思路-元素排序</button>
<ul class="list">
<li class="list-item">HTML+CSS</li>
<li class="list-item">JavaScript</li>
<li class="list-item">网络</li>
<li class="list-item">框架</li>
<li class="list-item">NodeJS</li>
<li class="list-item">工程化</li>
<li class="list-item">移动端</li>
</ul>
</div>
<div>
<canvas id="canvas_network"></canvas>
</div>
</div>
<img id="rh_animcrcl" width="50px" height="50px" src="http://139.224.164.2/src/assets/bigdata-icon.gif">
<main>
<section>
<h1>Good morning!</h1>
</section>
<section>
<h1>Good afternoon!</h1>
</section>
<section>
<h1>Good evening!</h1>
</section>
<section>
<h1>How are you?</h1>
</section>
<section>
<h1>Nice to meet you.</h1>
</section>
<section>
<h1>It's nice to see you.</h1>
</section>
<section>
<h1>Long time no see.</h1>
</section>
<section>
<h1>How's it going?</h1>
</section>
</main>
<script>
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
canvas.width = window.innerWidth * devicePixelRatio / 2
canvas.height = window.innerHeight * devicePixelRatio * 0.8
//坐标系XY轴转换成水平垂直
ctx.translate(canvas.width / 2, canvas.height)
ctx.scale(1, -1)
function drawBranch(v0, len, thick, angle) {
if (thick < 1) {
return
}
if (thick < 2 && Math.random() < 0.3) {
ctx.beginPath()
ctx.arc(...v0, 10, 0, 2 * Math.PI)
ctx.fillStyle = '#00ff00'
ctx.fill()
return
}
ctx.beginPath()
ctx.moveTo(...v0)
const v1 = [
v0[0] + len * Math.cos((angle * Math.PI) / 180),
v0[1] + len * Math.sin((angle * Math.PI) / 180),
]
ctx.lineTo(...v1)
ctx.strokeStyle = '#00bcd4'
ctx.lineWidth = thick
ctx.lineCap = 'round'
ctx.stroke()
//左分支
let leftK = 0.8
drawBranch(v1, len * leftK, thick * leftK / 2, angle + Math.random() * 30)
//右分支
let rightK = 0.8
drawBranch(v1, len * rightK, thick * rightK / 2, angle - Math.random() * 30)
}
drawBranch([0, 0], 200, 100, 90)
const overlap = document.querySelector('.overlap')
const txt = overlap.textContent
overlap.innerHTML = txt.split('').map((t, i) => `<span style="z-index:${txt.length-i}">${t}</span>`).join('')
function draw(id) {
var canvas = document.getElementById(id);
if (canvas == null)
return false;
var ctx = canvas.getContext('2d');
ctx.fillStyle = "#EEEEFF";
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = "green";
//蕨类植物
var a = [0, 0.2, -0.15, 0.85];
var b = [0, -0.26, 0.28, 0.04];
var c = [0, 0.23, 0.26, -0.04];
var d = [0.16, 0.22, 0.24, 0.85];
var e = [0, 0, 0, 0];
var f = [0, 1.6, 0.44, 1.6];
var p = [0.01, 0.07, 0.07, 0.85];
//雪花图案
var a = [0.382, 0.382, 0.382, 0.382, 0.382];
var b = [0, 0, 0, 0, 0];
var c = [0, 0, 0, 0, 0];
var d = [0.382, 0.382, 0.382, 0.382, 0.382];
var e = [0.3072, 0.6033, 0.0139, 0.1253, 0.492];
var f = [0.619, 0.4044, 0.4044, 0.0595, 0.0595];
var p = [0.2, 0.2, 0.2, 0.2, 0.2];
//雪花图案1
var a = [0.255, 0.255, 0.255, 0.37];
var b = [0, 0, 0, -0.642];
var c = [0, 0, 0, 0.642];
var d = [0.255, 0.255, 0.255, 0.37];
var e = [0.3726, 0.1146, 0.6306, 0.6356];
var f = [0.6714, 0.2232, 0.2232, -0.0061];
var p = [0.2, 0.2, 0.2, 0.4];
//树形图案
var a = [0.195, 0.462, -0.058, -0.035, -0.637];
var b = [-0.488, 0.414, -0.07, 0.07, 0];
var c = [0.344, -0.252, 0.453, -0.469, 0];
var d = [0.433, 0.361, -0.111, -0.022, 0.501];
var e = [0.4431, 0.2511, 0.5976, 0.4884, 0.8562];
var f = [0.2452, 0.5692, 0.0969, 0.5069, 0.2513];
var p = [0.25, 0.25, 0.25, 0.2, 0.05];
//树形图案1
var a = [-0.04, -0.65, 0.41, 0.52];
var b = [0, 0, 0.46, -0.35];
var c = [-0.19, 0, -0.39, 0.25];
var d = [-0.47, 0.36, 0.61, 0.74];
var e = [-0.12, 0.06, 0.46, -0.48];
var f = [0.3, 1.56, 0.4, 0.38];
var p = [0.25, 0.25, 0.25, 0.25];
//树上的蝉
var a = [0.03, -0.03, 0.56, 0.56];
var b = [0, 0, -0.56, 0.56];
var c = [0, 0, 0.56, -0.56];
var d = [0.45, -0.45, 0.56, 0.56];
var e = [0, 0, 0, 0];
var f = [0, 0.4, 0.4, 0.4];
var p = [0.05, 0.15, 0.4, 0.4];
//树形图案2
var a = [0, 0.42, 0.42, 0.1];
var b = [0, -0.42, 0.42, 0];
var c = [0, 0.42, -0.42, 0];
var d = [0.5, 0.42, 0.42, 0.4];
var e = [0, 0, 0, 0];
var f = [0, 0.4, 0.4, 0.4];
var p = [0.05, 0.4, 0.4, 0.15];
//嫩枝图案
var a = [0.01, -0.01, 0.42, 0.42];
var b = [0, 0, -0.42, 0.42];
var c = [0, 0, 0.42, -0.42];
var d = [0.45, -0.45, 0.42, 0.42];
var e = [0, 0, 0, 0];
var f = [0, 0.4, 0.4, 0.4];
var p = [0.05, 0.15, 0.4, 0.4];
//蕨类植物1
var a = [0.387, 0.441, -0.468];
var b = [0.430, -0.091, 0.020];
var c = [0.430, -0.009, -0.113];
var d = [-0.387, -0.322, 0.015];
var e = [0.2560, 0.4219, 0.4];
var f = [0.5220, 0.5059, 0.4];
var p = [0.333, 0.333, 0.334];
//蕨类植物2
var a = [0, 0.21, -0.2, 0.85];
var b = [0, -0.25, 0.26, 0.1];
var c = [0, 0.25, 0.23, -0.05];
var d = [0.16, 0.21, 0.22, 0.85];
var e = [0, 0, 0, 0];
var f = [0, 0.44, 0, 0.6];
var p = [0.01, 0.07, 0.07, 0.85];
var x0 = 0;
var y0 = 0;
for (i = 0; i < 100000; i++) {
r = Math.random();
if (r <= p[0])
index = 0;
else if (r <= p[0] + p[1])
index = 1;
else if (r < p[0] + p[1] + p[2])
index = 2;
else
index = 3;
x1 = a[index] * x0 + b[index] * y0 + e[index];
y1 = c[index] * x0 + d[index] * y0 + f[index];
ctx.fillText('.', x1 * 50 + 100, 200 - y1 * 50);
x0 = x1;
y0 = y1;
}
}
draw('canvas_network')
const list = document.querySelector('.list')
let firstItem = document.querySelector('.list-item')
//记录动画元素的位置
function getLocation(){
const rect =firstItem.getBoundingClientRect()
return rect.top
}
//元素起始位置
const start = getLocation()
const btn = document.querySelector('.btn')
btn.onclick = ()=>{
list.insertBefore(firstItem,null)
//元素结束位置
const end = getLocation()
//元素偏移位置
const dis = start - end
firstItem.style.transform = `translateY(${dis}px)`
//也可以使用animation api
raf(()=>{
firstItem.style.transition = 'transform 1s'
firstItem.style.removeProperty('transform')
firstItem = document.querySelector('.list-item')
firstItem.style.removeProperty('transition')
})
}
//渲染主线程延时
function delay(duration = 1000){
const start = Date.now()
while((Date.now()-start)<duration){}
}
function raf(callback){
requestAnimationFrame(()=>{
requestAnimationFrame(callback)
})
}
const sections = document.querySelectorAll('section');
gsap.registerPlugin(ScrollTrigger); // 注册插件
sections.forEach(section => {
gsap.fromTo(section,
{backgroundPositionY: `-${window.innerHeight / 2}px`},
{
backgroundPositionY: `${window.innerHeight / 2}px`,
ease: 'none',
scrollTrigger: {
trigger: section,
scrub: true
}
});
});
const image = document.querySelector('#rh_animcrcl');
gsap.fromTo(image,
{x:0,rotate:0},
{x(_,target){return document.documentElement.clientWidth - 2*target.offsetWidth},
rotate:360,
duration: 10,
ease:'bounce.out',
scrollTrigger: {
trigger: image,
scrub: true,//元素和滚动条关联
//pin: true,
//start: 'center center'
}
})
</script>
</body>
</html>
看官们的重构
可以使用其它js库重构,比如echarts 底层是由 ZRender 引擎渲染,无论是简单的统计图,还是复杂的雷达图、地图、关系图,本质上都是通过 ZRender 引擎渲染绘制的。ZRender 封装了前端 canvas 的绘制逻辑,通过上层的接口去操作底层的绘制功能。从而屏蔽不同环境的差异性,提供统一的访问方式,并提供更高级的图形元素的绘制功能,方便使用者的调度,这都是封装的特点。
参见: