Open qaywsx22 opened 3 years ago
The only implementation of svg textpath that works across firefox and chrome is the hardest one to replicate.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Okay, I have made some progress on this one, while it works, there are some things I am having trouble getting right. pathSide right now only works in FF, so I don't know what to do with it. Also the size of the SVG, I have no idea how to accurately calculate it. Either way, here is my code:
My guess was that for fabric.Text we would not need tspan element in the SVG export. After getting rid of those, it was a simple solution. I am still not sure about the side effects of the following approach.
fabric.Text.prototype.toSVG = function (reviver) {
if (this.type === 'text') {
var noShadow = true,
textDecoration = this.getSvgTextDecoration(this);
var pathString = '';
this.path?.path.forEach(p => {
pathString += p.join(' ');
pathString += ', ';
});
return this._createBaseSVGMarkup(
this.path?.path ?
[
'<defs>',
'<path id="textOnPathId" fill="none" stroke="none" d="',
pathString,
'" />',
'</defs>',
'\t\t<text xml:space="preserve" ',
(this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ' : ''),
(this.fontSize ? 'font-size="' + this.fontSize + '" ' : ''),
(this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''),
(this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''),
(textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''),
'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >',
'<textPath side="',
this.pathSide,
'" href="#textOnPathId">',
this.text,
'</textPath>',
'</text>\n'
]
:
[
'\t\t<text xml:space="preserve" ',
(this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ' : ''),
(this.fontSize ? 'font-size="' + this.fontSize + '" ' : ''),
(this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''),
(this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''),
(textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''),
'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >',
this.text,
'</text>\n'
],
{ reviver: reviver, noStyle: true, withShadow: true }
);
} else {
return this._createBaseSVGMarkup(
this._toSVG(),
{ reviver: reviver, noStyle: true, withShadow: true }
);
}
};
great! textOnPathId
needs to be mixed with this.__uid in order to don't clash with multiple text on path on the same svg.
Also there is a common part of text code that we can reuse, right?
If you manage to get a single color, single path example working, open a PR. I m sure for styles and tSpan will be more complicated
facing same issue here https://web-platform-n8kzvg.stackblitz.io
you can check using developer tool console with below command - Please add text and then run in console
canvas.loadFromJSON(canvas.toJSON(['p1', 'p0', 'p2', 'line','canvas']))
This command should add same components again
Hello! Is there any improvement on this?
https://jsfiddle.net/3btoymjd/
Does this help? Used hoomanaskari's approach and some transformations. Need to think of a bounding rect or something to make it generalise
Added more parameters. side="right" will work on Firefox.
fabric.Text.prototype.toSVG = function (reviver) {
if (this.path) {
var noShadow = true,
textDecoration = this.getSvgTextDecoration(this);
var pathString = this.path?.path
.map(function (segment) {
return segment.join(" ");
})
.join(" ");
const id = Math.random().toString(36).substr(2, 9);
return this._createBaseSVGMarkup(
this.path?.path
? [
"<defs>",
'<path id="textOnPath' + id + '" fill="none" stroke="none" d="',
pathString,
'" />',
"</defs>\n",
'<text xml:space="preserve" ',
this.fontFamily
? 'font-family="' + this.fontFamily.replace(/"/g, "'") + '" '
: "",
this.fontSize ? 'font-size="' + this.fontSize + '" ' : "",
this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : "",
this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : "",
textDecoration ? 'text-decoration="' + textDecoration + '" ' : "",
'style="',
this.getSvgStyles(noShadow),
'"',
this.addPaintOrder(),
" >\n",
'<textPath side="' + this.pathSide + '" ',
'startOffset="' + this.pathStartOffset + '" ',
'href="#textOnPath' + id + '" ',
'xlink:href="#textOnPath' + id + '"> ',
this.text,
"</textPath>\n",
"</text>\n",
]
: [
'\t\t<text xml:space="preserve" ',
this.fontFamily
? 'font-family="' + this.fontFamily.replace(/"/g, "'") + '" '
: "",
this.fontSize ? 'font-size="' + this.fontSize + '" ' : "",
this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : "",
this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : "",
textDecoration ? 'text-decoration="' + textDecoration + '" ' : "",
'style="',
this.getSvgStyles(noShadow),
'"',
this.addPaintOrder(),
" >",
this.text,
"</text>\n",
],
{ reviver: reviver, noStyle: true, withShadow: true }
);
} else {
return this._createBaseSVGMarkup(this._toSVG(), {
reviver: reviver,
noStyle: true,
withShadow: true,
});
}
};
Based on hoomanaskari's and mudassirali007's approaches - You might also add a function to reverse path direction.
This way we get a workaround for the unsupported "side" attribute (at least in chromium).
Besides, I added some conditions to translate fabric.js baseline alignment options to svg's dominant-basline
values.
fabric.Text.prototype.toSVG = function (reviver) {
let fontFamily = this.fontFamily.replace(/"/g, "'");
let fontSize = this.fontSize;
let fontStyle = this.fontStyle;
let fontWeight = this.fontWeight;
let fill = this.fill;
if (this.path) {
let path = this.path;
let fillPath = path.fill ? path.fill : "none";
let strokePath = path.stroke ? path.stroke : "none";
let strokeWidth = path.strokeWidth ? path.strokeWidth : 0;
// get path length
let pathData = this.path.path;
let pathInfo = fabric.util.getPathSegmentsInfo(pathData);
let pathLength = pathInfo[pathInfo.length - 1].length;
// reverse pathdata to emulate side="right"
if (this.pathSide === "right") {
// clone pathdata for reversing
pathData = JSON.parse(JSON.stringify(pathData));
pathData = reversePathData(pathData);
}
// get pathdata d string
let d = pathData.flat().join(" ");
let id = Math.random().toString(36).substr(2, 9);
let dominantbaseline = "auto";
let pathStartOffset = this.pathStartOffset;
let dy = 0;
// translate fabric.js baseline offsets to svg dominant baseline values
if (this.pathAlign === "center") {
dominantbaseline = "middle";
} else if (this.pathAlign === "baseline") {
dominantbaseline = "auto";
} else if (this.pathAlign === "ascender") {
dominantbaseline = "hanging";
} else if (this.pathAlign === "descender") {
dominantbaseline = "auto";
dy = (fontSize / 100) * -22;
}
let textAnchor = "start";
if (this.textAlign == "center") {
textAnchor = "middle";
pathStartOffset += pathLength / 2;
}
if (this.textAlign == "right") {
textAnchor = "end";
pathStartOffset += pathLength;
}
// append texpath to defs or as rendered element
let textPathEl;
if (
(fillPath && fillPath !== "none") ||
(!strokePath && strokePath !== "none")
) {
textPathEl = `<path id="textOnPath${id}" fill="${fillPath}" stroke="${strokePath}" stroke-width="${strokeWidth}" d="${d}" />`;
} else {
textPathEl = `<defs>
<path id="textOnPath${id}" d="${d}" />
</defs>`;
}
return this._createBaseSVGMarkup(
this.path?.path
? [
textPathEl,
`<text
font-family="${fontFamily.replace(/"/g, "'")}"
fill="${fill}"
font-size="${fontSize}"
font-style="${fontStyle}"
font-weight="${fontWeight}"
>
<textPath text-anchor="${textAnchor}"
dominant-baseline="${dominantbaseline}"
startOffset="${pathStartOffset}"
href="#textOnPath${id}"
xlink:href="#textOnPath${id}">
<tspan dy="${dy}">${this.text}</tspan>
</textPath>
</text>`
]
: [
`<text
xml:space="preserve"
font-family="${fontFamily}"
font-size="${fontSize}"
font-style="${fontStyle}"
font-weight="${fontWeight}"
>
${this.addPaintOrder()}
${this.text}
</text>`
],
{ reviver: reviver, noStyle: true, withShadow: true }
);
} else {
return this._createBaseSVGMarkup(this._toSVG(), {
reviver: reviver,
noStyle: true,
withShadow: true
});
}
};
/**
* Reverse pathdata
*/
function reversePathData(pathData) {
// start compiling new path data
let pathDataNew = [];
// helper to rearrange control points for all command types
const reverseControlPoints = (values) => {
let controlPoints = [];
let endPoint = [];
for (let p = 0; p < values.length; p += 2) {
controlPoints.push([values[p], values[p + 1]]);
}
endPoint = controlPoints.pop();
controlPoints.reverse();
return [controlPoints, endPoint];
};
let closed =
pathData[pathData.length - 1][0].toLowerCase() === "z" ? true : false;
if (closed) {
// add lineto closing space between Z and M
pathData = addClosePathLineto(pathData);
// remove Z closepath
pathData.pop();
}
// define last point as new M if path isn't closed
let valuesLast = pathData[pathData.length - 1];
let valuesLastL = valuesLast.length;
let M = closed
? pathData[0]
: ["M", valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]];
// starting M stays the same – unless the path is not closed
pathDataNew.push(M);
// reverse path data command order for processing
pathData.reverse();
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let values = com.slice(1);
let comPrev = pathData[i - 1];
let typePrev = comPrev[0];
let valuesPrev = comPrev.slice(1);
// get reversed control points and new end coordinates
let [controlPointsPrev, endPointsPrev] = reverseControlPoints(valuesPrev);
let [controlPoints, endPoints] = reverseControlPoints(values);
// create new path data
let newValues = [];
newValues = controlPointsPrev.flat().concat(endPoints);
pathDataNew.push([typePrev, ...newValues]);
}
// add previously removed Z close path
if (closed) {
pathDataNew.push(["z"]);
}
return pathDataNew;
}
/**
* Add closing lineto:
* needed for path reversing or adding points
*/
function addClosePathLineto(pathData) {
let pathDataL = pathData.length;
let closed = pathData[pathDataL - 1][0] === "Z";
let M = pathData[0];
let [x0, y0] = [M[1], M[2]];
let lastCom = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
let lastComL = lastCom.length;
let [xE, yE] = [lastCom[lastComL - 2], lastCom[lastComL - 1]];
if (closed && (x0 !== xE || y0 !== yE)) {
pathData.pop();
pathData.push(["L", x0, y0], ["Z"]);
}
return path;
}
See codepen example
@herrstrietzel pretty good! but I don't know why if I change the path the svg is weird (not at the good place). For example with this : M 75 0 A 10 10 0 0 1 75 -150 A 1 1 0 0 1 75 0 Z Have you a idea ?
@Clemweb have you found a solution to this? facing the same problem, any different path get's posicioned wrong on the export
@herrstrietzel, @Clemweb: sorry for the late response.
See new codepen example
Not sure, but I added a cropping/repositioning function so off-viewBox paths get aligned to x/y=0 origin point
Hello, an export a text on path to SVG is not working. Test case: https://jsfiddle.net/meugen22/ojatr2cL/5/