').toLower().upperFirst()
- // if (Array.isArray(element)) {
- // element.forEach(element => {
- // fullText = fullText::prepend(element::curate())
- // })
- // } else if (element) {
- // fullText = fullText::prepend(element::curate())
- // }
-
- this.content.append({fullText});
- }
-
- renderQRCode(cardDesc, qrAnchor, outsideBorder) {
- return;
- const canvas = ;
- const { QrCode } = qrcodegen
- var qr = QrCode.encodeText("" + cardDesc.getId(), QrCode.Ecc.HIGH);
-
- function drawCanvas(qr, scale, border, lightColor, darkColor, canvas) {
- if (scale <= 0 || border < 0)
- throw new RangeError("Value out of range");
- const width = (qr.size + border * 2) * scale;
- canvas.width = width;
- canvas.height = width;
- let ctx = canvas.getContext("2d");
- for (let y = -border; y < qr.size + border; y++) {
- for (let x = -border; x < qr.size + border; x++) {
- ctx.fillStyle = qr.getModule(x, y) ? darkColor : lightColor;
- ctx.fillRect((x + border) * scale, (y + border) * scale, scale, scale);
- }
- }
- }
-
- const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);
-
- drawCanvas(qr, 3, 1, "white", BOX_STROKE_COLOR, canvas);
- this.content.append(canvas)
- }
-
- renderTags(cardDesc, tagsAnchor, outsideBorder) {
- const tags = cardDesc.getTags().sortBy(i => i, true).map(tag => #{tag}
);
- const FONT_SIZE = 7;
-
- this.content.append({...tags})
- }
-
- renderElementSymbol(element, pos, radius) {
- const innerBounds = lively.rect(0, 0, 10, 10)
- const svgInnerPos = innerBounds.center();
- const svgInnerRadius = innerBounds.width / 2;
- const outerBounds = lively.rect(pos.x - radius, pos.y - radius, radius * 2, radius * 2);
- const yourSvgString = SVG.outerSVG(SVG.elementSymbol(element, svgInnerPos, svgInnerRadius), innerBounds, outerBounds);
-
- this.content.insertAdjacentHTML('beforeend', yourSvgString)
- }
-
+ /*MD ## Basic Web Components MD*/
get content() {
return this.get('#content');
}
-
- renderIsBad(cardDesc, outsideBorder) {
- const slash = (color, width=2, offset=lively.pt(0,0)) => {
- const start = outsideBorder.topRight().addPt(offset);
- const end = outsideBorder.bottomLeft().addPt(offset);
- this.line(start, end, color, width)
- }
-
- if (cardDesc.hasTag('duplicate')) {
- slash('#bbbbbb', 2, lively.pt(-3, -3))
- }
- if (cardDesc.hasTag('unfinished')) {
- slash('#888888', 2, lively.pt(-2, -2))
- }
- if (cardDesc.hasTag('bad')) {
- slash('#ff0000', 2)
- }
- if (cardDesc.hasTag('deprecated')) {
- slash('#ff00ff', 2, lively.pt(2, 2))
- }
- if (cardDesc.getRating() === 'remove') {
- slash('#999999', 5, lively.pt(-5, -5))
- }
- }
-
- /*MD ## Basic Web Components MD*/
initialize() {
if (this.hasAttribute('for-preload')) {
return;
@@ -1984,20 +1255,31 @@ font-family: ${CSS_FONT_FAMILY_UNIVERS_55};
this.windowTitle = "UbgCard";
- this._attrObserver = new MutationObserver(mutations => {
+ this._mutationObserver = new MutationObserver(mutations => {
+ let rerenderRequired = false;
mutations.forEach(mutation => {
- let rerenderRequired = false;
- if (mutation.type == "attributes") {
- // console.log("observation", mutation.attributeName,mutation.target.getAttribute(mutation.attributeName));
+ if (mutation.type === 'characterData') {
+ lively.notify('Text content changed to:' + mutation.target.data);
+ rerenderRequired = true;
+ } else if (mutation.type === 'childList') {
+ lively.notify('Child nodes changed:' + mutation);
+ rerenderRequired = true;
+ } else if (mutation.type == "attributes") {
rerenderRequired = true;
this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, mutation.target.getAttribute(mutation.attributeName));
}
- if (rerenderRequired) {
- this.rerender()
- }
});
+
+ if (rerenderRequired) {
+ this.rerender()
+ }
+ });
+ this._mutationObserver.observe(this, {
+ attributes: true,
+ characterData: true,
+ subtree: true,
+ childList: true
});
- this._attrObserver.observe(this, { attributes: true });
}
rerender() {
@@ -2009,7 +1291,7 @@ font-family: ${CSS_FONT_FAMILY_UNIVERS_55};
}
attributeChangedCallback(name, oldValue, newValue) {
- lively.notify(`${oldValue} -> ${newValue}`, name)
+ lively.notify(`${oldValue} -> ${newValue}`, 'ATTR ' + name)
}
/*MD ## External API MD*/
@@ -2025,15 +1307,6 @@ font-family: ${CSS_FONT_FAMILY_UNIVERS_55};
return this.cards = cards;
}
- async render() {
- this._checkOptionsSet()
- const assetsInfo = await this.fetchAssetsInfo();
- const outsideBorder = lively.pt(0,0).extent(POKER_CARD_SIZE_MM);
- const cardToPrint = this.card;
- lively.error('FULL RENDER')
- await this.renderFullBleedStyle(cardToPrint, outsideBorder, assetsInfo)
- }
-
_checkOptionsSet() {
if (!this.src) {
lively.warn('cannot render: "src" not set')
diff --git a/src/components/widgets/ubg-rules-text.js.l4a b/src/components/widgets/ubg-rules-text.js.l4a
deleted file mode 100644
index 98cfef411..000000000
--- a/src/components/widgets/ubg-rules-text.js.l4a
+++ /dev/null
@@ -1,4 +0,0 @@
-{"type":"Reference","version":"daac81d04ebd730ca309649f1808b53986b50d7b","content":"/* global globalThis */\n\nimport Morph from 'src/components/widgets/lively-morph.js';\n\nimport { Point } from 'src/client/graphics.js'\n\nimport paper from 'src/client/paperjs-wrapper.js'\n// import 'https://lively-kernel.org/lively4/ubg-assets/load-assets.js';\n\nimport qrcodegen from 'https://lively-kernel.org/lively4/aexpr/src/external/qrcodegen.js'\n\nconst POKER_CARD_SIZE_INCHES = lively.pt(2.5, 3.5);\nconst POKER_CARD_SIZE_MM = POKER_CARD_SIZE_INCHES.scaleBy(25.4);\n\nconst CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD = \"Beaufort for LOL Bold\"\nconst CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_REGULAR = \"Beaufort for LOL Regular\"\nconst CSS_FONT_FAMILY_UNIVERS_55 = \"Univers 55\"\nconst CSS_FONT_FAMILY_UNIVERS_45_LIGHT_ITALIC = \"Univers 45 Light Italic\"\n\n// Card name, card cost, card stats -- Beaufort for LOL Bold\nconst CSS_FONT_FAMILY_CARD_NAME = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD\nconst CSS_FONT_FAMILY_CARD_COST = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD\nconst CSS_FONT_FAMILY_CARD_VP = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD\nconst CSS_FONT_FAMILY_CARD_TYPE = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_REGULAR\n// #TODO: Card group name (ELITE, SPIDER, YETI, etc.) -- Univers 59 // #BROKEN?? #TODO\n\n// Card description -- Univers 55\nconst CSS_FONT_FAMILY_CARD_TEXT = CSS_FONT_FAMILY_UNIVERS_55\n\nfunction identity(value) {\n return value;\n}\n\n/* `this` is a lively.rect */\nfunction xYWidthHeight() {\n return [this.x, this.y, this.width, this.height];\n}\n\n/* `this` is a Number */\nfunction pointToMM() {\n return this / 2.835;\n}\n\n/* `this` is a Number */\nfunction mmToPoint() {\n return this * 2.835;\n}\n\nconst fire = ;\nconst water = ;\nconst earth = ;\nconst wind = ;\nconst gray = ;\nconst question = ;\n\nclass PathDataScaleCache {\n static getPathData(element, size = lively.pt(10, 10)) {\n if (!this.cache) {\n this.cache = {}\n }\n \n const key = `${element}-${size.x}-${size.y}`;\n if (!this.cache[key]) {\n // lively.notify(`${element}-${size.x}-${size.y}`, 'cache miss')\n this.cache[key] = this._scalePathData(element, size)\n }\n \n return this.cache[key]\n }\n \n static _scalePathData(element, size) {\n const { glyph } = forElement(element);\n const path = new paper.Path(glyph.getAttribute('d'));\n\n path.scale(1, -1);\n\n const margin = size.scaleBy(0.1);\n const boundingRect = new paper.Path.Rectangle({\n point: margin.toPair(),\n size: size.subPt(margin.scaleBy(2)).toPair()\n });\n path.fitBounds(boundingRect.bounds);\n\n return path.pathData;\n }\n}\n\nfunction tenTenPathData(element) {\n return PathDataScaleCache.getPathData(element, lively.pt(10, 10));\n}\n\nconst elementInfo = {\n fire: {\n name: 'fire',\n faIcon: 'book',\n glyph: fire,\n get pathData() { return tenTenPathData('fire') },\n pathWidth: parseInt(fire.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(fire.getAttribute('vert-adv-y')),\n fill: '#ffbbbb',\n stroke: '#ff0000',\n others: ['water', 'earth', 'wind']\n },\n water: {\n name: 'water',\n faIcon: 'droplet',\n glyph: water,\n get pathData() { return tenTenPathData('water') },\n pathWidth: parseInt(water.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(water.getAttribute('vert-adv-y')),\n fill: '#8888ff',\n stroke: '#0000ff',\n others: ['fire', 'earth', 'wind']\n },\n earth: {\n name: 'earth',\n faIcon: 'mountain',\n glyph: earth,\n get pathData() { return tenTenPathData('earth') },\n pathWidth: parseInt(earth.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(earth.getAttribute('vert-adv-y')),\n fill: 'rgb(255, 255, 183)',\n stroke: '#ffd400',\n others: ['fire', 'water', 'wind']\n },\n wind: {\n name: 'wind',\n faIcon: 'cloud',\n glyph: wind,\n get pathData() { return tenTenPathData('wind') },\n pathWidth: parseInt(wind.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(wind.getAttribute('vert-adv-y')),\n fill: '#bbffbb',\n stroke: '#00ff00',\n others: ['fire', 'water', 'earth']\n },\n gray: {\n name: 'gray',\n faIcon: 'circle',\n glyph: gray,\n get pathData() { return tenTenPathData('gray') },\n pathWidth: parseInt(gray.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(gray.getAttribute('vert-adv-y')),\n fill: '#dddddd',\n stroke: '#5A5A5A',\n others: ['gray', 'gray', 'gray']\n },\n unknown: {\n name: 'unknown',\n faIcon: 'question',\n glyph: question,\n get pathData() { return tenTenPathData('question') },\n pathWidth: parseInt(question.getAttribute('horiz-adv-x')),\n pathHeight: parseInt(question.getAttribute('vert-adv-y')),\n fill: 'pink',\n stroke: 'violet',\n others: ['question', 'question', 'question']\n }\n};\n\nfunction forElement(element) {\n const cleanElement = (element || '').toLowerCase();\n return elementInfo[cleanElement] || elementInfo.unknown;\n}\n\nclass SVG {\n\n static outerSVG(children, innerBounds, outerBounds, attrs = '', style = '') {\n return ``;\n }\n\n static inlineSVG(children, bounds = lively.rect(0, 0, 10, 10), attrs = '', style = '') {\n return ``;\n }\n\n /*MD ## Basic Shapes MD*/\n static circleRing(center, innerRadius, outerRadius, attrs) {\n return ``\n }\n\n static circle(center, radius, attrs) {\n return ``\n }\n\n /*MD ## Icons MD*/\n static elementGlyph(element, center, radius, attrs) {\n const pathData = PathDataScaleCache.getPathData(element, lively.pt(2 * radius, 2 * radius));\n return ``\n }\n \n static elementSymbol(element, center, radius) {\n const { name: elementName, fill, stroke } = forElement(element);\n const innerRadius = .9 * radius;\n return `${SVG.circle(center, innerRadius, `fill=\"${fill}\"`)}\n${SVG.elementGlyph(elementName, center, innerRadius, `fill=\"${stroke}\"`)}\n ${SVG.circleRing(center, innerRadius, radius, `fill=\"${stroke}\"`)}`\n }\n}\n\nconst castIcon = do {\n const size = 100;\n const bounds = lively.rect(0, 0, size, size)\n const innerBounds = bounds.insetBy(5);\n \n const innerRadius = innerBounds.width / 2;\n const outerCircle = SVG.circleRing(bounds.center(), innerRadius, bounds.width / 2, `fill=\"#7A7A7A\"`);\n \n const sqrt2 = 2**.5\n const radius = innerRadius * 1 / (sqrt2 + 1);\n const distToMiddle = innerRadius * sqrt2 / (sqrt2 + 1);\n const elements = ['water', 'earth', 'fire', 'wind'];\n const mainElements = elements.map((element, i) => {\n const center = bounds.center().addPt(Point.polar(distToMiddle, Math.PI / 2 * i));\n return SVG.elementSymbol(element, center, radius)\n }).join('\\n');\n\n SVG.inlineSVG(`${outerCircle}\n${mainElements}`, bounds);\n}\n\n\nfunction previewSVG(svg) {\n const hedronTemp = document.getElementById(svg.id)\n if (hedronTemp) {\n hedronTemp.remove()\n }\n document.body.insertAdjacentHTML(\"afterbegin\", svg.outerHTML)\n}\n\nconst hedronSVG = do {\n function point(pt) {\n return `${pt.x} ${pt.y}`;\n }\n\n const topB = lively.pt(11.5, 14.401);\n const topL = topB.addXY(-11.5, -4.758);\n const topT = topL.addXY(11.5, -9.66);\n const topR = topT.addXY(11.5, 9.66);\n const topB2 = topR.addXY(-11.5, 4.758);\n const topLeftData = `M${point(topB)} L ${point(topL)} ${point(topT)} z`;\n const topRightData = `M${point(topB)} L ${point(topT)} ${point(topR)} z`;\n\n const bottomB = lively.pt(11.5, 16.036);\n const bottomL = bottomB.addXY(-11.5, -5.050);\n const bottomT = bottomL.addXY(11.5, 12.030);\n const bottomR = bottomT.addXY(11.5, -12.030);\n const bottomB2 = bottomR.addXY(-11.5, 5.050);\n const bottomLeftData = `M${point(bottomB)} L ${point(bottomL)} ${point(bottomT)} z`;\n const bottomRightData = `M${point(bottomB)} L ${point(bottomT)} ${point(bottomR)} ${point(bottomB2)} z`;\n \n const greenHedron = true;\n ;\n};\n// previewSVG(hedronSVG)\n\nconst upgradeSVG = do {\n const svg = ();\nsvg\n};\n// previewSVG(upgradeSVG)\n\n\nfunction rectToViewBox(rect) {\n return `${rect.x} ${rect.y} ${rect.width} ${rect.height}`\n}\n\nconst TAP_VIEWBOX = lively.rect(2 ,20 ,103 ,103);\nconst tapSVG = do {\n function toPair(pt) {\n return `${pt.x} ${pt.y}`\n }\n\n const size = 18\n const tip = lively.pt(83.5, 65)\n const anchor = tip.subY(size);\n const tail = anchor.subXY(20, 25)\n const tipLeft = tip.subXY(size, size)\n const tipRight = tip.addXY(size, -size)\n const anchorLeft = tipLeft.addX(12.5)\n const anchorRight = tipRight.subX(12.5)\n const controlLeft = anchorLeft.subY(18)\n const controlRight = anchorRight.subY(18)\n const controlTail = tail.addX(5)\n const path = \n;\n const C_BACKCARD_FILL = \"transparent\";\n const C_BACKCARD_STROKE = \"black\";\n const C_FRONTCARD_FILL = \"black\";\n const C_FRONTCARD_STROKE = \"black\";\n\n const svg = ();\nsvg\n}; \n// previewSVG(tapSVG)\n\n\n\n\n\nconst tradeSVG = do {\n const path1 = \"M19.335 11.943c1.463 0.801 2.775 2.074 4.369 4.148 0.005-0.056 0.010-0.113 0.016-0.171 0.309-3.338 0.912-9.84-9.249-13.17 0.113 0.146 0.508 0.575 0.958 1.064 0.75 0.815 1.651 1.795 1.651 1.901-0.903-0.529-5.419-1.906-9.333 0.847s-5.189 6.67-4.616 11.329c0.455 3.7 3.289 6.799 6.95 8.289-2.584-1.464-4.341-4.342-4.341-7.654 0-4.795 3.684-8.682 8.229-8.682 2.050 0 3.925 0.791 5.366 2.099z\";\n const path2 = \"M12.665 20.057c-1.463-0.801-2.775-2.074-4.369-4.148-0.005 0.056-0.010 0.113-0.016 0.171-0.309 3.338-0.912 9.839 9.249 13.17-0.113-0.145-0.508-0.575-0.958-1.064-0.75-0.815-1.651-1.795-1.651-1.901 0.903 0.529 5.419 1.906 9.333-0.847s5.189-6.67 4.616-11.329c-0.454-3.7-3.289-6.799-6.95-8.289 2.584 1.464 4.341 4.342 4.341 7.654 0 4.795-3.684 8.682-8.229 8.682-2.050 0-3.925-0.791-5.366-2.099z\";\n const svg = ();\nsvg\n};\n// previewSVG(tradeSVG)\n\n\nconst CARD_COST_ONE_VIEWBOX = lively.rect(0, 0, 270, 270);\nconst cardCostOneSVG = do {\n '#d3d3d3'\n const C_OUTER = '#252525'\n const C_INNER = '#d1d1d1'\n const C_TOP = '#e1e5e4'\n const C_IMAGE = '#f4f4f4'\n \n const outer = lively.rect(0, 0, 190, 270)\n const inner = outer.insetBy(15)\n const top = inner.insetBy(10)\n const image = top.insetByRect(lively.rect(0, 30, 0, 45))\n const svg = (\n\n);\nsvg\n};\n// previewSVG(cardCostOneSVG)\n\n\nconst CARD_COST_VIEWBOX = lively.rect(0, 0, 376, 326);\nconst cardCostTwoSVG = do {\n const C_OUTER = 'rgb(243, 243, 243)'\n const C_INNER = 'rgb(129, 129, 129)'\n const C_TOP = 'rgb(162, 165, 168)'\n const C_IMAGE = 'rgb(148, 147, 152)'\n const C_BOTTOM = C_TOP;\n \n const svg = (\n);\nsvg\n};\n\n\nclass FileCache {\n\n constructor() {\n this.files = {};\n }\n\n dirtyFolder(path) {}\n\n getFile(path, callback) {\n if (this.files[path]) {\n // lively.notify('cache hit')\n } else {\n // lively.notify('cache miss')\n this.files[path] = callback(path);\n }\n\n return this.files[path];\n }\n\n}\n\nif (globalThis.__ubg_file_cache__) {\n globalThis.__ubg_file_cache__.migrateTo(FileCache);\n} else {\n globalThis.__ubg_file_cache__ = new FileCache();\n}\n\nconst VP_FILL = 'violet';\nconst VP_STROKE = '#9400d3'; // darkviolet\nconst VP_FILL_ZERO = '#ddd';\nconst VP_STROKE_ZERO = 'gray';\nconst AFFECT_ALL_COLOR = 'rgba(255, 0, 0, 0.2)';\n\nclass RuleTextRenderer {\n \n static parseEffectsAndLists(printedRules) {\n function prepRule(rule) {\n return rule\n return `${rule}`\n }\n\n const lines = printedRules.split('\\n');\n if (lines.length === 0) {\n return printedRules;\n }\n\n const result = [`\n${prepRule(lines.shift())}
`];\n \n lines.forEach(line => {\n const bulletMatch = line.match(/^\\s*-\\s*(.+)/);\n if (bulletMatch) {\n const content = bulletMatch[1];\n result.push(`• ${prepRule(content)}
`);\n } else {\n result.push(`${prepRule(line)}
`);\n }\n });\n\n return result.join('\\n');\n }\n \n static chip(text) {\n return `${text}`\n }\n \n static manaCost(element) {\n const { others } = forElement(element);\n\n return SVG.inlineSVG(`${SVG.elementSymbol(element, lively.pt(5, 5), 5)}\n${SVG.elementSymbol(others[0], lively.pt(12.5, 1.5), 1.5)}\n${SVG.elementSymbol(others[1], lively.pt(13, 5), 1.5)}\n${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 15, 10));\n }\n \n /*MD ## --- MD*/\n // #important\n static async renderRuleText(cardEditor, cardDesc) {\n let printedRules = cardDesc.getText() || '';\n\n // old big cast icon with small tap\n // printedRules = printedRules.replace(/(^|\\n)t3x(fire|water|earth|wind|gray)([^\\n]*)/gi, function replacer(match, p1, pElement, pText, offset, string, groups) {\n // return `tap 3x${pElement}${pText}
`;\n // });\n\n // separate rules\n printedRules = printedRules.replace(/affectAll(.*)\\/affectAll/gmi, function replacer(match, innerText, offset, string, groups) {\n return `${innerText}
`;\n });\n \n printedRules = printedRules.replace(/!!(.*?)!!/gmi, function replacer(match, content) {\n return `${content}`;\n });\n printedRules = printedRules.replace(/\\*(.*?)\\*/gmi, (match, content) => {\n return this.italic(content);\n });\n \n printedRules = this.parseEffectsAndLists(printedRules);\n\n printedRules = this.renderReminderText(printedRules, cardEditor, cardDesc)\n \n printedRules = printedRules.replace(/\\b(?:\\d|-|\\+)*x(?:\\d|-|\\+|vp)*\\b/gmi, function replacer(match, innerText, offset, string, groups) {\n // find the bigger pattern, then just replace all x instead of reconstructing its surrounding characters\n return match.replace('x', 'hedron')\n });\n\n printedRules = printedRules.replace(/blitz/gmi, '');\n printedRules = printedRules.replace(/passive/gmi, ``);\n printedRules = printedRules.replace(/start of turn,?/gmi, '');\n printedRules = printedRules.replace(/ignition/gmi, '');\n printedRules = printedRules.replace(/\\btrain\\b/gmi, '');\n printedRules = printedRules.replace(/\\bdaybreak\\b/gmi, '');\n printedRules = printedRules.replace(/\\bnightfall\\b/gmi, '');\n \n // \n printedRules = printedRules.replace(/\\bcardname(?::(\\d+))?/gmi, (match, cardId, offset, string, groups) => {\n // lor blue card name #519ff1\n // #ffe967\n // #f8d66a\n // #de9b75\n function highlightName(name) {\n return `${name}`\n }\n if (!cardId) {\n return highlightName(cardEditor.getNameFromCard(cardDesc))\n }\n const card = cardEditor.cards.find(card => card.getId() + '' === cardId)\n if (card) {\n return highlightName(cardEditor.getNameFromCard(card))\n } else {\n return `unknown id: ${cardId}`\n }\n });\n\n printedRules = printedRules.replace(/actionFree/gmi, () => this.chip('free'));\n printedRules = printedRules.replace(/actionMulti/gmi, () => this.chip('multi'));\n printedRules = this.renderXPerTurnOrGame(printedRules, cardEditor, cardDesc);\n \n printedRules = printedRules.replace(/actionMain:?/gmi, () => {\n return ''\n });\n \n printedRules = this.renderCastIcon(printedRules)\n\n printedRules = printedRules.replace(/manaCost(fire|water|earth|wind|gray)/gmi, (match, pElement, offset, string, groups) => {\n return this.manaCost(pElement);\n });\n\n printedRules = this.renderElementIcon(printedRules)\n printedRules = this.renderVPIcon(printedRules)\n printedRules = this.renderCardIcon(printedRules)\n printedRules = this.renderCoinIcon(printedRules)\n printedRules = this.renderBracketIcon(printedRules)\n \n printedRules = this.renderKeywords(printedRules)\n printedRules = this.renderHedronIcon(printedRules)\n printedRules = this.renderTapIcon(printedRules)\n printedRules = printedRules.replace(/\\bgear\\b/gmi, '');\n printedRules = printedRules.replace(/combat/gmi, () => {\n return \"\";\n });\n \n this.renderToDoc(cardEditor, printedRules, cardDesc)\n }\n \n static renderXPerTurnOrGame(printedRules, cardEditor, cardDesc) {\n return printedRules.replace(/\\b((?:\\d+)?(?:hedron)?)\\/(game|turn)\\b/gmi, (match, times, type, string, groups) => {\n let color = 'black';\n if (type === 'turn') {\n const perTurnColors = [\n // https://materialui.co/colors\n 'black',\n '#283593',\n '#303F9F',\n '#3949AB',\n '#3F51B5',\n ]\n \n color = perTurnColors[times] || perTurnColors.last\n }\n if (type === 'game') {\n const perGameColors = [\n 'black',\n '#4A148C',\n '#6A1B9A',\n '#7B1FA2',\n '#8E24AA',\n ]\n \n color = perGameColors[1]\n }\n return `${times}/${type}`\n })\n }\n\n static italic(text) {\n return `${text}`\n }\n\n static renderReminderText(printedRules, cardEditor, cardDesc) {\n return printedRules.replace(/\\bremind(?:er)?(\\w+(?:\\-(\\w|\\(|\\))*)*)\\b/gmi, (match, myMatch, offset, string, groups) => {\n const keywords = {\n actionquest: () => {\n return 'You may play this when you perform the action.'\n },\n \n accelerate: (cost) => {\n return `You may play or buy this as a card costing (${cost}). If you do, exec its accelerate effect, !!then trash it!!.)`\n },\n \n affinity: (...args) => {\n let subject = 'This costs'\n if (args.includes('all')) {\n args = args.filter(arg => arg !== 'all')\n // keyword granted\n subject = 'They cost'\n }\n\n if (args.includes('power')) {\n return subject + ' (x) less.'\n }\n\n if (args.includes('vpchips')) {\n return subject + ' (1) less per collected vp.'\n }\n\n if (args.includes('coins')) {\n return subject + ' (1) less per () you have.'\n }\n\n if (args.includes('cards')) {\n return subject + ' (1) less for each of those cards.'\n }\n\n if (args.includes('mana')) {\n const elements = args.filter(arg => arg !== 'mana')\n let elementString\n if (elements.length === 1) {\n elementString = elements.first; \n } else {\n elementString = `${elements.slice(0, -1).join(', ')} or ${elements.last}`; \n }\n return subject + ` (1) less for each mana on ${elementString}.`\n }\n\n throw new Error('unspecified type of Affinity')\n },\n \n blueprint: (cost) => {\n return `Effects below are blocked unless this has stored cards costing (${cost}) or more. As a free action, you may store a card from hand, play or trash.`\n },\n\n bound: (...args) => {\n return 'Only exec bound abilities if the element is called.'\n },\n \n brittle: (...args) => {\n if (args.includes('all')) {\n // keyword granted\n return 'Trash brittle cards after casting them.'\n }\n \n return 'Trash this after casting it.'\n },\n\n clash: (where) => {\n if (!where) {\n throw new Error('no clash area given')\n }\n \n return `To clash, each involved player selects a card from ${where} and draws a card. Compare the total cost of these cards. Higher costs win.`\n },\n\n convokecast: (...args) => {\n if (args.includes('all')) {\n // keyword granted\n return 'Increase their x by 1 for each other card sharing an element with them.'\n }\n \n return 'Increase this card\\'s x by 1 for each other card sharing an element with it.'\n },\n \n countingquest: () => {\n return 'If you fulfill its condition (track with []), as a free action you may trash this to create an Achievement Token.'\n },\n \n cycle: (cost) => {\n if (cost) {\n return `To cycle (${cost}), pay (${cost}) and sacrifice the card to play a card of equal or lower cost.`\n }\n return `To cycle, sacrifice the card to play a card of equal or lower cost.`\n },\n \n cycling: (cost, who) => {\n if (['acard', 'one', 'all'].includes(cost)) {\n who = cost\n cost = undefined\n }\n\n let whoToPrint = 'this'\n if (who === 'acard') {\n whoToPrint = 'a card'\n } else if (who === 'one') {\n whoToPrint = 'the card'\n } else if (who === 'all') {\n whoToPrint = 'a card'\n }\n\n if (cost) {\n return `gear Pay (${cost}) and sacrifice ${whoToPrint} to play a card of equal or lower cost.`\n }\n return `gear Sacrifice ${whoToPrint} to play a card of equal or lower cost.`\n },\n \n dash: (cost, who) => {\n let thatCard = 'this'\n let it = 'this'\n \n if (who === 'one') {\n thatCard = 'that card'\n it = 'it'\n }\n\n if (cost === 'action') {\n return `To dash, play ${thatCard}, but sacrifice ${it} at end of turn.`\n } else {\n return `Pay (${cost}) to play ${thatCard}, but sacrifice ${it} at end of turn.`\n }\n },\n \n \n delirium: () => {\n // alternative: if you have cards of four different elements in trash.\n return `Only activate delirium abilities if you have fire, water, earth and wind cards in trash.`\n },\n \n \n discover: (howMany) => {\n return `To discover ${howMany}, reveal top ${howMany} cards of any piles. Add 1 to your hand, trash the rest.`\n },\n \n emerge: (...args) => {\n if (args.includes('all')) {\n // keyword granted\n return 'When you buy a card: You may sacrifice a card for a discount equal to its cost.'\n }\n \n if (args.includes('one')) {\n // keyword granted\n return 'When you buy the card: You may sacrifice a card for a discount equal to its cost.'\n }\n \n return 'When you buy this: You may sacrifice a card for a discount equal to its cost.'\n },\n\n evoke: (cost, who) => {\n if (who === 'all') {\n return `gear Pay the cost and discard a card to exec its blitz effects.`\n }\n if (who === 'one') {\n return `gear Pay the cost and discard that card to exec its blitz effects.`\n }\n return `gear Pay (${cost}) and discard this to exec its blitz effects.`\n },\n\n flashback: (who) => {\n let subject = 'this';\n if (who === 'all') {\n subject = 'a card';\n }\n if (who === 'one') {\n subject = 'the card';\n }\n return `gear Sacrifice ${subject} to exec its blitz effects.`\n },\n\n impulse: () => {\n return `To impulse a card, set it aside. You may buy it this turn as gear. If you don't: Trash it at end of turn.`\n },\n \n instant: () => {\n return 'gear Buy this.'\n },\n \n invoke: () => {\n return 'gear Discard or sacrifice this to exec the effect.'\n },\n \n manaburst: () => {\n return 'Only activate manaburst abilities if x is 4+.'\n },\n \n magnetic: () => {\n return 'gear Pay this card\\'s cost to meld it from hand to a card on field. The melded card has all abilities and combined stats ((), vp, element, type) of its parts.'\n },\n \n meld: () => {\n return 'The melded card has all abilities and combined stats ((), vp, element, type) of its parts.'\n },\n \n postpone: (cost, delay) => {\n return `You may buy this for ${cost} instead of its normal cost. If you do, put this with [${delay}] in your suspend zone. Start of turn Remove [1] from here. Passive If last [] is removed, play this.`\n },\n \n potion: (...args) => {\n return `Discard or sacrifice this to exec the effect.`\n },\n \n quest: () => {\n return 'As a free action, you may play this if you fulfill its condition.'\n },\n \n quickcast: (...args) => {\n if (args.includes('all')) {\n // keyword granted\n return 'Blitz You may cast it.'\n }\n if (args.includes('one')) {\n // keyword granted\n return 'Blitz You may cast it.'\n }\n \n return 'Blitz You may cast this.'\n },\n\n reap: (...args) => {\n return `To reap a card, gain () equal to its cost OR vp equal to its base vp.`\n },\n \n resonance: (...args) => {\n if (args.includes('all')) {\n // keyword granted\n return 'While a card\\'s element is called, you may cast it along your main spell.'\n }\n if (args.includes('one')) {\n // keyword granted\n return 'While that card\\'s element is called, you may cast it along your main spell.'\n }\n if (args.includes('this')) {\n // variable element known\n return 'While this card\\'s element is called, you may cast this along your main spell.'\n }\n const elements = cardEditor.getElementsFromCard(cardDesc, false)\n let elementString;\n if (elements.length === 0 || (elements.length === 1 && elements.first === 'gray')) {\n elementString = 'this card\\'s element';\n } else if (elements.length === 1) {\n elementString = elements.first;\n } else {\n elementString = `${elements.slice(0, -1).join(', ')} or ${elements.last}`; \n }\n \n // Goal: While wind is called, you may cast this as a free action.\n // While wind is called, you may cast this along another spell.\n return `While ${elementString} is called, you may cast this along your main spell.`\n },\n \n saga: (...args) => {\n return 'Blitz and Start of Turn Put [1] here. Then, exec the corresponding chapter\\'s effect.'\n },\n \n seek: (...args) => {\n return 'Reveal cards from deck until you reveal the appropriate card(s), return the others to the game box.'\n },\n \n stuncounter: (...args) => {\n return 'Casting a card with a stun counter removes the counter instead of the effect.'\n },\n\n synchro: (...args) => {\n return 'Play this as a free action by trashing 2+ cards from field with total cost equal to this card\\'s.'\n },\n\n tiny: () => {\n return 'Tiny cards do not count for triggering the game end.'\n },\n\n trade: (...args) => {\n return 'To trade, discard the card to draw a card.'\n },\n\n trading: (who) => {\n let whoText = 'this'\n if (who === 'one') {\n whoText = 'the card'\n } else if (who === 'all') {\n whoText = 'a card'\n }\n\n return `gear 1/turn Discard ${whoText} to draw a card.`\n },\n\n upgrade: (diff, who) => {\n let whoText = 'this'\n if (who === 'one') {\n whoText = 'the card'\n }\n \n return `To upgrade, trash ${whoText} to play a card costing up to (${diff}) more.`\n },\n };\n \n const modifiers = myMatch.split('-')\n const keyword = modifiers.shift()\n const reminderText = keywords[keyword.toLowerCase()];\n if (!reminderText) {\n lively.error(keyword, 'unknown reminder text')\n return `unknown reminder text '${keyword}''`;\n }\n \n return this.italic(`(${reminderText(...modifiers)})`);\n });\n }\n \n static renderKeywords(printedRules) {\n const C_DARKGRAY = '#555';\n const C_LIGHTGRAY = '#999';\n \n function highlightKeyword(pattern, color=C_DARKGRAY, icon) {\n printedRules = printedRules.replace(pattern, (match, pElement, offset, string, groups) => {\n const text = match;\n return `${icon || ''}${text}`\n });\n }\n \n const C_DARKBEIGE = '#550';\n const C_BROWN = '#a50';\n \n const C_RED_LIGHT = '#d44';\n const C_RED = '#f00';\n const C_DARKRED = '#a11';\n \n const C_ORANGE = '#f50';\n \n const C_GREEN_LIGHT = '#292';\n const C_GREEN = '#090';\n const C_GREEN_DARK = '#170';\n \n const C_TEAL_LIGHT = '#085';\n const C_TEAL_DARK = '#164';\n \n const C_TEAL_BLUE = '#05a';\n \n const C_BLUE = '#00f';\n const C_BLUE_DARK = '#00c';\n \n const C_BLUE_VIOLET = '#219';\n const C_VIOLET_BLUE = '#30a';\n \n const C_VIOLET = '#708';\n\n \n highlightKeyword(/affinity\\b/gmi, C_DARKBEIGE);\n highlightKeyword(/\\bbound\\b(\\sto)?/gmi, C_VIOLET_BLUE);\n highlightKeyword(/brittle\\b/gmi, C_RED);\n highlightKeyword(/cycl(ed?|ing)\\b/gmi, C_DARKGRAY);\n highlightKeyword(/dash(ed|ing)?\\b/gmi, C_BROWN);\n highlightKeyword(/delirium:?\\b/gmi, C_DARKGRAY);\n highlightKeyword(/discover\\b/gmi, C_DARKGRAY, ' ');\n highlightKeyword(/magnetic\\b/gmi, C_RED_LIGHT, ' ');\n highlightKeyword(/manaburst\\b:?/gmi, C_VIOLET, ' ');\n highlightKeyword(/\\b(un)?meld(ed|s)?\\b/gmi, C_BLUE_VIOLET);\n highlightKeyword(/potion\\b/gmi, C_BLUE_VIOLET, ' ');\n highlightKeyword(/quickcast\\b/gmi, C_DARKGRAY);\n highlightKeyword(/resonance\\b/gmi, C_GREEN);\n highlightKeyword(/seek\\b/gmi, C_GREEN, ' ');\n //'#3FDAA5' some turquise\n highlightKeyword(/trad(ed?|ing)\\b/gmi, '#2E9F78', SVG.inlineSVG(tradeSVG.innerHTML, lively.rect(0, 0, 36, 36), 'x=\"10%\" y=\"10%\" width=\"80%\" height=\"80%\"', ''));\n highlightKeyword(/troph(y|ies)(\\spoints?)?\\b/gmi, C_ORANGE, ' ');\n highlightKeyword(/upgraded?\\b/gmi, C_ORANGE, SVG.inlineSVG(upgradeSVG.innerHTML, lively.rect(0, 0, 36, 36), 'x=\"10%\" y=\"10%\" width=\"80%\" height=\"80%\"', ''));\n \n return printedRules\n }\n \n static renderElementIcon(printedRules) {\n function inlineElement(element) {\n return SVG.inlineSVG(SVG.elementSymbol(element, lively.pt(5, 5), 5));\n }\n\n return printedRules.replace(/\\b(fire|water|earth|wind|gray)\\b/gmi, (match, pElement, offset, string, groups) => inlineElement(pElement));\n }\n \n static renderHedronIcon(printedRules) {\n function inlineHedron() {\n return SVG.inlineSVG(hedronSVG.innerHTML, lively.rect(0, 0, 23, 23), 'x=\"10%\" y=\"10%\" width=\"80%\" height=\"80%\"', '')\n }\n\n return printedRules.replace(/hedron/gmi, (match, pElement, offset, string, groups) => inlineHedron());\n }\n \n static renderTapIcon(printedRules) {\n function inlineTapIcon() {\n return SVG.inlineSVG(tapSVG.innerHTML, TAP_VIEWBOX, 'x=\"10%\" y=\"10%\" width=\"80%\" height=\"80%\"', '')\n }\n\n return printedRules.replace(/\\btap\\b/gmi, (match, pElement, offset, string, groups) => inlineTapIcon());\n }\n \n static __textOnIcon__(text, rect, center) {\n let textToPrint\n if (text.includes('hedron') || text.includes('x')) {\n const parts = []\n let isFirst = true;\n for (let part of text.split(/x|hedron/i)) {\n if (isFirst) {\n isFirst = false\n } else {\n parts.push(`hedron`)\n }\n if (part) { // part is not an empty string\n parts.push(part)\n }\n }\n // split available space\n const lengthPerPart = [];\n for (let part of parts) {\n const lengthOfPart = part === 'hedron' ? 1 : part.length\n lengthPerPart.push(lengthOfPart)\n }\n const totalLength = lengthPerPart.sum()\n const percentageSpacePerPart = lengthPerPart.map(len => len / totalLength)\n let iteratingLength = 0\n textToPrint = parts.map((part, i) => {\n let startingLength = iteratingLength\n const endingLength = iteratingLength = startingLength + percentageSpacePerPart[i]\n const middle = (startingLength + endingLength) / 2;\n if (part === 'hedron') {\n const scaleFactor = totalLength > 1 ? .7 : 1\n return `${part}`\n } else {\n return `${part}`\n }\n }).join('')\n } else {\n // simple form: just some text\n textToPrint = `${text}`;\n }\n return textToPrint\n }\n\n static renderVPIcon(printedRules) {\n const printVP = vp => {\n const rect = lively.rect(0, 0, 10, 10)\n const center = rect.center();\n \n let textToPrint = this.__textOnIcon__(vp, rect, center);\n \n Math.sqrt(.5) \n return `${SVG.inlineSVG(`\n \n \n\n${textToPrint}\n`)}`;\n }\n\n return printedRules.replace(/(\\-?\\+?(?:\\d+|\\*|d+\\*|\\d+(?:x|y|z|hedron)|(?:x|y|z|hedron)|\\b)\\-?\\+?)VP\\b/gmi, function replacer(match, vp, offset, string, groups) {\n return printVP(vp);\n });\n }\n \n static renderCardIcon(printedRules) {\n var that = this;\n function inlineCardCost(cost) {\n const rect = CARD_COST_ONE_VIEWBOX\n const center = rect.center();\n \n let textToPrint = that.__textOnIcon__(cost, rect, center);\n return SVG.inlineSVG(`${cardCostOneSVG.innerHTML}\n${textToPrint}`, CARD_COST_ONE_VIEWBOX, 'x=\"10%\" y=\"10%\" width=\"80%\" height=\"80%\"', '')\n }\n\n return printedRules.replace(/\\(\\(((?:[*0-9xyz+-]|hedron)*)\\)\\)/gmi, (match, pElement, offset, string, groups) => inlineCardCost(pElement));\n }\n \n static renderCoinIcon(printedRules) {\n const coin = text => {\n const rect = lively.rect(0, 0, 10, 10)\n const center = rect.center();\n \n let textToPrint = this.__textOnIcon__(text, rect, center);\n \n return SVG.inlineSVG(`${SVG.circle(center, 5, `fill=\"goldenrod\"`)}\n${SVG.circleRing(center, 4.75, 5, `fill=\"darkviolet\"`)}\n${textToPrint}`);\n }\n\n return printedRules.replace(/\\(((?:[*0-9xyz+-]|hedron)*)\\)/gmi, function replacer(match, p1, offset, string, groups) {\n return coin(p1);\n });\n }\n \n static renderBracketIcon(printedRules) {\n const bracket = text => {\n const rect = lively.rect(0, 0, 10, 10)\n const center = rect.center();\n \n let textToPrint = this.__textOnIcon__(text, rect, center);\n\n return SVG.inlineSVG(`\n\n\n${textToPrint}`, undefined, undefined, 'transform:scale(1);');\n }\n\n return printedRules.replace(/\\[((?:[*0-9xyz+-]|hedron)*)\\]/gmi, function replacer(match, p1, offset, string, groups) {\n return bracket(p1);\n });\n }\n \n static renderCastIcon(printedRules) {\n return printedRules.replace(/t?3x(fire|water|earth|wind|gray)\\:?/gi, (match, pElement, offset, string, groups) => {\n return `${castIcon} Cast:`;\n });\n }\n\n static async renderToDoc(rulesTextElement, printedRules, cardDesc) {\n const elements = ['fire', 'water', 'earth', 'wind'];\n elements.forEach(element => rulesTextElement.content.classList.remove(element))\n\n const cardElements = rulesTextElement.getElementsFromCard(cardDesc, false);\n cardElements.forEach(element => rulesTextElement.content.classList.add(element))\n\n rulesTextElement.content.innerHTML = printedRules\n }\n}\n\nconst OUTSIDE_BORDER_ROUNDING = lively.pt(3, 3)\n\nexport default class UbgRulesText extends Morph {\n \n applyRulesText(cardDesc) {\n this.cardDesc = cardDesc\n this.renderRuleText(cardDesc)\n }\n \n /*MD ## Filter MD*/\n get assetsFolder() {\n return this.src.replace(/(.*)\\/.*$/i, '$1/assets/');\n }\n\n /*MD ## Build MD*/\n async fetchAssetsInfo() {\n return (await this.assetsFolder.fetchStats()).contents;\n }\n\n /*MD ## Extract Card Info MD*/\n colorsForCard(card) {\n const BOX_FILL_OPACITY = 0.7;\n\n const currentVersion = card.versions.last;\n \n if (card.getType() === 'character') {\n return ['#efc241', '#b8942d', BOX_FILL_OPACITY];\n }\n\n const multiElement = Array.isArray(card.getElement());\n if (multiElement) {\n return ['#ff88ff', '#ff00ff', BOX_FILL_OPACITY];\n }\n\n const singleElementColors = {\n fire: ['#ffaaaa', '#dd0000', BOX_FILL_OPACITY],\n water: ['#aaaaff', '#0000ff', BOX_FILL_OPACITY],\n earth: ['#eeee88', '#cccc00', BOX_FILL_OPACITY],\n wind: ['#88ff88', '#00bb00', BOX_FILL_OPACITY]\n }[currentVersion.element && currentVersion.element.toLowerCase && currentVersion.element.toLowerCase()];\n if (singleElementColors) {\n return singleElementColors;\n }\n\n return ['#ffffff', '#888888', BOX_FILL_OPACITY];\n }\n\n getNameFromCard(cardDesc) {\n const currentVersion = cardDesc.versions.last;\n return currentVersion.name || ''\n }\n \n getElementsFromCard(cardDesc, grayIfEmpty) {\n const element = cardDesc.getElement();\n if (Array.isArray(element)) {\n return element\n } else if (element) {\n return [element]\n } else {\n return grayIfEmpty ? ['gray'] : []\n }\n }\n\n /*MD ## Debugging MD*/\n debugPoint(pt, color = 'red') {\n this.content.append();\n }\n \n debugRect(rect, color = 'red') {\n return this.roundedRect(rect, 'transparent', color, 1 / 3.7795275591, 0);\n }\n \n /*MD ## Rendering Helpers MD*/\n line(start, end, color, width) {\n const startX = start.x;\n const startY = start.y;\n const endX = end.x;\n const endY = end.y;\n\n const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));\n const angle = Math.atan2(endY - startY, endX - startX) * (180 / Math.PI);\n\n const line = document.createElement('div');\n line.style.width = length + 'mm';\n line.style.transform = `rotate(${angle}deg) translateY(${-width / 2}mm)`;\n line.style.position = 'absolute';\n line.style.top = startY + 'mm';\n line.style.left = startX + 'mm';\n line.style.height = width + 'mm';\n line.style.backgroundColor = color;\n line.style.transformOrigin = 'top left';\n\n this.content.append(line)\n }\n\n roundedRect(rect, fill, stroke, strokeWidth, borderRadius) {\n const element = ;\n\n this.content.append(element)\n \n return element;\n }\n \n colorWithOpacity(color, opacity) {\n return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`\n }\n\n /*MD ## Background Images MD*/\n filePathForBackgroundImage(cardDesc, assetsInfo) {\n const id = cardDesc.id;\n if (id) {\n const possibleFileNames = ['jpg', 'png'].map(ending => `${id}.${ending}`);\n const foundEntry = assetsInfo.find(entry => entry.type === 'file' && possibleFileNames.includes(entry.name));\n if (foundEntry) {\n return this.assetsFolder + foundEntry.name;\n }\n }\n\n const typeString = cardDesc.getType() && cardDesc.getType().toLowerCase && cardDesc.getType().toLowerCase()\n const defaultFiles = {\n gadget: 'default-gadget.jpg',\n character: 'default-character.jpg',\n spell: 'default-spell.jpg'\n };\n return this.assetsFolder + (defaultFiles[typeString] || 'default.jpg');\n }\n\n async loadImage(filePath) {\n return new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener('load', resolve);\n image.addEventListener('error', reject);\n image.src = filePath;\n });\n }\n\n /*MD ## Rendering MD*/\n async renderMagicStyle(cardDesc, outsideBorder, assetsInfo) {\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n // black border\n this.roundedRect(outsideBorder, 'black', 'transparent', '0', '0')\n\n // innerBorder\n const INNER_INSET = 3;\n const innerBorder = outsideBorder.insetBy(INNER_INSET);\n\n // card image\n const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo);\n const newBG = ;\n this.content.append(newBG)\n\n // title bar\n const TITLE_BAR_HEIGHT = 7;\n const titleBar = innerBorder.insetBy(1);\n titleBar.height = TITLE_BAR_HEIGHT;\n const TITLE_BAR_BORDER_WIDTH = 0.200025;\n this.roundedRect(titleBar, this.colorWithOpacity(BOX_FILL_COLOR, .5), BOX_STROKE_COLOR, TITLE_BAR_BORDER_WIDTH, 1)\n\n // card name\n {\n const pos = titleBar.leftCenter().addX(2);\n const fontSize = .6 * titleBar.height::mmToPoint();\n\n const cardName = this.getNameFromCard(cardDesc);\n this.content.append({cardName})\n }\n\n // cost\n const COIN_RADIUS = 4;\n const coinPos = titleBar.bottomLeft().addY(1).addXY(COIN_RADIUS, COIN_RADIUS);\n this.renderCost(cardDesc, coinPos, COIN_RADIUS)\n\n // type & elements\n const typePos = coinPos.addY(COIN_RADIUS * 1.5)\n this.renderType(cardDesc, typePos, BOX_FILL_COLOR, BOX_FILL_OPACITY)\n\n // rule box\n const ruleBox = outsideBorder.copy()\n const height = outsideBorder.height * .4;\n ruleBox.y = ruleBox.bottom() - height;\n ruleBox.height = height;\n // this.debugRect(ruleBox)\n const ruleBoxInset = 1 + INNER_INSET;\n const ruleTextInset = 2;\n await this.renderRuleText(cardDesc, outsideBorder, ruleBox, {\n insetBoxBy: ruleBoxInset,\n insetTextBy: ruleTextInset,\n innerStrokeColor: BOX_STROKE_COLOR,\n innerFillColor: BOX_FILL_COLOR,\n innerFillOpacity: BOX_FILL_OPACITY,\n outerStrokeColor: 'transparent',\n outerFillColor: 'transparent',\n outerFillOpacity: 0,\n });\n \n // tags\n const tagsAnchor = titleBar.bottomRight().addY(1);\n this.renderTags(cardDesc, tagsAnchor, outsideBorder)\n }\n\n async renderFullBleedStyle(cardDesc, outsideBorder, assetsInfo) {\n const type = cardDesc.getType();\n const typeString = type && type.toLowerCase && type.toLowerCase() || '';\n\n if (typeString === 'spell') {\n await this.renderSpell(cardDesc, outsideBorder, assetsInfo)\n } else if (typeString === 'gadget') {\n await this.renderGadget(cardDesc, outsideBorder, assetsInfo)\n } else if (typeString === 'character') {\n await this.renderCharacter(cardDesc, outsideBorder, assetsInfo)\n } else {\n await this.renderMagicStyle(cardDesc, outsideBorder, assetsInfo)\n }\n \n this.renderIsBad(cardDesc, outsideBorder)\n }\n \n maskedCircle(outsideBorder, center, radius, strokeWidth, fillColor, fillOpacity, strokeColor) {\n strokeWidth *= 2; // half covered by mask\n\n const svg = ;\n\n this.content.insertAdjacentHTML('beforeend', svg.outerHTML)\n \n }\n /*MD ### Rendering Card Types MD*/\n // #important\n async renderSpell(cardDesc, outsideBorder, assetsInfo) {\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n // spell circle\n {\n const CIRCLE_BORDER = -3;\n const radius = (outsideBorder.width - CIRCLE_BORDER) / 2;\n const center = outsideBorder.center().withY(outsideBorder.top() + CIRCLE_BORDER + radius)\n const strokeWidth = 1;\n this.maskedCircle(outsideBorder, center, radius, strokeWidth, BOX_FILL_COLOR, BOX_FILL_OPACITY, BOX_STROKE_COLOR)\n }\n \n // innerBorder\n const innerBorder = outsideBorder.insetBy(3);\n\n // title\n const TITLE_BAR_HEIGHT = 7;\n const COST_COIN_RADIUS = 4;\n const COST_COIN_MARGIN = 2;\n \n const titleBorder = innerBorder.insetBy(1);\n titleBorder.height = TITLE_BAR_HEIGHT;\n\n this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN)\n\n // rule box\n const ruleBox = outsideBorder.copy()\n const height = outsideBorder.height * .3;\n ruleBox.y = ruleBox.bottom() - height;\n ruleBox.height = height;\n // this.debugRect(ruleBox)\n \n // rule text\n const RULE_BOX_INSET = 1;\n const RULE_TEXT_INSET = 1;\n await this.renderRuleText(cardDesc, outsideBorder, ruleBox, {\n insetBoxBy: RULE_BOX_INSET,\n insetTextBy: RULE_TEXT_INSET,\n outerStrokeColor: 'transparent',\n outerFillColor: 'transparent',\n outerFillOpacity: 0,\n });\n \n // tags\n const tagsAnchor = lively.pt(titleBorder.right(), titleBorder.bottom()).addXY(-RULE_TEXT_INSET, 1);\n this.renderTags(cardDesc, tagsAnchor, outsideBorder)\n }\n\n // #important\n async renderGadget(cardDesc, outsideBorder, assetsInfo) {\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n // innerBorder\n const innerBorder = outsideBorder.insetBy(3);\n // this.roundedRect(innerBorder, 'steelblue', 'red', 3, 0)\n\n // top box\n const topBox = outsideBorder.copy()\n {\n topBox.height = 13;\n const box = this.roundedRect(topBox, this.colorWithOpacity(BOX_FILL_COLOR, BOX_FILL_OPACITY), 'transparent', 0, 0);\n box.style.backdropFilter = 'blur(4px)';\n \n this.line(topBox.bottomLeft(), topBox.bottomRight(), BOX_STROKE_COLOR, 1)\n }\n \n // title\n {\n const TITLE_BAR_HEIGHT = 7;\n const COST_COIN_RADIUS = 4;\n const COST_COIN_MARGIN = 2;\n \n const titleBorder = innerBorder.insetBy(1);\n titleBorder.height = TITLE_BAR_HEIGHT;\n \n this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN)\n }\n \n // rule box border calc\n const ruleBox = outsideBorder.copy()\n const height = outsideBorder.height * .4;\n ruleBox.y = ruleBox.bottom() - height;\n ruleBox.height = height;\n // this.debugRect(ruleBox)\n \n // rule text\n const RULE_BOX_INSET = 1;\n const RULE_TEXT_INSET = 1;\n await this.renderRuleText(cardDesc, outsideBorder, ruleBox, {\n insetBoxBy: RULE_BOX_INSET,\n insetTextBy: RULE_TEXT_INSET,\n outerStrokeColor: BOX_STROKE_COLOR,\n outerFillColor: BOX_FILL_COLOR,\n outerFillOpacity: BOX_FILL_OPACITY,\n });\n \n // tags\n const tagsAnchor = lively.pt(topBox.right(), topBox.bottom()).addXY(-RULE_TEXT_INSET, 1);\n this.renderTags(cardDesc, tagsAnchor, outsideBorder)\n }\n\n // #important\n async renderCharacter(cardDesc, outsideBorder, assetsInfo) {\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n // Zohar design\n {\n const ZOHAR_DESIGN_BORDER_WIDTH = .5;\n [[outsideBorder.topLeft(), lively.pt(1, 0)], [outsideBorder.topRight(), lively.pt(-1, 0)]].forEach(([startingPt, direction]) => {\n const dirX = direction.x;\n startingPt = startingPt.subY(5)\n const topMost = startingPt.addXY(dirX*8, 0);\n const triangleTop = topMost.addXY(0, 15 + 5);\n const triangleOuter = triangleTop.addXY(-dirX*15, 15);\n const triangleBottom = triangleOuter.addXY(dirX*15, 15);\n const bottom = triangleBottom.addXY(0, 100);\n const bottomOuter = bottom.addXY(-dirX*10, 0);\n const diamondPoints = `${startingPt.x} ${startingPt.y}, ${topMost.x} ${topMost.y}, ${triangleTop.x} ${triangleTop.y}, ${triangleOuter.x} ${triangleOuter.y}, ${triangleBottom.x} ${triangleBottom.y}, ${bottom.x} ${bottom.y}, ${bottomOuter.x} ${bottomOuter.y}`;\n\n const svg = ;\n\n this.content.insertAdjacentHTML('beforeend', svg.outerHTML)\n })\n }\n \n // innerBorder\n const innerBorder = outsideBorder.insetBy(3);\n // this.roundedRect(innerBorder, 'steelblue', 'red', 3, 0)\n\n // title\n const TITLE_BAR_HEIGHT = 7;\n const COST_COIN_RADIUS = 4;\n const COST_COIN_MARGIN = 2;\n \n const titleBorder = innerBorder.insetBy(1);\n titleBorder.height = TITLE_BAR_HEIGHT;\n \n this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN)\n \n // rule box border calc\n const ruleBox = outsideBorder.copy()\n const height = outsideBorder.height * .4;\n ruleBox.y = ruleBox.bottom() - height;\n ruleBox.height = height;\n \n // rule text\n const RULE_BOX_INSET = 1;\n const RULE_TEXT_INSET = 1;\n await this.renderRuleText(cardDesc, outsideBorder, ruleBox, {\n insetBoxBy: RULE_BOX_INSET,\n insetTextBy: RULE_TEXT_INSET,\n outerStrokeColor: BOX_STROKE_COLOR,\n outerFillColor: BOX_FILL_COLOR,\n outerFillOpacity: BOX_FILL_OPACITY,\n });\n \n // tags\n const tagsAnchor = lively.pt(titleBorder.right(), titleBorder.bottom()).addXY(-RULE_TEXT_INSET, 1);\n this.renderTags(cardDesc, tagsAnchor, outsideBorder)\n }\n \n /*MD ### Rendering Card Components MD*/\n renderTitleBarAndCost(cardDesc, border, costCoinRadius, costCoinMargin) {\n const TITLE_BAR_BORDER_WIDTH = 0.200025;\n\n const titleBar = border.copy()\n const coinLeftCenter = titleBar.leftCenter()\n const spacingForCoin = 2*costCoinRadius + costCoinMargin\n titleBar.x += spacingForCoin\n titleBar.width -= spacingForCoin\n\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n // title space\n // this.roundedRect(border, this.colorWithOpacity('#ffffff', .5), 'transparent', 0, 0)\n this.roundedRect(titleBar, this.colorWithOpacity('#ffffff', .5), 'transparent', 0, 1)\n \n // title bar\n this.roundedRect(titleBar, this.colorWithOpacity(BOX_FILL_COLOR, .5), BOX_STROKE_COLOR, TITLE_BAR_BORDER_WIDTH, 1)\n \n // card name\n {\n const pos = titleBar.leftCenter().addX(2);\n const fontSize = .6 * titleBar.height::mmToPoint();\n this.content.append({this.getNameFromCard(cardDesc)})\n }\n\n const coinCenter = coinLeftCenter.addX(costCoinRadius);\n this.renderInHandSymbols(cardDesc, border, costCoinRadius, costCoinMargin, coinCenter)\n }\n \n renderInHandSymbols(cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) {\n let currentCenter = coinCenter;\n\n // cost\n this.renderCost(cardDesc, currentCenter, costCoinRadius)\n\n if ((cardDesc.getType() || '').toLowerCase() !== 'character') {\n // vp\n currentCenter = currentCenter.addY(costCoinRadius * 2.75);\n this.renderBaseVP(cardDesc, currentCenter, costCoinRadius)\n\n // element (list)\n currentCenter = currentCenter.addY(costCoinRadius * 2.75);\n const elementListDirection = 1;\n currentCenter = this.renderElementList(cardDesc, currentCenter, costCoinRadius, elementListDirection)\n } else {\n currentCenter = currentCenter.addY(costCoinRadius * 1);\n }\n\n // type\n currentCenter = currentCenter.addY(costCoinRadius * .75)\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n this.renderType(cardDesc, currentCenter, BOX_FILL_COLOR, BOX_FILL_OPACITY)\n }\n\n renderElementList(cardDesc, pos, radius, direction) {\n const elements = this.getElementsFromCard(cardDesc, true);\n for (let element of elements) {\n this.renderElementSymbol(element, pos, radius)\n pos = pos.addY(direction * radius * .75);\n }\n return pos.addY(direction * radius * .25);\n }\n\n renderCost(cardDesc, pos, coinRadius) {\n const costSize = coinRadius / 3;\n\n const costDesc = cardDesc.getCost();\n const cost = Array.isArray(costDesc) ? costDesc.first : costDesc;\n\n const coinCenter = pos;\n const strokeWidth = .2 * costSize;\n const size = `${2 * coinRadius}mm`;\n const svg = ;\n this.content.insertAdjacentHTML('beforeend', svg.outerHTML)\n\n// {\n// const r = ;\n// const str = ;\n// this.content.append(str);\n// }\n\n this.renderIconText(coinCenter, costSize, cost, CSS_FONT_FAMILY_CARD_COST)\n }\n\n renderBaseVP(cardDesc, pos, coinRadius) {\n const costSize = coinRadius / 3;\n \n const vp = cardDesc.getBaseVP() || 0;\n const fillColor = vp === 0 ? VP_FILL_ZERO : VP_FILL\n const fillOpacity = .9\n const strokeColor = vp === 0 ? VP_STROKE_ZERO : VP_STROKE\n const strokeWidth = .2 * costSize;\n\n const iconCenter = pos;\n\n // diamond shape\n const diagonal = coinRadius * .9 * Math.sqrt(2)\n const down = pos.addY(diagonal)\n const left = pos.addX(-diagonal)\n const up = pos.addY(-diagonal)\n const right = pos.addX(diagonal)\n const diamondPoints = `${down.x} ${down.y}, ${left.x} ${right.y}, ${up.x} ${up.y}, ${right.x} ${right.y}`;\n\n const svg = ;\n this.content.insertAdjacentHTML('beforeend', svg.outerHTML)\n this.renderIconText(iconCenter, costSize, vp, CSS_FONT_FAMILY_CARD_VP)\n }\n\n renderIconText(centerPos, size, text, font) {\n if (text === undefined) {\n return\n }\n \n const iconText = {'' + text};\n \n this.content.append(iconText)\n }\n \n // #important\n async renderRuleText(cardDesc) {\n lively.notify('render rules text')\n return RuleTextRenderer.renderRuleText(this, cardDesc)\n }\n\n renderType(cardDesc, anchorPt, color, opacity) {\n // function curate() {\n // return this.toLower().upperFirst();\n // }\n // function prepend(other) {\n // return other + ' ' + this;\n // }\n // const element = cardDesc.getElement();\n let fullText = (cardDesc.getType() || '').toLower().upperFirst()\n // if (Array.isArray(element)) {\n // element.forEach(element => {\n // fullText = fullText::prepend(element::curate())\n // })\n // } else if (element) {\n // fullText = fullText::prepend(element::curate())\n // }\n\n this.content.append({fullText});\n }\n \n renderQRCode(cardDesc, qrAnchor, outsideBorder) {\n return;\n const canvas = ;\n const { QrCode } = qrcodegen\n var qr = QrCode.encodeText(\"\" + cardDesc.getId(), QrCode.Ecc.HIGH);\n\n function drawCanvas(qr, scale, border, lightColor, darkColor, canvas) {\n if (scale <= 0 || border < 0)\n throw new RangeError(\"Value out of range\");\n const width = (qr.size + border * 2) * scale;\n canvas.width = width;\n canvas.height = width;\n let ctx = canvas.getContext(\"2d\");\n for (let y = -border; y < qr.size + border; y++) {\n for (let x = -border; x < qr.size + border; x++) {\n ctx.fillStyle = qr.getModule(x, y) ? darkColor : lightColor;\n ctx.fillRect((x + border) * scale, (y + border) * scale, scale, scale);\n }\n }\n }\n\n const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc);\n\n drawCanvas(qr, 3, 1, \"white\", BOX_STROKE_COLOR, canvas);\n this.content.append(canvas)\n }\n \n renderTags(cardDesc, tagsAnchor, outsideBorder) {\n const tags = cardDesc.getTags().sortBy(i => i, true).map(tag => #{tag}
);\n const FONT_SIZE = 7;\n \n this.content.append({...tags})\n }\n\n renderElementSymbol(element, pos, radius) {\n const innerBounds = lively.rect(0, 0, 10, 10)\n const svgInnerPos = innerBounds.center();\n const svgInnerRadius = innerBounds.width / 2;\n const outerBounds = lively.rect(pos.x - radius, pos.y - radius, radius * 2, radius * 2);\n const yourSvgString = SVG.outerSVG(SVG.elementSymbol(element, svgInnerPos, svgInnerRadius), innerBounds, outerBounds);\n\n this.content.insertAdjacentHTML('beforeend', yourSvgString)\n }\n\n get content() {\n return this.get('#content');\n }\n \n renderIsBad(cardDesc, outsideBorder) {\n const slash = (color, width=2, offset=lively.pt(0,0)) => {\n const start = outsideBorder.topRight().addPt(offset);\n const end = outsideBorder.bottomLeft().addPt(offset);\n this.line(start, end, color, width)\n }\n \n if (cardDesc.hasTag('duplicate')) {\n slash('#bbbbbb', 2, lively.pt(-3, -3))\n }\n if (cardDesc.hasTag('unfinished')) {\n slash('#888888', 2, lively.pt(-2, -2))\n }\n if (cardDesc.hasTag('bad')) {\n slash('#ff0000', 2)\n }\n if (cardDesc.hasTag('deprecated')) {\n slash('#ff00ff', 2, lively.pt(2, 2))\n }\n if (cardDesc.getRating() === 'remove') {\n slash('#999999', 5, lively.pt(-5, -5))\n }\n }\n\n /*MD ## Basic Web Components MD*/\n initialize() {\n if (this.hasAttribute('for-preload')) {\n return;\n }\n\n this.windowTitle = \"UbgCard\";\n \n this._attrObserver = new MutationObserver(mutations => {\n mutations.forEach(mutation => {\n let rerenderRequired = false;\n if (mutation.type == \"attributes\") {\n // console.log(\"observation\", mutation.attributeName,mutation.target.getAttribute(mutation.attributeName));\n rerenderRequired = true;\n this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, mutation.target.getAttribute(mutation.attributeName));\n }\n if (rerenderRequired) {\n this.rerender()\n }\n });\n });\n this._attrObserver.observe(this, { attributes: true });\n }\n \n rerender() {\n lively.notify('RERENDER')\n }\n \n static get observedAttributes() {\n return [\"card\", \"src\", \"is-cardback\"];\n }\n \n attributeChangedCallback(name, oldValue, newValue) {\n lively.notify(`${oldValue} -> ${newValue}`, name)\n }\n \n /*MD ## External API MD*/\n setSrc(src) {\n return this.src = src;\n }\n\n setCard(card) {\n return this.card = card;\n }\n\n setCards(cards) {\n return this.cards = cards;\n }\n\n async render() {\n this._checkOptionsSet()\n const assetsInfo = await this.fetchAssetsInfo();\n const outsideBorder = lively.pt(0,0).extent(POKER_CARD_SIZE_MM);\n const cardToPrint = this.card;\n lively.error('FULL RENDER')\n await this.renderFullBleedStyle(cardToPrint, outsideBorder, assetsInfo)\n }\n\n _checkOptionsSet() {\n if (!this.src) {\n lively.warn('cannot render: \"src\" not set')\n }\n if (!this.card) {\n lively.warn('cannot render: \"card\" not set')\n }\n if (!this.cards) {\n lively.warn('cannot render: \"cards\" not set')\n }\n }\n\n /*MD ## Lively-specific API MD*/\n livelyPrepareSave() {\n // this.setAttribute(\"data-mydata\", this.get(\"#textField\").value)\n }\n \n livelyPreMigrate() {\n // is called on the old object before the migration\n }\n \n livelyMigrate(other) {\n lively.notify('migrate rules text')\n this.applyRulesText(other.cardDesc)\n }\n \n async livelyExample() {\n }\n \n}\n"}
-{"from":48503,"to":48503,"name":"color","color":"#9ecae1"}
-{"from":48505,"to":48505,"name":"color","color":"#9ecae1"}
-{"from":48511,"to":48511,"name":"color","color":"#9ecae1"}
\ No newline at end of file
diff --git a/src/components/widgets/ubg-utils.js b/src/components/widgets/ubg-utils.js
new file mode 100644
index 000000000..56151555a
--- /dev/null
+++ b/src/components/widgets/ubg-utils.js
@@ -0,0 +1,154 @@
+/* global globalThis */
+
+import paper from 'src/client/paperjs-wrapper.js'
+
+export const fire = ;
+export const water = ;
+export const earth = ;
+export const wind = ;
+export const gray = ;
+export const question = ;
+
+export class PathDataScaleCache {
+ static getPathData(element, size = lively.pt(10, 10)) {
+ if (!this.cache) {
+ this.cache = {}
+ }
+
+ const key = `${element}-${size.x}-${size.y}`;
+ if (!this.cache[key]) {
+ // lively.notify(`${element}-${size.x}-${size.y}`, 'cache miss')
+ this.cache[key] = this._scalePathData(element, size)
+ }
+
+ return this.cache[key]
+ }
+
+ static _scalePathData(element, size) {
+ const { glyph } = forElement(element);
+ const path = new paper.Path(glyph.getAttribute('d'));
+
+ path.scale(1, -1);
+
+ const margin = size.scaleBy(0.1);
+ const boundingRect = new paper.Path.Rectangle({
+ point: margin.toPair(),
+ size: size.subPt(margin.scaleBy(2)).toPair()
+ });
+ path.fitBounds(boundingRect.bounds);
+
+ return path.pathData;
+ }
+}
+
+export function tenTenPathData(element) {
+ return PathDataScaleCache.getPathData(element, lively.pt(10, 10));
+}
+
+export const elementInfo = {
+ fire: {
+ name: 'fire',
+ faIcon: 'book',
+ glyph: fire,
+ get pathData() { return tenTenPathData('fire') },
+ pathWidth: parseInt(fire.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(fire.getAttribute('vert-adv-y')),
+ fill: '#ffbbbb',
+ stroke: '#ff0000',
+ others: ['water', 'earth', 'wind']
+ },
+ water: {
+ name: 'water',
+ faIcon: 'droplet',
+ glyph: water,
+ get pathData() { return tenTenPathData('water') },
+ pathWidth: parseInt(water.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(water.getAttribute('vert-adv-y')),
+ fill: '#8888ff',
+ stroke: '#0000ff',
+ others: ['fire', 'earth', 'wind']
+ },
+ earth: {
+ name: 'earth',
+ faIcon: 'mountain',
+ glyph: earth,
+ get pathData() { return tenTenPathData('earth') },
+ pathWidth: parseInt(earth.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(earth.getAttribute('vert-adv-y')),
+ fill: 'rgb(255, 255, 183)',
+ stroke: '#ffd400',
+ others: ['fire', 'water', 'wind']
+ },
+ wind: {
+ name: 'wind',
+ faIcon: 'cloud',
+ glyph: wind,
+ get pathData() { return tenTenPathData('wind') },
+ pathWidth: parseInt(wind.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(wind.getAttribute('vert-adv-y')),
+ fill: '#bbffbb',
+ stroke: '#00ff00',
+ others: ['fire', 'water', 'earth']
+ },
+ gray: {
+ name: 'gray',
+ faIcon: 'circle',
+ glyph: gray,
+ get pathData() { return tenTenPathData('gray') },
+ pathWidth: parseInt(gray.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(gray.getAttribute('vert-adv-y')),
+ fill: '#dddddd',
+ stroke: '#5A5A5A',
+ others: ['gray', 'gray', 'gray']
+ },
+ unknown: {
+ name: 'unknown',
+ faIcon: 'question',
+ glyph: question,
+ get pathData() { return tenTenPathData('question') },
+ pathWidth: parseInt(question.getAttribute('horiz-adv-x')),
+ pathHeight: parseInt(question.getAttribute('vert-adv-y')),
+ fill: 'pink',
+ stroke: 'violet',
+ others: ['question', 'question', 'question']
+ }
+};
+
+export function forElement(element) {
+ const cleanElement = (element || '').toLowerCase();
+ return elementInfo[cleanElement] || elementInfo.unknown;
+}
+
+export class SVG {
+
+ static outerSVG(children, innerBounds, outerBounds, attrs = '', style = '') {
+ return ``;
+ }
+
+ static inlineSVG(children, bounds = lively.rect(0, 0, 10, 10), attrs = '', style = '') {
+ return ``;
+ }
+
+ /*MD ## Basic Shapes MD*/
+ static circleRing(center, innerRadius, outerRadius, attrs) {
+ return ``
+ }
+
+ static circle(center, radius, attrs) {
+ return ``
+ }
+
+ /*MD ## Icons MD*/
+ static elementGlyph(element, center, radius, attrs) {
+ const pathData = PathDataScaleCache.getPathData(element, lively.pt(2 * radius, 2 * radius));
+ return ``
+ }
+
+ static elementSymbol(element, center, radius) {
+ const { name: elementName, fill, stroke } = forElement(element);
+ const innerRadius = .9 * radius;
+ return `${SVG.circle(center, innerRadius, `fill="${fill}"`)}
+${SVG.elementGlyph(elementName, center, innerRadius, `fill="${stroke}"`)}
+ ${SVG.circleRing(center, innerRadius, radius, `fill="${stroke}"`)}`
+ }
+}