diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/extension/internal/polyfill/hatch.js | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/extension/internal/polyfill/hatch.js | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/src/extension/internal/polyfill/hatch.js b/src/extension/internal/polyfill/hatch.js new file mode 100644 index 0000000..c805425 --- /dev/null +++ b/src/extension/internal/polyfill/hatch.js @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: CC0 +/** @file + * Use patterns to render a hatch paint server via this polyfill + *//* + * Authors: + * - Valentin Ionita (2019) + * License: CC0 / Public Domain + */ + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const unitObjectBoundingBox = 'objectBoundingBox'; + const unitUserSpace = 'userSpaceOnUse'; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Copy attributes from the hatch with 'id' to the current element + const setReference = (el, id) => { + const attr = [ + 'x', 'y', 'pitch', 'rotate', + 'hatchUnits', 'hatchContentUnits', 'transform' + ]; + const template = document.getElementById(id.slice(1)); + + if (template && template.nodeName === 'hatch') { + attr.forEach(a => { + let t = template.getAttribute(a); + if (el.getAttribute(a) === null && t !== null) { + el.setAttribute(a, t); + } + }); + + if (el.children.length === 0) { + Array.from(template.children).forEach(c => { + el.appendChild(c.cloneNode(true)); + }); + } + } + }; + + // Order pain-order of hatchpaths relative to their pitch + const orderHatchPaths = (paths) => { + const nodeArray = []; + paths.forEach(p => nodeArray.push(p)); + + return nodeArray.sort((a, b) => + // (pitch - a.offset) - (pitch - b.offset) + Number(b.getAttribute('offset')) - Number(a.getAttribute('offset')) + ); + }; + + // Generate x-axis coordinates for the pattern paths + const generatePositions = (width, diagonal, initial, distance) => { + const offset = (diagonal - width) / 2; + const leftDistance = initial + offset; + const rightDistance = width + offset + distance; + const units = Math.round(leftDistance / distance) + 1; + let array = []; + + for (let i = initial - units * distance; i < rightDistance; i += distance) { + array.push(i); + } + + return array; + }; + + // Turn a path array into a tokenized version of it + const parsePath = (data) => { + let array = []; + let i = 0; + let len = data.length; + let last = 0; + + /* + * Last state (last) index map + * 0 => () + * 1 => (x y) + * 2 => (x) + * 3 => (y) + * 4 => (x1 y1 x2 y2 x y) + * 5 => (x2 y2 x y) + * 6 => (_ _ _ _ _ x y) + * 7 => (_) + */ + + while (i < len) { + switch (data[i].toUpperCase()) { + case 'Z': + array.push(data[i]); + i += 1; + last = 0; + break; + case 'M': + case 'L': + case 'T': + array.push(data[i], new Point(Number(data[i + 1]), Number(data[i + 2]))); + i += 3; + last = 1; + break; + case 'H': + array.push(data[i], new Point(Number(data[i + 1]), null)); + i += 2; + last = 2; + break; + case 'V': + array.push(data[i], new Point(null, Number(data[i + 1]))); + i += 2; + last = 3; + break; + case 'C': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])), + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + last = 4; + break; + case 'S': + case 'Q': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])) + ); + i += 5; + last = 5; + break; + case 'A': + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + data[i + 5], new Point(Number(data[i + 6]), Number(data[i + 7])) + ); + i += 8; + last = 6; + break; + case 'B': + array.push(data[i], data[i + 1]); + i += 2; + last = 7; + break; + default: + switch (last) { + case 1: + array.push(new Point(Number(data[i]), Number(data[i + 1]))); + i += 2; + break; + case 2: + array.push(new Point(Number(data[i]), null)); + i += 1; + break; + case 3: + array.push(new Point(null, Number(data[i]))); + i += 1; + break; + case 4: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])), + new Point(Number(data[i + 4]), Number(data[i + 5])) + ); + i += 6; + break; + case 5: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])) + ); + i += 4; + break; + case 6: + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + break; + default: + array.push(data[i]); + i += 1; + } + } + } + + return array; + }; + + const getYDistance = (hatchpath) => { + const path = document.createElementNS(svgNS, 'path'); + let d = hatchpath.getAttribute('d'); + + if (d[0].toUpperCase() !== 'M') { + d = `M 0,0 ${d}`; + } + + path.setAttribute('d', d); + + return path.getPointAtLength(path.getTotalLength()).y - + path.getPointAtLength(0).y; + }; + + // Point class -------------------------------------- + class Point { + constructor (x, y) { + this.x = x; + this.y = y; + } + + toString () { + return `${this.x} ${this.y}`; + } + + isPoint () { + return true; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'hatch_shape_' + i; + shape.setAttribute('id', shapeId); + } + + const fill = shape.getAttribute('fill') || shape.style.fill; + const fillURL = fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const hatch = document.getElementById(fillURL[1]); + + if (hatch && hatch.nodeName === 'hatch') { + const href = hatch.getAttributeNS(xlinkNS, 'href'); + + if (href !== null && href !== '') { + setReference(hatch, href); + } + + // Degenerate hatch, with no hatchpath children + if (hatch.children.length === 0) { + return; + } + + const bbox = shape.getBBox(); + const hatchDiag = Math.ceil(Math.sqrt( + bbox.width * bbox.width + bbox.height * bbox.height + )); + + // Hatch variables + const units = hatch.getAttribute('hatchUnits') || unitObjectBoundingBox; + const contentUnits = hatch.getAttribute('hatchContentUnits') || unitUserSpace; + const rotate = Number(hatch.getAttribute('rotate')) || 0; + const transform = hatch.getAttribute('transform') || + hatch.getAttribute('hatchTransform') || ''; + const hatchpaths = orderHatchPaths(hatch.querySelectorAll('hatchpath,hatchPath')); + const x = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('x')) * bbox.width) || 0 + : Number(hatch.getAttribute('x')) || 0; + const y = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('y')) * bbox.width) || 0 + : Number(hatch.getAttribute('y')) || 0; + let pitch = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('pitch')) * bbox.width) || 0 + : Number(hatch.getAttribute('pitch')) || 0; + + if (contentUnits === unitObjectBoundingBox && bbox.height) { + pitch /= bbox.height; + } + + // A negative value is an error. + // A value of zero disables rendering of the element + if (pitch <= 0) { + console.error('Non-positive pitch'); + return; + } + + // Pattern variables + const pattern = document.createElementNS(svgNS, 'pattern'); + const patternId = `${fillURL[1]}_pattern`; + let patternWidth = bbox.width - bbox.width % pitch; + let patternHeight = 0; + + const xPositions = generatePositions(patternWidth, hatchDiag, x, pitch); + + hatchpaths.forEach(hatchpath => { + let offset = Number(hatchpath.getAttribute('offset')) || 0; + offset = offset > pitch ? (offset % pitch) : offset; + const currentXPositions = xPositions.map(p => p + offset); + + const path = document.createElementNS(svgNS, 'path'); + let d = ''; + + for (let j = 0; j < hatchpath.attributes.length; ++j) { + const attr = hatchpath.attributes.item(j); + if (attr.name !== 'd') { + path.setAttribute(attr.name, attr.value); + } + } + + if (hatchpath.getAttribute('d') === null) { + d += currentXPositions.reduce( + (acc, xPos) => `${acc}M ${xPos} ${y} V ${hatchDiag} `, '' + ); + patternHeight = hatchDiag; + } else { + const hatchData = hatchpath.getAttribute('d'); + const data = parsePath( + hatchData.match(/([+-]?(\d+(\.\d+)?))|[MmZzLlHhVvCcSsQqTtAaBb]/g) + ); + const len = data.length; + const startsWithM = data[0] === 'M'; + const relative = data[0].toLowerCase() === data[0]; + const point = new Point(0, 0); + let yOffset = getYDistance(hatchpath); + + if (data[len - 1].y !== undefined && yOffset < data[len - 1].y) { + yOffset = data[len - 1].y; + } + + // The offset must be positive + if (yOffset <= 0) { + console.error('y offset is non-positive'); + return; + } + patternHeight = bbox.height - bbox.height % yOffset; + + const currentYPositions = generatePositions( + patternHeight, hatchDiag, y, yOffset + ); + + currentXPositions.forEach(xPos => { + point.x = xPos; + + if (!startsWithM && !relative) { + d += `M ${xPos} 0`; + } + + currentYPositions.forEach(yPos => { + point.y = yPos; + + if (relative) { + // Path is relative, set the first point in each path render + d += `M ${xPos} ${yPos} ${hatchData}`; + } else { + // Path is absolute, translate every point + d += data.map(e => e.isPoint && e.isPoint() ? e.add(point) : e) + .map(e => e.isPoint && e.isPoint() ? e.toString() : e) + .reduce((acc, e) => `${acc} ${e}`, ''); + } + }); + }); + + // The hatchpaths are infinite, so they have no fill + path.style.fill = 'none'; + } + + path.setAttribute('d', d); + pattern.appendChild(path); + }); + + setAttributes(pattern, { + 'id': patternId, + 'patternUnits': unitUserSpace, + 'patternContentUnits': contentUnits, + 'width': patternWidth, + 'height': patternHeight, + 'x': bbox.x, + 'y': bbox.y, + 'patternTransform': `rotate(${rotate} ${0} ${0}) ${transform}` + }); + hatch.parentElement.insertBefore(pattern, hatch); + + shape.style.fill = `url(#${patternId})`; + shape.setAttribute('fill', `url(#${patternId})`); + } + } + }); +})(); |