I am working with Bézier curves in HTML.
I have a cubic Bézier curve that I want to draw in multiple colors (color 1 from t = 0 to t = x, color 2 from t = x to t = 1). I could do this either with canvas or with SVG.
Is there a way to specify the stroke color based on the t value?
If not, is there a way to specify the t-interval in which the curve should be drawn? Then I could just draw it multiple times in different intervals with the particular color.
You need to split your <path>
into 2 separate paths at the given t
value.
let t = 0.75;
let path = document.querySelector('#pathCubic');
/**
* parse pathData
* quadratic and arc commands will be converted to cubic
*/
let pathData = path.getPathData({
normalize: true
});
let pathDataSplit = splitNthPathSegmentAtT(pathData, 1, t);
let d0 = pathDataSplit[0].map(com => {
return `${com.type} ${com.values.join(' ')}`
}).join(' ');
let d1 = pathDataSplit[1].map(com => {
return `${com.type} ${com.values.join(' ')}`
}).join(' ');
// apply path data
pathSplit0.setAttribute('d', d0);
pathSplit1.setAttribute('d', d1);
/**
* split nth path segment at "t"
*/
function splitNthPathSegmentAtT(pathData, i = 1, t = 0.5) {
let pathData0 = [pathData[i - 1]];
let pathData1 = [];
let com = pathData[i];
let [type, values] = [com.type, com.values];
let valuesL = values.length;
let comPrev = pathData[i - 1];
let valuesPrev = comPrev ? comPrev.values : [];
let valuesPrevL = valuesPrev.length;
let p0, cp1, cp2, p1, p2, m0, m1, m2, m3, m4;
switch (type) {
case "C":
p0 = {
x: valuesPrev[valuesPrevL - 2],
y: valuesPrev[valuesPrevL - 1]
};
cp1 = {
x: values[valuesL - 6],
y: values[valuesL - 5]
};
cp2 = {
x: values[valuesL - 4],
y: values[valuesL - 3]
};
p1 = {
x: values[valuesL - 2],
y: values[valuesL - 1]
};
m0 = interpolatedPoint(p0, cp1, t);
m1 = interpolatedPoint(cp1, cp2, t);
m2 = interpolatedPoint(cp2, p1, t);
m3 = interpolatedPoint(m0, m1, t);
m4 = interpolatedPoint(m1, m2, t);
// split end point
p2 = interpolatedPoint(m3, m4, t);
// first segment
pathData0.push({
type: "C",
values: [m0.x, m0.y, m3.x, m3.y, p2.x, p2.y]
});
// second segment
pathData1.push({
type: "M",
values: [p2.x, p2.y]
});
pathData1.push({
type: "C",
values: [m4.x, m4.y, m2.x, m2.y, p1.x, p1.y]
});
break;
case "L":
p0 = {
x: valuesPrev[valuesPrevL - 2],
y: valuesPrev[valuesPrevL - 1]
};
p1 = {
x: values[valuesL - 2],
y: values[valuesL - 1]
};
m1 = interpolatedPoint(p0, p1, t);
// first segment
pathData0.push({
type: "L",
values: [m1.x, m1.y]
});
// second segment
pathData1.push({
type: "M",
values: [m1.x, m1.y]
});
pathData1.push({
type: "L",
values: [p1.x, p1.y]
});
break;
}
return [pathData0, pathData1];
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
.layout {
display: flex;
gap: 1em;
}
svg{
width:100%
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>
<div class="layout">
<svg viewBox="0 0 100 100">
<path id="pathCubic" d="M0 100 C33.33 33.33 66.67 33.33 100 100" stroke="#000" fill="none" />
</svg>
<svg viewBox="0 0 100 100">
<path id="pathSplit0" d="" stroke="green" fill="none" />
<path id="pathSplit1" d="" stroke="red" fill="none" />
</svg>
</div>
I'm using getPathData()
polyfilled to parse the d
pathData attribute to an array of commands.
Helper function splitNthPathSegmentAtT()
will calculate the new interpolated C
command control points according to the desired t
value.
Q
and A
commands.If you need to color you path at certain lengths you could use the pathLength
attribute combined with stroke-dasharray
and stroke-dashoffset
.
Not the same, since we're visually "splitting" (by duplicating the path and applying different stroke-dasharray values) according to a path length percentage.
let t = 0.75;
let path = document.querySelector('#pathCubic');
/**
* emulate split path by stroke-dasharray
* split via path length
*/
path.setAttribute('pathLength', 1);
let clonedSeg = path.cloneNode();
let svg = path.closest('svg');
svg.appendChild(clonedSeg);
// 1. segment
path.setAttribute('stroke', 'green');
path.setAttribute('stroke-dashoffset', `0`);
path.setAttribute('stroke-dasharray', `${t} 1`);
// 2. segment
clonedSeg.setAttribute('stroke', 'red');
clonedSeg.setAttribute('stroke-dashoffset', `-${t}`);
clonedSeg.setAttribute('stroke-dasharray', `${1 - t} 1`);
/**
* parse pathData
* quadratic and arc commands will be converted to cubic
*/
let pathData = path.getPathData({
normalize: true
});
let pathDataSplit = splitNthPathSegmentAtT(pathData, 1, t);
let d0 = pathDataSplit[0].map(com => {
return `${com.type} ${com.values.join(' ')}`
}).join(' ');
let d1 = pathDataSplit[1].map(com => {
return `${com.type} ${com.values.join(' ')}`
}).join(' ');
// apply path data
pathSplit0.setAttribute('d', d0);
pathSplit1.setAttribute('d', d1);
/**
* split nth path segment at "t"
*/
function splitNthPathSegmentAtT(pathData, i = 1, t = 0.5) {
let pathData0 = [pathData[i - 1]];
let pathData1 = [];
let com = pathData[i];
let [type, values] = [com.type, com.values];
let valuesL = values.length;
let comPrev = pathData[i - 1];
let valuesPrev = comPrev ? comPrev.values : [];
let valuesPrevL = valuesPrev.length;
let p0, cp1, cp2, p1, p2, m0, m1, m2, m3, m4;
switch (type) {
case "C":
p0 = {
x: valuesPrev[valuesPrevL - 2],
y: valuesPrev[valuesPrevL - 1]
};
cp1 = {
x: values[valuesL - 6],
y: values[valuesL - 5]
};
cp2 = {
x: values[valuesL - 4],
y: values[valuesL - 3]
};
p1 = {
x: values[valuesL - 2],
y: values[valuesL - 1]
};
m0 = interpolatedPoint(p0, cp1, t);
m1 = interpolatedPoint(cp1, cp2, t);
m2 = interpolatedPoint(cp2, p1, t);
m3 = interpolatedPoint(m0, m1, t);
m4 = interpolatedPoint(m1, m2, t);
// split end point
p2 = interpolatedPoint(m3, m4, t);
// first segment
pathData0.push({
type: "C",
values: [m0.x, m0.y, m3.x, m3.y, p2.x, p2.y]
});
// second segment
pathData1.push({
type: "M",
values: [p2.x, p2.y]
});
pathData1.push({
type: "C",
values: [m4.x, m4.y, m2.x, m2.y, p1.x, p1.y]
});
break;
case "L":
p0 = {
x: valuesPrev[valuesPrevL - 2],
y: valuesPrev[valuesPrevL - 1]
};
p1 = {
x: values[valuesL - 2],
y: values[valuesL - 1]
};
m1 = interpolatedPoint(p0, p1, t);
// first segment
pathData0.push({
type: "L",
values: [m1.x, m1.y]
});
// second segment
pathData1.push({
type: "M",
values: [m1.x, m1.y]
});
pathData1.push({
type: "L",
values: [p1.x, p1.y]
});
break;
}
return [pathData0, pathData1];
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
.layout {
display: flex;
gap: 1em;
}
svg{
width:100%
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>
<div class="layout">
<svg viewBox="0 0 100 100">
<path id="pathCubic" d="M0 100 C33.33 33.33 66.67 33.33 100 100" stroke="#000" fill="none" />
</svg>
<svg viewBox="0 0 100 100">
<path id="pathSplit0" d="" stroke="green" fill="none" />
<path id="pathSplit1" d="" stroke="red" fill="none" />
</svg>
</div>
Left: split at pathLength*0.75; Right split at t
0.75.