基本原理
线性渐变是一条直线的渐变。看下面的代码及其效果:
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
gradient.addColorStop(0, 'blue');
gradient.addColorStop(1, 'yellow');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
从上面的代码及其运行效果可知,要使用线性渐变,需有4步骤:
- 通过调用createLinearGradient来指定一条直线的起点与终点两个坐标,代表了线性渐变的方向
- 通过调用addColorStop,在渐变路径相应的百分比位置(值域为[0.0, 1.0]),添加相应的颜色
- 将渐变色赋值于ctx的fillStyle属性
- 填充图形
上面的代码,线性渐变的方向为从(0, 0)到(200, 0)。渐变颜色,从位于0%位置的蓝色,渐变到位于100%位置的黄色。然后,将此渐变色填充0, 0, 200, 100的矩形。
标注渐变色的元素
function Point(x, y) {
return {x:x, y:y};
}
const gradentFromPos = Point(0, 0);
const gradentToPos = Point(200, 0);
const gradient = ctx.createLinearGradient(gradentFromPos.x, gradentFromPos.y, gradentToPos.x, gradentToPos.y);
const colorStops = [
{pos: 0, color: 'red'},
{pos: 0.5, color: 'green'},
{pos: 1, color: 'blue'}
];
colorStops.forEach(stop => {
gradient.addColorStop(stop.pos, stop.color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
drawHelpers();
function drawHelpers() {
colorStops.forEach(stop => {
let pt = getPointOnLine(gradentFromPos, gradentToPos, stop.pos);
plotPoint(pt, 5);
});
}
function getPointOnLine(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
function plotPoint(point, r, strokeColor = 'gray') {
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2);
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
colorStops包装了在不同的位置应填充什么颜色:
const colorStops = [
{pos: 0, color: 'red'},
{pos: 0.5, color: 'green'},
{pos: 1, color: 'blue'}
];
这次,我们依序设置了红、绿、蓝三种填充颜色。因为中间色相会自动添加,因此虽然我们只指定了三种颜色,但色彩一下了丰富了许多。
为清晰地看到我们所显式指定的颜色,上面的代码还在各个渐变点以小圆圈进行了标注。
我们可以通过以hsl的方式来指定颜色,以方便均衡地添加渐变色:
const colorStops = [
{pos: 0, color: 'hsl(0, 100%, 50%)'},
{pos: 0.25, color: 'hsl(90, 100%, 50%)'},
{pos: 0.5, color: 'hsl(180, 100%, 50%)'},
{pos: 0.75, color: 'hsl(270, 100%, 50%)'},
{pos: 1, color: 'hsl(360, 100%, 50%)'}
];
效果如下:
function Point(x, y) {
return {x:x, y:y};
}
const gradentFromPos = Point(0, 0);
const gradentToPos = Point(200, 0);
const gradient = ctx.createLinearGradient(gradentFromPos.x, gradentFromPos.y, gradentToPos.x, gradentToPos.y);
const colorStops = [
{pos: 0, color: 'hsl(0, 100%, 50%)'},
{pos: 0.25, color: 'hsl(90, 100%, 50%)'},
{pos: 0.5, color: 'hsl(180, 100%, 50%)'},
{pos: 0.75, color: 'hsl(270, 100%, 50%)'},
{pos: 1, color: 'hsl(360, 100%, 50%)'}
];
colorStops.forEach(stop => {
gradient.addColorStop(stop.pos, stop.color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
drawHelpers();
function drawHelpers() {
colorStops.forEach(stop => {
let pt = getPointOnLine(gradentFromPos, gradentToPos, stop.pos);
plotPoint(pt, 5);
});
}
function getPointOnLine(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
function plotPoint(point, r, strokeColor = 'gray') {
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2);
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
现在,色轮上整个色域中的颜色都在上面了。
addColorStop类似于动画中的添加关键帧,然后再自动生成插值。但关键渐变色应尽可能少取,太多了反倒不好看。
渐变色可脱离填充区域
上面的代码,渐变色的起点、终点,及其渐变方向,均与我们所要填充的区域完全一致。但渐变色可脱离填充区域。
只需重新定义渐变色的起点与终点,便可得到截然不同的效果:
const gradentFromPos = Point(0, 0);
const gradentToPos = Point(200, 150);
function Point(x, y) {
return {x:x, y:y};
}
const gradentFromPos = Point(0, 0);
const gradentToPos = Point(200, 150);
const gradient = ctx.createLinearGradient(gradentFromPos.x, gradentFromPos.y, gradentToPos.x, gradentToPos.y);
const colorStops = [
{pos: 0, color: 'hsl(0, 100%, 50%)'},
{pos: 0.25, color: 'hsl(90, 100%, 50%)'},
{pos: 0.5, color: 'hsl(180, 100%, 50%)'},
{pos: 0.75, color: 'hsl(270, 100%, 50%)'},
{pos: 1, color: 'hsl(360, 100%, 50%)'}
];
colorStops.forEach(stop => {
gradient.addColorStop(stop.pos, stop.color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
drawHelpers();
function drawHelpers() {
drawLine(gradentFromPos, gradentToPos);
colorStops.forEach(stop => {
let pt = getPointOnLine(gradentFromPos, gradentToPos, stop.pos);
plotPoint(pt, 5);
});
}
function getPointOnLine(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
function plotPoint(point, r, strokeColor = 'gray') {
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2);
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
function drawLine(pt1, pt2) {
ctx.beginPath();
ctx.moveTo(pt1.x, pt1.y);
ctx.lineTo(pt2.x, pt2.y);
ctx.strokeStyle = 'gray';
ctx.stroke();
}
金属渐变色
金属的特点是有高光及阴影,下面通过渐变色来模拟出金属的特点。
function Point(x, y) {
return {x:x, y:y};
}
const gradentFromPos = Point(0, 0);
const gradentToPos = Point(200, 0);
const gradient = ctx.createLinearGradient(gradentFromPos.x, gradentFromPos.y, gradentToPos.x, gradentToPos.y);
let hue = 30;
let saturation = '35%';
const colorStops = [
{pos: 0, color: `hsl(${hue}, ${saturation}, 25%)`},
{pos: 0.3, color: `hsl(${hue}, ${saturation}, 65%)`},
{pos: 0.75, color: `hsl(${hue}, ${saturation}, 20%)`},
{pos: 0.9, color: `hsl(${hue}, ${saturation}, 15%)`},
{pos: 1, color: `hsl(${hue}, ${saturation}, 20%)`}
];
colorStops.forEach(stop => {
gradient.addColorStop(stop.pos, stop.color);
});
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 200, 100);
drawHelpers();
function drawHelpers() {
drawLine(gradentFromPos, gradentToPos);
colorStops.forEach(stop => {
let pt = getPointOnLine(gradentFromPos, gradentToPos, stop.pos);
plotPoint(pt, 5);
});
}
function getPointOnLine(srcPt, dstPt, offset) {
return Point(srcPt.x + (dstPt.x - srcPt.x) * offset, srcPt.y + (dstPt.y - srcPt.y) * offset);
}
function plotPoint(point, r, strokeColor = 'gray') {
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2);
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
function drawLine(pt1, pt2) {
ctx.beginPath();
ctx.moveTo(pt1.x, pt1.y);
ctx.lineTo(pt2.x, pt2.y);
ctx.strokeStyle = 'gray';
ctx.stroke();
}
共打了5个关键渐变色。第一个为正常的亮度25%,然后是高光65%,然后慢慢恢复到正常亮度值以下的20%,并准备开始进入阴影区。接着设置最黑的阴影区域15%。最后,恢复较正常的亮度20%,以模拟出受环境光影响的特点。这样,青铜的特点就出来了。
我们还通过hue来存储色相值,saturation来存储颜色饱和值,这样这两个值可以独立地变化了。试着改变这两值,看看可得到哪些渐变效果。