// 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})`); } } }); })();