我希望这篇文章不会重复。
我想绘制一条线,如图所示,可能有不同的线宽和渐变。我尝试过createLinearGradient,但它并不像我预期的那样。我应该使用图像吗?或者我如何渲染上面的线?
我可能会与PixiJS合作。
更新: 我现在可以使用渐变颜色生成线条但是如何创建动态宽度线条?
$(function() {
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
painting = false,
lastX = 0,
lastY = 0;
canvas.onmousedown = function (e) {
if (!painting) {
painting = true;
} else {
painting = false;
}
lastX = e.pageX - this.offsetLeft;
lastY = e.pageY - this.offsetTop;
ctx.lineJoin = ctx.lineCap = 'round';
};
var img = new Image();
img.src = "http://i.imgur.com/K6qXHJm.png";
canvas.onmousemove = function (e) {
if (painting) {
mouseX = e.pageX - this.offsetLeft;
mouseY = e.pageY - this.offsetTop;
// var grad= ctx.createLinearGradient(lastX, lastY, mouseX, mouseY);
// grad.addColorStop(0, "red");
// grad.addColorStop(1, "green");
//ctx.strokeStyle = grad;
ctx.lineWidth = 15;
//ctx.createPattern(img, 'repeat');
ctx.strokeStyle = ctx.createPattern(img, 'repeat');
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(mouseX, mouseY);
ctx.stroke();
$('#output').html('current: '+mouseX+', '+mouseY+'<br/>last: '+lastX+', '+lastY+'<br/>mousedown: '+"mousedown");
lastX = mouseX;
lastY = mouseY;
}
}
function fadeOut() {
ctx.fillStyle = "rgba(255,255,255,0.3)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
setTimeout(fadeOut,100);
}
fadeOut();
});
&#13;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="800" height="500"></canvas>
<div id="output"></div>
&#13;
答案 0 :(得分:6)
没有简单的方法来创建你想要的线型而不会牺牲很多质量。
为了获得最佳质量,您需要将线条渲染为一组垂直于线条的小条带,并沿着线条的长度一直渲染。对于每个零件,您可以计算宽度和颜色,然后渲染该条带。
下图将有助于解释我的意思。
中间的线是定义曲线。外线显示宽度的变化。标记为A的部分是单个条带(放大)
将线分成相同的小部分,对于沿线所需的每个点,您需要找到线上的位置以及垂直于线上该点的矢量。然后,您可以在正确距离处找到点上方和下方的点,以使宽度成为该点的直线。
然后以正确的颜色绘制每个条带。
问题是2D API在连接单独的渲染路径时非常糟糕,因此由于每个条带之间的抗锯齿,此方法将产生垂直线条的模式。
您可以通过勾勒相同颜色笔划的每个条带来解决这个问题,但这会破坏外边缘的质量,在线条外边缘的每个接缝处产生小凸起。
如果将剪辑区域设置为直线,则可以停止此操作。您可以通过描绘线的轮廓并将其设置为剪辑来完成此操作。
然后,您可以以可通过的质量呈现该行
在一个答案中解释的数学太多了。您需要在贝塞尔曲线上找到点和切线,您需要插入一个渐变,并且您需要一种方法来定义平滑宽度函数(另一个贝塞尔曲线)或者如示例中的复杂抛物线(函数{{ 1}})
以下示例将从单个bezier(第2和第3个订单)创建您所在行的类型。您可以使用多条曲线和线段来调整它。
这是关于你可以得到的最好的质量(虽然你可以渲染2或4倍的res和down样本以获得轻微的改进)
对于像素完美抗锯齿结果,您必须使用webGL渲染最终路径(但您仍需要生成路径,如示例所示)
curve
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 400;
// Minimum groover.geom library needed to use vecAt and tangentAsVec for bezier curves.
const geom = (()=>{
const v1 = new Vec();
const v2 = new Vec();
const v3 = new Vec();
const v4 = new Vec();
function Vec(x,y){
this.x = x;
this.y = y;
};
function Bezier(p1,p2,cp1,cp2){
this.p1 = p1;
this.p2 = p2;
this.cp1 = cp1;
this.cp2 = cp2;
}
Bezier.prototype = {
//======================================================================================
// single dimension polynomials for 2nd (a,b,c) and 3rd (a,b,c,d) order bezier
//======================================================================================
// for quadratic f(t) = a(1-t)^2+2b(1-t)t+ct^2
// = a+2(-a+b)t+(a-2b+c)t^2
// The derivative f'(t) = 2(1-t)(b-a)+2(c-b)t
//======================================================================================
// for cubic f(t) = a(1-t)^3 + 3bt(1-t)^2 + 3c(1-t)t^2 + dt^3
// = a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3
// The derivative f'(t) = -3a(1-t)^2+b(3(1-t)^2-6(1-t)t)+c(6(1-t)t-3t^2) +3dt^2
// The 2nd derivative f"(t) = 6(1-t)(c-2b+a)+6t(d-2c+b)
//======================================================================================
p1 : undefined,
p2 : undefined,
cp1 : undefined,
cp2 : undefined,
vecAt(position,vec){
var c;
if (vec === undefined) { vec = new Vec() }
if (position === 0) {
vec.x = this.p1.x;
vec.y = this.p1.y;
return vec;
}else if (position === 1) {
vec.x = this.p2.x;
vec.y = this.p2.y;
return vec;
}
v1.x = this.p1.x;
v1.y = this.p1.y;
c = position;
if (this.cp2 === undefined) {
v2.x = this.cp1.x;
v2.y = this.cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (this.p2.x - v2.x) * c;
v2.y += (this.p2.y - v2.y) * c;
vec.x = v1.x + (v2.x - v1.x) * c;
vec.y = v1.y + (v2.y - v1.y) * c;
return vec;
}
v2.x = this.cp1.x;
v2.y = this.cp1.y;
v3.x = this.cp2.x;
v3.y = this.cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (this.p2.x - v3.x) * c;
v3.y += (this.p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
vec.x = v1.x + (v2.x - v1.x) * c;
vec.y = v1.y + (v2.y - v1.y) * c;
return vec;
},
tangentAsVec (position, vec ) {
var a, b, c, u;
if (vec === undefined) { vec = new Vec(); }
if (this.cp2 === undefined) {
a = (1-position) * 2;
b = position * 2;
vec.x = a * (this.cp1.x - this.p1.x) + b * (this.p2.x - this.cp1.x);
vec.y = a * (this.cp1.y - this.p1.y) + b * (this.p2.y - this.cp1.y);
}else{
a = (1-position)
b = 6 * a * position; // (6*(1-t)*t)
a *= 3 * a; // 3 * ( 1 - t) ^ 2
c = 3 * position * position; // 3 * t ^ 2
vec.x = -this.p1.x * a + this.cp1.x * (a - b) + this.cp2.x * (b - c) + this.p2.x * c;
vec.y = -this.p1.y * a + this.cp1.y * (a - b) + this.cp2.y * (b - c) + this.p2.y * c;
}
u = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
vec.x /= u;
vec.y /= u;
return vec;
},
}
return { Vec, Bezier,}
})()
// this function is used to define the width of the curve
// It creates a smooth transition.
// power changes the rate of change
function curve(x,power){ // simple smooth curve x range 0-2 return value between 0 and 1
x = 1 - Math.abs(x - 1);
return Math.pow(x,power);
}
// this function returns a colour at a point in a gradient
// the pos is from 0 - 1
// the grad is an array of positions and colours with each
// an array [position, red, green, blue] Position is the position in the gradient
// A simple 2 colour gradient from black (start position = 0) to white (end position = 1)
// would be [[0,0,0,0],[1,255,255,255]]
// The bool isHSL if true will interpolate the values as HUE Saturation and luminiance
function getColFromGrad(pos,grad,isHSL){ // pos 0 - 1, grad array of [pos,r,g,b]
var i = 0;
while(i < grad.length -1 && grad[i][0] <= pos && grad[i+1][0] < pos){ i ++ }
var g1 = grad[i];
var g2 = grad[i + 1];
var p = (pos - g1[0]) / (g2[0] - g1[0]);
var r = (g2[1]-g1[1]) * p + g1[1];
var g = (g2[2]-g1[2]) * p + g1[2];
var b = (g2[3]-g1[3]) * p + g1[3];
if(isHSL){ return `hsl(${(r|0)%360},${g|0}%,${b|0}%)` }
return `rgb(${r|0},${g|0},${b|0})`
}
function drawLine(path,width,gradient){
var steps = 300;
var step = 1/steps;
var i = 0;
var pos = V(0,0);
var tangent = V(0,0);
var p = []; // holds the points
// i <= 1 + step/2 // this is to stop floating point error from missing the end value
for(i = 0; i <= 1 + step/2; i += step){
path.vecAt(i,pos); // get position along curve
path.tangentAsVec(i,tangent); // get tangent at that point]
var w = curve(i * 2,1/2) * width; // get the line width for this point
p.push(V(pos.x -tangent.y * w, pos.y + tangent.x * w)); // add the edge point above the line
p.push(V(pos.x +tangent.y * w, pos.y - tangent.x * w)); // add the edge point below
}
// save context and create the clip path
ctx.save();
ctx.beginPath();
// path alone the top edge
for(i = 0; i < p.length; i += 2){
ctx.lineTo(p[i].x,p[i].y);
}
// then back along the bottom
for(i = 1; i < p.length; i += 2){
ctx.lineTo(p[p.length - i].x,p[p.length - i].y);
}
// set this as the clip
ctx.clip();
// then for each strip
ctx.lineWidth = 1;
for(i = 0; i < p.length-4; i += 2){
ctx.beginPath();
// get the colour for this strip
ctx.strokeStyle = ctx.fillStyle = getColFromGrad(i / (p.length-4),gradient);
// define the path
ctx.lineTo(p[i].x,p[i].y);
ctx.lineTo(p[i+1].x,p[i+1].y);
ctx.lineTo(p[i+3].x,p[i+3].y);
ctx.lineTo(p[i+2].x,p[i+2].y);
// cover the seams
ctx.stroke();
// fill the strip
ctx.fill();
}
// remove the clip
ctx.restore();
}
// create quick shortcut to create a Vector object
var V = (x,y)=> new geom.Vec(x,y);
// create a quadratice bezier
var b = new geom.Bezier(V(50,50),V(50,390),V(500,10));
// create a gradient
var grad = [[0,0,0,0],[0.25,0,255,0],[0.5,255,0,255],[1,255,255,0]];
// draw the gradient line
drawLine(b,10,grad);
// and do a cubic bezier to make sure it all works.
var b = new geom.Bezier(V(350,50),V(390,390),V(300,10),V(10,0));
var grad = [[0,255,0,0],[0.25,0,255,0],[0.5,0,255,255],[1,0,0,255]];
drawLine(b,20,grad);
canvas { border : 2px solid black; }
答案 1 :(得分:0)
我也在网上找到了类似的解决方案:)
(function($) {
$.fn.ribbon = function(options) {
var opts = $.extend({}, $.fn.ribbon.defaults, options);
var cache = {},canvas,context,container,brush,painters,unpainters,timers,mouseX,mouseY;
return this.each(function() {
//start functionality
container = $(this).parent();
canvas = this;
context = this.getContext('2d');
canvas.style.cursor = 'crosshair';
$(this).attr("width",opts.screenWidth).attr("height",opts.screenHeight)
painters = [];
//hist = [];
unpainters = [];
timers = [];
brush = init(this.context);
start = false;
clearCanvasTimeout = null;
canvas.addEventListener('mousedown', onWindowMouseDown, false);
canvas.addEventListener('mouseup', onWindowMouseUp, false);
canvas.addEventListener('mousemove', onWindowMouseMove, false);
window.addEventListener('resize', onWindowResize, false);
//document.addEventListener('mouseout', onDocumentMouseOut, false);
//canvas.addEventListener('mouseover', onCanvasMouseOver, false);
onWindowResize(null);
});
function init() {
context = context;
mouseX = opts.screenWidth / 2;
mouseY = opts.screenHeight / 2;
// for(var i = 0; i < opts.strokes; i++) {
// var ease = Math.random() * 0.05 + opts.easing;
// painters.push({
// dx : opts.screenWidth / 2,
// dy : opts.screenHeight / 2,
// ax : 0,
// ay : 0,
// div : 0.1,
// ease : ease
// });
// }
this.interval = setInterval(update, opts.refreshRate);
function update() {
var i;
context.lineWidth = opts.brushSize;
//context.strokeStyle = "rgba(" + opts.color[0] + ", " + opts.color[1] + ", " + opts.color[2] + ", " + opts.brushPressure + ")";
context.lineCap = "round";
context.lineJoin = "round";
var img = new Image;
img.onload = function() {
context.strokeStyle = context.createPattern(img, 'repeat');;
};
img.src = "http://i.imgur.com/K6qXHJm.png";
if(start){
//if(clearCanvasTimeout!=null) clearTimeout(clearCanvasTimeout);
for( i = 0; i < painters.length; i++) {
context.beginPath();
var dx = painters[i].dx;
var dy = painters[i].dy;
context.moveTo(dx, dy);
var dx1 = painters[i].ax = (painters[i].ax + (painters[i].dx - mouseX) * painters[i].div) * painters[i].ease;
painters[i].dx -= dx1;
var dx2 = painters[i].dx;
var dy1 = painters[i].ay = (painters[i].ay + (painters[i].dy - mouseY) * painters[i].div) * painters[i].ease;
painters[i].dy -= dy1;
var dy2 = painters[i].dy;
context.lineTo(dx2, dy2);
context.stroke();
}
}else{
// if(clearCanvasTimeout==null){
// clearCanvasTimeout = setTimeout(function(){
context.clearRect(0, 0, opts.screenWidth, opts.screenWidth);
// clearCanvasTimeout = null;
// }, 3000);
// }else{
// }
//console.log(hist.length);
// for( i = hist.length/2; i < hist.length; i++) {
// context.beginPath();
// var dx = hist[i].dx;
// var dy = hist[i].dy;
// context.moveTo(dx, dy);
// var dx1 = hist[i].ax = (hist[i].ax + (hist[i].dx - mouseX) * hist[i].div) * hist[i].ease;
// hist[i].dx -= dx1;
// var dx2 = hist[i].dx;
// var dy1 = hist[i].ay = (hist[i].ay + (hist[i].dy - mouseY) * hist[i].div) * hist[i].ease;
// hist[i].dy -= dy1;
// var dy2 = hist[i].dy;
// context.lineTo(dx, dy);
// context.stroke();
// }
}
}
};
function destroy() {
clearInterval(this.interval);
};
function strokestart(mouseX, mouseY) {
mouseX = mouseX;
mouseY = mouseY
for(var i = 0; i < painters.length; i++) {
painters[i].dx = mouseX;
painters[i].dy = mouseY;
}
};
function stroke(mouseX, mouseY) {
mouseX = mouseX;
mouseY = mouseY;
};
function strokeEnd() {
//this.destroy()
}
function onWindowMouseMove(event) {
mouseX = event.clientX;
mouseY = event.clientY;
}
function onWindowMouseDown(event){
start = true;
for(var i = 0; i < opts.strokes; i++) {
var ease = Math.random() * 0.05 + opts.easing;
painters.push({
dx : event.clientX,
dy : event.clientY,
ax : 0,
ay : 0,
div : 0.1,
ease : ease
});
}
}
function onWindowMouseUp(){
start = false;
//hist = painters;
painters = [];
}
function onWindowResize() {
opts.screenWidth = window.innerWidth;
opts.screenHeight = window.innerHeight;
}
function onDocumentMouseOut(event) {
onCanvasMouseUp();
}
function onCanvasMouseOver(event) {
strokestart(event.clientX, event.clientY);
window.addEventListener('mousemove', onCanvasMouseMove, false);
window.addEventListener('mouseup', onCanvasMouseUp, false);
}
function onCanvasMouseMove(event) {
stroke(event.clientX, event.clientY);
}
function onCanvasMouseUp() {
strokeEnd();
}
}
$.fn.ribbon.defaults = {
canvas : null,
context : null,
container : null,
userAgent : $.browser,
screenWidth : $(window).width(),
screenHeight : $(window).height(),
duration : 6000, // how long to keep the line there
fadesteps : 10, // how many steps to fade the lines out by, reduce to optimize
strokes : 20, // how many strokes to draw
refreshRate : 30, // set this higher if performace is an issue directly affects easing
easing : .7, // kind of "how loopy" higher= bigger loops
brushSize : 2, // pixel width
brushPressure : 1, // 1 by default but originally variable setting from wacom and touch device sensitivity
color : [0, 0, 0], // color val RGB 0-255, 0-255, 0-255
backgroundColor : [255, 255, 255], // color val RGB 0-255, 0-255, 0-25
brush : null,
mouseX : 0,
mouseY : 0,
i : 0
}
})(jQuery);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas style="border: 1px solid black;" id="canvas" width="800" height="500"></canvas>
<script>
$(document).ready(function(){
var config = {
screenWidth : $("#canvas").width(),
screenHeight : $("#canvas").height(),
strokes: 150,
};
$("#canvas").ribbon(config);
});
</script>