我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛
实现一个月亮
月亮的实现通过如下步骤:
- 填充画布黑色,作为黑夜背景
- 画一个圆,填充颜色,表示月亮
<canvas id="c1" width="600" height="500"></canvas>
<script>
let canvas=document.getElementById("c1");
let ctx=canvas.getContext("2d");
let width=canvas.width,height=canvas.height;
// 圆点
let origin={
x:width/2,
y:height/2
}
// 填充整个画布
ctx.fillStyle="#000";
ctx.fillRect(0,0,canvas.width,canvas.height);
// 月亮
let moon_radius=80;
let moon_origin={
x:origin.x-60,
y:origin.y
}
ctx.beginPath();
ctx.arc(moon_origin.x,moon_origin.y,moon_radius,0,2*Math.PI);
ctx.fillStyle="yellow";
ctx.fill();
ctx.stroke();
</script>
添加一个球形对月亮的遮挡
由于公转等因素,地球和月亮相对的远近距离,会对月亮造成不同的遮挡效果。
如下,画一个比月亮稍大的阴影球,实现对月亮的遮挡!
let earth_radius=100;
ctx.beginPath();
ctx.arc(origin.x, origin.y, earth_radius, 0, 2 * Math.PI);
ctx.fillStyle="#000";
ctx.fill();
ctx.stroke();
动画化月亮的遮挡效果
一个在canvas上绘制的球类
class Ball {
constructor(props) {
// 小球坐标 默认0,0 半径 20
this.x = 0;
this.y = 0;
this.r = 20;
// 横向 纵向 缩放倍数
this.scaleX = 1;
this.scaleY = 1;
// 绘制开始结束弧度
this.startArc=0;
this.endArc=2 * Math.PI;
//描边和填充颜色
this.strokeStyle = "rgba(0,0,0,0)"; //透明黑色,不描边
this.fillStyle = "red"
this.alpha = 1;
Object.assign(this, props); // 初始化props配置参数
return this;
}
render(ctx) {
let {
x,
y,
r,
scaleX,
scaleY,
fillStyle,
strokeStyle,
alpha,
startArc,
endArc
} = this;
ctx.save();
// 坐标原点移动到绘制坐标
ctx.translate(x, y);
ctx.scale(scaleX, scaleY);
ctx.strokeStyle = strokeStyle,
ctx.fillStyle = fillStyle;
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(0, 0, r, startArc, endArc);
ctx.fill();
ctx.stroke();
ctx.restore();
return this;
}
}
改造月亮遮挡代码为Ball Class实现
let width=canvas.width,height=canvas.height;
ctx.fillStyle="#000";
ctx.fillRect(0,0,canvas.width,canvas.height);
// 月亮
let moon=new Ball({
r:80,
x:width/2-60,
y:height/2,
fillStyle:"yellow"
});
moon.render(ctx);
let earth=new Ball({
r:100,
x:width/2,
y:height/2,
fillStyle:"#000"
});
earth.render(ctx);
遮挡动画
function initNight(ctx){
ctx.save();
ctx.fillStyle="#000";
ctx.fillRect(0,0,width,height);
ctx.restore();
}
initNight(ctx);
// 月亮
let moon=new Ball({
r:80,
x:width/2-50,
y:height/2,
fillStyle:"yellow"
});
let earth=new Ball({
r:90,
x:moon.x+moon.r+90,
y:height/2,
fillStyle:"#000"
});
moon.render(ctx);
earth.render(ctx);
// 开始位置
let startX=earth.x;
let endX=earth.x-earth.r*2-moon.r*2;
let delta=endX-startX;
let speed=0.1,nowStep = 0, step = 100;
function maskMove(){
nowStep+=speed;
if(nowStep<step){
let currX=startX+ delta*(nowStep/step);
earth.x=currX;
ctx.clearRect(0, 0, width, height);
initNight(ctx);
moon.render(ctx);
earth.render(ctx);
}
else{
nowStep=0;
}
}
(function move() {
window.requestAnimationFrame(move);
maskMove();
})();
如下,是月亮从新月、峨眉月,到上玄月、满月、下玄月等的一周运动过程。
月相变化
月相
月相变化的顺序是:新月——娥眉月——上弦月——盈凸(凸月)—— 满月 ——亏凸(凸月)——下弦月——残月(下峨嵋月)——新月,月相变化是周期性的,周期大约是月。
月相的更替周期是29.53天,称为一个朔望月。
新月也叫朔,或朔日。
下面是一个月相的变化周期图:
月相和角度
不同月相的角度:
1、新月(农历初一日,即朔日):0度;
2、上峨嵋月(一般为农历的初二夜左右-------初七日左右):0度----90度;
3、上弦月(农历初八左右):90度;
4、凸月(农历初九左右-----农历十四左右):90度----180度;
5、满月(望月,农历十五日夜或十六日左右):180度;
6、凸月(农历十六左右-----农历二十三左右):180度----270度;
7、下弦月(农历二十三左右):270度;
8、下峨嵋月(农历二十四左右----月末):270度-----360度;另外,农历月最后一天称为晦日月亮,即不见。
修改真实的月相变化效果
从月相的变化来看,整个月亮的变化并不是一个球体移动过去的效果。更像是半圆开合。
使用arcTo实现近似的效果
// 起始点
let phaseMoonStartX=width/2;
let phaseMoonStartY=height/2;
let phaseMoon_r=80;
ctx.save();
// 坐标原点移动到绘制坐标
ctx.translate(phaseMoonStartX, phaseMoonStartY);
ctx.beginPath();
ctx.moveTo(0,0);
ctx.arcTo(200, 80, 0, 160, phaseMoon_r);
ctx.lineTo(0, 160);
ctx.fillStyle="yellow";
ctx.stroke();
ctx.fill();
ctx.restore();
ctx.save();
// 坐标原点移动到绘制坐标
ctx.translate(phaseMoonStartX, phaseMoonStartY);
ctx.beginPath();
ctx.moveTo(0,0);
// 控制点1 x坐标为100
// ctx.arcTo(100, 80, 0, 160, phaseMoon_r);
// 控制点1 x坐标为20
ctx.arcTo(20, 80, 0, 160, phaseMoon_r*6);
ctx.lineTo(0, 160);
ctx.fillStyle="#000";
ctx.stroke();
ctx.fill();
ctx.restore();
但是可以看到,使用arcTo函数,生成的线条非常不流畅,有些生硬,重点是arcTo的半径也不好控制。
两个弧线相交实现的月亮效果
如下,如果一个大圆弧和一小小圆弧,相交生成的月亮效果图,各自的半径关系可以很好的计算出来。
但是随着dist的增加,大圆的半径也是呈指数级别增长,直到无穷大。
关于正负无穷的变化和计算,是一个问题...
而第二个大圆,或圆弧,改为arcTo实现,同样面对着无穷大的问题,或者条线变化生硬的问题。
那么,除此之外就是使用贝赛尔曲线(bezierCurveTo()
)实现小圆竖直中心线相交点,作为贝赛尔曲线的起点和终点,进行绘制,不知是否线条会生硬?
最后,就是借助缩放,在x轴方向缩放半圆,实现类似的效果。
使用缩放模拟真实的月相变换效果
如下,通过一个圆(作为银白色的月亮),两个phase作为半圆并遮挡银色月亮,以缩放为原理,模拟实现月相变化的效果。
- 首先,绘制月亮,左右两个半圆phase,覆盖遮挡月亮。
- 右侧半圆缩放,显示出下方的右半侧月亮。(上玄月)
- 右侧半圆缩放到负值,转向左侧,颜色改为银白色,作为月亮左半侧的显示。盈凸(凸月)
- 右侧半圆完全位于左侧。将左侧半圆直接缩放到右侧,颜色为银白色,和右侧半圆(现在位于左侧),构成月亮,月亮变为黑色。(满月)
- 缩放左侧半圆,银色月亮逐渐变少。亏凸(凸月)
- 左侧半圆从右侧缩放至左侧,颜色变为黑色,逐渐覆盖左侧银白色。(下玄月)
- 左侧半圆完全位于左侧,月亮看不见。(晦日月亮)
- 从1开始重复。
利用了缩放范围phaseXScale从1~-1及从-1~1的循环过程。
// 月亮
let moon1 = new Ball({
r: 80,
x: width / 2,
y: height / 2,
fillStyle: "#c0c0c0",
//strokeStyle:"rgba(192,192,192.6)"
});
let phase1 = new Ball({
r: moon1.r,
x: moon1.x,
y: moon1.y,
fillStyle: "#111",
startArc:-Math.PI/2,
endArc:Math.PI/2
});
let phase2 = new Ball({
r: moon1.r,
x: moon1.x,
y: moon1.y,
fillStyle: "#111",
startArc:Math.PI/2,
endArc:Math.PI*3/2
});
moon1.render(ctx);
phase1.render(ctx);
phase2.render(ctx);
let phaseXScale = 1; // -1 ~ 1
let pSpeed = 0.01;
(function phaseMove() {
window.requestAnimationFrame(phaseMove);
ctx.clearRect(0, 0, width, height);
initNight(ctx);
if(phaseXScale>=1){
phaseSpeed = - pSpeed;
}
if(phaseXScale<=-1){
phaseSpeed = pSpeed;
}
phaseXScale += phaseSpeed;
phase1.fillStyle=phaseXScale<0?"#c0c0c0":"#111";
moon1.render(ctx);
// 从右向左
if(phaseSpeed<0){
moon1.fillStyle= "#c0c0c0";
phase1.scaleX = phaseXScale;
phase2.fillStyle="#111";
//左半边为#c0c0c0
phase1.fillStyle=phaseXScale<0?"#c0c0c0":"#111";
phase2.render(ctx);
phase1.render(ctx);
}
else{
moon1.fillStyle= "#111";
phase2.scaleX = phaseXScale;
phase1.fillStyle="#c0c0c0";
//左半边为111
phase2.fillStyle=phaseXScale<0?"#c0c0c0":"#111";
phase1.render(ctx);
phase2.render(ctx);
}
})();
相对来说,线条还是比较柔顺,没有那么生硬。