Newer
Older

svornost
committed
App.Art.cacheArtData = function() {
/** @param {Element} node */
function removeBadNamespaces(node) {
const attrs = node.attributes;
for (let index = 0; index < attrs.length; ++index) {
const attr = attrs[index];
if (attr.name.startsWith("sodipodi") || attr.name.startsWith("inkscape")) {
node.attributes.removeNamedItem(attr.name);
}
for (const child of node.children) {
removeBadNamespaces(child);
}
}
}

svornost
committed
/**
* @param {NodeListOf<Element>} imagePassages
* @returns {Map<string, Element>}
*/
function makeCache(imagePassages) {
const dict = new Map();
for (const ip of imagePassages) {
const name = ip.attributes.getNamedItem("name").value;
let div = document.createElement("div");
const svgData = atob(ip.innerHTML.replace(/data:image\/svg\+xml;base64,/, ''));
div.innerHTML = svgData.trim();
removeBadNamespaces(div.children.item(0));

svornost
committed
dict.set(name, div.children.item(0));
}
return dict;
}
App.Data.Art = {};
App.Data.Art.Vector = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector"]:not([name^="Art_Vector_Revamp"])'));

svornost
committed
App.Data.Art.VectorRevamp = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector_Revamp"]'));
};
App.Art.SvgQueue = class {
/**
* @param {{trigger:string, action:string, value:string}[]} transformRules - when a 'data-transform' attribute with value "trigger" is seen on an element, perform 'action' with 'value'
* @param {Map<string, Element>} cache
* @param {string} displayClass
*/
constructor(transformRules, cache, displayClass) {
this._transformRules = transformRules;
/** @type {{attrs: NamedNodeMap, nodes: Element[]}[]} */
this._container = [];
this._cache = cache;
this._displayClass = displayClass;
this._defIDs = [];
this._rndID = 0;

svornost
committed
}
/** transform a node via the transform rules
* @param {Element} node
*/
_transform(node) {
const trigger = node.getAttribute("data-transform");
if (trigger) {
const rule = this._transformRules.find((r) => r.trigger === trigger);
if (rule && rule.value) {
if (rule.action === "text-content") {
node.textContent = rule.value;
} else {
// by default, set attribute (usually 'transform')
node.setAttribute(rule.action, rule.value);
}
}
}
}
/** select clip-path via the transform rules
* @param {Element} node
*/
_setclip(node) {
const trigger = node.getAttribute("select_clip");
if (trigger) {
const rule = this._transformRules.find((r) => r.trigger === trigger);
if (rule && rule.value) {
// by default, set attribute (usually 'clip-path')
node.setAttribute(rule.action, rule.value);
}
}
}
/** Append IDs
* @param {Element} node
*/
_replaceIDs(node) {
if (node.hasAttribute('clip-path')) {
let cp = node.getAttribute('clip-path').split(")", 1);
let cpID = cp[0].split("_rndID_", 1);
node.setAttribute('clip-path', `${cpID}_rndID_${this._rndID})`);
}
if (node.hasAttribute('style')) {
let st = node.getAttribute('style');
if (st.search("filter") > -1) {
for (const defID of this._defIDs) {
// Loop through all filter IDs and append the randum number to it
// Remove if another number is already in the ID
let origID = defID.split("_rndID_", 1);
st = st.replaceAll(defID, `${origID}_rndID_${this._rndID}`);
}
node.setAttribute('style', st);
}
}
for (const nodeChild of node.children) {
this._replaceIDs(nodeChild);
}
}

svornost
committed
/** add an SVG from the cache to the render queue
* @param {string} id
*/
add(id) {
const res = this._cache.get(id);
let clones = [];
if (!res) {
console.error(`Missing art resource: ${id}`);
return;
}

svornost
committed
for (const srcNode of res.children) {
const node = /** @type {Element} */ (srcNode.cloneNode(true));

svornost
committed
this._transform(node);

svornost
committed
let transformNodes = node.querySelectorAll('g[data-transform]');
for (const child of transformNodes) {
this._transform(child);
}
let clipNodes = node.querySelectorAll('g[select_clip]');
for (const child of clipNodes) {
this._setclip(child);
}

svornost
committed
clones.push(node);
}
this._container.push({attrs: res.attributes, nodes: clones});
}
/** add an revamped SVG from the cache to the render queue
* @param {string} id
* @param {int} rndID
*/
addRevamped(id,rndID) {
const res = this._cache.get(id);
this._defIDs = [];
this._rndID = rndID;
let clones = [];
if (!res) {
console.error(`Missing art resource: ${id}`);
return;
}
for (const srcNode of res.children) {
const node = /** @type {Element} */ (srcNode.cloneNode(true));
if (node.nodeName == "defs") {
for (const defChild of node.children) {
if (defChild.nodeName == "filter") {
this._defIDs.push(defChild.id);
for (const srcNode of res.children) {
if (srcNode.nodeName == "defs") {
for (const defNode of srcNode.children) {
defNode.setAttribute("id", `${defNode.id.split("_rndID_", 1)}_rndID_${this._rndID}`);
}
let n = srcNode.innerHTML.search("clipPath");
}
for (const srcNode of res.children) {
const node = /** @type {Element} */ (srcNode.cloneNode(true));
this._transform(node);
this._setclip(node);
let transformNodes = node.querySelectorAll('g[data-transform]');
for (const child of transformNodes) {
this._transform(child);
}
let clipNodes = node.querySelectorAll('g[select_clip]');
for (const child of clipNodes) {
this._setclip(child);
}
clones.push(node);
}
this._container.push({attrs: res.attributes, nodes: clones});
}

svornost
committed
/** concatenate the contents of a second queue into this one.
* displayClass must match. cache and transformFunc may differ (they are used only by add).

svornost
committed
* @param {App.Art.SvgQueue} queue
*/
concat(queue) {
if (this._displayClass !== queue._displayClass) {
throw "Incompatible SVG queues. displayClass must match.";
}
this._container.push(...queue._container);
}
/** merge consecutive svg child nodes in the queue with the same svg attributes into bigger svg nodes, and write them out
* this prevents re-evaluating viewboxes and classes unnecessarily and improves layout performance with lots of art
* @returns {DocumentFragment}
*/
output() {
/** evaluate whether an attribute list is equivalent or not
* @param {NamedNodeMap} left
* @param {NamedNodeMap} right
*/
function equalAttributes(left, right) {
/** get all the attribute names from an attribute list

svornost
committed
* @param {NamedNodeMap} attrs
* @returns {string[]}
*/

svornost
committed
let names = [];
for (let index = 0; index < attrs.length; ++index) {
names.push(attrs[index].nodeName);
}
return names;
}
if (!left && !right) {
return true; // both are nullish, treat as equal
} else if (!left || !right) {
return false; // only one is nullish, not equal

svornost
committed
}
const leftNames = attrNames(left), rightNames = attrNames(right);
const intersectionLength = _.intersection(leftNames, rightNames).length;
if (leftNames.length !== intersectionLength || rightNames.length !== intersectionLength) {
return false; // contain different attributes, not equal
}
// are all values equal?
return leftNames.every((attr) => left.getNamedItem(attr).nodeValue === right.getNamedItem(attr).nodeValue);

svornost
committed
}
let frag = document.createDocumentFragment();
let currentAttributes, outSvg;
for (const svg of this._container) {
if (!equalAttributes(currentAttributes, svg.attrs)) {
outSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
outSvg.setAttribute("class", this._displayClass);
frag.appendChild(outSvg);
for (let index = 0; index < svg.attrs.length; ++index) {
outSvg.setAttribute(svg.attrs[index].nodeName, svg.attrs[index].nodeValue);
}
currentAttributes = svg.attrs;
}
outSvg.append(...svg.nodes);
}
return frag;
}
};